Upstream changes (#23)

This commit is contained in:
2024-07-11 02:12:32 +03:00
committed by GitHub
parent 8a6378f509
commit 3503ecffab
906 changed files with 23577 additions and 24115 deletions
@@ -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)
}
}
}
}
@@ -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?)
}
@@ -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()
)
}
}
@@ -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
)
}
}
@@ -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)
}
@@ -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
)
}
}
@@ -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)
}
}
)
}
@@ -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))
}
}
@@ -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))
}
}
@@ -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,
)
}
@@ -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))
}
}