mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
Refactor app navigation to use Navigation3
Integrate `navigation3` into `AppScreen` to manage tab transitions and backstack state. Replaced manual tab state management with `NavDisplay` and `NavBackStack`, and added horizontal slide animations for switching between Home, Libraries, and Downloads screens.
This commit is contained in:
@@ -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<AppTabRoute>
|
||||
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<AppTabRoute.Home>(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<AppTabRoute.Libraries>(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<AppTabRoute.Downloads>(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<Scene<AppTabRoute>>.appTabTransition(): ContentTransform {
|
||||
val initialIndex = initialState.metadata.appTabIndex()
|
||||
val targetIndex = targetState.metadata.appTabIndex()
|
||||
val animationSpec = tween<IntOffset>(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<String, Any> =
|
||||
mapOf(APP_TAB_INDEX_METADATA to route.toTabIndex())
|
||||
|
||||
private fun Map<String, Any>.appTabIndex(): Int =
|
||||
this[APP_TAB_INDEX_METADATA] as? Int ?: 0
|
||||
|
||||
private const val APP_TAB_INDEX_METADATA = "app_tab_index"
|
||||
|
||||
Reference in New Issue
Block a user