update package name (even bigger one)

This commit is contained in:
2024-07-16 07:02:50 +03:00
parent 4f9e49003b
commit c8b1d72f08
367 changed files with 12 additions and 25 deletions
@@ -0,0 +1,487 @@
package dev.meloda.fast.settings
import android.content.res.Resources
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.UserConfig
import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.isSdkAtLeast
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.data.db.AccountsRepository
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.model.database.AccountEntity
import dev.meloda.fast.settings.model.SettingsItem
import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.SettingsShowOptions
import dev.meloda.fast.settings.model.TextProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import dev.meloda.fast.ui.R as UiR
interface SettingsViewModel {
val screenState: StateFlow<SettingsScreenState>
val hapticType: StateFlow<HapticType?>
fun onLogOutAlertDismissed()
fun onLogOutAlertPositiveClick()
fun onPerformCrashAlertDismissed()
fun onPerformCrashPositiveButtonClicked()
fun onSettingsItemClicked(key: String)
fun onSettingsItemLongClicked(key: String)
fun onSettingsItemChanged(key: String, newValue: Any?)
fun onHapticPerformed()
}
class SettingsViewModelImpl(
private val accountsRepository: AccountsRepository,
private val userSettings: UserSettings,
private val resources: Resources,
private val longPollController: LongPollController
) : SettingsViewModel, ViewModel() {
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
override val hapticType = MutableStateFlow<HapticType?>(null)
private val settings = MutableStateFlow<List<SettingsItem<*>>>(emptyList())
init {
createSettings()
}
override fun onLogOutAlertDismissed() {
emitShowOptions { old -> old.copy(showLogOut = false) }
}
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 onPerformCrashAlertDismissed() {
emitShowOptions { old -> old.copy(showPerformCrash = false) }
}
override fun onPerformCrashPositiveButtonClicked() {
throw Exception("Test exception")
}
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 = AppSettings.Debug.showDebugCategory
if (!showDebugCategory) return
onSettingsItemChanged(key, false)
createSettings()
hapticType.update { HapticType.REJECT }
screenState.setValue { old -> old.copy(showDebugOptions = false) }
}
}
}
override fun onSettingsItemLongClicked(key: String) {
when (key) {
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
if (AppSettings.Debug.showDebugCategory) return
onSettingsItemChanged(key, true)
createSettings()
hapticType.update { HapticType.LONG_PRESS }
screenState.setValue { old -> old.copy(showDebugOptions = true) }
}
}
}
override fun onSettingsItemChanged(key: String, newValue: Any?) {
settings.value.findWithIndex { it.key == key }?.let { (index, item) ->
item.updateValue(newValue)
item.updateText()
screenState.setValue { old ->
old.copy(
settings = old.settings.toMutableList().apply {
this[index] = item.asPresentation(resources)
}
)
}
}
when (key) {
SettingsKeys.KEY_USE_CONTACT_NAMES -> {
val isUsing = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES
userSettings.onUseContactNamesChanged(isUsing)
}
SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH -> {
val enable =
newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH
userSettings.onEnablePullToRefreshChanged(enable)
}
SettingsKeys.KEY_APPEARANCE_MULTILINE -> {
val isUsing = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_MULTILINE
userSettings.onEnableMultilineChanged(isUsing)
}
SettingsKeys.KEY_APPEARANCE_DARK_MODE -> {
val newMode = newValue as? Int ?: SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_MODE
AppCompatDelegate.setDefaultNightMode(newMode)
userSettings.onDarkModeChanged(DarkMode.parse(newMode))
}
SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> {
val isUsing =
newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME
userSettings.onEnableAmoledDarkChanged(isUsing)
}
SettingsKeys.KEY_USE_DYNAMIC_COLORS -> {
val isUsing = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS
userSettings.onEnableDynamicColorsChanged(isUsing)
}
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
val newLanguage = newValue as? String ?: SettingsKeys.DEFAULT_APPEARANCE_LANGUAGE
userSettings.onAppLanguageChanged(newLanguage)
}
SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT -> {
val newText = newValue as? String ?: SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT
userSettings.onFastTextChanged(newText)
}
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
val isUsing = newValue as? Boolean
?: SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS
userSettings.onSendOnlineStatusChanged(isUsing)
}
SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT -> {
val show = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
userSettings.onShowAlertAfterCrashChanged(show)
}
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
val inBackground = newValue as? Boolean
?: SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
userSettings.onLongPollInBackgroundChanged(inBackground)
longPollController.setStateToApply(
longPollController.stateToApply.value.let { state ->
if (state.isLaunched()) {
if (inBackground) LongPollState.Background
else LongPollState.InApp
} else state
}
)
}
SettingsKeys.KEY_APPEARANCE_USE_BLUR -> {
val isUsing =
newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR
userSettings.onUseBlurChanged(isUsing)
}
SettingsKeys.KEY_SHOW_EMOJI_BUTTON -> {
val show = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
userSettings.onShowEmojiButtonChanged(show)
}
SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES -> {
val show = newValue as? Boolean
?: SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES
userSettings.onShowTimeInActionMessagesChanged(show)
}
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY -> {
val show = newValue as? Boolean ?: false
userSettings.onShowDebugCategoryChanged(show)
}
}
}
override fun onHapticPerformed() {
hapticType.update { null }
}
private fun emitShowOptions(function: (SettingsShowOptions) -> SettingsShowOptions) {
val newShowOptions = function.invoke(screenState.value.showOptions)
screenState.setValue { old -> old.copy(showOptions = newShowOptions) }
}
private fun createSettings() {
val accountVisible = UserConfig.isLoggedIn()
val accountTitle = SettingsItem.Title(
key = SettingsKeys.KEY_ACCOUNT,
title = UiText.Resource(UiR.string.settings_account_title),
isVisible = accountVisible
)
val accountLogOut = SettingsItem.TitleText(
key = SettingsKeys.KEY_ACCOUNT_LOGOUT,
title = UiText.Resource(UiR.string.settings_account_logout_title),
text = UiText.Resource(UiR.string.settings_account_logout_summary),
isVisible = accountVisible
)
val generalTitle = SettingsItem.Title(
key = SettingsKeys.KEY_GENERAL,
title = UiText.Resource(UiR.string.settings_general_title)
)
val generalUseContactNames = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_CONTACT_NAMES,
title = UiText.Resource(UiR.string.settings_general_contact_names_title),
text = UiText.Resource(UiR.string.settings_general_contact_names_summary),
defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES
)
val generalEnablePullToRefresh = SettingsItem.Switch(
key = SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH,
defaultValue = SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH,
title = UiText.Resource(UiR.string.settings_general_enable_pull_to_refresh_title)
)
val appearanceTitle = SettingsItem.Title(
key = SettingsKeys.KEY_APPEARANCE,
title = UiText.Resource(UiR.string.settings_appearance_title)
)
val appearanceMultiline = SettingsItem.Switch(
key = SettingsKeys.KEY_APPEARANCE_MULTILINE,
defaultValue = SettingsKeys.DEFAULT_VALUE_MULTILINE,
title = UiText.Resource(UiR.string.settings_appearance_multiline_title),
text = UiText.Resource(UiR.string.settings_appearance_multiline_summary)
)
val darkThemeValues = listOf(
DarkMode.ENABLED to UiText.Resource(UiR.string.settings_dark_theme_value_enabled),
DarkMode.FOLLOW_SYSTEM to UiText.Resource(UiR.string.settings_dark_theme_value_follow_system),
DarkMode.AUTO_BATTERY to UiText.Resource(UiR.string.settings_dark_theme_value_battery_saver),
DarkMode.DISABLED to UiText.Resource(UiR.string.settings_dark_theme_value_disabled)
).toMap()
val appearanceDarkTheme = SettingsItem.ListItem(
key = SettingsKeys.KEY_APPEARANCE_DARK_MODE,
title = UiText.Resource(UiR.string.settings_dark_theme),
valueClass = Int::class,
defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_MODE,
titles = darkThemeValues.values.toList(),
values = darkThemeValues.keys.toList().map(DarkMode::value)
).apply {
textProvider = TextProvider { item ->
val darkThemeValue = darkThemeValues[DarkMode.parse(item.value)]
UiText.ResourceParams(
value = UiR.string.settings_dark_theme_current_value,
args = listOf(
darkThemeValue
?: UiText.Resource(UiR.string.settings_dark_theme_current_value_unknown)
)
)
}
}
val appearanceUseAmoledDarkTheme = SettingsItem.Switch(
key = SettingsKeys.KEY_APPEARANCE_AMOLED_THEME,
defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME,
title = UiText.Resource(UiR.string.settings_amoled_dark_theme),
text = UiText.Resource(UiR.string.settings_amoled_dark_theme_description)
)
val appearanceUseDynamicColors = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_DYNAMIC_COLORS,
title = UiText.Resource(UiR.string.settings_dynamic_colors),
isVisible = isSdkAtLeast(Build.VERSION_CODES.S),
text = UiText.Resource(UiR.string.settings_dynamic_colors_description),
defaultValue = SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS
)
val appearanceLanguage = SettingsItem.TitleText(
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
title = UiText.Resource(UiR.string.settings_application_language),
)
val featuresTitle = SettingsItem.Title(
key = "features",
title = UiText.Resource(UiR.string.settings_features_title)
)
val featuresFastText = SettingsItem.TextField(
key = SettingsKeys.KEY_FEATURES_FAST_TEXT,
title = UiText.Resource(UiR.string.settings_features_fast_text_title),
defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT
).apply {
textProvider = TextProvider { settingsItem ->
UiText.ResourceParams(
UiR.string.pref_message_fast_text_summary,
listOf(settingsItem.value)
)
}
}
val activityTitle = SettingsItem.Title(
key = "activity",
title = UiText.Resource(UiR.string.settings_activity_title)
)
val visibilitySendOnlineStatus = SettingsItem.Switch(
key = SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS,
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS,
title = UiText.Resource(UiR.string.settings_activity_send_online_title),
text = UiText.Resource(UiR.string.settings_activity_send_online_summary)
)
val debugTitle = SettingsItem.Title(
key = "debug",
title = UiText.Resource(UiR.string.settings_debug_title)
)
val debugPerformCrash = SettingsItem.TitleText(
key = SettingsKeys.KEY_DEBUG_PERFORM_CRASH,
title = UiText.Simple("Perform crash"),
text = UiText.Simple("App will be crashed. Obviously")
)
val debugShowCrashAlert = SettingsItem.Switch(
key = SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT,
defaultValue = true,
title = UiText.Simple("Show alert after crash"),
text = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)")
)
val debugLongPollBackground = SettingsItem.Switch(
key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND,
title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title),
text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary)
)
val debugUseBlur = SettingsItem.Switch(
key = SettingsKeys.KEY_APPEARANCE_USE_BLUR,
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR,
title = UiText.Simple("[WIP] Use blur"),
text = UiText.Simple("Adds blur wherever possible\nOn android 11 and older will have transparency instead of blurring"),
)
val debugShowEmojiButton = SettingsItem.Switch(
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
title = UiText.Simple("Show emoji button"),
text = UiText.Simple("Show emoji button in chat panel"),
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
)
val debugShowTimeInActionMessages = SettingsItem.Switch(
key = SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES,
defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES,
title = UiText.Simple("Show time in action messages")
)
val debugHideDebugList = SettingsItem.TitleText(
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
title = UiText.Simple("Hide debug list")
)
val accountList = listOf(
accountTitle,
accountLogOut
)
val generalList = listOf(
generalTitle,
generalUseContactNames,
generalEnablePullToRefresh
)
val appearanceList = listOf(
appearanceTitle,
appearanceMultiline,
appearanceDarkTheme,
appearanceUseAmoledDarkTheme,
appearanceUseDynamicColors,
appearanceLanguage
)
val featuresList = listOf(
featuresTitle,
featuresFastText
)
val visibilityList = listOf(
activityTitle,
visibilitySendOnlineStatus,
)
val debugList = mutableListOf<SettingsItem<*>>()
listOf(
debugTitle,
debugPerformCrash,
debugShowCrashAlert,
debugLongPollBackground,
debugUseBlur,
debugShowEmojiButton,
debugShowTimeInActionMessages
).forEach(debugList::add)
debugList += debugHideDebugList
val settingsList = mutableListOf<SettingsItem<*>>()
listOf(
accountList,
generalList,
appearanceList,
featuresList,
visibilityList,
debugList,
).forEach(settingsList::addAll)
if (!AppSettings.Debug.showDebugCategory) {
settingsList.removeAll(debugList)
}
emitSettings(settingsList)
}
private fun emitSettings(newSettings: List<SettingsItem<*>>) {
settings.update { newSettings }
val uiSettings = newSettings.map { item ->
item.asPresentation(resources)
}
screenState.setValue { old -> old.copy(settings = uiSettings) }
}
}
enum class HapticType {
LONG_PRESS, REJECT;
fun getHaptic(): Int = when (this) {
LONG_PRESS -> HapticFeedbackConstantsCompat.LONG_PRESS
REJECT -> HapticFeedbackConstantsCompat.REJECT
}
}
@@ -0,0 +1,11 @@
package dev.meloda.fast.settings.di
import dev.meloda.fast.settings.SettingsViewModel
import dev.meloda.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,250 @@
package dev.meloda.fast.settings.model
import android.content.res.Resources
import androidx.compose.runtime.Immutable
import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString
import dev.meloda.fast.datastore.AppSettings
import kotlin.reflect.KClass
@Immutable
sealed class SettingsItem<T>(
val key: String,
value: T,
defaultValue: T?
) {
private val haveValue
get() = this::class !in listOf<KClass<*>>(
Title::class,
TitleText::class
)
init {
require(key.trim().isNotEmpty()) {
"Key must not be empty"
}
require(!haveValue || defaultValue != null) {
"Default value must not be null"
}
}
var isVisible: Boolean = true
var isEnabled: Boolean = true
var value: T = value
protected set(newValue) {
field = newValue
AppSettings.put(key, newValue)
}
var title: UiText? = null
var text: UiText? = null
var textProvider: TextProvider<T, SettingsItem<T>>? = null
set(value) {
field = value
updateText()
}
fun updateText() {
textProvider?.provideText(this)?.let { newText -> text = newText }
}
@Suppress("UNCHECKED_CAST")
fun updateValue(newValue: Any?) {
if (!haveValue) throw IllegalStateException("This item does not have a value")
value = newValue as T
}
class Title(
key: String,
title: UiText,
isVisible: Boolean = true
) : SettingsItem<Unit>(
key = key,
value = Unit,
defaultValue = null
) {
init {
this.title = title
this.isVisible = isVisible
}
}
class TitleText(
key: String,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true
) : SettingsItem<Unit>(
key = key,
value = Unit,
defaultValue = null
) {
init {
require(title != null || text != null) {
"Either title or text must not be null"
}
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
}
}
class Switch(
key: String,
defaultValue: Boolean,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true,
isChecked: Boolean? = null
) : SettingsItem<Boolean>(
key = key,
value = isChecked ?: getCurrentValue(key, defaultValue),
defaultValue = defaultValue
) {
init {
require(title != null || text != null) {
"Either title or text must not be null"
}
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
}
}
class TextField(
key: String,
defaultValue: String,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true,
fieldText: String? = null
) : SettingsItem<String>(
key = key,
value = fieldText ?: getCurrentValue(key, defaultValue),
defaultValue = defaultValue
) {
init {
require(title != null || text != null) {
"Either title or text must not be null"
}
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
}
}
class ListItem<T : Any>(
key: String,
defaultValue: T,
valueClass: KClass<T>,
title: UiText? = null,
text: UiText? = null,
isVisible: Boolean = true,
isEnabled: Boolean = true,
selectedValue: T? = null,
val titles: List<UiText>,
val values: List<T>
) : SettingsItem<T>(
key = key,
value = selectedValue ?: AppSettings.get(valueClass, key, defaultValue),
defaultValue = defaultValue
) {
init {
require(title != null || text != null) {
"Either title or text must not be null"
}
require(titles.isNotEmpty()) {
"titles must not be empty"
}
this.title = title
this.text = textProvider?.provideText(this) ?: text
this.isVisible = isVisible
this.isEnabled = isEnabled
}
}
fun asPresentation(resources: Resources): UiItem = when (val item = this) {
is Title -> {
UiItem.Title(
key = item.key,
title = item.title.parseString(resources).orEmpty(),
isVisible = item.isVisible
)
}
is TitleText -> {
UiItem.TitleText(
key = item.key,
title = item.title.parseString(resources),
text = item.text.parseString(resources),
isVisible = item.isVisible,
isEnabled = item.isEnabled
)
}
is Switch -> {
UiItem.Switch(
key = item.key,
title = item.title.parseString(resources),
text = item.text.parseString(resources),
isVisible = item.isVisible,
isEnabled = item.isEnabled,
isChecked = item.value
)
}
is TextField -> {
UiItem.TextField(
key = item.key,
title = item.title.parseString(resources),
text = item.text.parseString(resources),
isVisible = item.isVisible,
isEnabled = item.isEnabled,
fieldText = item.value
)
}
is ListItem<*> -> {
UiItem.List(
key = item.key,
title = item.title.parseString(resources),
text = item.text.parseString(resources),
isVisible = item.isVisible,
isEnabled = item.isEnabled,
selectedValue = item.value,
titles = item.titles.mapNotNull { it.parseString(resources) },
values = item.values
)
}
}
}
private inline fun <reified T> getCurrentValue(key: String, defaultValue: T): T {
if (T::class == Nothing::class) {
throw IllegalStateException("Items with Nothing does not have a value")
} else {
return AppSettings.get(key, defaultValue)
}
}
@@ -0,0 +1,20 @@
package dev.meloda.fast.settings.model
import androidx.compose.runtime.Immutable
import dev.meloda.fast.datastore.AppSettings
@Immutable
data class SettingsScreenState(
val showOptions: SettingsShowOptions,
val settings: List<UiItem>,
val showDebugOptions: Boolean
) {
companion object {
val EMPTY: SettingsScreenState = SettingsScreenState(
showOptions = SettingsShowOptions.EMPTY,
settings = emptyList(),
showDebugOptions = AppSettings.Debug.showDebugCategory
)
}
}
@@ -0,0 +1,14 @@
package dev.meloda.fast.settings.model
data class SettingsShowOptions(
val showLogOut: Boolean,
val showPerformCrash: Boolean,
) {
companion object {
val EMPTY: SettingsShowOptions = SettingsShowOptions(
showLogOut = false,
showPerformCrash = false,
)
}
}
@@ -0,0 +1,7 @@
package dev.meloda.fast.settings.model
import dev.meloda.fast.common.model.UiText
fun interface TextProvider<T, S : SettingsItem<T>> {
fun provideText(item: S): UiText?
}
@@ -0,0 +1,7 @@
package dev.meloda.fast.settings.model
import dev.meloda.fast.common.model.UiText
fun interface TitleProvider<T, S : SettingsItem<T>> {
fun provideTitle(item: S): UiText?
}
@@ -0,0 +1,53 @@
package dev.meloda.fast.settings.model
import androidx.compose.runtime.Immutable
typealias StringList = List<String>
typealias TypeList<T> = List<T>
@Immutable
sealed class UiItem(open val key: String) {
data class Title(
override val key: String,
val title: String,
val isVisible: Boolean
) : UiItem(key)
data class TitleText(
override val key: String,
val title: String?,
val text: String?,
val isVisible: Boolean,
val isEnabled: Boolean
) : UiItem(key)
data class Switch(
override val key: String,
val title: String?,
val text: String?,
val isVisible: Boolean,
val isEnabled: Boolean,
val isChecked: Boolean
) : UiItem(key)
data class TextField(
override val key: String,
val title: String?,
val text: String?,
val isVisible: Boolean,
val isEnabled: Boolean,
val fieldText: String
) : UiItem(key)
data class List<T>(
override val key: String,
val title: String?,
val text: String?,
val isVisible: Boolean,
val isEnabled: Boolean,
val selectedValue: T,
val titles: StringList,
val values: TypeList<T>
) : UiItem(key)
}
@@ -0,0 +1,28 @@
package dev.meloda.fast.settings.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import dev.meloda.fast.settings.presentation.SettingsRoute
import kotlinx.serialization.Serializable
@Serializable
object Settings
fun NavGraphBuilder.settingsScreen(
onBack: () -> Unit,
onLogOutButtonClicked: () -> Unit,
onLanguageItemClicked: () -> Unit
) {
composable<Settings> {
SettingsRoute(
onBack = onBack,
onLogOutButtonClicked = onLogOutButtonClicked,
onLanguageItemClicked = onLanguageItemClicked
)
}
}
fun NavController.navigateToSettings() {
this.navigate(Settings)
}
@@ -0,0 +1,305 @@
package dev.meloda.fast.settings.presentation
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.foundation.lazy.items
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.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.fast.common.UserConfig
import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.settings.HapticType
import dev.meloda.fast.settings.SettingsViewModel
import dev.meloda.fast.settings.SettingsViewModelImpl
import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.settings.presentation.item.ListItem
import dev.meloda.fast.settings.presentation.item.SwitchItem
import dev.meloda.fast.settings.presentation.item.TextFieldItem
import dev.meloda.fast.settings.presentation.item.TitleItem
import dev.meloda.fast.settings.presentation.item.TitleTextItem
import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.theme.LocalThemeConfig
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 dev.meloda.fast.ui.R as UiR
@Composable
fun SettingsRoute(
onBack: () -> Unit,
onLogOutButtonClicked: () -> Unit,
onLanguageItemClicked: () -> Unit,
viewModel: SettingsViewModel = koinViewModel<SettingsViewModelImpl>()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val hapticType by viewModel.hapticType.collectAsStateWithLifecycle()
SettingsScreen(
screenState = screenState,
hapticType = hapticType,
onBack = onBack,
onHapticPerformed = viewModel::onHapticPerformed,
onSettingsItemClicked = { key ->
when (key) {
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
onLanguageItemClicked()
}
else -> viewModel.onSettingsItemClicked(key)
}
},
onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked,
onSettingsItemValueChanged = viewModel::onSettingsItemChanged
)
HandlePopups(
performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked,
performCrashDismissed = viewModel::onPerformCrashAlertDismissed,
logoutPositiveClick = {
viewModel.onLogOutAlertPositiveClick()
onLogOutButtonClicked()
},
logoutDismissed = viewModel::onLogOutAlertDismissed,
screenState = screenState
)
}
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun SettingsScreen(
screenState: SettingsScreenState = SettingsScreenState.EMPTY,
hapticType: HapticType? = null,
onBack: () -> Unit = {},
onHapticPerformed: () -> Unit = {},
onSettingsItemClicked: (key: String) -> Unit = {},
onSettingsItemLongClicked: (key: String) -> Unit = {},
onSettingsItemValueChanged: (key: String, newValue: Any?) -> Unit = { _, _ -> }
) {
val view = LocalView.current
LaunchedEffect(hapticType) {
if (hapticType != null) {
view.performHapticFeedback(hapticType.getHaptic())
onHapticPerformed()
}
}
val themeConfig = LocalThemeConfig.current
val hazeState = remember { HazeState() }
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
TopAppBar(
title = { Text(text = stringResource(id = UiR.string.title_settings)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(id = UiR.drawable.ic_round_arrow_back_24),
contentDescription = "Back button"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface.copy(
alpha = if (themeConfig.enableBlur) 0f else 1f
)
),
modifier = Modifier
.then(
if (themeConfig.enableBlur) {
Modifier.hazeChild(
state = hazeState,
style = HazeMaterials.thick()
)
} else {
Modifier
}
)
.fillMaxWidth()
)
}
) { padding ->
LazyColumn(
modifier = Modifier
.then(
if (themeConfig.enableBlur) {
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())
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(
items = screenState.settings,
key = UiItem::key,
contentType = { item ->
when (item) {
is UiItem.Title -> "title"
is UiItem.TitleText -> "title_text"
is UiItem.Switch -> "switch"
is UiItem.TextField -> "text_field"
is UiItem.List<*> -> "list"
}
}
) { item ->
when (item) {
is UiItem.Title -> {
TitleItem(item = item)
}
is UiItem.TitleText -> {
TitleTextItem(
item = item,
onClick = onSettingsItemClicked,
onLongClick = onSettingsItemLongClicked
)
}
is UiItem.Switch -> {
SwitchItem(
item = item,
onClick = { onSettingsItemClicked(item.key) },
onLongClick = { onSettingsItemLongClicked(item.key) },
onChanged = { onSettingsItemValueChanged(item.key, it) }
)
}
is UiItem.TextField -> {
TextFieldItem(
item = item,
onClick = { onSettingsItemClicked(item.key) },
onLongClick = { onSettingsItemLongClicked(item.key) },
onChanged = { onSettingsItemValueChanged(item.key, it) }
)
}
is UiItem.List<*> -> {
ListItem(
item = item,
onClick = { onSettingsItemClicked(item.key) },
onLongClick = { onSettingsItemLongClicked(item.key) },
onChanged = { onSettingsItemValueChanged(item.key, it) }
)
}
}
}
item {
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
}
@Composable
fun HandlePopups(
performCrashPositiveClick: () -> Unit,
performCrashDismissed: () -> Unit,
logoutPositiveClick: () -> Unit,
logoutDismissed: () -> Unit,
screenState: SettingsScreenState
) {
val showOptions = screenState.showOptions
PerformCrashDialog(
positiveClick = performCrashPositiveClick,
dismiss = performCrashDismissed,
show = showOptions.showPerformCrash
)
LogOutDialog(
positiveClick = logoutPositiveClick,
dismiss = logoutDismissed,
show = showOptions.showLogOut
)
}
@Composable
fun PerformCrashDialog(
positiveClick: () -> Unit,
dismiss: () -> Unit,
show: Boolean,
) {
if (show) {
MaterialDialog(
onDismissRequest = dismiss,
title = "Perform crash",
text = "App will be crashed. Are you sure?",
confirmAction = positiveClick,
confirmText = stringResource(id = UiR.string.yes),
cancelText = stringResource(id = UiR.string.cancel),
actionInvokeDismiss = ActionInvokeDismiss.Always
)
}
}
@Composable
fun LogOutDialog(
positiveClick: () -> Unit,
dismiss: () -> Unit,
show: Boolean
) {
if (show) {
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
MaterialDialog(
onDismissRequest = dismiss,
title = stringResource(
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.sign_out_confirm_title
),
text = stringResource(id = UiR.string.sign_out_confirm),
confirmAction = positiveClick,
confirmText = stringResource(
id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.action_sign_out
),
cancelText = stringResource(id = UiR.string.no),
actionInvokeDismiss = ActionInvokeDismiss.Always
)
}
}
@@ -0,0 +1,145 @@
package dev.meloda.fast.settings.presentation.item
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.components.SelectionType
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListItem(
item: UiItem.List<*>,
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit,
onChanged: (newValue: Any?) -> Unit
) {
if (!item.isVisible) return
var showDialog by rememberSaveable {
mutableStateOf(false)
}
val currentTheme = LocalThemeConfig.current
Row(
modifier = modifier
.heightIn(min = 56.dp)
.fillMaxWidth()
.animateContentSize()
.combinedClickable(
enabled = item.isEnabled,
onClick = {
onClick()
showDialog = true
},
onLongClick = onLongClick,
)
) {
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Spacer(modifier = Modifier.height(14.dp))
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.high else ContentAlpha.disabled
) {
item.title?.let { title ->
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled
) {
item.text?.let { text ->
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Spacer(modifier = Modifier.height(14.dp))
}
Spacer(modifier = Modifier.width(16.dp))
}
if (showDialog) {
ListAlertDialog(
item = item,
onDismissAction = { showDialog = false },
onConfirmButtonClicked = onChanged
)
}
}
@Composable
fun ListAlertDialog(
item: UiItem.List<*>,
onDismissAction: () -> Unit,
onConfirmButtonClicked: (newValue: Any?) -> Unit
) {
val currentValueIndex = remember {
item.values.indexOf(item.selectedValue)
}
var selectedOptionIndex by rememberSaveable {
mutableIntStateOf(currentValueIndex)
}
MaterialDialog(
onDismissRequest = onDismissAction,
title = item.title,
items = item.titles.toImmutableList(),
preSelectedItems = ImmutableList.of(selectedOptionIndex),
selectionType = SelectionType.Single,
onItemClick = { newIndex -> selectedOptionIndex = newIndex },
confirmText = stringResource(id = R.string.ok),
confirmAction = {
if (currentValueIndex != selectedOptionIndex) {
onConfirmButtonClicked(item.values[selectedOptionIndex])
}
},
actionInvokeDismiss = ActionInvokeDismiss.Always
)
}
@@ -0,0 +1,98 @@
package dev.meloda.fast.settings.presentation.item
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SwitchItem(
item: UiItem.Switch,
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit,
onChanged: (isChecked: Boolean) -> Unit
) {
if (!item.isVisible) return
val currentTheme = LocalThemeConfig.current
Row(
modifier = modifier
.fillMaxSize()
.heightIn(min = 56.dp)
.animateContentSize()
.combinedClickable(
enabled = item.isEnabled,
onClick = {
onClick()
onChanged(!item.isChecked)
},
onLongClick = onLongClick,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Spacer(modifier = Modifier.height(14.dp))
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.high else ContentAlpha.disabled
) {
item.title?.let { title ->
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled
) {
item.text?.let { text ->
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Spacer(modifier = Modifier.height(14.dp))
}
Row {
Spacer(modifier = Modifier.width(16.dp))
Switch(
enabled = item.isEnabled,
checked = item.isChecked,
onCheckedChange = null
)
}
Spacer(modifier = Modifier.width(16.dp))
}
}
@@ -0,0 +1,166 @@
package dev.meloda.fast.settings.presentation.item
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TextFieldItem(
item: UiItem.TextField,
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit,
onChanged: (fieldText: String) -> Unit
) {
if (!item.isVisible) return
val currentTheme = LocalThemeConfig.current
var showDialog by rememberSaveable {
mutableStateOf(false)
}
if (showDialog) {
EditTextAlert(
item = item,
onAlertConfirmClicked = onChanged,
onDismiss = { showDialog = false },
)
}
Row(
modifier = modifier
.heightIn(min = 56.dp)
.fillMaxWidth()
.animateContentSize()
.combinedClickable(
enabled = item.isEnabled,
onClick = {
onClick()
showDialog = true
},
onLongClick = onLongClick,
)
) {
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Spacer(modifier = Modifier.height(14.dp))
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.high else ContentAlpha.disabled
) {
item.title?.let { title ->
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled
) {
item.text?.let { text ->
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Spacer(modifier = Modifier.height(14.dp))
}
Spacer(modifier = Modifier.width(16.dp))
}
}
@Composable
fun EditTextAlert(
item: UiItem.TextField,
onAlertConfirmClicked: (newValue: String) -> Unit,
onDismiss: () -> Unit
) {
val (textFieldFocusable) = FocusRequester.createRefs()
var textFieldValue by remember {
mutableStateOf(TextFieldValue(item.fieldText))
}
MaterialDialog(
onDismissRequest = onDismiss,
title = item.title,
confirmText = stringResource(id = R.string.ok),
confirmAction = {
onAlertConfirmClicked(textFieldValue.text.trim())
},
cancelText = stringResource(id = R.string.cancel),
actionInvokeDismiss = ActionInvokeDismiss.Always
) {
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,38 @@
package dev.meloda.fast.settings.presentation.item
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.ui.theme.LocalThemeConfig
@Composable
fun TitleItem(
item: UiItem.Title,
modifier: Modifier = Modifier
) {
if (!item.isVisible) return
val currentTheme = LocalThemeConfig.current
Text(
text = item.title,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.padding(
top = 14.dp,
end = 16.dp,
start = 16.dp,
bottom = 4.dp
)
.animateContentSize(),
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
@@ -0,0 +1,87 @@
package dev.meloda.fast.settings.presentation.item
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.meloda.fast.settings.model.UiItem
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TitleTextItem(
item: UiItem.TitleText,
modifier: Modifier = Modifier,
onClick: (key: String) -> Unit = {},
onLongClick: (key: String) -> Unit = {}
) {
if (!item.isVisible) return
val currentTheme = LocalThemeConfig.current
Row(
modifier = modifier
.heightIn(min = 56.dp)
.fillMaxWidth()
.animateContentSize()
.combinedClickable(
enabled = item.isEnabled,
onClick = { onClick(item.key) },
onLongClick = { 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 (item.isEnabled) ContentAlpha.high
else ContentAlpha.disabled
) {
item.title?.let { title ->
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
LocalContentAlpha(
alpha = if (item.isEnabled) ContentAlpha.medium else ContentAlpha.disabled
) {
item.text?.let { text ->
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
maxLines = if (currentTheme.enableMultiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Spacer(modifier = Modifier.height(14.dp))
}
Spacer(modifier = Modifier.width(16.dp))
}
}