feat(settings): add SettingsScreen and integrate settings management

This commit is contained in:
2026-05-03 14:32:19 +02:00
parent f925ca57ea
commit a5bb1f84dc
10 changed files with 323 additions and 2 deletions

View File

@@ -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<Route>.appRouteEntryBuilder() {
@@ -53,4 +54,7 @@ fun EntryProviderScope<Route>.appRouteEntryBuilder() {
entry<Route.LoginRoute> {
LoginScreen()
}
entry<Route.SettingsRoute> {
SettingsScreen()
}
}

View File

@@ -94,7 +94,7 @@ fun AppScreen(
onProfileClick = {},
onCheckForUpdates = updateViewModel::checkForUpdates,
isCheckingForUpdates = isCheckingForUpdates,
onSettingsClick = {},
onSettingsClick = viewModel::openSettings,
onLogoutClick = viewModel::logout,
onSearchClick = viewModel::openSearch,
snackbarHostState = snackbarHostState,

View File

@@ -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
)
}
)
}

View File

@@ -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) }
)
}

View File

@@ -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<Double>,
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()
)
}
}

View File

@@ -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)
)
}

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -2,20 +2,50 @@ package hu.bbara.purefin.settings
sealed interface SettingOption<T> {
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<Double> = 0.0..100.0,
) : SettingOption<Double>
data class BooleanSetting(
override val key: String,
override val title: String,
override val defaultValue: Boolean
) : SettingOption<Boolean>
data class StringSetting(
override val key: String,
override val title: String,
override val defaultValue: String
) : SettingOption<String>
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)
}