diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt index 12983b36..568b22ed 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt @@ -1,34 +1,52 @@ package hu.bbara.purefin.ui.screen +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.IntOffset import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.scene.Scene +import androidx.navigation3.ui.NavDisplay import hu.bbara.purefin.feature.browse.home.AppViewModel +import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.ui.screen.download.DownloadsScreen import hu.bbara.purefin.ui.screen.home.HomeScreen import hu.bbara.purefin.ui.screen.home.components.HomeNavItem import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen +import kotlinx.serialization.Serializable +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith @Composable fun AppScreen( viewModel: AppViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - var selectedTab by remember { mutableIntStateOf(0) } - val libraries by viewModel.libraries.collectAsState() val libraryContent by viewModel.latestLibraryContent.collectAsState() val suggestions by viewModel.suggestions.collectAsState() val continueWatching by viewModel.continueWatching.collectAsState() val nextUp by viewModel.nextUp.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState() + val navigationManager = LocalNavigationManager.current + + @Suppress("UNCHECKED_CAST") + val backStack = rememberNavBackStack(AppTabRoute.Home) as NavBackStack + val currentRoute = backStack.lastOrNull() ?: AppTabRoute.Home + val selectedTab = currentRoute.toTabIndex() val libraryNavItems = libraries.map { HomeNavItem( @@ -43,43 +61,131 @@ fun AppScreen( onPauseOrDispose { } } - when (selectedTab) { - 0 -> HomeScreen( - libraries = libraries, - libraryContent = libraryContent, - suggestions = suggestions, - continueWatching = continueWatching, - nextUp = nextUp, - isRefreshing = isRefreshing, - onRefresh = viewModel::onRefresh, - onMediaSelected = viewModel::onMediaSelected, - onLibrarySelected = { library -> - viewModel.onLibrarySelected( - library.id, - library.name - ) - }, - onProfileClick = {}, - onSettingsClick = {}, - onLogoutClick = viewModel::logout, - selectedTab = selectedTab, - onTabSelected = { selectedTab = it }, - modifier = modifier.fillMaxSize() - ) - 1 -> LibrariesScreen( - items = libraryNavItems, - onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) }, - onProfileClick = {}, - onSettingsClick = {}, - onLogoutClick = viewModel::logout, - selectedTab = selectedTab, - onTabSelected = { selectedTab = it }, - modifier = modifier.fillMaxSize() - ) - 2 -> DownloadsScreen( - selectedTab = selectedTab, - onTabSelected = { selectedTab = it }, - modifier = modifier.fillMaxSize() - ) + val onTabSelected = remember(backStack) { + { selectedIndex: Int -> + val route = selectedIndex.toAppTabRoute() + if (backStack.lastOrNull() != route) { + backStack.add(route) + } + } + } + + val tabEntryProvider = entryProvider { + entry(metadata = appTabMetadata(AppTabRoute.Home)) { + HomeScreen( + libraries = libraries, + libraryContent = libraryContent, + suggestions = suggestions, + continueWatching = continueWatching, + nextUp = nextUp, + isRefreshing = isRefreshing, + onRefresh = viewModel::onRefresh, + onMediaSelected = viewModel::onMediaSelected, + onLibrarySelected = { library -> + viewModel.onLibrarySelected( + library.id, + library.name + ) + }, + onProfileClick = {}, + onSettingsClick = {}, + onLogoutClick = viewModel::logout, + selectedTab = selectedTab, + onTabSelected = onTabSelected, + modifier = Modifier.fillMaxSize() + ) + } + entry(metadata = appTabMetadata(AppTabRoute.Libraries)) { + LibrariesScreen( + items = libraryNavItems, + onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) }, + onProfileClick = {}, + onSettingsClick = {}, + onLogoutClick = viewModel::logout, + selectedTab = selectedTab, + onTabSelected = onTabSelected, + modifier = Modifier.fillMaxSize() + ) + } + entry(metadata = appTabMetadata(AppTabRoute.Downloads)) { + DownloadsScreen( + selectedTab = selectedTab, + onTabSelected = onTabSelected, + modifier = Modifier.fillMaxSize() + ) + } + } + + NavDisplay( + backStack = backStack, + onBack = { + if (backStack.size > 1) { + backStack.removeLastOrNull() + } else { + navigationManager.pop() + } + }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator() + ), + transitionSpec = { appTabTransition() }, + popTransitionSpec = { appTabTransition() }, + predictivePopTransitionSpec = { _ -> appTabTransition() }, + entryProvider = tabEntryProvider, + modifier = modifier.fillMaxSize() + ) +} + +@Serializable +private sealed interface AppTabRoute : NavKey { + @Serializable + data object Home : AppTabRoute + + @Serializable + data object Libraries : AppTabRoute + + @Serializable + data object Downloads : AppTabRoute +} + +private fun AppTabRoute.toTabIndex(): Int = when (this) { + AppTabRoute.Home -> 0 + AppTabRoute.Libraries -> 1 + AppTabRoute.Downloads -> 2 +} + +private fun Int.toAppTabRoute(): AppTabRoute = when (this) { + 0 -> AppTabRoute.Home + 1 -> AppTabRoute.Libraries + 2 -> AppTabRoute.Downloads + else -> AppTabRoute.Home +} + +private fun AnimatedContentTransitionScope>.appTabTransition(): ContentTransform { + val initialIndex = initialState.metadata.appTabIndex() + val targetIndex = targetState.metadata.appTabIndex() + val animationSpec = tween(durationMillis = 140) + + return when { + targetIndex > initialIndex -> { + slideInHorizontally(animationSpec = animationSpec) { fullWidth -> fullWidth } togetherWith + slideOutHorizontally(animationSpec = animationSpec) { fullWidth -> -fullWidth } + } + targetIndex < initialIndex -> { + slideInHorizontally(animationSpec = animationSpec) { fullWidth -> -fullWidth } togetherWith + slideOutHorizontally(animationSpec = animationSpec) { fullWidth -> fullWidth } + } + else -> { + slideInHorizontally(animationSpec = animationSpec) { 0 } togetherWith + slideOutHorizontally(animationSpec = animationSpec) { 0 } + } } } + +private fun appTabMetadata(route: AppTabRoute): Map = + mapOf(APP_TAB_INDEX_METADATA to route.toTabIndex()) + +private fun Map.appTabIndex(): Int = + this[APP_TAB_INDEX_METADATA] as? Int ?: 0 + +private const val APP_TAB_INDEX_METADATA = "app_tab_index"