forked from melod1n/fast-messenger
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.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -47,6 +48,7 @@ fun RootScreen(
|
|||||||
navController: NavHostController = rememberNavController(),
|
navController: NavHostController = rememberNavController(),
|
||||||
viewModel: MainViewModel
|
viewModel: MainViewModel
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalActivity.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
|
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
|
||||||
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
|
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
|
||||||
@@ -129,6 +131,7 @@ fun RootScreen(
|
|||||||
viewModel.onUserAuthenticated()
|
viewModel.onUserAuthenticated()
|
||||||
navController.navigateToMain()
|
navController.navigateToMain()
|
||||||
},
|
},
|
||||||
|
onNavigateToSettings = navController::navigateToSettings,
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,7 +165,15 @@ fun RootScreen(
|
|||||||
settingsScreen(
|
settingsScreen(
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
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)
|
languagePickerScreen(onBack = navController::navigateUp)
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ object SettingsKeys {
|
|||||||
const val DEFAULT_ENABLE_HAPTIC = true
|
const val DEFAULT_ENABLE_HAPTIC = true
|
||||||
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
|
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
|
||||||
const val DEFAULT_NETWORK_LOG_LEVEL = 3
|
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 KEY_USE_SYSTEM_FONT = "use_system_font"
|
||||||
const val DEFAULT_USE_SYSTEM_FONT = false
|
const val DEFAULT_USE_SYSTEM_FONT = false
|
||||||
const val KEY_MORE_ANIMATIONS = "more_animations"
|
const val KEY_MORE_ANIMATIONS = "more_animations"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ object AuthGraph
|
|||||||
|
|
||||||
fun NavGraphBuilder.authNavGraph(
|
fun NavGraphBuilder.authNavGraph(
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
|
onNavigateToSettings: () -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
navigation<AuthGraph>(startDestination = Login) {
|
navigation<AuthGraph>(startDestination = Login) {
|
||||||
@@ -54,6 +55,7 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onNavigateToSettings = onNavigateToSettings,
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,49 +35,13 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
interface LoginViewModel {
|
class 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(
|
|
||||||
private val oAuthUseCase: OAuthUseCase,
|
private val oAuthUseCase: OAuthUseCase,
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val loadUserByIdUseCase: LoadUserByIdUseCase,
|
private val loadUserByIdUseCase: LoadUserByIdUseCase,
|
||||||
@@ -85,18 +49,30 @@ class LoginViewModelImpl(
|
|||||||
private val loginValidator: LoginValidator,
|
private val loginValidator: LoginValidator,
|
||||||
private val longPollController: LongPollController,
|
private val longPollController: LongPollController,
|
||||||
private val userSettings: UserSettings
|
private val userSettings: UserSettings
|
||||||
) : ViewModel(), LoginViewModel {
|
) : ViewModel() {
|
||||||
|
private val _screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||||
|
val screenState = _screenState.asStateFlow()
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
private val _loginDialog = MutableStateFlow<LoginDialog?>(null)
|
||||||
override val loginDialog = MutableStateFlow<LoginDialog?>(null)
|
val loginDialog = _loginDialog.asStateFlow()
|
||||||
|
|
||||||
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
private val _validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||||
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
val validationArguments = _validationArguments.asStateFlow()
|
||||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
|
||||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
|
||||||
|
|
||||||
override val isNeedToClearCaptchaCode = MutableStateFlow(false)
|
private val _captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||||
override val isNeedToClearValidationCode = MutableStateFlow(false)
|
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>> =
|
private val validationState: StateFlow<List<LoginValidationResult>> =
|
||||||
screenState.map(loginValidator::validate)
|
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)
|
onDialogDismissed(dialog)
|
||||||
|
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
@@ -128,20 +104,24 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogDismissed(dialog: LoginDialog) {
|
fun onDialogDismissed(dialog: LoginDialog) {
|
||||||
loginDialog.setValue { null }
|
when (dialog) {
|
||||||
|
is LoginDialog.Error -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
_loginDialog.setValue { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
fun onBackPressed() {
|
||||||
screenState.setValue { old -> old.copy(showLogo = true) }
|
_screenState.setValue { old -> old.copy(showLogo = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPasswordVisibilityButtonClicked() {
|
fun onPasswordVisibilityButtonClicked() {
|
||||||
screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
|
_screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoginInputChanged(newLogin: String) {
|
fun onLoginInputChanged(newLogin: String) {
|
||||||
screenState.setValue { old ->
|
_screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
login = newLogin.trim(),
|
login = newLogin.trim(),
|
||||||
loginError = false
|
loginError = false
|
||||||
@@ -149,8 +129,8 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPasswordInputChanged(newPassword: String) {
|
fun onPasswordInputChanged(newPassword: String) {
|
||||||
screenState.setValue { old ->
|
_screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
password = newPassword.trim(),
|
password = newPassword.trim(),
|
||||||
passwordError = false
|
passwordError = false
|
||||||
@@ -158,18 +138,18 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignInButtonClicked() {
|
fun onSignInButtonClicked() {
|
||||||
if (screenState.value.isLoading) return
|
if (screenState.value.isLoading) return
|
||||||
|
|
||||||
if (screenState.value.showLogo) {
|
if (screenState.value.showLogo) {
|
||||||
screenState.setValue { old -> old.copy(showLogo = false) }
|
_screenState.setValue { old -> old.copy(showLogo = false) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLogoClicked() {
|
fun onLogoClicked() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
userSettings.onEnableDynamicColorsChanged(
|
userSettings.onEnableDynamicColorsChanged(
|
||||||
!userSettings.enableDynamicColors.value
|
!userSettings.enableDynamicColors.value
|
||||||
@@ -177,36 +157,36 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToMain() {
|
fun onNavigatedToMain() {
|
||||||
isNeedToOpenMain.update { false }
|
_isNeedToOpenMain.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToUserBanned() {
|
fun onNavigatedToUserBanned() {
|
||||||
userBannedArguments.update { null }
|
_userBannedArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToCaptcha() {
|
fun onNavigatedToCaptcha() {
|
||||||
captchaArguments.update { null }
|
_captchaArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToValidation() {
|
fun onNavigatedToValidation() {
|
||||||
validationArguments.update { null }
|
_validationArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidationCodeReceived(code: String?) {
|
fun onValidationCodeReceived(code: String?) {
|
||||||
validationCode.update { code }
|
validationCode.update { code }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidationCodeCleared() {
|
fun onValidationCodeCleared() {
|
||||||
isNeedToClearValidationCode.update { false }
|
_isNeedToClearValidationCode.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCaptchaCodeReceived(code: String?) {
|
fun onCaptchaCodeReceived(code: String?) {
|
||||||
captchaCode.update { code }
|
captchaCode.update { code }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCaptchaCodeCleared() {
|
fun onCaptchaCodeCleared() {
|
||||||
isNeedToClearCaptchaCode.update { false }
|
_isNeedToClearCaptchaCode.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(forceSms: Boolean = false) {
|
private fun login(forceSms: Boolean = false) {
|
||||||
@@ -223,7 +203,7 @@ class LoginViewModelImpl(
|
|||||||
processValidation()
|
processValidation()
|
||||||
if (!validationState.value.contains(LoginValidationResult.Valid)) return
|
if (!validationState.value.contains(LoginValidationResult.Valid)) return
|
||||||
|
|
||||||
screenState.updateValue { copy(isLoading = true) }
|
_screenState.updateValue { copy(isLoading = true) }
|
||||||
|
|
||||||
val currentValidationSid = validationSid.value
|
val currentValidationSid = validationSid.value
|
||||||
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
|
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
|
||||||
@@ -242,7 +222,7 @@ class LoginViewModelImpl(
|
|||||||
error = { error ->
|
error = { error ->
|
||||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||||
|
|
||||||
screenState.updateValue { copy(isLoading = false) }
|
_screenState.updateValue { copy(isLoading = false) }
|
||||||
captchaSid.setValue { null }
|
captchaSid.setValue { null }
|
||||||
|
|
||||||
parseError(error)
|
parseError(error)
|
||||||
@@ -250,8 +230,8 @@ class LoginViewModelImpl(
|
|||||||
success = { response ->
|
success = { response ->
|
||||||
val exceptionHandler =
|
val exceptionHandler =
|
||||||
CoroutineExceptionHandler { _, _ ->
|
CoroutineExceptionHandler { _, _ ->
|
||||||
screenState.updateValue { copy(isLoading = false) }
|
_screenState.updateValue { copy(isLoading = false) }
|
||||||
loginDialog.setValue { LoginDialog.Error() }
|
_loginDialog.setValue { LoginDialog.Error() }
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
|
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
|
||||||
@@ -277,8 +257,8 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exchangeToken == null) {
|
if (exchangeToken == null) {
|
||||||
screenState.updateValue { copy(isLoading = false) }
|
_screenState.updateValue { copy(isLoading = false) }
|
||||||
loginDialog.setValue { LoginDialog.Error() }
|
_loginDialog.setValue { LoginDialog.Error() }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,15 +296,15 @@ class LoginViewModelImpl(
|
|||||||
).listenValue(viewModelScope) { state ->
|
).listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
any = {
|
any = {
|
||||||
screenState.updateValue { copy(isLoading = false) }
|
_screenState.updateValue { copy(isLoading = false) }
|
||||||
},
|
},
|
||||||
error = ::parseError,
|
error = ::parseError,
|
||||||
success = { user ->
|
success = { user ->
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
loginDialog.update { LoginDialog.Error() }
|
_loginDialog.update { LoginDialog.Error() }
|
||||||
} else {
|
} else {
|
||||||
screenState.updateValue { copy(login = "", password = "") }
|
_screenState.updateValue { copy(login = "", password = "") }
|
||||||
isNeedToOpenMain.update { true }
|
_isNeedToOpenMain.update { true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -347,7 +327,7 @@ class LoginViewModelImpl(
|
|||||||
validationType = error.validationType.value,
|
validationType = error.validationType.value,
|
||||||
canResendSms = error.validationResend == "sms"
|
canResendSms = error.validationResend == "sms"
|
||||||
)
|
)
|
||||||
validationArguments.update { arguments }
|
_validationArguments.update { arguments }
|
||||||
validationSid.update { error.validationSid }
|
validationSid.update { error.validationSid }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,12 +336,12 @@ class LoginViewModelImpl(
|
|||||||
captchaSid = error.captchaSid,
|
captchaSid = error.captchaSid,
|
||||||
captchaImageUrl = error.captchaImageUrl
|
captchaImageUrl = error.captchaImageUrl
|
||||||
)
|
)
|
||||||
captchaArguments.update { arguments }
|
_captchaArguments.update { arguments }
|
||||||
captchaSid.update { error.captchaSid }
|
captchaSid.update { error.captchaSid }
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.InvalidCredentialsError -> {
|
OAuthErrorDomain.InvalidCredentialsError -> {
|
||||||
loginDialog.setValue {
|
_loginDialog.setValue {
|
||||||
LoginDialog.Error(errorText = "Wrong login or password.")
|
LoginDialog.Error(errorText = "Wrong login or password.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,33 +353,33 @@ class LoginViewModelImpl(
|
|||||||
restoreUrl = error.restoreUrl,
|
restoreUrl = error.restoreUrl,
|
||||||
accessToken = error.accessToken
|
accessToken = error.accessToken
|
||||||
)
|
)
|
||||||
userBannedArguments.update { arguments }
|
_userBannedArguments.update { arguments }
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.WrongValidationCode -> {
|
OAuthErrorDomain.WrongValidationCode -> {
|
||||||
isNeedToClearValidationCode.update { true }
|
_isNeedToClearValidationCode.update { true }
|
||||||
validationCode.update { null }
|
validationCode.update { null }
|
||||||
loginDialog.setValue {
|
_loginDialog.setValue {
|
||||||
LoginDialog.Error(errorText = "Wrong validation code.")
|
LoginDialog.Error(errorText = "Wrong validation code.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
||||||
isNeedToClearValidationCode.update { true }
|
_isNeedToClearValidationCode.update { true }
|
||||||
validationCode.update { null }
|
validationCode.update { null }
|
||||||
loginDialog.setValue {
|
_loginDialog.setValue {
|
||||||
LoginDialog.Error(errorText = "Wrong validation code format.")
|
LoginDialog.Error(errorText = "Wrong validation code format.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.TooManyTriesError -> {
|
OAuthErrorDomain.TooManyTriesError -> {
|
||||||
loginDialog.setValue {
|
_loginDialog.setValue {
|
||||||
LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.")
|
LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.UnknownError -> {
|
OAuthErrorDomain.UnknownError -> {
|
||||||
loginDialog.setValue { LoginDialog.Error() }
|
_loginDialog.setValue { LoginDialog.Error() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,11 +392,11 @@ class LoginViewModelImpl(
|
|||||||
validationState.value.forEach { result ->
|
validationState.value.forEach { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
LoginValidationResult.LoginEmpty -> {
|
LoginValidationResult.LoginEmpty -> {
|
||||||
screenState.setValue { old -> old.copy(loginError = true) }
|
_screenState.setValue { old -> old.copy(loginError = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginValidationResult.PasswordEmpty -> {
|
LoginValidationResult.PasswordEmpty -> {
|
||||||
screenState.setValue { old -> old.copy(passwordError = true) }
|
_screenState.setValue { old -> old.copy(passwordError = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginValidationResult.Empty -> Unit
|
LoginValidationResult.Empty -> Unit
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package dev.meloda.fast.auth.login.di
|
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.OAuthUseCase
|
||||||
import dev.meloda.fast.domain.OAuthUseCaseImpl
|
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.singleOf
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
@@ -11,6 +11,6 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val loginModule = module {
|
val loginModule = module {
|
||||||
singleOf(::LoginValidator)
|
singleOf(::LoginValidator)
|
||||||
viewModelOf(::LoginViewModelImpl) bind dev.meloda.fast.auth.login.LoginViewModel::class
|
viewModelOf(::LoginViewModel)
|
||||||
singleOf(::OAuthUseCaseImpl) bind OAuthUseCase::class
|
singleOf(::OAuthUseCaseImpl) bind OAuthUseCase::class
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -8,7 +8,6 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import dev.meloda.fast.auth.login.LoginViewModel
|
import dev.meloda.fast.auth.login.LoginViewModel
|
||||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
|
||||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
@@ -24,11 +23,12 @@ fun NavGraphBuilder.loginScreen(
|
|||||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||||
|
onNavigateToSettings: () -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
composable<Login> { backStackEntry ->
|
composable<Login> { backStackEntry ->
|
||||||
val viewModel: LoginViewModel =
|
val viewModel: LoginViewModel =
|
||||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
backStackEntry.sharedViewModel<LoginViewModel>(navController = navController)
|
||||||
|
|
||||||
val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle()
|
val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle()
|
||||||
val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle()
|
val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle()
|
||||||
@@ -55,6 +55,7 @@ fun NavGraphBuilder.loginScreen(
|
|||||||
onNavigateToMain = onNavigateToMain,
|
onNavigateToMain = onNavigateToMain,
|
||||||
onNavigateToCaptcha = onNavigateToCaptcha,
|
onNavigateToCaptcha = onNavigateToCaptcha,
|
||||||
onNavigateToValidation = onNavigateToValidation,
|
onNavigateToValidation = onNavigateToValidation,
|
||||||
|
onNavigateToSettings = onNavigateToSettings,
|
||||||
validationCode = validationCode,
|
validationCode = validationCode,
|
||||||
captchaCode = captchaCode,
|
captchaCode = captchaCode,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
|
|||||||
+12
-5
@@ -39,6 +39,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.autofill.ContentType
|
import androidx.compose.ui.autofill.ContentType
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
|
||||||
|
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
@@ -56,7 +58,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.meloda.fast.auth.login.LoginViewModel
|
import dev.meloda.fast.auth.login.LoginViewModel
|
||||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
|
||||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginDialog
|
import dev.meloda.fast.auth.login.model.LoginDialog
|
||||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||||
@@ -76,9 +77,10 @@ fun LoginRoute(
|
|||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||||
|
onNavigateToSettings: () -> Unit,
|
||||||
validationCode: String?,
|
validationCode: String?,
|
||||||
captchaCode: String?,
|
captchaCode: String?,
|
||||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
viewModel: LoginViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||||
@@ -132,7 +134,8 @@ fun LoginRoute(
|
|||||||
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
||||||
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
||||||
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
||||||
onLogoClicked = viewModel::onLogoClicked
|
onLogoClicked = viewModel::onLogoClicked,
|
||||||
|
onLogoLongClicked = onNavigateToSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
@@ -151,7 +154,8 @@ fun LoginScreen(
|
|||||||
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
||||||
onPasswordFieldGoAction: () -> Unit = {},
|
onPasswordFieldGoAction: () -> Unit = {},
|
||||||
onSignInButtonClicked: () -> Unit = {},
|
onSignInButtonClicked: () -> Unit = {},
|
||||||
onLogoClicked: () -> Unit = {}
|
onLogoClicked: () -> Unit = {},
|
||||||
|
onLogoLongClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val size = LocalSizeConfig.current
|
val size = LocalSizeConfig.current
|
||||||
@@ -185,7 +189,10 @@ fun LoginScreen(
|
|||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
label = "Logo visibility"
|
label = "Logo visibility"
|
||||||
) {
|
) {
|
||||||
Logo(onLogoClicked = onLogoClicked)
|
Logo(
|
||||||
|
onLogoClicked = onLogoClicked,
|
||||||
|
onLogoLongClicked = onLogoLongClicked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import dev.meloda.fast.ui.theme.LocalSizeConfig
|
|||||||
@Composable
|
@Composable
|
||||||
fun Logo(
|
fun Logo(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLogoClicked: () -> Unit = {}
|
onLogoClicked: () -> Unit = {},
|
||||||
|
onLogoLongClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val size = LocalSizeConfig.current
|
val size = LocalSizeConfig.current
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ fun Logo(
|
|||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
indication = null,
|
indication = null,
|
||||||
onLongClick = null,
|
onLongClick = onLogoLongClicked,
|
||||||
onClick = onLogoClicked
|
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 dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
interface ConversationsViewModel {
|
class ConversationsViewModel(
|
||||||
|
updatesParser: LongPollUpdatesParser,
|
||||||
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(
|
|
||||||
private val filter: ConversationsFilter,
|
private val filter: ConversationsFilter,
|
||||||
private val updatesParser: LongPollUpdatesParser,
|
|
||||||
private val conversationsUseCase: ConversationsUseCase,
|
private val conversationsUseCase: ConversationsUseCase,
|
||||||
private val messagesUseCase: MessagesUseCase,
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
@@ -93,23 +54,34 @@ class ConversationsViewModelImpl(
|
|||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
|
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
|
||||||
) : ConversationsViewModel, ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val _screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
||||||
|
val screenState = _screenState.asStateFlow()
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
private val _navigation = MutableStateFlow<ConversationNavigation?>(null)
|
||||||
override val navigation = MutableStateFlow<ConversationNavigation?>(null)
|
val navigation = _navigation.asStateFlow()
|
||||||
override val dialog = MutableStateFlow<ConversationDialog?>(null)
|
|
||||||
|
|
||||||
override val conversations = MutableStateFlow<List<VkConversation>>(emptyList())
|
private val _dialog = MutableStateFlow<ConversationDialog?>(null)
|
||||||
override val uiConversations = MutableStateFlow<List<UiConversation>>(emptyList())
|
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 ->
|
private val pinnedConversationsCount = conversations.map { conversations ->
|
||||||
conversations.count(VkConversation::isPinned)
|
conversations.count(VkConversation::isPinned)
|
||||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
}.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)
|
private val _currentOffset = MutableStateFlow(0)
|
||||||
override val canPaginate = MutableStateFlow(false)
|
val currentOffset = _currentOffset.asStateFlow()
|
||||||
|
|
||||||
|
private val _canPaginate = MutableStateFlow(false)
|
||||||
|
val canPaginate = _canPaginate.asStateFlow()
|
||||||
|
|
||||||
private val expandedConversationId = MutableStateFlow(0L)
|
private val expandedConversationId = MutableStateFlow(0L)
|
||||||
|
|
||||||
@@ -118,7 +90,7 @@ class ConversationsViewModelImpl(
|
|||||||
private val interactionsTimers = hashMapOf<Long, InteractionJob?>()
|
private val interactionsTimers = hashMapOf<Long, InteractionJob?>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) }
|
_screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) }
|
||||||
|
|
||||||
loadConversations()
|
loadConversations()
|
||||||
|
|
||||||
@@ -137,11 +109,11 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationConsumed() {
|
fun onNavigationConsumed() {
|
||||||
navigation.setValue { null }
|
_navigation.setValue { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) {
|
fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) {
|
||||||
onDialogDismissed(dialog)
|
onDialogDismissed(dialog)
|
||||||
|
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
@@ -170,11 +142,11 @@ class ConversationsViewModelImpl(
|
|||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogDismissed(dialog: ConversationDialog) {
|
fun onDialogDismissed(dialog: ConversationDialog) {
|
||||||
this.dialog.setValue { null }
|
_dialog.setValue { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) {
|
fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) {
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
is ConversationDialog.ConversationDelete -> Unit
|
is ConversationDialog.ConversationDelete -> Unit
|
||||||
is ConversationDialog.ConversationPin -> Unit
|
is ConversationDialog.ConversationPin -> Unit
|
||||||
@@ -184,7 +156,7 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onErrorButtonClicked() {
|
fun onErrorButtonClicked() {
|
||||||
when (baseError.value) {
|
when (baseError.value) {
|
||||||
null -> Unit
|
null -> Unit
|
||||||
|
|
||||||
@@ -197,22 +169,22 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPaginationConditionsMet() {
|
fun onPaginationConditionsMet() {
|
||||||
currentOffset.update { conversations.value.size }
|
_currentOffset.update { conversations.value.size }
|
||||||
loadConversations()
|
loadConversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
fun onRefresh() {
|
||||||
onErrorConsumed()
|
onErrorConsumed()
|
||||||
loadConversations(offset = 0)
|
loadConversations(offset = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConversationItemClick(conversation: UiConversation) {
|
fun onConversationItemClick(conversation: UiConversation) {
|
||||||
collapseConversations()
|
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 {
|
expandedConversationId.setValue {
|
||||||
if (conversation.isExpanded) 0
|
if (conversation.isExpanded) 0
|
||||||
else conversation.id
|
else conversation.id
|
||||||
@@ -220,13 +192,13 @@ class ConversationsViewModelImpl(
|
|||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionClicked(
|
fun onOptionClicked(
|
||||||
conversation: UiConversation,
|
conversation: UiConversation,
|
||||||
option: ConversationOption
|
option: ConversationOption
|
||||||
) {
|
) {
|
||||||
when (option) {
|
when (option) {
|
||||||
ConversationOption.Delete -> {
|
ConversationOption.Delete -> {
|
||||||
dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) }
|
_dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ConversationOption.MarkAsRead -> {
|
ConversationOption.MarkAsRead -> {
|
||||||
@@ -240,37 +212,37 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ConversationOption.Pin -> {
|
ConversationOption.Pin -> {
|
||||||
dialog.setValue { ConversationDialog.ConversationPin(conversation.id) }
|
_dialog.setValue { ConversationDialog.ConversationPin(conversation.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ConversationOption.Unpin -> {
|
ConversationOption.Unpin -> {
|
||||||
dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) }
|
_dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ConversationOption.Archive -> {
|
ConversationOption.Archive -> {
|
||||||
dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) }
|
_dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ConversationOption.Unarchive -> {
|
ConversationOption.Unarchive -> {
|
||||||
dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) }
|
_dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onErrorConsumed() {
|
fun onErrorConsumed() {
|
||||||
baseError.setValue { null }
|
_baseError.setValue { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setScrollIndex(index: Int) {
|
fun setScrollIndex(index: Int) {
|
||||||
screenState.setValue { old -> old.copy(scrollIndex = index) }
|
_screenState.setValue { old -> old.copy(scrollIndex = index) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setScrollOffset(offset: Int) {
|
fun setScrollOffset(offset: Int) {
|
||||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
_screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateChatButtonClicked() {
|
fun onCreateChatButtonClicked() {
|
||||||
navigation.setValue { ConversationNavigation.CreateChat }
|
_navigation.setValue { ConversationNavigation.CreateChat }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun collapseConversations() {
|
private fun collapseConversations() {
|
||||||
@@ -289,7 +261,7 @@ class ConversationsViewModelImpl(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
val newBaseError = VkUtils.parseError(error)
|
val newBaseError = VkUtils.parseError(error)
|
||||||
baseError.update { newBaseError }
|
_baseError.update { newBaseError }
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val conversations = response
|
val conversations = response
|
||||||
@@ -304,7 +276,7 @@ class ConversationsViewModelImpl(
|
|||||||
val paginationExhausted = !itemsCountSufficient &&
|
val paginationExhausted = !itemsCountSufficient &&
|
||||||
this.conversations.value.isNotEmpty()
|
this.conversations.value.isNotEmpty()
|
||||||
|
|
||||||
screenState.updateValue {
|
_screenState.updateValue {
|
||||||
copy(isPaginationExhausted = paginationExhausted)
|
copy(isPaginationExhausted = paginationExhausted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,13 +293,13 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
conversationsUseCase.storeConversations(response)
|
conversationsUseCase.storeConversations(response)
|
||||||
|
|
||||||
this.conversations.emit(fullConversations)
|
_conversations.emit(fullConversations)
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
canPaginate.setValue { itemsCountSufficient }
|
_canPaginate.setValue { itemsCountSufficient }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
screenState.setValue { old ->
|
_screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
isLoading = offset == 0 && state.isLoading(),
|
isLoading = offset == 0 && state.isLoading(),
|
||||||
isPaginating = offset > 0 && state.isLoading()
|
isPaginating = offset > 0 && state.isLoading()
|
||||||
@@ -347,11 +319,11 @@ class ConversationsViewModelImpl(
|
|||||||
?: return@processState
|
?: return@processState
|
||||||
|
|
||||||
newConversations.removeAt(conversationIndex)
|
newConversations.removeAt(conversationIndex)
|
||||||
conversations.update { newConversations.sorted() }
|
_conversations.update { newConversations.sorted() }
|
||||||
syncUiConversation()
|
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)
|
.copy(lastMessage = message)
|
||||||
|
|
||||||
newConversations.add(pinnedConversationsCount.value, conversation)
|
newConversations.add(pinnedConversationsCount.value, conversation)
|
||||||
conversations.update { newConversations.sorted() }
|
_conversations.update { newConversations.sorted() }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -461,7 +433,7 @@ class ConversationsViewModelImpl(
|
|||||||
newConversations.add(toPosition, newConversation)
|
newConversations.add(toPosition, newConversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
conversations.update { newConversations.sorted() }
|
_conversations.update { newConversations.sorted() }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,7 +452,7 @@ class ConversationsViewModelImpl(
|
|||||||
lastMessageId = message.id,
|
lastMessageId = message.id,
|
||||||
lastCmId = message.cmId
|
lastCmId = message.cmId
|
||||||
)
|
)
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +472,7 @@ class ConversationsViewModelImpl(
|
|||||||
unreadCount = event.unreadCount
|
unreadCount = event.unreadCount
|
||||||
)
|
)
|
||||||
|
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,7 +492,7 @@ class ConversationsViewModelImpl(
|
|||||||
unreadCount = event.unreadCount
|
unreadCount = event.unreadCount
|
||||||
)
|
)
|
||||||
|
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,7 +513,7 @@ class ConversationsViewModelImpl(
|
|||||||
interactionIds = userIds
|
interactionIds = userIds
|
||||||
)
|
)
|
||||||
|
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
|
|
||||||
interactionsTimers[peerId]?.let { interactionJob ->
|
interactionsTimers[peerId]?.let { interactionJob ->
|
||||||
@@ -583,7 +555,7 @@ class ConversationsViewModelImpl(
|
|||||||
interactionIds = emptyList()
|
interactionIds = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
|
|
||||||
interactionJob.timerJob.cancel()
|
interactionJob.timerJob.cancel()
|
||||||
@@ -601,7 +573,7 @@ class ConversationsViewModelImpl(
|
|||||||
newConversations[conversationIndex] =
|
newConversations[conversationIndex] =
|
||||||
newConversations[conversationIndex].copy(majorId = event.majorId)
|
newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||||
|
|
||||||
conversations.setValue { newConversations.sorted() }
|
_conversations.setValue { newConversations.sorted() }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,7 +589,7 @@ class ConversationsViewModelImpl(
|
|||||||
newConversations[conversationIndex] =
|
newConversations[conversationIndex] =
|
||||||
newConversations[conversationIndex].copy(minorId = event.minorId)
|
newConversations[conversationIndex].copy(minorId = event.minorId)
|
||||||
|
|
||||||
conversations.setValue { newConversations.sorted() }
|
_conversations.setValue { newConversations.sorted() }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,7 +604,7 @@ class ConversationsViewModelImpl(
|
|||||||
} else {
|
} else {
|
||||||
newConversations.removeAt(conversationIndex)
|
newConversations.removeAt(conversationIndex)
|
||||||
|
|
||||||
conversations.setValue { newConversations.sorted() }
|
_conversations.setValue { newConversations.sorted() }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,7 +627,7 @@ class ConversationsViewModelImpl(
|
|||||||
newConversations.removeAt(index)
|
newConversations.removeAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,7 +641,7 @@ class ConversationsViewModelImpl(
|
|||||||
newConversations.add(pinnedConversationsCount.value, conversation)
|
newConversations.add(pinnedConversationsCount.value, conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
conversations.update { newConversations.sorted() }
|
_conversations.update { newConversations.sorted() }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,7 +663,7 @@ class ConversationsViewModelImpl(
|
|||||||
newConversations[conversationIndex] =
|
newConversations[conversationIndex] =
|
||||||
newConversations[conversationIndex].copy(inRead = startMessageId)
|
newConversations[conversationIndex].copy(inRead = startMessageId)
|
||||||
|
|
||||||
conversations.update { newConversations }
|
_conversations.update { newConversations }
|
||||||
syncUiConversation()
|
syncUiConversation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -763,7 +735,7 @@ class ConversationsViewModelImpl(
|
|||||||
options = options.toImmutableList()
|
options = options.toImmutableList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
uiConversations.setValue { newUiConversations }
|
_uiConversations.setValue { newUiConversations }
|
||||||
|
|
||||||
return newUiConversations
|
return newUiConversations
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
package dev.meloda.fast.conversations.di
|
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.ConversationsUseCase
|
||||||
import dev.meloda.fast.domain.ConversationsUseCaseImpl
|
import dev.meloda.fast.domain.ConversationsUseCaseImpl
|
||||||
import dev.meloda.fast.model.ConversationsFilter
|
import dev.meloda.fast.model.ConversationsFilter
|
||||||
@@ -22,8 +22,8 @@ val conversationsModule = module {
|
|||||||
singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class
|
singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModelImpl {
|
private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModel {
|
||||||
return ConversationsViewModelImpl(
|
return ConversationsViewModel(
|
||||||
filter = filter,
|
filter = filter,
|
||||||
updatesParser = get(),
|
updatesParser = get(),
|
||||||
conversationsUseCase = get(),
|
conversationsUseCase = get(),
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navigation
|
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.conversations.presentation.ConversationsRoute
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.ConversationsFilter
|
import dev.meloda.fast.model.ConversationsFilter
|
||||||
@@ -33,7 +33,7 @@ fun NavGraphBuilder.conversationsGraph(
|
|||||||
navigation<ConversationsGraph>(
|
navigation<ConversationsGraph>(
|
||||||
startDestination = Conversations
|
startDestination = Conversations
|
||||||
) {
|
) {
|
||||||
val conversationsViewModel: ConversationsViewModelImpl = with(activity) {
|
val conversationsViewModel: ConversationsViewModel = with(activity) {
|
||||||
getViewModel(qualifier = named(ConversationsFilter.ALL))
|
getViewModel(qualifier = named(ConversationsFilter.ALL))
|
||||||
}
|
}
|
||||||
composable<Conversations> {
|
composable<Conversations> {
|
||||||
@@ -53,7 +53,7 @@ fun NavGraphBuilder.conversationsGraph(
|
|||||||
|
|
||||||
ConversationsRoute(
|
ConversationsRoute(
|
||||||
viewModel = with(activity) {
|
viewModel = with(activity) {
|
||||||
getViewModel<ConversationsViewModelImpl>(
|
getViewModel<ConversationsViewModel>(
|
||||||
qualifier = named(ConversationsFilter.ARCHIVE)
|
qualifier = named(ConversationsFilter.ARCHIVE)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package dev.meloda.fast.settings
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.meloda.fast.common.LongPollController
|
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.findWithIndex
|
||||||
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.model.DarkMode
|
import dev.meloda.fast.common.model.DarkMode
|
||||||
import dev.meloda.fast.common.model.LogLevel
|
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.common.model.parseString
|
||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.data.db.AccountsRepository
|
import dev.meloda.fast.data.db.AccountsRepository
|
||||||
|
import dev.meloda.fast.data.processState
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.datastore.SettingsKeys
|
import dev.meloda.fast.datastore.SettingsKeys
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
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.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.SettingsItem
|
||||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||||
import dev.meloda.fast.settings.model.SettingsShowOptions
|
|
||||||
import dev.meloda.fast.settings.model.TextProvider
|
import dev.meloda.fast.settings.model.TextProvider
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
import dev.meloda.fast.ui.R as UiR
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
interface SettingsViewModel {
|
class SettingsViewModel(
|
||||||
|
private val loadUserByIdUseCase: LoadUserByIdUseCase,
|
||||||
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,
|
|
||||||
private val accountsRepository: AccountsRepository,
|
private val accountsRepository: AccountsRepository,
|
||||||
|
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
|
||||||
private val userSettings: UserSettings,
|
private val userSettings: UserSettings,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
private val longPollController: LongPollController
|
private val longPollController: LongPollController
|
||||||
) : SettingsViewModel, ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
private val _screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||||
override val hapticType = MutableStateFlow<HapticType?>(null)
|
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())
|
private val settings = MutableStateFlow<List<SettingsItem<*>>>(emptyList())
|
||||||
|
|
||||||
@@ -68,24 +66,87 @@ class SettingsViewModelImpl(
|
|||||||
createSettings()
|
createSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLogOutAlertDismissed() {
|
fun onDialogConfirmed(dialog: SettingsDialog, bundle: Bundle) {
|
||||||
emitShowOptions { old -> old.copy(showLogOut = false) }
|
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() {
|
fun onDialogDismissed(dialog: SettingsDialog) {
|
||||||
withContext(Dispatchers.IO) {
|
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(
|
val tasks = listOf(
|
||||||
// async {
|
|
||||||
// suspendCoroutine { continuation ->
|
|
||||||
// authUseCase.logout().listenValue(viewModelScope) { state ->
|
|
||||||
// state.processState(
|
|
||||||
// any = { continuation.resume(Unit) },
|
|
||||||
// success = {},
|
|
||||||
// error = {}
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
async {
|
async {
|
||||||
accountsRepository.storeAccounts(
|
accountsRepository.storeAccounts(
|
||||||
listOf(
|
listOf(
|
||||||
@@ -106,22 +167,32 @@ class SettingsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPerformCrashAlertDismissed() {
|
fun onPerformCrashPositiveButtonClicked() {
|
||||||
emitShowOptions { old -> old.copy(showPerformCrash = false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPerformCrashPositiveButtonClicked() {
|
|
||||||
throw Exception("Test exception")
|
throw Exception("Test exception")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSettingsItemClicked(key: String) {
|
fun onSettingsItemClicked(key: String) {
|
||||||
when (key) {
|
when (key) {
|
||||||
SettingsKeys.KEY_ACCOUNT_LOGOUT -> {
|
SettingsKeys.KEY_ACCOUNT_LOGOUT -> {
|
||||||
emitShowOptions { old -> old.copy(showLogOut = true) }
|
_dialog.setValue { SettingsDialog.LogOut }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> {
|
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 -> {
|
SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> {
|
||||||
@@ -132,13 +203,13 @@ class SettingsViewModelImpl(
|
|||||||
|
|
||||||
createSettings()
|
createSettings()
|
||||||
|
|
||||||
hapticType.update { HapticType.REJECT }
|
_hapticType.update { HapticType.REJECT }
|
||||||
screenState.setValue { old -> old.copy(showDebugOptions = false) }
|
_screenState.setValue { old -> old.copy(showDebugOptions = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSettingsItemLongClicked(key: String) {
|
fun onSettingsItemLongClicked(key: String) {
|
||||||
when (key) {
|
when (key) {
|
||||||
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
|
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
|
||||||
if (AppSettings.Debug.showDebugCategory) return
|
if (AppSettings.Debug.showDebugCategory) return
|
||||||
@@ -148,18 +219,18 @@ class SettingsViewModelImpl(
|
|||||||
|
|
||||||
createSettings()
|
createSettings()
|
||||||
|
|
||||||
hapticType.update { HapticType.LONG_PRESS }
|
_hapticType.update { HapticType.LONG_PRESS }
|
||||||
screenState.setValue { old -> old.copy(showDebugOptions = true) }
|
_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) ->
|
settings.value.findWithIndex { it.key == key }?.let { (index, item) ->
|
||||||
item.updateValue(newValue)
|
item.updateValue(newValue)
|
||||||
item.updateText()
|
item.updateText()
|
||||||
|
|
||||||
screenState.setValue { old ->
|
_screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
settings = old.settings.toMutableList().apply {
|
settings = old.settings.toMutableList().apply {
|
||||||
this[index] = item.asPresentation(resources)
|
this[index] = item.asPresentation(resources)
|
||||||
@@ -240,13 +311,8 @@ class SettingsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHapticPerformed() {
|
fun onHapticPerformed() {
|
||||||
hapticType.update { null }
|
_hapticType.update { null }
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitShowOptions(function: (SettingsShowOptions) -> SettingsShowOptions) {
|
|
||||||
val newShowOptions = function.invoke(screenState.value.showOptions)
|
|
||||||
screenState.setValue { old -> old.copy(showOptions = newShowOptions) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSettings() {
|
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(
|
val debugHideDebugList = SettingsItem.TitleText(
|
||||||
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
|
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
|
||||||
title = UiText.Simple("Hide debug list")
|
title = UiText.Simple("Hide debug list")
|
||||||
@@ -492,6 +570,8 @@ class SettingsViewModelImpl(
|
|||||||
debugPerformCrash,
|
debugPerformCrash,
|
||||||
debugShowCrashAlert,
|
debugShowCrashAlert,
|
||||||
debugNetworkLogLevel,
|
debugNetworkLogLevel,
|
||||||
|
debugImportAuthData,
|
||||||
|
debugExportAuthData
|
||||||
).forEach(debugList::add)
|
).forEach(debugList::add)
|
||||||
|
|
||||||
debugList += debugHideDebugList
|
debugList += debugHideDebugList
|
||||||
@@ -521,15 +601,6 @@ class SettingsViewModelImpl(
|
|||||||
item.asPresentation(resources)
|
item.asPresentation(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.setValue { old -> old.copy(settings = uiSettings) }
|
_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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package dev.meloda.fast.settings.di
|
package dev.meloda.fast.settings.di
|
||||||
|
|
||||||
import dev.meloda.fast.settings.SettingsViewModel
|
import dev.meloda.fast.settings.SettingsViewModel
|
||||||
import dev.meloda.fast.settings.SettingsViewModelImpl
|
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.bind
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val settingsModule = 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
|
@Immutable
|
||||||
data class SettingsScreenState(
|
data class SettingsScreenState(
|
||||||
val showOptions: SettingsShowOptions,
|
|
||||||
val settings: List<UiItem>,
|
val settings: List<UiItem>,
|
||||||
val showDebugOptions: Boolean
|
val showDebugOptions: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: SettingsScreenState = SettingsScreenState(
|
val EMPTY: SettingsScreenState = SettingsScreenState(
|
||||||
showOptions = SettingsShowOptions.EMPTY,
|
|
||||||
settings = emptyList(),
|
settings = emptyList(),
|
||||||
showDebugOptions = AppSettings.Debug.showDebugCategory
|
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(
|
fun NavGraphBuilder.settingsScreen(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onLogOutButtonClicked: () -> Unit,
|
onLogOutButtonClicked: () -> Unit,
|
||||||
onLanguageItemClicked: () -> Unit
|
onLanguageItemClicked: () -> Unit,
|
||||||
|
onRestartRequired: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composable<Settings> {
|
composable<Settings> {
|
||||||
SettingsRoute(
|
SettingsRoute(
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onLogOutButtonClicked = onLogOutButtonClicked,
|
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.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.data.UserConfig
|
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.datastore.SettingsKeys
|
import dev.meloda.fast.settings.model.HapticType
|
||||||
import dev.meloda.fast.settings.HapticType
|
|
||||||
import dev.meloda.fast.settings.SettingsViewModel
|
|
||||||
import dev.meloda.fast.settings.SettingsViewModelImpl
|
|
||||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||||
import dev.meloda.fast.settings.model.UiItem
|
import dev.meloda.fast.settings.model.UiItem
|
||||||
import dev.meloda.fast.settings.presentation.item.ListItem
|
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.TextFieldItem
|
||||||
import dev.meloda.fast.settings.presentation.item.TitleItem
|
import dev.meloda.fast.settings.presentation.item.TitleItem
|
||||||
import dev.meloda.fast.settings.presentation.item.TitleTextItem
|
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 dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
import dev.meloda.fast.ui.R as UiR
|
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(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
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