Use user friendly error messages in login following iOS logic (#174)
* Use user friendly error messages in login following iOS logic, fix dialog colors. * Use `AlertDialogDefaults` for the default properties of dialogs * Improve Maestro tests with wrong password dialog * Add tests for error messages
This commit is contained in:
parent
f1afd67397
commit
d36ca2907c
46 changed files with 300 additions and 113 deletions
|
|
@ -9,9 +9,15 @@ appId: ${APP_ID}
|
|||
id: "login-email_username"
|
||||
- inputText: ${USERNAME}
|
||||
- pressKey: Enter
|
||||
- tapOn: "Password"
|
||||
- tapOn:
|
||||
id: "login-password"
|
||||
- inputText: "wrong-password"
|
||||
- pressKey: Enter
|
||||
- tapOn: "Continue"
|
||||
- tapOn: "OK"
|
||||
- tapOn:
|
||||
id: "login-password"
|
||||
- eraseText: 20
|
||||
- inputText: ${PASSWORD}
|
||||
- pressKey: Enter
|
||||
- tapOn: "Continue"
|
||||
|
|
|
|||
1
changelog.d/132.bugfix
Normal file
1
changelog.d/132.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Use user friendly error messages in login following iOS logic, fix dialog colors.
|
||||
|
|
@ -25,6 +25,12 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.features.login"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.login.R
|
||||
import io.element.android.features.login.error.changeServerError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
|
|
@ -239,7 +240,7 @@ fun ChangeServerView(
|
|||
@Composable
|
||||
internal fun ChangeServerErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||
ErrorDialog(
|
||||
content = error.localizedMessage ?: stringResource(id = StringR.string.unknown_error),
|
||||
content = stringResource(changeServerError(error)),
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,34 +16,28 @@
|
|||
|
||||
package io.element.android.features.login.error
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.login.root.LoginFormState
|
||||
import io.element.android.libraries.core.uri.isValidUrl
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
import io.element.android.libraries.matrix.api.auth.AuthErrorCode
|
||||
import io.element.android.libraries.matrix.api.auth.errorCode
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationException
|
||||
import io.element.android.libraries.ui.strings.R.string as StringR
|
||||
|
||||
@Composable
|
||||
fun loginError(
|
||||
data: LoginFormState,
|
||||
throwable: Throwable?
|
||||
): String {
|
||||
return when {
|
||||
data.login.isEmpty() -> "Please enter a login"
|
||||
data.password.isEmpty() -> "Please enter a password"
|
||||
throwable != null -> stringResource(id = StringR.string.auth_invalid_login_param)
|
||||
else -> "No error provided"
|
||||
throwable: Throwable
|
||||
): Int {
|
||||
val authException = throwable as? AuthenticationException ?: return StringR.unknown_error
|
||||
return when (authException.errorCode) {
|
||||
AuthErrorCode.FORBIDDEN -> StringR.auth_invalid_login_param
|
||||
AuthErrorCode.USER_DEACTIVATED -> StringR.auth_invalid_login_deactivated_account
|
||||
AuthErrorCode.UNKNOWN -> StringR.unknown_error
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun changeServerError(
|
||||
data: String,
|
||||
throwable: Throwable?
|
||||
): String {
|
||||
return when {
|
||||
data.isEmpty() -> "Please enter a server URL"
|
||||
!data.isValidUrl() -> stringResource(id = StringR.string.login_error_invalid_home_server)
|
||||
throwable != null -> "That server doesn’t seem right. Please check the address."
|
||||
else -> "No error provided"
|
||||
throwable: Throwable
|
||||
): Int {
|
||||
val authException = throwable as? AuthenticationException ?: return StringR.unknown_error
|
||||
return when (authException) {
|
||||
is AuthenticationException.InvalidServerName -> StringR.login_error_homeserver_not_found
|
||||
else -> StringR.unknown_error
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.login.error.loginError
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
|
|
@ -153,6 +154,12 @@ fun LoginRootScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
|
||||
LoginErrorDialog(error = state.loggedInState.failure, onDismiss = {
|
||||
state.eventSink(LoginRootEvents.ClearError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -299,12 +306,6 @@ internal fun LoginForm(
|
|||
singleLine = true,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
|
||||
LoginErrorDialog(error = state.loggedInState.failure, onDismiss = {
|
||||
eventSink(LoginRootEvents.ClearError)
|
||||
})
|
||||
}
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
// Submit
|
||||
|
|
@ -323,7 +324,7 @@ internal fun LoginForm(
|
|||
@Composable
|
||||
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||
ErrorDialog(
|
||||
content = error.localizedMessage ?: stringResource(id = StringR.string.unknown_error),
|
||||
content = stringResource(loginError(error)),
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.error
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationException
|
||||
|
||||
class ErrorFormatterTests {
|
||||
|
||||
// region loginError
|
||||
@Test
|
||||
fun `loginError - invalid unknown error returns unknown error message`() {
|
||||
val error = Throwable("Some unknown error")
|
||||
assertThat(loginError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginError - invalid auth error returns unknown error message`() {
|
||||
val error = AuthenticationException.SlidingSyncNotAvailable("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||
assertThat(loginError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginError - unknown error returns unknown error message`() {
|
||||
val error = AuthenticationException.Generic("M_UNKNOWN")
|
||||
assertThat(loginError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginError - forbidden error returns incorrect credentials message`() {
|
||||
val error = AuthenticationException.Generic("M_FORBIDDEN")
|
||||
assertThat(loginError(error)).isEqualTo(R.string.auth_invalid_login_param)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginError - user_deactivated error returns deactivated account message`() {
|
||||
val error = AuthenticationException.Generic("M_USER_DEACTIVATED")
|
||||
assertThat(loginError(error)).isEqualTo(R.string.auth_invalid_login_deactivated_account)
|
||||
}
|
||||
|
||||
// endregion loginError
|
||||
|
||||
// region changeServerError
|
||||
|
||||
@Test
|
||||
fun `changeServerError - invalid unknown error returns unknown error message`() {
|
||||
val error = Throwable("Some unknown error")
|
||||
assertThat(changeServerError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changeServerError - invalid auth error returns unknown error message`() {
|
||||
val error = AuthenticationException.SlidingSyncNotAvailable("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||
assertThat(changeServerError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changeServerError - unknown error returns unknown error message`() {
|
||||
val error = AuthenticationException.Generic("M_UNKNOWN")
|
||||
assertThat(changeServerError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changeServerError - forbidden error returns unknown error message`() {
|
||||
val error = AuthenticationException.Generic("M_FORBIDDEN")
|
||||
assertThat(changeServerError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changeServerError - user_deactivated error returns unknown error message`() {
|
||||
val error = AuthenticationException.Generic("M_USER_DEACTIVATED")
|
||||
assertThat(changeServerError(error)).isEqualTo(R.string.unknown_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changeServerError - invalid server name error returns invalid server name error message`() {
|
||||
val error = AuthenticationException.InvalidServerName("Server is not valid")
|
||||
assertThat(changeServerError(error)).isEqualTo(R.string.login_error_homeserver_not_found)
|
||||
}
|
||||
|
||||
// endregion changeServerError
|
||||
}
|
||||
|
|
@ -48,10 +48,10 @@ fun ConfirmationDialog(
|
|||
onThirdButtonClicked: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
shape: Shape = AlertDialogDefaults.shape,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
iconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
titleContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
containerColor: Color = AlertDialogDefaults.containerColor,
|
||||
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
|
||||
textContentColor: Color = AlertDialogDefaults.textContentColor,
|
||||
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
|
||||
) {
|
||||
AlertDialog(
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.components.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -31,10 +27,8 @@ import androidx.compose.ui.graphics.Shape
|
|||
import androidx.compose.ui.res.stringResource
|
||||
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.preview.ElementPreviewDark
|
||||
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.Text
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
|
|
@ -46,10 +40,10 @@ fun ErrorDialog(
|
|||
submitText: String = stringResource(id = StringR.string.ok),
|
||||
onDismiss: () -> Unit = {},
|
||||
shape: Shape = AlertDialogDefaults.shape,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
iconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
titleContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
containerColor: Color = AlertDialogDefaults.containerColor,
|
||||
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
|
||||
textContentColor: Color = AlertDialogDefaults.textContentColor,
|
||||
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
|
||||
) {
|
||||
AlertDialog(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ val materialColorSchemeLight = lightColorScheme(
|
|||
surface = Color.White,
|
||||
onSurface = Color.Black,
|
||||
surfaceVariant = Gray_25,
|
||||
onSurfaceVariant = Gray_150,
|
||||
onSurfaceVariant = Gray_200,
|
||||
// TODO surfaceTint = primary,
|
||||
// TODO inverseSurface = ColorLightTokens.InverseSurface,
|
||||
// TODO inverseOnSurface = ColorLightTokens.InverseOnSurface,
|
||||
|
|
|
|||
|
|
@ -41,4 +41,7 @@ dependencies {
|
|||
implementation(libs.serialization.json)
|
||||
api(projects.libraries.sessionStorage.api)
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationException
|
||||
|
||||
enum class AuthErrorCode(val value: String) {
|
||||
UNKNOWN("M_UNKNOWN"),
|
||||
USER_DEACTIVATED("M_USER_DEACTIVATED"),
|
||||
FORBIDDEN("M_FORBIDDEN")
|
||||
}
|
||||
|
||||
// This is taken from the iOS version. It seems like currently there's no better way to extract error codes
|
||||
val AuthenticationException.errorCode: AuthErrorCode
|
||||
get() {
|
||||
val message = (this as? AuthenticationException.Generic)?.message ?: return AuthErrorCode.UNKNOWN
|
||||
return enumValues<AuthErrorCode>()
|
||||
.firstOrNull { message.contains(it.value) }
|
||||
?: AuthErrorCode.UNKNOWN
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationException
|
||||
|
||||
class AuthErrorCodeTests {
|
||||
|
||||
@Test
|
||||
fun `errorCode finds UNKNOWN code`() {
|
||||
val error = AuthenticationException.Generic("M_UNKNOWN")
|
||||
assertThat(error.errorCode).isEqualTo(AuthErrorCode.UNKNOWN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `errorCode finds USER_DEACTIVATED code`() {
|
||||
val error = AuthenticationException.Generic("M_USER_DEACTIVATED")
|
||||
assertThat(error.errorCode).isEqualTo(AuthErrorCode.USER_DEACTIVATED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `errorCode finds FORBIDDEN code`() {
|
||||
val error = AuthenticationException.Generic("M_FORBIDDEN")
|
||||
assertThat(error.errorCode).isEqualTo(AuthErrorCode.FORBIDDEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `errorCode cannot find code so it returns UNKNOWN`() {
|
||||
val error = AuthenticationException.Generic("Some other error")
|
||||
assertThat(error.errorCode).isEqualTo(AuthErrorCode.UNKNOWN)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc9385ba5a8f78b91146404e40328b378038962171da2c00483b91cffbaea74c
|
||||
size 36587
|
||||
oid sha256:e157e6b9c4d3e6c888f4360b45ed3ae13306407bcb4b55ef6a168341ad56b250
|
||||
size 36585
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6ecfecebd483538023eca4e3ccaae2c43758c68e93dc75db861eec480386ab1
|
||||
size 40491
|
||||
oid sha256:a30b81972f9927741ec5298d32cd28d13bb2ad5b4f044f42e42275722cb2dc40
|
||||
size 40490
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8355435407ed01e3807c6ac397891262f6d3286956f8d8c181dc329ca899674e
|
||||
size 39292
|
||||
oid sha256:307c1a0c2741b43ebfc2c135367865e4cb85f8ddf31f6c6fb112c18e617fb47d
|
||||
size 39314
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f4005736ea7e7b5986e7dbbecb6c221b9a518ad45ed9d11abc6b0d5fd22721f
|
||||
size 40529
|
||||
oid sha256:194e3055a30dcb721a0c62e81cf9a326993e587d0d3805f71ca3eaed901f8fa4
|
||||
size 40524
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6ecfecebd483538023eca4e3ccaae2c43758c68e93dc75db861eec480386ab1
|
||||
size 40491
|
||||
oid sha256:a30b81972f9927741ec5298d32cd28d13bb2ad5b4f044f42e42275722cb2dc40
|
||||
size 40490
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3460e8f37e746e0ae7a30c1a055a3b68cc013197c4f2567d19ad51c00f8d6385
|
||||
size 34814
|
||||
oid sha256:b21168d83ed8d7c116e72dd76292975530f1c898ef32502aa382f292aef4f8aa
|
||||
size 30951
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:556d19b2b10461d0bd7e78aaa12d13e1d6c65dd61fba1350eaf6bcb570eaadfe
|
||||
size 31118
|
||||
oid sha256:5e3e148e980f5096934ec4742a3f8b05442f0019dbcf6435fcdc1875c326a6ae
|
||||
size 31513
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d47db02362874b68abf7198b59fc18b6c6190308abe2836129d7cd60eb4cfe29
|
||||
size 33806
|
||||
oid sha256:21e9c4c0ecf5a348361ff30195195743192cb784c9d6d248ca83010e3646c39f
|
||||
size 34212
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9abadaf4be40c1079a6348a0cb2b6e29d6ec4deac05c924a5a5f0bfec0203f9
|
||||
size 33003
|
||||
oid sha256:f81c7b793611e5f3f975245bf5ba52d59bf5725519292f05dd7840c2e38d3166
|
||||
size 33237
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97aac787891269fe3a5bdb46d299cf11ff7d2225a3bc8535d0e02005ccb1b400
|
||||
size 32076
|
||||
oid sha256:04c34dbc9fe2df9af3133e1b7c444d58e7d8a35161a97155c384dceac28ee698
|
||||
size 32324
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9abadaf4be40c1079a6348a0cb2b6e29d6ec4deac05c924a5a5f0bfec0203f9
|
||||
size 33003
|
||||
oid sha256:922ee759a8e4b689ced8ade809c651464956395ffc0a2b3d9025f067a9807aa9
|
||||
size 30066
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9abadaf4be40c1079a6348a0cb2b6e29d6ec4deac05c924a5a5f0bfec0203f9
|
||||
size 33003
|
||||
oid sha256:f81c7b793611e5f3f975245bf5ba52d59bf5725519292f05dd7840c2e38d3166
|
||||
size 33237
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e18184ac335b06a2818227810eca4fac94460b0006198e96d0cfa00bdcf26608
|
||||
size 43067
|
||||
oid sha256:9190e58380b54eef63a423531e5427f02dfea6d1ca00b1ee83470c2576f345b0
|
||||
size 43077
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88b326eaa9d0ff2ac2367ee845e6ac951c1980be9aa0088ec516fd7f0e73e73b
|
||||
size 42966
|
||||
oid sha256:1878fdda863f1e5980e078a19091e849469c3b7a75f1080f3e2b3469ea5359d8
|
||||
size 42970
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:717fd885ea80d9e19668f397db70d5888f4527d1bdfd6605d2a9df34a78c3cba
|
||||
size 52161
|
||||
oid sha256:b8afbc2eeebd599d774de01d3f5fdd3cb44d1a0f4453b766fc1bc4be72acec48
|
||||
size 52437
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:760144e24bed219ce08723703a927f24e5c92fa179d6b2e2b0d7b65e24b9084d
|
||||
size 175776
|
||||
oid sha256:8723ba56f85d921fea103ad7ed1798a4ed6edfbb78e4e84031a14f92d096c838
|
||||
size 176068
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f24fb0d7e0388a38a5e1c6a8a11cef07243c06114492c2a6f6c789b4c4ec86ca
|
||||
size 26923
|
||||
oid sha256:e828aac8c3edcda28cb04a7c1469148c99b3151786a67ac63a260b1310bb96e6
|
||||
size 26511
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:851ae2dca4f64e94318a7f3808cb8b56a3078b0908516e4a550526027941db16
|
||||
size 24219
|
||||
oid sha256:98d52f889f8803dd751f84b9e3fa065309a972e9f7b9a86f0cae697898c81670
|
||||
size 24745
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12698e405bee4a38198be6019e02fdf909065443d30d2c78eb86f5361a51669b
|
||||
size 29483
|
||||
oid sha256:01e8b6c8a7419f5053f398bfc2d0ce37fc1381274d568b344632bce1b8d02659
|
||||
size 29276
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2ecaed816791a6b3175efdc2fcaaba08e0603c28e00554388292a99ff94a9784
|
||||
size 26658
|
||||
oid sha256:9b94b561d6e4211772c1fd5315f9db28f26a964af46ca94af8bc5cd6fc48c578
|
||||
size 27270
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:907bf4448631bb9f93c59e3cdc6692fa6c1cd824838e148c12987d4223b3f627
|
||||
size 23226
|
||||
oid sha256:8fc3bf4aa9a13f88a8366f2feb8dbfda64ceb2c04f819a1ff96cb09d88221a16
|
||||
size 23233
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2c202193a8571e7d2fb63077a30bdc87caeaceffc5fe035bd422e669dd191efb
|
||||
size 12440
|
||||
oid sha256:edd3b0f29a3fbf9035cc8ba1d7a5a3a74be30d4e6ec8f654832f7ee916497453
|
||||
size 12457
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:35e5e99149fa7c8366d5188b385a33d0f7a503901d2f7bc5a4507dc8ee1b5b56
|
||||
size 33462
|
||||
oid sha256:7d27f9fbe2b6d803198f0015cc391d6dfdd5d2708e837319c6eb0e82f8ab832c
|
||||
size 33488
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15ed353fa5cddfea55b6ce83d48f104e0ca1a1e050113bed726db6bb34986f33
|
||||
size 13290
|
||||
oid sha256:001f8509e1e21f53e4d19285875ac76b937ea73931f5df5bf57b050a008d45c1
|
||||
size 13126
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4f86906586e386ebebc6b3db39bfe43d117a8dbbbddbaebcbd595accb0a77408
|
||||
size 12882
|
||||
oid sha256:3961fa465a6b11c6b762f39980febed8cce6be557780327b24dada42c486d258
|
||||
size 13001
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2bb2940111afff24f4e4b0cfaf3a1a7e6a00e67465045b859a136b13631bb90e
|
||||
size 10578
|
||||
oid sha256:4f3acd334b5698e5b8f709eace3b8ba93218928012777a827d50d8057a02bda5
|
||||
size 10484
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b4d83ee5530bcc083a28ed107fe7a36419c8d1758ebb2258d9e14568e10a802
|
||||
size 10388
|
||||
oid sha256:34420f53dfeb8f753575aa1d40b368d82ecc3e294cdf0d5183055394c361f2c5
|
||||
size 10428
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7aef76310161fb327f5e2f431674d1dedcd266cc51aa61e553588896cd329fdd
|
||||
size 9421
|
||||
oid sha256:e73ef250e3acf077d3d0f523c4da886edf3a3ede3214e80b87fe0758d6d3c19f
|
||||
size 9617
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5f85fe20149306154f4f152818bba66ea9b91bd495115dd8527b50a8880d1ad3
|
||||
size 5895
|
||||
oid sha256:badb4d11df0d7f14d48962db795bff4030f68007acd4d4dc210d0ae52a818f29
|
||||
size 5903
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:89938b231fbd1e8fd7be781a3ba501ca843ea6c3454060c7801c590b8ecfa1da
|
||||
size 37047
|
||||
oid sha256:41066edc69f69eaced418f211d682bfc1202801900f0fc47af51b3b1ac6290d1
|
||||
size 37081
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4f8a9e835ac2c19291d6cffbcfa93f9b4845b5d1ab46d6b7895cce4cf386fd6
|
||||
size 38943
|
||||
oid sha256:5386114bbb3cbf6c8a3de0642f1efa7a3b5d7efa46168d70aeffc413e37ec803
|
||||
size 39014
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90a6828f639684e573076f30680813bfcfba878c2047a2da6b8f439e2ad00093
|
||||
size 101415
|
||||
oid sha256:12e121289ac0035f40e7e2cbf5bb16b57fdc58c3eea3cfba33a2c0af4ab24675
|
||||
size 101764
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8dba1ff14a7e732a422fd1e6f50d0ce0879dd945a1c208df9ed1bfc4b00f006
|
||||
size 115662
|
||||
oid sha256:8c0847975608f68e32e497a043c373cd471b18dcefb1cfd0a6abbfb2e3963265
|
||||
size 115619
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue