mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +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
|
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.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
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.feature.browse.home.AppViewModel
|
||||||
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||||
import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen
|
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
|
@Composable
|
||||||
fun AppScreen(
|
fun AppScreen(
|
||||||
viewModel: AppViewModel = hiltViewModel(),
|
viewModel: AppViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
|
||||||
|
|
||||||
val libraries by viewModel.libraries.collectAsState()
|
val libraries by viewModel.libraries.collectAsState()
|
||||||
val libraryContent by viewModel.latestLibraryContent.collectAsState()
|
val libraryContent by viewModel.latestLibraryContent.collectAsState()
|
||||||
val suggestions by viewModel.suggestions.collectAsState()
|
val suggestions by viewModel.suggestions.collectAsState()
|
||||||
val continueWatching by viewModel.continueWatching.collectAsState()
|
val continueWatching by viewModel.continueWatching.collectAsState()
|
||||||
val nextUp by viewModel.nextUp.collectAsState()
|
val nextUp by viewModel.nextUp.collectAsState()
|
||||||
val isRefreshing by viewModel.isRefreshing.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 {
|
val libraryNavItems = libraries.map {
|
||||||
HomeNavItem(
|
HomeNavItem(
|
||||||
@@ -43,8 +61,18 @@ fun AppScreen(
|
|||||||
onPauseOrDispose { }
|
onPauseOrDispose { }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (selectedTab) {
|
val onTabSelected = remember(backStack) {
|
||||||
0 -> HomeScreen(
|
{ 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,
|
libraries = libraries,
|
||||||
libraryContent = libraryContent,
|
libraryContent = libraryContent,
|
||||||
suggestions = suggestions,
|
suggestions = suggestions,
|
||||||
@@ -63,23 +91,101 @@ fun AppScreen(
|
|||||||
onSettingsClick = {},
|
onSettingsClick = {},
|
||||||
onLogoutClick = viewModel::logout,
|
onLogoutClick = viewModel::logout,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = onTabSelected,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
1 -> LibrariesScreen(
|
}
|
||||||
|
entry<AppTabRoute.Libraries>(metadata = appTabMetadata(AppTabRoute.Libraries)) {
|
||||||
|
LibrariesScreen(
|
||||||
items = libraryNavItems,
|
items = libraryNavItems,
|
||||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
||||||
onProfileClick = {},
|
onProfileClick = {},
|
||||||
onSettingsClick = {},
|
onSettingsClick = {},
|
||||||
onLogoutClick = viewModel::logout,
|
onLogoutClick = viewModel::logout,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = onTabSelected,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
|
||||||
2 -> DownloadsScreen(
|
|
||||||
selectedTab = selectedTab,
|
|
||||||
onTabSelected = { selectedTab = it },
|
|
||||||
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