diff --git a/app-tv/src/main/java/hu/bbara/purefin/modules/TvNavigationModule.kt b/app-tv/src/main/java/hu/bbara/purefin/modules/TvNavigationModule.kt index 60a62bab..6c2f0a75 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/modules/TvNavigationModule.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/modules/TvNavigationModule.kt @@ -13,6 +13,7 @@ import hu.bbara.purefin.navigation.tvLibrarySection import hu.bbara.purefin.navigation.tvLoginSection import hu.bbara.purefin.navigation.tvMovieSection import hu.bbara.purefin.navigation.tvPlayerSection +import hu.bbara.purefin.navigation.tvSettingsSection import hu.bbara.purefin.navigation.tvSeriesSection @Module @@ -60,4 +61,10 @@ object TvNavigationModule { fun provideTvLibraryEntryBuilder(): EntryProviderScope.() -> Unit = { tvLibrarySection() } + + @IntoSet + @Provides + fun provideTvSettingsEntryBuilder(): EntryProviderScope.() -> Unit = { + tvSettingsSection() + } } diff --git a/app-tv/src/main/java/hu/bbara/purefin/navigation/TvRouteEntryBuilder.kt b/app-tv/src/main/java/hu/bbara/purefin/navigation/TvRouteEntryBuilder.kt index 1e171c9f..3023f224 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/navigation/TvRouteEntryBuilder.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/navigation/TvRouteEntryBuilder.kt @@ -9,6 +9,7 @@ import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.screen.TvAppScreen import hu.bbara.purefin.ui.screen.library.TvLibraryScreen import hu.bbara.purefin.ui.screen.player.TvPlayerScreen +import hu.bbara.purefin.ui.screen.settings.TvSettingsScreen fun EntryProviderScope.tvHomeSection() { entry { @@ -62,3 +63,9 @@ fun EntryProviderScope.tvLibrarySection() { ) } } + +fun EntryProviderScope.tvSettingsSection() { + entry { + TvSettingsScreen() + } +} diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt index d3e27b45..e419f22c 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Movie +import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Tv import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -27,6 +28,7 @@ import hu.bbara.purefin.ui.screen.home.TvHomeScreen import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestinationItem import hu.bbara.purefin.ui.screen.home.components.TvNavigationDrawer import hu.bbara.purefin.ui.screen.library.TvLibraryScreen +import hu.bbara.purefin.ui.screen.settings.TvSettingsScreen @Composable fun TvAppScreen( @@ -62,7 +64,11 @@ fun TvAppScreen( LibraryKind.SERIES -> Icons.Outlined.Tv } ) - } + } + TvDrawerDestinationItem( + destination = Route.SettingsRoute, + label = "Settings", + icon = Icons.Outlined.Settings + ) } LifecycleResumeEffect(Unit) { @@ -88,6 +94,12 @@ fun TvAppScreen( modifier = Modifier.fillMaxSize() ) } + entry { + TvSettingsScreen( + onBack = { backStack.removeLastOrNull() }, + modifier = Modifier.fillMaxSize() + ) + } } TvNavigationDrawer( @@ -97,7 +109,7 @@ fun TvAppScreen( if (selectedDestination != destination) { backStack.clear() backStack.add(Route.Home) - if (destination is Route.LibraryRoute) { + if (destination != Route.Home) { backStack.add(destination) } } 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 new file mode 100644 index 00000000..5322f721 --- /dev/null +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt @@ -0,0 +1,214 @@ +package hu.bbara.purefin.ui.screen.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardCapitalization +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 java.util.Locale + +@Composable +fun TvSettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), + onBack: () -> Unit = viewModel::onBack, + modifier: Modifier = Modifier +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + topBar = { + TvSettingsTopBar(onBack = 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 = 28.dp, vertical = 16.dp) + ) + } + + 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.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() + } + } + } + } +} + +@Composable +private fun TvSettingsTopBar( + onBack: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back" + ) + } + } +} + +@Composable +private fun TvNumberSettingItem( + title: String, + value: Double, + valueRange: ClosedFloatingPointRange, + onValueChange: (Double) -> Unit, + modifier: Modifier = Modifier +) { + var sliderValue by remember(value) { mutableStateOf(value.toFloat()) } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 28.dp, vertical = 16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = String.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.fillMaxWidth() + ) + } +} + +@Composable +private fun TvBooleanSettingItem( + title: String, + value: Boolean, + onValueChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier + .fillMaxWidth() + .clickable { onValueChange(!value) } + .padding(horizontal = 28.dp, vertical = 16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge + ) + Switch( + checked = value, + onCheckedChange = onValueChange + ) + } +} + +@Composable +private fun TvStringSettingItem( + title: String, + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(title) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Words + ), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 28.dp, vertical = 16.dp) + ) +}