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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize 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.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack 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.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -32,7 +35,13 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.core.feature.settings.SettingsViewModel 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.SettingsOptions
import hu.bbara.purefin.core.settings.StringSetting
import hu.bbara.purefin.core.settings.VoidSetting
import java.util.Locale import java.util.Locale
@Composable @Composable
@@ -63,17 +72,39 @@ fun TvSettingsScreen(
) )
} }
SettingsOptions.groups.forEach { group ->
group.title?.let { title ->
item { item {
Text( Text(
text = "Playback", text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 28.dp, vertical = 8.dp) modifier = Modifier.padding(horizontal = 28.dp, vertical = 8.dp)
) )
} }
}
SettingsOptions.numberSettings.forEach { option -> group.options.forEach { option ->
item(key = option.key) { 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) val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvNumberSettingItem( TvNumberSettingItem(
title = option.title, title = option.title,
@@ -81,35 +112,54 @@ fun TvSettingsScreen(
valueRange = option.valueRange, valueRange = option.valueRange,
onValueChange = { viewModel.set(option, it) } onValueChange = { viewModel.set(option, it) }
) )
HorizontalDivider()
}
} }
SettingsOptions.booleanSettings.forEach { option -> is BooleanSetting -> {
item(key = option.key) {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue) val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvBooleanSettingItem( TvBooleanSettingItem(
title = option.title, title = option.title,
value = value, value = value,
onValueChange = { viewModel.set(option, it) } onValueChange = { viewModel.set(option, it) }
) )
HorizontalDivider()
}
} }
SettingsOptions.stringSettings.forEach { option -> is StringSetting -> {
item(key = option.key) {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue) val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
TvStringSettingItem( TvStringSettingItem(
title = option.title, title = option.title,
value = value, value = value,
onValueChange = { viewModel.set(option, it) } 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 @Composable
@@ -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 @Composable
private fun TvNumberSettingItem( private fun TvNumberSettingItem(
title: String, title: String,

View File

@@ -17,12 +17,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.core.feature.settings.SettingsViewModel 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.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.DefaultTopBar
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBarIconButton 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.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.StringSettingItem
import hu.bbara.purefin.ui.screen.settings.components.VoidSettingItem
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -51,53 +59,94 @@ fun SettingsScreen(
) )
} }
SettingsOptions.groups.forEach { group ->
group.title?.let { title ->
item { item {
Text( Text(
text = "Playback", text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
) )
} }
}
SettingsOptions.numberSettings.forEach { option -> group.options.forEach { option ->
item(key = option.key) { 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) val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
NumberSettingItem( RangeSettingItem(
title = option.title, title = option.title,
value = value, value = value,
valueRange = option.valueRange, valueRange = option.valueRange,
onValueChange = { viewModel.set(option, it) } onValueChange = { viewModel.set(option, it) }
) )
HorizontalDivider()
}
} }
SettingsOptions.booleanSettings.forEach { option -> is BooleanSetting -> {
item(key = option.key) {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue) val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
BooleanSettingItem( BooleanSettingItem(
title = option.title, title = option.title,
value = value, value = value,
onValueChange = { viewModel.set(option, it) } onValueChange = { viewModel.set(option, it) }
) )
HorizontalDivider()
}
} }
SettingsOptions.stringSettings.forEach { option -> is StringSetting -> {
item(key = option.key) {
val value by viewModel.value(option).collectAsState(initial = option.defaultValue) val value by viewModel.value(option).collectAsState(initial = option.defaultValue)
StringSettingItem( StringSettingItem(
title = option.title, title = option.title,
value = value, value = value,
onValueChange = { viewModel.set(option, it) } 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 @Composable

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 import java.util.Locale
@Composable @Composable
fun NumberSettingItem( fun RangeSettingItem(
title: String, title: String,
value: Double, value: Double,
valueRange: ClosedFloatingPointRange<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 dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.settings.BooleanSetting 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.SettingsRepository
import hu.bbara.purefin.core.settings.StringSetting import hu.bbara.purefin.core.settings.StringSetting
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -17,13 +18,15 @@ class SettingsViewModel @Inject constructor(
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
) : ViewModel() { ) : 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: BooleanSetting) = settingsRepository.value(option)
fun value(option: StringSetting) = 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 { viewModelScope.launch {
settingsRepository.set(option, value) 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() { fun onBack() {
navigationManager.pop() navigationManager.pop()
} }

View File

@@ -6,7 +6,7 @@ sealed interface SettingOption<T> {
val defaultValue: T val defaultValue: T
} }
data class NumberSetting( data class RangeSetting(
override val key: String, override val key: String,
override val title: String, override val title: String,
override val defaultValue: Double, override val defaultValue: Double,
@@ -25,8 +25,26 @@ data class StringSetting(
override val defaultValue: String override val defaultValue: String
) : SettingOption<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 { object SettingsOptions {
val defaultPlaybackSpeed = NumberSetting( val defaultPlaybackSpeed = RangeSetting(
key = "default_playback_speed", key = "default_playback_speed",
title = "Default playback speed", title = "Default playback speed",
defaultValue = 1.0, defaultValue = 1.0,
@@ -45,7 +63,28 @@ object SettingsOptions {
defaultValue = "English" defaultValue = "English"
) )
val numberSettings = listOf(defaultPlaybackSpeed) val resetPlaybackSettings = VoidSetting(
val booleanSettings = listOf(confirmMobileDataPlayback) key = "reset_playback_settings",
val stringSettings = listOf(preferredAudioLanguage) 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 { interface SettingsRepository {
val settings: Flow<Settings> 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: BooleanSetting): Flow<Boolean>
fun value(option: StringSetting): Flow<String> 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: BooleanSetting, value: Boolean)
suspend fun set(option: StringSetting, value: String) 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 androidx.datastore.core.DataStore
import hu.bbara.purefin.core.settings.BooleanSetting 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.SettingsRepository
import hu.bbara.purefin.core.settings.StringSetting import hu.bbara.purefin.core.settings.StringSetting
import hu.bbara.purefin.model.Settings import hu.bbara.purefin.model.Settings
@@ -17,7 +18,7 @@ class SettingsRepositoryImpl @Inject constructor(
override val settings: Flow<Settings> = settingsDataStore.data override val settings: Flow<Settings> = settingsDataStore.data
override fun value(option: NumberSetting): Flow<Double> { override fun value(option: RangeSetting): Flow<Double> {
return settings return settings
.map { it.numberSettings[option.key] ?: option.defaultValue } .map { it.numberSettings[option.key] ?: option.defaultValue }
.distinctUntilChanged() .distinctUntilChanged()
@@ -35,7 +36,16 @@ class SettingsRepositoryImpl @Inject constructor(
.distinctUntilChanged() .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 -> settingsDataStore.updateData { current ->
current.copy( current.copy(
numberSettings = current.numberSettings + (option.key to value) 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())
)
}
}
} }