Files
TVTime/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt
Owen LeJeune d6f43b7579 some ui tweaks
2023-05-31 17:36:26 -04:00

476 lines
19 KiB
Kotlin

package com.owenlejeune.tvtime
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.animation.rememberSplineBasedDecay
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
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.rememberWindowSizeClass
import com.owenlejeune.tvtime.preferences.AppPreferences
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.SearchScreen
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get
@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : MonetCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.IO).launch {
SessionManager.initialize()
}
var mainNavStartRoute = BottomNavItem.SortedItems[0].route
intent.data?.let {
when (it.host) {
getString(R.string.intent_route_auth_return) -> mainNavStartRoute = BottomNavItem.Account.route
}
}
lifecycleScope.launchWhenCreated {
monet.awaitMonetReady()
setContent {
AppKeyboardFocusManager()
TVTimeTheme(monetCompat = monet) {
val windowSize = rememberWindowSizeClass()
val appNavController = rememberNavController()
Box {
MainNavigationRoutes(
appNavController = appNavController,
mainNavStartRoute = mainNavStartRoute,
windowSize = windowSize
)
}
}
}
}
}
@Composable
private fun AppScaffold(
appNavController: NavHostController,
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route,
windowSize: WindowSizeClass,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val appBarTitle = rememberSaveable { mutableStateOf(BottomNavItem.getByRoute(currentRoute)?.name ?: BottomNavItem.SortedItems[0].name) }
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState)
}
val appBarActions = remember { mutableStateOf<@Composable RowScope.() -> Unit>({}) }
val fab = remember { mutableStateOf<@Composable () -> Unit>({}) }
Scaffold (
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
if (windowSize != WindowSizeClass.Expanded) {
TopBar(
appNavController = appNavController,
title = appBarTitle,
scrollBehavior = scrollBehavior,
appBarActions = appBarActions
)
}
},
floatingActionButton = {
fab.value()
},
bottomBar = {
if (windowSize != WindowSizeClass.Expanded) {
BottomNavBar(navController = navController, appBarTitle = appBarTitle)
}
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
MainContent(
windowSize = windowSize,
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = scrollBehavior,
mainNavStartRoute = mainNavStartRoute
)
}
}
}
@Composable
private fun TopBar(
appNavController: NavHostController,
title: MutableState<String>,
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
),
actions = {
appBarActions.value(this)
defaultAppBarActions()
}
)
}
@Composable
private fun BottomNavBar(
navController: NavController,
appBarTitle: MutableState<String>,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
NavigationBar {
BottomNavItem.SortedItems.forEach { item ->
NavigationBarItem(
modifier = Modifier
.padding(4.dp)
.clip(RoundedCornerShape(24.dp)),
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
label = {
val name = if (preferences.showBottomTabLabels) item.name else " "
Text(text = name)
},
selected = currentRoute == item.route,
onClick = {
onBottomAppBarItemClicked(
navController = navController,
appBarTitle = appBarTitle,
item = item
)
}
)
}
}
}
private fun onBottomAppBarItemClicked(
navController: NavController,
appBarTitle: MutableState<String>,
item: BottomNavItem
) {
navigateToRoute(navController, item.route)
appBarTitle.value = item.name
}
private fun navigateToRoute(navController: NavController, route: String) {
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { screenRoute ->
popUpTo(screenRoute) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
@Composable
private fun MainContent(
windowSize: WindowSizeClass,
appNavController: NavHostController,
navController: NavHostController,
fab: MutableState<@Composable () -> Unit>,
topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) {
if (windowSize == WindowSizeClass.Expanded) {
DualColumnMainContent(
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = topBarScrollBehaviour,
mainNavStartRoute = mainNavStartRoute
)
} else {
SingleColumnMainContent(
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
mainNavStartRoute = mainNavStartRoute
)
}
}
@Composable
private fun SingleColumnMainContent(
appNavController: NavHostController,
navController: NavHostController,
fab: MutableState<@Composable () -> Unit>,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) {
MainMediaView(
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
mainNavStartRoute = mainNavStartRoute
)
}
@Composable
private fun DualColumnMainContent(
appNavController: NavHostController,
navController: NavHostController,
fab: MutableState<@Composable () -> Unit>,
topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail {
Spacer(modifier = Modifier.weight(1f))
BottomNavItem.SortedItems.forEachIndexed { index, item ->
NavigationRailItem(
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
label = { if (preferences.showBottomTabLabels) Text(item.name) },
selected = currentRoute == item.route,
onClick = {
onBottomAppBarItemClicked(
navController = navController,
appBarTitle = appBarTitle,
item = item
)
}
)
if (index < BottomNavItem.SortedItems.size - 1) {
Spacer(modifier = Modifier.height(20.dp))
}
}
Spacer(modifier = Modifier.weight(1f))
}
Column {
TopBar(
appNavController = appNavController,
title = appBarTitle,
scrollBehavior = topBarScrollBehaviour,
appBarActions = appBarActions
)
MainMediaView(
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
mainNavStartRoute = mainNavStartRoute
)
}
}
}
@Composable
private fun MainMediaView(
appNavController: NavHostController,
navController: NavHostController,
fab: MutableState<@Composable () -> Unit>,
appBarTitle: MutableState<String>,
appBarActions: MutableState<RowScope.() -> Unit> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) {
Column {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
MainNavGraph(
activity = this@MainActivity,
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
startDestination = mainNavStartRoute
)
}
}
private object NavConstants {
const val ID_KEY = "id_key"
const val TYPE_KEY = "type_key"
const val SETTINGS_KEY = "settings_key"
const val SEARCH_ID_KEY = "search_key"
const val SEARCH_TITLE_KEY = "search_title_key"
}
@Composable
private fun MainNavigationRoutes(
startDestination: String = MainNavItem.MainView.route,
mainNavStartRoute: String = MainNavItem.Items[0].route,
appNavController: NavHostController,
windowSize: WindowSizeClass,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
NavHost(navController = appNavController, startDestination = startDestination) {
composable(MainNavItem.MainView.route) {
AppScaffold(
appNavController = appNavController,
mainNavStartRoute = mainNavStartRoute,
windowSize = windowSize
)
}
composable(
MainNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
arguments = listOf(
navArgument(NavConstants.ID_KEY) { type = NavType.IntType },
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) }
)
) { navBackStackEntry ->
val args = navBackStackEntry.arguments
val mediaType = args?.getSerializable(NavConstants.TYPE_KEY) as MediaViewType
when (mediaType) {
MediaViewType.PERSON -> {
PersonDetailView(
appNavController = appNavController,
personId = args.getInt(NavConstants.ID_KEY)
)
}
MediaViewType.LIST -> {
ListDetailView(
appNavController = appNavController,
itemId = args.getInt(NavConstants.ID_KEY),
windowSize = windowSize
)
}
else -> {
MediaDetailView(
appNavController = appNavController,
itemId = args.getInt(NavConstants.ID_KEY),
type = mediaType,
windowSize = windowSize
)
}
}
}
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)
}
composable(
route = MainNavItem.SearchView.route.plus("/{${NavConstants.SEARCH_ID_KEY}}/{${NavConstants.SEARCH_TITLE_KEY}}"),
arguments = listOf(
navArgument(NavConstants.SEARCH_ID_KEY) { type = NavType.IntType },
navArgument(NavConstants.SEARCH_TITLE_KEY) { type = NavType.StringType }
)
) {
it.arguments?.let { arguments ->
val (type, title) = if (preferences.multiSearch) {
Pair(
MediaViewType.MIXED,
stringResource(id = R.string.search_all_title)
)
} else {
Pair(
MediaViewType[arguments.getInt(NavConstants.SEARCH_ID_KEY)],
arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: ""
)
}
SearchScreen(
appNavController = appNavController,
title = title,
mediaViewType = type
)
}
}
}
}
@Composable
private fun AppKeyboardFocusManager() {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
DisposableEffect(key1 = context) {
val keyboardManager = KeyboardManager.getInstance(context)
keyboardManager.attachKeyboardDismissListener {
focusManager.clearFocus()
}
onDispose {
keyboardManager.release()
}
}
}
}