feat: enhance settings screen with dropdown and void setting options for improved user experience

This commit is contained in:
2026-05-08 20:19:05 +02:00
parent e05041fd14
commit af2e8b0dc9
9 changed files with 422 additions and 97 deletions

View File

@@ -2,6 +2,7 @@ package hu.bbara.purefin.ui.screen.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
@@ -11,6 +12,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -32,7 +35,13 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.core.feature.settings.SettingsViewModel
import hu.bbara.purefin.core.settings.BooleanSetting
import hu.bbara.purefin.core.settings.DropdownSetting
import hu.bbara.purefin.core.settings.RangeSetting
import hu.bbara.purefin.core.settings.SettingOption
import hu.bbara.purefin.core.settings.SettingsOptions
import hu.bbara.purefin.core.settings.StringSetting
import hu.bbara.purefin.core.settings.VoidSetting
import java.util.Locale
@Composable
@@ -63,17 +72,39 @@ fun TvSettingsScreen(
)
}
SettingsOptions.groups.forEach { group ->
group.title?.let { title ->
item {
Text(
text = "Playback",
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 28.dp, vertical = 8.dp)
)
}
}
SettingsOptions.numberSettings.forEach { option ->
group.options.forEach { option ->
item(key = option.key) {
TvSettingOptionItem(
option = option,
viewModel = viewModel
)
HorizontalDivider()
}
}
}
}
}
}
@Composable
private fun TvSettingOptionItem(
option: SettingOption<*>,
viewModel: SettingsViewModel
) {
when (option) {
is RangeSetting -> {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvNumberSettingItem(
title = option.title,
@@ -81,37 +112,56 @@ fun TvSettingsScreen(
valueRange = option.valueRange,
onValueChange = { viewModel.set(option, it) }
)
HorizontalDivider()
}
}
SettingsOptions.booleanSettings.forEach { option ->
item(key = option.key) {
is BooleanSetting -> {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvBooleanSettingItem(
title = option.title,
value = value,
onValueChange = { viewModel.set(option, it) }
)
HorizontalDivider()
}
}
SettingsOptions.stringSettings.forEach { option ->
item(key = option.key) {
is StringSetting -> {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvStringSettingItem(
title = option.title,
value = value,
onValueChange = { viewModel.set(option, it) }
)
HorizontalDivider()
}
}
}
is VoidSetting -> {
TvVoidSettingItem(
title = option.title,
onClick = {}
)
}
is DropdownSetting<*> -> {
TvDropdownSettingOptionItem(
option = option,
viewModel = viewModel
)
}
}
}
@Composable
private fun <T> TvDropdownSettingOptionItem(
option: DropdownSetting<T>,
viewModel: SettingsViewModel
) {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvDropdownSettingItem(
title = option.title,
value = value,
options = option.options,
onValueChange = { viewModel.set(option, it) }
)
}
@Composable
private fun TvSettingsTopBar(
onBack: () -> Unit
@@ -131,6 +181,72 @@ private fun TvSettingsTopBar(
}
}
@Composable
private fun TvVoidSettingItem(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 28.dp, vertical = 16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge
)
}
}
@Composable
private fun <T> TvDropdownSettingItem(
title: String,
value: T,
options: List<T>,
onValueChange: (T) -> Unit,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = true }
.padding(horizontal = 28.dp, vertical = 16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge
)
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option.toString()) },
onClick = {
expanded = false
onValueChange(option)
}
)
}
}
}
}
@Composable
private fun TvNumberSettingItem(
title: String,

View File

@@ -17,12 +17,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.core.feature.settings.SettingsViewModel
import hu.bbara.purefin.core.settings.BooleanSetting
import hu.bbara.purefin.core.settings.DropdownSetting
import hu.bbara.purefin.core.settings.RangeSetting
import hu.bbara.purefin.core.settings.SettingOption
import hu.bbara.purefin.core.settings.SettingsOptions
import hu.bbara.purefin.core.settings.StringSetting
import hu.bbara.purefin.core.settings.VoidSetting
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBarIconButton
import hu.bbara.purefin.ui.screen.settings.components.BooleanSettingItem
import hu.bbara.purefin.ui.screen.settings.components.NumberSettingItem
import hu.bbara.purefin.ui.screen.settings.components.DropdownSettingItem
import hu.bbara.purefin.ui.screen.settings.components.RangeSettingItem
import hu.bbara.purefin.ui.screen.settings.components.StringSettingItem
import hu.bbara.purefin.ui.screen.settings.components.VoidSettingItem
@Composable
fun SettingsScreen(
@@ -51,55 +59,96 @@ fun SettingsScreen(
)
}
SettingsOptions.groups.forEach { group ->
group.title?.let { title ->
item {
Text(
text = "Playback",
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
)
}
}
SettingsOptions.numberSettings.forEach { option ->
group.options.forEach { option ->
item(key = option.key) {
SettingOptionItem(
option = option,
viewModel = viewModel
)
HorizontalDivider()
}
}
}
}
}
}
@Composable
private fun SettingOptionItem(
option: SettingOption<*>,
viewModel: SettingsViewModel
) {
when (option) {
is RangeSetting -> {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
NumberSettingItem(
RangeSettingItem(
title = option.title,
value = value,
valueRange = option.valueRange,
onValueChange = { viewModel.set(option, it) }
)
HorizontalDivider()
}
}
SettingsOptions.booleanSettings.forEach { option ->
item(key = option.key) {
is BooleanSetting -> {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
BooleanSettingItem(
title = option.title,
value = value,
onValueChange = { viewModel.set(option, it) }
)
HorizontalDivider()
}
}
SettingsOptions.stringSettings.forEach { option ->
item(key = option.key) {
is StringSetting -> {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
StringSettingItem(
title = option.title,
value = value,
onValueChange = { viewModel.set(option, it) }
)
HorizontalDivider()
}
}
}
is VoidSetting -> {
VoidSettingItem(
title = option.title,
onClick = {}
)
}
is DropdownSetting<*> -> {
DropdownSettingOptionItem(
option = option,
viewModel = viewModel
)
}
}
}
@Composable
private fun <T> DropdownSettingOptionItem(
option: DropdownSetting<T>,
viewModel: SettingsViewModel
) {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
DropdownSettingItem(
title = option.title,
value = value,
options = option.options,
onValueChange = { viewModel.set(option, it) }
)
}
@Composable
private fun SettingsTopBar(
onBack: () -> Unit

View File

@@ -0,0 +1,65 @@
package hu.bbara.purefin.ui.screen.settings.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
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.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> DropdownSettingItem(
title: String,
value: T,
options: List<T>,
onValueChange: (T) -> Unit,
modifier: Modifier = Modifier.Companion
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp)
) {
OutlinedTextField(
value = value.toString(),
onValueChange = {},
readOnly = true,
label = { Text(title) },
singleLine = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option.toString()) },
onClick = {
expanded = false
onValueChange(option)
}
)
}
}
}
}

