mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat(settings): add SettingsScreen and integrate settings management
This commit is contained in:
@@ -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.episode.EpisodeScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.components.search.SearchScreen
|
import hu.bbara.purefin.ui.screen.home.components.search.SearchScreen
|
||||||
import hu.bbara.purefin.ui.screen.movie.MovieScreen
|
import hu.bbara.purefin.ui.screen.movie.MovieScreen
|
||||||
|
import hu.bbara.purefin.ui.screen.settings.SettingsScreen
|
||||||
import hu.bbara.purefin.ui.screen.series.SeriesScreen
|
import hu.bbara.purefin.ui.screen.series.SeriesScreen
|
||||||
|
|
||||||
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
||||||
@@ -53,4 +54,7 @@ fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
|||||||
entry<Route.LoginRoute> {
|
entry<Route.LoginRoute> {
|
||||||
LoginScreen()
|
LoginScreen()
|
||||||
}
|
}
|
||||||
|
entry<Route.SettingsRoute> {
|
||||||
|
SettingsScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fun AppScreen(
|
|||||||
onProfileClick = {},
|
onProfileClick = {},
|
||||||
onCheckForUpdates = updateViewModel::checkForUpdates,
|
onCheckForUpdates = updateViewModel::checkForUpdates,
|
||||||
isCheckingForUpdates = isCheckingForUpdates,
|
isCheckingForUpdates = isCheckingForUpdates,
|
||||||
onSettingsClick = {},
|
onSettingsClick = viewModel::openSettings,
|
||||||
onLogoutClick = viewModel::logout,
|
onLogoutClick = viewModel::logout,
|
||||||
onSearchClick = viewModel::openSearch,
|
onSearchClick = viewModel::openSearch,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -256,6 +256,10 @@ class AppViewModel @Inject constructor(
|
|||||||
navigationManager.navigate(Route.HomeSearchRoute)
|
navigationManager.navigate(Route.HomeSearchRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openSettings() {
|
||||||
|
navigationManager.navigate(Route.SettingsRoute)
|
||||||
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
userSessionRepository.setLoggedIn(false)
|
userSessionRepository.setLoggedIn(false)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@ sealed interface Route : NavKey {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data object LoginRoute : Route
|
data object LoginRoute : Route
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SettingsRoute : Route
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerRoute(val mediaId: String) : Route
|
data class PlayerRoute(val mediaId: String) : Route
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,50 @@ package hu.bbara.purefin.settings
|
|||||||
|
|
||||||
sealed interface SettingOption<T> {
|
sealed interface SettingOption<T> {
|
||||||
val key: String
|
val key: String
|
||||||
|
val title: String
|
||||||
val defaultValue: T
|
val defaultValue: T
|
||||||
}
|
}
|
||||||
|
|
||||||
data class NumberSetting(
|
data class NumberSetting(
|
||||||
override val key: String,
|
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>
|
) : SettingOption<Double>
|
||||||
|
|
||||||
data class BooleanSetting(
|
data class BooleanSetting(
|
||||||
override val key: String,
|
override val key: String,
|
||||||
|
override val title: String,
|
||||||
override val defaultValue: Boolean
|
override val defaultValue: Boolean
|
||||||
) : SettingOption<Boolean>
|
) : SettingOption<Boolean>
|
||||||
|
|
||||||
data class StringSetting(
|
data class StringSetting(
|
||||||
override val key: String,
|
override val key: String,
|
||||||
|
override val title: String,
|
||||||
override val defaultValue: String
|
override val defaultValue: String
|
||||||
) : SettingOption<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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user