diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt index 49596ac6..f4422851 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt @@ -42,8 +42,11 @@ import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.service.OnlineService import dev.meloda.fast.service.longpolling.LongPollingService +import dev.meloda.fast.ui.model.DeviceSize +import dev.meloda.fast.ui.model.SizeConfig import dev.meloda.fast.ui.model.ThemeConfig import dev.meloda.fast.ui.theme.AppTheme +import dev.meloda.fast.ui.theme.LocalSizeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.isNeedToEnableDarkMode import org.koin.androidx.compose.koinViewModel @@ -152,12 +155,44 @@ class MainActivity : AppCompatActivity() { } } - val isDeviceCompact by remember(true) { + val deviceWidthDp = remember(true) { + context.resources.displayMetrics.widthPixels.pxToDp() + } + val deviceHeightDp = remember(true) { + context.resources.displayMetrics.heightPixels.pxToDp() + } + + val deviceWidthSize by remember(deviceWidthDp) { derivedStateOf { - context.resources.displayMetrics.widthPixels.pxToDp() <= 360 + when { + deviceWidthDp <= 360 -> DeviceSize.Small + deviceWidthDp <= 600 -> DeviceSize.Compact + deviceWidthDp <= 840 -> DeviceSize.Medium + else -> DeviceSize.Expanded + } } } + val deviceHeightSize by remember(deviceHeightDp) { + derivedStateOf { + when { + deviceHeightDp <= 480 -> DeviceSize.Small + deviceHeightDp <= 700 -> DeviceSize.Compact + deviceHeightDp <= 900 -> DeviceSize.Medium + else -> DeviceSize.Expanded + } + } + } + + val sizeConfig by remember(deviceWidthSize, deviceHeightSize) { + mutableStateOf( + SizeConfig( + widthSize = deviceWidthSize, + heightSize = deviceHeightSize + ) + ) + } + val darkMode by userSettings.darkMode.collectAsStateWithLifecycle() val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle() val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle() @@ -181,13 +216,15 @@ class MainActivity : AppCompatActivity() { selectedColorScheme = 0, amoledDark = amoledDark, enableBlur = enableBlur, - enableMultiline = enableMultiline, - isDeviceCompact = isDeviceCompact + enableMultiline = enableMultiline ) ) } - CompositionLocalProvider(LocalThemeConfig provides themeConfig) { + CompositionLocalProvider( + LocalThemeConfig provides themeConfig, + LocalSizeConfig provides sizeConfig + ) { AppTheme( useDarkTheme = themeConfig.darkMode, useDynamicColors = themeConfig.dynamicColors, diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/VkOAuthErrorType.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/VkOAuthErrorType.kt index 488b38a5..82588a6a 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/VkOAuthErrorType.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/VkOAuthErrorType.kt @@ -3,7 +3,8 @@ package dev.meloda.fast.network enum class VkOAuthErrorType(val value: String) { WRONG_OTP_FORMAT("otp_format_is_incorrect"), WRONG_OTP("wrong_otp"), - PASSWORD_BRUTEFORCE_ATTEMPT("password_bruteforce_attempt"); + PASSWORD_BRUTEFORCE_ATTEMPT("password_bruteforce_attempt"), + USERNAME_OR_PASSWORD_IS_INCORRECT("username_or_password_is_incorrect"); companion object { fun parse(value: String): VkOAuthErrorType = entries.firstOrNull { it.value == value } diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/DeviceSize.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/DeviceSize.kt new file mode 100644 index 00000000..16f4d5fa --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/DeviceSize.kt @@ -0,0 +1,8 @@ +package dev.meloda.fast.ui.model + +sealed class DeviceSize { + data object Small : DeviceSize() + data object Compact : DeviceSize() + data object Medium : DeviceSize() + data object Expanded : DeviceSize() +} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/SizeConfig.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/SizeConfig.kt new file mode 100644 index 00000000..6a13bc5b --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/SizeConfig.kt @@ -0,0 +1,10 @@ +package dev.meloda.fast.ui.model + +data class SizeConfig( + val widthSize: DeviceSize, + val heightSize: DeviceSize +) { + + val isHeightSmall: Boolean get() = heightSize is DeviceSize.Small + val isWidthSmall: Boolean get() = widthSize is DeviceSize.Small +} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/ThemeConfig.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/ThemeConfig.kt index a24ee3fd..3eb7b337 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/ThemeConfig.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/ThemeConfig.kt @@ -6,6 +6,5 @@ data class ThemeConfig( val selectedColorScheme: Int, val amoledDark: Boolean, val enableBlur: Boolean, - val enableMultiline: Boolean, - val isDeviceCompact: Boolean + val enableMultiline: Boolean ) diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt index be1b33fc..35982632 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt @@ -20,9 +20,11 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat -import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.model.ThemeConfig import dev.chrisbanes.haze.HazeState +import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.model.DeviceSize +import dev.meloda.fast.ui.model.SizeConfig +import dev.meloda.fast.ui.model.ThemeConfig private val googleSansFonts = FontFamily( Font(resId = R.font.google_sans_regular), @@ -110,8 +112,14 @@ val LocalThemeConfig = compositionLocalOf { selectedColorScheme = 0, amoledDark = false, enableBlur = false, - enableMultiline = false, - isDeviceCompact = false + enableMultiline = false + ) +} + +val LocalSizeConfig = compositionLocalOf { + SizeConfig( + widthSize = DeviceSize.Compact, + heightSize = DeviceSize.Compact ) } diff --git a/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/OAuthUseCaseImpl.kt b/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/OAuthUseCaseImpl.kt index e9d984d1..9c3342ae 100644 --- a/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/OAuthUseCaseImpl.kt +++ b/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/OAuthUseCaseImpl.kt @@ -1,12 +1,12 @@ package dev.meloda.fast.auth.login +import dev.meloda.fast.auth.login.model.AuthInfo import dev.meloda.fast.data.State import dev.meloda.fast.data.api.oauth.OAuthRepository import dev.meloda.fast.network.OAuthErrorDomain import dev.meloda.fast.network.ValidationType import dev.meloda.fast.network.VkOAuthError import dev.meloda.fast.network.VkOAuthErrorType -import dev.meloda.fast.auth.login.model.AuthInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -94,6 +94,8 @@ class OAuthUseCaseImpl( VkOAuthError.INVALID_REQUEST -> { when (errorType) { + null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) + VkOAuthErrorType.WRONG_OTP -> { State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode) } @@ -106,7 +108,9 @@ class OAuthUseCaseImpl( State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError) } - null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) + VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> { + State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError) + } } } diff --git a/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt b/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt index cc6d86ef..5a240d00 100644 --- a/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt +++ b/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt @@ -50,14 +50,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dev.meloda.fast.ui.basic.autoFillRequestHandler -import dev.meloda.fast.ui.basic.connectNode -import dev.meloda.fast.ui.basic.defaultFocusChangeAutoFill -import dev.meloda.fast.ui.components.MaterialDialog -import dev.meloda.fast.ui.components.TextFieldErrorText -import dev.meloda.fast.ui.theme.LocalThemeConfig -import dev.meloda.fast.ui.util.handleEnterKey -import dev.meloda.fast.ui.util.handleTabKey import dev.meloda.fast.auth.login.LoginViewModel import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.auth.login.model.CaptchaArguments @@ -65,6 +57,14 @@ import dev.meloda.fast.auth.login.model.LoginError import dev.meloda.fast.auth.login.model.LoginScreenState import dev.meloda.fast.auth.login.model.LoginUserBannedArguments import dev.meloda.fast.auth.login.model.LoginValidationArguments +import dev.meloda.fast.ui.basic.autoFillRequestHandler +import dev.meloda.fast.ui.basic.connectNode +import dev.meloda.fast.ui.basic.defaultFocusChangeAutoFill +import dev.meloda.fast.ui.components.MaterialDialog +import dev.meloda.fast.ui.components.TextFieldErrorText +import dev.meloda.fast.ui.theme.LocalSizeConfig +import dev.meloda.fast.ui.util.handleEnterKey +import dev.meloda.fast.ui.util.handleTabKey import org.koin.androidx.compose.koinViewModel import dev.meloda.fast.ui.R as UiR @@ -156,7 +156,7 @@ fun LoginScreen( onPasswordFieldGoAction: () -> Unit = {}, onSignInButtonClicked: () -> Unit = {} ) { - val currentTheme = LocalThemeConfig.current + val currentSize = LocalSizeConfig.current val focusManager = LocalFocusManager.current val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() @@ -182,19 +182,19 @@ fun LoginScreen( } ) - val titleStyle = if (currentTheme.isDeviceCompact) { - MaterialTheme.typography.displaySmall + val titleStyle = if (currentSize.isWidthSmall) { + MaterialTheme.typography.displayMedium } else { MaterialTheme.typography.displayMedium } - val titleSpacerSize = if (currentTheme.isDeviceCompact) { + val titleSpacerSize = if (currentSize.isHeightSmall) { 24.dp } else { 58.dp } - val bottomPadding = if (currentTheme.isDeviceCompact) { + val bottomPadding = if (currentSize.isHeightSmall) { 10.dp } else { 30.dp @@ -354,19 +354,27 @@ fun LoginScreen( modifier = Modifier.align(Alignment.BottomCenter), contentAlignment = Alignment.Center ) { - FloatingActionButton( - onClick = { - focusManager.clearFocus() - onSignInButtonClicked() - }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.testTag("sing_in_fab") + AnimatedVisibility( + visible = !screenState.isLoading, + enter = fadeIn(), + exit = fadeOut() ) { - Icon( - painter = painterResource(id = UiR.drawable.ic_arrow_end), - contentDescription = "Sign in icon", - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) + FloatingActionButton( + onClick = { + if (!screenState.isLoading) { + focusManager.clearFocus() + onSignInButtonClicked() + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.testTag("sing_in_fab") + ) { + Icon( + painter = painterResource(id = UiR.drawable.ic_arrow_end), + contentDescription = "Sign in icon", + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } } AnimatedVisibility( visible = screenState.isLoading, diff --git a/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt b/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt index 7cb8122a..e7b33f99 100644 --- a/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt +++ b/feature/auth/login/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt @@ -1,6 +1,9 @@ package dev.meloda.fast.auth.login.presentation +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -14,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -34,13 +38,14 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.meloda.fast.auth.login.BuildConfig import dev.meloda.fast.auth.login.LoginViewModel import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.ui.components.ActionInvokeDismiss import dev.meloda.fast.ui.components.MaterialDialog -import dev.meloda.fast.ui.theme.LocalThemeConfig +import dev.meloda.fast.ui.theme.LocalSizeConfig import org.koin.androidx.compose.koinViewModel import dev.meloda.fast.ui.R as UiR @@ -50,6 +55,7 @@ fun LogoRoute( onGoNextButtonClicked: () -> Unit, viewModel: LoginViewModel = koinViewModel() ) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle() val isNeedToShowSignInAlert by viewModel.isNeedToShowFastSignInAlert.collectAsStateWithLifecycle() @@ -61,6 +67,7 @@ fun LogoRoute( } LogoScreen( + isLoading = screenState.isLoading, onLogoLongClicked = viewModel::onLogoLongClicked, onGoNextButtonClicked = onGoNextButtonClicked ) @@ -76,10 +83,11 @@ fun LogoRoute( @OptIn(ExperimentalFoundationApi::class) @Composable fun LogoScreen( + isLoading: Boolean, onLogoLongClicked: () -> Unit = {}, onGoNextButtonClicked: () -> Unit = {} ) { - val currentTheme = LocalThemeConfig.current + val currentSize = LocalSizeConfig.current Scaffold { padding -> val topPadding by animateDpAsState( @@ -100,19 +108,19 @@ fun LogoScreen( label = "startPaddingAnimation" ) - val iconWidth = if (currentTheme.isDeviceCompact) { - 100.dp + val iconWidth = if (currentSize.isWidthSmall) { + 110.dp } else { 134.dp } - val appNameTextStyle = if (currentTheme.isDeviceCompact) { - MaterialTheme.typography.displaySmall + val appNameTextStyle = if (currentSize.isWidthSmall) { + MaterialTheme.typography.displayMedium.copy(fontSize = 40.sp) } else { MaterialTheme.typography.displayMedium } - val bottomAdditionalPadding = if (currentTheme.isDeviceCompact) { + val bottomAdditionalPadding = if (currentSize.isHeightSmall) { 10.dp } else { 30.dp @@ -158,18 +166,34 @@ fun LogoScreen( ) } - FloatingActionButton( - onClick = onGoNextButtonClicked, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier - .align(Alignment.BottomCenter) - .testTag("go_next_fab") + AnimatedVisibility( + visible = !isLoading, + modifier = Modifier.align(Alignment.BottomCenter), + enter = fadeIn(), + exit = fadeOut() ) { - Icon( - painter = painterResource(id = UiR.drawable.ic_arrow_end), - contentDescription = "Go button", - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) + FloatingActionButton( + onClick = { + if (!isLoading) { + onGoNextButtonClicked() + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.testTag("go_next_fab") + ) { + Icon( + painter = painterResource(id = UiR.drawable.ic_arrow_end), + contentDescription = "Go button", + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + + AnimatedVisibility( + visible = isLoading, + modifier = Modifier.align(Alignment.BottomCenter) + ) { + CircularProgressIndicator() } } }