Update API version (#147)

* Bump VK Api version to 5.238
* Implemented new authorization flow (at the moment, without auto re-requesting token)
* Add support for sticker pack preview attachments
* Bump LongPoll to version 19
* Improved messages handling
* Fixed coloring issues
* Cache improvements
* Archive screen with full functionality
* Recomposition fixes
* Markdown support for messages bubbles
* Adjust app name font size based on screen width
* Navigation related improvements
* Add logout functionality
This commit is contained in:
2025-04-04 20:43:59 +03:00
committed by GitHub
parent add67b6f8d
commit 89748b72ed
237 changed files with 4896 additions and 3289 deletions
@@ -59,23 +59,23 @@ fun NavGraphBuilder.authNavGraph(
validationScreen(
onBack = {
navController.navigateUp()
navController.setValidationResult(null)
navController.navigateUp()
},
onResult = { code ->
navController.popBackStack()
navController.setValidationResult(code)
navController.popBackStack()
}
)
captchaScreen(
onBack = {
navController.navigateUp()
navController.setCaptchaResult(null)
navController.navigateUp()
},
onResult = { code ->
navController.popBackStack()
navController.setCaptchaResult(code)
navController.popBackStack()
}
)
@@ -34,7 +34,7 @@ fun NavController.navigateToCaptcha(captchaImageUrl: String) {
}
fun NavController.setCaptchaResult(code: String?) {
this.currentBackStackEntry
this.previousBackStackEntry
?.savedStateHandle
?.set("captcha_code", code)
}
@@ -8,10 +8,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
@@ -24,6 +27,7 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@@ -123,7 +127,9 @@ fun CaptchaScreen(
val focusManager = LocalFocusManager.current
Scaffold { padding ->
Scaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -15,18 +15,21 @@ import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.extensions.updateValue
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.data.State
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.api.auth.AuthRepository
import dev.meloda.fast.data.db.AccountsRepository
import dev.meloda.fast.data.processState
import dev.meloda.fast.data.success
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.OAuthUseCase
import dev.meloda.fast.model.database.AccountEntity
import dev.meloda.fast.network.OAuthErrorDomain
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -39,13 +42,14 @@ interface LoginViewModel {
val screenState: StateFlow<LoginScreenState>
val loginDialog: StateFlow<LoginDialog?>
val validationCode: StateFlow<String?>
val validationArguments: StateFlow<LoginValidationArguments?>
val captchaCode: StateFlow<String?>
val captchaArguments: StateFlow<CaptchaArguments?>
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
val isNeedToOpenMain: StateFlow<Boolean>
val isNeedToClearCaptchaCode: StateFlow<Boolean>
val isNeedToClearValidationCode: StateFlow<Boolean>
fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle)
fun onDialogDismissed(dialog: LoginDialog)
@@ -63,14 +67,15 @@ interface LoginViewModel {
fun onNavigatedToCaptcha()
fun onNavigatedToValidation()
fun onValidationCodeReceived(code: String)
fun onCaptchaCodeReceived(code: String)
fun onLogoLongClicked()
fun onValidationCodeReceived(code: String?)
fun onValidationCodeCleared()
fun onCaptchaCodeReceived(code: String?)
fun onCaptchaCodeCleared()
}
class LoginViewModelImpl(
private val oAuthUseCase: OAuthUseCase,
private val authRepository: AuthRepository,
private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val accountsRepository: AccountsRepository,
private val loginValidator: LoginValidator,
@@ -80,27 +85,41 @@ class LoginViewModelImpl(
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
override val loginDialog = MutableStateFlow<LoginDialog?>(null)
override val validationCode = MutableStateFlow<String?>(null)
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
override val captchaCode = MutableStateFlow<String?>(null)
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
override val isNeedToOpenMain = MutableStateFlow(false)
override val isNeedToClearCaptchaCode = MutableStateFlow(false)
override val isNeedToClearValidationCode = MutableStateFlow(false)
private val validationState: StateFlow<List<LoginValidationResult>> =
screenState.map(loginValidator::validate)
.stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty))
private val captchaSid = MutableStateFlow<String?>(null)
private val captchaCode = MutableStateFlow<String?>(null)
private val validationSid = MutableStateFlow<String?>(null)
private val validationCode = MutableStateFlow<String?>(null)
init {
captchaCode.listenValue(viewModelScope) {
if (it != null) {
login()
}
}
validationCode.listenValue(viewModelScope) {
if (it != null) {
login()
}
}
}
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)
}
}
}
@@ -161,68 +180,20 @@ class LoginViewModelImpl(
validationArguments.update { null }
}
override fun onValidationCodeReceived(code: String) {
override fun onValidationCodeReceived(code: String?) {
validationCode.update { code }
login()
}
override fun onCaptchaCodeReceived(code: String) {
override fun onValidationCodeCleared() {
isNeedToClearValidationCode.update { false }
}
override fun onCaptchaCodeReceived(code: String?) {
captchaCode.update { code }
login()
}
override fun onLogoLongClicked() {
loginDialog.setValue { LoginDialog.FastAuth }
}
private fun fastAuth(token: String) {
var currentAccount = AccountEntity(
userId = -1,
accessToken = token,
fastToken = null,
trustedHash = null
).also { account ->
UserConfig.currentUserId = account.userId
UserConfig.userId = account.userId
UserConfig.accessToken = account.accessToken
UserConfig.fastToken = account.fastToken
UserConfig.trustedHash = account.trustedHash
}
loadUserByIdUseCase(
userId = null,
fields = VkConstants.USER_FIELDS,
nomCase = null
).listenValue(viewModelScope) { state ->
state.processState(
error = {
UserConfig.currentUserId = -1
UserConfig.userId = -1
UserConfig.accessToken = ""
loginDialog.setValue { LoginDialog.Error() }
},
success = { response ->
val actualUserId = requireNotNull(response).id
currentAccount = currentAccount.copy(userId = actualUserId)
UserConfig.userId = actualUserId
UserConfig.currentUserId = actualUserId
startLongPoll()
viewModelScope.launch(Dispatchers.IO) {
accountsRepository.storeAccounts(listOf(currentAccount))
delay(350)
isNeedToOpenMain.update { true }
}
}
)
screenState.setValue { old -> old.copy(isLoading = state.isLoading()) }
}
override fun onCaptchaCodeCleared() {
isNeedToClearCaptchaCode.update { false }
}
private fun login(forceSms: Boolean = false) {
@@ -239,77 +210,120 @@ class LoginViewModelImpl(
processValidation()
if (!validationState.value.contains(LoginValidationResult.Valid)) return
oAuthUseCase.auth(
screenState.updateValue { copy(isLoading = false) }
val currentValidationSid = validationSid.value
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
val currentCaptchaSid = captchaSid.value
val currentCaptchaCode = captchaCode.value?.takeIf { currentCaptchaSid != null }
oAuthUseCase.getSilentToken(
login = currentState.login,
password = currentState.password,
forceSms = forceSms,
validationCode = validationCode.value,
captchaSid = captchaArguments.value?.captchaSid,
captchaKey = captchaCode.value
validationCode = currentValidationCode,
captchaSid = currentCaptchaSid,
captchaKey = currentCaptchaCode
).listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
Log.d("LoginViewModelImpl", "login: error: $error")
validationCode.update { null }
captchaCode.update { null }
screenState.updateValue { copy(isLoading = false) }
captchaSid.setValue { null }
parseError(error)
},
success = { response ->
val userId = response.userId
val accessToken = response.accessToken
val exceptionHandler =
CoroutineExceptionHandler { _, _ ->
screenState.updateValue { copy(isLoading = false) }
loginDialog.setValue { LoginDialog.Error() }
}
if (userId == null || accessToken == null) {
loginDialog.setValue { LoginDialog.Error() }
return@processState
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
val (anonymToken) = authRepository.getAnonymToken(
VkConstants.MESSENGER_APP_ID.toString(),
VkConstants.MESSENGER_APP_SECRET
).success()
val exchangeSilentTokenResponse = authRepository.exchangeSilentToken(
anonymToken = anonymToken,
silentToken = response.silentToken,
silentUuid = response.silentTokenUuid
).success()
val getExchangeTokenResponse =
authRepository.getExchangeToken(exchangeSilentTokenResponse.accessToken)
.success()
val exchangeToken =
getExchangeTokenResponse.usersTokens.firstOrNull {
it.userId == exchangeSilentTokenResponse.userId
}
if (exchangeToken == null) {
screenState.updateValue { copy(isLoading = false) }
loginDialog.setValue { LoginDialog.Error() }
return@launch
}
val userId = exchangeSilentTokenResponse.userId
val accessToken = exchangeSilentTokenResponse.accessToken
// TODO: 30-Mar-25, Danil Nikolaev: get fast's app token
val currentAccount = AccountEntity(
userId = userId,
accessToken = accessToken,
fastToken = null,
trustedHash = response.trustedHash,
exchangeToken = exchangeToken.commonToken
).also { account ->
UserConfig.currentUserId = account.userId
UserConfig.userId = account.userId
UserConfig.accessToken = account.accessToken
UserConfig.fastToken = account.fastToken
UserConfig.trustedHash = account.trustedHash
UserConfig.exchangeToken = account.exchangeToken
}
accountsRepository.storeAccounts(listOf(currentAccount))
startLongPoll()
captchaSid.update { null }
validationSid.update { null }
loadUserByIdUseCase(
userId = userId,
fields = VkConstants.USER_FIELDS,
nomCase = null
).listenValue(viewModelScope) { state ->
state.processState(
any = {
screenState.updateValue { copy(isLoading = false) }
},
error = ::parseError,
success = { user ->
if (user == null) {
loginDialog.update { LoginDialog.Error() }
} else {
screenState.updateValue { copy(login = "", password = "") }
isNeedToOpenMain.update { true }
}
}
)
}
}
loadUserByIdUseCase(
userId = userId,
fields = VkConstants.USER_FIELDS,
nomCase = null
)
val currentAccount = AccountEntity(
userId = userId,
accessToken = accessToken,
fastToken = null,
trustedHash = response.validationHash
).also { account ->
UserConfig.currentUserId = account.userId
UserConfig.userId = account.userId
UserConfig.accessToken = account.accessToken
UserConfig.fastToken = account.fastToken
UserConfig.trustedHash = account.trustedHash
}
startLongPoll()
accountsRepository.storeAccounts(listOf(currentAccount))
captchaArguments.update { null }
captchaCode.update { null }
validationArguments.update { null }
validationCode.update { null }
screenState.setValue { old ->
old.copy(
login = "",
password = "",
)
}
isNeedToOpenMain.update { true }
}
)
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
}
}
private fun parseError(stateError: State.Error): Boolean {
return when (stateError) {
private fun parseError(stateError: State.Error) {
when (stateError) {
is State.Error.OAuthError -> {
when (val error = stateError.error) {
is OAuthErrorDomain.ValidationRequiredError -> {
@@ -321,6 +335,7 @@ class LoginViewModelImpl(
canResendSms = error.validationResend == "sms"
)
validationArguments.update { arguments }
validationSid.update { error.validationSid }
}
is OAuthErrorDomain.CaptchaRequiredError -> {
@@ -329,6 +344,7 @@ class LoginViewModelImpl(
captchaImageUrl = error.captchaImageUrl
)
captchaArguments.update { arguments }
captchaSid.update { error.captchaSid }
}
OAuthErrorDomain.InvalidCredentialsError -> {
@@ -348,12 +364,16 @@ class LoginViewModelImpl(
}
OAuthErrorDomain.WrongValidationCode -> {
isNeedToClearValidationCode.update { true }
validationCode.update { null }
loginDialog.setValue {
LoginDialog.Error(errorText = "Wrong validation code.")
}
}
OAuthErrorDomain.WrongValidationCodeFormat -> {
isNeedToClearValidationCode.update { true }
validationCode.update { null }
loginDialog.setValue {
LoginDialog.Error(errorText = "Wrong validation code format.")
}
@@ -369,18 +389,9 @@ class LoginViewModelImpl(
loginDialog.setValue { LoginDialog.Error() }
}
}
true
}
is State.Error.TestError -> {
loginDialog.setValue {
LoginDialog.Error(errorText = stateError.message)
}
true
}
else -> false
else -> Unit
}
}
@@ -5,8 +5,6 @@ import androidx.compose.runtime.Immutable
@Immutable
sealed class LoginDialog {
data object FastAuth : LoginDialog()
data class Error(
val errorText: String? = null,
val errorTextResId: Int? = null
@@ -1,13 +0,0 @@
package dev.meloda.fast.auth.login.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class LoginError {
data object Unknown : LoginError()
data object WrongCredentials : LoginError()
data object TooManyTries : LoginError()
data object WrongValidationCode : LoginError()
data object WrongValidationCodeFormat : LoginError()
data class SimpleError(val message: String): LoginError()
}
@@ -1,5 +1,8 @@
package dev.meloda.fast.auth.login.navigation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
@@ -27,6 +30,23 @@ fun NavGraphBuilder.loginScreen(
val viewModel: LoginViewModel =
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle()
val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle()
LaunchedEffect(clearValidationCode) {
if (clearValidationCode) {
backStackEntry.savedStateHandle["validation_code"] = null
viewModel.onValidationCodeCleared()
}
}
LaunchedEffect(clearCaptchaCode) {
if (clearCaptchaCode) {
backStackEntry.savedStateHandle["captcha_code"] = null
viewModel.onCaptchaCodeCleared()
}
}
val validationCode = backStackEntry.getValidationResult()
val captchaCode = backStackEntry.getCaptchaResult()
@@ -4,7 +4,6 @@ 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
@@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
@@ -56,16 +54,17 @@ 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.datastore.UserSettings
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.compose.koinInject
import dev.meloda.fast.ui.R as UiR
@Composable
@@ -90,41 +89,36 @@ fun LoginRoute(
onBack = viewModel::onBackPressed
)
LaunchedEffect(
isNeedToOpenMain,
userBannedArguments,
captchaArguments,
validationArguments,
validationCode,
captchaCode
) {
LaunchedEffect(isNeedToOpenMain) {
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)
}
if (validationCode != null) {
viewModel.onValidationCodeReceived(validationCode)
}
if (captchaCode != null) {
viewModel.onCaptchaCodeReceived(captchaCode)
}
}
LaunchedEffect(validationCode) {
viewModel.onValidationCodeReceived(validationCode)
}
LaunchedEffect(captchaCode) {
viewModel.onCaptchaCodeReceived(captchaCode)
}
LoginScreen(
@@ -134,8 +128,7 @@ fun LoginRoute(
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
onSignInButtonClicked = viewModel::onSignInButtonClicked,
onLogoLongClicked = viewModel::onLogoLongClicked
onSignInButtonClicked = viewModel::onSignInButtonClicked
)
HandleDialogs(
@@ -153,8 +146,7 @@ fun LoginScreen(
onPasswordFieldEnterKeyClicked: () -> Unit = {},
onPasswordVisibilityButtonClicked: () -> Unit = {},
onPasswordFieldGoAction: () -> Unit = {},
onSignInButtonClicked: () -> Unit = {},
onLogoLongClicked: () -> Unit = {}
onSignInButtonClicked: () -> Unit = {}
) {
val size = LocalSizeConfig.current
val focusManager = LocalFocusManager.current
@@ -181,7 +173,7 @@ fun LoginScreen(
enter = fadeIn(),
exit = fadeOut()
) {
Logo(onLogoLongClicked = onLogoLongClicked)
Logo()
}
AnimatedVisibility(
@@ -371,79 +363,8 @@ fun HandleDialogs(
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,
error: LoginError?,
) {
when (error) {
null -> Unit
LoginError.Unknown -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = "Unknown error",
confirmText = stringResource(id = UiR.string.ok)
)
}
LoginError.WrongCredentials -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = "Wrong login or password.",
confirmText = stringResource(id = UiR.string.ok)
)
}
LoginError.TooManyTries -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = "Too many tries. Try in another hour or later.",
confirmText = stringResource(id = UiR.string.ok)
)
}
LoginError.WrongValidationCode -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = "Wrong validation code.",
confirmText = stringResource(id = UiR.string.ok)
)
}
LoginError.WrongValidationCodeFormat -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = "Wrong validation code format.",
confirmText = stringResource(id = UiR.string.ok)
)
}
is LoginError.SimpleError -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = error.message,
?: loginDialog.errorText
?: stringResource(UiR.string.unknown_error_occurred),
confirmText = stringResource(id = UiR.string.ok)
)
}
@@ -1,5 +1,6 @@
package dev.meloda.fast.auth.login.presentation
import android.os.Build
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -24,21 +25,22 @@ 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.datastore.UserSettings
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.theme.LocalSizeConfig
import org.koin.compose.koinInject
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Logo(
modifier: Modifier = Modifier,
onLogoLongClicked: () -> Unit = {}
) {
fun Logo(modifier: Modifier = Modifier) {
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 appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 40)
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
val userSettings: UserSettings = koinInject()
Box(
modifier = modifier
.fillMaxSize()
@@ -61,8 +63,14 @@ fun Logo(
.combinedClickable(
interactionSource = null,
indication = null,
onLongClick = onLogoLongClicked,
onClick = {}
onLongClick = null,
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
userSettings.onEnableDynamicColorsChanged(
!userSettings.enableDynamicColors.value
)
}
}
)
)
@@ -1,51 +0,0 @@
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") }
)
}
}
}
@@ -38,7 +38,7 @@ fun NavController.navigateToValidation(arguments: ValidationArguments) {
}
fun NavController.setValidationResult(code: String?) {
this.currentBackStackEntry
this.previousBackStackEntry
?.savedStateHandle
?.set("validation_code", code)
}
@@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
@@ -23,6 +26,7 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@@ -35,10 +39,13 @@ 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.autofill.ContentType
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.TextFieldValue
@@ -146,7 +153,9 @@ fun ValidationScreen(
var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) }
Scaffold { padding ->
Scaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -210,7 +219,8 @@ fun ValidationScreen(
placeholder = { Text(text = "Code") },
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp)),
.clip(RoundedCornerShape(10.dp))
.semantics { contentType = ContentType.SmsOtpCode },
leadingIcon = {
Icon(
painter = painterResource(id = UiR.drawable.round_qr_code_24),