Improve ButtonWithProgress component, replace login and change server buttons (#235)

* Improve `ButtonWithProgress` component.

* Replace `CircularProgresIndicator` with `ButtonWithProgress` in login and server selection screens.
This commit is contained in:
Jorge Martin Espinosa 2023-03-28 22:56:59 +02:00 committed by GitHub
parent 486e63dcd3
commit c75ebfb1e7
17 changed files with 96 additions and 104 deletions

1
changelog.d/235.misc Normal file
View file

@ -0,0 +1 @@
Improve ButtonWithProgress component, replace login and change server buttons

View file

@ -23,5 +23,5 @@ data class ChangeServerState(
val changeServerAction: Async<Unit>, val changeServerAction: Async<Unit>,
val eventSink: (ChangeServerEvents) -> Unit, val eventSink: (ChangeServerEvents) -> Unit,
) { ) {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction is Async.Uninitialized val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading)
} }

View file

@ -64,14 +64,12 @@ import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.LinkColor import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -94,9 +92,9 @@ fun ChangeServerView(
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val interactionEnabled by remember(state.changeServerAction) { val isLoading by remember(state.changeServerAction) {
derivedStateOf { derivedStateOf {
state.changeServerAction !is Async.Loading state.changeServerAction is Async.Loading
} }
} }
val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage
@ -114,7 +112,7 @@ fun ChangeServerView(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = {}, title = {},
navigationIcon = { BackButton(onClick = onBackPressed, enabled = interactionEnabled) } navigationIcon = { BackButton(onClick = onBackPressed) }
) )
} }
) { padding -> ) { padding ->
@ -179,7 +177,7 @@ fun ChangeServerView(
var homeserverFieldState by textFieldState(stateValue = state.homeserver) var homeserverFieldState by textFieldState(stateValue = state.homeserver)
TextField( TextField(
value = homeserverFieldState, value = homeserverFieldState,
readOnly = !interactionEnabled, readOnly = isLoading,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(TestTags.changeServerServer) .testTag(TestTags.changeServerServer)
@ -201,7 +199,7 @@ fun ChangeServerView(
{ {
IconButton(onClick = { IconButton(onClick = {
eventSink(ChangeServerEvents.SetServer("")) eventSink(ChangeServerEvents.SetServer(""))
}, enabled = interactionEnabled) { }, enabled = !isLoading) {
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear)) Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear))
} }
} }
@ -244,37 +242,23 @@ fun ChangeServerView(
}) })
} }
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
Button( ButtonWithProgress(
text = stringResource(id = R.string.screen_change_server_submit),
showProgress = isLoading,
onClick = ::submit, onClick = ::submit,
enabled = interactionEnabled && state.submitEnabled, enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(TestTags.changeServerContinue) .testTag(TestTags.changeServerContinue)
) { )
Text(text = stringResource(id = R.string.screen_change_server_submit), style = ElementTextStyles.Button)
}
if (state.changeServerAction is Async.Success) { if (state.changeServerAction is Async.Success) {
onChangeServerSuccess() onChangeServerSuccess()
} }
} }
if (state.changeServerAction is Async.Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
} }
} }
} }
@Composable
internal fun ChangeServerErrorDialog(title: String, message: String, onDismiss: () -> Unit) {
ErrorDialog(
title = title,
content = message,
onDismiss = onDismiss
)
}
@Composable @Composable
internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) { internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) {
ConfirmationDialog( ConfirmationDialog(

View file

@ -27,8 +27,8 @@ data class LoginRootState(
val formState: LoginFormState, val formState: LoginFormState,
val eventSink: (LoginRootEvents) -> Unit val eventSink: (LoginRootEvents) -> Unit
) { ) {
val submitEnabled = val submitEnabled: Boolean get() =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState !is LoggedInState.ErrorLoggingIn
} }
sealed interface LoggedInState { sealed interface LoggedInState {

View file

@ -65,13 +65,12 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.loginError import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -93,16 +92,16 @@ fun LoginRootView(
onLoginWithSuccess: (SessionId) -> Unit = {}, onLoginWithSuccess: (SessionId) -> Unit = {},
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
) { ) {
val interactionEnabled by remember(state.loggedInState) { val isLoading by remember(state.loggedInState) {
derivedStateOf { derivedStateOf {
state.loggedInState != LoggedInState.LoggingIn state.loggedInState == LoggedInState.LoggingIn
} }
} }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = {}, title = {},
navigationIcon = { BackButton(onClick = onBackPressed, enabled = interactionEnabled) }, navigationIcon = { BackButton(onClick = onBackPressed) },
) )
} }
) { padding -> ) { padding ->
@ -131,14 +130,14 @@ fun LoginRootView(
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
ChangeServerSection( ChangeServerSection(
interactionEnabled = interactionEnabled, interactionEnabled = !isLoading,
homeserver = state.homeserverDetails.url, homeserver = state.homeserverDetails.url,
onChangeServer = onChangeServer onChangeServer = onChangeServer
) )
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
LoginForm(state = state, interactionEnabled = interactionEnabled) LoginForm(state = state, isLoading = isLoading)
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -147,12 +146,6 @@ fun LoginRootView(
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
else -> Unit else -> Unit
} }
if (state.loggedInState is LoggedInState.LoggingIn) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
} }
} }
@ -216,7 +209,7 @@ internal fun ChangeServerSection(
@Composable @Composable
internal fun LoginForm( internal fun LoginForm(
state: LoginRootState, state: LoginRootState,
interactionEnabled: Boolean, isLoading: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var loginFieldState by textFieldState(stateValue = state.formState.login) var loginFieldState by textFieldState(stateValue = state.formState.login)
@ -242,7 +235,7 @@ internal fun LoginForm(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
TextField( TextField(
value = loginFieldState, value = loginFieldState,
readOnly = !interactionEnabled, readOnly = isLoading,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager) .onTabOrEnterKeyFocusNext(focusManager)
@ -282,7 +275,7 @@ internal fun LoginForm(
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
TextField( TextField(
value = passwordFieldState, value = passwordFieldState,
readOnly = !interactionEnabled, readOnly = isLoading,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager) .onTabOrEnterKeyFocusNext(focusManager)
@ -318,15 +311,15 @@ internal fun LoginForm(
Spacer(Modifier.height(28.dp)) Spacer(Modifier.height(28.dp))
// Submit // Submit
Button( ButtonWithProgress(
text = stringResource(R.string.screen_login_submit),
showProgress = isLoading,
onClick = ::submit, onClick = ::submit,
enabled = interactionEnabled && state.submitEnabled, enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(TestTags.loginContinue) .testTag(TestTags.loginContinue)
) { )
Text(text = stringResource(R.string.screen_login_submit), style = ElementTextStyles.Button)
}
} }
} }

View file

@ -22,8 +22,6 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
@ -34,6 +32,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ChangeServerPresenterTest { class ChangeServerPresenterTest {
@Test @Test
fun `present - should start with default homeserver`() = runTest { fun `present - should start with default homeserver`() = runTest {
@ -92,7 +91,7 @@ class ChangeServerPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ChangeServerEvents.Submit) initialState.eventSink.invoke(ChangeServerEvents.Submit)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isFalse() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
@ -114,7 +113,7 @@ class ChangeServerPresenterTest {
awaitItem() awaitItem()
initialState.eventSink.invoke(ChangeServerEvents.Submit) initialState.eventSink.invoke(ChangeServerEvents.Submit)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isFalse() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
awaitItem() // Skip changing the url to the parsed domain awaitItem() // Skip changing the url to the parsed domain
val successState = awaitItem() val successState = awaitItem()

View file

@ -56,7 +56,6 @@ import io.element.android.libraries.designsystem.components.button.ButtonWithPro
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
@ -257,21 +256,12 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
.padding(horizontal = 20.dp), .padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (isVerifying) { ButtonWithProgress(
ButtonWithProgress( text = positiveButtonTitle?.let { stringResource(it) },
modifier = Modifier.fillMaxWidth(), showProgress = isVerifying,
onClick = { positiveButtonEvent?.let { eventSink(it) } } modifier = Modifier.fillMaxWidth(),
) { onClick = { positiveButtonEvent?.let { eventSink(it) } }
positiveButtonTitle?.let { Text(stringResource(it), style = ElementTextStyles.Button) } )
}
} else {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { positiveButtonEvent?.let { eventSink(it) } }
) {
positiveButtonTitle?.let { Text(stringResource(it), style = ElementTextStyles.Button) }
}
}
if (negativeButtonTitle != null) { if (negativeButtonTitle != null) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
TextButton( TextButton(

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.button
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -33,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
@ -40,10 +40,20 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.ElementButtonDefaults import io.element.android.libraries.designsystem.theme.components.ElementButtonDefaults
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
/**
* A component that will display a button with an indeterminate circular progressbar.
* When [showProgress] is true:
* - A circular progressbar is displayed.
* - [text] is replaced by [progressText], if defined.
* - [onClick] gets disabled.
*/
@Composable @Composable
fun ButtonWithProgress( fun ButtonWithProgress(
text: String?,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showProgress: Boolean = false,
progressText: String? = text,
enabled: Boolean = true, enabled: Boolean = true,
shape: Shape = ElementButtonDefaults.shape, shape: Shape = ElementButtonDefaults.shape,
colors: ButtonColors = ElementButtonDefaults.buttonColors(), colors: ButtonColors = ElementButtonDefaults.buttonColors(),
@ -51,10 +61,11 @@ fun ButtonWithProgress(
border: BorderStroke? = null, border: BorderStroke? = null,
contentPadding: PaddingValues = ElementButtonDefaults.ContentPadding, contentPadding: PaddingValues = ElementButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) { ) {
Button( Button(
onClick = onClick, onClick = {
if (!showProgress) { onClick() }
},
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
shape = shape, shape = shape,
@ -64,15 +75,21 @@ fun ButtonWithProgress(
contentPadding = contentPadding, contentPadding = contentPadding,
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
CircularProgressIndicator( if (showProgress) {
modifier = Modifier CircularProgressIndicator(
.progressSemantics() modifier = Modifier
.size(18.dp), .progressSemantics()
color = MaterialTheme.colorScheme.onPrimary, .size(18.dp),
strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary,
) strokeWidth = 2.dp,
Spacer(Modifier.width(10.dp)) )
content() if (progressText != null) {
Spacer(Modifier.width(10.dp))
Text(progressText, style = ElementTextStyles.Button)
}
} else if (text != null) {
Text(text, style = ElementTextStyles.Button)
}
} }
} }
@ -86,7 +103,9 @@ internal fun ButtonWithProgressDarkPreview() = ElementPreviewDark { ContentToPre
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
ButtonWithProgress(onClick = {}) { ButtonWithProgress(
Text(text = "Button with progress") text = "Button with progress",
} onClick = {},
showProgress = true,
)
} }

View file

@ -122,10 +122,16 @@
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string> <string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
<string name="report_content_explanation">"Reporting this message will send its unique event ID to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."</string> <string name="report_content_explanation">"Reporting this message will send its unique event ID to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."</string>
<string name="report_content_hint">"Reason for reporting this content"</string> <string name="report_content_hint">"Reason for reporting this content"</string>
<string name="room_details_encryption_enabled_description">"Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile."</string>
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string> <string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string> <string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string> <string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_leave_room_title">"Leave room"</string>
<string name="screen_room_details_people_title">"People"</string>
<string name="screen_room_details_security_title">"Security"</string>
<string name="screen_room_details_topic_title">"Topic"</string>
<string name="settings_rageshake">"Rageshake"</string> <string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string> <string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
<string name="settings_title_general">"General"</string> <string name="settings_title_general">"General"</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cb351458d7b5d70f2201f94e4466d5c7dbebb60bca164a986967d2d8006405da oid sha256:5450116320f6d75befbc784a31a8ad51c868936794e75e65bf24e6e1b69f1e23
size 43814 size 44421

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f51052d3ece86fe7e07cc686a539b81d23f795c64bf66bd093b9d0e8e9fc7192 oid sha256:af77d0276f38c78524201c49484548e29d1c99168e68e11874c49f0db0803df2
size 40335 size 41392

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:00fbd0fb3779ed0d1a0c9bf32b6628e4c03f8cf686111d6ca438b5f513152f6f oid sha256:762a1d43dc3bc9edfa47f081b8385614f7075b5cecdd3eb3cb6b4959baf6811c
size 32704 size 33445

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2a2ef5bf60554a079c2a71d5f3e30e42b8e19203ca3784c77fe3aff49d4b9650 oid sha256:7559f64cb74c33ef441dd15d557607061b72ffcdd3dce7ec7f8a663d928fd681
size 30254 size 29585

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4afa56c98494f6d6366c8e7e0032481c813a2798fbada236aa2f5f431be04de0 oid sha256:2fafe08ac635bf3f0e62753c832ead92de297f0826ec735efe1a1f90481cb421
size 31485 size 32417

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6a257ed45da52a2ef3f90376353ed2fa08b02c6e5919ebfae36f7b37a4166ea7 oid sha256:7954d3f7064666b287ce6ab05fb6814f0e4570db2db6a44437291f0f90d8e87f
size 30073 size 29062

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:01f7d29ebcba189d7b662c4427faeed01edcc60818e73437274520c2993e225b oid sha256:6aa8d1bbb81616f3561e23b5fae5a37d7d826f67c562ff7fb978681296fae8ec
size 9827 size 10275

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6371d2ea1465a5410e9b54f7bba662e3b8b334aacddcdf7b78d304f0ed580ddb oid sha256:c456be740c9f8480c4eb06f5f67eef868b6d27e206a7515b4d3356d1d0e2cde4
size 10315 size 10672