From a5bb1f84dc324ea9725f95b2a8722cb0171969cf Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sun, 3 May 2026 14:32:19 +0200 Subject: [PATCH] feat(settings): add SettingsScreen and integrate settings management --- .../purefin/navigation/RouteEntryBuilder.kt | 4 + .../hu/bbara/purefin/ui/screen/AppScreen.kt | 2 +- .../ui/screen/settings/SettingsScreen.kt | 116 ++++++++++++++++++ .../settings/components/BooleanSettingItem.kt | 33 +++++ .../settings/components/NumberSettingItem.kt | 52 ++++++++ .../settings/components/StringSettingItem.kt | 32 +++++ .../feature/browse/home/AppViewModel.kt | 4 + .../feature/settings/SettingsViewModel.kt | 47 +++++++ .../java/hu/bbara/purefin/navigation/Route.kt | 3 + .../bbara/purefin/settings/SettingOption.kt | 32 ++++- 10 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt create mode 100644 app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/BooleanSettingItem.kt create mode 100644 app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/NumberSettingItem.kt create mode 100644 app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/StringSettingItem.kt create mode 100644 core/src/main/java/hu/bbara/purefin/feature/settings/SettingsViewModel.kt diff --git a/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt b/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt index 17ddb728..7420355b 100644 --- a/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt +++ b/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt @@ -9,6 +9,7 @@ import hu.bbara.purefin.ui.screen.AppScreen import hu.bbara.purefin.ui.screen.episode.EpisodeScreen import hu.bbara.purefin.ui.screen.home.components.search.SearchScreen import hu.bbara.purefin.ui.screen.movie.MovieScreen +import hu.bbara.purefin.ui.screen.settings.SettingsScreen import hu.bbara.purefin.ui.screen.series.SeriesScreen fun EntryProviderScope.appRouteEntryBuilder() { @@ -53,4 +54,7 @@ fun EntryProviderScope.appRouteEntryBuilder() { entry { LoginScreen() } + entry { + SettingsScreen() + } } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt index 16cf2350..38a36cc0 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt @@ -94,7 +94,7 @@ fun AppScreen( onProfileClick = {}, onCheckForUpdates = updateViewModel::checkForUpdates, isCheckingForUpdates = isCheckingForUpdates, - onSettingsClick = {}, + onSettingsClick = viewModel::openSettings, onLogoutClick = viewModel::logout, onSearchClick = viewModel::openSearch, snackbarHostState = snackbarHostState, 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 new file mode 100644 index 00000000..47d97f81 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt @@ -0,0 +1,116 @@ +package hu.bbara.purefin.ui.screen.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import hu.bbara.purefin.feature.settings.SettingsViewModel +import hu.bbara.purefin.settings.SettingsOptions +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.StringSettingItem + +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + topBar = { + SettingsTopBar(onBack = viewModel::onBack) + } + ) { innerPadding -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + item { + Text( + text = "Settings", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) + ) + } + + 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.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() + } + } + } + } +} + +@Composable +private fun SettingsTopBar( + onBack: () -> Unit +) { + DefaultTopBar( + leftActions = { + DefaultTopBarIconButton( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back", + onClick = onBack + ) + } + ) +} diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/BooleanSettingItem.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/BooleanSettingItem.kt new file mode 100644 index 00000000..1f506ad0 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/BooleanSettingItem.kt @@ -0,0 +1,33 @@ +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.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun BooleanSettingItem( + title: String, + value: Boolean, + onValueChange: (Boolean) -> Unit, + modifier: Modifier = Modifier.Companion +) { + ListItem( + headlineContent = { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge + ) + }, + trailingContent = { + Switch( + checked = value, + onCheckedChange = onValueChange + ) + }, + modifier = modifier.clickable { onValueChange(!value) } + ) +} \ No newline at end of file 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/NumberSettingItem.kt new file mode 100644 index 00000000..37a24135 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/NumberSettingItem.kt @@ -0,0 +1,52 @@ +package hu.bbara.purefin.ui.screen.settings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +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 +import java.util.Locale + +@Composable +fun NumberSettingItem( + title: String, + value: Double, + valueRange: ClosedFloatingPointRange, + onValueChange: (Double) -> Unit, + modifier: Modifier = Modifier.Companion +) { + var sliderValue by remember(value) { mutableStateOf(value.toFloat()) } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = String.Companion.format(Locale.US, "%.1f", sliderValue), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Slider( + value = sliderValue, + onValueChange = { sliderValue = it }, + onValueChangeFinished = { onValueChange(sliderValue.toDouble()) }, + valueRange = valueRange.start.toFloat()..valueRange.endInclusive.toFloat(), + modifier = Modifier.Companion.fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/StringSettingItem.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/StringSettingItem.kt new file mode 100644 index 00000000..fe442bba --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/components/StringSettingItem.kt @@ -0,0 +1,32 @@ +package hu.bbara.purefin.ui.screen.settings.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp + +@Composable +fun StringSettingItem( + title: String, + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier.Companion +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(title) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Companion.Words + ), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + ) +} \ No newline at end of file diff --git a/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt b/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt index b16b2d85..a16a19df 100644 --- a/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt @@ -256,6 +256,10 @@ class AppViewModel @Inject constructor( navigationManager.navigate(Route.HomeSearchRoute) } + fun openSettings() { + navigationManager.navigate(Route.SettingsRoute) + } + fun logout() { viewModelScope.launch { userSessionRepository.setLoggedIn(false) diff --git a/core/src/main/java/hu/bbara/purefin/feature/settings/SettingsViewModel.kt b/core/src/main/java/hu/bbara/purefin/feature/settings/SettingsViewModel.kt new file mode 100644 index 00000000..9d49ef1f --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/feature/settings/SettingsViewModel.kt @@ -0,0 +1,47 @@ +package hu.bbara.purefin.feature.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import hu.bbara.purefin.navigation.NavigationManager +import hu.bbara.purefin.settings.BooleanSetting +import hu.bbara.purefin.settings.NumberSetting +import hu.bbara.purefin.settings.SettingsRepository +import hu.bbara.purefin.settings.StringSetting +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val settingsRepository: SettingsRepository, + private val navigationManager: NavigationManager, +) : ViewModel() { + + fun value(option: NumberSetting) = settingsRepository.value(option) + + fun value(option: BooleanSetting) = settingsRepository.value(option) + + fun value(option: StringSetting) = settingsRepository.value(option) + + fun set(option: NumberSetting, value: Double) { + viewModelScope.launch { + settingsRepository.set(option, value) + } + } + + fun set(option: BooleanSetting, value: Boolean) { + viewModelScope.launch { + settingsRepository.set(option, value) + } + } + + fun set(option: StringSetting, value: String) { + viewModelScope.launch { + settingsRepository.set(option, value) + } + } + + fun onBack() { + navigationManager.pop() + } +} diff --git a/core/src/main/java/hu/bbara/purefin/navigation/Route.kt b/core/src/main/java/hu/bbara/purefin/navigation/Route.kt index 327d1fbe..942379a1 100644 --- a/core/src/main/java/hu/bbara/purefin/navigation/Route.kt +++ b/core/src/main/java/hu/bbara/purefin/navigation/Route.kt @@ -26,6 +26,9 @@ sealed interface Route : NavKey { @Serializable data object LoginRoute : Route + @Serializable + data object SettingsRoute : Route + @Serializable data class PlayerRoute(val mediaId: String) : Route } diff --git a/core/src/main/java/hu/bbara/purefin/settings/SettingOption.kt b/core/src/main/java/hu/bbara/purefin/settings/SettingOption.kt index 61464a6d..1e3b4185 100644 --- a/core/src/main/java/hu/bbara/purefin/settings/SettingOption.kt +++ b/core/src/main/java/hu/bbara/purefin/settings/SettingOption.kt @@ -2,20 +2,50 @@ package hu.bbara.purefin.settings sealed interface SettingOption { val key: String + val title: String val defaultValue: T } data class NumberSetting( override val key: String, - override val defaultValue: Double + override val title: String, + override val defaultValue: Double, + val valueRange: ClosedFloatingPointRange = 0.0..100.0, ) : SettingOption data class BooleanSetting( override val key: String, + override val title: String, override val defaultValue: Boolean ) : SettingOption data class StringSetting( override val key: String, + override val title: String, override val defaultValue: String ) : SettingOption + +object SettingsOptions { + val defaultPlaybackSpeed = NumberSetting( + key = "default_playback_speed", + title = "Default playback speed", + defaultValue = 1.0, + valueRange = 0.5..2.0 + ) + + val confirmMobileDataPlayback = BooleanSetting( + key = "confirm_mobile_data_playback", + title = "Confirm mobile data playback", + defaultValue = true + ) + + val preferredAudioLanguage = StringSetting( + key = "preferred_audio_language", + title = "Preferred audio language", + defaultValue = "English" + ) + + val numberSettings = listOf(defaultPlaybackSpeed) + val booleanSettings = listOf(confirmMobileDataPlayback) + val stringSettings = listOf(preferredAudioLanguage) +}