Merge pull request #4581 from element-hq/feature/fga/advanced_settings_theme_rework

change (preferences) : use PreferenceDropdown for appearance
This commit is contained in:
ganfra 2025-04-14 15:19:48 +02:00 committed by GitHub
commit 34b9348fa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 222 additions and 168 deletions

View file

@ -7,16 +7,13 @@
package io.element.android.features.preferences.impl.advanced
import io.element.android.compound.theme.Theme
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
sealed interface AdvancedSettingsEvents {
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
data object ChangeTheme : AdvancedSettingsEvents
data object CancelChangeTheme : AdvancedSettingsEvents
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
data class SetTheme(val theme: ThemeOption) : AdvancedSettingsEvents
data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents
data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents
}

View file

@ -9,11 +9,10 @@ package io.element.android.features.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.mapToTheme
import io.element.android.libraries.architecture.Presenter
@ -39,11 +38,9 @@ class AdvancedSettingsPresenter @Inject constructor(
val doesCompressMedia by remember {
sessionPreferencesStore.doesCompressMedia()
}.collectAsState(initial = true)
val theme by remember {
val theme = remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}.collectAsState(initial = Theme.System)
var showChangeThemeDialog by remember { mutableStateOf(false) }
val hideInviteAvatars by remember {
appPreferencesStore.getHideInviteAvatarsFlow()
}.collectAsState(false)
@ -52,6 +49,16 @@ class AdvancedSettingsPresenter @Inject constructor(
appPreferencesStore.getTimelineMediaPreviewValueFlow()
}.collectAsState(initial = MediaPreviewValue.On)
val themeOption by remember {
derivedStateOf {
when (theme.value) {
Theme.System -> ThemeOption.System
Theme.Dark -> ThemeOption.Dark
Theme.Light -> ThemeOption.Light
}
}
}
fun handleEvents(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
@ -63,11 +70,12 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
sessionPreferencesStore.setCompressMedia(event.compress)
}
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
appPreferencesStore.setTheme(event.theme.name)
showChangeThemeDialog = false
when (event.theme) {
ThemeOption.System -> appPreferencesStore.setTheme(Theme.System.name)
ThemeOption.Dark -> appPreferencesStore.setTheme(Theme.Dark.name)
ThemeOption.Light -> appPreferencesStore.setTheme(Theme.Light.name)
}
}
is AdvancedSettingsEvents.SetHideInviteAvatars -> localCoroutineScope.launch {
appPreferencesStore.setHideInviteAvatars(event.value)
@ -82,8 +90,7 @@ class AdvancedSettingsPresenter @Inject constructor(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
doesCompressMedia = doesCompressMedia,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
theme = themeOption,
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
eventSink = { handleEvents(it) }

View file

@ -7,16 +7,33 @@
package io.element.android.features.preferences.impl.advanced
import io.element.android.compound.theme.Theme
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.preferences.DropdownOption
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.ui.strings.CommonStrings
data class AdvancedSettingsState(
val isDeveloperModeEnabled: Boolean,
val isSharePresenceEnabled: Boolean,
val doesCompressMedia: Boolean,
val theme: Theme,
val showChangeThemeDialog: Boolean,
val theme: ThemeOption,
val hideInviteAvatars: Boolean,
val timelineMediaPreviewValue: MediaPreviewValue,
val eventSink: (AdvancedSettingsEvents) -> Unit
)
enum class ThemeOption : DropdownOption {
System {
@Composable
override fun getText(): String = stringResource(CommonStrings.common_system)
},
Dark {
@Composable
override fun getText(): String = stringResource(CommonStrings.common_dark)
},
Light {
@Composable
override fun getText(): String = stringResource(CommonStrings.common_light)
}
}

View file

@ -8,7 +8,6 @@
package io.element.android.features.preferences.impl.advanced
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.compound.theme.Theme
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
@ -16,7 +15,6 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
get() = sequenceOf(
aAdvancedSettingsState(),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(showChangeThemeDialog = true),
aAdvancedSettingsState(isSharePresenceEnabled = true),
aAdvancedSettingsState(doesCompressMedia = true),
aAdvancedSettingsState(hideInviteAvatars = true),
@ -28,16 +26,15 @@ fun aAdvancedSettingsState(
isDeveloperModeEnabled: Boolean = false,
isSharePresenceEnabled: Boolean = false,
doesCompressMedia: Boolean = false,
showChangeThemeDialog: Boolean = false,
hideInviteAvatars: Boolean = false,
theme: ThemeOption = ThemeOption.System,
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
eventSink: (AdvancedSettingsEvents) -> Unit = {},
) = AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
doesCompressMedia = doesCompressMedia,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
theme = theme,
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
eventSink = eventSink

View file

@ -12,14 +12,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.themes
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.dialogs.ListOption
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -34,8 +31,7 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable
fun AdvancedSettingsView(
@ -49,15 +45,12 @@ fun AdvancedSettingsView(
onBackClick = onBackClick,
title = stringResource(id = CommonStrings.common_advanced_settings)
) {
ListItem(
headlineContent = {
Text(text = stringResource(id = CommonStrings.common_appearance))
},
trailingContent = ListItemContent.Text(
state.theme.toHumanReadable()
),
onClick = {
state.eventSink(AdvancedSettingsEvents.ChangeTheme)
PreferenceDropdown(
title = stringResource(id = CommonStrings.common_appearance),
selectedOption = state.theme,
options = ThemeOption.entries.toPersistentList(),
onSelectOption = { logLevel ->
state.eventSink(AdvancedSettingsEvents.SetTheme(logLevel))
}
)
ListItem(
@ -108,21 +101,6 @@ fun AdvancedSettingsView(
)
ModerationAndSafety(state)
}
if (state.showChangeThemeDialog) {
SingleSelectionDialog(
options = getOptions(),
initialSelection = themes.indexOf(state.theme),
onSelectOption = {
state.eventSink(
AdvancedSettingsEvents.SetTheme(
themes[it]
)
)
},
onDismissRequest = { state.eventSink(AdvancedSettingsEvents.CancelChangeTheme) },
)
}
}
@Composable
@ -176,24 +154,6 @@ private fun ModerationAndSafety(
}
}
@Composable
private fun getOptions(): ImmutableList<ListOption> {
return themes.map {
ListOption(title = it.toHumanReadable())
}.toImmutableList()
}
@Composable
private fun Theme.toHumanReadable(): String {
return stringResource(
id = when (this) {
Theme.System -> CommonStrings.common_system
Theme.Dark -> CommonStrings.common_dark
Theme.Light -> CommonStrings.common_light
}
)
}
@PreviewWithLargeHeight
@Composable
internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =

View file

@ -7,22 +7,28 @@
package io.element.android.features.preferences.impl.developer.tracing
import androidx.compose.runtime.Composable
import io.element.android.libraries.designsystem.components.preferences.DropdownOption
enum class LogLevelItem : DropdownOption {
ERROR {
override val text: String = "Error"
@Composable
override fun getText(): String = "Error"
},
WARN {
override val text: String = "Warn"
@Composable
override fun getText(): String = "Warn"
},
INFO {
override val text: String = "Info"
@Composable
override fun getText(): String = "Info"
},
DEBUG {
override val text: String = "Debug"
@Composable
override fun getText(): String = "Debug"
},
TRACE {
override val text: String = "Trace"
@Composable
override fun getText(): String = "Trace"
}
}

View file

@ -11,11 +11,10 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.compound.theme.Theme
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -30,12 +29,12 @@ class AdvancedSettingsPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.showChangeThemeDialog).isFalse()
assertThat(initialState.isSharePresenceEnabled).isTrue()
assertThat(initialState.doesCompressMedia).isTrue()
assertThat(initialState.theme).isEqualTo(Theme.System)
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isFalse()
assertThat(isSharePresenceEnabled).isTrue()
assertThat(doesCompressMedia).isTrue()
assertThat(theme).isEqualTo(ThemeOption.System)
}
}
}
@ -45,12 +44,17 @@ class AdvancedSettingsPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(true))
assertThat(awaitItem().isDeveloperModeEnabled).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(false))
assertThat(awaitItem().isDeveloperModeEnabled).isFalse()
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isFalse()
eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(true))
}
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isTrue()
eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(false))
}
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isFalse()
}
}
}
@ -60,12 +64,17 @@ class AdvancedSettingsPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isSharePresenceEnabled).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSharePresenceEnabled(false))
assertThat(awaitItem().isSharePresenceEnabled).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
assertThat(awaitItem().isSharePresenceEnabled).isTrue()
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(false))
}
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isFalse()
eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
}
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
}
}
}
@ -75,12 +84,17 @@ class AdvancedSettingsPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.doesCompressMedia).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
assertThat(awaitItem().doesCompressMedia).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
assertThat(awaitItem().doesCompressMedia).isTrue()
with(awaitItem()) {
assertThat(doesCompressMedia).isTrue()
eventSink(AdvancedSettingsEvents.SetCompressMedia(false))
}
with(awaitItem()) {
assertThat(doesCompressMedia).isFalse()
eventSink(AdvancedSettingsEvents.SetCompressMedia(true))
}
with(awaitItem()) {
assertThat(doesCompressMedia).isTrue()
}
}
}
@ -90,20 +104,61 @@ class AdvancedSettingsPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme)
val withDialog = awaitItem()
assertThat(withDialog.showChangeThemeDialog).isTrue()
// Cancel
withDialog.eventSink(AdvancedSettingsEvents.CancelChangeTheme)
val withoutDialog = awaitItem()
assertThat(withoutDialog.showChangeThemeDialog).isFalse()
withDialog.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme)
assertThat(awaitItem().showChangeThemeDialog).isTrue()
withDialog.eventSink(AdvancedSettingsEvents.SetTheme(Theme.Light))
val withNewTheme = awaitItem()
assertThat(withNewTheme.showChangeThemeDialog).isFalse()
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.System)
eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Dark)
eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.Light))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Light)
eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.System))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.System)
}
}
}
@Test
fun `present - hide invite avatars`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
with(awaitItem()) {
assertThat(hideInviteAvatars).isFalse()
eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(true))
}
with(awaitItem()) {
assertThat(hideInviteAvatars).isTrue()
eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(false))
}
with(awaitItem()) {
assertThat(hideInviteAvatars).isFalse()
}
}
}
@Test
fun `present - timeline media preview value`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
with(awaitItem()) {
assertThat(timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On)
eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
}
with(awaitItem()) {
assertThat(timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off)
eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
}
with(awaitItem()) {
assertThat(timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private)
}
}
}

