add option to reorder home tabs

This commit is contained in:
Owen LeJeune
2022-09-09 15:51:44 -04:00
parent dd0a395bf9
commit 266ade86a1
12 changed files with 492 additions and 106 deletions

View File

@@ -58,7 +58,7 @@ class MainActivity : MonetCompatActivity() {
SessionManager.initialize() SessionManager.initialize()
} }
var mainNavStartRoute = BottomNavItem.Items[0].route var mainNavStartRoute = BottomNavItem.SortedItems[0].route
intent.data?.let { intent.data?.let {
when (it.host) { when (it.host) {
getString(R.string.intent_route_auth_return) -> mainNavStartRoute = BottomNavItem.Account.route getString(R.string.intent_route_auth_return) -> mainNavStartRoute = BottomNavItem.Account.route
@@ -82,7 +82,7 @@ class MainActivity : MonetCompatActivity() {
@Composable @Composable
private fun AppScaffold( private fun AppScaffold(
appNavController: NavHostController, appNavController: NavHostController,
mainNavStartRoute: String = BottomNavItem.Items[0].route, mainNavStartRoute: String = BottomNavItem.SortedItems[0].route,
preferences: AppPreferences = get(AppPreferences::class.java) preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
val windowSize = rememberWindowSizeClass() val windowSize = rememberWindowSizeClass()
@@ -91,7 +91,7 @@ class MainActivity : MonetCompatActivity() {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
val appBarTitle = rememberSaveable { mutableStateOf(BottomNavItem.getByRoute(currentRoute)?.name ?: BottomNavItem.Items[0].name) } val appBarTitle = rememberSaveable { mutableStateOf(BottomNavItem.getByRoute(currentRoute)?.name ?: BottomNavItem.SortedItems[0].name) }
val decayAnimationSpec = rememberSplineBasedDecay<Float>() val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState() val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) { val scrollBehavior = remember(decayAnimationSpec) {
@@ -101,8 +101,6 @@ class MainActivity : MonetCompatActivity() {
val appBarActions = remember { mutableStateOf<@Composable RowScope.() -> Unit>({}) } val appBarActions = remember { mutableStateOf<@Composable RowScope.() -> Unit>({}) }
val fab = remember { mutableStateOf<@Composable () -> Unit>({}) } val fab = remember { mutableStateOf<@Composable () -> Unit>({}) }
// todo - scroll state not remember when returing from detail screen
Scaffold ( Scaffold (
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
@@ -170,18 +168,25 @@ class MainActivity : MonetCompatActivity() {
} }
@Composable @Composable
private fun BottomNavBar(navController: NavController, appBarTitle: MutableState<String>) { private fun BottomNavBar(
navController: NavController,
appBarTitle: MutableState<String>,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
NavigationBar { NavigationBar {
BottomNavItem.Items.forEach { item -> BottomNavItem.SortedItems.forEach { item ->
NavigationBarItem( NavigationBarItem(
modifier = Modifier modifier = Modifier
.padding(4.dp) .padding(4.dp)
.clip(RoundedCornerShape(24.dp)), .clip(RoundedCornerShape(24.dp)),
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) }, icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
label = { Text(item.name) }, label = {
val name = if (preferences.showBottomTabLabels) item.name else " "
Text(text = name)
},
selected = currentRoute == item.route, selected = currentRoute == item.route,
onClick = { onClick = {
onBottomAppBarItemClicked( onBottomAppBarItemClicked(
@@ -225,7 +230,7 @@ class MainActivity : MonetCompatActivity() {
topBarScrollBehaviour: TopAppBarScrollBehavior, topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) { ) {
if (windowSize == WindowSizeClass.Expanded) { if (windowSize == WindowSizeClass.Expanded) {
DualColumnMainContent( DualColumnMainContent(
@@ -256,7 +261,7 @@ class MainActivity : MonetCompatActivity() {
fab: MutableState<@Composable () -> Unit>, fab: MutableState<@Composable () -> Unit>,
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) { ) {
MainMediaView( MainMediaView(
appNavController = appNavController, appNavController = appNavController,
@@ -276,7 +281,8 @@ class MainActivity : MonetCompatActivity() {
topBarScrollBehaviour: TopAppBarScrollBehavior, topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route mainNavStartRoute: String = BottomNavItem.SortedItems[0].route,
preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
@@ -284,10 +290,10 @@ class MainActivity : MonetCompatActivity() {
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
NavigationRail { NavigationRail {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
BottomNavItem.Items.forEach { item -> BottomNavItem.SortedItems.forEach { item ->
NavigationRailItem( NavigationRailItem(
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) }, icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
label = { Text(item.name) }, label = { if (preferences.showBottomTabLabels) Text(item.name) },
selected = currentRoute == item.route, selected = currentRoute == item.route,
onClick = { onClick = {
onBottomAppBarItemClicked( onBottomAppBarItemClicked(
@@ -326,7 +332,7 @@ class MainActivity : MonetCompatActivity() {
fab: MutableState<@Composable () -> Unit>, fab: MutableState<@Composable () -> Unit>,
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
appBarActions: MutableState<RowScope.() -> Unit> = mutableStateOf({}), appBarActions: MutableState<RowScope.() -> Unit> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) { ) {
Column { Column {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
@@ -413,13 +419,6 @@ class MainActivity : MonetCompatActivity() {
) )
) { ) {
it.arguments?.let { arguments -> it.arguments?.let { arguments ->
// val title = arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: ""
// val type = if (preferences.multiSearch) {
// MediaViewType.MIXED
// } else {
// MediaViewType[arguments.getInt(NavConstants.SEARCH_ID_KEY)]
// }
val (type, title) = if (preferences.multiSearch) { val (type, title) = if (preferences.multiSearch) {
Pair(MediaViewType.MIXED, "") Pair(MediaViewType.MIXED, "")
} else { } else {

View File

@@ -36,6 +36,16 @@ fun <T: Any> LazyListScope.listItems(
} }
} }
fun <T: Any?> LazyListScope.listItems(
items: List<T?>,
key: (T?) -> Any,
itemContent: @Composable (value: T?) -> Unit
) {
items(items.size, key = { key(items[it]) }) { index ->
itemContent(items[index])
}
}
fun <T: Any> LazyListScope.lazyPagingItems( fun <T: Any> LazyListScope.lazyPagingItems(
lazyPagingItems: LazyPagingItems<T>, lazyPagingItems: LazyPagingItems<T>,
itemContent: @Composable LazyItemScope.(value: T?) -> Unit itemContent: @Composable LazyItemScope.(value: T?) -> Unit

View File

@@ -9,10 +9,15 @@ import com.owenlejeune.tvtime.utils.SessionManager
class AppPreferences(context: Context) { class AppPreferences(context: Context) {
enum class DarkMode {
Automatic,
Dark,
Light
}
companion object { companion object {
private val PREF_FILE = "tvtime_shared_preferences" private val PREF_FILE = "tvtime_shared_preferences"
// private val USE_PREFERENCES = "use_android_12_colors"
private val PERSISTENT_SEARCH = "persistent_search" private val PERSISTENT_SEARCH = "persistent_search"
private val GUEST_SESSION = "guest_session_id" private val GUEST_SESSION = "guest_session_id"
private val AUTHORIZED_SESSION = "authorized_session_id" private val AUTHORIZED_SESSION = "authorized_session_id"
@@ -27,38 +32,50 @@ class AppPreferences(context: Context) {
private val USE_WALLPAPER_COLORS = "use_wallpaper_colors" private val USE_WALLPAPER_COLORS = "use_wallpaper_colors"
private val DARK_THEME = "dark_theme" private val DARK_THEME = "dark_theme"
private val MULTI_SEARCH = "multi_search" private val MULTI_SEARCH = "multi_search"
private val MOVIES_TAB_POSITION = "movies_tab_position"
private val TV_TAB_POSITION = "tv_tab_position"
private val PEOPLE_TAB_POSITION = "people_tab_position"
private val ACCOUNT_TAB_POSITION = "account_tab_position"
private val SHOW_BTAB_LABELS = "show_btab_labels"
} }
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
/******** Search Preferences ********/ /******** Search Preferences ********/
val showSearchBarDefault: Boolean = false
var showSearchBar: Boolean var showSearchBar: Boolean
get() = preferences.getBoolean(PERSISTENT_SEARCH, true) get() = preferences.getBoolean(PERSISTENT_SEARCH, showSearchBarDefault)
set(value) { preferences.put(PERSISTENT_SEARCH, value) } set(value) { preferences.put(PERSISTENT_SEARCH, value) }
val multiSearchDefault: Boolean = true
var multiSearch: Boolean var multiSearch: Boolean
get() = preferences.getBoolean(MULTI_SEARCH, false) get() = preferences.getBoolean(MULTI_SEARCH, multiSearchDefault)
set(value) { preferences.put(MULTI_SEARCH, value) } set(value) { preferences.put(MULTI_SEARCH, value) }
/******* Design Preferences ********/ /******* Design Preferences ********/
val useWallpaperColorsDefault: Boolean = true
var useWallpaperColors: Boolean var useWallpaperColors: Boolean
get() = preferences.getBoolean(USE_WALLPAPER_COLORS, true) get() = preferences.getBoolean(USE_WALLPAPER_COLORS, useWallpaperColorsDefault)
set(value) { preferences.put(USE_WALLPAPER_COLORS, value) } set(value) { preferences.put(USE_WALLPAPER_COLORS, value) }
val darkThemeDefault: Int = DarkMode.Automatic.ordinal
var darkTheme: Int var darkTheme: Int
get() = preferences.getInt(DARK_THEME, 0) get() = preferences.getInt(DARK_THEME, darkThemeDefault)
set(value) { preferences.put(DARK_THEME, value) } set(value) { preferences.put(DARK_THEME, value) }
val useSystemColorsDefault: Boolean = true
var useSystemColors: Boolean var useSystemColors: Boolean
get() = preferences.getBoolean(USE_SYSTEM_COLORS, true) get() = preferences.getBoolean(USE_SYSTEM_COLORS, useSystemColorsDefault)
set(value) { preferences.put(USE_SYSTEM_COLORS, value) } set(value) { preferences.put(USE_SYSTEM_COLORS, value) }
val chromeMultiplyerDefault: Double = MonetCompat.chromaMultiplier.toFloat().toDouble()
var chromaMultiplier: Double var chromaMultiplier: Double
get() = preferences.getFloat(CHROMA_MULTIPLIER, MonetCompat.chromaMultiplier.toFloat()).toDouble() get() = preferences.getFloat(CHROMA_MULTIPLIER, chromeMultiplyerDefault.toFloat()).toDouble()
set(value) { preferences.put(CHROMA_MULTIPLIER, value) } set(value) { preferences.put(CHROMA_MULTIPLIER, value) }
val selectedColorDefault: Int = Int.MAX_VALUE
var selectedColor: Int var selectedColor: Int
get() = preferences.getInt(SELECTED_COLOR, Int.MAX_VALUE) get() = preferences.getInt(SELECTED_COLOR, selectedColorDefault)
set(value) { preferences.put(SELECTED_COLOR, value) } set(value) { preferences.put(SELECTED_COLOR, value) }
/******* Session Tokens ********/ /******* Session Tokens ********/
@@ -76,21 +93,50 @@ class AppPreferences(context: Context) {
get() = preferences.getString(AUTHORIZED_SESSION, "") ?: "" get() = preferences.getString(AUTHORIZED_SESSION, "") ?: ""
set(value) { preferences.put(AUTHORIZED_SESSION, value) } set(value) { preferences.put(AUTHORIZED_SESSION, value) }
/******** Home Screen Preferences ********/
val moviesTabPositionDefault: Int = 0
var moviesTabPosition: Int
get() = preferences.getInt(MOVIES_TAB_POSITION, moviesTabPositionDefault)
set(value) { preferences.put(MOVIES_TAB_POSITION, value) }
val tvTabPositionDefault: Int = 1
var tvTabPosition: Int
get() = preferences.getInt(TV_TAB_POSITION, tvTabPositionDefault)
set(value) { preferences.put(TV_TAB_POSITION, value) }
val peopleTabPositionDefault: Int = 2
var peopleTabPosition: Int
get() = preferences.getInt(PEOPLE_TAB_POSITION, peopleTabPositionDefault)
set(value) { preferences.put(PEOPLE_TAB_POSITION, value) }
val accountTabPositionDefault: Int = 3
var accountTabPosition: Int
get() = preferences.getInt(ACCOUNT_TAB_POSITION, accountTabPositionDefault)
set(value) { preferences.put(ACCOUNT_TAB_POSITION, value) }
val showBottomTabLabelsDefault: Boolean = true
var showBottomTabLabels: Boolean
get() = preferences.getBoolean(SHOW_BTAB_LABELS, showBottomTabLabelsDefault)
set(value) { preferences.put(SHOW_BTAB_LABELS, value) }
/******** Dev Preferences ********/ /******** Dev Preferences ********/
val firstLaunchTestingDefault: Boolean = false
var firstLaunchTesting: Boolean var firstLaunchTesting: Boolean
get() = preferences.getBoolean(FIRST_LAUNCH_TESTING, false) get() = preferences.getBoolean(FIRST_LAUNCH_TESTING, firstLaunchTestingDefault)
set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) } set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) }
var firstLaunch: Boolean var firstLaunch: Boolean
get() = if (BuildConfig.DEBUG) firstLaunchTesting else preferences.getBoolean(FIRST_LAUNCH, true) get() = if (BuildConfig.DEBUG) firstLaunchTesting else preferences.getBoolean(FIRST_LAUNCH, true)
set(value) { preferences.put(FIRST_LAUNCH, value) } set(value) { preferences.put(FIRST_LAUNCH, value) }
val useV4ApiDefault: Boolean = false
var useV4Api: Boolean var useV4Api: Boolean
get() = preferences.getBoolean(USE_V4_API, true) get() = preferences.getBoolean(USE_V4_API, useV4ApiDefault)
set(value) { preferences.put(USE_V4_API, value) } set(value) { preferences.put(USE_V4_API, value) }
var showBackdropGallery: Boolean// = true val showBackdropGalleryDefault: Boolean = true
get() = preferences.getBoolean(SHOW_BACKDROP_GALLERY, true) var showBackdropGallery: Boolean
get() = preferences.getBoolean(SHOW_BACKDROP_GALLERY, showBackdropGalleryDefault)
set(value) { preferences.put(SHOW_BACKDROP_GALLERY, value) } set(value) { preferences.put(SHOW_BACKDROP_GALLERY, value) }
/********* Helpers ********/ /********* Helpers ********/

View File

@@ -1,35 +1,48 @@
package com.owenlejeune.tvtime.ui.navigation package com.owenlejeune.tvtime.ui.navigation
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.utils.ResourceUtils import com.owenlejeune.tvtime.utils.ResourceUtils
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): KoinComponent { sealed class BottomNavItem(
stringRes: Int,
val icon: Int,
val route: String,
private val orderGetter: (AppPreferences) -> Int,
private val orderSetter: (AppPreferences, Int) -> Unit
): KoinComponent {
private val appPreferences: AppPreferences by inject()
private val resourceUtils: ResourceUtils by inject() private val resourceUtils: ResourceUtils by inject()
val name = resourceUtils.getString(stringRes) val name = resourceUtils.getString(stringRes)
var order: Int
get() = orderGetter.invoke(appPreferences)
set(value) { orderSetter.invoke(appPreferences, value) }
companion object { companion object {
val Items by lazy { listOf(Movies, TV, People, Account) } val SortedItems
val SearchableRoutes by lazy { listOf(Movies.route, TV.route, People.route) } get() = Items.filter { it.order > -1 }.sortedBy { it.order }.ifEmpty { Items }
val Items by lazy {
listOf(Movies, TV, People, Account)
}
fun getByRoute(route: String?): BottomNavItem? { fun getByRoute(route: String?): BottomNavItem? {
return when (route) { return when (route) {
Movies.route -> Movies Movies.route -> Movies
TV.route -> TV TV.route -> TV
Account.route -> Account Account.route -> Account
Favourites.route -> Favourites
else -> null else -> null
} }
} }
} }
object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route") object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route", { it.moviesTabPosition }, { p, i -> p.moviesTabPosition = i } )
object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route") object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route", { it.tvTabPosition }, { p, i -> p.tvTabPosition = i } )
object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route") object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route", { it.accountTabPosition }, { p, i -> p.accountTabPosition = i } )
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", { it.peopleTabPosition }, { p, i -> p.peopleTabPosition = i } )
object People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_route")
} }

View File

@@ -6,11 +6,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.owenlejeune.tvtime.ui.screens.main.MediaDetailView
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.* import com.owenlejeune.tvtime.ui.screens.main.*
@@ -27,7 +24,7 @@ fun MainNavGraph(
fab: MutableState<@Composable () -> Unit>, fab: MutableState<@Composable () -> Unit>,
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
startDestination: String = BottomNavItem.Items[0].route startDestination: String = BottomNavItem.SortedItems[0].route
) { ) {
NavHost(navController = navController, startDestination = startDestination) { NavHost(navController = navController, startDestination = startDestination) {
composable(BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) {
@@ -59,9 +56,5 @@ fun MainNavGraph(
fab = fab fab = fab
) )
} }
composable(BottomNavItem.Favourites.route) {
appBarActions.value = {}
FavouritesTab()
}
} }
} }

View File

@@ -328,7 +328,7 @@ private fun TvSearchResultView(
backdropModel = { TmdbUtils.getFullBackdropPath(result.backdropPath) }, backdropModel = { TmdbUtils.getFullBackdropPath(result.backdropPath) },
additionalDetails = { additionalDetails = {
listOf( listOf(
"${TmdbUtils.releaseYearFromData(result.releaseDate)} ${context.getString(R.string.search_result_tv_services)}", "${TmdbUtils.releaseYearFromData(result.releaseDate)} ${context.getString(R.string.search_result_tv_series)}",
cast.value?.joinToString(separator = ", ") { it.name } ?: "" cast.value?.joinToString(separator = ", ") { it.name } ?: ""
) )
} }

View File

@@ -1,24 +0,0 @@
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun FavouritesTab() {
Column(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
) {
Text(
text = "Favourites Tab",
color = MaterialTheme.colorScheme.onBackground
)
}
}

View File

@@ -4,8 +4,10 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -23,13 +25,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kieronquinn.monetcompat.core.MonetCompat import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.views.HomeTabRecyclerAdapter
import com.owenlejeune.tvtime.ui.views.ItemMoveCallback
import com.owenlejeune.tvtime.utils.ResourceUtils import com.owenlejeune.tvtime.utils.ResourceUtils
import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.SessionManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -52,6 +60,8 @@ fun SettingsTab(
} }
val appBarTitle = remember { mutableStateOf("") } val appBarTitle = remember { mutableStateOf("") }
val defaultRestoreAction = ::resetAllPreferences
val restoreAction = remember { mutableStateOf<(AppPreferences) -> Unit>(defaultRestoreAction) }
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -72,6 +82,16 @@ fun SettingsTab(
contentDescription = stringResource(id = R.string.content_description_back_button) contentDescription = stringResource(id = R.string.content_description_back_button)
) )
} }
},
actions = {
IconButton(onClick = {
restoreAction.value(preferences)
}) {
Icon(
imageVector = Icons.Filled.SettingsBackupRestore,
contentDescription = stringResource(R.string.preferences_restore_content_description)
)
}
} }
) )
} }
@@ -82,14 +102,16 @@ fun SettingsTab(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(all = 24.dp), .padding(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp) verticalArrangement = Arrangement.spacedBy(18.dp)
) { ) {
SettingsPage.getByRoute(route).apply { SettingsPage.getByRoute(route).apply {
appBarTitle.value = name appBarTitle.value = name
restoreAction.value = resetPreferencesHandler
SettingsPageRenderer(appNavController, activity, preferences) SettingsPageRenderer(appNavController, activity, preferences)
} }
} }
} else { } else {
restoreAction.value = ::resetAllPreferences
SettingsTabView( SettingsTabView(
appNavController = appNavController, appNavController = appNavController,
appBarTitle = appBarTitle appBarTitle = appBarTitle
@@ -128,6 +150,14 @@ private fun SettingsTabView(
appNavController = appNavController appNavController = appNavController
) )
TopLevelSettingsCard(
title = stringResource(id = R.string.preference_heading_home_screen),
subtitle = stringResource(R.string.preference_subtitle_home_screen),
icon = Icons.Filled.Home,
settingsView = SettingsPage.HomeScreenSettings,
appNavController = appNavController
)
TopLevelSettingsCard( TopLevelSettingsCard(
title = stringResource(id = R.string.preferences_debug_title), title = stringResource(id = R.string.preferences_debug_title),
subtitle = stringResource(R.string.preference_subtitle_debug), subtitle = stringResource(R.string.preference_subtitle_debug),
@@ -196,8 +226,8 @@ private fun SearchPreferences(
val multiSearch = remember { mutableStateOf(preferences.multiSearch) } val multiSearch = remember { mutableStateOf(preferences.multiSearch) }
SwitchPreference( SwitchPreference(
titleText = "Multi Search", titleText = stringResource(R.string.preference_multi_search_title),
subtitleText = "Search across movies, TV, and people at the same time", subtitleText = stringResource(R.string.preference_multi_search_subtitle),
checkState = multiSearch.value, checkState = multiSearch.value,
onCheckedChange = { isChecked -> onCheckedChange = { isChecked ->
multiSearch.value = isChecked multiSearch.value = isChecked
@@ -288,12 +318,6 @@ private fun DesignPreferences(
} }
} }
enum class DarkMode {
Automatic,
Dark,
Light
}
@Composable @Composable
private fun DarkModePreferences( private fun DarkModePreferences(
activity: AppCompatActivity, activity: AppCompatActivity,
@@ -308,35 +332,79 @@ private fun DarkModePreferences(
activity.recreate() activity.recreate()
} }
PreferenceHeading(text = "Automatic") PreferenceHeading(text = stringResource(R.string.preference_dark_mode_automatic_heading))
RadioButtonPreference( RadioButtonPreference(
selected = isSelected(DarkMode.Automatic.ordinal), selected = isSelected(AppPreferences.DarkMode.Automatic.ordinal),
title = "Follow system", title = stringResource(R.string.preference_dark_mode_follow_system_label),
icon = Icons.Filled.Brightness6, icon = Icons.Filled.Brightness6,
onClick = { onClick = {
onChangeState(DarkMode.Automatic.ordinal) onChangeState(AppPreferences.DarkMode.Automatic.ordinal)
} }
) )
PreferenceHeading(text = "Manual") PreferenceHeading(text = stringResource(R.string.preference_dark_mode_maual_heading))
RadioButtonPreference( RadioButtonPreference(
selected = isSelected(DarkMode.Light.ordinal), selected = isSelected(AppPreferences.DarkMode.Light.ordinal),
title = "Light mode", title = stringResource(R.string.preference_dark_mode_light_mode_label),
icon = Icons.Outlined.LightMode, icon = Icons.Outlined.LightMode,
onClick = { onClick = {
onChangeState(DarkMode.Light.ordinal) onChangeState(AppPreferences.DarkMode.Light.ordinal)
} }
) )
RadioButtonPreference( RadioButtonPreference(
selected = isSelected(DarkMode.Dark.ordinal), selected = isSelected(AppPreferences.DarkMode.Dark.ordinal),
title = "Dark mode", title = stringResource(R.string.preference_dark_mode_dark_mode_label),
icon = Icons.Outlined.DarkMode, icon = Icons.Outlined.DarkMode,
onClick = { onClick = {
onChangeState(DarkMode.Dark.ordinal) onChangeState(AppPreferences.DarkMode.Dark.ordinal)
} }
) )
} }
@Composable
private fun HomeScreenPreferences(
preferences: AppPreferences = get(AppPreferences::class.java)
) {
PreferenceHeading(text = stringResource(R.string.preference_look_and_feel_heading))
val showTabLabels = remember { mutableStateOf(preferences.showBottomTabLabels) }
SwitchPreference(
titleText = stringResource(R.string.preference_show_text_labels_title),
subtitleText = stringResource(R.string.preference_show_text_labels_subtitle),
checkState = showTabLabels.value,
onCheckedChange = { isChecked ->
showTabLabels.value = isChecked
preferences.showBottomTabLabels = isChecked
}
)
PreferenceHeading(text = stringResource(R.string.preference_home_tab_order_heading))
Box(modifier = Modifier
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(10.dp)
)
.padding(horizontal = 12.dp)
) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context)
val mAdapter = HomeTabRecyclerAdapter()
val touchCallback = ItemMoveCallback(mAdapter)
val touchHelper = ItemTouchHelper(touchCallback)
touchHelper.attachToRecyclerView(this)
adapter = mAdapter
}
},
update = { }
)
}
}
@Composable @Composable
private fun DevPreferences( private fun DevPreferences(
preferences: AppPreferences = get(AppPreferences::class.java) preferences: AppPreferences = get(AppPreferences::class.java)
@@ -513,13 +581,64 @@ private fun WallpaperPicker(
} }
} }
private sealed class SettingsPage(stringRes: Int, val route: String, val SettingsPageRenderer: @Composable (NavController, AppCompatActivity, AppPreferences) -> Unit): KoinComponent { private fun resetAllPreferences(preferences: AppPreferences) {
resetSearchPreferences(preferences = preferences)
resetDesignPreferences(preferences = preferences)
resetHomeScreenPreferences(preferences = preferences)
resetDevModePreference(preferences = preferences)
}
private fun resetSearchPreferences(preferences: AppPreferences) {
preferences.showSearchBar = preferences.showSearchBarDefault
preferences.multiSearch = preferences.multiSearchDefault
}
private fun resetDesignPreferences(preferences: AppPreferences) {
preferences.useWallpaperColors = preferences.useWallpaperColorsDefault
preferences.useSystemColors = preferences.useSystemColorsDefault
preferences.chromaMultiplier = preferences.chromeMultiplyerDefault
preferences.selectedColor = preferences.selectedColorDefault
resetDarkModePreferences(preferences = preferences)
}
private fun resetDarkModePreferences(preferences: AppPreferences) {
preferences.darkTheme = preferences.darkThemeDefault
}
private fun resetDevModePreference(preferences: AppPreferences) {
preferences.firstLaunchTesting = preferences.firstLaunchTestingDefault
preferences.useV4Api = preferences.useV4ApiDefault
preferences.showBackdropGallery = preferences.showBackdropGalleryDefault
}
private fun resetHomeScreenPreferences(preferences: AppPreferences) {
preferences.moviesTabPosition = preferences.moviesTabPositionDefault
preferences.tvTabPosition = preferences.tvTabPositionDefault
preferences.peopleTabPosition = preferences.peopleTabPositionDefault
preferences.accountTabPosition = preferences.accountTabPositionDefault
preferences.showBottomTabLabels = preferences.showBottomTabLabelsDefault
}
private sealed class SettingsPage(
stringRes: Int,
val route: String,
val SettingsPageRenderer: @Composable (NavController, AppCompatActivity, AppPreferences) -> Unit,
val resetPreferencesHandler: (AppPreferences) -> Unit
): KoinComponent {
private val resources: ResourceUtils by inject() private val resources: ResourceUtils by inject()
val name = resources.getString(stringRes) val name = resources.getString(stringRes)
companion object { companion object {
val Pages by lazy {listOf(SearchSettings, DesignSettings, DeveloperSettings, DarkModeSettings) } val Pages by lazy {
listOf(
SearchSettings,
DesignSettings,
DeveloperSettings,
DarkModeSettings,
HomeScreenSettings
)
}
fun getByRoute(route: String): SettingsPage { fun getByRoute(route: String): SettingsPage {
return Pages.map { it.route to it } return Pages.map { it.route to it }
@@ -529,8 +648,34 @@ private sealed class SettingsPage(stringRes: Int, val route: String, val Setting
} }
} }
object SearchSettings: SettingsPage(R.string.preference_heading_search, "search", @Composable { n, a, p -> SearchPreferences(p) } ) object SearchSettings: SettingsPage(
object DesignSettings: SettingsPage(R.string.preference_heading_design, "design", @Composable { n, a, p -> DesignPreferences(n, a, p) } ) R.string.preference_heading_search,
object DeveloperSettings: SettingsPage(R.string.preferences_debug_title,"dev", @Composable { n, a, p -> DevPreferences(p) } ) "search",
object DarkModeSettings: SettingsPage(R.string.preference_heading_dark_mode, "darkmode", @Composable { n, a, p -> DarkModePreferences(a, p) } ) @Composable { _, _, p -> SearchPreferences(p) },
::resetSearchPreferences
)
object DesignSettings: SettingsPage(
R.string.preference_heading_design,
"design",
@Composable { n, a, p -> DesignPreferences(n, a, p) },
::resetDesignPreferences
)
object HomeScreenSettings: SettingsPage(
R.string.preference_heading_home_screen,
"home",
@Composable { _, _, p -> HomeScreenPreferences(p) },
::resetHomeScreenPreferences
)
object DeveloperSettings: SettingsPage(
R.string.preferences_debug_title,
"dev",
@Composable { _, _, p -> DevPreferences(p) },
::resetDevModePreference
)
object DarkModeSettings: SettingsPage(
R.string.preference_heading_dark_mode,
"darkmode",
@Composable { _, a, p -> DarkModePreferences(a, p) },
::resetDarkModePreferences
)
} }

View File

@@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.kieronquinn.monetcompat.core.MonetCompat import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.screens.main.DarkMode
import org.koin.java.KoinJavaComponent.get import org.koin.java.KoinJavaComponent.get
private val DarkColorPalette = darkColorScheme( private val DarkColorPalette = darkColorScheme(
@@ -77,9 +76,9 @@ fun TVTimeTheme(
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val isDarkTheme = when(preferences.darkTheme) { val isDarkTheme = when(preferences.darkTheme) {
DarkMode.Automatic.ordinal -> isSystemInDarkTheme() AppPreferences.DarkMode.Automatic.ordinal -> isSystemInDarkTheme()
DarkMode.Dark.ordinal -> true AppPreferences.DarkMode.Dark.ordinal -> true
DarkMode.Light.ordinal -> false AppPreferences.DarkMode.Light.ordinal -> false
else -> throw IllegalArgumentException("Illegal theme value ${preferences.darkTheme}") else -> throw IllegalArgumentException("Illegal theme value ${preferences.darkTheme}")
} }

View File

@@ -0,0 +1,137 @@
package com.owenlejeune.tvtime.ui.views
import android.view.ViewGroup
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.recyclerview.widget.RecyclerView
import com.owenlejeune.tvtime.ui.navigation.BottomNavItem
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class HomeTabRecyclerAdapter: RecyclerView.Adapter<HomeTabRecyclerAdapter.TabViewHolder>(),
ItemMoveCallback.ItemTouchHelperContract, KoinComponent
{
class TabViewHolder(itemView: ComposeView): RecyclerView.ViewHolder(itemView)
private val pages: MutableList<BottomNavItem?>
private val indexOfDivider
get() = pages.indexOf(null)
init {
val visiblePages = BottomNavItem.Items.filter { it.order > -1 }.sortedBy { it.order }
val hiddenPages = BottomNavItem.Items.filter { it.order < 0 }
pages = ArrayList<BottomNavItem?>().apply {
addAll(visiblePages)
add(null)
addAll(hiddenPages)
}
}
override fun getItemCount(): Int {
return pages.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
val composeView = ComposeView(get()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
}
return TabViewHolder(composeView)
}
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
val itemView = holder.itemView
if (itemView is ComposeView) {
val page = pages[position]
itemView.setContent {
if (page == null) {
ItemDivider()
} else {
ItemRow(page = page)
}
}
}
}
@Composable
private fun ItemRow(page: BottomNavItem) {
Row(
modifier = Modifier
.height(50.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = page.icon),
contentDescription = page.name
)
Text(
modifier = Modifier
.padding(start = 8.dp),
text = page.name,
color = MaterialTheme.colorScheme.onBackground,
fontSize = 16.sp
)
Spacer(modifier = Modifier.weight(1f))
Icon(
modifier = Modifier
.size(24.dp),
imageVector = Icons.Filled.DragIndicator,
contentDescription = null
)
}
}
@Composable
private fun ItemDivider() {
Row(modifier = Modifier.height(50.dp)) {
Text(
text = "Hidden",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(end = 8.dp)
)
Divider(
modifier = Modifier.align(Alignment.CenterVertically),
color = MaterialTheme.colorScheme.onBackground,
thickness = 2.dp
)
}
}
override fun onRowClear(myViewHolder: RecyclerView.ViewHolder) {
myViewHolder.itemView.alpha = 1f
}
override fun onRowMoved(fromPosition: Int, toPosition: Int) {
if (indexOfDivider == 1 && toPosition > 1) {
return
}
pages.add(
toPosition,
pages.removeAt(fromPosition)
)
pages.forEachIndexed { index, bottomNavItem ->
bottomNavItem?.order = if (index > indexOfDivider) -1 else index
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onRowSelected(myViewHolder: RecyclerView.ViewHolder) {
myViewHolder.itemView.alpha = 0.6f
}
}

View File

@@ -0,0 +1,54 @@
package com.owenlejeune.tvtime.ui.views
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class ItemMoveCallback(private val mAdapter: ItemTouchHelperContract): ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled(): Boolean {
return true
}
override fun isItemViewSwipeEnabled(): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// left blank
}
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val flags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(flags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
mAdapter.onRowMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
return true
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
viewHolder?.let { mAdapter.onRowSelected(viewHolder) }
}
super.onSelectedChanged(viewHolder, actionState)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
mAdapter.onRowClear(viewHolder)
}
interface ItemTouchHelperContract {
fun onRowMoved(fromPosition: Int, toPosition: Int)
fun onRowSelected(myViewHolder: RecyclerView.ViewHolder)
fun onRowClear(myViewHolder: RecyclerView.ViewHolder)
}
}

View File

@@ -66,6 +66,20 @@
<string name="preference_subtitle_debug">Secret developer options</string> <string name="preference_subtitle_debug">Secret developer options</string>
<string name="preference_heading_dark_mode">Dark mode</string> <string name="preference_heading_dark_mode">Dark mode</string>
<string name="preference_subtitle_dark_mode">Light, dark, or auto</string> <string name="preference_subtitle_dark_mode">Light, dark, or auto</string>
<string name="preference_heading_home_screen">Home Screen</string>
<string name="preference_subtitle_home_screen">Home screen layout options</string>
<string name="preference_dark_mode_automatic_heading">Automatic</string>
<string name="preference_dark_mode_maual_heading">Manual</string>
<string name="preference_dark_mode_follow_system_label">Follow system</string>
<string name="preference_dark_mode_light_mode_label">Light mode</string>
<string name="preference_dark_mode_dark_mode_label">Dark mode</string>
<string name="preference_look_and_feel_heading">Look &amp; Feel</string>
<string name="preferences_restore_content_description">Restore settings</string>
<string name="preference_multi_search_title">Multi Search</string>
<string name="preference_multi_search_subtitle">Search across movies, TV, and people at the same time</string>
<string name="preference_show_text_labels_title">Show tab text labels</string>
<string name="preference_show_text_labels_subtitle">Show text labels for tab items in the bottom tab bar or navigation rail</string>
<string name="preference_home_tab_order_heading">Home Tab Order</string>
<!-- video type --> <!-- video type -->
<string name="video_type_clip">Clips</string> <string name="video_type_clip">Clips</string>
@@ -122,6 +136,6 @@
<string name="example_page_desc">This is an example</string> <string name="example_page_desc">This is an example</string>
<!-- search results --> <!-- search results -->
<string name="search_result_tv_services">TV Series</string> <string name="search_result_tv_series">TV Series</string>
<string name="no_search_results">No search results found</string> <string name="no_search_results">No search results found</string>
</resources> </resources>