improvements in ui

This commit is contained in:
2024-07-19 06:52:25 +03:00
parent 2b018add7c
commit ce306c995e
9 changed files with 156 additions and 57 deletions
@@ -42,8 +42,11 @@ import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.service.OnlineService import dev.meloda.fast.service.OnlineService
import dev.meloda.fast.service.longpolling.LongPollingService 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.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme 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.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -152,10 +155,42 @@ class MainActivity : AppCompatActivity() {
} }
} }
val isDeviceCompact by remember(true) { val deviceWidthDp = remember(true) {
derivedStateOf { context.resources.displayMetrics.widthPixels.pxToDp()
context.resources.displayMetrics.widthPixels.pxToDp() <= 360
} }
val deviceHeightDp = remember(true) {
context.resources.displayMetrics.heightPixels.pxToDp()
}
val deviceWidthSize by remember(deviceWidthDp) {
derivedStateOf {
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 darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
@@ -181,13 +216,15 @@ class MainActivity : AppCompatActivity() {
selectedColorScheme = 0, selectedColorScheme = 0,
amoledDark = amoledDark, amoledDark = amoledDark,
enableBlur = enableBlur, enableBlur = enableBlur,
enableMultiline = enableMultiline, enableMultiline = enableMultiline
isDeviceCompact = isDeviceCompact
) )
) )
} }
CompositionLocalProvider(LocalThemeConfig provides themeConfig) { CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig
) {
AppTheme( AppTheme(
useDarkTheme = themeConfig.darkMode, useDarkTheme = themeConfig.darkMode,
useDynamicColors = themeConfig.dynamicColors, useDynamicColors = themeConfig.dynamicColors,
@@ -3,7 +3,8 @@ package dev.meloda.fast.network
enum class VkOAuthErrorType(val value: String) { enum class VkOAuthErrorType(val value: String) {
WRONG_OTP_FORMAT("otp_format_is_incorrect"), WRONG_OTP_FORMAT("otp_format_is_incorrect"),
WRONG_OTP("wrong_otp"), 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 { companion object {
fun parse(value: String): VkOAuthErrorType = entries.firstOrNull { it.value == value } fun parse(value: String): VkOAuthErrorType = entries.firstOrNull { it.value == value }
@@ -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()
}
@@ -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
}
@@ -6,6 +6,5 @@ data class ThemeConfig(
val selectedColorScheme: Int, val selectedColorScheme: Int,
val amoledDark: Boolean, val amoledDark: Boolean,
val enableBlur: Boolean, val enableBlur: Boolean,
val enableMultiline: Boolean, val enableMultiline: Boolean
val isDeviceCompact: Boolean
) )
@@ -20,9 +20,11 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat 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.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( private val googleSansFonts = FontFamily(
Font(resId = R.font.google_sans_regular), Font(resId = R.font.google_sans_regular),
@@ -110,8 +112,14 @@ val LocalThemeConfig = compositionLocalOf {
selectedColorScheme = 0, selectedColorScheme = 0,
amoledDark = false, amoledDark = false,
enableBlur = false, enableBlur = false,
enableMultiline = false, enableMultiline = false
isDeviceCompact = false )
}
val LocalSizeConfig = compositionLocalOf {
SizeConfig(
widthSize = DeviceSize.Compact,
heightSize = DeviceSize.Compact
) )
} }
@@ -1,12 +1,12 @@
package dev.meloda.fast.auth.login 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.State
import dev.meloda.fast.data.api.oauth.OAuthRepository import dev.meloda.fast.data.api.oauth.OAuthRepository
import dev.meloda.fast.network.OAuthErrorDomain import dev.meloda.fast.network.OAuthErrorDomain
import dev.meloda.fast.network.ValidationType import dev.meloda.fast.network.ValidationType
import dev.meloda.fast.network.VkOAuthError import dev.meloda.fast.network.VkOAuthError
import dev.meloda.fast.network.VkOAuthErrorType import dev.meloda.fast.network.VkOAuthErrorType
import dev.meloda.fast.auth.login.model.AuthInfo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -94,6 +94,8 @@ class OAuthUseCaseImpl(
VkOAuthError.INVALID_REQUEST -> { VkOAuthError.INVALID_REQUEST -> {
when (errorType) { when (errorType) {
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError)
VkOAuthErrorType.WRONG_OTP -> { VkOAuthErrorType.WRONG_OTP -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode) State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
} }
@@ -106,7 +108,9 @@ class OAuthUseCaseImpl(
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError) State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
} }
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
} }
} }
@@ -50,14 +50,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.LoginViewModel
import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.auth.login.LoginViewModelImpl
import dev.meloda.fast.auth.login.model.CaptchaArguments 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.LoginScreenState
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
import dev.meloda.fast.auth.login.model.LoginValidationArguments 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 org.koin.androidx.compose.koinViewModel
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
@@ -156,7 +156,7 @@ fun LoginScreen(
onPasswordFieldGoAction: () -> Unit = {}, onPasswordFieldGoAction: () -> Unit = {},
onSignInButtonClicked: () -> Unit = {} onSignInButtonClicked: () -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val currentSize = LocalSizeConfig.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
@@ -182,19 +182,19 @@ fun LoginScreen(
} }
) )
val titleStyle = if (currentTheme.isDeviceCompact) { val titleStyle = if (currentSize.isWidthSmall) {
MaterialTheme.typography.displaySmall MaterialTheme.typography.displayMedium
} else { } else {
MaterialTheme.typography.displayMedium MaterialTheme.typography.displayMedium
} }
val titleSpacerSize = if (currentTheme.isDeviceCompact) { val titleSpacerSize = if (currentSize.isHeightSmall) {
24.dp 24.dp
} else { } else {
58.dp 58.dp
} }
val bottomPadding = if (currentTheme.isDeviceCompact) { val bottomPadding = if (currentSize.isHeightSmall) {
10.dp 10.dp
} else { } else {
30.dp 30.dp
@@ -353,11 +353,18 @@ fun LoginScreen(
Box( Box(
modifier = Modifier.align(Alignment.BottomCenter), modifier = Modifier.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) {
AnimatedVisibility(
visible = !screenState.isLoading,
enter = fadeIn(),
exit = fadeOut()
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
if (!screenState.isLoading) {
focusManager.clearFocus() focusManager.clearFocus()
onSignInButtonClicked() onSignInButtonClicked()
}
}, },
containerColor = MaterialTheme.colorScheme.secondaryContainer, containerColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.testTag("sing_in_fab") modifier = Modifier.testTag("sing_in_fab")
@@ -368,6 +375,7 @@ fun LoginScreen(
tint = MaterialTheme.colorScheme.onSecondaryContainer tint = MaterialTheme.colorScheme.onSecondaryContainer
) )
} }
}
AnimatedVisibility( AnimatedVisibility(
visible = screenState.isLoading, visible = screenState.isLoading,
enter = fadeIn(), enter = fadeIn(),
@@ -1,6 +1,9 @@
package dev.meloda.fast.auth.login.presentation package dev.meloda.fast.auth.login.presentation
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState 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.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.res.stringResource
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.fast.auth.login.BuildConfig import dev.meloda.fast.auth.login.BuildConfig
import dev.meloda.fast.auth.login.LoginViewModel import dev.meloda.fast.auth.login.LoginViewModel
import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.auth.login.LoginViewModelImpl
import dev.meloda.fast.ui.components.ActionInvokeDismiss import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.MaterialDialog 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 org.koin.androidx.compose.koinViewModel
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
@@ -50,6 +55,7 @@ fun LogoRoute(
onGoNextButtonClicked: () -> Unit, onGoNextButtonClicked: () -> Unit,
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>() viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
) { ) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle() val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
val isNeedToShowSignInAlert by viewModel.isNeedToShowFastSignInAlert.collectAsStateWithLifecycle() val isNeedToShowSignInAlert by viewModel.isNeedToShowFastSignInAlert.collectAsStateWithLifecycle()
@@ -61,6 +67,7 @@ fun LogoRoute(
} }
LogoScreen( LogoScreen(
isLoading = screenState.isLoading,
onLogoLongClicked = viewModel::onLogoLongClicked, onLogoLongClicked = viewModel::onLogoLongClicked,
onGoNextButtonClicked = onGoNextButtonClicked onGoNextButtonClicked = onGoNextButtonClicked
) )
@@ -76,10 +83,11 @@ fun LogoRoute(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LogoScreen( fun LogoScreen(
isLoading: Boolean,
onLogoLongClicked: () -> Unit = {}, onLogoLongClicked: () -> Unit = {},
onGoNextButtonClicked: () -> Unit = {} onGoNextButtonClicked: () -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val currentSize = LocalSizeConfig.current
Scaffold { padding -> Scaffold { padding ->
val topPadding by animateDpAsState( val topPadding by animateDpAsState(
@@ -100,19 +108,19 @@ fun LogoScreen(
label = "startPaddingAnimation" label = "startPaddingAnimation"
) )
val iconWidth = if (currentTheme.isDeviceCompact) { val iconWidth = if (currentSize.isWidthSmall) {
100.dp 110.dp
} else { } else {
134.dp 134.dp
} }
val appNameTextStyle = if (currentTheme.isDeviceCompact) { val appNameTextStyle = if (currentSize.isWidthSmall) {
MaterialTheme.typography.displaySmall MaterialTheme.typography.displayMedium.copy(fontSize = 40.sp)
} else { } else {
MaterialTheme.typography.displayMedium MaterialTheme.typography.displayMedium
} }
val bottomAdditionalPadding = if (currentTheme.isDeviceCompact) { val bottomAdditionalPadding = if (currentSize.isHeightSmall) {
10.dp 10.dp
} else { } else {
30.dp 30.dp
@@ -158,12 +166,20 @@ fun LogoScreen(
) )
} }
AnimatedVisibility(
visible = !isLoading,
modifier = Modifier.align(Alignment.BottomCenter),
enter = fadeIn(),
exit = fadeOut()
) {
FloatingActionButton( FloatingActionButton(
onClick = onGoNextButtonClicked, onClick = {
if (!isLoading) {
onGoNextButtonClicked()
}
},
containerColor = MaterialTheme.colorScheme.secondaryContainer, containerColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier modifier = Modifier.testTag("go_next_fab")
.align(Alignment.BottomCenter)
.testTag("go_next_fab")
) { ) {
Icon( Icon(
painter = painterResource(id = UiR.drawable.ic_arrow_end), painter = painterResource(id = UiR.drawable.ic_arrow_end),
@@ -172,6 +188,14 @@ fun LogoScreen(
) )
} }
} }
AnimatedVisibility(
visible = isLoading,
modifier = Modifier.align(Alignment.BottomCenter)
) {
CircularProgressIndicator()
}
}
} }
} }