View File

@@ -17,7 +17,7 @@ import androidx.compose.ui.unit.dp
import java.util.Locale
@Composable
fun NumberSettingItem(
fun RangeSettingItem(
title: String,
value: Double,
valueRange: ClosedFloatingPointRange<Double>,

View File

@@ -0,0 +1,25 @@
package hu.bbara.purefin.ui.screen.settings.components
import androidx.compose.foundation.clickable
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun VoidSettingItem(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier.Companion
) {
ListItem(
headlineContent = {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge
)
},
modifier = modifier.clickable(onClick = onClick)
)
}

View File

@@ -5,7 +5,8 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.settings.BooleanSetting
import hu.bbara.purefin.core.settings.NumberSetting
import hu.bbara.purefin.core.settings.DropdownSetting
import hu.bbara.purefin.core.settings.RangeSetting
import hu.bbara.purefin.core.settings.SettingsRepository
import hu.bbara.purefin.core.settings.StringSetting
import kotlinx.coroutines.launch
@@ -17,13 +18,15 @@ class SettingsViewModel @Inject constructor(
private val navigationManager: NavigationManager,
) : ViewModel() {
fun value(option: NumberSetting) = settingsRepository.value(option)
fun value(option: RangeSetting) = settingsRepository.value(option)
fun value(option: BooleanSetting) = settingsRepository.value(option)
fun value(option: StringSetting) = settingsRepository.value(option)
fun set(option: NumberSetting, value: Double) {
fun <T> value(option: DropdownSetting<T>) = settingsRepository.value(option)
fun set(option: RangeSetting, value: Double) {
viewModelScope.launch {
settingsRepository.set(option, value)
}
@@ -41,6 +44,12 @@ class SettingsViewModel @Inject constructor(
}
}
fun <T> set(option: DropdownSetting<T>, value: T) {
viewModelScope.launch {
settingsRepository.set(option, value)
}
}
fun onBack() {
navigationManager.pop()
}

View File

@@ -6,7 +6,7 @@ sealed interface SettingOption<T> {
val defaultValue: T
}
data class NumberSetting(
data class RangeSetting(
override val key: String,
override val title: String,
override val defaultValue: Double,
@@ -25,8 +25,26 @@ data class StringSetting(
override val defaultValue: String
) : SettingOption<String>
data class VoidSetting(
override val key: String,
override val title: String,
override val defaultValue: Unit = Unit
) : SettingOption<Unit>
data class DropdownSetting<T>(
override val key: String,
override val title: String,
override val defaultValue: T,
val options: List<T>
): SettingOption<T>
data class SettingGroup(
val title: String?,
val options: List<SettingOption<*>>
)
object SettingsOptions {
val defaultPlaybackSpeed = NumberSetting(
val defaultPlaybackSpeed = RangeSetting(
key = "default_playback_speed",
title = "Default playback speed",
defaultValue = 1.0,
@@ -45,7 +63,28 @@ object SettingsOptions {
defaultValue = "English"
)
val numberSettings = listOf(defaultPlaybackSpeed)
val booleanSettings = listOf(confirmMobileDataPlayback)
val stringSettings = listOf(preferredAudioLanguage)
val resetPlaybackSettings = VoidSetting(
key = "reset_playback_settings",
title = "Reset playback settings"
)
val streamingQuality = DropdownSetting(
key = "streaming_quality",
title = "Streaming quality",
defaultValue = "Auto",
options = listOf("Auto", "Low", "Medium", "High")
)
val groups = listOf(
SettingGroup(
title = "Playback",
options = listOf(
defaultPlaybackSpeed,
confirmMobileDataPlayback,
preferredAudioLanguage,
resetPlaybackSettings,
streamingQuality
)
)
)
}

View File

@@ -6,15 +6,19 @@ import kotlinx.coroutines.flow.Flow
interface SettingsRepository {
val settings: Flow<Settings>
fun value(option: NumberSetting): Flow<Double>
fun value(option: RangeSetting): Flow<Double>
fun value(option: BooleanSetting): Flow<Boolean>
fun value(option: StringSetting): Flow<String>
suspend fun set(option: NumberSetting, value: Double)
fun <T> value(option: DropdownSetting<T>): Flow<T>
suspend fun set(option: RangeSetting, value: Double)
suspend fun set(option: BooleanSetting, value: Boolean)
suspend fun set(option: StringSetting, value: String)
suspend fun <T> set(option: DropdownSetting<T>, value: T)
}

View File

@@ -2,7 +2,8 @@ package hu.bbara.purefin.data.settings
import androidx.datastore.core.DataStore
import hu.bbara.purefin.core.settings.BooleanSetting
import hu.bbara.purefin.core.settings.NumberSetting
import hu.bbara.purefin.core.settings.DropdownSetting
import hu.bbara.purefin.core.settings.RangeSetting
import hu.bbara.purefin.core.settings.SettingsRepository
import hu.bbara.purefin.core.settings.StringSetting
import hu.bbara.purefin.model.Settings
@@ -17,7 +18,7 @@ class SettingsRepositoryImpl @Inject constructor(
override val settings: Flow<Settings> = settingsDataStore.data
override fun value(option: NumberSetting): Flow<Double> {
override fun value(option: RangeSetting): Flow<Double> {
return settings
.map { it.numberSettings[option.key] ?: option.defaultValue }
.distinctUntilChanged()
@@ -35,7 +36,16 @@ class SettingsRepositoryImpl @Inject constructor(
.distinctUntilChanged()
}
override suspend fun set(option: NumberSetting, value: Double) {
override fun <T> value(option: DropdownSetting<T>): Flow<T> {
return settings
.map { settings ->
val storedValue = settings.stringSettings[option.key]
option.options.firstOrNull { it.toString() == storedValue } ?: option.defaultValue
}
.distinctUntilChanged()
}
override suspend fun set(option: RangeSetting, value: Double) {
settingsDataStore.updateData { current ->
current.copy(
numberSettings = current.numberSettings + (option.key to value)
@@ -58,4 +68,12 @@ class SettingsRepositoryImpl @Inject constructor(
)
}
}
override suspend fun <T> set(option: DropdownSetting<T>, value: T) {
settingsDataStore.updateData { current ->
current.copy(
stringSettings = current.stringSettings + (option.key to value.toString())
)
}
}
}