diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index b9d55a814..2dc1a9882 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Search @@ -17,14 +19,16 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.compose.ui.zIndex import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import coil3.ImageLoader @@ -37,6 +41,7 @@ import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsScreen import com.nuvio.app.features.home.HomeCatalogSection import com.nuvio.app.features.home.HomeScreen +import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.search.SearchScreen import com.nuvio.app.features.settings.SettingsScreen import com.nuvio.app.features.streams.StreamsRepository @@ -44,13 +49,7 @@ import com.nuvio.app.features.streams.StreamsScreen import kotlinx.serialization.Serializable @Serializable -object HomeRoute - -@Serializable -object SearchRoute - -@Serializable -object SettingsRoute +object TabsRoute @Serializable data class DetailRoute(val type: String, val id: String) @@ -86,6 +85,29 @@ enum class AppScreenTab { Settings, } +@Composable +fun AppScreen( + tab: AppScreenTab, + modifier: Modifier = Modifier, + onCatalogClick: ((HomeCatalogSection) -> Unit)? = null, + onPosterClick: ((MetaPreview) -> Unit)? = null, +) { + when (tab) { + AppScreenTab.Home -> HomeScreen( + modifier = modifier, + onCatalogClick = onCatalogClick, + onPosterClick = onPosterClick, + ) + AppScreenTab.Search -> SearchScreen( + modifier = modifier, + onPosterClick = onPosterClick, + ) + AppScreenTab.Settings -> SettingsScreen( + modifier = modifier, + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable @Preview @@ -97,10 +119,7 @@ fun App() { } NuvioTheme { val navController = rememberNavController() - val navBackStackEntry = navController.currentBackStackEntryAsState().value - val currentDestination = navBackStackEntry?.destination - val currentTab = AppScreenTab.entries.firstOrNull { tab -> tab.matches(currentDestination) } - val showBottomBar = currentTab != null + var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) } val onPlay: (String, String, String, String?, String?, String?, Int?, Int?, String?, String?) -> Unit = { type, videoId, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail -> @@ -143,148 +162,119 @@ fun App() { containerColor = MaterialTheme.colorScheme.background, contentWindowInsets = WindowInsets(0), bottomBar = { - if (showBottomBar) { - NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, - windowInsets = WindowInsets(0), - ) { - AppScreenTab.entries.forEach { tab -> - NavigationBarItem( - selected = currentTab == tab, - onClick = { navController.navigateToTab(tab) }, - icon = { - Icon( - imageVector = tab.icon(), - contentDescription = null, - ) - }, - label = { Text(tab.label()) }, - ) - } - } + NavigationBar( + containerColor = MaterialTheme.colorScheme.surface, + windowInsets = WindowInsets(0), + ) { + NavigationBarItem( + selected = selectedTab == AppScreenTab.Home, + onClick = { selectedTab = AppScreenTab.Home }, + icon = { Icon(Icons.Rounded.Home, contentDescription = null) }, + label = { Text("Home") }, + ) + NavigationBarItem( + selected = selectedTab == AppScreenTab.Search, + onClick = { selectedTab = AppScreenTab.Search }, + icon = { Icon(Icons.Rounded.Search, contentDescription = null) }, + label = { Text("Search") }, + ) + NavigationBarItem( + selected = selectedTab == AppScreenTab.Settings, + onClick = { selectedTab = AppScreenTab.Settings }, + icon = { Icon(Icons.Rounded.Settings, contentDescription = null) }, + label = { Text("Settings") }, + ) } }, ) { innerPadding -> - NavHost( - navController = navController, - startDestination = HomeRoute, + Box( modifier = Modifier .fillMaxSize() .padding(innerPadding), ) { - composable { - HomeScreen( - modifier = Modifier.fillMaxSize(), + AppScreenTab.entries.forEach { tab -> + val tabAlpha by animateFloatAsState( + targetValue = if (selectedTab == tab) 1f else 0f, + animationSpec = tween(durationMillis = 220), + label = "tab_alpha_${tab.name}", + ) + AppScreen( + tab = tab, + modifier = Modifier + .fillMaxSize() + .alpha(tabAlpha) + .zIndex(if (selectedTab == tab) 1f else 0f), onCatalogClick = onCatalogClick, onPosterClick = { meta -> navController.navigate(DetailRoute(type = meta.type, id = meta.id)) }, ) } - composable { - SearchScreen( - modifier = Modifier.fillMaxSize(), - onPosterClick = { meta -> - navController.navigate(DetailRoute(type = meta.type, id = meta.id)) - }, - ) - } - composable { - SettingsScreen( - modifier = Modifier.fillMaxSize(), - ) - } - composable { backStackEntry -> - val route = backStackEntry.toRoute() - MetaDetailsScreen( - type = route.type, - id = route.id, - onBack = { - MetaDetailsRepository.clear() - navController.popBackStack() - }, - onPlay = onPlay, - modifier = Modifier.fillMaxSize(), - ) - } - composable { backStackEntry -> - val route = backStackEntry.toRoute() - StreamsScreen( - type = route.type, - videoId = route.videoId, - title = route.title, - logo = route.logo, - poster = route.poster, - background = route.background, - seasonNumber = route.seasonNumber, - episodeNumber = route.episodeNumber, - episodeTitle = route.episodeTitle, - episodeThumbnail = route.episodeThumbnail, - onBack = { - StreamsRepository.clear() - navController.popBackStack() - }, - modifier = Modifier.fillMaxSize(), - ) - } - composable { backStackEntry -> - val route = backStackEntry.toRoute() - CatalogScreen( - title = route.title, - subtitle = route.subtitle, - manifestUrl = route.manifestUrl, - type = route.type, - catalogId = route.catalogId, - supportsPagination = route.supportsPagination, - genre = route.genre, - onBack = { - CatalogRepository.clear() - navController.popBackStack() - }, - onPosterClick = { meta -> - navController.navigate(DetailRoute(type = meta.type, id = meta.id)) - }, - modifier = Modifier.fillMaxSize(), - ) - } + } + } + + NavHost( + navController = navController, + startDestination = TabsRoute, + modifier = Modifier.fillMaxSize(), + ) { + composable { + Unit + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + MetaDetailsScreen( + type = route.type, + id = route.id, + onBack = { + MetaDetailsRepository.clear() + navController.popBackStack() + }, + onPlay = onPlay, + modifier = Modifier.fillMaxSize(), + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + StreamsScreen( + type = route.type, + videoId = route.videoId, + title = route.title, + logo = route.logo, + poster = route.poster, + background = route.background, + seasonNumber = route.seasonNumber, + episodeNumber = route.episodeNumber, + episodeTitle = route.episodeTitle, + episodeThumbnail = route.episodeThumbnail, + onBack = { + StreamsRepository.clear() + navController.popBackStack() + }, + modifier = Modifier.fillMaxSize(), + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + CatalogScreen( + title = route.title, + subtitle = route.subtitle, + manifestUrl = route.manifestUrl, + type = route.type, + catalogId = route.catalogId, + supportsPagination = route.supportsPagination, + genre = route.genre, + onBack = { + CatalogRepository.clear() + navController.popBackStack() + }, + onPosterClick = { meta -> + navController.navigate(DetailRoute(type = meta.type, id = meta.id)) + }, + modifier = Modifier.fillMaxSize(), + ) } } } } } - -private fun AppScreenTab.matches(destination: NavDestination?): Boolean = - when (this) { - AppScreenTab.Home -> destination?.hasRoute() == true - AppScreenTab.Search -> destination?.hasRoute() == true - AppScreenTab.Settings -> destination?.hasRoute() == true - } - -private fun AppScreenTab.icon() = - when (this) { - AppScreenTab.Home -> Icons.Rounded.Home - AppScreenTab.Search -> Icons.Rounded.Search - AppScreenTab.Settings -> Icons.Rounded.Settings - } - -private fun AppScreenTab.label(): String = - when (this) { - AppScreenTab.Home -> "Home" - AppScreenTab.Search -> "Search" - AppScreenTab.Settings -> "Settings" - } - -private fun androidx.navigation.NavHostController.navigateToTab(tab: AppScreenTab) { - val route = when (tab) { - AppScreenTab.Home -> HomeRoute - AppScreenTab.Search -> SearchRoute - AppScreenTab.Settings -> SettingsRoute - } - navigate(route) { - launchSingleTop = true - restoreState = true - popUpTo(graph.findStartDestination().id) { - saveState = true - } - } -}