ability to import/export auth data and some refactoring
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -23,6 +23,7 @@ object AuthGraph
|
||||
|
||||
fun NavGraphBuilder.authNavGraph(
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
navigation<AuthGraph>(startDestination = Login) {
|
||||
@@ -54,6 +55,7 @@ fun NavGraphBuilder.authNavGraph(
|
||||
)
|
||||
)
|
||||
},
|
||||
onNavigateToSettings = onNavigateToSettings,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
|
||||
@@ -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<LoginScreenState>
|
||||
val loginDialog: StateFlow<LoginDialog?>
|
||||
|
||||
val validationArguments: StateFlow<LoginValidationArguments?>
|
||||
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)
|
||||
|
||||
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<LoginDialog?>(null)
|
||||
private val _loginDialog = MutableStateFlow<LoginDialog?>(null)
|
||||
val loginDialog = _loginDialog.asStateFlow()
|
||||
|
||||
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||
private val _validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||
val validationArguments = _validationArguments.asStateFlow()
|
||||
|
||||
override val isNeedToClearCaptchaCode = MutableStateFlow(false)
|
||||
override val isNeedToClearValidationCode = MutableStateFlow(false)
|
||||
private val _captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||
val captchaArguments = _captchaArguments.asStateFlow()
|
||||
|
||||
private val _userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(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<List<LoginValidationResult>> =
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+3
-2
@@ -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<Login> { backStackEntry ->
|
||||
val viewModel: LoginViewModel =
|
||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||
backStackEntry.sharedViewModel<LoginViewModel>(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
|
||||
|
||||
+12
-5
@@ -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<LoginViewModelImpl>()
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
+73
-101
@@ -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<ConversationsScreenState>
|
||||
val navigation: StateFlow<ConversationNavigation?>
|
||||
val dialog: StateFlow<ConversationDialog?>
|
||||
|
||||
val conversations: StateFlow<List<VkConversation>>
|
||||
val uiConversations: StateFlow<List<UiConversation>>
|
||||
|
||||
val baseError: StateFlow<BaseError?>
|
||||
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
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<ConversationNavigation?>(null)
|
||||
override val dialog = MutableStateFlow<ConversationDialog?>(null)
|
||||
private val _navigation = MutableStateFlow<ConversationNavigation?>(null)
|
||||
val navigation = _navigation.asStateFlow()
|
||||
|
||||
override val conversations = MutableStateFlow<List<VkConversation>>(emptyList())
|
||||
override val uiConversations = MutableStateFlow<List<UiConversation>>(emptyList())
|
||||
private val _dialog = MutableStateFlow<ConversationDialog?>(null)
|
||||
val dialog = _dialog.asStateFlow()
|
||||
|
||||
private val _conversations = MutableStateFlow<List<VkConversation>>(emptyList())
|
||||
val conversations = _conversations.asStateFlow()
|
||||
|
||||
private val _uiConversations = MutableStateFlow<List<UiConversation>>(emptyList())
|
||||
val uiConversations = _uiConversations.asStateFlow()
|
||||
|
||||
private val pinnedConversationsCount = conversations.map { conversations ->
|
||||
conversations.count(VkConversation::isPinned)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
private val _baseError = MutableStateFlow<BaseError?>(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<Long, InteractionJob?>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+3
-3
@@ -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(),
|
||||
|
||||
+3
-3
@@ -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<ConversationsGraph>(
|
||||
startDestination = Conversations
|
||||
) {
|
||||
val conversationsViewModel: ConversationsViewModelImpl = with(activity) {
|
||||
val conversationsViewModel: ConversationsViewModel = with(activity) {
|
||||
getViewModel(qualifier = named(ConversationsFilter.ALL))
|
||||
}
|
||||
composable<Conversations> {
|
||||
@@ -53,7 +53,7 @@ fun NavGraphBuilder.conversationsGraph(
|
||||
|
||||
ConversationsRoute(
|
||||
viewModel = with(activity) {
|
||||
getViewModel<ConversationsViewModelImpl>(
|
||||
getViewModel<ConversationsViewModel>(
|
||||
qualifier = named(ConversationsFilter.ARCHIVE)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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<SettingsScreenState>
|
||||
val hapticType: StateFlow<HapticType?>
|
||||
|
||||
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<HapticType?>(null)
|
||||
private val _screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||
val screenState = _screenState.asStateFlow()
|
||||
|
||||
private val _hapticType = MutableStateFlow<HapticType?>(null)
|
||||
val hapticType = _hapticType.asStateFlow()
|
||||
|
||||
private val _dialog = MutableStateFlow<SettingsDialog?>(null)
|
||||
val dialog = _dialog.asStateFlow()
|
||||
|
||||
private val _isNeedToRestart = MutableStateFlow(false)
|
||||
val isNeedToRestart = _isNeedToRestart.asStateFlow()
|
||||
|
||||
private val settings = MutableStateFlow<List<SettingsItem<*>>>(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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -5,14 +5,12 @@ import dev.meloda.fast.datastore.AppSettings
|
||||
|
||||
@Immutable
|
||||
data class SettingsScreenState(
|
||||
val showOptions: SettingsShowOptions,
|
||||
val settings: List<UiItem>,
|
||||
val showDebugOptions: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: SettingsScreenState = SettingsScreenState(
|
||||
showOptions = SettingsShowOptions.EMPTY,
|
||||
settings = emptyList(),
|
||||
showDebugOptions = AppSettings.Debug.showDebugCategory
|
||||
)
|
||||
|
||||
-14
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -12,13 +12,15 @@ object Settings
|
||||
fun NavGraphBuilder.settingsScreen(
|
||||
onBack: () -> Unit,
|
||||
onLogOutButtonClicked: () -> Unit,
|
||||
onLanguageItemClicked: () -> Unit
|
||||
onLanguageItemClicked: () -> Unit,
|
||||
onRestartRequired: () -> Unit,
|
||||
) {
|
||||
composable<Settings> {
|
||||
SettingsRoute(
|
||||
onBack = onBack,
|
||||
onLogOutButtonClicked = onLogOutButtonClicked,
|
||||
onLanguageItemClicked = onLanguageItemClicked
|
||||
onLanguageItemClicked = onLanguageItemClicked,
|
||||
onRestartRequired = onRestartRequired
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+282
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -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
|
||||
)
|
||||
}
|
||||
+1
-124
@@ -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<SettingsViewModelImpl>()
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user