From 789283fcff6120aaa9d8f9457cbec0c2d34dfbe8 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 15 Jul 2024 22:34:00 +0300 Subject: [PATCH] settings refactoring --- .../app/fast/datastore/SettingsController.kt | 28 +- .../app/fast/ui/components/MaterialDialog.kt | 32 +- .../meloda/app/fast/ui/util/ImmutableList.kt | 5 + .../app/fast/settings/SettingsViewModel.kt | 442 +++++++++--------- .../app/fast/settings/model/SettingsItem.kt | 385 ++++++++------- .../fast/settings/model/SettingsListeners.kt | 13 - .../settings/model/SettingsScreenState.kt | 6 +- .../app/fast/settings/model/TextProvider.kt | 7 + .../app/fast/settings/model/TitleProvider.kt | 7 + .../meloda/app/fast/settings/model/UiItem.kt | 53 +++ .../settings/navigation/SettingsNavigation.kt | 4 - .../settings/presentation/SettingsScreen.kt | 102 ++-- .../settings/presentation/item/ListItem.kt | 145 ++++++ .../settings/presentation/item/SwitchItem.kt | 98 ++++ .../TextFieldItem.kt} | 92 ++-- .../settings/presentation/item/TitleItem.kt | 38 ++ .../TitleTextItem.kt} | 57 +-- .../presentation/items/ListSettingsItem.kt | 154 ------ .../presentation/items/SwitchSettingsItem.kt | 134 ------ .../presentation/items/TitleSettingsItem.kt | 47 -- 20 files changed, 942 insertions(+), 907 deletions(-) delete mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsListeners.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TextProvider.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TitleProvider.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/UiItem.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/ListItem.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/SwitchItem.kt rename feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/{items/TextFieldSettingsItem.kt => item/TextFieldItem.kt} (59%) create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleItem.kt rename feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/{items/TitleSummarySettingsItem.kt => item/TitleTextItem.kt} (53%) delete mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/ListSettingsItem.kt delete mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/SwitchSettingsItem.kt delete mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSettingsItem.kt diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt index 5f00ed8c..1c24bbb8 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt @@ -3,6 +3,7 @@ package com.meloda.app.fast.datastore import android.content.SharedPreferences import androidx.core.content.edit import kotlin.properties.Delegates +import kotlin.reflect.KClass object SettingsController { @@ -39,6 +40,29 @@ object SettingsController { return preferences.getFloat(key, defaultValue) } + @Suppress("UNCHECKED_CAST") + fun get(clazz: KClass, key: String, defaultValue: T): T { + return when (clazz) { + String::class -> getString(key, defaultValue as String) + Boolean::class -> getBoolean(key, defaultValue as Boolean) + Int::class -> getInt(key, defaultValue as Int) + Long::class -> getLong(key, defaultValue as Long) + Float::class -> getFloat(key, defaultValue as Float) + else -> throw IllegalStateException("Unsupported class: $clazz") + } as T + } + + inline fun get(key: String, defaultValue: T): T { + return when (T::class) { + String::class -> getString(key, defaultValue as String) + Boolean::class -> getBoolean(key, defaultValue as Boolean) + Int::class -> getInt(key, defaultValue as Int) + Long::class -> getLong(key, defaultValue as Long) + Float::class -> getFloat(key, defaultValue as Float) + else -> throw IllegalStateException("Unsupported class: ${T::class}") + } as T + } + fun put(key: String, newValue: T?) { preferences.edit { when (newValue) { @@ -52,13 +76,13 @@ object SettingsController { } var isLongPollInBackgroundEnabled: Boolean - get() = getBoolean( + get() = get( SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND ) set(value) = put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value) var deviceId: String - get() = getString("device_id", "").orEmpty() + get() = get("device_id", "") set(value) = put("device_id", value) } diff --git a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/components/MaterialDialog.kt b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/components/MaterialDialog.kt index bb9101b2..267ee00c 100644 --- a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/components/MaterialDialog.kt +++ b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/components/MaterialDialog.kt @@ -52,13 +52,13 @@ fun MaterialDialog( neutralAction: (() -> Unit)? = null, title: String? = null, text: String? = null, - itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None, + selectionType: SelectionType = SelectionType.None, items: ImmutableList = ImmutableList.empty(), preSelectedItems: ImmutableList = ImmutableList.empty(), onItemClick: ((index: Int) -> Unit)? = null, properties: DialogProperties = DialogProperties(), actionInvokeDismiss: ActionInvokeDismiss = ActionInvokeDismiss.IfNoAction, - customContent: (ColumnScope.() -> Unit)? = null + customContent: (@Composable ColumnScope.() -> Unit)? = null ) { var alertItems by remember { mutableStateOf( @@ -135,12 +135,12 @@ fun MaterialDialog( if (alertItems.isNotEmpty()) { Spacer(modifier = Modifier.height(4.dp)) AlertItems( - selectionType = itemsSelectionType, + selectionType = selectionType, items = alertItems, onItemClick = { index -> onItemClick?.invoke(index) - if (itemsSelectionType == ItemsSelectionType.None) { + if (selectionType == SelectionType.None) { onDismissRequest.invoke() } else { val newItems = @@ -255,7 +255,7 @@ fun MaterialDialog( cancelAction: (() -> Unit)? = null, neutralText: UiText? = null, neutralAction: (() -> Unit)? = null, - itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None, + selectionType: SelectionType = SelectionType.None, preSelectedItems: ImmutableList = ImmutableList.empty(), items: ImmutableList = ImmutableList.empty(), onItemClick: ((index: Int) -> Unit)? = null, @@ -356,12 +356,12 @@ fun MaterialDialog( if (alertItems.isNotEmpty()) { Spacer(modifier = Modifier.height(4.dp)) AlertItems( - selectionType = itemsSelectionType, + selectionType = selectionType, items = alertItems, onItemClick = { index -> onItemClick?.invoke(index) - if (itemsSelectionType == ItemsSelectionType.None) { + if (selectionType == SelectionType.None) { onDismissRequest.invoke() } else { val newItems = @@ -457,7 +457,7 @@ fun MaterialDialog( @Composable fun AlertItems( - selectionType: ItemsSelectionType, + selectionType: SelectionType, items: ImmutableList, onItemClick: ((index: Int) -> Unit)? = null, onItemCheckedChanged: ((index: Int) -> Unit)? = null @@ -468,7 +468,7 @@ fun AlertItems( .fillMaxWidth() .height(48.dp) .clickable { - if (selectionType == ItemsSelectionType.Multi) { + if (selectionType == SelectionType.Multi) { onItemCheckedChanged?.invoke(index) } else { onItemClick?.invoke(index) @@ -478,7 +478,7 @@ fun AlertItems( ) { // TODO: 29/12/2023, Danil Nikolaev: check onClick & onCheckedChange actions when (selectionType) { - ItemsSelectionType.Multi -> { + SelectionType.Multi -> { Spacer(modifier = Modifier.width(10.dp)) Checkbox( checked = item.isSelected, @@ -486,7 +486,7 @@ fun AlertItems( ) } - ItemsSelectionType.Single -> { + SelectionType.Single -> { Spacer(modifier = Modifier.width(10.dp)) RadioButton( selected = item.isSelected, @@ -494,7 +494,7 @@ fun AlertItems( ) } - ItemsSelectionType.None -> { + SelectionType.None -> { Spacer(modifier = Modifier.width(26.dp)) } } @@ -520,8 +520,8 @@ sealed class ActionInvokeDismiss { data object Always : ActionInvokeDismiss() } -sealed class ItemsSelectionType { - data object Single : ItemsSelectionType() - data object Multi : ItemsSelectionType() - data object None : ItemsSelectionType() +sealed class SelectionType { + data object Single : SelectionType() + data object Multi : SelectionType() + data object None : SelectionType() } diff --git a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/ImmutableList.kt b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/ImmutableList.kt index 356964be..b5d1fa5f 100644 --- a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/ImmutableList.kt +++ b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/ImmutableList.kt @@ -57,6 +57,11 @@ class ImmutableList(val values: List) : Iterable { fun List.toImmutableList(): ImmutableList = ImmutableList(this) fun empty(): ImmutableList = ImmutableList(emptyList()) + + fun of(vararg elements: T) = + if (elements.isNotEmpty()) copyOf(elements.asList()) else empty() + + fun of(element: T) = ImmutableList(listOf(element)) } override fun iterator(): Iterator = values.listIterator() diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt index d2cce83f..74074689 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt @@ -1,5 +1,6 @@ package com.meloda.app.fast.settings +import android.content.res.Resources import android.os.Build import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.HapticFeedbackConstantsCompat @@ -7,6 +8,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.extensions.findWithIndex import com.meloda.app.fast.common.extensions.isSdkAtLeast import com.meloda.app.fast.common.extensions.setValue import com.meloda.app.fast.data.db.AccountsRepository @@ -19,9 +21,11 @@ import com.meloda.app.fast.model.database.AccountEntity import com.meloda.app.fast.settings.model.SettingsItem import com.meloda.app.fast.settings.model.SettingsScreenState import com.meloda.app.fast.settings.model.SettingsShowOptions +import com.meloda.app.fast.settings.model.TextProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import com.meloda.app.fast.ui.R as UiR @@ -48,11 +52,14 @@ interface SettingsViewModel { class SettingsViewModelImpl( private val accountsRepository: AccountsRepository, - private val userSettings: UserSettings + private val userSettings: UserSettings, + private val resources: Resources ) : SettingsViewModel, ViewModel() { override val screenState = MutableStateFlow(SettingsScreenState.EMPTY) + private val settings = MutableStateFlow>>(emptyList()) + override val isLongPollBackgroundEnabled = MutableStateFlow(null) init { @@ -111,7 +118,7 @@ class SettingsViewModelImpl( screenState.setValue { old -> old.copy( - useHaptics = HapticType.Reject, + useHaptics = HapticType.REJECT, showDebugOptions = false ) } @@ -122,8 +129,7 @@ class SettingsViewModelImpl( override fun onSettingsItemLongClicked(key: String) { when (key) { SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> { - val showDebugCategory = isDebugSettingsShown() - if (showDebugCategory) return + if (isDebugSettingsShown()) return SettingsController.put(SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, true) @@ -131,7 +137,7 @@ class SettingsViewModelImpl( screenState.setValue { old -> old.copy( - useHaptics = HapticType.LongPress, + useHaptics = HapticType.LONG_PRESS, showDebugOptions = true ) } @@ -140,6 +146,19 @@ class SettingsViewModelImpl( } override fun onSettingsItemChanged(key: String, newValue: Any?) { + settings.value.findWithIndex { it.key == key }?.let { (index, item) -> + item.updateValue(newValue) + item.updateText() + + screenState.setValue { old -> + old.copy( + settings = old.settings.toMutableList().apply { + this[index] = item.asPresentation(resources) + } + ) + } + } + when (key) { SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { val isEnabled = (newValue as? Boolean) == true @@ -167,7 +186,6 @@ class SettingsViewModelImpl( userSettings.useMultiline(isUsing) } - SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> { val isUsing = newValue as? Boolean ?: false userSettings.useAmoledThemeChanged(isUsing) @@ -196,7 +214,7 @@ class SettingsViewModelImpl( } override fun onHapticPerformed() { - screenState.setValue { old -> old.copy(useHaptics = HapticType.None) } + screenState.setValue { old -> old.copy(useHaptics = null) } } override fun onNotificationsPermissionRequested() { @@ -209,221 +227,223 @@ class SettingsViewModelImpl( } private fun createSettings() { - viewModelScope.launch { - val accountVisible = UserConfig.isLoggedIn() - val accountTitle = SettingsItem.Title.build( - key = SettingsKeys.KEY_ACCOUNT, - title = UiText.Resource(UiR.string.settings_account_title) - ) { - isVisible = accountVisible - } - val accountLogOut = SettingsItem.TitleSummary.build( - key = SettingsKeys.KEY_ACCOUNT_LOGOUT, - title = UiText.Resource(UiR.string.settings_account_logout_title), - summary = UiText.Resource(UiR.string.settings_account_logout_summary) - ) { - isVisible = accountVisible - } + val accountVisible = UserConfig.isLoggedIn() + val accountTitle = SettingsItem.Title( + key = SettingsKeys.KEY_ACCOUNT, + title = UiText.Resource(UiR.string.settings_account_title), + isVisible = accountVisible + ) + val accountLogOut = SettingsItem.TitleText( + key = SettingsKeys.KEY_ACCOUNT_LOGOUT, + title = UiText.Resource(UiR.string.settings_account_logout_title), + text = UiText.Resource(UiR.string.settings_account_logout_summary), + isVisible = accountVisible + ) - val generalTitle = SettingsItem.Title.build( - key = SettingsKeys.KEY_GENERAL, - title = UiText.Resource(UiR.string.settings_general_title) - ) - val generalUseContactNames = SettingsItem.Switch.build( - key = SettingsKeys.KEY_USE_CONTACT_NAMES, - title = UiText.Resource(UiR.string.settings_general_contact_names_title), - summary = UiText.Resource(UiR.string.settings_general_contact_names_summary), - defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES - ) + val generalTitle = SettingsItem.Title( + key = SettingsKeys.KEY_GENERAL, + title = UiText.Resource(UiR.string.settings_general_title) + ) + val generalUseContactNames = SettingsItem.Switch( + key = SettingsKeys.KEY_USE_CONTACT_NAMES, + title = UiText.Resource(UiR.string.settings_general_contact_names_title), + text = UiText.Resource(UiR.string.settings_general_contact_names_summary), + defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES + ) - val appearanceTitle = SettingsItem.Title.build( - key = SettingsKeys.KEY_APPEARANCE, - title = UiText.Resource(UiR.string.settings_appearance_title) - ) - val appearanceMultiline = SettingsItem.Switch.build( - key = SettingsKeys.KEY_APPEARANCE_MULTILINE, - defaultValue = SettingsKeys.DEFAULT_VALUE_MULTILINE, - title = UiText.Resource(UiR.string.settings_appearance_multiline_title), - summary = UiText.Resource(UiR.string.settings_appearance_multiline_summary) - ) + val appearanceTitle = SettingsItem.Title( + key = SettingsKeys.KEY_APPEARANCE, + title = UiText.Resource(UiR.string.settings_appearance_title) + ) + val appearanceMultiline = SettingsItem.Switch( + key = SettingsKeys.KEY_APPEARANCE_MULTILINE, + defaultValue = SettingsKeys.DEFAULT_VALUE_MULTILINE, + title = UiText.Resource(UiR.string.settings_appearance_multiline_title), + text = UiText.Resource(UiR.string.settings_appearance_multiline_summary) + ) - val darkThemeValues = listOf( - AppCompatDelegate.MODE_NIGHT_YES to UiText.Resource(UiR.string.settings_dark_theme_value_enabled), - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM to UiText.Resource(UiR.string.settings_dark_theme_value_follow_system), - AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY to UiText.Resource(UiR.string.settings_dark_theme_value_battery_saver), - AppCompatDelegate.MODE_NIGHT_NO to UiText.Resource(UiR.string.settings_dark_theme_value_disabled) - ).toMap() + val darkThemeValues = listOf( + AppCompatDelegate.MODE_NIGHT_YES to UiText.Resource(UiR.string.settings_dark_theme_value_enabled), + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM to UiText.Resource(UiR.string.settings_dark_theme_value_follow_system), + AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY to UiText.Resource(UiR.string.settings_dark_theme_value_battery_saver), + AppCompatDelegate.MODE_NIGHT_NO to UiText.Resource(UiR.string.settings_dark_theme_value_disabled) + ).toMap() - val appearanceDarkTheme = SettingsItem.ListItem.build( - key = SettingsKeys.KEY_APPEARANCE_DARK_THEME, - title = UiText.Resource(UiR.string.settings_dark_theme), - values = darkThemeValues.keys.toList(), - valueTitles = darkThemeValues.values.toList(), - defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) { - summaryProvider = SettingsItem.SummaryProvider { item -> - val darkThemeValue = darkThemeValues[item.value ?: 0] + val appearanceDarkTheme = SettingsItem.ListItem( + key = SettingsKeys.KEY_APPEARANCE_DARK_THEME, + title = UiText.Resource(UiR.string.settings_dark_theme), + valueClass = Int::class, + defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME, + titles = darkThemeValues.values.toList(), + values = darkThemeValues.keys.toList() + ).apply { + textProvider = TextProvider { item -> + val darkThemeValue = darkThemeValues[item.value] - UiText.ResourceParams( - value = UiR.string.settings_dark_theme_current_value, - args = listOf( - darkThemeValue - ?: UiText.Resource(UiR.string.settings_dark_theme_current_value_unknown) - ) + UiText.ResourceParams( + value = UiR.string.settings_dark_theme_current_value, + args = listOf( + darkThemeValue + ?: UiText.Resource(UiR.string.settings_dark_theme_current_value_unknown) ) - } + ) } - val appearanceUseAmoledDarkTheme = SettingsItem.Switch.build( - key = SettingsKeys.KEY_APPEARANCE_AMOLED_THEME, - defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME, - title = UiText.Resource(UiR.string.settings_amoled_dark_theme), - summary = UiText.Resource(UiR.string.settings_amoled_dark_theme_description) - ) - val appearanceUseDynamicColors = SettingsItem.Switch.build( - key = SettingsKeys.KEY_USE_DYNAMIC_COLORS, - title = UiText.Resource(UiR.string.settings_dynamic_colors), - isVisible = isSdkAtLeast(Build.VERSION_CODES.S), - summary = UiText.Resource(UiR.string.settings_dynamic_colors_description), - defaultValue = SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - - val appearanceLanguage = SettingsItem.TitleSummary.build( - key = SettingsKeys.KEY_APPEARANCE_LANGUAGE, - title = UiText.Resource(UiR.string.settings_application_language), - ) - - val featuresTitle = SettingsItem.Title.build( - key = "features", - title = UiText.Resource(UiR.string.settings_features_title) - ) - val featuresFastText = SettingsItem.TextField.build( - key = SettingsKeys.KEY_FEATURES_FAST_TEXT, - title = UiText.Resource(UiR.string.settings_features_fast_text_title), - defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT - ).apply { - summaryProvider = SettingsItem.SummaryProvider { settingsItem -> - UiText.ResourceParams( - UiR.string.pref_message_fast_text_summary, - listOf(settingsItem.value?.ifEmpty { null }) - ) - } - } - val debugLongPollBackground = SettingsItem.Switch.build( - key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, - title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title), - summary = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary) - ) - - val activityTitle = SettingsItem.Title.build( - key = "activity", - title = UiText.Resource(UiR.string.settings_activity_title) - ) - val visibilitySendOnlineStatus = SettingsItem.Switch.build( - key = SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, - defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS, - title = UiText.Resource(UiR.string.settings_activity_send_online_title), - summary = UiText.Resource(UiR.string.settings_activity_send_online_summary) - ) - - val debugTitle = SettingsItem.Title.build( - key = "debug", - title = UiText.Resource(UiR.string.settings_debug_title) - ) - val debugPerformCrash = SettingsItem.TitleSummary.build( - key = SettingsKeys.KEY_DEBUG_PERFORM_CRASH, - title = UiText.Simple("Perform crash"), - summary = UiText.Simple("App will be crashed. Obviously") - ) - val debugShowCrashAlert = SettingsItem.Switch.build( - key = SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, - defaultValue = true, - title = UiText.Simple("Show alert after crash"), - summary = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)") - ) - val debugUseBlur = SettingsItem.Switch.build( - key = SettingsKeys.KEY_APPEARANCE_BLUR, - defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_BLUR, - title = UiText.Simple("[WIP] Use blur"), - summary = UiText.Simple("Adds blur wherever possible\nOn android 11 and older will have transparency instead of blurring"), - ) - val debugShowEmojiButton = SettingsItem.Switch.build( - key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON, - title = UiText.Simple("Show emoji button"), - summary = UiText.Simple("Show emoji button in chat panel"), - defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON - ) - - val debugHideDebugList = SettingsItem.TitleSummary.build( - key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, - title = UiText.Simple("Hide debug list") - ) - - val accountList = listOf( - accountTitle, - accountLogOut - ) - val generalList = listOf( - generalTitle, - generalUseContactNames - ) - val appearanceList = listOf( - appearanceTitle, - appearanceMultiline, - appearanceDarkTheme, - appearanceUseAmoledDarkTheme, - appearanceUseDynamicColors, - appearanceLanguage - ) - val featuresList = listOf( - featuresTitle, - featuresFastText - ) - val visibilityList = listOf( - activityTitle, - visibilitySendOnlineStatus, - ) - val debugList = mutableListOf>() - listOf( - debugTitle, - debugPerformCrash, - debugShowCrashAlert, - debugLongPollBackground, - debugUseBlur, - debugShowEmojiButton - ).forEach(debugList::add) - - debugList += debugHideDebugList - - val settingsList = mutableListOf>() - listOf( - accountList, - generalList, - appearanceList, - featuresList, - visibilityList, - debugList, - ).forEach(settingsList::addAll) - - if (!isDebugSettingsShown()) { - settingsList.removeAll(debugList) - } - - screenState.setValue { old -> old.copy(settings = settingsList) } } + val appearanceUseAmoledDarkTheme = SettingsItem.Switch( + key = SettingsKeys.KEY_APPEARANCE_AMOLED_THEME, + defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME, + title = UiText.Resource(UiR.string.settings_amoled_dark_theme), + text = UiText.Resource(UiR.string.settings_amoled_dark_theme_description) + ) + val appearanceUseDynamicColors = SettingsItem.Switch( + key = SettingsKeys.KEY_USE_DYNAMIC_COLORS, + title = UiText.Resource(UiR.string.settings_dynamic_colors), + isVisible = isSdkAtLeast(Build.VERSION_CODES.S), + text = UiText.Resource(UiR.string.settings_dynamic_colors_description), + defaultValue = SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS + ) + + val appearanceLanguage = SettingsItem.TitleText( + key = SettingsKeys.KEY_APPEARANCE_LANGUAGE, + title = UiText.Resource(UiR.string.settings_application_language), + ) + + val featuresTitle = SettingsItem.Title( + key = "features", + title = UiText.Resource(UiR.string.settings_features_title) + ) + val featuresFastText = SettingsItem.TextField( + key = SettingsKeys.KEY_FEATURES_FAST_TEXT, + title = UiText.Resource(UiR.string.settings_features_fast_text_title), + defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT + ).apply { + textProvider = TextProvider { settingsItem -> + UiText.ResourceParams( + UiR.string.pref_message_fast_text_summary, + listOf(settingsItem.value) + ) + } + } + val debugLongPollBackground = SettingsItem.Switch( + key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, + title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title), + text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary) + ) + + val activityTitle = SettingsItem.Title( + key = "activity", + title = UiText.Resource(UiR.string.settings_activity_title) + ) + val visibilitySendOnlineStatus = SettingsItem.Switch( + key = SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, + defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS, + title = UiText.Resource(UiR.string.settings_activity_send_online_title), + text = UiText.Resource(UiR.string.settings_activity_send_online_summary) + ) + + val debugTitle = SettingsItem.Title( + key = "debug", + title = UiText.Resource(UiR.string.settings_debug_title) + ) + val debugPerformCrash = SettingsItem.TitleText( + key = SettingsKeys.KEY_DEBUG_PERFORM_CRASH, + title = UiText.Simple("Perform crash"), + text = UiText.Simple("App will be crashed. Obviously") + ) + val debugShowCrashAlert = SettingsItem.Switch( + key = SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, + defaultValue = true, + title = UiText.Simple("Show alert after crash"), + text = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)") + ) + val debugUseBlur = SettingsItem.Switch( + key = SettingsKeys.KEY_APPEARANCE_BLUR, + defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_BLUR, + title = UiText.Simple("[WIP] Use blur"), + text = UiText.Simple("Adds blur wherever possible\nOn android 11 and older will have transparency instead of blurring"), + ) + val debugShowEmojiButton = SettingsItem.Switch( + key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON, + title = UiText.Simple("Show emoji button"), + text = UiText.Simple("Show emoji button in chat panel"), + defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON + ) + + val debugHideDebugList = SettingsItem.TitleText( + key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, + title = UiText.Simple("Hide debug list") + ) + + val accountList = listOf( + accountTitle, + accountLogOut + ) + val generalList = listOf( + generalTitle, + generalUseContactNames + ) + val appearanceList = listOf( + appearanceTitle, + appearanceMultiline, + appearanceDarkTheme, + appearanceUseAmoledDarkTheme, + appearanceUseDynamicColors, + appearanceLanguage + ) + val featuresList = listOf( + featuresTitle, + featuresFastText + ) + val visibilityList = listOf( + activityTitle, + visibilitySendOnlineStatus, + ) + val debugList = mutableListOf>() + listOf( + debugTitle, + debugPerformCrash, + debugShowCrashAlert, + debugLongPollBackground, + debugUseBlur, + debugShowEmojiButton + ).forEach(debugList::add) + + debugList += debugHideDebugList + + val settingsList = mutableListOf>() + listOf( + accountList, + generalList, + appearanceList, + featuresList, + visibilityList, + debugList, + ).forEach(settingsList::addAll) + + if (!isDebugSettingsShown()) { + settingsList.removeAll(debugList) + } + + emitSettings(settingsList) + } + + private fun emitSettings(newSettings: List>) { + settings.update { newSettings } + + val uiSettings = newSettings.map { item -> + item.asPresentation(resources) + } + + screenState.setValue { old -> old.copy(settings = uiSettings) } } } -sealed interface HapticType { - data object LongPress : HapticType - data object Reject : HapticType - data object None : HapticType +enum class HapticType { + LONG_PRESS, REJECT; - fun getHaptic(): Int { - return when (this) { - LongPress -> HapticFeedbackConstantsCompat.LONG_PRESS - Reject -> HapticFeedbackConstantsCompat.REJECT - None -> -1 - } + fun getHaptic(): Int = when (this) { + LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS + REJECT -> HapticFeedbackConstantsCompat.REJECT } } diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsItem.kt index f1368232..c0787b35 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsItem.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsItem.kt @@ -1,223 +1,250 @@ package com.meloda.app.fast.settings.model +import android.content.res.Resources +import androidx.compose.runtime.Immutable import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.common.parseString import com.meloda.app.fast.datastore.SettingsController -import kotlin.properties.Delegates +import kotlin.reflect.KClass -// TODO: 24/12/2023, Danil Nikolaev: refactor -sealed class SettingsItem( - open val key: String, +@Immutable +sealed class SettingsItem( + val key: String, + value: T, + defaultValue: T? ) { - var onTitleChanged: ((newTitle: UiText?) -> Unit)? = null + private val haveValue + get() = this::class !in listOf>( + Title::class, + TitleText::class + ) - var title: UiText? by Delegates.observable(null) { _, _, newValue -> - onTitleChanged?.invoke(newValue) + init { + require(key.trim().isNotEmpty()) { + "Key must not be empty" + } + + require(!haveValue || defaultValue != null) { + "Default value must not be null" + } } - var onSummaryChanged: ((newSummary: UiText?) -> Unit)? = null + var isVisible: Boolean = true - var summary: UiText? by Delegates.observable(null) { _, _, newValue -> - onSummaryChanged?.invoke(newValue) - } + var isEnabled: Boolean = true - var onEnabledStateChanged: ((newEnabled: Boolean) -> Unit)? = null + var value: T = value + protected set(newValue) { + field = newValue - var isEnabled: Boolean by Delegates.observable(true) { _, _, newValue -> - onEnabledStateChanged?.invoke(newValue) - } + SettingsController.put(key, newValue) + } - var onVisibleStateChanged: ((newVisible: Boolean) -> Unit)? = null + var title: UiText? = null - var isVisible: Boolean by Delegates.observable(true) { _, _, newValue -> - onVisibleStateChanged?.invoke(newValue) - } + var text: UiText? = null - var onValueChanged: ((newValue: Value?) -> Unit)? = null + var textProvider: TextProvider>? = null + set(value) { + field = value + updateText() + } - var value: Value? by Delegates.observable(null) { _, oldValue, newValue -> - if (key.trim().isEmpty() || oldValue == newValue) return@observable - - onValueChanged?.invoke(newValue) - - SettingsController.put(key, newValue) + fun updateText() { + textProvider?.provideText(this)?.let { newText -> text = newText } } @Suppress("UNCHECKED_CAST") - protected fun getValueFromPreferences( + fun updateValue(newValue: Any?) { + if (!haveValue) throw IllegalStateException("This item does not have a value") + value = newValue as T + } + + class Title( key: String, - classToGet: Class, - defaultValue: Any? - ): T? { - return when (classToGet) { - String::class.java -> SettingsController.getString(key, defaultValue as? String) + title: UiText, + isVisible: Boolean = true + ) : SettingsItem( + key = key, + value = Unit, + defaultValue = null + ) { - Boolean::class.java -> { - SettingsController.getBoolean(key, defaultValue as? Boolean == true) - } - - Int::class.java -> SettingsController.getInt(key, defaultValue as? Int ?: -1) - Long::class.java -> SettingsController.getLong(key, defaultValue as? Long ?: -1) - Float::class.java -> SettingsController.getFloat(key, defaultValue as? Float ?: -1f) - else -> null - }.let { value -> value as? T } - } - - var defaultValue: Value? = null - - var titleProvider: TitleProvider>? by Delegates.observable(null) { _, _, _ -> - updateTitle() - } - - var summaryProvider: SummaryProvider>? by Delegates.observable(null) { _, _, _ -> - updateSummary() - } - - fun updateTitle() { - titleProvider?.provideTitle(this)?.let { newTitle -> title = newTitle } - } - - fun updateSummary() { - summaryProvider?.provideSummary(this)?.let { newSummary -> summary = newSummary } - } - - fun requireValue() = requireNotNull(value) - - fun interface TitleProvider> { - fun provideTitle(settingsItem: Item): UiText? - } - - fun interface SummaryProvider> { - fun provideSummary(settingsItem: Item): UiText? - } - - data class Title(override val key: String) : SettingsItem(key) { - - companion object { - fun build( - key: String, - title: UiText, - isEnabled: Boolean = true, - isVisible: Boolean = true, - builder: Title.() -> Unit = {} - ): Title { - return Title(key).apply { - this.title = title - this.isEnabled = isEnabled - this.isVisible = isVisible - }.apply(builder) - } + init { + this.title = title + this.isVisible = isVisible } } - data class TitleSummary(override val key: String) : SettingsItem(key) { + class TitleText( + key: String, + title: UiText? = null, + text: UiText? = null, + isVisible: Boolean = true, + isEnabled: Boolean = true + ) : SettingsItem( + key = key, + value = Unit, + defaultValue = null + ) { - companion object { - fun build( - key: String, - title: UiText? = null, - summary: UiText? = null, - isEnabled: Boolean = true, - isVisible: Boolean = true, - builder: TitleSummary.() -> Unit = {} - ): TitleSummary { - return TitleSummary(key).apply { - this.title = title - this.summary = summary - this.isEnabled = isEnabled - this.isVisible = isVisible - }.apply(builder) + init { + require(title != null || text != null) { + "Either title or text must not be null" } + + this.title = title + this.text = textProvider?.provideText(this) ?: text + this.isVisible = isVisible + this.isEnabled = isEnabled } } - data class TextField(override val key: String) : SettingsItem(key) { + class Switch( + key: String, + defaultValue: Boolean, + title: UiText? = null, + text: UiText? = null, + isVisible: Boolean = true, + isEnabled: Boolean = true, + isChecked: Boolean? = null + ) : SettingsItem( + key = key, + value = isChecked ?: getCurrentValue(key, defaultValue), + defaultValue = defaultValue + ) { - companion object { - fun build( - key: String, - title: UiText? = null, - summary: UiText? = null, - defaultValue: String? = null, - isEnabled: Boolean = true, - isVisible: Boolean = true, - builder: TextField.() -> Unit = {} - ): TextField { - return TextField(key).apply { - this.title = title - this.summary = summary - this.defaultValue = defaultValue - this.isEnabled = isEnabled - this.isVisible = isVisible - - this.value = SettingsController.getString(key, defaultValue) - }.apply(builder) + init { + require(title != null || text != null) { + "Either title or text must not be null" } + + this.title = title + this.text = textProvider?.provideText(this) ?: text + this.isVisible = isVisible + this.isEnabled = isEnabled } } - data class Switch(override val key: String) : SettingsItem(key) { + class TextField( + key: String, + defaultValue: String, + title: UiText? = null, + text: UiText? = null, + isVisible: Boolean = true, + isEnabled: Boolean = true, + fieldText: String? = null + ) : SettingsItem( + key = key, + value = fieldText ?: getCurrentValue(key, defaultValue), + defaultValue = defaultValue + ) { - companion object { - - fun build( - key: String, - title: UiText? = null, - summary: UiText? = null, - isEnabled: Boolean = true, - isChecked: Boolean? = null, - isVisible: Boolean = true, - defaultValue: Boolean? = null, - builder: Switch.() -> Unit = {} - ): Switch { - return Switch(key).apply { - this.title = title - this.summary = summary - this.isEnabled = isEnabled - this.defaultValue = defaultValue - this.isVisible = isVisible - - this.value = defaultValue - ?.let { value -> SettingsController.getBoolean(key, value) } - ?: isChecked - }.apply(builder) + init { + require(title != null || text != null) { + "Either title or text must not be null" } + + this.title = title + this.text = textProvider?.provideText(this) ?: text + this.isVisible = isVisible + this.isEnabled = isEnabled } } - data class ListItem( - override val key: String, - val values: List, - val valueTitles: List - ) : SettingsItem(key) { + class ListItem( + key: String, + defaultValue: T, + valueClass: KClass, + title: UiText? = null, + text: UiText? = null, + isVisible: Boolean = true, + isEnabled: Boolean = true, + selectedValue: T? = null, + val titles: List, + val values: List + ) : SettingsItem( + key = key, + value = selectedValue ?: SettingsController.get(valueClass, key, defaultValue), + defaultValue = defaultValue + ) { - companion object { - fun build( - key: String, - title: UiText? = null, - summary: UiText? = null, - isEnabled: Boolean = true, - isVisible: Boolean = true, - values: List, - valueTitles: List, - defaultValue: Int? = null, - selectedIndex: Int? = null, - builder: ListItem.() -> Unit = {} - ): ListItem { - return ListItem( - key = key, - values = values, - valueTitles = valueTitles - ).apply { - this.title = title - this.summary = summary - this.isEnabled = isEnabled - this.isVisible = isVisible - - this.value = defaultValue - ?.let { value -> getValueFromPreferences(key, Int::class.java, value) } - ?: selectedIndex?.let { values[it] } - }.apply(builder) + init { + require(title != null || text != null) { + "Either title or text must not be null" } + + require(titles.isNotEmpty()) { + "titles must not be empty" + } + + this.title = title + this.text = textProvider?.provideText(this) ?: text + this.isVisible = isVisible + this.isEnabled = isEnabled + } + } + + fun asPresentation(resources: Resources): UiItem = when (val item = this) { + is Title -> { + UiItem.Title( + key = item.key, + title = item.title.parseString(resources).orEmpty(), + isVisible = item.isVisible + ) + } + + is TitleText -> { + UiItem.TitleText( + key = item.key, + title = item.title.parseString(resources), + text = item.text.parseString(resources), + isVisible = item.isVisible, + isEnabled = item.isEnabled + ) + } + + is Switch -> { + UiItem.Switch( + key = item.key, + title = item.title.parseString(resources), + text = item.text.parseString(resources), + isVisible = item.isVisible, + isEnabled = item.isEnabled, + isChecked = item.value + ) + } + + is TextField -> { + UiItem.TextField( + key = item.key, + title = item.title.parseString(resources), + text = item.text.parseString(resources), + isVisible = item.isVisible, + isEnabled = item.isEnabled, + fieldText = item.value + ) + } + + is ListItem<*> -> { + UiItem.List( + key = item.key, + title = item.title.parseString(resources), + text = item.text.parseString(resources), + isVisible = item.isVisible, + isEnabled = item.isEnabled, + selectedValue = item.value, + titles = item.titles.mapNotNull { it.parseString(resources) }, + values = item.values + ) } } } + +private inline fun getCurrentValue(key: String, defaultValue: T): T { + if (T::class == Nothing::class) { + throw IllegalStateException("Items with Nothing does not have a value") + } else { + return SettingsController.get(key, defaultValue) + } +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsListeners.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsListeners.kt deleted file mode 100644 index 68ad12d3..00000000 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsListeners.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.app.fast.settings.model - -fun interface OnSettingsClickListener { - fun onClick(key: String) -} - -fun interface OnSettingsLongClickListener { - fun onLongClick(key: String) -} - -fun interface OnSettingsChangeListener { - fun onChange(key: String, newValue: Any?) -} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsScreenState.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsScreenState.kt index dd84df3f..20fa4955 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsScreenState.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsScreenState.kt @@ -7,8 +7,8 @@ import com.meloda.app.fast.settings.HapticType @Immutable data class SettingsScreenState( val showOptions: SettingsShowOptions, - val settings: List>, - val useHaptics: HapticType, + val settings: List, + val useHaptics: HapticType?, val isNeedToRequestNotificationPermission: Boolean, val showDebugOptions: Boolean ) { @@ -17,7 +17,7 @@ data class SettingsScreenState( val EMPTY: SettingsScreenState = SettingsScreenState( showOptions = SettingsShowOptions.EMPTY, settings = emptyList(), - useHaptics = HapticType.None, + useHaptics = null, isNeedToRequestNotificationPermission = false, showDebugOptions = isDebugSettingsShown() ) diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TextProvider.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TextProvider.kt new file mode 100644 index 00000000..a01a5166 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TextProvider.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.settings.model + +import com.meloda.app.fast.common.UiText + +fun interface TextProvider> { + fun provideText(item: S): UiText? +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TitleProvider.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TitleProvider.kt new file mode 100644 index 00000000..f90dd816 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/TitleProvider.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.settings.model + +import com.meloda.app.fast.common.UiText + +fun interface TitleProvider> { + fun provideTitle(item: S): UiText? +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/UiItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/UiItem.kt new file mode 100644 index 00000000..3163cd7f --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/UiItem.kt @@ -0,0 +1,53 @@ +package com.meloda.app.fast.settings.model + +import androidx.compose.runtime.Immutable + +typealias StringList = List +typealias TypeList = List + +@Immutable +sealed class UiItem(open val key: String) { + + data class Title( + override val key: String, + val title: String, + val isVisible: Boolean + ) : UiItem(key) + + data class TitleText( + override val key: String, + val title: String?, + val text: String?, + val isVisible: Boolean, + val isEnabled: Boolean + ) : UiItem(key) + + data class Switch( + override val key: String, + val title: String?, + val text: String?, + val isVisible: Boolean, + val isEnabled: Boolean, + val isChecked: Boolean + ) : UiItem(key) + + data class TextField( + override val key: String, + val title: String?, + val text: String?, + val isVisible: Boolean, + val isEnabled: Boolean, + val fieldText: String + ) : UiItem(key) + + data class List( + override val key: String, + val title: String?, + val text: String?, + val isVisible: Boolean, + val isEnabled: Boolean, + val selectedValue: T, + val titles: StringList, + val values: TypeList + ) : UiItem(key) +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt index 0cfd1fb5..f5798ed5 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt @@ -3,11 +3,7 @@ package com.meloda.app.fast.settings.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.meloda.app.fast.datastore.SettingsKeys -import com.meloda.app.fast.model.BaseError -import com.meloda.app.fast.settings.model.OnSettingsClickListener import com.meloda.app.fast.settings.presentation.SettingsRoute -import com.meloda.app.fast.settings.presentation.SettingsScreen import kotlinx.serialization.Serializable @Serializable diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt index f087cba5..bea533be 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt @@ -38,16 +38,15 @@ import com.meloda.app.fast.common.UserConfig import com.meloda.app.fast.datastore.SettingsKeys import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.datastore.isUsingDarkMode -import com.meloda.app.fast.settings.HapticType import com.meloda.app.fast.settings.SettingsViewModel import com.meloda.app.fast.settings.SettingsViewModelImpl -import com.meloda.app.fast.settings.model.SettingsItem import com.meloda.app.fast.settings.model.SettingsScreenState -import com.meloda.app.fast.settings.presentation.items.EditTextSettingsItem -import com.meloda.app.fast.settings.presentation.items.ListSettingsItem -import com.meloda.app.fast.settings.presentation.items.SwitchSettingsItem -import com.meloda.app.fast.settings.presentation.items.TitleSettingsItem -import com.meloda.app.fast.settings.presentation.items.TitleSummarySettingsItem +import com.meloda.app.fast.settings.model.UiItem +import com.meloda.app.fast.settings.presentation.item.ListItem +import com.meloda.app.fast.settings.presentation.item.SwitchItem +import com.meloda.app.fast.settings.presentation.item.TextFieldItem +import com.meloda.app.fast.settings.presentation.item.TitleItem +import com.meloda.app.fast.settings.presentation.item.TitleTextItem import com.meloda.app.fast.ui.components.ActionInvokeDismiss import com.meloda.app.fast.ui.components.MaterialDialog import com.meloda.app.fast.ui.theme.LocalTheme @@ -90,6 +89,8 @@ fun SettingsRoute( }, onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked, onSettingsItemValueChanged = { key, newValue -> + viewModel.onSettingsItemChanged(key, newValue) + when (key) { SettingsKeys.KEY_APPEARANCE_DARK_THEME -> { val newMode = newValue as? Int ?: 0 @@ -104,8 +105,6 @@ fun SettingsRoute( userSettings.useDarkThemeChanged(isUsing) } - - else -> viewModel.onSettingsItemChanged(key, newValue) } } ) @@ -139,7 +138,7 @@ fun SettingsScreen( val hapticType = screenState.useHaptics LaunchedEffect(hapticType) { - if (hapticType != HapticType.None) { + if (hapticType != null) { view.performHapticFeedback(hapticType.getHaptic()) onHapticPerformed() } @@ -201,60 +200,59 @@ fun SettingsScreen( item { Spacer(modifier = Modifier.height(padding.calculateTopPadding())) } + items( items = screenState.settings, - key = { item -> item.key }, + key = UiItem::key, contentType = { item -> when (item) { - is SettingsItem.ListItem -> "list_item" - is SettingsItem.Switch -> "switch" - is SettingsItem.TextField -> "text_field" - is SettingsItem.Title -> "title" - is SettingsItem.TitleSummary -> "title_summary" + is UiItem.Title -> "title" + is UiItem.TitleText -> "title_text" + is UiItem.Switch -> "switch" + is UiItem.TextField -> "text_field" + is UiItem.List<*> -> "list" } } ) { item -> when (item) { - is SettingsItem.Title -> TitleSettingsItem( - item = item, - isMultiline = currentTheme.multiline, - modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) - ) + is UiItem.Title -> { + TitleItem(item = item) + } - is SettingsItem.TitleSummary -> TitleSummarySettingsItem( - item = item, - isMultiline = currentTheme.multiline, - onSettingsClickListener = onSettingsItemClicked, - onSettingsLongClickListener = onSettingsItemLongClicked, - modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) - ) + is UiItem.TitleText -> { + TitleTextItem( + item = item, + onClick = onSettingsItemClicked, + onLongClick = onSettingsItemLongClicked + ) + } - is SettingsItem.Switch -> SwitchSettingsItem( - item = item, - isMultiline = currentTheme.multiline, - onSettingsClickListener = onSettingsItemClicked, - onSettingsLongClickListener = onSettingsItemLongClicked, - onSettingsChangeListener = onSettingsItemValueChanged, - modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) - ) + is UiItem.Switch -> { + SwitchItem( + item = item, + onClick = { onSettingsItemClicked(item.key) }, + onLongClick = { onSettingsItemLongClicked(item.key) }, + onChanged = { onSettingsItemValueChanged(item.key, it) } + ) + } - is SettingsItem.TextField -> EditTextSettingsItem( - item = item, - isMultiline = currentTheme.multiline, - onSettingsClickListener = onSettingsItemClicked, - onSettingsLongClickListener = onSettingsItemLongClicked, - onSettingsChangeListener = onSettingsItemValueChanged, - modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) - ) + is UiItem.TextField -> { + TextFieldItem( + item = item, + onClick = { onSettingsItemClicked(item.key) }, + onLongClick = { onSettingsItemLongClicked(item.key) }, + onChanged = { onSettingsItemValueChanged(item.key, it) } + ) + } - is SettingsItem.ListItem -> ListSettingsItem( - item = item, - isMultiline = currentTheme.multiline, - onSettingsClickListener = onSettingsItemClicked, - onSettingsLongClickListener = onSettingsItemLongClicked, - onSettingsChangeListener = onSettingsItemValueChanged, - modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) - ) + is UiItem.List<*> -> { + ListItem( + item = item, + onClick = { onSettingsItemClicked(item.key) }, + onLongClick = { onSettingsItemLongClicked(item.key) }, + onChanged = { onSettingsItemValueChanged(item.key, it) } + ) + } } } diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/ListItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/ListItem.kt new file mode 100644 index 00000000..2ae76f16 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/ListItem.kt @@ -0,0 +1,145 @@ +package com.meloda.app.fast.settings.presentation.item + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.settings.model.UiItem +import com.meloda.app.fast.ui.R +import com.meloda.app.fast.ui.basic.ContentAlpha +import com.meloda.app.fast.ui.basic.LocalContentAlpha +import com.meloda.app.fast.ui.components.ActionInvokeDismiss +import com.meloda.app.fast.ui.components.MaterialDialog +import com.meloda.app.fast.ui.components.SelectionType +import com.meloda.app.fast.ui.theme.LocalTheme +import com.meloda.app.fast.ui.util.ImmutableList +import com.meloda.app.fast.ui.util.ImmutableList.Companion.toImmutableList + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ListItem( + item: UiItem.List<*>, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, + onChanged: (newValue: Any?) -> Unit +) { + if (!item.isVisible) return + + var showDialog by rememberSaveable { + mutableStateOf(false) + } + + val currentTheme = LocalTheme.current + + Row( + modifier = modifier + .heightIn(min = 56.dp) + .fillMaxWidth() + .animateContentSize() + .combinedClickable( + enabled = item.isEnabled, + onClick = { + onClick() + showDialog = true + }, + onLongClick = onLongClick, + ) + ) { + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Spacer(modifier = Modifier.height(14.dp)) + LocalContentAlpha( + alpha = if (item.isEnabled) ContentAlpha.high else ContentAlpha.disabled + ) { + item.title?.let { title -> + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + LocalContentAlpha( + alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled + ) { + item.text?.let { text -> + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + Spacer(modifier = Modifier.height(14.dp)) + } + Spacer(modifier = Modifier.width(16.dp)) + } + + if (showDialog) { + ListAlertDialog( + item = item, + onDismissAction = { showDialog = false }, + onConfirmButtonClicked = onChanged + ) + } +} + + +@Composable +fun ListAlertDialog( + item: UiItem.List<*>, + onDismissAction: () -> Unit, + onConfirmButtonClicked: (newValue: Any?) -> Unit +) { + val currentValueIndex = remember { + item.values.indexOf(item.selectedValue) + } + var selectedOptionIndex by rememberSaveable { + mutableIntStateOf(currentValueIndex) + } + + MaterialDialog( + onDismissRequest = onDismissAction, + title = item.title, + items = item.titles.toImmutableList(), + preSelectedItems = ImmutableList.of(selectedOptionIndex), + selectionType = SelectionType.Single, + onItemClick = { newIndex -> selectedOptionIndex = newIndex }, + confirmText = stringResource(id = R.string.ok), + confirmAction = { + if (currentValueIndex != selectedOptionIndex) { + onConfirmButtonClicked(item.values[selectedOptionIndex]) + } + }, + actionInvokeDismiss = ActionInvokeDismiss.Always + ) +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/SwitchItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/SwitchItem.kt new file mode 100644 index 00000000..53330083 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/SwitchItem.kt @@ -0,0 +1,98 @@ +package com.meloda.app.fast.settings.presentation.item + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.settings.model.UiItem +import com.meloda.app.fast.ui.basic.ContentAlpha +import com.meloda.app.fast.ui.basic.LocalContentAlpha +import com.meloda.app.fast.ui.theme.LocalTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SwitchItem( + item: UiItem.Switch, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, + onChanged: (isChecked: Boolean) -> Unit +) { + if (!item.isVisible) return + + val currentTheme = LocalTheme.current + + Row( + modifier = modifier + .fillMaxSize() + .heightIn(min = 56.dp) + .animateContentSize() + .combinedClickable( + enabled = item.isEnabled, + onClick = { + onClick() + onChanged(!item.isChecked) + }, + onLongClick = onLongClick, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier.weight(1f) + ) { + Spacer(modifier = Modifier.height(14.dp)) + LocalContentAlpha( + alpha = if (item.isEnabled) ContentAlpha.high else ContentAlpha.disabled + ) { + item.title?.let { title -> + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + LocalContentAlpha( + alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled + ) { + item.text?.let { text -> + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + Spacer(modifier = Modifier.height(14.dp)) + } + Row { + Spacer(modifier = Modifier.width(16.dp)) + Switch( + enabled = item.isEnabled, + checked = item.isChecked, + onCheckedChange = null + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TextFieldSettingsItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TextFieldItem.kt similarity index 59% rename from feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TextFieldSettingsItem.kt rename to feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TextFieldItem.kt index 89285817..29d886a6 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TextFieldSettingsItem.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TextFieldItem.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast.settings.presentation.items +package com.meloda.app.fast.settings.presentation.item import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi @@ -20,78 +20,63 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.meloda.app.fast.common.UiText -import com.meloda.app.fast.settings.model.OnSettingsChangeListener -import com.meloda.app.fast.settings.model.OnSettingsClickListener -import com.meloda.app.fast.settings.model.OnSettingsLongClickListener -import com.meloda.app.fast.settings.model.SettingsItem +import com.meloda.app.fast.settings.model.UiItem +import com.meloda.app.fast.ui.R import com.meloda.app.fast.ui.basic.ContentAlpha import com.meloda.app.fast.ui.basic.LocalContentAlpha +import com.meloda.app.fast.ui.components.ActionInvokeDismiss import com.meloda.app.fast.ui.components.MaterialDialog -import com.meloda.app.fast.ui.util.getString -import com.meloda.app.fast.ui.R as UiR +import com.meloda.app.fast.ui.theme.LocalTheme @OptIn(ExperimentalFoundationApi::class) @Composable -fun EditTextSettingsItem( - item: SettingsItem.TextField, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener, - modifier: Modifier +fun TextFieldItem( + item: UiItem.TextField, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, + onChanged: (fieldText: String) -> Unit ) { - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } + if (!item.isVisible) return - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } + val currentTheme = LocalTheme.current - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - var showDialog by remember { + var showDialog by rememberSaveable { mutableStateOf(false) } if (showDialog) { EditTextAlert( item = item, - onSettingsChangeListener = { key, newValue -> - summary = item.summaryProvider?.provideSummary(item) - onSettingsChangeListener.onChange(key, newValue) - }, - onDismiss = { showDialog = false } + onAlertConfirmClicked = onChanged, + onDismiss = { showDialog = false }, ) } - if (!isVisible) return Row( modifier = modifier .heightIn(min = 56.dp) .fillMaxWidth() .animateContentSize() .combinedClickable( - enabled = isEnabled, + enabled = item.isEnabled, onClick = { - onSettingsClickListener.onClick(item.key) + onClick() showDialog = true }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, + onLongClick = onLongClick, ) ) { Spacer(modifier = Modifier.width(16.dp)) @@ -102,26 +87,26 @@ fun EditTextSettingsItem( ) { Spacer(modifier = Modifier.height(14.dp)) LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.high else ContentAlpha.disabled + alpha = if (item.isEnabled) ContentAlpha.high else ContentAlpha.disabled ) { - title?.getString()?.let { title -> + item.title?.let { title -> Text( text = title, style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, overflow = TextOverflow.Ellipsis, ) } } LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.medium else ContentAlpha.disabled + alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled ) { - summary?.getString()?.let { summary -> + item.text?.let { text -> Text( - text = summary, + text = text, style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, overflow = TextOverflow.Ellipsis, ) } @@ -132,32 +117,27 @@ fun EditTextSettingsItem( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun EditTextAlert( - item: SettingsItem.TextField, - onSettingsChangeListener: OnSettingsChangeListener, + item: UiItem.TextField, + onAlertConfirmClicked: (newValue: String) -> Unit, onDismiss: () -> Unit ) { val (textFieldFocusable) = FocusRequester.createRefs() var textFieldValue by remember { - mutableStateOf(TextFieldValue(item.value.orEmpty())) + mutableStateOf(TextFieldValue(item.fieldText)) } MaterialDialog( + onDismissRequest = onDismiss, title = item.title, - confirmText = UiText.Resource(UiR.string.ok), + confirmText = stringResource(id = R.string.ok), confirmAction = { - val newValue = textFieldValue.text.trim() - - if (item.value != newValue) { - item.value = newValue - onSettingsChangeListener.onChange(item.key, newValue) - } + onAlertConfirmClicked(textFieldValue.text.trim()) }, - cancelText = UiText.Resource(UiR.string.cancel), - onDismissAction = onDismiss + cancelText = stringResource(id = R.string.cancel), + actionInvokeDismiss = ActionInvokeDismiss.Always ) { Row(modifier = Modifier.fillMaxWidth()) { Spacer(modifier = Modifier.width(20.dp)) diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleItem.kt new file mode 100644 index 00000000..4192106f --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleItem.kt @@ -0,0 +1,38 @@ +package com.meloda.app.fast.settings.presentation.item + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.settings.model.UiItem +import com.meloda.app.fast.ui.theme.LocalTheme + +@Composable +fun TitleItem( + item: UiItem.Title, + modifier: Modifier = Modifier +) { + if (!item.isVisible) return + + val currentTheme = LocalTheme.current + + Text( + text = item.title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + modifier = modifier + .padding( + top = 14.dp, + end = 16.dp, + start = 16.dp, + bottom = 4.dp + ) + .animateContentSize(), + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis, + ) +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSummarySettingsItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleTextItem.kt similarity index 53% rename from feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSummarySettingsItem.kt rename to feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleTextItem.kt index 3e22762f..0c0beabd 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSummarySettingsItem.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/item/TitleTextItem.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast.settings.presentation.items +package com.meloda.app.fast.settings.presentation.item import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi @@ -14,52 +14,36 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.meloda.app.fast.settings.model.OnSettingsClickListener -import com.meloda.app.fast.settings.model.OnSettingsLongClickListener -import com.meloda.app.fast.settings.model.SettingsItem +import com.meloda.app.fast.settings.model.UiItem import com.meloda.app.fast.ui.basic.ContentAlpha import com.meloda.app.fast.ui.basic.LocalContentAlpha -import com.meloda.app.fast.ui.util.getString +import com.meloda.app.fast.ui.theme.LocalTheme @OptIn(ExperimentalFoundationApi::class) @Composable -fun TitleSummarySettingsItem( - item: SettingsItem.TitleSummary, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - modifier: Modifier +fun TitleTextItem( + item: UiItem.TitleText, + modifier: Modifier = Modifier, + onClick: (key: String) -> Unit = {}, + onLongClick: (key: String) -> Unit = {} ) { - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } + if (!item.isVisible) return - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } + val currentTheme = LocalTheme.current - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return Row( modifier = modifier .heightIn(min = 56.dp) .fillMaxWidth() .animateContentSize() .combinedClickable( - enabled = isEnabled, - onClick = { onSettingsClickListener.onClick(item.key) }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, + enabled = item.isEnabled, + onClick = { onClick(item.key) }, + onLongClick = { onLongClick(item.key) }, ) ) { Spacer(modifier = Modifier.width(16.dp)) @@ -71,26 +55,27 @@ fun TitleSummarySettingsItem( Spacer(modifier = Modifier.height(14.dp)) LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.high else ContentAlpha.disabled + alpha = if (item.isEnabled) ContentAlpha.high + else ContentAlpha.disabled ) { - title?.getString()?.let { title -> + item.title?.let { title -> Text( text = title, style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, overflow = TextOverflow.Ellipsis, ) } } LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.medium else ContentAlpha.disabled + alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled ) { - summary?.getString()?.let { summary -> + item.text?.let { text -> Text( - text = summary, + text = text, style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, + maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1, overflow = TextOverflow.Ellipsis, ) } diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/ListSettingsItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/ListSettingsItem.kt deleted file mode 100644 index b3f98d42..00000000 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/ListSettingsItem.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.meloda.app.fast.settings.presentation.items - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.app.fast.common.UiText -import com.meloda.app.fast.settings.model.OnSettingsChangeListener -import com.meloda.app.fast.settings.model.OnSettingsClickListener -import com.meloda.app.fast.settings.model.OnSettingsLongClickListener -import com.meloda.app.fast.settings.model.SettingsItem -import com.meloda.app.fast.ui.basic.ContentAlpha -import com.meloda.app.fast.ui.basic.LocalContentAlpha -import com.meloda.app.fast.ui.components.ItemsSelectionType -import com.meloda.app.fast.ui.components.MaterialDialog -import com.meloda.app.fast.ui.util.ImmutableList.Companion.toImmutableList -import com.meloda.app.fast.ui.util.getString -import com.meloda.app.fast.ui.R as UiR - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ListSettingsItem( - item: SettingsItem.ListItem, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener, - modifier: Modifier -) { - var showDialog by remember { mutableStateOf(false) } - - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - Row( - modifier = modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .animateContentSize() - .combinedClickable( - enabled = isEnabled, - onClick = { - onSettingsClickListener.onClick(item.key) - showDialog = true - }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.high else ContentAlpha.disabled - ) { - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.medium else ContentAlpha.disabled - ) { - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } - - if (showDialog) { - ListAlertDialog( - onDismissAction = { - showDialog = false - }, - item = item, - onSettingsChangeListener = { key, newValue -> - onSettingsChangeListener.onChange(key, newValue) - item.updateSummary() - } - ) - } -} - -@Composable -fun ListAlertDialog( - onDismissAction: () -> Unit, - item: SettingsItem.ListItem, - onSettingsChangeListener: OnSettingsChangeListener -) { - var selectedOption = item.value - val checkedItem = item.values.indexOf(selectedOption) - - MaterialDialog( - onDismissAction = onDismissAction, - title = item.title, - items = item.valueTitles.toImmutableList(), - preSelectedItems = listOf(checkedItem).toImmutableList(), - itemsSelectionType = ItemsSelectionType.Single, - onItemClick = { index -> - selectedOption = item.values[index] - }, - confirmText = UiText.Resource(UiR.string.ok), - confirmAction = { - if (item.value != selectedOption) { - item.value = selectedOption - onSettingsChangeListener.onChange(item.key, selectedOption) - } - } - ) -} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/SwitchSettingsItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/SwitchSettingsItem.kt deleted file mode 100644 index 0ed66692..00000000 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/SwitchSettingsItem.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.meloda.app.fast.settings.presentation.items - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.app.fast.settings.model.OnSettingsChangeListener -import com.meloda.app.fast.settings.model.OnSettingsClickListener -import com.meloda.app.fast.settings.model.OnSettingsLongClickListener -import com.meloda.app.fast.settings.model.SettingsItem -import com.meloda.app.fast.ui.basic.ContentAlpha -import com.meloda.app.fast.ui.basic.LocalContentAlpha -import com.meloda.app.fast.ui.util.getString - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun SwitchSettingsItem( - item: SettingsItem.Switch, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener, - modifier: Modifier -) { - var isChecked by remember { - mutableStateOf(item.value == true) - } - - val onCheckedChange = { newValue: Boolean -> - isChecked = newValue - - if (item.value != isChecked) { - item.value = isChecked - onSettingsChangeListener.onChange(item.key, isChecked) - } - } - - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - var value by remember { mutableStateOf(item.value) } - item.onValueChanged = { newValue -> - value = newValue - isChecked = newValue == true - } - - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - Row( - modifier = modifier - .fillMaxSize() - .heightIn(min = 56.dp) - .animateContentSize() - .combinedClickable( - enabled = isEnabled, - onClick = { - onSettingsClickListener.onClick(item.key) - onCheckedChange.invoke(!isChecked) - }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f) - ) { - Spacer(modifier = Modifier.height(14.dp)) - LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.high else ContentAlpha.disabled - ) { - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - LocalContentAlpha( - alpha = if (isEnabled) ContentAlpha.medium else ContentAlpha.disabled - ) { - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - Spacer(modifier = Modifier.height(14.dp)) - } - Row { - Spacer(modifier = Modifier.width(16.dp)) - Switch( - enabled = isEnabled, - checked = isChecked, - onCheckedChange = onCheckedChange - ) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSettingsItem.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSettingsItem.kt deleted file mode 100644 index 09adf3a7..00000000 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSettingsItem.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.meloda.app.fast.settings.presentation.items - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.app.fast.settings.model.SettingsItem -import com.meloda.app.fast.ui.util.getString - -@Composable -fun TitleSettingsItem( - item: SettingsItem.Title, - isMultiline: Boolean, - modifier: Modifier -) { - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - - Text( - text = title.getString().orEmpty(), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - modifier = modifier - .padding( - top = 14.dp, - end = 16.dp, - start = 16.dp, - bottom = 4.dp - ) - .animateContentSize(), - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) -}