ability to import/export auth data and some refactoring
This commit is contained in:
@@ -2,11 +2,14 @@ package dev.meloda.fast.settings
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.meloda.fast.common.LongPollController
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.common.extensions.findWithIndex
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.common.model.DarkMode
|
||||
import dev.meloda.fast.common.model.LogLevel
|
||||
@@ -15,52 +18,47 @@ import dev.meloda.fast.common.model.UiText
|
||||
import dev.meloda.fast.common.model.parseString
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.db.AccountsRepository
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.SettingsKeys
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.AuthUseCase
|
||||
import dev.meloda.fast.domain.GetCurrentAccountUseCase
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.model.database.AccountEntity
|
||||
import dev.meloda.fast.settings.model.HapticType
|
||||
import dev.meloda.fast.settings.model.SettingsDialog
|
||||
import dev.meloda.fast.settings.model.SettingsItem
|
||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||
import dev.meloda.fast.settings.model.SettingsShowOptions
|
||||
import dev.meloda.fast.settings.model.TextProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
interface SettingsViewModel {
|
||||
|
||||
val screenState: StateFlow<SettingsScreenState>
|
||||
val hapticType: StateFlow<HapticType?>
|
||||
|
||||
fun onLogOutAlertDismissed()
|
||||
suspend fun onLogOutAlertPositiveClick()
|
||||
|
||||
fun onPerformCrashAlertDismissed()
|
||||
fun onPerformCrashPositiveButtonClicked()
|
||||
|
||||
fun onSettingsItemClicked(key: String)
|
||||
fun onSettingsItemLongClicked(key: String)
|
||||
fun onSettingsItemChanged(key: String, newValue: Any?)
|
||||
|
||||
fun onHapticPerformed()
|
||||
}
|
||||
|
||||
class SettingsViewModelImpl(
|
||||
private val authUseCase: AuthUseCase,
|
||||
class SettingsViewModel(
|
||||
private val loadUserByIdUseCase: LoadUserByIdUseCase,
|
||||
private val accountsRepository: AccountsRepository,
|
||||
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
|
||||
private val userSettings: UserSettings,
|
||||
private val resources: Resources,
|
||||
private val longPollController: LongPollController
|
||||
) : SettingsViewModel, ViewModel() {
|
||||
) : ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||
override val hapticType = MutableStateFlow<HapticType?>(null)
|
||||
private val _screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||
val screenState = _screenState.asStateFlow()
|
||||
|
||||
private val _hapticType = MutableStateFlow<HapticType?>(null)
|
||||
val hapticType = _hapticType.asStateFlow()
|
||||
|
||||
private val _dialog = MutableStateFlow<SettingsDialog?>(null)
|
||||
val dialog = _dialog.asStateFlow()
|
||||
|
||||
private val _isNeedToRestart = MutableStateFlow(false)
|
||||
val isNeedToRestart = _isNeedToRestart.asStateFlow()
|
||||
|
||||
private val settings = MutableStateFlow<List<SettingsItem<*>>>(emptyList())
|
||||
|
||||
@@ -68,24 +66,87 @@ class SettingsViewModelImpl(
|
||||
createSettings()
|
||||
}
|
||||
|
||||
override fun onLogOutAlertDismissed() {
|
||||
emitShowOptions { old -> old.copy(showLogOut = false) }
|
||||
fun onDialogConfirmed(dialog: SettingsDialog, bundle: Bundle) {
|
||||
onDialogDismissed(dialog)
|
||||
|
||||
when (dialog) {
|
||||
is SettingsDialog.LogOut -> onLogOutAlertPositiveClick()
|
||||
is SettingsDialog.PerformCrash -> onPerformCrashPositiveButtonClicked()
|
||||
|
||||
is SettingsDialog.ImportAuthData -> {
|
||||
val accessToken = bundle.getString("ACCESS_TOKEN") ?: return
|
||||
val exchangeToken = bundle.getString("EXCHANGE_TOKEN")
|
||||
val trustedHash = bundle.getString("TRUSTED_HASH")
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val oldToken = UserConfig.accessToken
|
||||
|
||||
UserConfig.accessToken = accessToken
|
||||
|
||||
loadUserByIdUseCase(
|
||||
userId = null,
|
||||
fields = VkConstants.USER_FIELDS
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
UserConfig.accessToken = oldToken
|
||||
},
|
||||
success = { user ->
|
||||
if (user == null) return@listenValue
|
||||
|
||||
UserConfig.currentUserId = user.id
|
||||
|
||||
val account = getCurrentAccountUseCase()
|
||||
?.copy(
|
||||
userId = user.id,
|
||||
accessToken = accessToken,
|
||||
fastToken = null,
|
||||
exchangeToken = exchangeToken,
|
||||
trustedHash = trustedHash
|
||||
) ?: AccountEntity(
|
||||
userId = user.id,
|
||||
accessToken = accessToken,
|
||||
fastToken = null,
|
||||
trustedHash = trustedHash,
|
||||
exchangeToken = exchangeToken
|
||||
)
|
||||
|
||||
accountsRepository.storeAccounts(listOf(account))
|
||||
|
||||
_isNeedToRestart.setValue { true }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SettingsDialog.ExportAuthData -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onLogOutAlertPositiveClick() {
|
||||
withContext(Dispatchers.IO) {
|
||||
fun onDialogDismissed(dialog: SettingsDialog) {
|
||||
when (dialog) {
|
||||
is SettingsDialog.LogOut -> Unit
|
||||
is SettingsDialog.PerformCrash -> Unit
|
||||
is SettingsDialog.ImportAuthData -> Unit
|
||||
is SettingsDialog.ExportAuthData -> Unit
|
||||
}
|
||||
|
||||
_dialog.setValue { null }
|
||||
}
|
||||
|
||||
fun onDialogItemPicked(dialog: SettingsDialog, bundle: Bundle) {
|
||||
when (dialog) {
|
||||
is SettingsDialog.LogOut -> Unit
|
||||
is SettingsDialog.PerformCrash -> Unit
|
||||
is SettingsDialog.ImportAuthData -> Unit
|
||||
is SettingsDialog.ExportAuthData -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun onLogOutAlertPositiveClick() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tasks = listOf(
|
||||
// async {
|
||||
// suspendCoroutine { continuation ->
|
||||
// authUseCase.logout().listenValue(viewModelScope) { state ->
|
||||
// state.processState(
|
||||
// any = { continuation.resume(Unit) },
|
||||
// success = {},
|
||||
// error = {}
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
async {
|
||||
accountsRepository.storeAccounts(
|
||||
listOf(
|
||||
@@ -106,22 +167,32 @@ class SettingsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPerformCrashAlertDismissed() {
|
||||
emitShowOptions { old -> old.copy(showPerformCrash = false) }
|
||||
}
|
||||
|
||||
override fun onPerformCrashPositiveButtonClicked() {
|
||||
fun onPerformCrashPositiveButtonClicked() {
|
||||
throw Exception("Test exception")
|
||||
}
|
||||
|
||||
override fun onSettingsItemClicked(key: String) {
|
||||
fun onSettingsItemClicked(key: String) {
|
||||
when (key) {
|
||||
SettingsKeys.KEY_ACCOUNT_LOGOUT -> {
|
||||
emitShowOptions { old -> old.copy(showLogOut = true) }
|
||||
_dialog.setValue { SettingsDialog.LogOut }
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> {
|
||||
emitShowOptions { old -> old.copy(showPerformCrash = true) }
|
||||
_dialog.setValue { SettingsDialog.PerformCrash }
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA -> {
|
||||
_dialog.setValue { SettingsDialog.ImportAuthData }
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA -> {
|
||||
_dialog.setValue {
|
||||
SettingsDialog.ExportAuthData(
|
||||
accessToken = UserConfig.accessToken,
|
||||
exchangeToken = UserConfig.exchangeToken,
|
||||
trustedHash = UserConfig.trustedHash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> {
|
||||
@@ -132,13 +203,13 @@ class SettingsViewModelImpl(
|
||||
|
||||
createSettings()
|
||||
|
||||
hapticType.update { HapticType.REJECT }
|
||||
screenState.setValue { old -> old.copy(showDebugOptions = false) }
|
||||
_hapticType.update { HapticType.REJECT }
|
||||
_screenState.setValue { old -> old.copy(showDebugOptions = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSettingsItemLongClicked(key: String) {
|
||||
fun onSettingsItemLongClicked(key: String) {
|
||||
when (key) {
|
||||
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
|
||||
if (AppSettings.Debug.showDebugCategory) return
|
||||
@@ -148,18 +219,18 @@ class SettingsViewModelImpl(
|
||||
|
||||
createSettings()
|
||||
|
||||
hapticType.update { HapticType.LONG_PRESS }
|
||||
screenState.setValue { old -> old.copy(showDebugOptions = true) }
|
||||
_hapticType.update { HapticType.LONG_PRESS }
|
||||
_screenState.setValue { old -> old.copy(showDebugOptions = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSettingsItemChanged(key: String, newValue: Any?) {
|
||||
fun onSettingsItemChanged(key: String, newValue: Any?) {
|
||||
settings.value.findWithIndex { it.key == key }?.let { (index, item) ->
|
||||
item.updateValue(newValue)
|
||||
item.updateText()
|
||||
|
||||
screenState.setValue { old ->
|
||||
_screenState.setValue { old ->
|
||||
old.copy(
|
||||
settings = old.settings.toMutableList().apply {
|
||||
this[index] = item.asPresentation(resources)
|
||||
@@ -240,13 +311,8 @@ class SettingsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHapticPerformed() {
|
||||
hapticType.update { null }
|
||||
}
|
||||
|
||||
private fun emitShowOptions(function: (SettingsShowOptions) -> SettingsShowOptions) {
|
||||
val newShowOptions = function.invoke(screenState.value.showOptions)
|
||||
screenState.setValue { old -> old.copy(showOptions = newShowOptions) }
|
||||
fun onHapticPerformed() {
|
||||
_hapticType.update { null }
|
||||
}
|
||||
|
||||
private fun createSettings() {
|
||||
@@ -446,6 +512,18 @@ class SettingsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
val debugImportAuthData = SettingsItem.TitleText(
|
||||
key = SettingsKeys.KEY_DEBUG_IMPORT_AUTH_DATA,
|
||||
title = UiText.Simple("Import auth data"),
|
||||
text = UiText.Simple("App will be restarted")
|
||||
)
|
||||
val debugExportAuthData = SettingsItem.TitleText(
|
||||
key = SettingsKeys.KEY_DEBUG_EXPORT_AUTH_DATA,
|
||||
title = UiText.Simple("Export auth data"),
|
||||
text = UiText.Simple("Be careful with this data. If another person gets it, your account will be at risk"),
|
||||
isVisible = UserConfig.isLoggedIn()
|
||||
)
|
||||
|
||||
val debugHideDebugList = SettingsItem.TitleText(
|
||||
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
|
||||
title = UiText.Simple("Hide debug list")
|
||||
@@ -492,6 +570,8 @@ class SettingsViewModelImpl(
|
||||
debugPerformCrash,
|
||||
debugShowCrashAlert,
|
||||
debugNetworkLogLevel,
|
||||
debugImportAuthData,
|
||||
debugExportAuthData
|
||||
).forEach(debugList::add)
|
||||
|
||||
debugList += debugHideDebugList
|
||||
@@ -521,15 +601,6 @@ class SettingsViewModelImpl(
|
||||
item.asPresentation(resources)
|
||||
}
|
||||
|
||||
screenState.setValue { old -> old.copy(settings = uiSettings) }
|
||||
}
|
||||
}
|
||||
|
||||
enum class HapticType {
|
||||
LONG_PRESS, REJECT;
|
||||
|
||||
fun getHaptic(): Int = when (this) {
|
||||
LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS
|
||||
REJECT -> HapticFeedbackConstantsCompat.REJECT
|
||||
_screenState.setValue { old -> old.copy(settings = uiSettings) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package dev.meloda.fast.settings.di
|
||||
|
||||
import dev.meloda.fast.settings.SettingsViewModel
|
||||
import dev.meloda.fast.settings.SettingsViewModelImpl
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val settingsModule = module {
|
||||
viewModelOf(::SettingsViewModelImpl) bind SettingsViewModel::class
|
||||
viewModelOf(::SettingsViewModel)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.meloda.fast.settings.model;
|
||||
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
|
||||
enum class HapticType {
|
||||
LONG_PRESS, REJECT;
|
||||
|
||||
fun getHaptic(): Int = when (this) {
|
||||
LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS
|
||||
REJECT -> HapticFeedbackConstantsCompat.REJECT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.meloda.fast.settings.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class SettingsDialog {
|
||||
data object LogOut : SettingsDialog()
|
||||
data object PerformCrash : SettingsDialog()
|
||||
data object ImportAuthData : SettingsDialog()
|
||||
|
||||
data class ExportAuthData(
|
||||
val accessToken: String,
|
||||
val exchangeToken: String?,
|
||||
val trustedHash: String?
|
||||
) : SettingsDialog()
|
||||
}
|
||||
@@ -5,14 +5,12 @@ import dev.meloda.fast.datastore.AppSettings
|
||||
|
||||
@Immutable
|
||||
data class SettingsScreenState(
|
||||
val showOptions: SettingsShowOptions,
|
||||
val settings: List<UiItem>,
|
||||
val showDebugOptions: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: SettingsScreenState = SettingsScreenState(
|
||||
showOptions = SettingsShowOptions.EMPTY,
|
||||
settings = emptyList(),
|
||||
showDebugOptions = AppSettings.Debug.showDebugCategory
|
||||
)
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package dev.meloda.fast.settings.model
|
||||
|
||||
data class SettingsShowOptions(
|
||||
val showLogOut: Boolean,
|
||||
val showPerformCrash: Boolean,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: SettingsShowOptions = SettingsShowOptions(
|
||||
showLogOut = false,
|
||||
showPerformCrash = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -12,13 +12,15 @@ object Settings
|
||||
fun NavGraphBuilder.settingsScreen(
|
||||
onBack: () -> Unit,
|
||||
onLogOutButtonClicked: () -> Unit,
|
||||
onLanguageItemClicked: () -> Unit
|
||||
onLanguageItemClicked: () -> Unit,
|
||||
onRestartRequired: () -> Unit,
|
||||
) {
|
||||
composable<Settings> {
|
||||
SettingsRoute(
|
||||
onBack = onBack,
|
||||
onLogOutButtonClicked = onLogOutButtonClicked,
|
||||
onLanguageItemClicked = onLanguageItemClicked
|
||||
onLanguageItemClicked = onLanguageItemClicked,
|
||||
onRestartRequired = onRestartRequired
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package dev.meloda.fast.settings.presentation
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.datastore.SettingsKeys
|
||||
import dev.meloda.fast.settings.model.SettingsDialog
|
||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun HandleDialogs(
|
||||
screenState: SettingsScreenState,
|
||||
dialog: SettingsDialog?,
|
||||
onConfirmed: (SettingsDialog, Bundle) -> Unit = { _, _ -> },
|
||||
onDismissed: (SettingsDialog) -> Unit = {},
|
||||
onItemPicked: (SettingsDialog, Bundle) -> Unit = { _, _ -> }
|
||||
) {
|
||||
if (dialog == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
when (dialog) {
|
||||
is SettingsDialog.LogOut -> {
|
||||
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
|
||||
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(
|
||||
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
|
||||
else UiR.string.sign_out_confirm_title
|
||||
),
|
||||
text = stringResource(id = UiR.string.sign_out_confirm),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(
|
||||
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
|
||||
else UiR.string.action_sign_out
|
||||
),
|
||||
cancelText = stringResource(id = UiR.string.no),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
|
||||
is SettingsDialog.PerformCrash -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = "Perform crash",
|
||||
text = "App will be crashed. Are you sure?",
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.yes),
|
||||
cancelText = stringResource(id = UiR.string.cancel),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
|
||||
is SettingsDialog.ImportAuthData -> {
|
||||
var accessToken by rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
var exchangeToken by rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
var trustedHash by rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = "Import auth data",
|
||||
confirmAction = {
|
||||
onConfirmed(
|
||||
dialog,
|
||||
bundleOf(
|
||||
"ACCESS_TOKEN" to accessToken,
|
||||
"EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null },
|
||||
"TRUSTED_HASH" to trustedHash.ifEmpty { null }
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmText = "Import",
|
||||
cancelText = stringResource(UiR.string.cancel)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
TextField(
|
||||
value = accessToken,
|
||||
onValueChange = { accessToken = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text(text = "Access token") }
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = exchangeToken,
|
||||
onValueChange = { exchangeToken = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text(text = "Exchange token") }
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = trustedHash,
|
||||
onValueChange = { trustedHash = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text(text = "Trusted hash") }
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val manager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
manager.primaryClip?.let { data ->
|
||||
val importedData = try {
|
||||
val data = data.getItemAt(0).text.trim()
|
||||
if (data.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val split = data.split("\n")
|
||||
if (split.isEmpty() || split.size < 3) {
|
||||
null
|
||||
} else {
|
||||
val (newAccessToken) = split
|
||||
val newExchangeToken = split[1].trim().ifEmpty { null }
|
||||
val newTrustedHash = split[2].trim().ifEmpty { null }
|
||||
|
||||
accessToken = newAccessToken
|
||||
exchangeToken = newExchangeToken.orEmpty()
|
||||
trustedHash = newTrustedHash.orEmpty()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
if (importedData == null) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Invalid data format. Can\'t import",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@let
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Success",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text(text = "Import from clipboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SettingsDialog.ExportAuthData -> {
|
||||
var accessToken by rememberSaveable {
|
||||
mutableStateOf(dialog.accessToken)
|
||||
}
|
||||
var exchangeToken by rememberSaveable {
|
||||
mutableStateOf(dialog.exchangeToken.orEmpty())
|
||||
}
|
||||
var trustedHash by rememberSaveable {
|
||||
mutableStateOf(dialog.trustedHash.orEmpty())
|
||||
}
|
||||
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = "Export auth data",
|
||||
confirmAction = {
|
||||
onConfirmed(
|
||||
dialog,
|
||||
bundleOf(
|
||||
"ACCESS_TOKEN" to accessToken,
|
||||
"EXCHANGE_TOKEN" to exchangeToken.ifEmpty { null },
|
||||
"TRUSTED_HASH" to trustedHash.ifEmpty { null }
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmText = stringResource(UiR.string.ok),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
TextField(
|
||||
value = accessToken,
|
||||
onValueChange = { accessToken = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text(text = "Access token") }
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = exchangeToken,
|
||||
onValueChange = { exchangeToken = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text(text = "Exchange token") }
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = trustedHash,
|
||||
onValueChange = { trustedHash = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text(text = "Trusted hash") }
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val manager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
val textToCopy = buildString {
|
||||
append(accessToken)
|
||||
append("\n")
|
||||
|
||||
if (exchangeToken.isNotEmpty()) {
|
||||
append(exchangeToken)
|
||||
}
|
||||
|
||||
append("\n")
|
||||
if (trustedHash.isNotEmpty()) {
|
||||
append(trustedHash)
|
||||
}
|
||||
}
|
||||
|
||||
manager.setPrimaryClip(
|
||||
ClipData.newPlainText("Fast auth data", textToCopy)
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Auth data copied to clipboard. Be careful with this data. If another person gets it, your account will be at risk",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
onDismissed(dialog)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text(text = "Copy to clipboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package dev.meloda.fast.settings.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.meloda.fast.datastore.SettingsKeys
|
||||
import dev.meloda.fast.settings.SettingsViewModel
|
||||
import dev.meloda.fast.settings.model.SettingsDialog
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun SettingsRoute(
|
||||
onBack: () -> Unit,
|
||||
onLogOutButtonClicked: () -> Unit,
|
||||
onLanguageItemClicked: () -> Unit,
|
||||
onRestartRequired: () -> Unit,
|
||||
viewModel: SettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val hapticType by viewModel.hapticType.collectAsStateWithLifecycle()
|
||||
val dialog by viewModel.dialog.collectAsStateWithLifecycle()
|
||||
val isNeedToRestart by viewModel.isNeedToRestart.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToRestart) {
|
||||
if (isNeedToRestart) {
|
||||
onRestartRequired()
|
||||
}
|
||||
}
|
||||
|
||||
SettingsScreen(
|
||||
screenState = screenState,
|
||||
hapticType = hapticType,
|
||||
onBack = onBack,
|
||||
onHapticPerformed = viewModel::onHapticPerformed,
|
||||
onSettingsItemClicked = { key ->
|
||||
when (key) {
|
||||
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
|
||||
onLanguageItemClicked()
|
||||
}
|
||||
|
||||
else -> viewModel.onSettingsItemClicked(key)
|
||||
}
|
||||
},
|
||||
onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked,
|
||||
onSettingsItemValueChanged = viewModel::onSettingsItemChanged
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
screenState = screenState,
|
||||
dialog = dialog,
|
||||
onConfirmed = { dialog, bundle ->
|
||||
when (dialog) {
|
||||
is SettingsDialog.LogOut -> {
|
||||
onLogOutButtonClicked()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
viewModel.onDialogConfirmed(dialog, bundle)
|
||||
},
|
||||
onDismissed = viewModel::onDialogDismissed,
|
||||
onItemPicked = viewModel::onDialogItemPicked
|
||||
)
|
||||
}
|
||||
+1
-124
@@ -22,27 +22,20 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.SettingsKeys
|
||||
import dev.meloda.fast.settings.HapticType
|
||||
import dev.meloda.fast.settings.SettingsViewModel
|
||||
import dev.meloda.fast.settings.SettingsViewModelImpl
|
||||
import dev.meloda.fast.settings.model.HapticType
|
||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||
import dev.meloda.fast.settings.model.UiItem
|
||||
import dev.meloda.fast.settings.presentation.item.ListItem
|
||||
@@ -50,56 +43,9 @@ import dev.meloda.fast.settings.presentation.item.SwitchItem
|
||||
import dev.meloda.fast.settings.presentation.item.TextFieldItem
|
||||
import dev.meloda.fast.settings.presentation.item.TitleItem
|
||||
import dev.meloda.fast.settings.presentation.item.TitleTextItem
|
||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun SettingsRoute(
|
||||
onBack: () -> Unit,
|
||||
onLogOutButtonClicked: () -> Unit,
|
||||
onLanguageItemClicked: () -> Unit,
|
||||
viewModel: SettingsViewModel = koinViewModel<SettingsViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val hapticType by viewModel.hapticType.collectAsStateWithLifecycle()
|
||||
|
||||
SettingsScreen(
|
||||
screenState = screenState,
|
||||
hapticType = hapticType,
|
||||
onBack = onBack,
|
||||
onHapticPerformed = viewModel::onHapticPerformed,
|
||||
onSettingsItemClicked = { key ->
|
||||
when (key) {
|
||||
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
|
||||
onLanguageItemClicked()
|
||||
}
|
||||
|
||||
else -> viewModel.onSettingsItemClicked(key)
|
||||
}
|
||||
},
|
||||
onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked,
|
||||
onSettingsItemValueChanged = viewModel::onSettingsItemChanged
|
||||
)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
HandlePopups(
|
||||
performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked,
|
||||
performCrashDismissed = viewModel::onPerformCrashAlertDismissed,
|
||||
logoutPositiveClick = {
|
||||
scope.launch {
|
||||
viewModel.onLogOutAlertPositiveClick()
|
||||
onLogOutButtonClicked()
|
||||
}
|
||||
},
|
||||
logoutDismissed = viewModel::onLogOutAlertDismissed,
|
||||
screenState = screenState
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
@@ -248,72 +194,3 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HandlePopups(
|
||||
performCrashPositiveClick: () -> Unit,
|
||||
performCrashDismissed: () -> Unit,
|
||||
logoutPositiveClick: () -> Unit,
|
||||
logoutDismissed: () -> Unit,
|
||||
screenState: SettingsScreenState
|
||||
) {
|
||||
val showOptions = screenState.showOptions
|
||||
|
||||
PerformCrashDialog(
|
||||
positiveClick = performCrashPositiveClick,
|
||||
dismiss = performCrashDismissed,
|
||||
show = showOptions.showPerformCrash
|
||||
)
|
||||
|
||||
LogOutDialog(
|
||||
positiveClick = logoutPositiveClick,
|
||||
dismiss = logoutDismissed,
|
||||
show = showOptions.showLogOut
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PerformCrashDialog(
|
||||
positiveClick: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
show: Boolean,
|
||||
) {
|
||||
if (show) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = dismiss,
|
||||
title = "Perform crash",
|
||||
text = "App will be crashed. Are you sure?",
|
||||
confirmAction = positiveClick,
|
||||
confirmText = stringResource(id = UiR.string.yes),
|
||||
cancelText = stringResource(id = UiR.string.cancel),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogOutDialog(
|
||||
positiveClick: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
show: Boolean
|
||||
) {
|
||||
if (show) {
|
||||
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
|
||||
|
||||
MaterialDialog(
|
||||
onDismissRequest = dismiss,
|
||||
title = stringResource(
|
||||
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
|
||||
else UiR.string.sign_out_confirm_title
|
||||
),
|
||||
text = stringResource(id = UiR.string.sign_out_confirm),
|
||||
confirmAction = positiveClick,
|
||||
confirmText = stringResource(
|
||||
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
|
||||
else UiR.string.action_sign_out
|
||||
),
|
||||
cancelText = stringResource(id = UiR.string.no),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user