View file

@ -14,8 +14,8 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.compose.LocalAnalyticsService
@ -29,6 +29,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class AdvancedSettingsViewTest {
@ -49,29 +50,17 @@ class AdvancedSettingsViewTest {
}
}
@Test
fun `clicking on Appearance emits the expected event`() {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.common_appearance)
eventsRecorder.assertSingle(AdvancedSettingsEvents.ChangeTheme)
}
@Test
fun `clicking on other theme emits the expected event`() {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
eventSink = eventsRecorder,
showChangeThemeDialog = true
),
)
rule.clickOn(CommonStrings.common_appearance)
rule.clickOn(CommonStrings.common_dark)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(Theme.Dark))
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark))
}
@Test
@ -140,6 +129,34 @@ class AdvancedSettingsViewTest {
)
)
}
@Test
@Config(qualifiers = "h640dp")
fun `clicking on hide invite avatars emits the expected event`() {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
eventSink = eventsRecorder,
hideInviteAvatars = false
),
)
rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetHideInviteAvatars(true))
}
@Test
@Config(qualifiers = "h640dp")
fun `clicking on timeline media preview emits the expected event`() {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On
),
)
rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView(

View file

@ -99,9 +99,10 @@ fun <T : DropdownOption> PreferenceDropdown(
*/
interface DropdownOption {
/**
* The text to display for this option.
* Returns the text to be displayed for this option.
*/
val text: String
@Composable
fun getText(): String
}
@Composable
@ -123,7 +124,7 @@ private fun <T : DropdownOption> DropdownTrailingContent(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = selectedOption?.text.orEmpty(),
text = selectedOption?.getText().orEmpty(),
maxLines = 1,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
@ -139,7 +140,7 @@ private fun <T : DropdownOption> DropdownTrailingContent(
DropdownMenuItem(
text = {
Text(
text = option.text,
text = option.getText(),
style = ElementTheme.typography.fontBodyMdRegular
)
},
@ -158,13 +159,16 @@ private fun <T : DropdownOption> DropdownTrailingContent(
internal fun PreferenceDropdownPreview() = ElementThemedPreview {
val options = listOf(
object : DropdownOption {
override val text = "Option 1"
@Composable
override fun getText(): String = "Option 1"
},
object : DropdownOption {
override val text = "Option 2"
@Composable
override fun getText(): String = "Option 2"
},
object : DropdownOption {
override val text = "Option 3"
@Composable
override fun getText(): String = "Option 3"
},
).toImmutableList()

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:926ce46cacac7beaac403e1c4e8034398d91dc48ab5f651bc7f0639ac65fe33a
size 46759
oid sha256:733b71c4199d1a5908b5e76d8e44f5781cee39e97d57a049be99f9e66c3d30e1
size 46936

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84c2474bc4d10e1aa3184c41b12b1bcf51d7e2b7ec801944608613aa46f50eba
size 46636
oid sha256:ef41e0df758991e44e9abcf3d7d511c7cc932711db58e94ad1b4617fe45c60c3
size 46826

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:611d435a2b3e6e0f8c7721905f635c1c93aa217df3fe4f4b6bb38bf5700e3629
size 34586
oid sha256:de45164ae8112206c1cf1a67c284cdea1509f4fa070f963fddf6a87eb9183ee1
size 46813

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d9b0a865db8f87d6c1c95b8fb6d916f261d44cecec52fdebd56288f2df35f0f
size 46627
oid sha256:7c3c1d2b2fc9e5a16a1bc57551238c5190f25a873575eaf3731c36aaf19d7f00
size 46802

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0505bdd3cfccf156249ab27ed0f087bd4d01d11121cb1f5e0c8450c6b7b58059
size 46615
oid sha256:1759e7063862ec1de7ce2b894067478599afb7c87b183e904c8d0199c93ed546
size 46646

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a004afcec0cb40f82172c7abdc4404d1a0d879d0e23c63d0dcd1d7325cddff43
size 46471
oid sha256:420e10e0eb2c90d38e28262152dd2dee54cfb73f7b031b7a34acea17a8f784fa
size 46943

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb09af4f9c96afb60b917d9484e28bacb73c494f9dfc2f42abb482896027b7b7
size 46767

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4dfb5f83a129332a84f0184ba6a37ba9a95ded6fd9692a6e5710aaeb6b424c7c
size 48610
oid sha256:7faf10e196f9275fdafabb53cb354532b2c5aa73869d1fe48ac67b53bbb34531
size 48681

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:903d0aa3faa5b4feae990139c467a5a6662808ffb768c7a2a5cd314f9ea09ba2
size 48482
oid sha256:15b0cf42d99a96ef4b52349301e4e90a5478150396a6890a66df1d33c3b6b89d
size 48558

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4a2538700b5dc993568575e7017fc26c3cba7af7b7567fac069af5aa0c6f6b5f
size 36484
oid sha256:f7debb36ffa6a60d7d30f9d59e247e6091c455860bdaae67502a52a5cec5e9c9
size 48572

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9177ba1065f5cfe3137a16c024445332b2fe326969f5f6d65ccf6b1707deabe9
size 48502
oid sha256:106251ed2fb0a330fa98d4dabd8aebcb946fdbd098b18372c8a9c3b8df39086f
size 48554

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f07064f41aafea6a91e3314d7d3bb099918e348ef9f9154a057d90758b9fb71
size 48480
oid sha256:a9b8cb284cc78c03f4bd4f4364d898e656a07ebe228243fdded35c283a1205e0
size 48492

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:92c1c93890214c88ed0e4d1d9faee27cc671db94fe928633ccc82471c137b64a
size 48422
oid sha256:ddb21e08e8047aada11330634ad9c137cdb8a3a349f457c066243394fcc23924
size 48682

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f21d860514e97b48c3fab7fef4fbf6d710854d21ea2eecc3b44a5ba340c92eb
size 48608