settings refactoring

This commit is contained in:
2024-07-15 22:34:00 +03:00
parent 304c630d1d
commit 789283fcff
20 changed files with 942 additions and 907 deletions
@@ -3,6 +3,7 @@ package com.meloda.app.fast.datastore
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.KClass
object SettingsController { object SettingsController {
@@ -39,6 +40,29 @@ object SettingsController {
return preferences.getFloat(key, defaultValue) return preferences.getFloat(key, defaultValue)
} }
@Suppress("UNCHECKED_CAST")
fun <T : Any> get(clazz: KClass<T>, 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 <reified T> 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 <T> put(key: String, newValue: T?) { fun <T> put(key: String, newValue: T?) {
preferences.edit { preferences.edit {
when (newValue) { when (newValue) {
@@ -52,13 +76,13 @@ object SettingsController {
} }
var isLongPollInBackgroundEnabled: Boolean var isLongPollInBackgroundEnabled: Boolean
get() = getBoolean( get() = get(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
) )
set(value) = put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value) set(value) = put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value)
var deviceId: String var deviceId: String
get() = getString("device_id", "").orEmpty() get() = get("device_id", "")
set(value) = put("device_id", value) set(value) = put("device_id", value)
} }
@@ -52,13 +52,13 @@ fun MaterialDialog(
neutralAction: (() -> Unit)? = null, neutralAction: (() -> Unit)? = null,
title: String? = null, title: String? = null,
text: String? = null, text: String? = null,
itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None, selectionType: SelectionType = SelectionType.None,
items: ImmutableList<String> = ImmutableList.empty(), items: ImmutableList<String> = ImmutableList.empty(),
preSelectedItems: ImmutableList<Int> = ImmutableList.empty(), preSelectedItems: ImmutableList<Int> = ImmutableList.empty(),
onItemClick: ((index: Int) -> Unit)? = null, onItemClick: ((index: Int) -> Unit)? = null,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
actionInvokeDismiss: ActionInvokeDismiss = ActionInvokeDismiss.IfNoAction, actionInvokeDismiss: ActionInvokeDismiss = ActionInvokeDismiss.IfNoAction,
customContent: (ColumnScope.() -> Unit)? = null customContent: (@Composable ColumnScope.() -> Unit)? = null
) { ) {
var alertItems by remember { var alertItems by remember {
mutableStateOf( mutableStateOf(
@@ -135,12 +135,12 @@ fun MaterialDialog(
if (alertItems.isNotEmpty()) { if (alertItems.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
AlertItems( AlertItems(
selectionType = itemsSelectionType, selectionType = selectionType,
items = alertItems, items = alertItems,
onItemClick = { index -> onItemClick = { index ->
onItemClick?.invoke(index) onItemClick?.invoke(index)
if (itemsSelectionType == ItemsSelectionType.None) { if (selectionType == SelectionType.None) {
onDismissRequest.invoke() onDismissRequest.invoke()
} else { } else {
val newItems = val newItems =
@@ -255,7 +255,7 @@ fun MaterialDialog(
cancelAction: (() -> Unit)? = null, cancelAction: (() -> Unit)? = null,
neutralText: UiText? = null, neutralText: UiText? = null,
neutralAction: (() -> Unit)? = null, neutralAction: (() -> Unit)? = null,
itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None, selectionType: SelectionType = SelectionType.None,
preSelectedItems: ImmutableList<Int> = ImmutableList.empty(), preSelectedItems: ImmutableList<Int> = ImmutableList.empty(),
items: ImmutableList<UiText> = ImmutableList.empty(), items: ImmutableList<UiText> = ImmutableList.empty(),
onItemClick: ((index: Int) -> Unit)? = null, onItemClick: ((index: Int) -> Unit)? = null,
@@ -356,12 +356,12 @@ fun MaterialDialog(
if (alertItems.isNotEmpty()) { if (alertItems.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
AlertItems( AlertItems(
selectionType = itemsSelectionType, selectionType = selectionType,
items = alertItems, items = alertItems,
onItemClick = { index -> onItemClick = { index ->
onItemClick?.invoke(index) onItemClick?.invoke(index)
if (itemsSelectionType == ItemsSelectionType.None) { if (selectionType == SelectionType.None) {
onDismissRequest.invoke() onDismissRequest.invoke()
} else { } else {
val newItems = val newItems =
@@ -457,7 +457,7 @@ fun MaterialDialog(
@Composable @Composable
fun AlertItems( fun AlertItems(
selectionType: ItemsSelectionType, selectionType: SelectionType,
items: ImmutableList<DialogItem>, items: ImmutableList<DialogItem>,
onItemClick: ((index: Int) -> Unit)? = null, onItemClick: ((index: Int) -> Unit)? = null,
onItemCheckedChanged: ((index: Int) -> Unit)? = null onItemCheckedChanged: ((index: Int) -> Unit)? = null
@@ -468,7 +468,7 @@ fun AlertItems(
.fillMaxWidth() .fillMaxWidth()
.height(48.dp) .height(48.dp)
.clickable { .clickable {
if (selectionType == ItemsSelectionType.Multi) { if (selectionType == SelectionType.Multi) {
onItemCheckedChanged?.invoke(index) onItemCheckedChanged?.invoke(index)
} else { } else {
onItemClick?.invoke(index) onItemClick?.invoke(index)
@@ -478,7 +478,7 @@ fun AlertItems(
) { ) {
// TODO: 29/12/2023, Danil Nikolaev: check onClick & onCheckedChange actions // TODO: 29/12/2023, Danil Nikolaev: check onClick & onCheckedChange actions
when (selectionType) { when (selectionType) {
ItemsSelectionType.Multi -> { SelectionType.Multi -> {
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Checkbox( Checkbox(
checked = item.isSelected, checked = item.isSelected,
@@ -486,7 +486,7 @@ fun AlertItems(
) )
} }
ItemsSelectionType.Single -> { SelectionType.Single -> {
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
RadioButton( RadioButton(
selected = item.isSelected, selected = item.isSelected,
@@ -494,7 +494,7 @@ fun AlertItems(
) )
} }
ItemsSelectionType.None -> { SelectionType.None -> {
Spacer(modifier = Modifier.width(26.dp)) Spacer(modifier = Modifier.width(26.dp))
} }
} }
@@ -520,8 +520,8 @@ sealed class ActionInvokeDismiss {
data object Always : ActionInvokeDismiss() data object Always : ActionInvokeDismiss()
} }
sealed class ItemsSelectionType { sealed class SelectionType {
data object Single : ItemsSelectionType() data object Single : SelectionType()
data object Multi : ItemsSelectionType() data object Multi : SelectionType()
data object None : ItemsSelectionType() data object None : SelectionType()
} }
@@ -57,6 +57,11 @@ class ImmutableList<T>(val values: List<T>) : Iterable<T> {
fun <T> List<T>.toImmutableList(): ImmutableList<T> = ImmutableList(this) fun <T> List<T>.toImmutableList(): ImmutableList<T> = ImmutableList(this)
fun <T> empty(): ImmutableList<T> = ImmutableList(emptyList()) fun <T> empty(): ImmutableList<T> = ImmutableList(emptyList())
fun <T> of(vararg elements: T) =
if (elements.isNotEmpty()) copyOf(elements.asList()) else empty()
fun <T> of(element: T) = ImmutableList(listOf(element))
} }
override fun iterator(): Iterator<T> = values.listIterator() override fun iterator(): Iterator<T> = values.listIterator()
@@ -1,5 +1,6 @@
package com.meloda.app.fast.settings package com.meloda.app.fast.settings
import android.content.res.Resources
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.HapticFeedbackConstantsCompat import androidx.core.view.HapticFeedbackConstantsCompat
@@ -7,6 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.UserConfig 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.isSdkAtLeast
import com.meloda.app.fast.common.extensions.setValue import com.meloda.app.fast.common.extensions.setValue
import com.meloda.app.fast.data.db.AccountsRepository 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.SettingsItem
import com.meloda.app.fast.settings.model.SettingsScreenState import com.meloda.app.fast.settings.model.SettingsScreenState
import com.meloda.app.fast.settings.model.SettingsShowOptions import com.meloda.app.fast.settings.model.SettingsShowOptions
import com.meloda.app.fast.settings.model.TextProvider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.meloda.app.fast.ui.R as UiR import com.meloda.app.fast.ui.R as UiR
@@ -48,11 +52,14 @@ interface SettingsViewModel {
class SettingsViewModelImpl( class SettingsViewModelImpl(
private val accountsRepository: AccountsRepository, private val accountsRepository: AccountsRepository,
private val userSettings: UserSettings private val userSettings: UserSettings,
private val resources: Resources
) : SettingsViewModel, ViewModel() { ) : SettingsViewModel, ViewModel() {
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY) override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
private val settings = MutableStateFlow<List<SettingsItem<*>>>(emptyList())
override val isLongPollBackgroundEnabled = MutableStateFlow<Boolean?>(null) override val isLongPollBackgroundEnabled = MutableStateFlow<Boolean?>(null)
init { init {
@@ -111,7 +118,7 @@ class SettingsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
useHaptics = HapticType.Reject, useHaptics = HapticType.REJECT,
showDebugOptions = false showDebugOptions = false
) )
} }
@@ -122,8 +129,7 @@ class SettingsViewModelImpl(
override fun onSettingsItemLongClicked(key: String) { override fun onSettingsItemLongClicked(key: String) {
when (key) { when (key) {
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> { SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
val showDebugCategory = isDebugSettingsShown() if (isDebugSettingsShown()) return
if (showDebugCategory) return
SettingsController.put(SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, true) SettingsController.put(SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, true)
@@ -131,7 +137,7 @@ class SettingsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
useHaptics = HapticType.LongPress, useHaptics = HapticType.LONG_PRESS,
showDebugOptions = true showDebugOptions = true
) )
} }
@@ -140,6 +146,19 @@ class SettingsViewModelImpl(
} }
override fun onSettingsItemChanged(key: String, newValue: Any?) { 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) { when (key) {
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
val isEnabled = (newValue as? Boolean) == true val isEnabled = (newValue as? Boolean) == true
@@ -167,7 +186,6 @@ class SettingsViewModelImpl(
userSettings.useMultiline(isUsing) userSettings.useMultiline(isUsing)
} }
SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> { SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> {
val isUsing = newValue as? Boolean ?: false val isUsing = newValue as? Boolean ?: false
userSettings.useAmoledThemeChanged(isUsing) userSettings.useAmoledThemeChanged(isUsing)
@@ -196,7 +214,7 @@ class SettingsViewModelImpl(
} }
override fun onHapticPerformed() { override fun onHapticPerformed() {
screenState.setValue { old -> old.copy(useHaptics = HapticType.None) } screenState.setValue { old -> old.copy(useHaptics = null) }
} }
override fun onNotificationsPermissionRequested() { override fun onNotificationsPermissionRequested() {
@@ -209,221 +227,223 @@ class SettingsViewModelImpl(
} }
private fun createSettings() { private fun createSettings() {
viewModelScope.launch { val accountVisible = UserConfig.isLoggedIn()
val accountVisible = UserConfig.isLoggedIn() val accountTitle = SettingsItem.Title(
val accountTitle = SettingsItem.Title.build( key = SettingsKeys.KEY_ACCOUNT,
key = SettingsKeys.KEY_ACCOUNT, title = UiText.Resource(UiR.string.settings_account_title),
title = UiText.Resource(UiR.string.settings_account_title) isVisible = accountVisible
) { )
isVisible = accountVisible val accountLogOut = SettingsItem.TitleText(
} key = SettingsKeys.KEY_ACCOUNT_LOGOUT,
val accountLogOut = SettingsItem.TitleSummary.build( title = UiText.Resource(UiR.string.settings_account_logout_title),
key = SettingsKeys.KEY_ACCOUNT_LOGOUT, text = UiText.Resource(UiR.string.settings_account_logout_summary),
title = UiText.Resource(UiR.string.settings_account_logout_title), isVisible = accountVisible
summary = UiText.Resource(UiR.string.settings_account_logout_summary) )
) {
isVisible = accountVisible
}
val generalTitle = SettingsItem.Title.build( val generalTitle = SettingsItem.Title(
key = SettingsKeys.KEY_GENERAL, key = SettingsKeys.KEY_GENERAL,
title = UiText.Resource(UiR.string.settings_general_title) title = UiText.Resource(UiR.string.settings_general_title)
) )
val generalUseContactNames = SettingsItem.Switch.build( val generalUseContactNames = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_CONTACT_NAMES, key = SettingsKeys.KEY_USE_CONTACT_NAMES,
title = UiText.Resource(UiR.string.settings_general_contact_names_title), title = UiText.Resource(UiR.string.settings_general_contact_names_title),
summary = UiText.Resource(UiR.string.settings_general_contact_names_summary), text = UiText.Resource(UiR.string.settings_general_contact_names_summary),
defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES
) )
val appearanceTitle = SettingsItem.Title.build( val appearanceTitle = SettingsItem.Title(
key = SettingsKeys.KEY_APPEARANCE, key = SettingsKeys.KEY_APPEARANCE,
title = UiText.Resource(UiR.string.settings_appearance_title) title = UiText.Resource(UiR.string.settings_appearance_title)
) )
val appearanceMultiline = SettingsItem.Switch.build( val appearanceMultiline = SettingsItem.Switch(
key = SettingsKeys.KEY_APPEARANCE_MULTILINE, key = SettingsKeys.KEY_APPEARANCE_MULTILINE,
defaultValue = SettingsKeys.DEFAULT_VALUE_MULTILINE, defaultValue = SettingsKeys.DEFAULT_VALUE_MULTILINE,
title = UiText.Resource(UiR.string.settings_appearance_multiline_title), title = UiText.Resource(UiR.string.settings_appearance_multiline_title),
summary = UiText.Resource(UiR.string.settings_appearance_multiline_summary) text = UiText.Resource(UiR.string.settings_appearance_multiline_summary)
) )
val darkThemeValues = listOf( val darkThemeValues = listOf(
AppCompatDelegate.MODE_NIGHT_YES to UiText.Resource(UiR.string.settings_dark_theme_value_enabled), 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_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_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) AppCompatDelegate.MODE_NIGHT_NO to UiText.Resource(UiR.string.settings_dark_theme_value_disabled)
).toMap() ).toMap()
val appearanceDarkTheme = SettingsItem.ListItem.build( val appearanceDarkTheme = SettingsItem.ListItem(
key = SettingsKeys.KEY_APPEARANCE_DARK_THEME, key = SettingsKeys.KEY_APPEARANCE_DARK_THEME,
title = UiText.Resource(UiR.string.settings_dark_theme), title = UiText.Resource(UiR.string.settings_dark_theme),
values = darkThemeValues.keys.toList(), valueClass = Int::class,
valueTitles = darkThemeValues.values.toList(), defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME,
defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME titles = darkThemeValues.values.toList(),
) { values = darkThemeValues.keys.toList()
summaryProvider = SettingsItem.SummaryProvider { item -> ).apply {
val darkThemeValue = darkThemeValues[item.value ?: 0] textProvider = TextProvider { item ->
val darkThemeValue = darkThemeValues[item.value]
UiText.ResourceParams( UiText.ResourceParams(
value = UiR.string.settings_dark_theme_current_value, value = UiR.string.settings_dark_theme_current_value,
args = listOf( args = listOf(
darkThemeValue darkThemeValue
?: UiText.Resource(UiR.string.settings_dark_theme_current_value_unknown) ?: 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<SettingsItem<*>>()
listOf(
debugTitle,
debugPerformCrash,
debugShowCrashAlert,
debugLongPollBackground,
debugUseBlur,
debugShowEmojiButton
).forEach(debugList::add)
debugList += debugHideDebugList
val settingsList = mutableListOf<SettingsItem<*>>()
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<SettingsItem<*>>()
listOf(
debugTitle,
debugPerformCrash,
debugShowCrashAlert,
debugLongPollBackground,
debugUseBlur,
debugShowEmojiButton
).forEach(debugList::add)
debugList += debugHideDebugList
val settingsList = mutableListOf<SettingsItem<*>>()
listOf(
accountList,
generalList,
appearanceList,
featuresList,
visibilityList,
debugList,
).forEach(settingsList::addAll)
if (!isDebugSettingsShown()) {
settingsList.removeAll(debugList)
}
emitSettings(settingsList)
}
private fun emitSettings(newSettings: List<SettingsItem<*>>) {
settings.update { newSettings }
val uiSettings = newSettings.map { item ->
item.asPresentation(resources)
}
screenState.setValue { old -> old.copy(settings = uiSettings) }
} }
} }
sealed interface HapticType { enum class HapticType {
data object LongPress : HapticType LONG_PRESS, REJECT;
data object Reject : HapticType
data object None : HapticType
fun getHaptic(): Int { fun getHaptic(): Int = when (this) {
return when (this) { LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS
LongPress -> HapticFeedbackConstantsCompat.LONG_PRESS REJECT -> HapticFeedbackConstantsCompat.REJECT
Reject -> HapticFeedbackConstantsCompat.REJECT
None -> -1
}
} }
} }
@@ -1,223 +1,250 @@
package com.meloda.app.fast.settings.model 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.UiText
import com.meloda.app.fast.common.parseString
import com.meloda.app.fast.datastore.SettingsController import com.meloda.app.fast.datastore.SettingsController
import kotlin.properties.Delegates import kotlin.reflect.KClass
// TODO: 24/12/2023, Danil Nikolaev: refactor @Immutable
sealed class SettingsItem<Value>( sealed class SettingsItem<T>(
open val key: String, val key: String,
value: T,
defaultValue: T?
) { ) {
var onTitleChanged: ((newTitle: UiText?) -> Unit)? = null private val haveValue
get() = this::class !in listOf<KClass<*>>(
Title::class,
TitleText::class
)
var title: UiText? by Delegates.observable(null) { _, _, newValue -> init {
onTitleChanged?.invoke(newValue) 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 -> var isEnabled: Boolean = true
onSummaryChanged?.invoke(newValue)
}
var onEnabledStateChanged: ((newEnabled: Boolean) -> Unit)? = null var value: T = value
protected set(newValue) {
field = newValue
var isEnabled: Boolean by Delegates.observable(true) { _, _, newValue -> SettingsController.put(key, newValue)
onEnabledStateChanged?.invoke(newValue) }
}
var onVisibleStateChanged: ((newVisible: Boolean) -> Unit)? = null var title: UiText? = null
var isVisible: Boolean by Delegates.observable(true) { _, _, newValue -> var text: UiText? = null
onVisibleStateChanged?.invoke(newValue)
}
var onValueChanged: ((newValue: Value?) -> Unit)? = null var textProvider: TextProvider<T, SettingsItem<T>>? = null
set(value) {
field = value
updateText()
}
var value: Value? by Delegates.observable(null) { _, oldValue, newValue -> fun updateText() {
if (key.trim().isEmpty() || oldValue == newValue) return@observable textProvider?.provideText(this)?.let { newText -> text = newText }
onValueChanged?.invoke(newValue)
SettingsController.put(key, newValue)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
protected fun <T> getValueFromPreferences( fun updateValue(newValue: Any?) {
if (!haveValue) throw IllegalStateException("This item does not have a value")
value = newValue as T
}
class Title(
key: String, key: String,
classToGet: Class<T>, title: UiText,
defaultValue: Any? isVisible: Boolean = true
): T? { ) : SettingsItem<Unit>(
return when (classToGet) { key = key,
String::class.java -> SettingsController.getString(key, defaultValue as? String) value = Unit,
defaultValue = null
) {
Boolean::class.java -> { init {
SettingsController.getBoolean(key, defaultValue as? Boolean == true) this.title = title
} this.isVisible = isVisible
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<SettingsItem<Value>>? by Delegates.observable(null) { _, _, _ ->
updateTitle()
}
var summaryProvider: SummaryProvider<SettingsItem<Value>>? 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<Item : SettingsItem<*>> {
fun provideTitle(settingsItem: Item): UiText?
}
fun interface SummaryProvider<Item : SettingsItem<*>> {
fun provideSummary(settingsItem: Item): UiText?
}
data class Title(override val key: String) : SettingsItem<Nothing>(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)
}
} }
} }
data class TitleSummary(override val key: String) : SettingsItem<String>(key) { class TitleText(
key: String,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true
) : SettingsItem<Unit>(
key = key,
value = Unit,
defaultValue = null
) {
companion object { init {
fun build( require(title != null || text != null) {
key: String, "Either title or text must not be null"
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)
} }
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
} }
} }
data class TextField(override val key: String) : SettingsItem<String>(key) { class Switch(
key: String,
defaultValue: Boolean,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true,
isChecked: Boolean? = null
) : SettingsItem<Boolean>(
key = key,
value = isChecked ?: getCurrentValue(key, defaultValue),
defaultValue = defaultValue
) {
companion object { init {
fun build( require(title != null || text != null) {
key: String, "Either title or text must not be null"
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)
} }
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
} }
} }
data class Switch(override val key: String) : SettingsItem<Boolean>(key) { class TextField(
key: String,
defaultValue: String,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true,
fieldText: String? = null
) : SettingsItem<String>(
key = key,
value = fieldText ?: getCurrentValue(key, defaultValue),
defaultValue = defaultValue
) {
companion object { init {
require(title != null || text != null) {
fun build( "Either title or text must not be null"
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)
} }
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
} }
} }
data class ListItem( class ListItem<T : Any>(
override val key: String, key: String,
val values: List<Int>, defaultValue: T,
val valueTitles: List<UiText> valueClass: KClass<T>,
) : SettingsItem<Int>(key) { title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true,
selectedValue: T? = null,
val titles: List<UiText>,
val values: List<T>
) : SettingsItem<T>(
key = key,
value = selectedValue ?: SettingsController.get(valueClass, key, defaultValue),
defaultValue = defaultValue
) {
companion object { init {
fun build( require(title != null || text != null) {
key: String, "Either title or text must not be null"
title: UiText? = null,
summary: UiText? = null,
isEnabled: Boolean = true,
isVisible: Boolean = true,
values: List<Int>,
valueTitles: List<UiText>,
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)
} }
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 <reified T> 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)
}
}
@@ -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?)
}
@@ -7,8 +7,8 @@ import com.meloda.app.fast.settings.HapticType
@Immutable @Immutable
data class SettingsScreenState( data class SettingsScreenState(
val showOptions: SettingsShowOptions, val showOptions: SettingsShowOptions,
val settings: List<SettingsItem<*>>, val settings: List<UiItem>,
val useHaptics: HapticType, val useHaptics: HapticType?,
val isNeedToRequestNotificationPermission: Boolean, val isNeedToRequestNotificationPermission: Boolean,
val showDebugOptions: Boolean val showDebugOptions: Boolean
) { ) {
@@ -17,7 +17,7 @@ data class SettingsScreenState(
val EMPTY: SettingsScreenState = SettingsScreenState( val EMPTY: SettingsScreenState = SettingsScreenState(
showOptions = SettingsShowOptions.EMPTY, showOptions = SettingsShowOptions.EMPTY,
settings = emptyList(), settings = emptyList(),
useHaptics = HapticType.None, useHaptics = null,
isNeedToRequestNotificationPermission = false, isNeedToRequestNotificationPermission = false,
showDebugOptions = isDebugSettingsShown() showDebugOptions = isDebugSettingsShown()
) )
@@ -0,0 +1,7 @@
package com.meloda.app.fast.settings.model
import com.meloda.app.fast.common.UiText
fun interface TextProvider<T, S : SettingsItem<T>> {
fun provideText(item: S): UiText?
}
@@ -0,0 +1,7 @@
package com.meloda.app.fast.settings.model
import com.meloda.app.fast.common.UiText
fun interface TitleProvider<T, S : SettingsItem<T>> {
fun provideTitle(item: S): UiText?
}
@@ -0,0 +1,53 @@
package com.meloda.app.fast.settings.model
import androidx.compose.runtime.Immutable
typealias StringList = List<String>
typealias TypeList<T> = List<T>
@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<T>(
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<T>
) : UiItem(key)
}
@@ -3,11 +3,7 @@ package com.meloda.app.fast.settings.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import 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.SettingsRoute
import com.meloda.app.fast.settings.presentation.SettingsScreen
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -38,16 +38,15 @@ import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.datastore.SettingsKeys import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.datastore.isUsingDarkMode 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.SettingsViewModel
import com.meloda.app.fast.settings.SettingsViewModelImpl 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.model.SettingsScreenState
import com.meloda.app.fast.settings.presentation.items.EditTextSettingsItem import com.meloda.app.fast.settings.model.UiItem
import com.meloda.app.fast.settings.presentation.items.ListSettingsItem import com.meloda.app.fast.settings.presentation.item.ListItem
import com.meloda.app.fast.settings.presentation.items.SwitchSettingsItem import com.meloda.app.fast.settings.presentation.item.SwitchItem
import com.meloda.app.fast.settings.presentation.items.TitleSettingsItem import com.meloda.app.fast.settings.presentation.item.TextFieldItem
import com.meloda.app.fast.settings.presentation.items.TitleSummarySettingsItem 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.ActionInvokeDismiss
import com.meloda.app.fast.ui.components.MaterialDialog import com.meloda.app.fast.ui.components.MaterialDialog
import com.meloda.app.fast.ui.theme.LocalTheme import com.meloda.app.fast.ui.theme.LocalTheme
@@ -90,6 +89,8 @@ fun SettingsRoute(
}, },
onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked, onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked,
onSettingsItemValueChanged = { key, newValue -> onSettingsItemValueChanged = { key, newValue ->
viewModel.onSettingsItemChanged(key, newValue)
when (key) { when (key) {
SettingsKeys.KEY_APPEARANCE_DARK_THEME -> { SettingsKeys.KEY_APPEARANCE_DARK_THEME -> {
val newMode = newValue as? Int ?: 0 val newMode = newValue as? Int ?: 0
@@ -104,8 +105,6 @@ fun SettingsRoute(
userSettings.useDarkThemeChanged(isUsing) userSettings.useDarkThemeChanged(isUsing)
} }
else -> viewModel.onSettingsItemChanged(key, newValue)
} }
} }
) )
@@ -139,7 +138,7 @@ fun SettingsScreen(
val hapticType = screenState.useHaptics val hapticType = screenState.useHaptics
LaunchedEffect(hapticType) { LaunchedEffect(hapticType) {
if (hapticType != HapticType.None) { if (hapticType != null) {
view.performHapticFeedback(hapticType.getHaptic()) view.performHapticFeedback(hapticType.getHaptic())
onHapticPerformed() onHapticPerformed()
} }
@@ -201,60 +200,59 @@ fun SettingsScreen(
item { item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding())) Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
} }
items( items(
items = screenState.settings, items = screenState.settings,
key = { item -> item.key }, key = UiItem::key,
contentType = { item -> contentType = { item ->
when (item) { when (item) {
is SettingsItem.ListItem -> "list_item" is UiItem.Title -> "title"
is SettingsItem.Switch -> "switch" is UiItem.TitleText -> "title_text"
is SettingsItem.TextField -> "text_field" is UiItem.Switch -> "switch"
is SettingsItem.Title -> "title" is UiItem.TextField -> "text_field"
is SettingsItem.TitleSummary -> "title_summary" is UiItem.List<*> -> "list"
} }
} }
) { item -> ) { item ->
when (item) { when (item) {
is SettingsItem.Title -> TitleSettingsItem( is UiItem.Title -> {
item = item, TitleItem(item = item)
isMultiline = currentTheme.multiline, }
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
)
is SettingsItem.TitleSummary -> TitleSummarySettingsItem( is UiItem.TitleText -> {
item = item, TitleTextItem(
isMultiline = currentTheme.multiline, item = item,
onSettingsClickListener = onSettingsItemClicked, onClick = onSettingsItemClicked,
onSettingsLongClickListener = onSettingsItemLongClicked, onLongClick = onSettingsItemLongClicked
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) )
) }
is SettingsItem.Switch -> SwitchSettingsItem( is UiItem.Switch -> {
item = item, SwitchItem(
isMultiline = currentTheme.multiline, item = item,
onSettingsClickListener = onSettingsItemClicked, onClick = { onSettingsItemClicked(item.key) },
onSettingsLongClickListener = onSettingsItemLongClicked, onLongClick = { onSettingsItemLongClicked(item.key) },
onSettingsChangeListener = onSettingsItemValueChanged, onChanged = { onSettingsItemValueChanged(item.key, it) }
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) )
) }
is SettingsItem.TextField -> EditTextSettingsItem( is UiItem.TextField -> {
item = item, TextFieldItem(
isMultiline = currentTheme.multiline, item = item,
onSettingsClickListener = onSettingsItemClicked, onClick = { onSettingsItemClicked(item.key) },
onSettingsLongClickListener = onSettingsItemLongClicked, onLongClick = { onSettingsItemLongClicked(item.key) },
onSettingsChangeListener = onSettingsItemValueChanged, onChanged = { onSettingsItemValueChanged(item.key, it) }
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) )
) }
is SettingsItem.ListItem -> ListSettingsItem( is UiItem.List<*> -> {
item = item, ListItem(
isMultiline = currentTheme.multiline, item = item,
onSettingsClickListener = onSettingsItemClicked, onClick = { onSettingsItemClicked(item.key) },
onSettingsLongClickListener = onSettingsItemLongClicked, onLongClick = { onSettingsItemLongClicked(item.key) },
onSettingsChangeListener = onSettingsItemValueChanged, onChanged = { onSettingsItemValueChanged(item.key, it) }
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) )
) }
} }
} }
@@ -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
)
}
@@ -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))
}
}
@@ -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.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -20,78 +20,63 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.meloda.app.fast.common.UiText import com.meloda.app.fast.settings.model.UiItem
import com.meloda.app.fast.settings.model.OnSettingsChangeListener import com.meloda.app.fast.ui.R
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.ContentAlpha
import com.meloda.app.fast.ui.basic.LocalContentAlpha 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.MaterialDialog
import com.meloda.app.fast.ui.util.getString import com.meloda.app.fast.ui.theme.LocalTheme
import com.meloda.app.fast.ui.R as UiR
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun EditTextSettingsItem( fun TextFieldItem(
item: SettingsItem.TextField, item: UiItem.TextField,
isMultiline: Boolean, modifier: Modifier = Modifier,
onSettingsClickListener: OnSettingsClickListener, onClick: () -> Unit,
onSettingsLongClickListener: OnSettingsLongClickListener, onLongClick: () -> Unit,
onSettingsChangeListener: OnSettingsChangeListener, onChanged: (fieldText: String) -> Unit
modifier: Modifier
) { ) {
var title by remember { mutableStateOf(item.title) } if (!item.isVisible) return
item.onTitleChanged = { newTitle -> title = newTitle }
var summary by remember { mutableStateOf(item.summary) } val currentTheme = LocalTheme.current
item.onSummaryChanged = { newSummary -> summary = newSummary }
var isEnabled by remember { mutableStateOf(item.isEnabled) } var showDialog by rememberSaveable {
item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled }
var isVisible by remember { mutableStateOf(item.isVisible) }
item.onVisibleStateChanged = { newVisible -> isVisible = newVisible }
var showDialog by remember {
mutableStateOf(false) mutableStateOf(false)
} }
if (showDialog) { if (showDialog) {
EditTextAlert( EditTextAlert(
item = item, item = item,
onSettingsChangeListener = { key, newValue -> onAlertConfirmClicked = onChanged,
summary = item.summaryProvider?.provideSummary(item) onDismiss = { showDialog = false },
onSettingsChangeListener.onChange(key, newValue)
},
onDismiss = { showDialog = false }
) )
} }
if (!isVisible) return
Row( Row(
modifier = modifier modifier = modifier
.heightIn(min = 56.dp) .heightIn(min = 56.dp)
.fillMaxWidth() .fillMaxWidth()
.animateContentSize() .animateContentSize()
.combinedClickable( .combinedClickable(
enabled = isEnabled, enabled = item.isEnabled,
onClick = { onClick = {
onSettingsClickListener.onClick(item.key) onClick()
showDialog = true showDialog = true
}, },
onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, onLongClick = onLongClick,
) )
) { ) {
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -102,26 +87,26 @@ fun EditTextSettingsItem(
) { ) {
Spacer(modifier = Modifier.height(14.dp)) Spacer(modifier = Modifier.height(14.dp))
LocalContentAlpha( 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(
text = title, text = title,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
maxLines = if (isMultiline) Int.MAX_VALUE else 1, maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
} }
LocalContentAlpha( 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(
text = summary, text = text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = if (isMultiline) Int.MAX_VALUE else 1, maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
@@ -132,32 +117,27 @@ fun EditTextSettingsItem(
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun EditTextAlert( fun EditTextAlert(
item: SettingsItem.TextField, item: UiItem.TextField,
onSettingsChangeListener: OnSettingsChangeListener, onAlertConfirmClicked: (newValue: String) -> Unit,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val (textFieldFocusable) = FocusRequester.createRefs() val (textFieldFocusable) = FocusRequester.createRefs()
var textFieldValue by remember { var textFieldValue by remember {
mutableStateOf(TextFieldValue(item.value.orEmpty())) mutableStateOf(TextFieldValue(item.fieldText))
} }
MaterialDialog( MaterialDialog(
onDismissRequest = onDismiss,
title = item.title, title = item.title,
confirmText = UiText.Resource(UiR.string.ok), confirmText = stringResource(id = R.string.ok),
confirmAction = { confirmAction = {
val newValue = textFieldValue.text.trim() onAlertConfirmClicked(textFieldValue.text.trim())
if (item.value != newValue) {
item.value = newValue
onSettingsChangeListener.onChange(item.key, newValue)
}
}, },
cancelText = UiText.Resource(UiR.string.cancel), cancelText = stringResource(id = R.string.cancel),
onDismissAction = onDismiss actionInvokeDismiss = ActionInvokeDismiss.Always
) { ) {
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
@@ -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,
)
}
@@ -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.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -14,52 +14,36 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.meloda.app.fast.settings.model.OnSettingsClickListener import com.meloda.app.fast.settings.model.UiItem
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.ContentAlpha
import com.meloda.app.fast.ui.basic.LocalContentAlpha 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun TitleSummarySettingsItem( fun TitleTextItem(
item: SettingsItem.TitleSummary, item: UiItem.TitleText,
isMultiline: Boolean, modifier: Modifier = Modifier,
onSettingsClickListener: OnSettingsClickListener, onClick: (key: String) -> Unit = {},
onSettingsLongClickListener: OnSettingsLongClickListener, onLongClick: (key: String) -> Unit = {}
modifier: Modifier
) { ) {
var title by remember { mutableStateOf(item.title) } if (!item.isVisible) return
item.onTitleChanged = { newTitle -> title = newTitle }
var summary by remember { mutableStateOf(item.summary) } val currentTheme = LocalTheme.current
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( Row(
modifier = modifier modifier = modifier
.heightIn(min = 56.dp) .heightIn(min = 56.dp)
.fillMaxWidth() .fillMaxWidth()
.animateContentSize() .animateContentSize()
.combinedClickable( .combinedClickable(
enabled = isEnabled, enabled = item.isEnabled,
onClick = { onSettingsClickListener.onClick(item.key) }, onClick = { onClick(item.key) },
onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, onLongClick = { onLongClick(item.key) },
) )
) { ) {
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -71,26 +55,27 @@ fun TitleSummarySettingsItem(
Spacer(modifier = Modifier.height(14.dp)) Spacer(modifier = Modifier.height(14.dp))
LocalContentAlpha( 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(
text = title, text = title,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
maxLines = if (isMultiline) Int.MAX_VALUE else 1, maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
} }
LocalContentAlpha( 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(
text = summary, text = text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = if (isMultiline) Int.MAX_VALUE else 1, maxLines = if (currentTheme.multiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
@@ -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)
}
}
)
}
@@ -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))
}
}
@@ -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,
)
}