refactor: improve auth screen and bump haze version

* Bump haze version to 1.6.0.
* Blur now works on android 11 and older
* Add "Sign up" and "Forgot password?" links to the auth screen.
* Add logic to toggle dynamic colors on logo click in the auth screen (Android 12+).
This commit is contained in:
2025-05-13 22:52:53 +03:00
parent b63cc86e48
commit 325211ad5f
13 changed files with 128 additions and 46 deletions
@@ -68,7 +68,7 @@ fun MainScreen(
) { ) {
val activity = LocalActivity.current as? AppCompatActivity ?: return val activity = LocalActivity.current as? AppCompatActivity ?: return
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
val hazeState = remember { HazeState() } val hazeState = remember { HazeState(true) }
val navController = rememberNavController() val navController = rememberNavController()
var selectedItemIndex by rememberSaveable { var selectedItemIndex by rememberSaveable {
@@ -130,7 +130,7 @@ val LocalSizeConfig = compositionLocalOf {
) )
} }
val LocalHazeState = compositionLocalOf { HazeState() } val LocalHazeState = compositionLocalOf { HazeState(true) }
val LocalBottomPadding = compositionLocalOf { 0.dp } val LocalBottomPadding = compositionLocalOf { 0.dp }
val LocalUser = compositionLocalOf<VkUser?> { null } val LocalUser = compositionLocalOf<VkUser?> { null }
val LocalReselectedTab = compositionLocalOf { mapOf<Any, Boolean>() } val LocalReselectedTab = compositionLocalOf { mapOf<Any, Boolean>() }
+3 -1
View File
@@ -222,7 +222,7 @@
<string name="settings_general_show_emoji_button_summary">Показывать кнопку эмоджи на панели чата</string> <string name="settings_general_show_emoji_button_summary">Показывать кнопку эмоджи на панели чата</string>
<string name="settings_features_show_time_in_action_messages_title">Показывать время в сервисных сообщениях</string> <string name="settings_features_show_time_in_action_messages_title">Показывать время в сервисных сообщениях</string>
<string name="settings_experimental_use_blur_title">Использовать размытие</string> <string name="settings_experimental_use_blur_title">Использовать размытие</string>
<string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно.\\nРаботает только с 12 версии Android</string> <string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно</string>
<string name="settings_experimental_more_animations_title">Больше анимаций</string> <string name="settings_experimental_more_animations_title">Больше анимаций</string>
<string name="warning_confirmation">Подтверждение</string> <string name="warning_confirmation">Подтверждение</string>
<string name="captcha_exit_warning">Вы уверены? Процесс ввода капчи будет отменён</string> <string name="captcha_exit_warning">Вы уверены? Процесс ввода капчи будет отменён</string>
@@ -269,4 +269,6 @@
<string name="italic">Курсив</string> <string name="italic">Курсив</string>
<string name="underline">Подчёркнутый</string> <string name="underline">Подчёркнутый</string>
<string name="link">Ссылка</string> <string name="link">Ссылка</string>
<string name="login_sign_up">Регистрация</string>
<string name="login_forgot_password">Забыли пароль?</string>
</resources> </resources>
+4 -2
View File
@@ -270,7 +270,7 @@
<string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string> <string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string>
<string name="settings_features_show_time_in_action_messages_title">Show time in action messages</string> <string name="settings_features_show_time_in_action_messages_title">Show time in action messages</string>
<string name="settings_experimental_use_blur_title">Use blur</string> <string name="settings_experimental_use_blur_title">Use blur</string>
<string name="settings_experimental_use_blur_summary">Adds blur wherever possible.\nWorks on android 12 and newer</string> <string name="settings_experimental_use_blur_summary">Adds blur wherever possible</string>
<string name="settings_experimental_more_animations_title">More animations</string> <string name="settings_experimental_more_animations_title">More animations</string>
<string name="settings_experimental_more_animations_summary">Use animations wherever possible</string> <string name="settings_experimental_more_animations_summary">Use animations wherever possible</string>
@@ -340,9 +340,11 @@
<string name="unspam_message_text">Are you sure you want to unmark this message as spam?</string> <string name="unspam_message_text">Are you sure you want to unmark this message as spam?</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
<string name="autofill">Autofill</string> <string name="autofill" tools:ignore="PrivateResource">Autofill</string>
<string name="bold">Bold</string> <string name="bold">Bold</string>
<string name="italic">Italic</string> <string name="italic">Italic</string>
<string name="underline">Underline</string> <string name="underline">Underline</string>
<string name="link">Link</string> <string name="link">Link</string>
<string name="login_sign_up">Sign up</string>
<string name="login_forgot_password">Forgot password?</string>
</resources> </resources>
@@ -1,5 +1,6 @@
package dev.meloda.fast.auth.login package dev.meloda.fast.auth.login
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -24,6 +25,7 @@ import dev.meloda.fast.data.db.AccountsRepository
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.data.success import dev.meloda.fast.data.success
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.domain.OAuthUseCase
import dev.meloda.fast.model.database.AccountEntity import dev.meloda.fast.model.database.AccountEntity
@@ -62,6 +64,8 @@ interface LoginViewModel {
fun onSignInButtonClicked() fun onSignInButtonClicked()
fun onLogoClicked()
fun onNavigatedToMain() fun onNavigatedToMain()
fun onNavigatedToUserBanned() fun onNavigatedToUserBanned()
fun onNavigatedToCaptcha() fun onNavigatedToCaptcha()
@@ -79,7 +83,8 @@ class LoginViewModelImpl(
private val loadUserByIdUseCase: LoadUserByIdUseCase, private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val accountsRepository: AccountsRepository, private val accountsRepository: AccountsRepository,
private val loginValidator: LoginValidator, private val loginValidator: LoginValidator,
private val longPollController: LongPollController private val longPollController: LongPollController,
private val userSettings: UserSettings
) : ViewModel(), LoginViewModel { ) : ViewModel(), LoginViewModel {
override val screenState = MutableStateFlow(LoginScreenState.EMPTY) override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
@@ -164,6 +169,14 @@ class LoginViewModelImpl(
login() login()
} }
override fun onLogoClicked() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
userSettings.onEnableDynamicColorsChanged(
!userSettings.enableDynamicColors.value
)
}
}
override fun onNavigatedToMain() { override fun onNavigatedToMain() {
isNeedToOpenMain.update { false } isNeedToOpenMain.update { false }
} }
@@ -210,7 +223,7 @@ class LoginViewModelImpl(
processValidation() processValidation()
if (!validationState.value.contains(LoginValidationResult.Valid)) return if (!validationState.value.contains(LoginValidationResult.Valid)) return
screenState.updateValue { copy(isLoading = false) } screenState.updateValue { copy(isLoading = true) }
val currentValidationSid = validationSid.value val currentValidationSid = validationSid.value
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null } val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
@@ -1,5 +1,6 @@
package dev.meloda.fast.auth.login.presentation package dev.meloda.fast.auth.login.presentation
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -8,6 +9,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -27,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -37,18 +40,20 @@ import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentType import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.password
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
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
@@ -126,7 +131,8 @@ fun LoginRoute(
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked, onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
onPasswordFieldGoAction = viewModel::onSignInButtonClicked, onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
onSignInButtonClicked = viewModel::onSignInButtonClicked onSignInButtonClicked = viewModel::onSignInButtonClicked,
onLogoClicked = viewModel::onLogoClicked
) )
HandleDialogs( HandleDialogs(
@@ -144,13 +150,21 @@ fun LoginScreen(
onPasswordFieldEnterKeyClicked: () -> Unit = {}, onPasswordFieldEnterKeyClicked: () -> Unit = {},
onPasswordVisibilityButtonClicked: () -> Unit = {}, onPasswordVisibilityButtonClicked: () -> Unit = {},
onPasswordFieldGoAction: () -> Unit = {}, onPasswordFieldGoAction: () -> Unit = {},
onSignInButtonClicked: () -> Unit = {} onSignInButtonClicked: () -> Unit = {},
onLogoClicked: () -> Unit = {}
) { ) {
val context = LocalContext.current
val size = LocalSizeConfig.current val size = LocalSizeConfig.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val titleSpacerSize by animateDpAsState(if (size.isHeightSmall) 24.dp else 58.dp) val titleSpacerSize by animateDpAsState(
val bottomPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp) targetValue = if (size.isHeightSmall) 24.dp else 58.dp,
label = "title spacer size"
)
val bottomPadding by animateDpAsState(
targetValue = if (size.isHeightSmall) 10.dp else 30.dp,
label = "bottom padding"
)
val (loginFocusable, passwordFocusable) = val (loginFocusable, passwordFocusable) =
FocusRequester.createRefs() FocusRequester.createRefs()
@@ -163,15 +177,15 @@ fun LoginScreen(
.padding(padding) .padding(padding)
.padding(top = 30.dp) .padding(top = 30.dp)
.padding(horizontal = 30.dp) .padding(horizontal = 30.dp)
.padding(bottom = bottomPadding)
.fillMaxSize() .fillMaxSize()
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = screenState.showLogo, visible = screenState.showLogo,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut() exit = fadeOut(),
label = "Logo visibility"
) { ) {
Logo() Logo(onLogoClicked = onLogoClicked)
} }
AnimatedVisibility( AnimatedVisibility(
@@ -180,7 +194,8 @@ fun LoginScreen(
.align(Alignment.Center), .align(Alignment.Center),
visible = !screenState.showLogo, visible = !screenState.showLogo,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut() exit = fadeOut(),
label = "Login visibility"
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -235,7 +250,10 @@ fun LoginScreen(
isError = screenState.loginError, isError = screenState.loginError,
singleLine = true singleLine = true
) )
AnimatedVisibility(visible = screenState.loginError) { AnimatedVisibility(
visible = screenState.loginError,
label = "Login error visibility"
) {
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
} }
@@ -300,16 +318,18 @@ fun LoginScreen(
}, },
singleLine = true singleLine = true
) )
AnimatedVisibility(visible = screenState.passwordError) { AnimatedVisibility(
visible = screenState.passwordError,
label = "Password error visibility"
) {
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
} }
} }
} }
Column(
Box(
modifier = Modifier.align(Alignment.BottomCenter), modifier = Modifier.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center horizontalAlignment = Alignment.CenterHorizontally
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
@@ -324,7 +344,8 @@ fun LoginScreen(
AnimatedVisibility( AnimatedVisibility(
visible = screenState.isLoading, visible = screenState.isLoading,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut() exit = fadeOut(),
label = "Progress indicator visibility"
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
} }
@@ -332,7 +353,8 @@ fun LoginScreen(
AnimatedVisibility( AnimatedVisibility(
visible = !screenState.isLoading, visible = !screenState.isLoading,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut() exit = fadeOut(),
label = "Sign in icon visibility"
) { ) {
Icon( Icon(
painter = painterResource(id = UiR.drawable.ic_arrow_end), painter = painterResource(id = UiR.drawable.ic_arrow_end),
@@ -341,11 +363,56 @@ fun LoginScreen(
) )
} }
} }
}
} AnimatedVisibility(
} visible = screenState.showLogo,
label = "Bottom padding visibility"
) {
Spacer(Modifier.height(bottomPadding))
} }
AnimatedVisibility(
visible = !screenState.showLogo,
label = "Spacer between fab and bottom text buttons visibility"
) {
Spacer(Modifier.height(4.dp))
}
AnimatedVisibility(
visible = !screenState.showLogo,
label = "Text button row visibility"
) {
Row(verticalAlignment = Alignment.CenterVertically) {
TextButton(
onClick = {
context.startActivity(
Intent(Intent.ACTION_VIEW, "https://vk.com/join".toUri())
)
}
) {
Text(stringResource(UiR.string.login_sign_up))
}
Text(
text = "",
color = MaterialTheme.colorScheme.primary
)
TextButton(
onClick = {
context.startActivity(
Intent(Intent.ACTION_VIEW, "https://vk.com/restore".toUri())
)
}
) {
Text(stringResource(UiR.string.login_forgot_password))
}
}
}
}
}
}
}
@Composable @Composable
fun HandleDialogs( fun HandleDialogs(
@@ -368,3 +435,13 @@ fun HandleDialogs(
} }
} }
} }
@Preview
@Composable
private fun LoginScreenPreview() {
LoginScreen(
screenState = LoginScreenState.EMPTY.copy(
showLogo = false
)
)
}
@@ -1,9 +1,7 @@
package dev.meloda.fast.auth.login.presentation package dev.meloda.fast.auth.login.presentation
import android.os.Build
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -26,22 +24,20 @@ import androidx.compose.ui.text.style.TextAlign
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 androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.theme.LocalSizeConfig import dev.meloda.fast.ui.theme.LocalSizeConfig
import org.koin.compose.koinInject
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun Logo(modifier: Modifier = Modifier) { fun Logo(
modifier: Modifier = Modifier,
onLogoClicked: () -> Unit = {}
) {
val size = LocalSizeConfig.current val size = LocalSizeConfig.current
val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp) val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp)
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 38) val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 38)
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp) val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
val userSettings: UserSettings = koinInject()
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
@@ -65,13 +61,7 @@ fun Logo(modifier: Modifier = Modifier) {
interactionSource = null, interactionSource = null,
indication = null, indication = null,
onLongClick = null, onLongClick = null,
onClick = { onClick = onLogoClicked
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
userSettings.onEnableDynamicColorsChanged(
!userSettings.enableDynamicColors.value
)
}
}
) )
) )
@@ -23,5 +23,4 @@ class LoginValidator {
return resultList return resultList
} }
} }
@@ -84,7 +84,7 @@ fun ChatMaterialsScreen(
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
val hazeState = remember { HazeState() } val hazeState = remember { HazeState(true) }
val titles = remember { val titles = remember {
listOf( listOf(
@@ -153,7 +153,7 @@ fun MessagesHistoryScreen(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
val listState = rememberLazyListState() val listState = rememberLazyListState()
val hazeState = remember { HazeState() } val hazeState = remember { HazeState(true) }
LaunchedEffect(scrollIndex) { LaunchedEffect(scrollIndex) {
if (scrollIndex != null) { if (scrollIndex != null) {
@@ -416,8 +416,7 @@ class SettingsViewModelImpl(
key = SettingsKeys.KEY_USE_BLUR, key = SettingsKeys.KEY_USE_BLUR,
defaultValue = SettingsKeys.DEFAULT_USE_BLUR, defaultValue = SettingsKeys.DEFAULT_USE_BLUR,
title = UiText.Resource(UiR.string.settings_experimental_use_blur_title), title = UiText.Resource(UiR.string.settings_experimental_use_blur_title),
text = UiText.Resource(UiR.string.settings_experimental_use_blur_summary), text = UiText.Resource(UiR.string.settings_experimental_use_blur_summary)
isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
) )
val enableAnimations = SettingsItem.Switch( val enableAnimations = SettingsItem.Switch(
key = SettingsKeys.KEY_MORE_ANIMATIONS, key = SettingsKeys.KEY_MORE_ANIMATIONS,
@@ -128,7 +128,7 @@ fun SettingsScreen(
val themeConfig = LocalThemeConfig.current val themeConfig = LocalThemeConfig.current
val hazeState = remember { HazeState() } val hazeState = remember { HazeState(true) }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
+1 -1
View File
@@ -8,7 +8,7 @@ versionName = "0.2.2"
agp = "8.10.0" agp = "8.10.0"
converterMoshi = "2.11.0" converterMoshi = "2.11.0"
eithernet = "2.0.0" eithernet = "2.0.0"
haze = "1.5.4" haze = "1.6.0"
kotlin = "2.1.20" kotlin = "2.1.20"
ksp = "2.1.20-2.0.1" ksp = "2.1.20-2.0.1"