diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index 6b26cef..3418724 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -4,10 +4,11 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Scaffold +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -30,7 +31,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.kieronquinn.monetcompat.app.MonetCompatActivity import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.rememberWindowSizeClass @@ -40,9 +40,7 @@ import com.owenlejeune.tvtime.ui.components.SearchFab import com.owenlejeune.tvtime.ui.navigation.BottomNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavGraph import com.owenlejeune.tvtime.ui.navigation.MainNavItem -import com.owenlejeune.tvtime.ui.screens.main.MediaDetailView -import com.owenlejeune.tvtime.ui.screens.main.MediaViewType -import com.owenlejeune.tvtime.ui.screens.main.PersonDetailView +import com.owenlejeune.tvtime.ui.screens.main.* import com.owenlejeune.tvtime.ui.theme.TVTimeTheme import com.owenlejeune.tvtime.utils.KeyboardManager import com.owenlejeune.tvtime.utils.SessionManager @@ -115,6 +113,7 @@ class MainActivity : MonetCompatActivity() { topBar = { if (windowSize != WindowSizeClass.Expanded) { TopBar( + appNavController = appNavController, title = appBarTitle, scrollBehavior = scrollBehavior, appBarActions = appBarActions @@ -151,19 +150,31 @@ class MainActivity : MonetCompatActivity() { @Composable private fun TopBar( + appNavController: NavHostController, title: MutableState, scrollBehavior: TopAppBarScrollBehavior, appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}) ) { + val defaultAppBarActions: @Composable RowScope.() -> Unit = { + IconButton( + onClick = { + appNavController.navigate(MainNavItem.SettingsView.route) + } + ) { + Icon(imageVector = Icons.Filled.Settings, contentDescription = stringResource(id = R.string.nav_settings_title)) + } + } LargeTopAppBar( title = { Text(text = title.value) }, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults .largeTopAppBarColors( - scrolledContainerColor = MaterialTheme.colorScheme.background, - titleContentColor = MaterialTheme.colorScheme.primary + scrolledContainerColor = MaterialTheme.colorScheme.background ), - actions = appBarActions.value + actions = { + defaultAppBarActions() + appBarActions.value(this) + } ) } @@ -316,6 +327,7 @@ class MainActivity : MonetCompatActivity() { } Column { TopBar( + appNavController = appNavController, title = appBarTitle, scrollBehavior = topBarScrollBehaviour, appBarActions = appBarActions @@ -365,12 +377,13 @@ class MainActivity : MonetCompatActivity() { private object NavConstants { const val ID_KEY = "id_key" const val TYPE_KEY = "type_key" + const val SETTINGS_KEY = "settings_key" } @Composable private fun MainNavigationRoutes( startDestination: String = MainNavItem.MainView.route, - mainNavStartRoute: String = BottomNavItem.Items[0].route, + mainNavStartRoute: String = MainNavItem.Items[0].route, appNavController: NavHostController, ) { NavHost(navController = appNavController, startDestination = startDestination) { @@ -399,6 +412,22 @@ class MainActivity : MonetCompatActivity() { ) } } + composable( + MainNavItem.SettingsView.route.plus("/{${NavConstants.SETTINGS_KEY}}"), + arguments = listOf( + navArgument(NavConstants.SETTINGS_KEY) { type = NavType.StringType } + ) + ) { + val route = it.arguments?.getString(NavConstants.SETTINGS_KEY) + SettingsTab( + appNavController = appNavController, + activity = this@MainActivity, + route = route + ) + } + composable(MainNavItem.SettingsView.route) { + SettingsTab(appNavController = appNavController, activity = this@MainActivity) + } } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index a0fa018..567d5b5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -27,6 +27,7 @@ class AppPreferences(context: Context) { private val USE_V4_API = "use_v4_api" private val SHOW_BACKDROP_GALLERY = "show_backdrop_gallery" private val USE_WALLPAPER_COLORS = "use_wallpaper_colors" + private val DARK_THEME = "dark_theme" } private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) @@ -46,6 +47,10 @@ class AppPreferences(context: Context) { /******* Design Preferences ********/ + var darkTheme: Int + get() = preferences.getInt(DARK_THEME, 0) + set(value) { preferences.put(DARK_THEME, value) } + var useSystemColors: Boolean get() = preferences.getBoolean(USE_SYSTEM_COLORS, true) set(value) { preferences.put(USE_SYSTEM_COLORS, value) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt index 09244c4..c9aee84 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt @@ -1,14 +1,18 @@ package com.owenlejeune.tvtime.ui.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.Text +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -92,7 +96,9 @@ fun SliderPreference( var sliderValue by remember { mutableStateOf(value) } Slider( - modifier = Modifier.fillMaxWidth().padding(start = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp), value = sliderValue, onValueChange = { sliderValue = it }, enabled = enabled, @@ -106,4 +112,60 @@ fun SliderPreference( ) ) } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RadioButtonPreference( + modifier: Modifier = Modifier, + enabled: Boolean = true, + selected: Boolean, + onClick: () -> Unit, + title: String, + icon: ImageVector, + titleTextColor: Color = MaterialTheme.colorScheme.onBackground, + disabledTextColor: Color = MaterialTheme.colorScheme.outline +) { + Row( +// modifier = modifier +// .clickable( +// enabled = enabled, +// onClick = onClick +// ) +// .semantics { role = Role.RadioButton } + modifier = modifier + .padding(all = 8.dp) + .selectable( + selected = selected, + onClick = onClick, + role = Role.RadioButton + ) + ) { + Icon( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp), + imageVector = icon, + contentDescription = title, + tint = MaterialTheme.colorScheme.primary + ) + + val titleColor = if (enabled) titleTextColor else disabledTextColor + Text( + modifier = Modifier + .align(Alignment.CenterVertically), + text = title, + style = MaterialTheme.typography.titleLarge, + color = titleColor, + fontSize = 20.sp + ) + + Spacer(modifier = Modifier.weight(1f)) + + RadioButton( + modifier = Modifier.align(Alignment.CenterVertically), + selected = selected, + onClick = null + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 3c5368c..f91ac83 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -21,6 +21,7 @@ import androidx.compose.material.OutlinedTextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -75,6 +76,7 @@ import kotlinx.coroutines.launch import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser +import kotlin.math.exp @Composable fun TopLevelSwitch( @@ -959,4 +961,52 @@ fun CenteredIconCircle( } } } +} + +@Composable +fun Spinner( + modifier: Modifier = Modifier, + list: List>, + preselected: Pair, + onSelectionChanged: (Pair) -> Unit +) { + var selected by remember { mutableStateOf(preselected) } + var expanded by remember { mutableStateOf(false) } + + Box(modifier = modifier) { + Column { + OutlinedTextField( + value = selected.first, + onValueChange = { }, + trailingIcon = { Icon(Icons.Outlined.ArrowDropDown, null) }, + readOnly = true, + modifier = Modifier.clickable( + onClick = { + expanded = true + } + ) + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + list.forEach { entry -> + DropdownMenuItem( + text = { Text(text = entry.first) }, + onClick = { + selected = entry + expanded = false + onSelectionChanged(selected) + } + ) + } + } + } + } +} + +@Composable +@Preview +fun SpinnerPreview() { + Spinner(list = listOf("T" to "T", "Q" to "Q", "F" to "F"), modifier = Modifier.width(300.dp), preselected = "T" to "T", onSelectionChanged = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt index fd8c654..6f7bf94 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt @@ -12,7 +12,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko val name = resourceUtils.getString(stringRes) companion object { - val Items by lazy { listOf(Movies, TV, People, Account, Settings) } + val Items by lazy { listOf(Movies, TV, People, Account) } val SearchableRoutes by lazy { listOf(Movies.route, TV.route, People.route) } fun getByRoute(route: String?): BottomNavItem? { @@ -20,8 +20,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko Movies.route -> Movies TV.route -> TV Account.route -> Account -// Favourites.route -> Favourites - Settings.route -> Settings + Favourites.route -> Favourites else -> null } } @@ -30,8 +29,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route") object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route") object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route") -// object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route") - object Settings: BottomNavItem(R.string.nav_settings_title, R.drawable.ic_settings, "settings_route") + object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route") object People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_route") } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainNavItem.kt index 7dbf8e9..b2a4331 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainNavItem.kt @@ -2,7 +2,12 @@ package com.owenlejeune.tvtime.ui.navigation sealed class MainNavItem(val route: String) { + companion object { + val Items = listOf(MainView, DetailView, SettingsView) + } + object MainView: MainNavItem("main_route") object DetailView: MainNavItem("detail_route") + object SettingsView: MainNavItem("settings_route") } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt index 06e06e1..56524a2 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt @@ -44,13 +44,9 @@ fun MainNavGraph( appBarActions.value = {} PeopleTab(appBarTitle = appBarTitle, appNavController = appNavController) } -// composable(BottomNavItem.Favourites.route) { -// appBarActions.value = {} -// FavouritesTab() -// } - composable(BottomNavItem.Settings.route) { + composable(BottomNavItem.Favourites.route) { appBarActions.value = {} - SettingsTab(appBarTitle = appBarTitle, activity = activity) + FavouritesTab() } } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt index 571484b..cb7205b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -289,7 +290,8 @@ private fun AccountDropdownMenu( IconButton( onClick = { expanded.value = true } ) { - Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null) +// Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null) + Icon(imageVector = Icons.Filled.AccountCircle, contentDescription = stringResource(id = R.string.nav_account_title)) } DropdownMenu( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/SettingsTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/SettingsTab.kt index 046a61b..6ad8a31 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/SettingsTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/SettingsTab.kt @@ -3,6 +3,7 @@ package com.owenlejeune.tvtime.ui.screens.main import android.os.Build import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,9 +13,9 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.ResetTv -import androidx.compose.material.icons.filled.RestartAlt +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.DarkMode +import androidx.compose.material.icons.outlined.LightMode import androidx.compose.material3.* import androidx.compose.material3.AlertDialog import androidx.compose.material3.Divider @@ -22,144 +23,417 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.Icon import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController import com.kieronquinn.monetcompat.core.MonetCompat import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.ui.components.* +import com.owenlejeune.tvtime.ui.navigation.BottomNavItem +import com.owenlejeune.tvtime.ui.navigation.MainNavItem +import com.owenlejeune.tvtime.utils.ResourceUtils import com.owenlejeune.tvtime.utils.SessionManager import dev.kdrag0n.monet.factory.ColorSchemeFactory import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.koin.java.KoinJavaComponent.get +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsTab( - appBarTitle: MutableState, + appNavController: NavController, activity: AppCompatActivity, + route: String? = null, preferences: AppPreferences = get(AppPreferences::class.java) ) { - appBarTitle.value = stringResource(id = R.string.nav_settings_title) + val decayAnimationSpec = rememberSplineBasedDecay() + val topAppBarScrollState = rememberTopAppBarScrollState() + val scrollBehavior = remember(decayAnimationSpec) { + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState) + } - val scrollState = rememberScrollState() + val appBarTitle = remember { mutableStateOf("") } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults + .largeTopAppBarColors( + scrolledContainerColor = MaterialTheme.colorScheme.background + ), + title = { Text(text = appBarTitle.value) }, + navigationIcon = { + IconButton( + onClick = { appNavController.popBackStack() } + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.content_description_back_button) + ) + } + } + ) + } + ) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + if (route != null) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + SettingsPage.getByRoute(route).apply { + appBarTitle.value = name + SettingsPageRenderer(appNavController, activity, preferences) + } + } + } else { + SettingsTabView( + appNavController = appNavController, + appBarTitle = appBarTitle + ) + } + } + } +} + +@Composable +private fun SettingsTabView( + appNavController: NavController, + appBarTitle: MutableState +) { + appBarTitle.value = stringResource(id = R.string.nav_settings_title) Column( modifier = Modifier .fillMaxSize() - .padding(start = 8.dp, top = 8.dp, bottom = 8.dp, end = 24.dp) - .verticalScroll(scrollState) + .padding(all = 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) ) { - /******* Search Preferences ********/ - PreferenceHeading(text = stringResource(R.string.preference_heading_search)) - - val persistentSearch = remember { mutableStateOf(preferences.persistentSearch) } - SwitchPreference( - titleText = stringResource(R.string.preferences_persistent_search_title), - subtitleText = stringResource(R.string.preferences_persistent_search_subtitle), - checkState = persistentSearch.value, - onCheckedChange = { isChecked -> - persistentSearch.value = isChecked - preferences.persistentSearch = isChecked - } + TopLevelSettingsCard( + title = stringResource(id = R.string.preference_heading_search), + subtitle = stringResource(R.string.preference_subtitle_search), + icon = Icons.Filled.Search, + settingsView = SettingsPage.SearchSettings, + appNavController = appNavController ) - val hideTitle = remember { mutableStateOf(preferences.hideTitle) } - SwitchPreference( - titleText = stringResource(R.string.preferences_hide_heading_title), - subtitleText = stringResource(R.string.preferences_hide_heading_subtitle), - checkState = hideTitle.value, - onCheckedChange = { isChecked -> - hideTitle.value = isChecked - preferences.hideTitle = isChecked - }, - enabled = persistentSearch.value + TopLevelSettingsCard( + title = stringResource(id = R.string.preference_heading_design), + subtitle = stringResource(R.string.preference_subtitle_design), + icon = Icons.Filled.Palette, + settingsView = SettingsPage.DesignSettings, + appNavController = appNavController ) - /******* Design Preferences ********/ - PreferenceHeading(text = stringResource(id = R.string.preference_heading_design)) - - val useWallpaperColors = remember { mutableStateOf(preferences.useWallpaperColors) } - SwitchPreference( - titleText = stringResource(R.string.preferences_use_wallpaper_colors_title), - subtitleText = stringResource(R.string.preferences_use_wallpaper_colors_subtitle), - checkState = useWallpaperColors.value, - onCheckedChange = { isChecked -> - useWallpaperColors.value = isChecked - preferences.useWallpaperColors = isChecked - activity.recreate() - } + TopLevelSettingsCard( + title = stringResource(id = R.string.preferences_debug_title), + subtitle = stringResource(R.string.preference_subtitle_debug), + icon = Icons.Filled.DeveloperBoard, + settingsView = SettingsPage.DeveloperSettings, + appNavController = appNavController ) + } +} - val isSystemColorsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - if (isSystemColorsSupported) { - val useSystemColors = remember { mutableStateOf(preferences.useSystemColors) } - SwitchPreference( - titleText = stringResource(id = R.string.preference_system_colors_heading), - subtitleText = stringResource(R.string.preference_system_colors_subtitle), - checkState = useSystemColors.value, - onCheckedChange = { isChecked -> - useSystemColors.value = isChecked - preferences.useSystemColors = isChecked - MonetCompat.useSystemColorsOnAndroid12 = isChecked - MonetCompat.getInstance().updateMonetColors() - }, - enabled = useWallpaperColors.value - ) - } - - SliderPreference( - titleText = stringResource(id = R.string.preference_chroma_factor_heading), - value = preferences.chromaMultiplier.toFloat() * 50f, - onValueChangeFinished = { value -> - with((value / 50f).toDouble()) { - preferences.chromaMultiplier = this - MonetCompat.chromaMultiplier = this +@Composable +private fun TopLevelSettingsCard( + title: String, + subtitle: String? = null, + icon: ImageVector, + settingsView: SettingsPage, + appNavController: NavController +) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .clickable( + onClick = { + appNavController.navigate("${MainNavItem.SettingsView.route}/${settingsView.route}") } + ) + ) { + Icon( + imageVector = icon, + contentDescription = subtitle, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp), + tint = MaterialTheme.colorScheme.primary + ) + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .weight(1f) + ) { + val titleColor = MaterialTheme.colorScheme.onBackground + val subtitleColor = MaterialTheme.colorScheme.onSurfaceVariant + Text(text = title, style = MaterialTheme.typography.titleLarge, color = titleColor, fontSize = 20.sp) + subtitle?.let { + Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = subtitleColor) + } + } + } +} + +@Composable +private fun SearchPreferences( + preferences: AppPreferences = get(AppPreferences::class.java) +) { + val persistentSearch = remember { mutableStateOf(preferences.persistentSearch) } + SwitchPreference( + titleText = stringResource(R.string.preferences_persistent_search_title), + subtitleText = stringResource(R.string.preferences_persistent_search_subtitle), + checkState = persistentSearch.value, + onCheckedChange = { isChecked -> + persistentSearch.value = isChecked + preferences.persistentSearch = isChecked + } + ) + + val hideTitle = remember { mutableStateOf(preferences.hideTitle) } + SwitchPreference( + titleText = stringResource(R.string.preferences_hide_heading_title), + subtitleText = stringResource(R.string.preferences_hide_heading_subtitle), + checkState = hideTitle.value, + onCheckedChange = { isChecked -> + hideTitle.value = isChecked + preferences.hideTitle = isChecked + }, + enabled = persistentSearch.value + ) +} + +@Composable +private fun DesignPreferences( + appNavController: NavController, + activity: AppCompatActivity, + preferences: AppPreferences = get(AppPreferences::class.java) +) { + TopLevelSettingsCard( + title = stringResource(id = R.string.preference_heading_dark_mode), + subtitle = stringResource(R.string.preference_subtitle_dark_mode), + icon = Icons.Outlined.DarkMode, + settingsView = SettingsPage.DarkModeSettings, + appNavController = appNavController + ) + + val useWallpaperColors = remember { mutableStateOf(preferences.useWallpaperColors) } + SwitchPreference( + titleText = stringResource(R.string.preferences_use_wallpaper_colors_title), + subtitleText = stringResource(R.string.preferences_use_wallpaper_colors_subtitle), + checkState = useWallpaperColors.value, + onCheckedChange = { isChecked -> + useWallpaperColors.value = isChecked + preferences.useWallpaperColors = isChecked + activity.recreate() + } + ) + + val isSystemColorsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + if (isSystemColorsSupported) { + val useSystemColors = remember { mutableStateOf(preferences.useSystemColors) } + SwitchPreference( + titleText = stringResource(id = R.string.preference_system_colors_heading), + subtitleText = stringResource(R.string.preference_system_colors_subtitle), + checkState = useSystemColors.value, + onCheckedChange = { isChecked -> + useSystemColors.value = isChecked + preferences.useSystemColors = isChecked + MonetCompat.useSystemColorsOnAndroid12 = isChecked MonetCompat.getInstance().updateMonetColors() }, - enabled = (if (isSystemColorsSupported) !preferences.useSystemColors else true).and(useWallpaperColors.value) + enabled = useWallpaperColors.value ) + } - val showWallpaperPicker = remember { mutableStateOf(false) } - val wallpaperPickerModifier = if (isSystemColorsSupported && preferences.useSystemColors && useWallpaperColors.value) { + SliderPreference( + titleText = stringResource(id = R.string.preference_chroma_factor_heading), + value = preferences.chromaMultiplier.toFloat() * 50f, + onValueChangeFinished = { value -> + with((value / 50f).toDouble()) { + preferences.chromaMultiplier = this + MonetCompat.chromaMultiplier = this + } + MonetCompat.getInstance().updateMonetColors() + }, + enabled = (if (isSystemColorsSupported) !preferences.useSystemColors else true).and( + useWallpaperColors.value + ) + ) + + val showWallpaperPicker = remember { mutableStateOf(false) } + val wallpaperPickerModifier = + if (isSystemColorsSupported && preferences.useSystemColors && useWallpaperColors.value) { Modifier } else { Modifier.clickable { showWallpaperPicker.value = true } } - Text( - text = stringResource(id = R.string.preference_wallpaper_color_heading), - style = MaterialTheme.typography.titleLarge, - fontSize = 20.sp, - color = if (isSystemColorsSupported && preferences.useSystemColors) { - MaterialTheme.colorScheme.outline - } else { - MaterialTheme.colorScheme.onBackground - }, - modifier = wallpaperPickerModifier - .padding(start = 8.dp) + Text( + text = stringResource(id = R.string.preference_wallpaper_color_heading), + style = MaterialTheme.typography.titleLarge, + fontSize = 20.sp, + color = if (isSystemColorsSupported && preferences.useSystemColors) { + MaterialTheme.colorScheme.outline + } else { + MaterialTheme.colorScheme.onBackground + }, + modifier = wallpaperPickerModifier + .padding(start = 8.dp) + ) + if (showWallpaperPicker.value) { + WallpaperPicker(showPopup = showWallpaperPicker) + } +} + +enum class DarkMode { + Automatic, + Dark, + Light +} + +@Composable +private fun DarkModePreferences( + activity: AppCompatActivity, + preferences: AppPreferences = get(AppPreferences::class.java) +) { + val selectedValue = remember { mutableStateOf(preferences.darkTheme) } + + val isSelected: (Int) -> Boolean = { selectedValue.value == it } + val onChangeState: (Int) -> Unit = { + selectedValue.value = it + preferences.darkTheme = it + activity.recreate() + } + + PreferenceHeading(text = "Automatic") + RadioButtonPreference( + selected = isSelected(DarkMode.Automatic.ordinal), + title = "Follow system", + icon = Icons.Filled.Brightness6, + onClick = { + onChangeState(DarkMode.Automatic.ordinal) + } + ) + + PreferenceHeading(text = "Manual") + RadioButtonPreference( + selected = isSelected(DarkMode.Light.ordinal), + title = "Light mode", + icon = Icons.Outlined.LightMode, + onClick = { + onChangeState(DarkMode.Light.ordinal) + } + ) + RadioButtonPreference( + selected = isSelected(DarkMode.Dark.ordinal), + title = "Dark mode", + icon = Icons.Outlined.DarkMode, + onClick = { + onChangeState(DarkMode.Dark.ordinal) + } + ) +} + +@Composable +private fun DevPreferences( + preferences: AppPreferences = get(AppPreferences::class.java) +) { + if (BuildConfig.DEBUG) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val firstLaunchTesting = remember { mutableStateOf(preferences.firstLaunchTesting) } + SwitchPreference( + titleText = "Always show onboarding flow", + subtitleText = "Show onboarding flow on each launch", + checkState = firstLaunchTesting.value, + onCheckedChange = { isChecked -> + firstLaunchTesting.value = isChecked + preferences.firstLaunchTesting = isChecked + } ) - if (showWallpaperPicker.value) { - WallpaperPicker(showPopup = showWallpaperPicker) + + val shouldShowPalette = remember { mutableStateOf(false) } + Text( + text = "Show material palette", + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 12.dp) + .clickable( + onClick = { + shouldShowPalette.value = true + } + ) + ) + if (shouldShowPalette.value) { + AlertDialog( + modifier = Modifier.padding(12.dp), + title = { Text(text = "Palette") }, + text = { PaletteView() }, + dismissButton = { + TextButton(onClick = { shouldShowPalette.value = false }) { + Text("Dismiss") + } + }, + confirmButton = {}, + onDismissRequest = { shouldShowPalette.value = false } + ) } - /******* Dev Preferences ********/ - if (BuildConfig.DEBUG) { - Divider(modifier = Modifier.padding(start = 8.dp, top = 20.dp), color = MaterialTheme.colorScheme.primaryContainer) - Text( - modifier = Modifier.padding(start = 8.dp, top = 20.dp), - text = stringResource(R.string.preferences_debug_title), - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary - ) - DebugOptions() - } + Text( + text = "Clear session", + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 12.dp) + .clickable( + onClick = { + preferences.guestSessionId = "" + coroutineScope.launch { + SessionManager.clearSession { + Toast + .makeText(context, "Cleared session: $it", Toast.LENGTH_SHORT) + .show() + } + SessionManager.clearSessionV4 { + Toast + .makeText( + context, + "Cleared session v4: $it", + Toast.LENGTH_SHORT + ) + .show() + } + } + } + ) + ) + + val useV4Api = remember { mutableStateOf(preferences.useV4Api) } + SwitchPreference( + titleText = "Use v4 API", + checkState = useV4Api.value, + onCheckedChange = { isChecked -> + useV4Api.value = isChecked + preferences.useV4Api = isChecked + } + ) } } @@ -242,80 +516,24 @@ private fun WallpaperPicker( } } -@Composable -private fun DebugOptions(preferences: AppPreferences = get(AppPreferences::class.java)) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() +private sealed class SettingsPage(stringRes: Int, val route: String, val SettingsPageRenderer: @Composable (NavController, AppCompatActivity, AppPreferences) -> Unit): KoinComponent { + private val resources: ResourceUtils by inject() - val firstLaunchTesting = remember { mutableStateOf(preferences.firstLaunchTesting) } - SwitchPreference( - titleText = "Always show onboarding flow", - subtitleText = "Show onboarding flow on each launch", - checkState = firstLaunchTesting.value, - onCheckedChange = { isChecked -> - firstLaunchTesting.value = isChecked - preferences.firstLaunchTesting = isChecked + val name = resources.getString(stringRes) + + companion object { + val Pages by lazy {listOf(SearchSettings, DesignSettings, DeveloperSettings, DarkModeSettings) } + + fun getByRoute(route: String): SettingsPage { + return Pages.map { it.route to it } + .find { it.first == route } + ?.second + ?: throw IllegalArgumentException("Invalid settings page route: $route") } - ) - - val shouldShowPalette = remember { mutableStateOf(false) } - Text( - text = "Show material palette", - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 12.dp) - .clickable( - onClick = { - shouldShowPalette.value = true - } - ) - ) - if (shouldShowPalette.value) { - AlertDialog( - modifier = Modifier.padding(12.dp), - title = { Text(text = "Palette") }, - text = { PaletteView() }, - dismissButton = { - TextButton(onClick = { shouldShowPalette.value = false }) { - Text("Dismiss") - } - }, - confirmButton = {}, - onDismissRequest = { shouldShowPalette.value = false } - ) } - Text( - text = "Clear session", - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 12.dp) - .clickable( - onClick = { - preferences.guestSessionId = "" - coroutineScope.launch { - SessionManager.clearSession { - Toast - .makeText(context, "Cleared session: $it", Toast.LENGTH_SHORT) - .show() - } - SessionManager.clearSessionV4 { - Toast - .makeText(context, "Cleared session v4: $it", Toast.LENGTH_SHORT) - .show() - } - } - } - ) - ) - - val useV4Api = remember { mutableStateOf(preferences.useV4Api) } - SwitchPreference( - titleText = "Use v4 API", - checkState = useV4Api.value, - onCheckedChange = { isChecked -> - useV4Api.value = isChecked - preferences.useV4Api = isChecked - } - ) + object SearchSettings: SettingsPage(R.string.preference_heading_search, "search", @Composable { n, a, p -> SearchPreferences(p) } ) + object DesignSettings: SettingsPage(R.string.preference_heading_design, "design", @Composable { n, a, p -> DesignPreferences(n, a, p) } ) + object DeveloperSettings: SettingsPage(R.string.preferences_debug_title,"dev", @Composable { n, a, p -> DevPreferences(p) } ) + object DarkModeSettings: SettingsPage(R.string.preference_heading_dark_mode, "darkmode", @Composable { n, a, p -> DarkModePreferences(a, p) } ) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/theme/Theme.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/theme/Theme.kt index 3a7e150..09f220e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/theme/Theme.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/theme/Theme.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.kieronquinn.monetcompat.core.MonetCompat import com.owenlejeune.tvtime.preferences.AppPreferences +import com.owenlejeune.tvtime.ui.screens.main.DarkMode import org.koin.java.KoinJavaComponent.get private val DarkColorPalette = darkColorScheme( @@ -69,46 +70,19 @@ private val LightColorPalette = lightColorScheme( inverseOnSurface = N1_50 ) -//@Composable -//fun TVTimeTheme( -// isDarkTheme: Boolean = isSystemInDarkTheme(), -// isDynamicColor: Boolean = true, -// content: @Composable () -> Unit -//) { -// val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -// val colorScheme = when { -// dynamicColor && isDarkTheme -> { -// dynamicDarkColorScheme(LocalContext.current) -// } -// dynamicColor && !isDarkTheme -> { -// dynamicLightColorScheme(LocalContext.current) -// } -// isDarkTheme -> DarkColorPalette -// else -> LightColorPalette -// } -// -// val systemUiController = rememberSystemUiController() -// systemUiController.setSystemBarsColor(colorScheme.background, !isDarkTheme) -// -// MaterialTheme( -// colorScheme = colorScheme, -// typography = Typography, -// content = content -// ) -//} - @Composable fun TVTimeTheme( preferences: AppPreferences = get(AppPreferences::class.java), - isDarkTheme: Boolean = isSystemInDarkTheme(), monetCompat: MonetCompat, content: @Composable () -> Unit ) { -// val colors = if(isDarkTheme) { -// monetCompat.darkMonetCompatScheme() -// } else { -// monetCompat.lightMonetCompatScheme() -// } + val isDarkTheme = when(preferences.darkTheme) { + DarkMode.Automatic.ordinal -> isSystemInDarkTheme() + DarkMode.Dark.ordinal -> true + DarkMode.Light.ordinal -> false + else -> throw IllegalArgumentException("Illegal theme value ${preferences.darkTheme}") + } + val colors = when { isDarkTheme && preferences.useWallpaperColors -> monetCompat.darkMonetCompatScheme() isDarkTheme && !preferences.useWallpaperColors -> DarkColorPalette diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b47ff11..0748df2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -117,4 +117,9 @@ Use wallpaper colors Use theme colors pulled from your device\'s wallpaper Use wallpaper colors chosen by your device + Search bar view settings + Design and theming options + Secret developer options + Dark mode + Light, dark, or auto \ No newline at end of file