Upstream changes (#23)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,422 @@
|
||||
package com.meloda.app.fast.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
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.isSdkAtLeast
|
||||
import com.meloda.app.fast.common.extensions.setValue
|
||||
import com.meloda.app.fast.data.db.AccountsRepository
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import com.meloda.app.fast.datastore.SettingsKeys
|
||||
import com.meloda.app.fast.datastore.isDebugSettingsShown
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
interface SettingsViewModel {
|
||||
|
||||
val screenState: StateFlow<SettingsScreenState>
|
||||
|
||||
val isLongPollBackgroundEnabled: StateFlow<Boolean?>
|
||||
|
||||
fun onLogOutAlertDismissed()
|
||||
|
||||
fun onPerformCrashAlertDismissed()
|
||||
|
||||
fun onPerformCrashPositiveButtonClicked()
|
||||
|
||||
fun onLogOutAlertPositiveClick()
|
||||
|
||||
fun onLongPollingAlertPositiveClicked()
|
||||
fun onLongPollingAlertDismissed()
|
||||
|
||||
fun onSettingsItemClicked(key: String)
|
||||
fun onSettingsItemLongClicked(key: String)
|
||||
fun onSettingsItemChanged(key: String, newValue: Any?)
|
||||
|
||||
fun onHapticsUsed()
|
||||
|
||||
fun onNotificationsPermissionRequested()
|
||||
}
|
||||
|
||||
class SettingsViewModelImpl(
|
||||
private val accountsRepository: AccountsRepository,
|
||||
) : SettingsViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||
|
||||
override val isLongPollBackgroundEnabled = MutableStateFlow<Boolean?>(null)
|
||||
|
||||
init {
|
||||
createSettings()
|
||||
}
|
||||
|
||||
override fun onLogOutAlertDismissed() {
|
||||
emitShowOptions { old -> old.copy(showLogOut = false) }
|
||||
}
|
||||
|
||||
override fun onPerformCrashAlertDismissed() {
|
||||
emitShowOptions { old -> old.copy(showPerformCrash = false) }
|
||||
}
|
||||
|
||||
override fun onPerformCrashPositiveButtonClicked() {
|
||||
throw Exception("Test exception")
|
||||
}
|
||||
|
||||
override fun onLogOutAlertPositiveClick() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountsRepository.storeAccounts(
|
||||
listOf(
|
||||
AccountEntity(
|
||||
userId = UserConfig.userId,
|
||||
accessToken = "",
|
||||
fastToken = UserConfig.fastToken,
|
||||
trustedHash = UserConfig.trustedHash
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
UserConfig.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongPollingAlertPositiveClicked() {
|
||||
screenState.setValue { old -> old.copy(isNeedToRequestNotificationPermission = true) }
|
||||
}
|
||||
|
||||
override fun onLongPollingAlertDismissed() {
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
showOptions = old.showOptions.copy(
|
||||
showLongPollNotifications = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSettingsItemClicked(key: String) {
|
||||
when (key) {
|
||||
SettingsKeys.KEY_ACCOUNT_LOGOUT -> {
|
||||
emitShowOptions { old -> old.copy(showLogOut = true) }
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_PERFORM_CRASH -> {
|
||||
emitShowOptions { old -> old.copy(showPerformCrash = true) }
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST -> {
|
||||
val showDebugCategory = isDebugSettingsShown()
|
||||
if (!showDebugCategory) return
|
||||
|
||||
SettingsController.put(
|
||||
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
|
||||
false
|
||||
)
|
||||
|
||||
createSettings()
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
useHaptics = HapticType.Reject,
|
||||
showDebugOptions = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSettingsItemLongClicked(key: String) {
|
||||
when (key) {
|
||||
SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS -> {
|
||||
val showDebugCategory = isDebugSettingsShown()
|
||||
if (showDebugCategory) return
|
||||
|
||||
SettingsController.put(SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, true)
|
||||
|
||||
createSettings()
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
useHaptics = HapticType.LongPress,
|
||||
showDebugOptions = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSettingsItemChanged(key: String, newValue: Any?) {
|
||||
when (key) {
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
||||
val isEnabled = (newValue as? Boolean) == true
|
||||
|
||||
if (isEnabled) {
|
||||
// TODO: 26/11/2023, Danil Nikolaev: implement
|
||||
val isNotificationsPermissionGranted = false
|
||||
|
||||
if (!isNotificationsPermissionGranted) {
|
||||
// TODO: 26/11/2023, Danil Nikolaev: implement restart
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHapticsUsed() {
|
||||
screenState.setValue { old -> old.copy(useHaptics = HapticType.None) }
|
||||
}
|
||||
|
||||
override fun onNotificationsPermissionRequested() {
|
||||
screenState.setValue { old -> old.copy(isNeedToRequestNotificationPermission = false) }
|
||||
}
|
||||
|
||||
private fun emitShowOptions(function: (SettingsShowOptions) -> SettingsShowOptions) {
|
||||
val newShowOptions = function.invoke(screenState.value.showOptions)
|
||||
screenState.setValue { old -> old.copy(showOptions = newShowOptions) }
|
||||
}
|
||||
|
||||
private fun createSettings() {
|
||||
viewModelScope.launch {
|
||||
val accountVisible = UserConfig.isLoggedIn()
|
||||
val accountTitle = SettingsItem.Title.build(
|
||||
key = SettingsKeys.KEY_ACCOUNT,
|
||||
title = UiText.Simple("Account")
|
||||
) {
|
||||
isVisible = accountVisible
|
||||
}
|
||||
val accountLogOut = SettingsItem.TitleSummary.build(
|
||||
key = SettingsKeys.KEY_ACCOUNT_LOGOUT,
|
||||
title = UiText.Simple("Log out"),
|
||||
summary = UiText.Simple("Log out from account and delete all local data related to this account")
|
||||
) {
|
||||
isVisible = accountVisible
|
||||
}
|
||||
|
||||
val generalTitle = SettingsItem.Title.build(
|
||||
key = SettingsKeys.KEY_GENERAL,
|
||||
title = UiText.Simple("General")
|
||||
)
|
||||
val generalUseContactNames = SettingsItem.Switch.build(
|
||||
key = SettingsKeys.KEY_USE_CONTACT_NAMES,
|
||||
title = UiText.Simple("Use contact names"),
|
||||
summary = UiText.Simple("App will use available contact names for users"),
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES
|
||||
)
|
||||
|
||||
val appearanceTitle = SettingsItem.Title.build(
|
||||
key = SettingsKeys.KEY_APPEARANCE,
|
||||
title = UiText.Simple("Appearance")
|
||||
)
|
||||
val appearanceMultiline = SettingsItem.Switch.build(
|
||||
key = SettingsKeys.KEY_APPEARANCE_MULTILINE,
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_MULTILINE,
|
||||
title = UiText.Simple("Multiline titles and messages"),
|
||||
summary = UiText.Simple("The title of the dialog and the text of the message can take up two lines")
|
||||
)
|
||||
|
||||
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]
|
||||
|
||||
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.Simple("Features")
|
||||
)
|
||||
val featuresHideKeyboardOnScroll = SettingsItem.Switch.build(
|
||||
key = SettingsKeys.KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL,
|
||||
defaultValue = true,
|
||||
title = UiText.Simple("Hide keyboard on scroll"),
|
||||
summary = UiText.Simple("Hides keyboard when you scrolling messages up in messages history screen")
|
||||
)
|
||||
val featuresFastText = SettingsItem.TextField.build(
|
||||
key = SettingsKeys.KEY_FEATURES_FAST_TEXT,
|
||||
title = UiText.Simple("Fast text"),
|
||||
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 featuresLongPollBackground = SettingsItem.Switch.build(
|
||||
key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
title = UiText.Simple("LongPoll in background"),
|
||||
summary = UiText.Simple(
|
||||
"Your messages will be updates even when app is not on the screen.\nApp will be restarted"
|
||||
)
|
||||
)
|
||||
|
||||
val visibilityTitle = SettingsItem.Title.build(
|
||||
key = "visibility",
|
||||
title = UiText.Simple("Visibility")
|
||||
)
|
||||
val visibilitySendOnlineStatus = SettingsItem.Switch.build(
|
||||
key = SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||
title = UiText.Simple("Send online status"),
|
||||
summary = UiText.Simple("Online status will be sent every five minutes")
|
||||
)
|
||||
|
||||
val debugTitle = SettingsItem.Title.build(
|
||||
key = "debug",
|
||||
title = UiText.Simple("Debug")
|
||||
)
|
||||
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 debugShowExactTimeOnTimeStamp = SettingsItem.Switch.build(
|
||||
key = SettingsKeys.KEY_SHOW_EXACT_TIME_ON_TIME_STAMP,
|
||||
title = UiText.Simple("[WIP] Show exact time on time stamp"),
|
||||
summary = UiText.Simple("Shows hours and minutes on time stamp in messages history"),
|
||||
defaultValue = false
|
||||
)
|
||||
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,
|
||||
featuresHideKeyboardOnScroll,
|
||||
featuresFastText,
|
||||
featuresLongPollBackground
|
||||
)
|
||||
val visibilityList = listOf(
|
||||
visibilityTitle,
|
||||
visibilitySendOnlineStatus,
|
||||
)
|
||||
val debugList = mutableListOf<SettingsItem<*>>()
|
||||
listOf(
|
||||
debugTitle,
|
||||
debugPerformCrash,
|
||||
debugShowCrashAlert,
|
||||
debugShowExactTimeOnTimeStamp,
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface HapticType {
|
||||
data object LongPress : HapticType
|
||||
data object Reject : HapticType
|
||||
data object None : HapticType
|
||||
|
||||
fun getHaptic(): Int {
|
||||
return when (this) {
|
||||
LongPress -> HapticFeedbackConstantsCompat.LONG_PRESS
|
||||
Reject -> HapticFeedbackConstantsCompat.REJECT
|
||||
None -> -1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.app.fast.settings.di
|
||||
|
||||
import com.meloda.app.fast.settings.SettingsViewModel
|
||||
import com.meloda.app.fast.settings.SettingsViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val settingsModule = module {
|
||||
viewModelOf(::SettingsViewModelImpl) bind SettingsViewModel::class
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.meloda.app.fast.settings.model
|
||||
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
// TODO: 24/12/2023, Danil Nikolaev: refactor
|
||||
sealed class SettingsItem<Value>(
|
||||
open val key: String,
|
||||
) {
|
||||
var onTitleChanged: ((newTitle: UiText?) -> Unit)? = null
|
||||
|
||||
var title: UiText? by Delegates.observable(null) { _, _, newValue ->
|
||||
onTitleChanged?.invoke(newValue)
|
||||
}
|
||||
|
||||
var onSummaryChanged: ((newSummary: UiText?) -> Unit)? = null
|
||||
|
||||
var summary: UiText? by Delegates.observable(null) { _, _, newValue ->
|
||||
onSummaryChanged?.invoke(newValue)
|
||||
}
|
||||
|
||||
var onEnabledStateChanged: ((newEnabled: Boolean) -> Unit)? = null
|
||||
|
||||
var isEnabled: Boolean by Delegates.observable(true) { _, _, newValue ->
|
||||
onEnabledStateChanged?.invoke(newValue)
|
||||
}
|
||||
|
||||
var onVisibleStateChanged: ((newVisible: Boolean) -> Unit)? = null
|
||||
|
||||
var isVisible: Boolean by Delegates.observable(true) { _, _, newValue ->
|
||||
onVisibleStateChanged?.invoke(newValue)
|
||||
}
|
||||
|
||||
var onValueChanged: ((newValue: Value?) -> Unit)? = null
|
||||
|
||||
var value: Value? by Delegates.observable(null) { _, oldValue, newValue ->
|
||||
if (key.trim().isEmpty() || oldValue == newValue) return@observable
|
||||
|
||||
onValueChanged?.invoke(newValue)
|
||||
|
||||
SettingsController.put(key, newValue)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
protected fun <T> getValueFromPreferences(
|
||||
key: String,
|
||||
classToGet: Class<T>,
|
||||
defaultValue: Any?
|
||||
): T? {
|
||||
return when (classToGet) {
|
||||
String::class.java -> SettingsController.getString(key, defaultValue as? String)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TitleSummary(override val key: String) : SettingsItem<String>(key) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TextField(override val key: String) : SettingsItem<String>(key) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Switch(override val key: String) : SettingsItem<Boolean>(key) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ListItem(
|
||||
override val key: String,
|
||||
val values: List<Int>,
|
||||
val valueTitles: List<UiText>
|
||||
) : SettingsItem<Int>(key) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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?)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.meloda.app.fast.settings.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.meloda.app.fast.datastore.isDebugSettingsShown
|
||||
import com.meloda.app.fast.settings.HapticType
|
||||
|
||||
@Immutable
|
||||
data class SettingsScreenState(
|
||||
val showOptions: SettingsShowOptions,
|
||||
val settings: List<SettingsItem<*>>,
|
||||
val useHaptics: HapticType,
|
||||
val isNeedToRequestNotificationPermission: Boolean,
|
||||
val showDebugOptions: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: SettingsScreenState = SettingsScreenState(
|
||||
showOptions = SettingsShowOptions.EMPTY,
|
||||
settings = emptyList(),
|
||||
useHaptics = HapticType.None,
|
||||
isNeedToRequestNotificationPermission = false,
|
||||
showDebugOptions = isDebugSettingsShown()
|
||||
)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.meloda.app.fast.settings.model
|
||||
|
||||
data class SettingsShowOptions(
|
||||
val showLogOut: Boolean,
|
||||
val showPerformCrash: Boolean,
|
||||
val showLongPollNotifications: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: SettingsShowOptions = SettingsShowOptions(
|
||||
showLogOut = false,
|
||||
showPerformCrash = false,
|
||||
showLongPollNotifications = false
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.meloda.app.fast.settings.presentation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object Settings
|
||||
|
||||
fun NavGraphBuilder.settingsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onNavigateToAuth: () -> Unit,
|
||||
onNavigateToLanguagePicker: () -> Unit
|
||||
) {
|
||||
composable<Settings> {
|
||||
SettingsScreen(
|
||||
onError = onError,
|
||||
onBack = onBack,
|
||||
onNavigateToAuth = onNavigateToAuth,
|
||||
onNavigateToLanguagePicker = onNavigateToLanguagePicker
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToSettings() {
|
||||
this.navigate(Settings)
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
package com.meloda.app.fast.settings.presentation
|
||||
|
||||
import android.os.PowerManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.meloda.app.fast.common.UiText
|
||||
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.designsystem.LocalTheme
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
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.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.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 dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalHazeMaterialsApi::class
|
||||
)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onNavigateToAuth: () -> Unit,
|
||||
onNavigateToLanguagePicker: () -> Unit,
|
||||
viewModel: SettingsViewModel = koinViewModel<SettingsViewModelImpl>()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
val hapticType = screenState.useHaptics
|
||||
if (hapticType != HapticType.None) {
|
||||
view.performHapticFeedback(hapticType.getHaptic())
|
||||
viewModel.onHapticsUsed()
|
||||
}
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
|
||||
LaunchedEffect(true) {
|
||||
userSettings.enableDebugSettings(screenState.showDebugOptions)
|
||||
}
|
||||
|
||||
val currentTheme = LocalTheme.current
|
||||
val settingsList = screenState.settings
|
||||
|
||||
val clickListener = OnSettingsClickListener { key ->
|
||||
when (key) {
|
||||
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
|
||||
onNavigateToLanguagePicker()
|
||||
}
|
||||
|
||||
else -> viewModel.onSettingsItemClicked(key)
|
||||
}
|
||||
|
||||
}
|
||||
val longClickListener = OnSettingsLongClickListener(viewModel::onSettingsItemLongClicked)
|
||||
val changeListener = OnSettingsChangeListener { key, newValue ->
|
||||
when (key) {
|
||||
SettingsKeys.KEY_APPEARANCE_MULTILINE -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.useMultiline(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_APPEARANCE_DARK_THEME -> {
|
||||
val newMode = newValue as? Int ?: return@OnSettingsChangeListener
|
||||
AppCompatDelegate.setDefaultNightMode(newMode)
|
||||
|
||||
val isUsing = context.getSystemService<PowerManager>()?.let { manager ->
|
||||
isUsingDarkMode(
|
||||
context.resources,
|
||||
manager
|
||||
)
|
||||
} ?: false
|
||||
|
||||
userSettings.useDarkThemeChanged(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.useAmoledThemeChanged(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_USE_DYNAMIC_COLORS -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.useDynamicColorsChanged(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_APPEARANCE_BLUR -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.useBlurChanged(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.setLongPollBackground(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.setOnline(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_USE_CONTACT_NAMES -> {
|
||||
val isUsing = newValue as? Boolean ?: false
|
||||
userSettings.onUseContactNamesChanged(isUsing)
|
||||
}
|
||||
|
||||
else -> viewModel.onSettingsItemChanged(key, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
val title = @Composable { Text(text = stringResource(id = UiR.string.title_settings)) }
|
||||
val navigationIcon = @Composable {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_round_arrow_back_24),
|
||||
contentDescription = "Back button"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = title,
|
||||
navigationIcon = navigationIcon,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(
|
||||
alpha = if (currentTheme.usingBlur) 0f else 1f
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.usingBlur) {
|
||||
Modifier.hazeChild(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.usingBlur) {
|
||||
Modifier.haze(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
||||
.padding(bottom = padding.calculateBottomPadding())
|
||||
) {
|
||||
items(
|
||||
count = settingsList.size,
|
||||
// key = { index ->
|
||||
// val item = settingsList[index]
|
||||
// requireNotNull(item.title ?: item.summary)
|
||||
// },
|
||||
contentType = { index ->
|
||||
when (settingsList[index]) {
|
||||
is SettingsItem.ListItem -> "listitem"
|
||||
is SettingsItem.Switch -> "switch"
|
||||
is SettingsItem.TextField -> "textfield"
|
||||
is SettingsItem.Title -> "title"
|
||||
is SettingsItem.TitleSummary -> "titlesummary"
|
||||
}
|
||||
}
|
||||
) { index ->
|
||||
val needToShowSpacer by remember {
|
||||
derivedStateOf {
|
||||
index == 0
|
||||
}
|
||||
}
|
||||
|
||||
if (needToShowSpacer) {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
|
||||
when (val item = settingsList[index]) {
|
||||
is SettingsItem.Title -> TitleSettingsItem(
|
||||
item = item,
|
||||
isMultiline = currentTheme.multiline,
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
)
|
||||
|
||||
is SettingsItem.TitleSummary -> TitleSummarySettingsItem(
|
||||
item = item,
|
||||
isMultiline = currentTheme.multiline,
|
||||
onSettingsClickListener = clickListener,
|
||||
onSettingsLongClickListener = longClickListener,
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
)
|
||||
|
||||
is SettingsItem.Switch -> SwitchSettingsItem(
|
||||
item = item,
|
||||
isMultiline = currentTheme.multiline,
|
||||
onSettingsClickListener = clickListener,
|
||||
onSettingsLongClickListener = longClickListener,
|
||||
onSettingsChangeListener = changeListener,
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
)
|
||||
|
||||
is SettingsItem.TextField -> EditTextSettingsItem(
|
||||
item = item,
|
||||
isMultiline = currentTheme.multiline,
|
||||
onSettingsClickListener = clickListener,
|
||||
onSettingsLongClickListener = longClickListener,
|
||||
onSettingsChangeListener = changeListener,
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
)
|
||||
|
||||
is SettingsItem.ListItem -> ListSettingsItem(
|
||||
item = item,
|
||||
isMultiline = currentTheme.multiline,
|
||||
onSettingsClickListener = clickListener,
|
||||
onSettingsLongClickListener = longClickListener,
|
||||
onSettingsChangeListener = changeListener,
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
)
|
||||
}
|
||||
|
||||
val showBottomNavigationBarsSpacer by remember {
|
||||
derivedStateOf {
|
||||
index == settingsList.size - 1
|
||||
}
|
||||
}
|
||||
|
||||
if (showBottomNavigationBarsSpacer) {
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HandlePopups(
|
||||
performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked,
|
||||
performCrashDismissed = viewModel::onPerformCrashAlertDismissed,
|
||||
logoutPositiveClick = {
|
||||
viewModel.onLogOutAlertPositiveClick()
|
||||
onNavigateToAuth()
|
||||
},
|
||||
logoutDismissed = viewModel::onLogOutAlertDismissed,
|
||||
longPollingPositiveClick = viewModel::onLongPollingAlertPositiveClicked,
|
||||
longPollingDismissed = viewModel::onLongPollingAlertDismissed,
|
||||
screenState = screenState
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: 12/04/2024, Danil Nikolaev: rewrite to UiAction
|
||||
@Composable
|
||||
fun HandlePopups(
|
||||
performCrashPositiveClick: () -> Unit,
|
||||
performCrashDismissed: () -> Unit,
|
||||
logoutPositiveClick: () -> Unit,
|
||||
logoutDismissed: () -> Unit,
|
||||
longPollingPositiveClick: () -> Unit,
|
||||
longPollingDismissed: () -> Unit,
|
||||
screenState: SettingsScreenState
|
||||
) {
|
||||
val showOptions = screenState.showOptions
|
||||
|
||||
PerformCrashDialog(
|
||||
positiveClick = performCrashPositiveClick,
|
||||
dismiss = performCrashDismissed,
|
||||
show = showOptions.showPerformCrash
|
||||
)
|
||||
|
||||
LogOutDialog(
|
||||
positiveClick = logoutPositiveClick,
|
||||
dismiss = logoutDismissed,
|
||||
show = showOptions.showLogOut
|
||||
)
|
||||
|
||||
LongPollingNotificationsPermission(
|
||||
positiveClick = longPollingPositiveClick,
|
||||
dismiss = longPollingDismissed,
|
||||
show = showOptions.showLongPollNotifications
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PerformCrashDialog(
|
||||
positiveClick: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
show: Boolean,
|
||||
) {
|
||||
if (show) {
|
||||
MaterialDialog(
|
||||
title = UiText.Simple("Perform Crash"),
|
||||
text = UiText.Simple("App will be crashed. Are you sure?"),
|
||||
confirmText = UiText.Resource(UiR.string.yes),
|
||||
confirmAction = positiveClick,
|
||||
cancelText = UiText.Resource(UiR.string.cancel),
|
||||
onDismissAction = dismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogOutDialog(
|
||||
positiveClick: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
show: Boolean
|
||||
) {
|
||||
if (show) {
|
||||
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
|
||||
|
||||
val title = UiText.Resource(
|
||||
if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
|
||||
else UiR.string.sign_out_confirm_title
|
||||
)
|
||||
|
||||
val positiveText = UiText.Resource(
|
||||
if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
|
||||
else UiR.string.action_sign_out
|
||||
)
|
||||
|
||||
MaterialDialog(
|
||||
title = title,
|
||||
text = UiText.Resource(UiR.string.sign_out_confirm),
|
||||
confirmText = positiveText,
|
||||
confirmAction = positiveClick,
|
||||
cancelText = UiText.Resource(UiR.string.cancel),
|
||||
onDismissAction = dismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongPollingNotificationsPermission(
|
||||
positiveClick: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
show: Boolean
|
||||
) {
|
||||
if (show) {
|
||||
MaterialDialog(
|
||||
title = UiText.Resource(UiR.string.warning),
|
||||
text = UiText.Simple("Long polling in background required notifications permission on Android 13 and up"),
|
||||
confirmText = UiText.Simple("Grant"),
|
||||
confirmAction = positiveClick,
|
||||
cancelText = UiText.Resource(UiR.string.cancel),
|
||||
onDismissAction = dismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
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.designsystem.ContentAlpha
|
||||
import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList
|
||||
import com.meloda.app.fast.designsystem.ItemsSelectionType
|
||||
import com.meloda.app.fast.designsystem.LocalContentAlpha
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.getString
|
||||
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.designsystem.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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
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.designsystem.ContentAlpha
|
||||
import com.meloda.app.fast.designsystem.LocalContentAlpha
|
||||
import com.meloda.app.fast.designsystem.getString
|
||||
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
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.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.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.designsystem.ContentAlpha
|
||||
import com.meloda.app.fast.designsystem.LocalContentAlpha
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.getString
|
||||
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.designsystem.R as UiR
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun EditTextSettingsItem(
|
||||
item: SettingsItem.TextField,
|
||||
isMultiline: Boolean,
|
||||
onSettingsClickListener: OnSettingsClickListener,
|
||||
onSettingsLongClickListener: OnSettingsLongClickListener,
|
||||
onSettingsChangeListener: OnSettingsChangeListener,
|
||||
modifier: Modifier
|
||||
) {
|
||||
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 }
|
||||
|
||||
var showDialog by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
EditTextAlert(
|
||||
item = item,
|
||||
onSettingsChangeListener = { key, newValue ->
|
||||
summary = item.summaryProvider?.provideSummary(item)
|
||||
onSettingsChangeListener.onChange(key, newValue)
|
||||
},
|
||||
onDismiss = { showDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun EditTextAlert(
|
||||
item: SettingsItem.TextField,
|
||||
onSettingsChangeListener: OnSettingsChangeListener,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val (textFieldFocusable) = FocusRequester.createRefs()
|
||||
|
||||
var textFieldValue by remember {
|
||||
mutableStateOf(TextFieldValue(item.value.orEmpty()))
|
||||
}
|
||||
|
||||
MaterialDialog(
|
||||
title = item.title,
|
||||
confirmText = UiText.Resource(UiR.string.ok),
|
||||
confirmAction = {
|
||||
val newValue = textFieldValue.text.trim()
|
||||
|
||||
if (item.value != newValue) {
|
||||
item.value = newValue
|
||||
onSettingsChangeListener.onChange(item.key, newValue)
|
||||
}
|
||||
},
|
||||
cancelText = UiText.Resource(UiR.string.cancel),
|
||||
onDismissAction = onDismiss
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.focusRequester(textFieldFocusable)
|
||||
.weight(1f),
|
||||
value = textFieldValue,
|
||||
onValueChange = { newText ->
|
||||
textFieldValue = newText
|
||||
},
|
||||
label = { Text(text = "Value") },
|
||||
placeholder = { Text(text = "Value") },
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
textFieldFocusable.requestFocus()
|
||||
textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.text.length))
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
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.designsystem.getString
|
||||
import com.meloda.app.fast.settings.model.SettingsItem
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
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.designsystem.ContentAlpha
|
||||
import com.meloda.app.fast.designsystem.LocalContentAlpha
|
||||
import com.meloda.app.fast.designsystem.getString
|
||||
import com.meloda.app.fast.settings.model.OnSettingsClickListener
|
||||
import com.meloda.app.fast.settings.model.OnSettingsLongClickListener
|
||||
import com.meloda.app.fast.settings.model.SettingsItem
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TitleSummarySettingsItem(
|
||||
item: SettingsItem.TitleSummary,
|
||||
isMultiline: Boolean,
|
||||
onSettingsClickListener: OnSettingsClickListener,
|
||||
onSettingsLongClickListener: OnSettingsLongClickListener,
|
||||
modifier: Modifier
|
||||
) {
|
||||
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) },
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user