refactor(settings): route settings UI through intents and navigation effects

This commit is contained in:
2026-05-30 19:16:38 +03:00
parent 10453287a7
commit 2daab8d0f7
11 changed files with 231 additions and 153 deletions
@@ -59,6 +59,7 @@ import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.Main
import dev.meloda.fast.navigation.mainScreen import dev.meloda.fast.navigation.mainScreen
import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog
import dev.meloda.fast.settings.model.SettingsNavigationIntent
import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.navigateToSettings
import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
@@ -362,18 +363,31 @@ fun RootScreen(
) )
settingsScreen( settingsScreen(
onBack = navController::navigateUp, handleNavigationIntent = { intent ->
onLogOutButtonClicked = { navController.navigateToAuth(true) }, when (intent) {
onLanguageItemClicked = navController::navigateToLanguagePicker, SettingsNavigationIntent.Back -> navController.navigateUp()
onRestartRequired = { SettingsNavigationIntent.Language -> navController.navigateToLanguagePicker()
activity?.let { SettingsNavigationIntent.Restart -> {
val intent = Intent(activity, MainActivity::class.java) activity?.let {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) val intent =
activity.startActivity(intent) Intent(activity, MainActivity::class.java)
activity.finish() intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP
)
activity.finish()
activity.startActivity(intent)
}
}
SettingsNavigationIntent.LogOut -> {
navController.navigateToAuth(true)
}
} }
} }
) )
languagePickerScreen(onBack = navController::navigateUp) languagePickerScreen(onBack = navController::navigateUp)
} }
@@ -641,7 +641,7 @@ class LongPollUpdatesParser(
).listenValue(this) { state -> ).listenValue(this) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
logger.error(this::class, "loadMessage(): ERROR: $error") logger.error(this@LongPollUpdatesParser::class, "loadMessage(): ERROR: $error")
continuation.resume(null) continuation.resume(null)
}, },
success = { response -> success = { response ->
@@ -670,7 +670,7 @@ class LongPollUpdatesParser(
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
logger.error(this::class, "loadConvo(): ERROR: $error") logger.error(this@LongPollUpdatesParser::class, "loadConvo(): ERROR: $error")
continuation.resume(null) continuation.resume(null)
}, },
success = { response -> success = { response ->
@@ -11,9 +11,10 @@ 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.listenValue
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.extensions.updateValue
import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.NetworkLogLevel
import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.common.model.NetworkLogLevel
import dev.meloda.fast.common.model.UiText 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
@@ -24,10 +25,13 @@ import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.logger.FastLogger
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.HapticType
import dev.meloda.fast.settings.model.SettingsDialog import dev.meloda.fast.settings.model.SettingsDialog
import dev.meloda.fast.settings.model.SettingsIntent
import dev.meloda.fast.settings.model.SettingsItem import dev.meloda.fast.settings.model.SettingsItem
import dev.meloda.fast.settings.model.SettingsNavigationIntent
import dev.meloda.fast.settings.model.SettingsScreenState import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.TextProvider import dev.meloda.fast.settings.model.TextProvider
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
@@ -45,35 +49,83 @@ class SettingsViewModel(
private val getCurrentAccountUseCase: GetCurrentAccountUseCase, 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,
private val logger: FastLogger
) : ViewModel() { ) : ViewModel() {
private val _screenState = MutableStateFlow(SettingsScreenState.EMPTY) private val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
val screenState = _screenState.asStateFlow() val screenStateFlow get() = screenState.asStateFlow()
private val _hapticType = MutableStateFlow<HapticType?>(null) private val hapticType = MutableStateFlow<HapticType?>(null)
val hapticType = _hapticType.asStateFlow() val hapticTypeFlow get() = hapticType.asStateFlow()
private val _dialog = MutableStateFlow<SettingsDialog?>(null) private val navigationIntent = MutableStateFlow<SettingsNavigationIntent?>(null)
val dialog = _dialog.asStateFlow() val navigationIntentFlow get() = navigationIntent.asStateFlow()
private val _isNeedToRestart = MutableStateFlow(false) private val settings = mutableListOf<SettingsItem<*>>()
val isNeedToRestart = _isNeedToRestart.asStateFlow() private var showDebugCategory: Boolean = userSettings.showDebugCategory.value
private val settings = MutableStateFlow<List<SettingsItem<*>>>(emptyList())
init { init {
createSettings() createSettings()
} }
fun onDialogConfirmed(dialog: SettingsDialog, bundle: Bundle) { fun handleIntent(intent: SettingsIntent) {
onDialogDismissed(dialog) when (intent) {
SettingsIntent.BackClick -> {
navigationIntent.setValue { SettingsNavigationIntent.Back }
}
SettingsIntent.ConsumePerformHaptic -> {
hapticType.setValue { null }
}
is SettingsIntent.ItemClick -> {
onSettingsItemClicked(intent.key)
}
is SettingsIntent.ItemLongClick -> {
onSettingsItemLongClicked(intent.key)
}
is SettingsIntent.ItemValueChanged -> {
onSettingsItemChanged(intent.key, intent.newValue)
}
is SettingsIntent.Dialog -> {
when (intent) {
SettingsIntent.Dialog.CancelClick -> Unit
is SettingsIntent.Dialog.ConfirmClick -> {
onDialogConfirmed(intent.bundle)
}
SettingsIntent.Dialog.Dismiss -> {
onDialogDismissed()
}
is SettingsIntent.Dialog.ItemPick -> {
onDialogItemPicked(intent.bundle)
}
}
}
}
}
private fun setDialog(dialog: SettingsDialog?) {
screenState.updateValue { copy(dialog = dialog) }
}
private fun onDialogConfirmed(bundle: Bundle?) {
val dialog = screenState.value.dialog ?: return
onDialogDismissed()
when (dialog) { when (dialog) {
is SettingsDialog.LogOut -> onLogOutAlertPositiveClick() is SettingsDialog.LogOut -> onLogOutAlertPositiveClick()
is SettingsDialog.PerformCrash -> onPerformCrashPositiveButtonClicked() is SettingsDialog.PerformCrash -> onPerformCrashPositiveButtonClicked()
is SettingsDialog.ImportAuthData -> { is SettingsDialog.ImportAuthData -> {
if (bundle == null) return
val accessToken = bundle.getString("ACCESS_TOKEN") ?: return val accessToken = bundle.getString("ACCESS_TOKEN") ?: return
val exchangeToken = bundle.getString("EXCHANGE_TOKEN") val exchangeToken = bundle.getString("EXCHANGE_TOKEN")
val trustedHash = bundle.getString("TRUSTED_HASH") val trustedHash = bundle.getString("TRUSTED_HASH")
@@ -89,6 +141,10 @@ class SettingsViewModel(
).listenValue(viewModelScope) { state -> ).listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
logger.error(
this@SettingsViewModel::class,
"importAuthInfo(): loadUserById(): ERROR: $error"
)
UserConfig.accessToken = oldToken UserConfig.accessToken = oldToken
}, },
success = { user -> success = { user ->
@@ -113,7 +169,7 @@ class SettingsViewModel(
accountsRepository.storeAccounts(listOf(account)) accountsRepository.storeAccounts(listOf(account))
_isNeedToRestart.setValue { true } navigationIntent.setValue { SettingsNavigationIntent.Restart }
} }
) )
} }
@@ -124,7 +180,8 @@ class SettingsViewModel(
} }
} }
fun onDialogDismissed(dialog: SettingsDialog) { private fun onDialogDismissed() {
val dialog = screenState.value.dialog ?: return
when (dialog) { when (dialog) {
is SettingsDialog.LogOut -> Unit is SettingsDialog.LogOut -> Unit
is SettingsDialog.PerformCrash -> Unit is SettingsDialog.PerformCrash -> Unit
@@ -132,10 +189,11 @@ class SettingsViewModel(
is SettingsDialog.ExportAuthData -> Unit is SettingsDialog.ExportAuthData -> Unit
} }
_dialog.setValue { null } setDialog(null)
} }
fun onDialogItemPicked(dialog: SettingsDialog, bundle: Bundle) { private fun onDialogItemPicked(bundle: Bundle?) {
val dialog = screenState.value.dialog ?: return
when (dialog) { when (dialog) {
is SettingsDialog.LogOut -> Unit is SettingsDialog.LogOut -> Unit
is SettingsDialog.PerformCrash -> Unit is SettingsDialog.PerformCrash -> Unit
@@ -144,7 +202,7 @@ class SettingsViewModel(
} }
} }
fun onLogOutAlertPositiveClick() { private fun onLogOutAlertPositiveClick() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val tasks = listOf( val tasks = listOf(
async { async {
@@ -164,35 +222,37 @@ class SettingsViewModel(
) )
tasks.awaitAll() tasks.awaitAll()
navigationIntent.setValue { SettingsNavigationIntent.LogOut }
} }
} }
fun onPerformCrashPositiveButtonClicked() { private fun onPerformCrashPositiveButtonClicked() {
throw Exception("Test exception") throw Exception("Test exception")
} }
fun onSettingsItemClicked(key: String) { private fun onSettingsItemClicked(key: String) {
when (key) { when (key) {
SettingsKeys.KEY_ACCOUNT_LOGOUT -> { SettingsKeys.KEY_ACCOUNT_LOGOUT -> {
_dialog.setValue { SettingsDialog.LogOut } setDialog(SettingsDialog.LogOut)
} }
SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> { SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> {
_dialog.setValue { SettingsDialog.PerformCrash } setDialog(SettingsDialog.PerformCrash)
} }
SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA -> { SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA -> {
_dialog.setValue { SettingsDialog.ImportAuthData } setDialog(SettingsDialog.ImportAuthData)
} }
SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA -> { SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA -> {
_dialog.setValue { setDialog(
SettingsDialog.ExportAuthData( SettingsDialog.ExportAuthData(
accessToken = UserConfig.accessToken, accessToken = UserConfig.accessToken,
exchangeToken = UserConfig.exchangeToken, exchangeToken = UserConfig.exchangeToken,
trustedHash = UserConfig.trustedHash trustedHash = UserConfig.trustedHash
) )
} )
} }
SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> { SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> {
@@ -203,13 +263,13 @@ class SettingsViewModel(
createSettings() createSettings()
_hapticType.update { HapticType.REJECT } hapticType.update { HapticType.REJECT }
_screenState.setValue { old -> old.copy(showDebugOptions = false) } showDebugCategory = false
} }
} }
} }
fun onSettingsItemLongClicked(key: String) { private 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
@@ -219,18 +279,18 @@ class SettingsViewModel(
createSettings() createSettings()
_hapticType.update { HapticType.LONG_PRESS } hapticType.update { HapticType.LONG_PRESS }
_screenState.setValue { old -> old.copy(showDebugOptions = true) } showDebugCategory = true
} }
} }
} }
fun onSettingsItemChanged(key: String, newValue: Any?) { private fun onSettingsItemChanged(key: String, newValue: Any?) {
settings.value.findWithIndex { it.key == key }?.let { (index, item) -> settings.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)
@@ -311,10 +371,6 @@ class SettingsViewModel(
} }
} }
fun onHapticPerformed() {
_hapticType.update { null }
}
private fun createSettings() { private fun createSettings() {
val accountVisible = UserConfig.isLoggedIn() val accountVisible = UserConfig.isLoggedIn()
val accountTitle = SettingsItem.Title( val accountTitle = SettingsItem.Title(
@@ -512,7 +568,8 @@ class SettingsViewModel(
values = logLevelValues.keys.toList().map(NetworkLogLevel::value) values = logLevelValues.keys.toList().map(NetworkLogLevel::value)
).apply { ).apply {
textProvider = TextProvider { item -> textProvider = TextProvider { item ->
val textValue = logLevelValues[NetworkLogLevel.parse(item.value)].parseString(resources) val textValue =
logLevelValues[NetworkLogLevel.parse(item.value)].parseString(resources)
UiText.Simple("Current value: $textValue") UiText.Simple("Current value: $textValue")
} }
@@ -602,12 +659,10 @@ class SettingsViewModel(
} }
private fun emitSettings(newSettings: List<SettingsItem<*>>) { private fun emitSettings(newSettings: List<SettingsItem<*>>) {
settings.update { newSettings } settings.clear()
settings.addAll(newSettings)
val uiSettings = newSettings.map { item -> val uiSettings = newSettings.map { it.asPresentation(resources) }
item.asPresentation(resources) screenState.setValue { old -> old.copy(settings = uiSettings) }
}
_screenState.setValue { old -> old.copy(settings = uiSettings) }
} }
} }
@@ -0,0 +1,20 @@
package dev.meloda.fast.settings.model
import android.os.Bundle
sealed class SettingsIntent {
data object BackClick : SettingsIntent()
data class ItemClick(val key: String) : SettingsIntent()
data class ItemLongClick(val key: String) : SettingsIntent()
data class ItemValueChanged(val key: String, val newValue: Any?) : SettingsIntent()
data object ConsumePerformHaptic : SettingsIntent()
sealed class Dialog : SettingsIntent() {
data object Dismiss : Dialog()
data class ConfirmClick(val bundle: Bundle? = null) : Dialog()
data object CancelClick : Dialog()
data class ItemPick(val bundle: Bundle? = null) : Dialog()
}
}
@@ -1,13 +1,13 @@
package dev.meloda.fast.settings.model package dev.meloda.fast.settings.model
import android.content.res.Resources import android.content.res.Resources
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable
import dev.meloda.fast.common.model.UiText 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.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import kotlin.reflect.KClass import kotlin.reflect.KClass
@Immutable @Stable
sealed class SettingsItem<T>( sealed class SettingsItem<T>(
val key: String, val key: String,
value: T, value: T,
@@ -0,0 +1,8 @@
package dev.meloda.fast.settings.model
sealed class SettingsNavigationIntent {
data object Back : SettingsNavigationIntent()
data object Language : SettingsNavigationIntent()
data object Restart : SettingsNavigationIntent()
data object LogOut : SettingsNavigationIntent()
}
@@ -6,13 +6,13 @@ import dev.meloda.fast.datastore.AppSettings
@Immutable @Immutable
data class SettingsScreenState( data class SettingsScreenState(
val settings: List<UiItem>, val settings: List<UiItem>,
val showDebugOptions: Boolean val dialog: SettingsDialog?
) { ) {
companion object { companion object {
val EMPTY: SettingsScreenState = SettingsScreenState( val EMPTY: SettingsScreenState = SettingsScreenState(
settings = emptyList(), settings = emptyList(),
showDebugOptions = AppSettings.Debug.showDebugCategory dialog = null
) )
} }
} }
@@ -1,26 +1,37 @@
package dev.meloda.fast.settings.navigation package dev.meloda.fast.settings.navigation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController 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.settings.SettingsViewModel
import dev.meloda.fast.settings.model.SettingsNavigationIntent
import dev.meloda.fast.settings.presentation.SettingsRoute import dev.meloda.fast.settings.presentation.SettingsRoute
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel
@Serializable @Serializable
object Settings object Settings
fun NavGraphBuilder.settingsScreen( fun NavGraphBuilder.settingsScreen(
onBack: () -> Unit, handleNavigationIntent: (SettingsNavigationIntent) -> Unit
onLogOutButtonClicked: () -> Unit,
onLanguageItemClicked: () -> Unit,
onRestartRequired: () -> Unit,
) { ) {
composable<Settings> { composable<Settings> {
val viewModel: SettingsViewModel = koinViewModel()
val screenState by viewModel.screenStateFlow.collectAsStateWithLifecycle()
val hapticType by viewModel.hapticTypeFlow.collectAsStateWithLifecycle()
val navigationIntent by viewModel.navigationIntentFlow.collectAsStateWithLifecycle()
LaunchedEffect(navigationIntent) {
navigationIntent?.let(handleNavigationIntent)
}
SettingsRoute( SettingsRoute(
onBack = onBack, handleIntent = viewModel::handleIntent,
onLogOutButtonClicked = onLogOutButtonClicked, screenState = screenState,
onLanguageItemClicked = onLanguageItemClicked, hapticType = hapticType
onRestartRequired = onRestartRequired
) )
} }
} }
@@ -3,7 +3,6 @@ package dev.meloda.fast.settings.presentation
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -26,20 +25,18 @@ import androidx.core.os.bundleOf
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.settings.model.SettingsDialog import dev.meloda.fast.settings.model.SettingsDialog
import dev.meloda.fast.settings.model.SettingsIntent
import dev.meloda.fast.settings.model.SettingsScreenState import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.ActionInvokeDismiss import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.R
@Composable @Composable
fun HandleDialogs( fun HandleDialogs(
handleIntent: (SettingsIntent.Dialog) -> Unit,
screenState: SettingsScreenState, screenState: SettingsScreenState,
dialog: SettingsDialog?,
onConfirmed: (SettingsDialog, Bundle) -> Unit = { _, _ -> },
onDismissed: (SettingsDialog) -> Unit = {},
onItemPicked: (SettingsDialog, Bundle) -> Unit = { _, _ -> }
) { ) {
if (dialog == null) return val dialog = screenState.dialog ?: return
val context = LocalContext.current val context = LocalContext.current
@@ -48,13 +45,13 @@ fun HandleDialogs(
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { handleIntent(SettingsIntent.Dialog.Dismiss) },
title = stringResource( title = stringResource(
id = if (isEasterEgg) R.string.easter_egg_log_out_dmitry id = if (isEasterEgg) R.string.easter_egg_log_out_dmitry
else R.string.sign_out_confirm_title else R.string.sign_out_confirm_title
), ),
text = stringResource(id = R.string.sign_out_confirm), text = stringResource(id = R.string.sign_out_confirm),
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { handleIntent(SettingsIntent.Dialog.ConfirmClick()) },
confirmText = stringResource( confirmText = stringResource(
id = if (isEasterEgg) R.string.easter_egg_log_out_dmitry id = if (isEasterEgg) R.string.easter_egg_log_out_dmitry
else R.string.action_sign_out else R.string.action_sign_out
@@ -66,10 +63,10 @@ fun HandleDialogs(
is SettingsDialog.PerformCrash -> { is SettingsDialog.PerformCrash -> {
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { handleIntent(SettingsIntent.Dialog.Dismiss) },
title = "Perform crash", title = "Perform crash",
text = "App will be crashed. Are you sure?", text = "App will be crashed. Are you sure?",
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { handleIntent(SettingsIntent.Dialog.ConfirmClick()) },
confirmText = stringResource(id = R.string.yes), confirmText = stringResource(id = R.string.yes),
cancelText = stringResource(id = R.string.cancel), cancelText = stringResource(id = R.string.cancel),
actionInvokeDismiss = ActionInvokeDismiss.Always actionInvokeDismiss = ActionInvokeDismiss.Always
@@ -88,15 +85,16 @@ fun HandleDialogs(
} }
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { handleIntent(SettingsIntent.Dialog.Dismiss) },
title = "Import auth data", title = "Import auth data",
confirmAction = { confirmAction = {
onConfirmed( handleIntent(
dialog, SettingsIntent.Dialog.ConfirmClick(
bundleOf( bundleOf(
"ACCESS_TOKEN" to accessToken, "ACCESS_TOKEN" to accessToken,
"EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null }, "EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null },
"TRUSTED_HASH" to trustedHash.ifEmpty { null } "TRUSTED_HASH" to trustedHash.ifEmpty { null }
)
) )
) )
}, },
@@ -198,15 +196,16 @@ fun HandleDialogs(
} }
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { handleIntent(SettingsIntent.Dialog.Dismiss) },
title = "Export auth data", title = "Export auth data",
confirmAction = { confirmAction = {
onConfirmed( handleIntent(
dialog, SettingsIntent.Dialog.ConfirmClick(
bundleOf( bundleOf(
"ACCESS_TOKEN" to accessToken, "ACCESS_TOKEN" to accessToken,
"EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null }, "EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null },
"TRUSTED_HASH" to trustedHash.ifEmpty { null } "TRUSTED_HASH" to trustedHash.ifEmpty { null }
)
) )
) )
}, },
@@ -269,7 +268,8 @@ fun HandleDialogs(
"Auth data copied to clipboard. Be careful with this data. If another person gets it, your account will be at risk", "Auth data copied to clipboard. Be careful with this data. If another person gets it, your account will be at risk",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
onDismissed(dialog)
handleIntent(SettingsIntent.Dialog.Dismiss)
}, },
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) { ) {
@@ -1,65 +1,24 @@
package dev.meloda.fast.settings.presentation package dev.meloda.fast.settings.presentation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import dev.meloda.fast.settings.model.HapticType
import androidx.compose.runtime.getValue import dev.meloda.fast.settings.model.SettingsIntent
import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.meloda.fast.settings.model.SettingsScreenState
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 @Composable
fun SettingsRoute( fun SettingsRoute(
onBack: () -> Unit, handleIntent: (SettingsIntent) -> Unit,
onLogOutButtonClicked: () -> Unit, screenState: SettingsScreenState,
onLanguageItemClicked: () -> Unit, hapticType: HapticType?
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( SettingsScreen(
handleIntent = handleIntent,
screenState = screenState, screenState = screenState,
hapticType = hapticType, 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( HandleDialogs(
handleIntent = handleIntent,
screenState = screenState, 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
) )
} }
@@ -22,7 +22,9 @@ 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.rememberUpdatedState
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
@@ -36,6 +38,7 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.settings.model.HapticType import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsIntent
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
@@ -43,8 +46,8 @@ 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.theme.LocalThemeConfig
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn( @OptIn(
@@ -53,22 +56,30 @@ import dev.meloda.fast.ui.R
) )
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
handleIntent: (SettingsIntent) -> Unit,
screenState: SettingsScreenState = SettingsScreenState.EMPTY, screenState: SettingsScreenState = SettingsScreenState.EMPTY,
hapticType: HapticType? = null, hapticType: HapticType?
onBack: () -> Unit = {},
onHapticPerformed: () -> Unit = {},
onSettingsItemClicked: (key: String) -> Unit = {},
onSettingsItemLongClicked: (key: String) -> Unit = {},
onSettingsItemValueChanged: (key: String, newValue: Any?) -> Unit = { _, _ -> }
) { ) {
val view = LocalView.current val view = LocalView.current
val onSettingsItemClicked by rememberUpdatedState { key: String ->
handleIntent(SettingsIntent.ItemClick(key))
}
val onSettingsItemLongClicked by rememberUpdatedState { key: String ->
handleIntent(SettingsIntent.ItemLongClick(key))
}
val onSettingsItemValueChanged by rememberUpdatedState { key: String, newValue: Any? ->
handleIntent(SettingsIntent.ItemValueChanged(key, newValue))
}
LaunchedEffect(hapticType) { LaunchedEffect(hapticType) {
if (hapticType != null) { if (hapticType != null) {
if (AppSettings.General.enableHaptic) { if (AppSettings.General.enableHaptic) {
view.performHapticFeedback(hapticType.getHaptic()) view.performHapticFeedback(hapticType.getHaptic())
} }
onHapticPerformed() handleIntent(SettingsIntent.ConsumePerformHaptic)
} }
} }
@@ -90,7 +101,7 @@ fun SettingsScreen(
) )
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = { handleIntent(SettingsIntent.BackClick) }) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_arrow_back_round_24), painter = painterResource(id = R.drawable.ic_arrow_back_round_24),
contentDescription = "Back button" contentDescription = "Back button"