From af2e8b0dc933a61230ac5144e4fb98b773810b14 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Fri, 8 May 2026 20:19:05 +0200 Subject: [PATCH] feat: enhance settings screen with dropdown and void setting options for improved user experience --- .../ui/screen/settings/TvSettingsScreen.kt | 198 ++++++++++++++---- .../ui/screen/settings/SettingsScreen.kt | 133 ++++++++---- .../components/DropdownSettingItem.kt | 65 ++++++ ...mberSettingItem.kt => RangeSettingItem.kt} | 2 +- .../settings/components/VoidSettingItem.kt | 25 +++ .../feature/settings/SettingsViewModel.kt | 15 +- .../purefin/core/settings/SettingOption.kt | 49 ++++- .../core/settings/SettingsRepository.kt | 8 +- .../data/settings/SettingsRepositoryImpl.kt | 24 ++- 9 files changed, 422 insertions(+), 97 deletions(-) create mode 100644 app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/DropdownSettingItem.kt rename app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/{NumberSettingItem.kt => RangeSettingItem.kt} (98%) create mode 100644 app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/VoidSettingItem.kt diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt index b028a9f9..f34f07dc 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt @@ -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,55 +72,96 @@ fun TvSettingsScreen( ) } - item { - Text( - text = "Playback", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 28.dp, vertical = 8.dp) - ) - } - - SettingsOptions.numberSettings.forEach { option -> - item(key = option.key) { - val value by viewModel.value(option).collectAsState(initial = option.defaultValue) - TvNumberSettingItem( - title = option.title, - value = value, - valueRange = option.valueRange, - onValueChange = { viewModel.set(option, it) } - ) - HorizontalDivider() + SettingsOptions.groups.forEach { group -> + group.title?.let { title -> + item { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 28.dp, vertical = 8.dp) + ) + } } - } - SettingsOptions.booleanSettings.forEach { option -> - item(key = option.key) { - 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) { - val value by viewModel.value(option).collectAsState(initial = option.defaultValue) - TvStringSettingItem( - title = option.title, - value = value, - onValueChange = { viewModel.set(option, it) } - ) - HorizontalDivider() + 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, + value = value, + valueRange = option.valueRange, + onValueChange = { viewModel.set(option, it) } + ) + } + + is BooleanSetting -> { + val value by viewModel.value(option).collectAsState(initial = option.defaultValue) + TvBooleanSettingItem( + title = option.title, + value = value, + onValueChange = { viewModel.set(option, it) } + ) + } + + is StringSetting -> { + val value by viewModel.value(option).collectAsState(initial = option.defaultValue) + TvStringSettingItem( + title = option.title, + value = value, + onValueChange = { viewModel.set(option, it) } + ) + } + + is VoidSetting -> { + TvVoidSettingItem( + title = option.title, + onClick = {} + ) + } + + is DropdownSetting<*> -> { + TvDropdownSettingOptionItem( + option = option, + viewModel = viewModel + ) + } + } +} + +@Composable +private fun TvDropdownSettingOptionItem( + option: DropdownSetting, + 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 TvDropdownSettingItem( + title: String, + value: T, + options: List, + 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, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt index 99332963..8a16586c 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt @@ -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( ) } - item { - Text( - text = "Playback", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) - ) - } - - SettingsOptions.numberSettings.forEach { option -> - item(key = option.key) { - val value by viewModel.value(option).collectAsState(initial = option.defaultValue) - NumberSettingItem( - title = option.title, - value = value, - valueRange = option.valueRange, - onValueChange = { viewModel.set(option, it) } - ) - HorizontalDivider() + SettingsOptions.groups.forEach { group -> + group.title?.let { title -> + item { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) + ) + } } - } - SettingsOptions.booleanSettings.forEach { option -> - item(key = option.key) { - 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) { - val value by viewModel.value(option).collectAsState(initial = option.defaultValue) - StringSettingItem( - title = option.title, - value = value, - onValueChange = { viewModel.set(option, it) } - ) - HorizontalDivider() + 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) + RangeSettingItem( + title = option.title, + value = value, + valueRange = option.valueRange, + onValueChange = { viewModel.set(option, it) } + ) + } + + is BooleanSetting -> { + val value by viewModel.value(option).collectAsState(initial = option.defaultValue) + BooleanSettingItem( + title = option.title, + value = value, + onValueChange = { viewModel.set(option, it) } + ) + } + + is StringSetting -> { + val value by viewModel.value(option).collectAsState(initial = option.defaultValue) + StringSettingItem( + title = option.title, + value = value, + onValueChange = { viewModel.set(option, it) } + ) + } + + is VoidSetting -> { + VoidSettingItem( + title = option.title, + onClick = {} + ) + } + + is DropdownSetting<*> -> { + DropdownSettingOptionItem( + option = option, + viewModel = viewModel + ) + } + } +} + +@Composable +private fun DropdownSettingOptionItem( + option: DropdownSetting, + 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 diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/DropdownSettingItem.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/DropdownSettingItem.kt new file mode 100644 index 00000000..09c4d039 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/DropdownSettingItem.kt @@ -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 DropdownSettingItem( + title: String, + value: T, + options: List, + 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) + } + ) + } + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/NumberSettingItem.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/RangeSettingItem.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/NumberSettingItem.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/RangeSettingItem.kt index 37a24135..4a780e71 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/NumberSettingItem.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/RangeSettingItem.kt @@ -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, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/VoidSettingItem.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/VoidSettingItem.kt new file mode 100644 index 00000000..554c79a6 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/VoidSettingItem.kt @@ -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) + ) +} diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/settings/SettingsViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/settings/SettingsViewModel.kt index 5766b195..7bb9403e 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/settings/SettingsViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/settings/SettingsViewModel.kt @@ -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 value(option: DropdownSetting) = settingsRepository.value(option) + + fun set(option: RangeSetting, value: Double) { viewModelScope.launch { settingsRepository.set(option, value) } @@ -41,6 +44,12 @@ class SettingsViewModel @Inject constructor( } } + fun set(option: DropdownSetting, value: T) { + viewModelScope.launch { + settingsRepository.set(option, value) + } + } + fun onBack() { navigationManager.pop() } diff --git a/core/src/main/java/hu/bbara/purefin/core/settings/SettingOption.kt b/core/src/main/java/hu/bbara/purefin/core/settings/SettingOption.kt index b0cfa1c9..c1d8be59 100644 --- a/core/src/main/java/hu/bbara/purefin/core/settings/SettingOption.kt +++ b/core/src/main/java/hu/bbara/purefin/core/settings/SettingOption.kt @@ -6,7 +6,7 @@ sealed interface SettingOption { 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 +data class VoidSetting( + override val key: String, + override val title: String, + override val defaultValue: Unit = Unit +) : SettingOption + +data class DropdownSetting( + override val key: String, + override val title: String, + override val defaultValue: T, + val options: List +): SettingOption + +data class SettingGroup( + val title: String?, + val options: List> +) + 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 + ) + ) + ) } diff --git a/core/src/main/java/hu/bbara/purefin/core/settings/SettingsRepository.kt b/core/src/main/java/hu/bbara/purefin/core/settings/SettingsRepository.kt index 44fe67a8..7dfc1709 100644 --- a/core/src/main/java/hu/bbara/purefin/core/settings/SettingsRepository.kt +++ b/core/src/main/java/hu/bbara/purefin/core/settings/SettingsRepository.kt @@ -6,15 +6,19 @@ import kotlinx.coroutines.flow.Flow interface SettingsRepository { val settings: Flow - fun value(option: NumberSetting): Flow + fun value(option: RangeSetting): Flow fun value(option: BooleanSetting): Flow fun value(option: StringSetting): Flow - suspend fun set(option: NumberSetting, value: Double) + fun value(option: DropdownSetting): Flow + + suspend fun set(option: RangeSetting, value: Double) suspend fun set(option: BooleanSetting, value: Boolean) suspend fun set(option: StringSetting, value: String) + + suspend fun set(option: DropdownSetting, value: T) } diff --git a/data/src/main/java/hu/bbara/purefin/data/settings/SettingsRepositoryImpl.kt b/data/src/main/java/hu/bbara/purefin/data/settings/SettingsRepositoryImpl.kt index 80d26bab..e37f2c6a 100644 --- a/data/src/main/java/hu/bbara/purefin/data/settings/SettingsRepositoryImpl.kt +++ b/data/src/main/java/hu/bbara/purefin/data/settings/SettingsRepositoryImpl.kt @@ -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 = settingsDataStore.data - override fun value(option: NumberSetting): Flow { + override fun value(option: RangeSetting): Flow { 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 value(option: DropdownSetting): Flow { + 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 set(option: DropdownSetting, value: T) { + settingsDataStore.updateData { current -> + current.copy( + stringSettings = current.stringSettings + (option.key to value.toString()) + ) + } + } }