From d46c72f7e6bc62bb0282f76d13aecd61fbc98ea4 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sat, 29 Mar 2025 22:03:37 +0300 Subject: [PATCH] improve login screen UI and logic & fixes for blur --- .../meloda/fast/presentation/MainScreen.kt | 26 +- .../dev/meloda/fast/ui/basic/AutoFill.kt | 135 ------ .../meloda/fast/ui/components/ErrorView.kt | 22 +- .../src/main/res/drawable/round_error_24.xml | 11 + feature/auth/build.gradle.kts | 7 + .../kotlin/dev/meloda/fast/auth/AuthGraph.kt | 8 +- .../meloda/fast/auth/login/LoginViewModel.kt | 110 +++-- .../fast/auth/login/model/LoginDialog.kt | 14 + .../fast/auth/login/model/LoginScreenState.kt | 2 + .../auth/login/navigation/LoginNavigation.kt | 16 - .../auth/login/presentation/LoginScreen.kt | 439 +++++++++--------- .../fast/auth/login/presentation/Logo.kt | 83 ++++ .../auth/login/presentation/LogoScreen.kt | 229 --------- .../auth/login/presentation/SignInAlert.kt | 51 ++ .../presentation/ChatMaterialsScreen.kt | 221 ++++----- .../presentation/ConversationsList.kt | 4 +- .../fast/friends/presentation/FriendsList.kt | 2 + .../MessagesHistoryViewModel.kt | 7 +- .../messageshistory/model/MessageDialog.kt | 2 + .../presentation/MessagesHistoryScreen.kt | 2 - gradle/libs.versions.toml | 14 +- 21 files changed, 610 insertions(+), 795 deletions(-) delete mode 100644 core/ui/src/main/kotlin/dev/meloda/fast/ui/basic/AutoFill.kt create mode 100644 core/ui/src/main/res/drawable/round_error_24.xml create mode 100644 feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt create mode 100644 feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt delete mode 100644 feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt create mode 100644 feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt index e22113ee..1451cb16 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -26,6 +25,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost @@ -42,6 +42,7 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.navigation.MainGraph import dev.meloda.fast.profile.navigation.profileScreen +import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalUser @@ -58,7 +59,7 @@ fun MainScreen( onMessageClicked: (userId: Int) -> Unit = {}, onCreateChatClicked: () -> Unit = {} ) { - val currentTheme = LocalThemeConfig.current + val theme = LocalThemeConfig.current val hazeState = remember { HazeState() } val navController = rememberNavController() @@ -75,18 +76,12 @@ fun MainScreen( bottomBar = { NavigationBar( modifier = Modifier - .then( - if (currentTheme.enableBlur) { - Modifier.hazeEffect( - state = hazeState, - style = HazeMaterials.thick() - ) - } else Modifier - ) - .fillMaxWidth(), - containerColor = NavigationBarDefaults.containerColor.copy( - alpha = if (currentTheme.enableBlur) 0f else 1f - ) + .fillMaxWidth() + .hazeEffect( + state = hazeState, + style = HazeMaterials.thick() + ), + containerColor = Color.Transparent ) { navigationItems.forEachIndexed { index, item -> NavigationBarItem( @@ -145,11 +140,10 @@ fun MainScreen( Box( modifier = Modifier .fillMaxSize() - .padding(bottom = padding.calculateBottomPadding()) ) { CompositionLocalProvider( LocalHazeState provides hazeState, -// LocalBottomPadding provides if (currentTheme.enableBlur) padding.calculateBottomPadding() else 0.dp + LocalBottomPadding provides padding.calculateBottomPadding() ) { NavHost( navController = navController, diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/basic/AutoFill.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/basic/AutoFill.kt deleted file mode 100644 index 41711e14..00000000 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/basic/AutoFill.kt +++ /dev/null @@ -1,135 +0,0 @@ -package dev.meloda.fast.ui.basic - -import android.os.Build -import android.view.autofill.AutofillManager -import androidx.annotation.RequiresApi -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.Autofill -import androidx.compose.ui.autofill.AutofillNode -import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalAutofill -import androidx.compose.ui.platform.LocalAutofillTree -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import kotlin.math.roundToInt - -fun Modifier.connectNode(handler: AutoFillHandler): Modifier { - return with(handler) { fillBounds() } -} - -fun Modifier.defaultFocusChangeAutoFill(handler: AutoFillHandler): Modifier { - return this.then( - Modifier.onFocusChanged { - if (it.isFocused) { - handler.request() - } else { - handler.cancel() - } - } - ) -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun autoFillRequestHandler( - autofillTypes: List = listOf(), - onFill: (String) -> Unit, -): AutoFillHandler { - val view = LocalView.current - val context = LocalContext.current - var isFillRecently = remember { false } - val autoFillNode = remember { - AutofillNode( - autofillTypes = autofillTypes, - onFill = { - isFillRecently = true - onFill(it) - } - ) - } - val autofill = LocalAutofill.current - LocalAutofillTree.current += autoFillNode - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return EmptyAutoFillHandler - - return remember { - @RequiresApi(Build.VERSION_CODES.O) - object : AutoFillHandler { - val autofillManager = context.getSystemService(AutofillManager::class.java) - override fun requestManual() { - autofillManager.requestAutofill( - view, - autoFillNode.id, - autoFillNode.boundingBox?.toAndroidRect() ?: error("BoundingBox is not provided yet") - ) - } - - override fun requestVerifyManual() { - if (isFillRecently) { - isFillRecently = false - requestManual() - } - } - - override val autoFill: Autofill? - get() = autofill - - override val autoFillNode: AutofillNode - get() = autoFillNode - - override fun request() { - autofill?.requestAutofillForNode(autofillNode = autoFillNode) - } - - override fun cancel() { - autofill?.cancelAutofillForNode(autofillNode = autoFillNode) - } - - override fun Modifier.fillBounds(): Modifier { - return this.then( - Modifier.onGloballyPositioned { - autoFillNode.boundingBox = it.boundsInWindow() - }) - } - } - } -} - -fun Rect.toAndroidRect(): android.graphics.Rect { - return android.graphics.Rect( - left.roundToInt(), - top.roundToInt(), - right.roundToInt(), - bottom.roundToInt() - ) -} - -@OptIn(ExperimentalComposeUiApi::class) -interface AutoFillHandler { - - val autoFill: Autofill? - val autoFillNode: AutofillNode? - fun requestVerifyManual() - fun requestManual() - fun request() - fun cancel() - fun Modifier.fillBounds(): Modifier -} - -@ExperimentalComposeUiApi -data object EmptyAutoFillHandler : AutoFillHandler { - override val autoFill: Autofill? = null - override val autoFillNode: AutofillNode? = null - override fun requestVerifyManual() {} - override fun requestManual() {} - override fun request() {} - override fun cancel() {} - override fun Modifier.fillBounds(): Modifier = this.then(Modifier) -} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt index a7140fec..8922460a 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt @@ -6,19 +6,26 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import dev.meloda.fast.ui.R as UiR + @Composable fun ErrorView( modifier: Modifier = Modifier, + iconResId: Int? = UiR.drawable.round_error_24, text: String, buttonText: String? = null, onButtonClick: (() -> Unit)? = null, @@ -30,6 +37,16 @@ fun ErrorView( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { + iconResId?.let { + Icon( + modifier = Modifier.size(120.dp), + painter = painterResource(iconResId), + contentDescription = null, + tint = MaterialTheme.colorScheme.primaryContainer + ) + Spacer(modifier = Modifier.height(12.dp)) + } + Text( text = text, style = MaterialTheme.typography.titleLarge, @@ -37,9 +54,10 @@ fun ErrorView( ) buttonText?.let { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(24.dp)) Button( - onClick = { onButtonClick?.invoke() } + onClick = { onButtonClick?.invoke() }, + shape = RoundedCornerShape(6.dp) ) { Text(text = buttonText) } diff --git a/core/ui/src/main/res/drawable/round_error_24.xml b/core/ui/src/main/res/drawable/round_error_24.xml new file mode 100644 index 00000000..73d61989 --- /dev/null +++ b/core/ui/src/main/res/drawable/round_error_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index a65db56e..930f0059 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -46,6 +46,13 @@ androidComponents { } } +// TODO: 29-Mar-25, Danil Nikolaev: remove when autofill changes will be in release +configurations.all { + resolutionStrategy { + force(libs.compose.ui) + } +} + android { namespace = "dev.meloda.fast.auth" diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt index d1aa7d82..b93d206b 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt @@ -6,9 +6,8 @@ import androidx.navigation.navigation import dev.meloda.fast.auth.captcha.navigation.captchaScreen import dev.meloda.fast.auth.captcha.navigation.navigateToCaptcha import dev.meloda.fast.auth.captcha.navigation.setCaptchaResult -import dev.meloda.fast.auth.login.navigation.Logo +import dev.meloda.fast.auth.login.navigation.Login import dev.meloda.fast.auth.login.navigation.loginScreen -import dev.meloda.fast.auth.login.navigation.navigateToLogin import dev.meloda.fast.auth.userbanned.model.UserBannedArguments import dev.meloda.fast.auth.userbanned.navigation.navigateToUserBanned import dev.meloda.fast.auth.userbanned.navigation.userBannedRoute @@ -26,9 +25,7 @@ fun NavGraphBuilder.authNavGraph( onNavigateToMain: () -> Unit, navController: NavController ) { - navigation( - startDestination = Logo - ) { + navigation(startDestination = Login) { loginScreen( onNavigateToCaptcha = { arguments -> navController.navigateToCaptcha( @@ -57,7 +54,6 @@ fun NavGraphBuilder.authNavGraph( ) ) }, - onNavigateToCredentials = navController::navigateToLogin, navController = navController ) diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt index 976ea9e0..8b76e732 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt @@ -1,10 +1,11 @@ package dev.meloda.fast.auth.login +import android.os.Bundle import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.meloda.fast.auth.login.model.CaptchaArguments -import dev.meloda.fast.auth.login.model.LoginError +import dev.meloda.fast.auth.login.model.LoginDialog import dev.meloda.fast.auth.login.model.LoginScreenState import dev.meloda.fast.auth.login.model.LoginUserBannedArguments import dev.meloda.fast.auth.login.model.LoginValidationArguments @@ -36,7 +37,7 @@ import kotlinx.coroutines.launch interface LoginViewModel { val screenState: StateFlow - val loginError: StateFlow + val loginDialog: StateFlow val validationCode: StateFlow val validationArguments: StateFlow @@ -44,7 +45,11 @@ interface LoginViewModel { val captchaArguments: StateFlow val userBannedArguments: StateFlow val isNeedToOpenMain: StateFlow - val isNeedToShowFastSignInAlert: StateFlow + + fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) + fun onDialogDismissed(dialog: LoginDialog) + + fun onBackPressed() fun onPasswordVisibilityButtonClicked() @@ -53,8 +58,6 @@ interface LoginViewModel { fun onSignInButtonClicked() - fun onErrorDialogDismissed() - fun onNavigatedToMain() fun onNavigatedToUserBanned() fun onNavigatedToCaptcha() @@ -64,9 +67,6 @@ interface LoginViewModel { fun onCaptchaCodeReceived(code: String) fun onLogoLongClicked() - - fun onFastLogInAlertDismissed() - fun onFastLogInAlertConfirmClicked(token: String) } class LoginViewModelImpl( @@ -78,7 +78,7 @@ class LoginViewModelImpl( ) : ViewModel(), LoginViewModel { override val screenState = MutableStateFlow(LoginScreenState.EMPTY) - override val loginError = MutableStateFlow(null) + override val loginDialog = MutableStateFlow(null) override val validationCode = MutableStateFlow(null) override val validationArguments = MutableStateFlow(null) @@ -86,39 +86,63 @@ class LoginViewModelImpl( override val captchaArguments = MutableStateFlow(null) override val userBannedArguments = MutableStateFlow(null) override val isNeedToOpenMain = MutableStateFlow(false) - override val isNeedToShowFastSignInAlert = MutableStateFlow(false) private val validationState: StateFlow> = screenState.map(loginValidator::validate) .stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty)) + override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) { + onDialogDismissed(dialog) + + when (dialog) { + is LoginDialog.Error -> Unit + + LoginDialog.FastAuth -> { + val token = bundle.getString("token")?.trim() ?: return + fastAuth(token) + } + } + } + + override fun onDialogDismissed(dialog: LoginDialog) { + loginDialog.setValue { null } + } + + override fun onBackPressed() { + screenState.setValue { old -> old.copy(showLogo = true) } + } + override fun onPasswordVisibilityButtonClicked() { screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) } } override fun onLoginInputChanged(newLogin: String) { - val newState = screenState.value.copy( - login = newLogin.trim(), - loginError = false - ) - screenState.setValue { newState } + screenState.setValue { old -> + old.copy( + login = newLogin.trim(), + loginError = false + ) + } } override fun onPasswordInputChanged(newPassword: String) { - val newState = screenState.value.copy( - password = newPassword.trim(), - passwordError = false - ) - screenState.setValue { newState } + screenState.setValue { old -> + old.copy( + password = newPassword.trim(), + passwordError = false + ) + } } override fun onSignInButtonClicked() { if (screenState.value.isLoading) return - login() - } - override fun onErrorDialogDismissed() { - loginError.update { null } + if (screenState.value.showLogo) { + screenState.setValue { old -> old.copy(showLogo = false) } + return + } + + login() } override fun onNavigatedToMain() { @@ -150,14 +174,10 @@ class LoginViewModelImpl( } override fun onLogoLongClicked() { - isNeedToShowFastSignInAlert.update { true } + loginDialog.setValue { LoginDialog.FastAuth } } - override fun onFastLogInAlertDismissed() { - isNeedToShowFastSignInAlert.update { false } - } - - override fun onFastLogInAlertConfirmClicked(token: String) { + private fun fastAuth(token: String) { var currentAccount = AccountEntity( userId = -1, accessToken = token, @@ -177,12 +197,12 @@ class LoginViewModelImpl( nomCase = null ).listenValue(viewModelScope) { state -> state.processState( - error = { error -> + error = { UserConfig.currentUserId = -1 UserConfig.userId = -1 UserConfig.accessToken = "" - // TODO: 19/07/2024, Danil Nikolaev: show error? + loginDialog.setValue { LoginDialog.Error() } }, success = { response -> val actualUserId = requireNotNull(response).id @@ -241,7 +261,7 @@ class LoginViewModelImpl( val accessToken = response.accessToken if (userId == null || accessToken == null) { - loginError.update { LoginError.Unknown } + loginDialog.setValue { LoginDialog.Error() } return@processState } @@ -312,7 +332,9 @@ class LoginViewModelImpl( } OAuthErrorDomain.InvalidCredentialsError -> { - loginError.update { LoginError.WrongCredentials } + loginDialog.setValue { + LoginDialog.Error(errorText = "Wrong login or password.") + } } is OAuthErrorDomain.UserBannedError -> { @@ -326,19 +348,25 @@ class LoginViewModelImpl( } OAuthErrorDomain.WrongValidationCode -> { - loginError.update { LoginError.WrongValidationCode } + loginDialog.setValue { + LoginDialog.Error(errorText = "Wrong validation code.") + } } OAuthErrorDomain.WrongValidationCodeFormat -> { - loginError.update { LoginError.WrongValidationCodeFormat } + loginDialog.setValue { + LoginDialog.Error(errorText = "Wrong validation code format.") + } } OAuthErrorDomain.TooManyTriesError -> { - loginError.update { LoginError.TooManyTries } + loginDialog.setValue { + LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.") + } } OAuthErrorDomain.UnknownError -> { - loginError.update { LoginError.Unknown } + loginDialog.setValue { LoginDialog.Error() } } } @@ -346,9 +374,9 @@ class LoginViewModelImpl( } is State.Error.TestError -> { - val message = stateError.message - val error = LoginError.SimpleError(message = message) - loginError.update { error } + loginDialog.setValue { + LoginDialog.Error(errorText = stateError.message) + } true } diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt new file mode 100644 index 00000000..ad6806cc --- /dev/null +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt @@ -0,0 +1,14 @@ +package dev.meloda.fast.auth.login.model + +import androidx.compose.runtime.Immutable + +@Immutable +sealed class LoginDialog { + + data object FastAuth : LoginDialog() + + data class Error( + val errorText: String? = null, + val errorTextResId: Int? = null + ) : LoginDialog() +} diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginScreenState.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginScreenState.kt index cdad4e06..ecc7cbb3 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginScreenState.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginScreenState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable @Immutable data class LoginScreenState( + val showLogo: Boolean, val login: String, val password: String, val isLoading: Boolean, @@ -14,6 +15,7 @@ data class LoginScreenState( companion object { val EMPTY = LoginScreenState( + showLogo = true, login = "", password = "", isLoading = false, diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt index 512aeeea..fc67491d 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt @@ -10,22 +10,17 @@ import dev.meloda.fast.auth.login.model.CaptchaArguments import dev.meloda.fast.auth.login.model.LoginUserBannedArguments import dev.meloda.fast.auth.login.model.LoginValidationArguments import dev.meloda.fast.auth.login.presentation.LoginRoute -import dev.meloda.fast.auth.login.presentation.LogoRoute import dev.meloda.fast.ui.extensions.sharedViewModel import kotlinx.serialization.Serializable @Serializable object Login -@Serializable -object Logo - fun NavGraphBuilder.loginScreen( onNavigateToCaptcha: (CaptchaArguments) -> Unit, onNavigateToValidation: (LoginValidationArguments) -> Unit, onNavigateToMain: () -> Unit, onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit, - onNavigateToCredentials: () -> Unit, navController: NavController ) { composable { backStackEntry -> @@ -45,17 +40,6 @@ fun NavGraphBuilder.loginScreen( viewModel = viewModel ) } - - composable { - LogoRoute( - onNavigateToMain = onNavigateToMain, - onGoNextButtonClicked = onNavigateToCredentials - ) - } -} - -fun NavController.navigateToLogin() { - this.navigate(route = Login) } fun NavBackStackEntry.getValidationResult(): String? { diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt index b85683a7..4a9229d8 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt @@ -1,6 +1,10 @@ package dev.meloda.fast.auth.login.presentation +import android.os.Bundle +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box @@ -28,13 +32,10 @@ import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -42,22 +43,23 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.meloda.fast.auth.login.LoginViewModel +import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.auth.login.model.CaptchaArguments +import dev.meloda.fast.auth.login.model.LoginDialog 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 @@ -74,50 +76,52 @@ fun LoginRoute( onNavigateToValidation: (LoginValidationArguments) -> Unit, validationCode: String?, captchaCode: String?, - viewModel: dev.meloda.fast.auth.login.LoginViewModel = koinViewModel() + viewModel: LoginViewModel = koinViewModel() ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle() val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle() val captchaArguments by viewModel.captchaArguments.collectAsStateWithLifecycle() val validationArguments by viewModel.validationArguments.collectAsStateWithLifecycle() - val loginError by viewModel.loginError.collectAsStateWithLifecycle() + val loginDialog by viewModel.loginDialog.collectAsStateWithLifecycle() - LaunchedEffect(isNeedToOpenMain) { + BackHandler( + enabled = !screenState.showLogo, + onBack = viewModel::onBackPressed + ) + + LaunchedEffect( + isNeedToOpenMain, + userBannedArguments, + captchaArguments, + validationArguments, + validationCode, + captchaCode + ) { if (isNeedToOpenMain) { viewModel.onNavigatedToMain() onNavigateToMain() } - } - LaunchedEffect(userBannedArguments) { userBannedArguments?.let { arguments -> viewModel.onNavigatedToUserBanned() onNavigateToUserBanned(arguments) } - } - LaunchedEffect(captchaArguments) { captchaArguments?.let { arguments -> viewModel.onNavigatedToCaptcha() onNavigateToCaptcha(arguments) } - } - LaunchedEffect(validationArguments) { validationArguments?.let { arguments -> viewModel.onNavigatedToValidation() onNavigateToValidation(arguments) } - } - LaunchedEffect(validationCode) { if (validationCode != null) { viewModel.onValidationCodeReceived(validationCode) } - } - LaunchedEffect(captchaCode) { if (captchaCode != null) { viewModel.onCaptchaCodeReceived(captchaCode) } @@ -125,247 +129,220 @@ fun LoginRoute( LoginScreen( screenState = screenState, - onLoginAutoFilled = viewModel::onLoginInputChanged, - onPasswordAutoFilled = viewModel::onPasswordInputChanged, onLoginInputChanged = viewModel::onLoginInputChanged, onPasswordInputChanged = viewModel::onPasswordInputChanged, onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked, onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, onPasswordFieldGoAction = viewModel::onSignInButtonClicked, - onSignInButtonClicked = viewModel::onSignInButtonClicked + onSignInButtonClicked = viewModel::onSignInButtonClicked, + onLogoLongClicked = viewModel::onLogoLongClicked ) - HandleError( - onDismiss = viewModel::onErrorDialogDismissed, - error = loginError + HandleDialogs( + loginDialog = loginDialog, + onConfirmed = viewModel::onDialogConfirmed, + onDismissed = viewModel::onDialogDismissed ) } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun LoginScreen( screenState: LoginScreenState = LoginScreenState.EMPTY, - onLoginAutoFilled: (String) -> Unit = {}, - onPasswordAutoFilled: (String) -> Unit = {}, onLoginInputChanged: (String) -> Unit = {}, onPasswordInputChanged: (String) -> Unit = {}, onPasswordFieldEnterKeyClicked: () -> Unit = {}, onPasswordVisibilityButtonClicked: () -> Unit = {}, onPasswordFieldGoAction: () -> Unit = {}, - onSignInButtonClicked: () -> Unit = {} + onSignInButtonClicked: () -> Unit = {}, + onLogoLongClicked: () -> Unit = {} ) { - val currentSize = LocalSizeConfig.current + val size = LocalSizeConfig.current val focusManager = LocalFocusManager.current - val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() - var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) } - val showLoginError = screenState.loginError + val titleSpacerSize by animateDpAsState(if (size.isHeightSmall) 24.dp else 58.dp) + val bottomPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp) - val autoFillEmailHandler = autoFillRequestHandler( - autofillTypes = listOf(AutofillType.EmailAddress), - onFill = { value -> - loginText = TextFieldValue(text = value, selection = TextRange(value.length)) - onLoginAutoFilled(value) - } - ) - - var passwordText by remember { mutableStateOf(TextFieldValue(screenState.password)) } - val showPasswordError = screenState.passwordError - - val autoFillPasswordHandler = autoFillRequestHandler( - autofillTypes = listOf(AutofillType.Password), - onFill = { value -> - passwordText = TextFieldValue(text = value, selection = TextRange(value.length)) - onPasswordAutoFilled(value) - } - ) - - val titleStyle = if (currentSize.isWidthSmall) { - MaterialTheme.typography.displayMedium - } else { - MaterialTheme.typography.displayMedium - } - - val titleSpacerSize = if (currentSize.isHeightSmall) { - 24.dp - } else { - 58.dp - } - - val bottomPadding = if (currentSize.isHeightSmall) { - 10.dp - } else { - 30.dp - } + val (loginFocusable, passwordFocusable) = + FocusRequester.createRefs() Scaffold( contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime) ) { padding -> Box( modifier = Modifier - .fillMaxSize() .padding(padding) .padding(top = 30.dp) .padding(horizontal = 30.dp) .padding(bottom = bottomPadding) + .fillMaxSize() ) { - Column( + AnimatedVisibility( + visible = screenState.showLogo, + enter = fadeIn(), + exit = fadeOut() + ) { + Logo(onLogoLongClicked = onLogoLongClicked) + } + + AnimatedVisibility( modifier = Modifier .fillMaxWidth() - .align(Alignment.Center) + .align(Alignment.Center), + visible = !screenState.showLogo, + enter = fadeIn(), + exit = fadeOut() ) { - Text( - text = stringResource(id = UiR.string.sign_in_to_vk), - color = MaterialTheme.colorScheme.onBackground, - style = titleStyle - ) - - Spacer(modifier = Modifier.height(titleSpacerSize)) - - TextField( + Column( modifier = Modifier - .height(58.dp) .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey { - passwordFocusable.requestFocus() - true - } - .handleTabKey { - passwordFocusable.requestFocus() - true - } - .focusRequester(loginFocusable) - .connectNode(handler = autoFillEmailHandler) - .defaultFocusChangeAutoFill(handler = autoFillEmailHandler), - value = loginText, - onValueChange = { newText -> - val text = newText.text - if (text.isEmpty()) { - autoFillEmailHandler.requestVerifyManual() - } + .align(Alignment.Center) + ) { + Text( + text = stringResource(id = UiR.string.sign_in_to_vk), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.displayMedium + ) - loginText = newText - onLoginInputChanged(text) - }, - label = { Text(text = stringResource(id = UiR.string.login_hint)) }, - placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) }, - leadingIcon = { - Icon( - painter = painterResource(id = UiR.drawable.ic_round_person_24), - contentDescription = "Login icon", - tint = if (showLoginError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary + Spacer(modifier = Modifier.height(titleSpacerSize)) + + TextField( + modifier = Modifier + .height(58.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .handleEnterKey { + passwordFocusable.requestFocus() + true } - ) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Email - ), - keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }), - isError = showLoginError, - singleLine = true - ) - AnimatedVisibility(visible = showLoginError) { - TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - TextField( - modifier = Modifier - .height(58.dp) - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey { - focusManager.clearFocus() - onPasswordFieldEnterKeyClicked() - true - } - .focusRequester(passwordFocusable) - .connectNode(handler = autoFillPasswordHandler) - .defaultFocusChangeAutoFill(handler = autoFillPasswordHandler), - value = passwordText, - onValueChange = { newText -> - val text = newText.text - if (text.isEmpty()) { - autoFillPasswordHandler.requestVerifyManual() - } - - passwordText = newText - onPasswordInputChanged(text) - }, - label = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, - placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, - leadingIcon = { - Icon( - painter = painterResource(id = UiR.drawable.round_vpn_key_24), - contentDescription = "Password icon", - tint = if (showPasswordError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary + .handleTabKey { + passwordFocusable.requestFocus() + true } - ) - }, - trailingIcon = { - val imagePainter = painterResource( - id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24 - else UiR.drawable.round_visibility_24 - ) - - IconButton(onClick = onPasswordVisibilityButtonClicked) { + .focusRequester(loginFocusable) + .semantics { + contentType = ContentType.Username + ContentType.EmailAddress + }, + value = screenState.login, + onValueChange = onLoginInputChanged, + label = { Text(text = stringResource(id = UiR.string.login_hint)) }, + placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) }, + leadingIcon = { Icon( - painter = imagePainter, - contentDescription = if (screenState.passwordVisible) "Password visible icon" - else "Password invisible icon" + painter = painterResource(id = UiR.drawable.ic_round_person_24), + contentDescription = "Login icon", + tint = if (screenState.loginError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } ) - } - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go, - keyboardType = KeyboardType.Password - ), - keyboardActions = KeyboardActions( - onGo = { - focusManager.clearFocus() - onPasswordFieldGoAction() - } - ), - isError = showPasswordError, - visualTransformation = if (screenState.passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - singleLine = true - ) - AnimatedVisibility(visible = showPasswordError) { - TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Email + ), + keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }), + isError = screenState.loginError, + singleLine = true + ) + AnimatedVisibility(visible = screenState.loginError) { + TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + modifier = Modifier + .height(58.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .handleEnterKey { + focusManager.clearFocus() + onPasswordFieldEnterKeyClicked() + true + } + .focusRequester(passwordFocusable) + .semantics { contentType = ContentType.Password }, + value = screenState.password, + onValueChange = onPasswordInputChanged, + label = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, + placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, + leadingIcon = { + Icon( + painter = painterResource(id = UiR.drawable.round_vpn_key_24), + contentDescription = "Password icon", + tint = if (screenState.passwordError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } + ) + }, + trailingIcon = { + val imagePainter = painterResource( + id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24 + else UiR.drawable.round_visibility_24 + ) + + IconButton(onClick = onPasswordVisibilityButtonClicked) { + Icon( + painter = imagePainter, + contentDescription = if (screenState.passwordVisible) "Password visible icon" + else "Password invisible icon" + ) + } + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go, + keyboardType = KeyboardType.Password + ), + keyboardActions = KeyboardActions( + onGo = { + focusManager.clearFocus() + onPasswordFieldGoAction() + } + ), + isError = screenState.passwordError, + visualTransformation = if (screenState.passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + singleLine = true + ) + AnimatedVisibility(visible = screenState.passwordError) { + TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) + } } } + Box( modifier = Modifier.align(Alignment.BottomCenter), contentAlignment = Alignment.Center ) { - AnimatedVisibility( - visible = !screenState.isLoading, - enter = fadeIn(), - exit = fadeOut() + FloatingActionButton( + onClick = { + if (!screenState.isLoading) { + focusManager.clearFocus() + onSignInButtonClicked() + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.testTag("sing_in_fab") ) { - FloatingActionButton( - onClick = { - if (!screenState.isLoading) { - focusManager.clearFocus() - onSignInButtonClicked() - } - }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.testTag("sing_in_fab") + AnimatedVisibility( + visible = screenState.isLoading, + enter = fadeIn(), + exit = fadeOut() + ) { + CircularProgressIndicator() + } + + AnimatedVisibility( + visible = !screenState.isLoading, + enter = fadeIn(), + exit = fadeOut() ) { Icon( painter = painterResource(id = UiR.drawable.ic_arrow_end), @@ -374,18 +351,40 @@ fun LoginScreen( ) } } - AnimatedVisibility( - visible = screenState.isLoading, - enter = fadeIn(), - exit = fadeOut() - ) { - CircularProgressIndicator() - } } } } } + +@Composable +fun HandleDialogs( + loginDialog: LoginDialog?, + onConfirmed: (LoginDialog, Bundle) -> Unit = { _, _ -> }, + onDismissed: (LoginDialog) -> Unit = {}, +) { + when (loginDialog) { + null -> Unit + + is LoginDialog.Error -> { + MaterialDialog( + onDismissRequest = { onDismissed(loginDialog) }, + title = stringResource(UiR.string.title_error), + text = loginDialog.errorTextResId?.let { stringResource(it) } + ?: loginDialog.errorText.orEmpty(), + confirmText = stringResource(id = UiR.string.ok) + ) + } + + LoginDialog.FastAuth -> { + SignInAlert( + onDismissRequest = { onDismissed(loginDialog) }, + onConfirmClick = { onConfirmed(loginDialog, bundleOf("token" to it)) } + ) + } + } +} + @Composable fun HandleError( onDismiss: () -> Unit, diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt new file mode 100644 index 00000000..fd328539 --- /dev/null +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt @@ -0,0 +1,83 @@ +package dev.meloda.fast.auth.login.presentation + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.theme.LocalSizeConfig + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun Logo( + modifier: Modifier = Modifier, + onLogoLongClicked: () -> Unit = {} +) { + val size = LocalSizeConfig.current + + val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp) + val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 40 else 40) + val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp) + + Box( + modifier = modifier + .fillMaxSize() + .padding(top = 30.dp) + .padding(horizontal = 30.dp) + .padding(bottom = bottomAdditionalPadding) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_logo_big), + contentDescription = "Application Logo", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .width(iconWidth) + .combinedClickable( + interactionSource = null, + indication = null, + onLongClick = onLogoLongClicked, + onClick = {} + ) + ) + + Spacer(modifier = Modifier.height(46.dp)) + Text( + text = stringResource(id = R.string.fast_messenger), + style = MaterialTheme.typography.displayMedium.copy(fontSize = appNameFontSize.sp), + color = MaterialTheme.colorScheme.onBackground + ) + } + } +} + +@Preview +@Composable +private fun LogoPreview() { + Logo() +} diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt deleted file mode 100644 index d777b0fc..00000000 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt +++ /dev/null @@ -1,229 +0,0 @@ -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 -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize -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 -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -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.BuildConfig -import dev.meloda.fast.ui.components.ActionInvokeDismiss -import dev.meloda.fast.ui.components.MaterialDialog -import dev.meloda.fast.ui.theme.LocalSizeConfig -import org.koin.androidx.compose.koinViewModel -import dev.meloda.fast.ui.R as UiR - -@Composable -fun LogoRoute( - onNavigateToMain: () -> Unit, - onGoNextButtonClicked: () -> Unit, - viewModel: dev.meloda.fast.auth.login.LoginViewModel = koinViewModel() -) { - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle() - val isNeedToShowSignInAlert by viewModel.isNeedToShowFastSignInAlert.collectAsStateWithLifecycle() - - LaunchedEffect(isNeedToOpenMain) { - if (isNeedToOpenMain) { - viewModel.onNavigatedToMain() - onNavigateToMain() - } - } - - LogoScreen( - isLoading = screenState.isLoading, - onLogoLongClicked = viewModel::onLogoLongClicked, - onGoNextButtonClicked = onGoNextButtonClicked - ) - - if (isNeedToShowSignInAlert) { - SignInAlert( - onDismissRequest = viewModel::onFastLogInAlertDismissed, - onConfirmClick = viewModel::onFastLogInAlertConfirmClicked, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun LogoScreen( - isLoading: Boolean = false, - onLogoLongClicked: () -> Unit = {}, - onGoNextButtonClicked: () -> Unit = {} -) { - val currentSize = LocalSizeConfig.current - - Scaffold { padding -> - val topPadding by animateDpAsState( - targetValue = padding.calculateTopPadding(), - label = "topPaddingAnimation" - ) - val bottomPadding by animateDpAsState( - targetValue = padding.calculateBottomPadding(), - label = "bottomPaddingAnimation" - ) - - val endPadding by animateDpAsState( - targetValue = padding.calculateEndPadding(LayoutDirection.Ltr), - label = "endPaddingAnimation" - ) - val startPadding by animateDpAsState( - targetValue = padding.calculateStartPadding(LayoutDirection.Ltr), - label = "startPaddingAnimation" - ) - - val iconWidth = if (currentSize.isWidthSmall) { - 110.dp - } else { - 134.dp - } - - val appNameTextStyle = if (currentSize.isWidthSmall) { - MaterialTheme.typography.displayMedium.copy(fontSize = 40.sp) - } else { - MaterialTheme.typography.displayMedium - } - - val bottomAdditionalPadding = if (currentSize.isHeightSmall) { - 10.dp - } else { - 30.dp - } - - Box( - modifier = Modifier - .fillMaxSize() - .padding( - start = startPadding, - top = topPadding, - end = endPadding, - bottom = bottomPadding - ) - .padding(top = 30.dp) - .padding(horizontal = 30.dp) - .padding(bottom = bottomAdditionalPadding) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - painter = painterResource(id = UiR.drawable.ic_logo_big), - contentDescription = "Application Logo", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .width(iconWidth) - .combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onLongClick = onLogoLongClicked, - onClick = {} - ) - ) - Spacer(modifier = Modifier.height(46.dp)) - Text( - text = stringResource(id = UiR.string.fast_messenger), - style = appNameTextStyle, - color = MaterialTheme.colorScheme.onBackground - ) - } - - AnimatedVisibility( - visible = !isLoading, - modifier = Modifier.align(Alignment.BottomCenter), - enter = fadeIn(), - exit = fadeOut() - ) { - 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() - } - } - } -} - -@Composable -fun SignInAlert( - onDismissRequest: () -> Unit, - onConfirmClick: (token: String) -> Unit -) { - var tokenText by rememberSaveable { - mutableStateOf(BuildConfig.debugToken) - } - - val maxWidthModifier = Modifier.fillMaxWidth() - - MaterialDialog( - onDismissRequest = onDismissRequest, - title = "Fast authorization", - confirmText = stringResource(id = UiR.string.action_authorize), - confirmAction = { onConfirmClick(tokenText) }, - cancelText = stringResource(id = UiR.string.cancel), - actionInvokeDismiss = ActionInvokeDismiss.Always - ) { - Column(modifier = maxWidthModifier) { - OutlinedTextField( - modifier = maxWidthModifier.padding(horizontal = 16.dp), - value = tokenText, - onValueChange = { tokenText = it }, - placeholder = { Text(text = "Access token") }, - label = { Text(text = "Access token") } - ) - } - } -} diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt new file mode 100644 index 00000000..e6f86aca --- /dev/null +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt @@ -0,0 +1,51 @@ +package dev.meloda.fast.auth.login.presentation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.meloda.fast.auth.BuildConfig +import dev.meloda.fast.ui.components.ActionInvokeDismiss +import dev.meloda.fast.ui.components.MaterialDialog + +import dev.meloda.fast.ui.R as UiR + +@Composable +fun SignInAlert( + onDismissRequest: () -> Unit = {}, + onConfirmClick: (token: String) -> Unit = {} +) { + var tokenText by rememberSaveable { + mutableStateOf(BuildConfig.debugToken) + } + + val maxWidthModifier = Modifier.fillMaxWidth() + + MaterialDialog( + onDismissRequest = onDismissRequest, + title = "Fast authorization", + confirmText = stringResource(id = UiR.string.action_authorize), + confirmAction = { onConfirmClick(tokenText) }, + cancelText = stringResource(id = UiR.string.cancel), + actionInvokeDismiss = ActionInvokeDismiss.Always + ) { + Column(modifier = maxWidthModifier) { + OutlinedTextField( + modifier = maxWidthModifier.padding(horizontal = 16.dp), + value = tokenText, + onValueChange = { tokenText = it }, + placeholder = { Text(text = "Access token") }, + label = { Text(text = "Access token") } + ) + } + } +} diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt index cb9e61ec..0e830fa3 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -52,6 +53,7 @@ import dev.meloda.fast.chatmaterials.presentation.materials.LinkMaterialsScreen import dev.meloda.fast.chatmaterials.presentation.materials.PhotoMaterialsScreen import dev.meloda.fast.chatmaterials.presentation.materials.VideoMaterialsScreen import dev.meloda.fast.ui.model.TabItem +import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @@ -118,9 +120,10 @@ fun ChatMaterialsScreen( ) val topBarContainerColor by animateColorAsState( - targetValue = - if (currentTheme.enableBlur || !canScrollBackward) MaterialTheme.colorScheme.surface - else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + targetValue = if (currentTheme.enableBlur || !canScrollBackward) + MaterialTheme.colorScheme.surface + else + MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), label = "toolbarColorAlpha", animationSpec = tween( durationMillis = 200, @@ -204,113 +207,115 @@ fun ChatMaterialsScreen( } } ) { padding -> - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxSize(), - ) { index -> - when (index) { - 0 -> { - val viewModel: ChatMaterialsViewModel = - koinViewModel(named(MaterialType.PHOTO)) - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + CompositionLocalProvider(LocalHazeState provides hazeState) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { index -> + when (index) { + 0 -> { + val viewModel: ChatMaterialsViewModel = + koinViewModel(named(MaterialType.PHOTO)) + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - PhotoMaterialsScreen( - modifier = Modifier, - screenState = screenState, - baseError = baseError, - padding = padding, - onRefresh = viewModel::onRefresh, - onSessionExpiredLogOutButtonClicked = { }, - setCanScrollBackward = { canScrollBackward = it }, - canPaginate = canPaginate, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet, - onPhotoClicked = onPhotoClicked - ) + PhotoMaterialsScreen( + modifier = Modifier, + screenState = screenState, + baseError = baseError, + padding = padding, + onRefresh = viewModel::onRefresh, + onSessionExpiredLogOutButtonClicked = { }, + setCanScrollBackward = { canScrollBackward = it }, + canPaginate = canPaginate, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onPhotoClicked = onPhotoClicked + ) + } + + 1 -> { + val viewModel: ChatMaterialsViewModel = + koinViewModel(named(MaterialType.VIDEO)) + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + VideoMaterialsScreen( + modifier = Modifier, + screenState = screenState, + baseError = baseError, + padding = padding, + onRefresh = viewModel::onRefresh, + onSessionExpiredLogOutButtonClicked = { }, + setCanScrollBackward = { canScrollBackward = it }, + canPaginate = canPaginate, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet + ) + } + + 2 -> { + val viewModel: ChatMaterialsViewModel = + koinViewModel(named(MaterialType.AUDIO)) + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + AudioMaterialsScreen( + modifier = Modifier, + screenState = screenState, + baseError = baseError, + padding = padding, + onRefresh = viewModel::onRefresh, + onSessionExpiredLogOutButtonClicked = { }, + setCanScrollBackward = { canScrollBackward = it }, + canPaginate = canPaginate, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet + ) + } + + 3 -> { + val viewModel: ChatMaterialsViewModel = + koinViewModel(named(MaterialType.FILE)) + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + FileMaterialsScreen( + modifier = Modifier, + screenState = screenState, + baseError = baseError, + padding = padding, + onRefresh = viewModel::onRefresh, + onSessionExpiredLogOutButtonClicked = { }, + setCanScrollBackward = { canScrollBackward = it }, + canPaginate = canPaginate, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet + ) + } + + 4 -> { + val viewModel: ChatMaterialsViewModel = + koinViewModel(named(MaterialType.LINK)) + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + LinkMaterialsScreen( + modifier = Modifier, + screenState = screenState, + baseError = baseError, + padding = padding, + onRefresh = viewModel::onRefresh, + onSessionExpiredLogOutButtonClicked = { }, + setCanScrollBackward = { canScrollBackward = it }, + canPaginate = canPaginate, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet + ) + } + + else -> Unit } - - 1 -> { - val viewModel: ChatMaterialsViewModel = - koinViewModel(named(MaterialType.VIDEO)) - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - - VideoMaterialsScreen( - modifier = Modifier, - screenState = screenState, - baseError = baseError, - padding = padding, - onRefresh = viewModel::onRefresh, - onSessionExpiredLogOutButtonClicked = { }, - setCanScrollBackward = { canScrollBackward = it }, - canPaginate = canPaginate, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet - ) - } - - 2 -> { - val viewModel: ChatMaterialsViewModel = - koinViewModel(named(MaterialType.AUDIO)) - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - - AudioMaterialsScreen( - modifier = Modifier, - screenState = screenState, - baseError = baseError, - padding = padding, - onRefresh = viewModel::onRefresh, - onSessionExpiredLogOutButtonClicked = { }, - setCanScrollBackward = { canScrollBackward = it }, - canPaginate = canPaginate, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet - ) - } - - 3 -> { - val viewModel: ChatMaterialsViewModel = - koinViewModel(named(MaterialType.FILE)) - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - - FileMaterialsScreen( - modifier = Modifier, - screenState = screenState, - baseError = baseError, - padding = padding, - onRefresh = viewModel::onRefresh, - onSessionExpiredLogOutButtonClicked = { }, - setCanScrollBackward = { canScrollBackward = it }, - canPaginate = canPaginate, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet - ) - } - - 4 -> { - val viewModel: ChatMaterialsViewModel = - koinViewModel(named(MaterialType.LINK)) - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - - LinkMaterialsScreen( - modifier = Modifier, - screenState = screenState, - baseError = baseError, - padding = padding, - onRefresh = viewModel::onRefresh, - onSessionExpiredLogOutButtonClicked = { }, - setCanScrollBackward = { canScrollBackward = it }, - canPaginate = canPaginate, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet - ) - } - - else -> Unit } } } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt index eae5105d..f3d302bb 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt @@ -26,18 +26,19 @@ import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.data.UserConfig import dev.meloda.fast.ui.model.api.ConversationOption import dev.meloda.fast.ui.model.api.UiConversation +import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun ConversationsList( + modifier: Modifier = Modifier, onConversationsClick: (Int) -> Unit, onConversationsLongClick: (UiConversation) -> Unit, screenState: ConversationsScreenState, state: LazyListState, maxLines: Int, - modifier: Modifier, onOptionClicked: (UiConversation, ConversationOption) -> Unit, padding: PaddingValues ) { @@ -116,6 +117,7 @@ fun ConversationsList( } Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(LocalBottomPadding.current)) } } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt index bcd350c4..48765b06 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dev.meloda.fast.friends.model.FriendsScreenState import dev.meloda.fast.ui.model.api.UiFriend +import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -98,6 +99,7 @@ fun FriendsList( } Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(LocalBottomPadding.current)) } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 34a2fdaa..d5b552bf 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -71,7 +71,6 @@ interface MessagesHistoryViewModel { val canPaginate: StateFlow fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) - fun onDialogCancelled(dialog: MessageDialog) fun onDialogDismissed(dialog: MessageDialog) fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) @@ -151,7 +150,7 @@ class MessagesHistoryViewModelImpl( } override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) { - messageDialog.setValue { null } + onDialogDismissed(dialog) when (dialog) { is MessageDialog.MessageOptions -> Unit @@ -223,10 +222,6 @@ class MessagesHistoryViewModelImpl( } } - override fun onDialogCancelled(dialog: MessageDialog) { - messageDialog.setValue { null } - } - override fun onDialogDismissed(dialog: MessageDialog) { messageDialog.setValue { null } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt index 4071c451..fed0ab64 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt @@ -1,7 +1,9 @@ package dev.meloda.fast.messageshistory.model +import androidx.compose.runtime.Immutable import dev.meloda.fast.model.api.domain.VkMessage +@Immutable sealed class MessageDialog { data class MessageOptions(val message: VkMessage) : MessageDialog() data class MessagePin(val messageId: Int) : MessageDialog() diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 9bebbf5d..b94b2ff1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -170,7 +170,6 @@ fun MessagesHistoryRoute( screenState = screenState, messageDialog = messageDialog, onConfirmed = viewModel::onDialogConfirmed, - onCancelled = viewModel::onDialogCancelled, onDismissed = viewModel::onDialogDismissed, onItemPicked = viewModel::onDialogItemPicked ) @@ -181,7 +180,6 @@ fun HandleDialogs( screenState: MessagesHistoryScreenState, messageDialog: MessageDialog?, onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> }, - onCancelled: (MessageDialog) -> Unit = {}, onDismissed: (MessageDialog) -> Unit = {}, onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> } ) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a30bd14..bd60723a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,12 +30,8 @@ retrofit = "2.11.0" room = "2.6.1" preference-ktx = "1.2.1" nanokt = "1.2.0" -junitVersion = "1.2.1" -espressoCore = "3.6.1" -appcompat = "1.7.0" androidx-navigation = "2.8.9" serialization = "1.8.0" -rebugger = "1.0.0-rc03" moduleGraph = "2.8.0" [libraries] @@ -69,18 +65,13 @@ preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = nanokt = { module = "com.conena.nanokt:nanokt", version.ref = "nanokt" } nanokt-android = { module = "com.conena.nanokt:nanokt-android", version.ref = "nanokt" } nanokt-jvm = { module = "com.conena.nanokt:nanokt-jvm", version.ref = "nanokt" } -ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } -rebugger = { module = "io.github.theapache64:rebugger-android", version.ref = "rebugger" } - compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } compose-material3 = { module = "androidx.compose.material3:material3" } -compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui = { module = "androidx.compose.ui:ui", version = "1.8.0-rc02" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" } @@ -92,10 +83,7 @@ compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } -koin-core-coroutines = { module = "io.insert-koin:koin-core-coroutines", version.ref = "koin" } -koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } -koin-android-test = { module = "io.insert-koin:koin-android-test", version.ref = "koin" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koin" }