ability to import/export auth data and some refactoring

This commit is contained in:
2025-07-09 17:29:51 +03:00
parent d2aaac68e2
commit 9e6b079bf6
21 changed files with 721 additions and 437 deletions
@@ -2,11 +2,14 @@ package dev.meloda.fast.settings
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.LogLevel
@@ -15,52 +18,47 @@ import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.db.AccountsRepository
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.AuthUseCase
import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.model.database.AccountEntity
import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsDialog
import dev.meloda.fast.settings.model.SettingsItem
import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.SettingsShowOptions
import dev.meloda.fast.settings.model.TextProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import dev.meloda.fast.ui.R as UiR
interface SettingsViewModel {
val screenState: StateFlow<SettingsScreenState>
val hapticType: StateFlow<HapticType?>
fun onLogOutAlertDismissed()
suspend fun onLogOutAlertPositiveClick()
fun onPerformCrashAlertDismissed()
fun onPerformCrashPositiveButtonClicked()
fun onSettingsItemClicked(key: String)
fun onSettingsItemLongClicked(key: String)
fun onSettingsItemChanged(key: String, newValue: Any?)
fun onHapticPerformed()
}
class SettingsViewModelImpl(
private val authUseCase: AuthUseCase,
class SettingsViewModel(
private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val accountsRepository: AccountsRepository,
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val userSettings: UserSettings,
private val resources: Resources,
private val longPollController: LongPollController
) : SettingsViewModel, ViewModel() {
) : ViewModel() {
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
override val hapticType = MutableStateFlow<HapticType?>(null)
private val _screenState = MutableStateFlow(SettingsScreenState.EMPTY)
val screenState = _screenState.asStateFlow()
private val _hapticType = MutableStateFlow<HapticType?>(null)
val hapticType = _hapticType.asStateFlow()
private val _dialog = MutableStateFlow<SettingsDialog?>(null)
val dialog = _dialog.asStateFlow()
private val _isNeedToRestart = MutableStateFlow(false)
val isNeedToRestart = _isNeedToRestart.asStateFlow()
private val settings = MutableStateFlow<List<SettingsItem<*>>>(emptyList())
@@ -68,24 +66,87 @@ class SettingsViewModelImpl(
createSettings()
}
override fun onLogOutAlertDismissed() {
emitShowOptions { old -> old.copy(showLogOut = false) }
fun onDialogConfirmed(dialog: SettingsDialog, bundle: Bundle) {
onDialogDismissed(dialog)
when (dialog) {
is SettingsDialog.LogOut -> onLogOutAlertPositiveClick()
is SettingsDialog.PerformCrash -> onPerformCrashPositiveButtonClicked()
is SettingsDialog.ImportAuthData -> {
val accessToken = bundle.getString("ACCESS_TOKEN") ?: return
val exchangeToken = bundle.getString("EXCHANGE_TOKEN")
val trustedHash = bundle.getString("TRUSTED_HASH")
viewModelScope.launch(Dispatchers.IO) {
val oldToken = UserConfig.accessToken
UserConfig.accessToken = accessToken
loadUserByIdUseCase(
userId = null,
fields = VkConstants.USER_FIELDS
).listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
UserConfig.accessToken = oldToken
},
success = { user ->
if (user == null) return@listenValue
UserConfig.currentUserId = user.id
val account = getCurrentAccountUseCase()
?.copy(
userId = user.id,
accessToken = accessToken,
fastToken = null,
exchangeToken = exchangeToken,
trustedHash = trustedHash
) ?: AccountEntity(
userId = user.id,
accessToken = accessToken,
fastToken = null,
trustedHash = trustedHash,
exchangeToken = exchangeToken
)
accountsRepository.storeAccounts(listOf(account))
_isNeedToRestart.setValue { true }
}
)
}
}
}
is SettingsDialog.ExportAuthData -> Unit
}
}
override suspend fun onLogOutAlertPositiveClick() {
withContext(Dispatchers.IO) {
fun onDialogDismissed(dialog: SettingsDialog) {
when (dialog) {
is SettingsDialog.LogOut -> Unit
is SettingsDialog.PerformCrash -> Unit
is SettingsDialog.ImportAuthData -> Unit
is SettingsDialog.ExportAuthData -> Unit
}
_dialog.setValue { null }
}
fun onDialogItemPicked(dialog: SettingsDialog, bundle: Bundle) {
when (dialog) {
is SettingsDialog.LogOut -> Unit
is SettingsDialog.PerformCrash -> Unit
is SettingsDialog.ImportAuthData -> Unit
is SettingsDialog.ExportAuthData -> Unit
}
}
fun onLogOutAlertPositiveClick() {
viewModelScope.launch(Dispatchers.IO) {
val tasks = listOf(
// async {
// suspendCoroutine { continuation ->
// authUseCase.logout().listenValue(viewModelScope) { state ->
// state.processState(
// any = { continuation.resume(Unit) },
// success = {},
// error = {}
// )
// }
// }
// },
async {
accountsRepository.storeAccounts(
listOf(
@@ -106,22 +167,32 @@ class SettingsViewModelImpl(
}
}
override fun onPerformCrashAlertDismissed() {
emitShowOptions { old -> old.copy(showPerformCrash = false) }
}
override fun onPerformCrashPositiveButtonClicked() {
fun onPerformCrashPositiveButtonClicked() {
throw Exception("Test exception")
}
override fun onSettingsItemClicked(key: String) {
fun onSettingsItemClicked(key: String) {
when (key) {
SettingsKeys.KEY_ACCOUNT_LOGOUT -> {
emitShowOptions { old -> old.copy(showLogOut = true) }
_dialog.setValue { SettingsDialog.LogOut }
}
SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> {
emitShowOptions { old -> old.copy(showPerformCrash = true) }
_dialog.setValue { SettingsDialog.PerformCrash }
}
SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA -> {
_dialog.setValue { SettingsDialog.ImportAuthData }
}
SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA -> {
_dialog.setValue {
SettingsDialog.ExportAuthData(
accessToken = UserConfig.accessToken,
exchangeToken = UserConfig.exchangeToken,
trustedHash = UserConfig.trustedHash
)
}
}
SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> {
@@ -132,13 +203,13 @@ class SettingsViewModelImpl(
createSettings()
hapticType.update { HapticType.REJECT }
screenState.setValue { old -> old.copy(showDebugOptions = false) }
_hapticType.update { HapticType.REJECT }
_screenState.setValue { old -> old.copy(showDebugOptions = false) }
}
}
}
override fun onSettingsItemLongClicked(key: String) {
fun onSettingsItemLongClicked(key: String) {
when (key) {
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
if (AppSettings.Debug.showDebugCategory) return
@@ -148,18 +219,18 @@ class SettingsViewModelImpl(
createSettings()
hapticType.update { HapticType.LONG_PRESS }
screenState.setValue { old -> old.copy(showDebugOptions = true) }
_hapticType.update { HapticType.LONG_PRESS }
_screenState.setValue { old -> old.copy(showDebugOptions = true) }
}
}
}
override fun onSettingsItemChanged(key: String, newValue: Any?) {
fun onSettingsItemChanged(key: String, newValue: Any?) {
settings.value.findWithIndex { it.key == key }?.let { (index, item) ->
item.updateValue(newValue)
item.updateText()
screenState.setValue { old ->
_screenState.setValue { old ->
old.copy(
settings = old.settings.toMutableList().apply {
this[index] = item.asPresentation(resources)
@@ -240,13 +311,8 @@ class SettingsViewModelImpl(
}
}
override fun onHapticPerformed() {
hapticType.update { null }
}
private fun emitShowOptions(function: (SettingsShowOptions) -> SettingsShowOptions) {
val newShowOptions = function.invoke(screenState.value.showOptions)
screenState.setValue { old -> old.copy(showOptions = newShowOptions) }
fun onHapticPerformed() {
_hapticType.update { null }
}
private fun createSettings() {
@@ -446,6 +512,18 @@ class SettingsViewModelImpl(
}
}
val debugImportAuthData = SettingsItem.TitleText(
key = SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA,
title = UiText.Simple("Import auth data"),
text = UiText.Simple("App will be restarted")
)
val debugExportAuthData = SettingsItem.TitleText(
key = SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA,
title = UiText.Simple("Export auth data"),
text = UiText.Simple("Be careful with this data. If another person gets it, your account will be at risk"),
isVisible = UserConfig.isLoggedIn()
)
val debugHideDebugList = SettingsItem.TitleText(
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
title = UiText.Simple("Hide debug list")
@@ -492,6 +570,8 @@ class SettingsViewModelImpl(
debugPerformCrash,
debugShowCrashAlert,
debugNetworkLogLevel,
debugImportAuthData,
debugExportAuthData
).forEach(debugList::add)
debugList += debugHideDebugList
@@ -521,15 +601,6 @@ class SettingsViewModelImpl(
item.asPresentation(resources)
}
screenState.setValue { old -> old.copy(settings = uiSettings) }
}
}
enum class HapticType {
LONG_PRESS, REJECT;
fun getHaptic(): Int = when (this) {
LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS
REJECT -> HapticFeedbackConstantsCompat.REJECT
_screenState.setValue { old -> old.copy(settings = uiSettings) }
}
}
@@ -1,11 +1,9 @@
package dev.meloda.fast.settings.di
import dev.meloda.fast.settings.SettingsViewModel
import dev.meloda.fast.settings.SettingsViewModelImpl
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.bind
import org.koin.dsl.module
val settingsModule = module {
viewModelOf(::SettingsViewModelImpl) bind SettingsViewModel::class
viewModelOf(::SettingsViewModel)
}
@@ -0,0 +1,12 @@
package dev.meloda.fast.settings.model;
import androidx.core.view.HapticFeedbackConstantsCompat
enum class HapticType {
LONG_PRESS, REJECT;
fun getHaptic(): Int = when (this) {
LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS
REJECT -> HapticFeedbackConstantsCompat.REJECT
}
}
@@ -0,0 +1,16 @@
package dev.meloda.fast.settings.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class SettingsDialog {
data object LogOut : SettingsDialog()
data object PerformCrash : SettingsDialog()
data object ImportAuthData : SettingsDialog()
data class ExportAuthData(
val accessToken: String,
val exchangeToken: String?,
val trustedHash: String?
) : SettingsDialog()
}
@@ -5,14 +5,12 @@ import dev.meloda.fast.datastore.AppSettings
@Immutable
data class SettingsScreenState(
val showOptions: SettingsShowOptions,
val settings: List<UiItem>,
val showDebugOptions: Boolean
) {
companion object {
val EMPTY: SettingsScreenState = SettingsScreenState(
showOptions = SettingsShowOptions.EMPTY,
settings = emptyList(),
showDebugOptions = AppSettings.Debug.showDebugCategory
)
@@ -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,
)
}
}
@@ -12,13 +12,15 @@ object Settings
fun NavGraphBuilder.settingsScreen(
onBack: () -> Unit,
onLogOutButtonClicked: () -> Unit,
onLanguageItemClicked: () -> Unit
onLanguageItemClicked: () -> Unit,
onRestartRequired: () -> Unit,
) {
composable<Settings> {
SettingsRoute(
onBack = onBack,
onLogOutButtonClicked = onLogOutButtonClicked,
onLanguageItemClicked = onLanguageItemClicked
onLanguageItemClicked = onLanguageItemClicked,
onRestartRequired = onRestartRequired
)
}
}
@@ -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")
}
}
}
}
}
}
@@ -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
)
}
@@ -22,27 +22,20 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.settings.HapticType
import dev.meloda.fast.settings.SettingsViewModel
import dev.meloda.fast.settings.SettingsViewModelImpl
import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.settings.presentation.item.ListItem
@@ -50,56 +43,9 @@ import dev.meloda.fast.settings.presentation.item.SwitchItem
import dev.meloda.fast.settings.presentation.item.TextFieldItem
import dev.meloda.fast.settings.presentation.item.TitleItem
import dev.meloda.fast.settings.presentation.item.TitleTextItem
import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.theme.LocalThemeConfig
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import dev.meloda.fast.ui.R as UiR
@Composable
fun SettingsRoute(
onBack: () -> Unit,
onLogOutButtonClicked: () -> Unit,
onLanguageItemClicked: () -> Unit,
viewModel: SettingsViewModel = koinViewModel<SettingsViewModelImpl>()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val hapticType by viewModel.hapticType.collectAsStateWithLifecycle()
SettingsScreen(
screenState = screenState,
hapticType = hapticType,
onBack = onBack,
onHapticPerformed = viewModel::onHapticPerformed,
onSettingsItemClicked = { key ->
when (key) {
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
onLanguageItemClicked()
}
else -> viewModel.onSettingsItemClicked(key)
}
},
onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked,
onSettingsItemValueChanged = viewModel::onSettingsItemChanged
)
val scope = rememberCoroutineScope()
HandlePopups(
performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked,
performCrashDismissed = viewModel::onPerformCrashAlertDismissed,
logoutPositiveClick = {
scope.launch {
viewModel.onLogOutAlertPositiveClick()
onLogOutButtonClicked()
}
},
logoutDismissed = viewModel::onLogOutAlertDismissed,
screenState = screenState
)
}
@OptIn(
ExperimentalMaterial3Api::class,
@@ -248,72 +194,3 @@ fun SettingsScreen(
}
}
}
@Composable
fun HandlePopups(
performCrashPositiveClick: () -> Unit,
performCrashDismissed: () -> Unit,
logoutPositiveClick: () -> Unit,
logoutDismissed: () -> Unit,
screenState: SettingsScreenState
) {
val showOptions = screenState.showOptions
PerformCrashDialog(
positiveClick = performCrashPositiveClick,
dismiss = performCrashDismissed,
show = showOptions.showPerformCrash
)
LogOutDialog(
positiveClick = logoutPositiveClick,
dismiss = logoutDismissed,
show = showOptions.showLogOut
)
}
@Composable
fun PerformCrashDialog(
positiveClick: () -> Unit,
dismiss: () -> Unit,
show: Boolean,
) {
if (show) {
MaterialDialog(
onDismissRequest = dismiss,
title = "Perform crash",
text = "App will be crashed. Are you sure?",
confirmAction = positiveClick,
confirmText = stringResource(id = UiR.string.yes),
cancelText = stringResource(id = UiR.string.cancel),
actionInvokeDismiss = ActionInvokeDismiss.Always
)
}
}
@Composable
fun LogOutDialog(
positiveClick: () -> Unit,
dismiss: () -> Unit,
show: Boolean
) {
if (show) {
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
MaterialDialog(
onDismissRequest = dismiss,
title = stringResource(
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.sign_out_confirm_title
),
text = stringResource(id = UiR.string.sign_out_confirm),
confirmAction = positiveClick,
confirmText = stringResource(
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.action_sign_out
),
cancelText = stringResource(id = UiR.string.no),
actionInvokeDismiss = ActionInvokeDismiss.Always
)
}
}