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
@@ -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<List<SettingsItem<*>>>(emptyList())
override val isLongPollBackgroundEnabled = MutableStateFlow<Boolean?>(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<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 {
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
}
}
@@ -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<Value>(
open val key: String,
@Immutable
sealed class SettingsItem<T>(
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 ->
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<T, SettingsItem<T>>? = 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 <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,
classToGet: Class<T>,
defaultValue: Any?
): T? {
return when (classToGet) {
String::class.java -> SettingsController.getString(key, defaultValue as? String)
title: UiText,
isVisible: Boolean = true
) : SettingsItem<Unit>(
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<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)
}
init {
this.title = title
this.isVisible = isVisible
}
}
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 {
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<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 {
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<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 {
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<Int>,
val valueTitles: List<UiText>
) : SettingsItem<Int>(key) {
class ListItem<T : Any>(
key: String,
defaultValue: T,
valueClass: KClass<T>,
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 {
fun build(
key: String,
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)
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 <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
data class SettingsScreenState(
val showOptions: SettingsShowOptions,
val settings: List<SettingsItem<*>>,
val useHaptics: HapticType,
val settings: List<UiItem>,
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()
)
@@ -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.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
@@ -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) }
)
}
}
}
@@ -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.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))
@@ -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.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,
)
}
@@ -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,
)
}