From 9e6b079bf699ef34637913f5aa3db4eed6f1cfe3 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Wed, 9 Jul 2025 17:29:51 +0300 Subject: [PATCH] ability to import/export auth data and some refactoring --- .../meloda/fast/presentation/RootScreen.kt | 13 +- .../dev/meloda/fast/datastore/SettingsKeys.kt | 3 + .../kotlin/dev/meloda/fast/auth/AuthGraph.kt | 2 + .../meloda/fast/auth/login/LoginViewModel.kt | 174 +++++------ .../meloda/fast/auth/login/di/LoginModule.kt | 6 +- .../auth/login/navigation/LoginNavigation.kt | 5 +- .../auth/login/presentation/LoginScreen.kt | 17 +- .../fast/auth/login/presentation/Logo.kt | 5 +- .../conversations/ConversationsViewModel.kt | 174 +++++------ .../conversations/di/ConversationsModule.kt | 6 +- .../navigation/ConversationsNavigation.kt | 6 +- .../meloda/fast/settings/SettingsViewModel.kt | 221 +++++++++----- .../meloda/fast/settings/di/SettingsModule.kt | 4 +- .../meloda/fast/settings/model/HapticType.kt | 12 + .../fast/settings/model/SettingsDialog.kt | 16 + .../settings/model/SettingsScreenState.kt | 2 - .../settings/model/SettingsShowOptions.kt | 14 - .../settings/navigation/SettingsNavigation.kt | 6 +- .../settings/presentation/SettingsDialogs.kt | 282 ++++++++++++++++++ .../settings/presentation/SettingsRoute.kt | 65 ++++ .../settings/presentation/SettingsScreen.kt | 125 +------- 21 files changed, 721 insertions(+), 437 deletions(-) create mode 100644 feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/HapticType.kt create mode 100644 feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsDialog.kt delete mode 100644 feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsShowOptions.kt create mode 100644 feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsDialogs.kt create mode 100644 feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsRoute.kt diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt index 53b5fc1a..f43754df 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -3,6 +3,7 @@ package dev.meloda.fast.presentation import android.content.Intent import android.net.Uri import android.provider.Settings +import androidx.activity.compose.LocalActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -47,6 +48,7 @@ fun RootScreen( navController: NavHostController = rememberNavController(), viewModel: MainViewModel ) { + val activity = LocalActivity.current val context = LocalContext.current val startDestination by viewModel.startDestination.collectAsStateWithLifecycle() val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle() @@ -129,6 +131,7 @@ fun RootScreen( viewModel.onUserAuthenticated() navController.navigateToMain() }, + onNavigateToSettings = navController::navigateToSettings, navController = navController ) @@ -162,7 +165,15 @@ fun RootScreen( settingsScreen( onBack = navController::navigateUp, onLogOutButtonClicked = { navController.navigateToAuth(true) }, - onLanguageItemClicked = navController::navigateToLanguagePicker + onLanguageItemClicked = navController::navigateToLanguagePicker, + onRestartRequired = { + activity?.let { + val intent = Intent(activity, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + activity.startActivity(intent) + activity.finish() + } + } ) languagePickerScreen(onBack = navController::navigateUp) diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt index 30a2b7a8..a111e5fe 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt @@ -50,6 +50,9 @@ object SettingsKeys { const val DEFAULT_ENABLE_HAPTIC = true const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level" const val DEFAULT_NETWORK_LOG_LEVEL = 3 + const val KEY_DEBUG_IMPORT_AUTH_DATA = "debug_import_auth_data" + const val KEY_DEBUG_EXPORT_AUTH_DATA = "debug_export_auth_data" + const val KEY_USE_SYSTEM_FONT = "use_system_font" const val DEFAULT_USE_SYSTEM_FONT = false const val KEY_MORE_ANIMATIONS = "more_animations" 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 74cb253b..20f6785d 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 @@ -23,6 +23,7 @@ object AuthGraph fun NavGraphBuilder.authNavGraph( onNavigateToMain: () -> Unit, + onNavigateToSettings: () -> Unit, navController: NavController ) { navigation(startDestination = Login) { @@ -54,6 +55,7 @@ fun NavGraphBuilder.authNavGraph( ) ) }, + onNavigateToSettings = onNavigateToSettings, 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 1277bc59..f3ba6563 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 @@ -35,49 +35,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -interface LoginViewModel { - val screenState: StateFlow - val loginDialog: StateFlow - - val validationArguments: StateFlow - val captchaArguments: StateFlow - val userBannedArguments: StateFlow - val isNeedToOpenMain: StateFlow - - val isNeedToClearCaptchaCode: StateFlow - val isNeedToClearValidationCode: StateFlow - - fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) - fun onDialogDismissed(dialog: LoginDialog) - - fun onBackPressed() - - fun onPasswordVisibilityButtonClicked() - - fun onLoginInputChanged(newLogin: String) - fun onPasswordInputChanged(newPassword: String) - - fun onSignInButtonClicked() - - fun onLogoClicked() - - fun onNavigatedToMain() - fun onNavigatedToUserBanned() - fun onNavigatedToCaptcha() - fun onNavigatedToValidation() - - fun onValidationCodeReceived(code: String?) - fun onValidationCodeCleared() - fun onCaptchaCodeReceived(code: String?) - fun onCaptchaCodeCleared() -} - -class LoginViewModelImpl( +class LoginViewModel( private val oAuthUseCase: OAuthUseCase, private val authRepository: AuthRepository, private val loadUserByIdUseCase: LoadUserByIdUseCase, @@ -85,18 +49,30 @@ class LoginViewModelImpl( private val loginValidator: LoginValidator, private val longPollController: LongPollController, private val userSettings: UserSettings -) : ViewModel(), LoginViewModel { +) : ViewModel() { + private val _screenState = MutableStateFlow(LoginScreenState.EMPTY) + val screenState = _screenState.asStateFlow() - override val screenState = MutableStateFlow(LoginScreenState.EMPTY) - override val loginDialog = MutableStateFlow(null) + private val _loginDialog = MutableStateFlow(null) + val loginDialog = _loginDialog.asStateFlow() - override val validationArguments = MutableStateFlow(null) - override val captchaArguments = MutableStateFlow(null) - override val userBannedArguments = MutableStateFlow(null) - override val isNeedToOpenMain = MutableStateFlow(false) + private val _validationArguments = MutableStateFlow(null) + val validationArguments = _validationArguments.asStateFlow() - override val isNeedToClearCaptchaCode = MutableStateFlow(false) - override val isNeedToClearValidationCode = MutableStateFlow(false) + private val _captchaArguments = MutableStateFlow(null) + val captchaArguments = _captchaArguments.asStateFlow() + + private val _userBannedArguments = MutableStateFlow(null) + val userBannedArguments = _userBannedArguments.asStateFlow() + + private val _isNeedToOpenMain = MutableStateFlow(false) + val isNeedToOpenMain = _isNeedToOpenMain.asStateFlow() + + private val _isNeedToClearCaptchaCode = MutableStateFlow(false) + val isNeedToClearCaptchaCode = _isNeedToClearCaptchaCode.asStateFlow() + + private val _isNeedToClearValidationCode = MutableStateFlow(false) + val isNeedToClearValidationCode = _isNeedToClearValidationCode.asStateFlow() private val validationState: StateFlow> = screenState.map(loginValidator::validate) @@ -120,7 +96,7 @@ class LoginViewModelImpl( } } - override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) { + fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) { onDialogDismissed(dialog) when (dialog) { @@ -128,20 +104,24 @@ class LoginViewModelImpl( } } - override fun onDialogDismissed(dialog: LoginDialog) { - loginDialog.setValue { null } + fun onDialogDismissed(dialog: LoginDialog) { + when (dialog) { + is LoginDialog.Error -> Unit + } + + _loginDialog.setValue { null } } - override fun onBackPressed() { - screenState.setValue { old -> old.copy(showLogo = true) } + fun onBackPressed() { + _screenState.setValue { old -> old.copy(showLogo = true) } } - override fun onPasswordVisibilityButtonClicked() { - screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) } + fun onPasswordVisibilityButtonClicked() { + _screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) } } - override fun onLoginInputChanged(newLogin: String) { - screenState.setValue { old -> + fun onLoginInputChanged(newLogin: String) { + _screenState.setValue { old -> old.copy( login = newLogin.trim(), loginError = false @@ -149,8 +129,8 @@ class LoginViewModelImpl( } } - override fun onPasswordInputChanged(newPassword: String) { - screenState.setValue { old -> + fun onPasswordInputChanged(newPassword: String) { + _screenState.setValue { old -> old.copy( password = newPassword.trim(), passwordError = false @@ -158,18 +138,18 @@ class LoginViewModelImpl( } } - override fun onSignInButtonClicked() { + fun onSignInButtonClicked() { if (screenState.value.isLoading) return if (screenState.value.showLogo) { - screenState.setValue { old -> old.copy(showLogo = false) } + _screenState.setValue { old -> old.copy(showLogo = false) } return } login() } - override fun onLogoClicked() { + fun onLogoClicked() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { userSettings.onEnableDynamicColorsChanged( !userSettings.enableDynamicColors.value @@ -177,36 +157,36 @@ class LoginViewModelImpl( } } - override fun onNavigatedToMain() { - isNeedToOpenMain.update { false } + fun onNavigatedToMain() { + _isNeedToOpenMain.update { false } } - override fun onNavigatedToUserBanned() { - userBannedArguments.update { null } + fun onNavigatedToUserBanned() { + _userBannedArguments.update { null } } - override fun onNavigatedToCaptcha() { - captchaArguments.update { null } + fun onNavigatedToCaptcha() { + _captchaArguments.update { null } } - override fun onNavigatedToValidation() { - validationArguments.update { null } + fun onNavigatedToValidation() { + _validationArguments.update { null } } - override fun onValidationCodeReceived(code: String?) { + fun onValidationCodeReceived(code: String?) { validationCode.update { code } } - override fun onValidationCodeCleared() { - isNeedToClearValidationCode.update { false } + fun onValidationCodeCleared() { + _isNeedToClearValidationCode.update { false } } - override fun onCaptchaCodeReceived(code: String?) { + fun onCaptchaCodeReceived(code: String?) { captchaCode.update { code } } - override fun onCaptchaCodeCleared() { - isNeedToClearCaptchaCode.update { false } + fun onCaptchaCodeCleared() { + _isNeedToClearCaptchaCode.update { false } } private fun login(forceSms: Boolean = false) { @@ -223,7 +203,7 @@ class LoginViewModelImpl( processValidation() if (!validationState.value.contains(LoginValidationResult.Valid)) return - screenState.updateValue { copy(isLoading = true) } + _screenState.updateValue { copy(isLoading = true) } val currentValidationSid = validationSid.value val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null } @@ -242,7 +222,7 @@ class LoginViewModelImpl( error = { error -> Log.d("LoginViewModelImpl", "login: error: $error") - screenState.updateValue { copy(isLoading = false) } + _screenState.updateValue { copy(isLoading = false) } captchaSid.setValue { null } parseError(error) @@ -250,8 +230,8 @@ class LoginViewModelImpl( success = { response -> val exceptionHandler = CoroutineExceptionHandler { _, _ -> - screenState.updateValue { copy(isLoading = false) } - loginDialog.setValue { LoginDialog.Error() } + _screenState.updateValue { copy(isLoading = false) } + _loginDialog.setValue { LoginDialog.Error() } } viewModelScope.launch(Dispatchers.IO + exceptionHandler) { @@ -277,8 +257,8 @@ class LoginViewModelImpl( } if (exchangeToken == null) { - screenState.updateValue { copy(isLoading = false) } - loginDialog.setValue { LoginDialog.Error() } + _screenState.updateValue { copy(isLoading = false) } + _loginDialog.setValue { LoginDialog.Error() } return@launch } @@ -316,15 +296,15 @@ class LoginViewModelImpl( ).listenValue(viewModelScope) { state -> state.processState( any = { - screenState.updateValue { copy(isLoading = false) } + _screenState.updateValue { copy(isLoading = false) } }, error = ::parseError, success = { user -> if (user == null) { - loginDialog.update { LoginDialog.Error() } + _loginDialog.update { LoginDialog.Error() } } else { - screenState.updateValue { copy(login = "", password = "") } - isNeedToOpenMain.update { true } + _screenState.updateValue { copy(login = "", password = "") } + _isNeedToOpenMain.update { true } } } ) @@ -347,7 +327,7 @@ class LoginViewModelImpl( validationType = error.validationType.value, canResendSms = error.validationResend == "sms" ) - validationArguments.update { arguments } + _validationArguments.update { arguments } validationSid.update { error.validationSid } } @@ -356,12 +336,12 @@ class LoginViewModelImpl( captchaSid = error.captchaSid, captchaImageUrl = error.captchaImageUrl ) - captchaArguments.update { arguments } + _captchaArguments.update { arguments } captchaSid.update { error.captchaSid } } OAuthErrorDomain.InvalidCredentialsError -> { - loginDialog.setValue { + _loginDialog.setValue { LoginDialog.Error(errorText = "Wrong login or password.") } } @@ -373,33 +353,33 @@ class LoginViewModelImpl( restoreUrl = error.restoreUrl, accessToken = error.accessToken ) - userBannedArguments.update { arguments } + _userBannedArguments.update { arguments } } OAuthErrorDomain.WrongValidationCode -> { - isNeedToClearValidationCode.update { true } + _isNeedToClearValidationCode.update { true } validationCode.update { null } - loginDialog.setValue { + _loginDialog.setValue { LoginDialog.Error(errorText = "Wrong validation code.") } } OAuthErrorDomain.WrongValidationCodeFormat -> { - isNeedToClearValidationCode.update { true } + _isNeedToClearValidationCode.update { true } validationCode.update { null } - loginDialog.setValue { + _loginDialog.setValue { LoginDialog.Error(errorText = "Wrong validation code format.") } } OAuthErrorDomain.TooManyTriesError -> { - loginDialog.setValue { + _loginDialog.setValue { LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.") } } OAuthErrorDomain.UnknownError -> { - loginDialog.setValue { LoginDialog.Error() } + _loginDialog.setValue { LoginDialog.Error() } } } } @@ -412,11 +392,11 @@ class LoginViewModelImpl( validationState.value.forEach { result -> when (result) { LoginValidationResult.LoginEmpty -> { - screenState.setValue { old -> old.copy(loginError = true) } + _screenState.setValue { old -> old.copy(loginError = true) } } LoginValidationResult.PasswordEmpty -> { - screenState.setValue { old -> old.copy(passwordError = true) } + _screenState.setValue { old -> old.copy(passwordError = true) } } LoginValidationResult.Empty -> Unit diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt index de8e3c95..04465862 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt @@ -1,9 +1,9 @@ package dev.meloda.fast.auth.login.di -import dev.meloda.fast.auth.login.LoginViewModelImpl +import dev.meloda.fast.auth.login.LoginViewModel +import dev.meloda.fast.auth.login.validation.LoginValidator import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.domain.OAuthUseCaseImpl -import dev.meloda.fast.auth.login.validation.LoginValidator import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind @@ -11,6 +11,6 @@ import org.koin.dsl.module val loginModule = module { singleOf(::LoginValidator) - viewModelOf(::LoginViewModelImpl) bind dev.meloda.fast.auth.login.LoginViewModel::class + viewModelOf(::LoginViewModel) singleOf(::OAuthUseCaseImpl) bind OAuthUseCase::class } 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 424f0216..ae2df718 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 @@ -8,7 +8,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable 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.LoginUserBannedArguments import dev.meloda.fast.auth.login.model.LoginValidationArguments @@ -24,11 +23,12 @@ fun NavGraphBuilder.loginScreen( onNavigateToValidation: (LoginValidationArguments) -> Unit, onNavigateToMain: () -> Unit, onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit, + onNavigateToSettings: () -> Unit, navController: NavController ) { composable { backStackEntry -> val viewModel: LoginViewModel = - backStackEntry.sharedViewModel(navController = navController) + backStackEntry.sharedViewModel(navController = navController) val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle() val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle() @@ -55,6 +55,7 @@ fun NavGraphBuilder.loginScreen( onNavigateToMain = onNavigateToMain, onNavigateToCaptcha = onNavigateToCaptcha, onNavigateToValidation = onNavigateToValidation, + onNavigateToSettings = onNavigateToSettings, validationCode = validationCode, captchaCode = captchaCode, viewModel = viewModel 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 501e1e3e..599fb27c 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 @@ -39,6 +39,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1 +import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -56,7 +58,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri 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.LoginScreenState @@ -76,9 +77,10 @@ fun LoginRoute( onNavigateToMain: () -> Unit, onNavigateToCaptcha: (CaptchaArguments) -> Unit, onNavigateToValidation: (LoginValidationArguments) -> Unit, + onNavigateToSettings: () -> Unit, validationCode: String?, captchaCode: String?, - viewModel: LoginViewModel = koinViewModel() + viewModel: LoginViewModel = koinViewModel() ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle() @@ -132,7 +134,8 @@ fun LoginRoute( onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, onPasswordFieldGoAction = viewModel::onSignInButtonClicked, onSignInButtonClicked = viewModel::onSignInButtonClicked, - onLogoClicked = viewModel::onLogoClicked + onLogoClicked = viewModel::onLogoClicked, + onLogoLongClicked = onNavigateToSettings ) HandleDialogs( @@ -151,7 +154,8 @@ fun LoginScreen( onPasswordVisibilityButtonClicked: () -> Unit = {}, onPasswordFieldGoAction: () -> Unit = {}, onSignInButtonClicked: () -> Unit = {}, - onLogoClicked: () -> Unit = {} + onLogoClicked: () -> Unit = {}, + onLogoLongClicked: () -> Unit = {} ) { val context = LocalContext.current val size = LocalSizeConfig.current @@ -185,7 +189,10 @@ fun LoginScreen( exit = fadeOut(), label = "Logo visibility" ) { - Logo(onLogoClicked = onLogoClicked) + Logo( + onLogoClicked = onLogoClicked, + onLogoLongClicked = onLogoLongClicked + ) } AnimatedVisibility( 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 index 85f3f498..8e64c4a0 100644 --- 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 @@ -30,7 +30,8 @@ import dev.meloda.fast.ui.theme.LocalSizeConfig @Composable fun Logo( modifier: Modifier = Modifier, - onLogoClicked: () -> Unit = {} + onLogoClicked: () -> Unit = {}, + onLogoLongClicked: () -> Unit = {} ) { val size = LocalSizeConfig.current @@ -60,7 +61,7 @@ fun Logo( .combinedClickable( interactionSource = null, indication = null, - onLongClick = null, + onLongClick = onLogoLongClicked, onClick = onLogoClicked ) ) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index c2527088..bdfa52e0 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -38,54 +38,15 @@ import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -interface ConversationsViewModel { - - val screenState: StateFlow - val navigation: StateFlow - val dialog: StateFlow - - val conversations: StateFlow> - val uiConversations: StateFlow> - - val baseError: StateFlow - - val currentOffset: StateFlow - val canPaginate: StateFlow - - fun onNavigationConsumed() - - fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) - fun onDialogDismissed(dialog: ConversationDialog) - fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) - - fun onErrorButtonClicked() - - fun onPaginationConditionsMet() - - fun onOptionClicked(conversation: UiConversation, option: ConversationOption) - - fun onRefresh() - - fun onConversationItemClick(conversation: UiConversation) - fun onConversationItemLongClick(conversation: UiConversation) - - fun onErrorConsumed() - - fun setScrollIndex(index: Int) - fun setScrollOffset(offset: Int) - - fun onCreateChatButtonClicked() -} - -class ConversationsViewModelImpl( +class ConversationsViewModel( + updatesParser: LongPollUpdatesParser, private val filter: ConversationsFilter, - private val updatesParser: LongPollUpdatesParser, private val conversationsUseCase: ConversationsUseCase, private val messagesUseCase: MessagesUseCase, private val resources: Resources, @@ -93,23 +54,34 @@ class ConversationsViewModelImpl( private val imageLoader: ImageLoader, private val applicationContext: Context, private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase -) : ConversationsViewModel, ViewModel() { +) : ViewModel() { + private val _screenState = MutableStateFlow(ConversationsScreenState.EMPTY) + val screenState = _screenState.asStateFlow() - override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY) - override val navigation = MutableStateFlow(null) - override val dialog = MutableStateFlow(null) + private val _navigation = MutableStateFlow(null) + val navigation = _navigation.asStateFlow() - override val conversations = MutableStateFlow>(emptyList()) - override val uiConversations = MutableStateFlow>(emptyList()) + private val _dialog = MutableStateFlow(null) + val dialog = _dialog.asStateFlow() + + private val _conversations = MutableStateFlow>(emptyList()) + val conversations = _conversations.asStateFlow() + + private val _uiConversations = MutableStateFlow>(emptyList()) + val uiConversations = _uiConversations.asStateFlow() private val pinnedConversationsCount = conversations.map { conversations -> conversations.count(VkConversation::isPinned) }.stateIn(viewModelScope, SharingStarted.Eagerly, 0) - override val baseError = MutableStateFlow(null) + private val _baseError = MutableStateFlow(null) + val baseError = _baseError.asStateFlow() - override val currentOffset = MutableStateFlow(0) - override val canPaginate = MutableStateFlow(false) + private val _currentOffset = MutableStateFlow(0) + val currentOffset = _currentOffset.asStateFlow() + + private val _canPaginate = MutableStateFlow(false) + val canPaginate = _canPaginate.asStateFlow() private val expandedConversationId = MutableStateFlow(0L) @@ -118,7 +90,7 @@ class ConversationsViewModelImpl( private val interactionsTimers = hashMapOf() init { - screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) } + _screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) } loadConversations() @@ -137,11 +109,11 @@ class ConversationsViewModelImpl( } } - override fun onNavigationConsumed() { - navigation.setValue { null } + fun onNavigationConsumed() { + _navigation.setValue { null } } - override fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) { + fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) { onDialogDismissed(dialog) when (dialog) { @@ -170,11 +142,11 @@ class ConversationsViewModelImpl( syncUiConversation() } - override fun onDialogDismissed(dialog: ConversationDialog) { - this.dialog.setValue { null } + fun onDialogDismissed(dialog: ConversationDialog) { + _dialog.setValue { null } } - override fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) { + fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) { when (dialog) { is ConversationDialog.ConversationDelete -> Unit is ConversationDialog.ConversationPin -> Unit @@ -184,7 +156,7 @@ class ConversationsViewModelImpl( } } - override fun onErrorButtonClicked() { + fun onErrorButtonClicked() { when (baseError.value) { null -> Unit @@ -197,22 +169,22 @@ class ConversationsViewModelImpl( } } - override fun onPaginationConditionsMet() { - currentOffset.update { conversations.value.size } + fun onPaginationConditionsMet() { + _currentOffset.update { conversations.value.size } loadConversations() } - override fun onRefresh() { + fun onRefresh() { onErrorConsumed() loadConversations(offset = 0) } - override fun onConversationItemClick(conversation: UiConversation) { + fun onConversationItemClick(conversation: UiConversation) { collapseConversations() - navigation.setValue { ConversationNavigation.MessagesHistory(peerId = conversation.id) } + _navigation.setValue { ConversationNavigation.MessagesHistory(peerId = conversation.id) } } - override fun onConversationItemLongClick(conversation: UiConversation) { + fun onConversationItemLongClick(conversation: UiConversation) { expandedConversationId.setValue { if (conversation.isExpanded) 0 else conversation.id @@ -220,13 +192,13 @@ class ConversationsViewModelImpl( syncUiConversation() } - override fun onOptionClicked( + fun onOptionClicked( conversation: UiConversation, option: ConversationOption ) { when (option) { ConversationOption.Delete -> { - dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) } + _dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) } } ConversationOption.MarkAsRead -> { @@ -240,37 +212,37 @@ class ConversationsViewModelImpl( } ConversationOption.Pin -> { - dialog.setValue { ConversationDialog.ConversationPin(conversation.id) } + _dialog.setValue { ConversationDialog.ConversationPin(conversation.id) } } ConversationOption.Unpin -> { - dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) } + _dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) } } ConversationOption.Archive -> { - dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) } + _dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) } } ConversationOption.Unarchive -> { - dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) } + _dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) } } } } - override fun onErrorConsumed() { - baseError.setValue { null } + fun onErrorConsumed() { + _baseError.setValue { null } } - override fun setScrollIndex(index: Int) { - screenState.setValue { old -> old.copy(scrollIndex = index) } + fun setScrollIndex(index: Int) { + _screenState.setValue { old -> old.copy(scrollIndex = index) } } - override fun setScrollOffset(offset: Int) { - screenState.setValue { old -> old.copy(scrollOffset = offset) } + fun setScrollOffset(offset: Int) { + _screenState.setValue { old -> old.copy(scrollOffset = offset) } } - override fun onCreateChatButtonClicked() { - navigation.setValue { ConversationNavigation.CreateChat } + fun onCreateChatButtonClicked() { + _navigation.setValue { ConversationNavigation.CreateChat } } private fun collapseConversations() { @@ -289,7 +261,7 @@ class ConversationsViewModelImpl( state.processState( error = { error -> val newBaseError = VkUtils.parseError(error) - baseError.update { newBaseError } + _baseError.update { newBaseError } }, success = { response -> val conversations = response @@ -304,7 +276,7 @@ class ConversationsViewModelImpl( val paginationExhausted = !itemsCountSufficient && this.conversations.value.isNotEmpty() - screenState.updateValue { + _screenState.updateValue { copy(isPaginationExhausted = paginationExhausted) } @@ -321,13 +293,13 @@ class ConversationsViewModelImpl( conversationsUseCase.storeConversations(response) - this.conversations.emit(fullConversations) + _conversations.emit(fullConversations) syncUiConversation() - canPaginate.setValue { itemsCountSufficient } + _canPaginate.setValue { itemsCountSufficient } } ) - screenState.setValue { old -> + _screenState.setValue { old -> old.copy( isLoading = offset == 0 && state.isLoading(), isPaginating = offset > 0 && state.isLoading() @@ -347,11 +319,11 @@ class ConversationsViewModelImpl( ?: return@processState newConversations.removeAt(conversationIndex) - conversations.update { newConversations.sorted() } + _conversations.update { newConversations.sorted() } syncUiConversation() } ) - screenState.emit(screenState.value.copy(isLoading = state.isLoading())) + _screenState.emit(screenState.value.copy(isLoading = state.isLoading())) } } @@ -374,7 +346,7 @@ class ConversationsViewModelImpl( } ) - screenState.setValue { old -> old.copy(isLoading = state.isLoading()) } + _screenState.setValue { old -> old.copy(isLoading = state.isLoading()) } } } @@ -420,7 +392,7 @@ class ConversationsViewModelImpl( .copy(lastMessage = message) newConversations.add(pinnedConversationsCount.value, conversation) - conversations.update { newConversations.sorted() } + _conversations.update { newConversations.sorted() } syncUiConversation() } ) @@ -461,7 +433,7 @@ class ConversationsViewModelImpl( newConversations.add(toPosition, newConversation) } - conversations.update { newConversations.sorted() } + _conversations.update { newConversations.sorted() } syncUiConversation() } } @@ -480,7 +452,7 @@ class ConversationsViewModelImpl( lastMessageId = message.id, lastCmId = message.cmId ) - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() } } @@ -500,7 +472,7 @@ class ConversationsViewModelImpl( unreadCount = event.unreadCount ) - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() } } @@ -520,7 +492,7 @@ class ConversationsViewModelImpl( unreadCount = event.unreadCount ) - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() } } @@ -541,7 +513,7 @@ class ConversationsViewModelImpl( interactionIds = userIds ) - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() interactionsTimers[peerId]?.let { interactionJob -> @@ -583,7 +555,7 @@ class ConversationsViewModelImpl( interactionIds = emptyList() ) - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() interactionJob.timerJob.cancel() @@ -601,7 +573,7 @@ class ConversationsViewModelImpl( newConversations[conversationIndex] = newConversations[conversationIndex].copy(majorId = event.majorId) - conversations.setValue { newConversations.sorted() } + _conversations.setValue { newConversations.sorted() } syncUiConversation() } } @@ -617,7 +589,7 @@ class ConversationsViewModelImpl( newConversations[conversationIndex] = newConversations[conversationIndex].copy(minorId = event.minorId) - conversations.setValue { newConversations.sorted() } + _conversations.setValue { newConversations.sorted() } syncUiConversation() } } @@ -632,7 +604,7 @@ class ConversationsViewModelImpl( } else { newConversations.removeAt(conversationIndex) - conversations.setValue { newConversations.sorted() } + _conversations.setValue { newConversations.sorted() } syncUiConversation() } } @@ -655,7 +627,7 @@ class ConversationsViewModelImpl( newConversations.removeAt(index) } - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() } @@ -669,7 +641,7 @@ class ConversationsViewModelImpl( newConversations.add(pinnedConversationsCount.value, conversation) } - conversations.update { newConversations.sorted() } + _conversations.update { newConversations.sorted() } syncUiConversation() } } @@ -691,7 +663,7 @@ class ConversationsViewModelImpl( newConversations[conversationIndex] = newConversations[conversationIndex].copy(inRead = startMessageId) - conversations.update { newConversations } + _conversations.update { newConversations } syncUiConversation() } ) @@ -763,7 +735,7 @@ class ConversationsViewModelImpl( options = options.toImmutableList() ) } - uiConversations.setValue { newUiConversations } + _uiConversations.setValue { newUiConversations } return newUiConversations } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt index cb3fe4dc..9fde6c17 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt @@ -1,6 +1,6 @@ package dev.meloda.fast.conversations.di -import dev.meloda.fast.conversations.ConversationsViewModelImpl +import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.domain.ConversationsUseCase import dev.meloda.fast.domain.ConversationsUseCaseImpl import dev.meloda.fast.model.ConversationsFilter @@ -22,8 +22,8 @@ val conversationsModule = module { singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class } -private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModelImpl { - return ConversationsViewModelImpl( +private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModel { + return ConversationsViewModel( filter = filter, updatesParser = get(), conversationsUseCase = get(), diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt index 91ad0fd0..4857fdc9 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt @@ -4,7 +4,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navigation -import dev.meloda.fast.conversations.ConversationsViewModelImpl +import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.conversations.presentation.ConversationsRoute import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.ConversationsFilter @@ -33,7 +33,7 @@ fun NavGraphBuilder.conversationsGraph( navigation( startDestination = Conversations ) { - val conversationsViewModel: ConversationsViewModelImpl = with(activity) { + val conversationsViewModel: ConversationsViewModel = with(activity) { getViewModel(qualifier = named(ConversationsFilter.ALL)) } composable { @@ -53,7 +53,7 @@ fun NavGraphBuilder.conversationsGraph( ConversationsRoute( viewModel = with(activity) { - getViewModel( + getViewModel( qualifier = named(ConversationsFilter.ARCHIVE) ) }, diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt index 5540ae09..9b0743e3 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt @@ -2,11 +2,14 @@ package dev.meloda.fast.settings import android.content.res.Resources import android.os.Build +import android.os.Bundle import androidx.appcompat.app.AppCompatDelegate -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dev.meloda.fast.common.LongPollController +import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.findWithIndex +import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.LogLevel @@ -15,52 +18,47 @@ import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.parseString import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.db.AccountsRepository +import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.UserSettings -import dev.meloda.fast.domain.AuthUseCase +import dev.meloda.fast.domain.GetCurrentAccountUseCase +import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.model.database.AccountEntity +import dev.meloda.fast.settings.model.HapticType +import dev.meloda.fast.settings.model.SettingsDialog import dev.meloda.fast.settings.model.SettingsItem import dev.meloda.fast.settings.model.SettingsScreenState -import dev.meloda.fast.settings.model.SettingsShowOptions import dev.meloda.fast.settings.model.TextProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import dev.meloda.fast.ui.R as UiR -interface SettingsViewModel { - - val screenState: StateFlow - val hapticType: StateFlow - - fun onLogOutAlertDismissed() - suspend fun onLogOutAlertPositiveClick() - - fun onPerformCrashAlertDismissed() - fun onPerformCrashPositiveButtonClicked() - - fun onSettingsItemClicked(key: String) - fun onSettingsItemLongClicked(key: String) - fun onSettingsItemChanged(key: String, newValue: Any?) - - fun onHapticPerformed() -} - -class SettingsViewModelImpl( - private val authUseCase: AuthUseCase, +class SettingsViewModel( + private val loadUserByIdUseCase: LoadUserByIdUseCase, private val accountsRepository: AccountsRepository, + private val getCurrentAccountUseCase: GetCurrentAccountUseCase, private val userSettings: UserSettings, private val resources: Resources, private val longPollController: LongPollController -) : SettingsViewModel, ViewModel() { +) : ViewModel() { - override val screenState = MutableStateFlow(SettingsScreenState.EMPTY) - override val hapticType = MutableStateFlow(null) + private val _screenState = MutableStateFlow(SettingsScreenState.EMPTY) + val screenState = _screenState.asStateFlow() + + private val _hapticType = MutableStateFlow(null) + val hapticType = _hapticType.asStateFlow() + + private val _dialog = MutableStateFlow(null) + val dialog = _dialog.asStateFlow() + + private val _isNeedToRestart = MutableStateFlow(false) + val isNeedToRestart = _isNeedToRestart.asStateFlow() private val settings = MutableStateFlow>>(emptyList()) @@ -68,24 +66,87 @@ class SettingsViewModelImpl( createSettings() } - override fun onLogOutAlertDismissed() { - emitShowOptions { old -> old.copy(showLogOut = false) } + fun onDialogConfirmed(dialog: SettingsDialog, bundle: Bundle) { + onDialogDismissed(dialog) + + when (dialog) { + is SettingsDialog.LogOut -> onLogOutAlertPositiveClick() + is SettingsDialog.PerformCrash -> onPerformCrashPositiveButtonClicked() + + is SettingsDialog.ImportAuthData -> { + val accessToken = bundle.getString("ACCESS_TOKEN") ?: return + val exchangeToken = bundle.getString("EXCHANGE_TOKEN") + val trustedHash = bundle.getString("TRUSTED_HASH") + + viewModelScope.launch(Dispatchers.IO) { + val oldToken = UserConfig.accessToken + + UserConfig.accessToken = accessToken + + loadUserByIdUseCase( + userId = null, + fields = VkConstants.USER_FIELDS + ).listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + UserConfig.accessToken = oldToken + }, + success = { user -> + if (user == null) return@listenValue + + UserConfig.currentUserId = user.id + + val account = getCurrentAccountUseCase() + ?.copy( + userId = user.id, + accessToken = accessToken, + fastToken = null, + exchangeToken = exchangeToken, + trustedHash = trustedHash + ) ?: AccountEntity( + userId = user.id, + accessToken = accessToken, + fastToken = null, + trustedHash = trustedHash, + exchangeToken = exchangeToken + ) + + accountsRepository.storeAccounts(listOf(account)) + + _isNeedToRestart.setValue { true } + } + ) + } + } + } + + is SettingsDialog.ExportAuthData -> Unit + } } - override suspend fun onLogOutAlertPositiveClick() { - withContext(Dispatchers.IO) { + fun onDialogDismissed(dialog: SettingsDialog) { + when (dialog) { + is SettingsDialog.LogOut -> Unit + is SettingsDialog.PerformCrash -> Unit + is SettingsDialog.ImportAuthData -> Unit + is SettingsDialog.ExportAuthData -> Unit + } + + _dialog.setValue { null } + } + + fun onDialogItemPicked(dialog: SettingsDialog, bundle: Bundle) { + when (dialog) { + is SettingsDialog.LogOut -> Unit + is SettingsDialog.PerformCrash -> Unit + is SettingsDialog.ImportAuthData -> Unit + is SettingsDialog.ExportAuthData -> Unit + } + } + + fun onLogOutAlertPositiveClick() { + viewModelScope.launch(Dispatchers.IO) { val tasks = listOf( -// async { -// suspendCoroutine { continuation -> -// authUseCase.logout().listenValue(viewModelScope) { state -> -// state.processState( -// any = { continuation.resume(Unit) }, -// success = {}, -// error = {} -// ) -// } -// } -// }, async { accountsRepository.storeAccounts( listOf( @@ -106,22 +167,32 @@ class SettingsViewModelImpl( } } - override fun onPerformCrashAlertDismissed() { - emitShowOptions { old -> old.copy(showPerformCrash = false) } - } - - override fun onPerformCrashPositiveButtonClicked() { + fun onPerformCrashPositiveButtonClicked() { throw Exception("Test exception") } - override fun onSettingsItemClicked(key: String) { + fun onSettingsItemClicked(key: String) { when (key) { SettingsKeys.KEY_ACCOUNT_LOGOUT -> { - emitShowOptions { old -> old.copy(showLogOut = true) } + _dialog.setValue { SettingsDialog.LogOut } } SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> { - emitShowOptions { old -> old.copy(showPerformCrash = true) } + _dialog.setValue { SettingsDialog.PerformCrash } + } + + SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA -> { + _dialog.setValue { SettingsDialog.ImportAuthData } + } + + SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA -> { + _dialog.setValue { + SettingsDialog.ExportAuthData( + accessToken = UserConfig.accessToken, + exchangeToken = UserConfig.exchangeToken, + trustedHash = UserConfig.trustedHash + ) + } } SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> { @@ -132,13 +203,13 @@ class SettingsViewModelImpl( createSettings() - hapticType.update { HapticType.REJECT } - screenState.setValue { old -> old.copy(showDebugOptions = false) } + _hapticType.update { HapticType.REJECT } + _screenState.setValue { old -> old.copy(showDebugOptions = false) } } } } - override fun onSettingsItemLongClicked(key: String) { + fun onSettingsItemLongClicked(key: String) { when (key) { SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> { if (AppSettings.Debug.showDebugCategory) return @@ -148,18 +219,18 @@ class SettingsViewModelImpl( createSettings() - hapticType.update { HapticType.LONG_PRESS } - screenState.setValue { old -> old.copy(showDebugOptions = true) } + _hapticType.update { HapticType.LONG_PRESS } + _screenState.setValue { old -> old.copy(showDebugOptions = true) } } } } - override fun onSettingsItemChanged(key: String, newValue: Any?) { + fun onSettingsItemChanged(key: String, newValue: Any?) { settings.value.findWithIndex { it.key == key }?.let { (index, item) -> item.updateValue(newValue) item.updateText() - screenState.setValue { old -> + _screenState.setValue { old -> old.copy( settings = old.settings.toMutableList().apply { this[index] = item.asPresentation(resources) @@ -240,13 +311,8 @@ class SettingsViewModelImpl( } } - override fun onHapticPerformed() { - hapticType.update { null } - } - - private fun emitShowOptions(function: (SettingsShowOptions) -> SettingsShowOptions) { - val newShowOptions = function.invoke(screenState.value.showOptions) - screenState.setValue { old -> old.copy(showOptions = newShowOptions) } + fun onHapticPerformed() { + _hapticType.update { null } } private fun createSettings() { @@ -446,6 +512,18 @@ class SettingsViewModelImpl( } } + val debugImportAuthData = SettingsItem.TitleText( + key = SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA, + title = UiText.Simple("Import auth data"), + text = UiText.Simple("App will be restarted") + ) + val debugExportAuthData = SettingsItem.TitleText( + key = SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA, + title = UiText.Simple("Export auth data"), + text = UiText.Simple("Be careful with this data. If another person gets it, your account will be at risk"), + isVisible = UserConfig.isLoggedIn() + ) + val debugHideDebugList = SettingsItem.TitleText( key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, title = UiText.Simple("Hide debug list") @@ -492,6 +570,8 @@ class SettingsViewModelImpl( debugPerformCrash, debugShowCrashAlert, debugNetworkLogLevel, + debugImportAuthData, + debugExportAuthData ).forEach(debugList::add) debugList += debugHideDebugList @@ -521,15 +601,6 @@ class SettingsViewModelImpl( item.asPresentation(resources) } - screenState.setValue { old -> old.copy(settings = uiSettings) } - } -} - -enum class HapticType { - LONG_PRESS, REJECT; - - fun getHaptic(): Int = when (this) { - LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS - REJECT -> HapticFeedbackConstantsCompat.REJECT + _screenState.setValue { old -> old.copy(settings = uiSettings) } } } diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt index 2e104d11..ecde42b6 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt @@ -1,11 +1,9 @@ package dev.meloda.fast.settings.di import dev.meloda.fast.settings.SettingsViewModel -import dev.meloda.fast.settings.SettingsViewModelImpl import org.koin.core.module.dsl.viewModelOf -import org.koin.dsl.bind import org.koin.dsl.module val settingsModule = module { - viewModelOf(::SettingsViewModelImpl) bind SettingsViewModel::class + viewModelOf(::SettingsViewModel) } diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/HapticType.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/HapticType.kt new file mode 100644 index 00000000..78fb41c3 --- /dev/null +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/HapticType.kt @@ -0,0 +1,12 @@ +package dev.meloda.fast.settings.model; + +import androidx.core.view.HapticFeedbackConstantsCompat + +enum class HapticType { + LONG_PRESS, REJECT; + + fun getHaptic(): Int = when (this) { + LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS + REJECT -> HapticFeedbackConstantsCompat.REJECT + } +} diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsDialog.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsDialog.kt new file mode 100644 index 00000000..01ca4586 --- /dev/null +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsDialog.kt @@ -0,0 +1,16 @@ +package dev.meloda.fast.settings.model + +import androidx.compose.runtime.Immutable + +@Immutable +sealed class SettingsDialog { + data object LogOut : SettingsDialog() + data object PerformCrash : SettingsDialog() + data object ImportAuthData : SettingsDialog() + + data class ExportAuthData( + val accessToken: String, + val exchangeToken: String?, + val trustedHash: String? + ) : SettingsDialog() +} diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsScreenState.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsScreenState.kt index be71268a..8028dfd7 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsScreenState.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsScreenState.kt @@ -5,14 +5,12 @@ import dev.meloda.fast.datastore.AppSettings @Immutable data class SettingsScreenState( - val showOptions: SettingsShowOptions, val settings: List, val showDebugOptions: Boolean ) { companion object { val EMPTY: SettingsScreenState = SettingsScreenState( - showOptions = SettingsShowOptions.EMPTY, settings = emptyList(), showDebugOptions = AppSettings.Debug.showDebugCategory ) diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsShowOptions.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsShowOptions.kt deleted file mode 100644 index 83d82a3e..00000000 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/model/SettingsShowOptions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.meloda.fast.settings.model - -data class SettingsShowOptions( - val showLogOut: Boolean, - val showPerformCrash: Boolean, -) { - - companion object { - val EMPTY: SettingsShowOptions = SettingsShowOptions( - showLogOut = false, - showPerformCrash = false, - ) - } -} diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/navigation/SettingsNavigation.kt index d9e1ae61..9365d415 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/navigation/SettingsNavigation.kt @@ -12,13 +12,15 @@ object Settings fun NavGraphBuilder.settingsScreen( onBack: () -> Unit, onLogOutButtonClicked: () -> Unit, - onLanguageItemClicked: () -> Unit + onLanguageItemClicked: () -> Unit, + onRestartRequired: () -> Unit, ) { composable { SettingsRoute( onBack = onBack, onLogOutButtonClicked = onLogOutButtonClicked, - onLanguageItemClicked = onLanguageItemClicked + onLanguageItemClicked = onLanguageItemClicked, + onRestartRequired = onRestartRequired ) } } diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsDialogs.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsDialogs.kt new file mode 100644 index 00000000..06746b3b --- /dev/null +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsDialogs.kt @@ -0,0 +1,282 @@ +package dev.meloda.fast.settings.presentation + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import dev.meloda.fast.data.UserConfig +import dev.meloda.fast.datastore.SettingsKeys +import dev.meloda.fast.settings.model.SettingsDialog +import dev.meloda.fast.settings.model.SettingsScreenState +import dev.meloda.fast.ui.components.ActionInvokeDismiss +import dev.meloda.fast.ui.components.MaterialDialog +import dev.meloda.fast.ui.R as UiR + +@Composable +fun HandleDialogs( + screenState: SettingsScreenState, + dialog: SettingsDialog?, + onConfirmed: (SettingsDialog, Bundle) -> Unit = { _, _ -> }, + onDismissed: (SettingsDialog) -> Unit = {}, + onItemPicked: (SettingsDialog, Bundle) -> Unit = { _, _ -> } +) { + if (dialog == null) return + + val context = LocalContext.current + + when (dialog) { + is SettingsDialog.LogOut -> { + val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY + + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = stringResource( + id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry + else UiR.string.sign_out_confirm_title + ), + text = stringResource(id = UiR.string.sign_out_confirm), + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource( + id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry + else UiR.string.action_sign_out + ), + cancelText = stringResource(id = UiR.string.no), + actionInvokeDismiss = ActionInvokeDismiss.Always + ) + } + + is SettingsDialog.PerformCrash -> { + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = "Perform crash", + text = "App will be crashed. Are you sure?", + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource(id = UiR.string.yes), + cancelText = stringResource(id = UiR.string.cancel), + actionInvokeDismiss = ActionInvokeDismiss.Always + ) + } + + is SettingsDialog.ImportAuthData -> { + var accessToken by rememberSaveable { + mutableStateOf("") + } + var exchangeToken by rememberSaveable { + mutableStateOf("") + } + var trustedHash by rememberSaveable { + mutableStateOf("") + } + + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = "Import auth data", + confirmAction = { + onConfirmed( + dialog, + bundleOf( + "ACCESS_TOKEN" to accessToken, + "EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null }, + "TRUSTED_HASH" to trustedHash.ifEmpty { null } + ) + ) + }, + confirmText = "Import", + cancelText = stringResource(UiR.string.cancel) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = accessToken, + onValueChange = { accessToken = it.trim() }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(text = "Access token") } + ) + + TextField( + value = exchangeToken, + onValueChange = { exchangeToken = it.trim() }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(text = "Exchange token") } + ) + + TextField( + value = trustedHash, + onValueChange = { trustedHash = it.trim() }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(text = "Trusted hash") } + ) + + Button( + onClick = { + val manager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + manager.primaryClip?.let { data -> + val importedData = try { + val data = data.getItemAt(0).text.trim() + if (data.isEmpty()) { + null + } else { + val split = data.split("\n") + if (split.isEmpty() || split.size < 3) { + null + } else { + val (newAccessToken) = split + val newExchangeToken = split[1].trim().ifEmpty { null } + val newTrustedHash = split[2].trim().ifEmpty { null } + + accessToken = newAccessToken + exchangeToken = newExchangeToken.orEmpty() + trustedHash = newTrustedHash.orEmpty() + } + } + } catch (e: Exception) { + e.printStackTrace() + null + } + + if (importedData == null) { + Toast.makeText( + context, + "Invalid data format. Can\'t import", + Toast.LENGTH_SHORT + ).show() + return@let + } + + Toast.makeText( + context, + "Success", + Toast.LENGTH_SHORT + ).show() + } + }, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text(text = "Import from clipboard") + } + } + } + } + + is SettingsDialog.ExportAuthData -> { + var accessToken by rememberSaveable { + mutableStateOf(dialog.accessToken) + } + var exchangeToken by rememberSaveable { + mutableStateOf(dialog.exchangeToken.orEmpty()) + } + var trustedHash by rememberSaveable { + mutableStateOf(dialog.trustedHash.orEmpty()) + } + + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = "Export auth data", + confirmAction = { + onConfirmed( + dialog, + bundleOf( + "ACCESS_TOKEN" to accessToken, + "EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null }, + "TRUSTED_HASH" to trustedHash.ifEmpty { null } + ) + ) + }, + confirmText = stringResource(UiR.string.ok), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + value = accessToken, + onValueChange = { accessToken = it.trim() }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(text = "Access token") } + ) + + TextField( + value = exchangeToken, + onValueChange = { exchangeToken = it.trim() }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(text = "Exchange token") } + ) + + TextField( + value = trustedHash, + onValueChange = { trustedHash = it.trim() }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(text = "Trusted hash") } + ) + + Button( + onClick = { + val manager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + val textToCopy = buildString { + append(accessToken) + append("\n") + + if (exchangeToken.isNotEmpty()) { + append(exchangeToken) + } + + append("\n") + if (trustedHash.isNotEmpty()) { + append(trustedHash) + } + } + + manager.setPrimaryClip( + ClipData.newPlainText("Fast auth data", textToCopy) + ) + Toast.makeText( + context, + "Auth data copied to clipboard. Be careful with this data. If another person gets it, your account will be at risk", + Toast.LENGTH_LONG + ).show() + onDismissed(dialog) + }, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text(text = "Copy to clipboard") + } + } + } + } + } +} diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsRoute.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsRoute.kt new file mode 100644 index 00000000..a66b75e0 --- /dev/null +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsRoute.kt @@ -0,0 +1,65 @@ +package dev.meloda.fast.settings.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.meloda.fast.datastore.SettingsKeys +import dev.meloda.fast.settings.SettingsViewModel +import dev.meloda.fast.settings.model.SettingsDialog +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun SettingsRoute( + onBack: () -> Unit, + onLogOutButtonClicked: () -> Unit, + onLanguageItemClicked: () -> Unit, + onRestartRequired: () -> Unit, + viewModel: SettingsViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val hapticType by viewModel.hapticType.collectAsStateWithLifecycle() + val dialog by viewModel.dialog.collectAsStateWithLifecycle() + val isNeedToRestart by viewModel.isNeedToRestart.collectAsStateWithLifecycle() + + LaunchedEffect(isNeedToRestart) { + if (isNeedToRestart) { + onRestartRequired() + } + } + + SettingsScreen( + screenState = screenState, + hapticType = hapticType, + onBack = onBack, + onHapticPerformed = viewModel::onHapticPerformed, + onSettingsItemClicked = { key -> + when (key) { + SettingsKeys.KEY_APPEARANCE_LANGUAGE -> { + onLanguageItemClicked() + } + + else -> viewModel.onSettingsItemClicked(key) + } + }, + onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked, + onSettingsItemValueChanged = viewModel::onSettingsItemChanged + ) + + HandleDialogs( + screenState = screenState, + dialog = dialog, + onConfirmed = { dialog, bundle -> + when (dialog) { + is SettingsDialog.LogOut -> { + onLogOutButtonClicked() + } + + else -> Unit + } + viewModel.onDialogConfirmed(dialog, bundle) + }, + onDismissed = viewModel::onDialogDismissed, + onItemPicked = viewModel::onDialogItemPicked + ) +} diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt index 2b12ef40..00bc77e0 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt @@ -22,27 +22,20 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection -import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials -import dev.meloda.fast.data.UserConfig import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.datastore.SettingsKeys -import dev.meloda.fast.settings.HapticType -import dev.meloda.fast.settings.SettingsViewModel -import dev.meloda.fast.settings.SettingsViewModelImpl +import dev.meloda.fast.settings.model.HapticType import dev.meloda.fast.settings.model.SettingsScreenState import dev.meloda.fast.settings.model.UiItem import dev.meloda.fast.settings.presentation.item.ListItem @@ -50,56 +43,9 @@ import dev.meloda.fast.settings.presentation.item.SwitchItem import dev.meloda.fast.settings.presentation.item.TextFieldItem import dev.meloda.fast.settings.presentation.item.TitleItem import dev.meloda.fast.settings.presentation.item.TitleTextItem -import dev.meloda.fast.ui.components.ActionInvokeDismiss -import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.theme.LocalThemeConfig -import kotlinx.coroutines.launch -import org.koin.androidx.compose.koinViewModel import dev.meloda.fast.ui.R as UiR -@Composable -fun SettingsRoute( - onBack: () -> Unit, - onLogOutButtonClicked: () -> Unit, - onLanguageItemClicked: () -> Unit, - viewModel: SettingsViewModel = koinViewModel() -) { - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val hapticType by viewModel.hapticType.collectAsStateWithLifecycle() - - SettingsScreen( - screenState = screenState, - hapticType = hapticType, - onBack = onBack, - onHapticPerformed = viewModel::onHapticPerformed, - onSettingsItemClicked = { key -> - when (key) { - SettingsKeys.KEY_APPEARANCE_LANGUAGE -> { - onLanguageItemClicked() - } - - else -> viewModel.onSettingsItemClicked(key) - } - }, - onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked, - onSettingsItemValueChanged = viewModel::onSettingsItemChanged - ) - - val scope = rememberCoroutineScope() - - HandlePopups( - performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked, - performCrashDismissed = viewModel::onPerformCrashAlertDismissed, - logoutPositiveClick = { - scope.launch { - viewModel.onLogOutAlertPositiveClick() - onLogOutButtonClicked() - } - }, - logoutDismissed = viewModel::onLogOutAlertDismissed, - screenState = screenState - ) -} @OptIn( ExperimentalMaterial3Api::class, @@ -248,72 +194,3 @@ fun SettingsScreen( } } } - -@Composable -fun HandlePopups( - performCrashPositiveClick: () -> Unit, - performCrashDismissed: () -> Unit, - logoutPositiveClick: () -> Unit, - logoutDismissed: () -> Unit, - screenState: SettingsScreenState -) { - val showOptions = screenState.showOptions - - PerformCrashDialog( - positiveClick = performCrashPositiveClick, - dismiss = performCrashDismissed, - show = showOptions.showPerformCrash - ) - - LogOutDialog( - positiveClick = logoutPositiveClick, - dismiss = logoutDismissed, - show = showOptions.showLogOut - ) -} - -@Composable -fun PerformCrashDialog( - positiveClick: () -> Unit, - dismiss: () -> Unit, - show: Boolean, -) { - if (show) { - MaterialDialog( - onDismissRequest = dismiss, - title = "Perform crash", - text = "App will be crashed. Are you sure?", - confirmAction = positiveClick, - confirmText = stringResource(id = UiR.string.yes), - cancelText = stringResource(id = UiR.string.cancel), - actionInvokeDismiss = ActionInvokeDismiss.Always - ) - } -} - -@Composable -fun LogOutDialog( - positiveClick: () -> Unit, - dismiss: () -> Unit, - show: Boolean -) { - if (show) { - val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY - - MaterialDialog( - onDismissRequest = dismiss, - title = stringResource( - id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry - else UiR.string.sign_out_confirm_title - ), - text = stringResource(id = UiR.string.sign_out_confirm), - confirmAction = positiveClick, - confirmText = stringResource( - id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry - else UiR.string.action_sign_out - ), - cancelText = stringResource(id = UiR.string.no), - actionInvokeDismiss = ActionInvokeDismiss.Always - ) - } -}