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() 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, 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, 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, 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, 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, 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, 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, appBarActions: MutableState 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() } } } }