diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index 768f9fe..2050131 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -58,7 +58,7 @@ class MainActivity : MonetCompatActivity() { SessionManager.initialize() } - var mainNavStartRoute = BottomNavItem.Items[0].route + var mainNavStartRoute = BottomNavItem.SortedItems[0].route intent.data?.let { when (it.host) { getString(R.string.intent_route_auth_return) -> mainNavStartRoute = BottomNavItem.Account.route @@ -82,7 +82,7 @@ class MainActivity : MonetCompatActivity() { @Composable private fun AppScaffold( appNavController: NavHostController, - mainNavStartRoute: String = BottomNavItem.Items[0].route, + mainNavStartRoute: String = BottomNavItem.SortedItems[0].route, preferences: AppPreferences = get(AppPreferences::class.java) ) { val windowSize = rememberWindowSizeClass() @@ -91,7 +91,7 @@ class MainActivity : MonetCompatActivity() { val navBackStackEntry by navController.currentBackStackEntryAsState() 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() val topAppBarScrollState = rememberTopAppBarScrollState() val scrollBehavior = remember(decayAnimationSpec) { @@ -101,8 +101,6 @@ class MainActivity : MonetCompatActivity() { val appBarActions = remember { mutableStateOf<@Composable RowScope.() -> Unit>({}) } val fab = remember { mutableStateOf<@Composable () -> Unit>({}) } - // todo - scroll state not remember when returing from detail screen - Scaffold ( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { @@ -170,18 +168,25 @@ class MainActivity : MonetCompatActivity() { } @Composable - private fun BottomNavBar(navController: NavController, appBarTitle: MutableState) { + private fun BottomNavBar( + navController: NavController, + appBarTitle: MutableState, + preferences: AppPreferences = get(AppPreferences::class.java) + ) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route NavigationBar { - BottomNavItem.Items.forEach { item -> + BottomNavItem.SortedItems.forEach { item -> NavigationBarItem( modifier = Modifier .padding(4.dp) .clip(RoundedCornerShape(24.dp)), 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, onClick = { onBottomAppBarItemClicked( @@ -225,7 +230,7 @@ class MainActivity : MonetCompatActivity() { topBarScrollBehaviour: TopAppBarScrollBehavior, appBarTitle: MutableState, appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), - mainNavStartRoute: String = BottomNavItem.Items[0].route + mainNavStartRoute: String = BottomNavItem.SortedItems[0].route ) { if (windowSize == WindowSizeClass.Expanded) { DualColumnMainContent( @@ -256,7 +261,7 @@ class MainActivity : MonetCompatActivity() { fab: MutableState<@Composable () -> Unit>, appBarTitle: MutableState, appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), - mainNavStartRoute: String = BottomNavItem.Items[0].route + mainNavStartRoute: String = BottomNavItem.SortedItems[0].route ) { MainMediaView( appNavController = appNavController, @@ -276,7 +281,8 @@ class MainActivity : MonetCompatActivity() { topBarScrollBehaviour: TopAppBarScrollBehavior, appBarTitle: MutableState, 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 currentRoute = navBackStackEntry?.destination?.route @@ -284,10 +290,10 @@ class MainActivity : MonetCompatActivity() { Row(modifier = Modifier.fillMaxSize()) { NavigationRail { Spacer(modifier = Modifier.weight(1f)) - BottomNavItem.Items.forEach { item -> + BottomNavItem.SortedItems.forEach { item -> NavigationRailItem( icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) }, - label = { Text(item.name) }, + label = { if (preferences.showBottomTabLabels) Text(item.name) }, selected = currentRoute == item.route, onClick = { onBottomAppBarItemClicked( @@ -326,7 +332,7 @@ class MainActivity : MonetCompatActivity() { fab: MutableState<@Composable () -> Unit>, appBarTitle: MutableState, appBarActions: MutableState Unit> = mutableStateOf({}), - mainNavStartRoute: String = BottomNavItem.Items[0].route + mainNavStartRoute: String = BottomNavItem.SortedItems[0].route ) { Column { val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -413,13 +419,6 @@ class MainActivity : MonetCompatActivity() { ) ) { 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) { Pair(MediaViewType.MIXED, "") } else { diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt index 66a2eca..ce26313 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt @@ -36,6 +36,16 @@ fun LazyListScope.listItems( } } +fun LazyListScope.listItems( + items: List, + key: (T?) -> Any, + itemContent: @Composable (value: T?) -> Unit +) { + items(items.size, key = { key(items[it]) }) { index -> + itemContent(items[index]) + } +} + fun LazyListScope.lazyPagingItems( lazyPagingItems: LazyPagingItems, itemContent: @Composable LazyItemScope.(value: T?) -> Unit 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 288ef9e..2ab4bc0 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -9,10 +9,15 @@ import com.owenlejeune.tvtime.utils.SessionManager class AppPreferences(context: Context) { + enum class DarkMode { + Automatic, + Dark, + Light + } + companion object { private val PREF_FILE = "tvtime_shared_preferences" -// private val USE_PREFERENCES = "use_android_12_colors" private val PERSISTENT_SEARCH = "persistent_search" private val GUEST_SESSION = "guest_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 DARK_THEME = "dark_theme" 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) /******** Search Preferences ********/ + val showSearchBarDefault: Boolean = false var showSearchBar: Boolean - get() = preferences.getBoolean(PERSISTENT_SEARCH, true) + get() = preferences.getBoolean(PERSISTENT_SEARCH, showSearchBarDefault) set(value) { preferences.put(PERSISTENT_SEARCH, value) } + val multiSearchDefault: Boolean = true var multiSearch: Boolean - get() = preferences.getBoolean(MULTI_SEARCH, false) + get() = preferences.getBoolean(MULTI_SEARCH, multiSearchDefault) set(value) { preferences.put(MULTI_SEARCH, value) } /******* Design Preferences ********/ + val useWallpaperColorsDefault: Boolean = true 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) } + val darkThemeDefault: Int = DarkMode.Automatic.ordinal var darkTheme: Int - get() = preferences.getInt(DARK_THEME, 0) + get() = preferences.getInt(DARK_THEME, darkThemeDefault) set(value) { preferences.put(DARK_THEME, value) } + val useSystemColorsDefault: Boolean = true 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) } + val chromeMultiplyerDefault: Double = MonetCompat.chromaMultiplier.toFloat().toDouble() 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) } + val selectedColorDefault: Int = Int.MAX_VALUE var selectedColor: Int - get() = preferences.getInt(SELECTED_COLOR, Int.MAX_VALUE) + get() = preferences.getInt(SELECTED_COLOR, selectedColorDefault) set(value) { preferences.put(SELECTED_COLOR, value) } /******* Session Tokens ********/ @@ -76,21 +93,50 @@ class AppPreferences(context: Context) { get() = preferences.getString(AUTHORIZED_SESSION, "") ?: "" 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 ********/ + val firstLaunchTestingDefault: Boolean = false 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) } var firstLaunch: Boolean get() = if (BuildConfig.DEBUG) firstLaunchTesting else preferences.getBoolean(FIRST_LAUNCH, true) set(value) { preferences.put(FIRST_LAUNCH, value) } + val useV4ApiDefault: Boolean = false 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) } - var showBackdropGallery: Boolean// = true - get() = preferences.getBoolean(SHOW_BACKDROP_GALLERY, true) + val showBackdropGalleryDefault: Boolean = true + var showBackdropGallery: Boolean + get() = preferences.getBoolean(SHOW_BACKDROP_GALLERY, showBackdropGalleryDefault) set(value) { preferences.put(SHOW_BACKDROP_GALLERY, value) } /********* Helpers ********/ 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 6f7bf94..35fac55 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 @@ -1,35 +1,48 @@ package com.owenlejeune.tvtime.ui.navigation import com.owenlejeune.tvtime.R +import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.utils.ResourceUtils import org.koin.core.component.KoinComponent 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() val name = resourceUtils.getString(stringRes) + var order: Int + get() = orderGetter.invoke(appPreferences) + set(value) { orderSetter.invoke(appPreferences, value) } companion object { - val Items by lazy { listOf(Movies, TV, People, Account) } - val SearchableRoutes by lazy { listOf(Movies.route, TV.route, People.route) } + val SortedItems + 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? { return when (route) { Movies.route -> Movies TV.route -> TV Account.route -> Account - Favourites.route -> Favourites else -> null } } } - 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 People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_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", { it.tvTabPosition }, { p, i -> p.tvTabPosition = i } ) + object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route", { it.accountTabPosition }, { p, i -> p.accountTabPosition = i } ) + object People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_route", { it.peopleTabPosition }, { p, i -> p.peopleTabPosition = i } ) } 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 e90926c..d00a7a2 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 @@ -6,11 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost 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.* @@ -27,7 +24,7 @@ fun MainNavGraph( fab: MutableState<@Composable () -> Unit>, appBarTitle: MutableState, appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}), - startDestination: String = BottomNavItem.Items[0].route + startDestination: String = BottomNavItem.SortedItems[0].route ) { NavHost(navController = navController, startDestination = startDestination) { composable(BottomNavItem.Movies.route) { @@ -59,9 +56,5 @@ fun MainNavGraph( fab = fab ) } - composable(BottomNavItem.Favourites.route) { - appBarActions.value = {} - FavouritesTab() - } } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt index 5c374a4..66c74c6 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt @@ -328,7 +328,7 @@ private fun TvSearchResultView( backdropModel = { TmdbUtils.getFullBackdropPath(result.backdropPath) }, additionalDetails = { 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 } ?: "" ) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/FavouritesTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/FavouritesTab.kt deleted file mode 100644 index 279e30c..0000000 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/FavouritesTab.kt +++ /dev/null @@ -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 - ) - } -} \ No newline at end of file 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 d11fe29..ae796b0 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 @@ -4,8 +4,10 @@ import android.os.Build import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.IconButton import androidx.compose.material.TextButton 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.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView 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.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.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.SessionManager import kotlinx.coroutines.launch @@ -52,6 +60,8 @@ fun SettingsTab( } val appBarTitle = remember { mutableStateOf("") } + val defaultRestoreAction = ::resetAllPreferences + val restoreAction = remember { mutableStateOf<(AppPreferences) -> Unit>(defaultRestoreAction) } Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -72,6 +82,16 @@ fun SettingsTab( 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 .fillMaxSize() .padding(all = 24.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) + verticalArrangement = Arrangement.spacedBy(18.dp) ) { SettingsPage.getByRoute(route).apply { appBarTitle.value = name + restoreAction.value = resetPreferencesHandler SettingsPageRenderer(appNavController, activity, preferences) } } } else { + restoreAction.value = ::resetAllPreferences SettingsTabView( appNavController = appNavController, appBarTitle = appBarTitle @@ -128,6 +150,14 @@ private fun SettingsTabView( 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( title = stringResource(id = R.string.preferences_debug_title), subtitle = stringResource(R.string.preference_subtitle_debug), @@ -196,8 +226,8 @@ private fun SearchPreferences( val multiSearch = remember { mutableStateOf(preferences.multiSearch) } SwitchPreference( - titleText = "Multi Search", - subtitleText = "Search across movies, TV, and people at the same time", + titleText = stringResource(R.string.preference_multi_search_title), + subtitleText = stringResource(R.string.preference_multi_search_subtitle), checkState = multiSearch.value, onCheckedChange = { isChecked -> multiSearch.value = isChecked @@ -288,12 +318,6 @@ private fun DesignPreferences( } } -enum class DarkMode { - Automatic, - Dark, - Light -} - @Composable private fun DarkModePreferences( activity: AppCompatActivity, @@ -308,35 +332,79 @@ private fun DarkModePreferences( activity.recreate() } - PreferenceHeading(text = "Automatic") + PreferenceHeading(text = stringResource(R.string.preference_dark_mode_automatic_heading)) RadioButtonPreference( - selected = isSelected(DarkMode.Automatic.ordinal), - title = "Follow system", + selected = isSelected(AppPreferences.DarkMode.Automatic.ordinal), + title = stringResource(R.string.preference_dark_mode_follow_system_label), icon = Icons.Filled.Brightness6, onClick = { - onChangeState(DarkMode.Automatic.ordinal) + onChangeState(AppPreferences.DarkMode.Automatic.ordinal) } ) - PreferenceHeading(text = "Manual") + PreferenceHeading(text = stringResource(R.string.preference_dark_mode_maual_heading)) RadioButtonPreference( - selected = isSelected(DarkMode.Light.ordinal), - title = "Light mode", + selected = isSelected(AppPreferences.DarkMode.Light.ordinal), + title = stringResource(R.string.preference_dark_mode_light_mode_label), icon = Icons.Outlined.LightMode, onClick = { - onChangeState(DarkMode.Light.ordinal) + onChangeState(AppPreferences.DarkMode.Light.ordinal) } ) RadioButtonPreference( - selected = isSelected(DarkMode.Dark.ordinal), - title = "Dark mode", + selected = isSelected(AppPreferences.DarkMode.Dark.ordinal), + title = stringResource(R.string.preference_dark_mode_dark_mode_label), icon = Icons.Outlined.DarkMode, 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 private fun DevPreferences( 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() val name = resources.getString(stringRes) 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 { 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 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) } ) + object SearchSettings: SettingsPage( + R.string.preference_heading_search, + "search", + @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 + ) } 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 09f220e..f2885e2 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,7 +9,6 @@ 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( @@ -77,9 +76,9 @@ fun TVTimeTheme( content: @Composable () -> Unit ) { val isDarkTheme = when(preferences.darkTheme) { - DarkMode.Automatic.ordinal -> isSystemInDarkTheme() - DarkMode.Dark.ordinal -> true - DarkMode.Light.ordinal -> false + AppPreferences.DarkMode.Automatic.ordinal -> isSystemInDarkTheme() + AppPreferences.DarkMode.Dark.ordinal -> true + AppPreferences.DarkMode.Light.ordinal -> false else -> throw IllegalArgumentException("Illegal theme value ${preferences.darkTheme}") } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/views/HomeTabRecyclerAdapter.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/views/HomeTabRecyclerAdapter.kt new file mode 100644 index 0000000..c2cac2c --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/views/HomeTabRecyclerAdapter.kt @@ -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(), + ItemMoveCallback.ItemTouchHelperContract, KoinComponent +{ + + class TabViewHolder(itemView: ComposeView): RecyclerView.ViewHolder(itemView) + + private val pages: MutableList + 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().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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/views/ItemMoveCallback.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/views/ItemMoveCallback.kt new file mode 100644 index 0000000..93ba1b1 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/views/ItemMoveCallback.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c29611..13c024b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,20 @@ Secret developer options Dark mode Light, dark, or auto + Home Screen + Home screen layout options + Automatic + Manual + Follow system + Light mode + Dark mode + Look & Feel + Restore settings + Multi Search + Search across movies, TV, and people at the same time + Show tab text labels + Show text labels for tab items in the bottom tab bar or navigation rail + Home Tab Order Clips @@ -122,6 +136,6 @@ This is an example - TV Series + TV Series No search results found \ No newline at end of file