mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat: integrate dynamic library navigation in TV drawer
Refactors the TV navigation drawer to use specific library routes instead of a static libraries overview. Added a `NavDisplay` with a backstack to manage navigation between the Home screen and individual Library screens directly from the drawer. - Replace `TvDrawerDestination` enum with `Route` in the navigation drawer - Dynamically populate drawer items based on available libraries - Add `NavDisplay` to `TvAppScreen` for nested TV navigation - Connect `TvLibraryScreen` to `AppViewModel` for media selection handling - Remove back button from `TvLibraryScreen` in favor of drawer navigation - Update `TvNavigationDrawerTest` to reflect routing changes
This commit is contained in:
@@ -24,9 +24,12 @@ import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.navigation.LibraryDto
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class TvNavigationDrawerTest {
|
||||
@@ -56,20 +59,26 @@ class TvNavigationDrawerTest {
|
||||
|
||||
@Test
|
||||
fun tvNavigationDrawer_updatesSelectedDestination() {
|
||||
var selectedDestination by mutableStateOf(TvDrawerDestination.HOME)
|
||||
val libraryRoute = Route.LibraryRoute(
|
||||
library = LibraryDto(
|
||||
id = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
||||
name = "Movies"
|
||||
)
|
||||
)
|
||||
var selectedDestination by mutableStateOf<Route>(Route.Home)
|
||||
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
TvNavigationDrawer(
|
||||
destinations = listOf(
|
||||
TvDrawerDestinationItem(
|
||||
destination = TvDrawerDestination.HOME,
|
||||
destination = Route.Home,
|
||||
label = "Home",
|
||||
icon = Icons.Outlined.Home
|
||||
),
|
||||
TvDrawerDestinationItem(
|
||||
destination = TvDrawerDestination.LIBRARIES,
|
||||
label = "Libraries",
|
||||
destination = libraryRoute,
|
||||
label = "Movies",
|
||||
icon = Icons.Outlined.Collections
|
||||
)
|
||||
),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package hu.bbara.purefin.navigation
|
||||
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||
import hu.bbara.purefin.ui.screen.episode.TvEpisodeScreen
|
||||
import hu.bbara.purefin.ui.screen.movie.TvMovieScreen
|
||||
import hu.bbara.purefin.ui.screen.series.TvSeriesScreen
|
||||
@@ -51,6 +53,10 @@ fun EntryProviderScope<Route>.tvPlayerSection() {
|
||||
|
||||
fun EntryProviderScope<Route>.tvLibrarySection() {
|
||||
entry<Route.LibraryRoute> { route ->
|
||||
TvLibraryScreen(library = route.library)
|
||||
val viewModel: AppViewModel = hiltViewModel()
|
||||
TvLibraryScreen(
|
||||
library = route.library,
|
||||
onMediaSelected = viewModel::onMediaSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package hu.bbara.purefin.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Movie
|
||||
import androidx.compose.material.icons.outlined.Tv
|
||||
@@ -10,18 +9,24 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import hu.bbara.purefin.navigation.LibraryDto
|
||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.ui.screen.home.TvHomeScreen
|
||||
import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestination
|
||||
import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestinationItem
|
||||
import hu.bbara.purefin.ui.screen.home.components.TvNavigationDrawer
|
||||
import hu.bbara.purefin.ui.screen.libraries.TvLibrariesOverviewScreen
|
||||
import hu.bbara.purefin.ui.screen.library.TvLibraryScreen
|
||||
|
||||
@Composable
|
||||
fun TvAppScreen(
|
||||
@@ -32,45 +37,41 @@ fun TvAppScreen(
|
||||
val continueWatching by viewModel.continueWatching.collectAsState()
|
||||
val nextUp by viewModel.nextUp.collectAsState()
|
||||
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
|
||||
val navigationManager = LocalNavigationManager.current
|
||||
|
||||
var selectedDestination by rememberSaveable { androidx.compose.runtime.mutableStateOf(TvDrawerDestination.HOME) }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
|
||||
val selectedDestination = backStack.lastOrNull() ?: Route.Home
|
||||
|
||||
val destinations = remember(libraries, selectedDestination) {
|
||||
val destinations = remember(libraries) {
|
||||
listOf(
|
||||
TvDrawerDestinationItem(
|
||||
destination = TvDrawerDestination.HOME,
|
||||
destination = Route.Home,
|
||||
label = "Home",
|
||||
icon = Icons.Outlined.Home,
|
||||
selected = selectedDestination == TvDrawerDestination.HOME
|
||||
),
|
||||
icon = Icons.Outlined.Home
|
||||
)
|
||||
) + libraries.map { library ->
|
||||
val destination = Route.LibraryRoute(
|
||||
library = LibraryDto(id = library.id, name = library.name)
|
||||
)
|
||||
TvDrawerDestinationItem(
|
||||
destination = TvDrawerDestination.LIBRARIES,
|
||||
label = "Libraries",
|
||||
icon = when {
|
||||
libraries.any { it.type == LibraryKind.MOVIES } -> Icons.Outlined.Movie
|
||||
libraries.any { it.type == LibraryKind.SERIES } -> Icons.Outlined.Tv
|
||||
else -> Icons.Outlined.Collections
|
||||
},
|
||||
selected = selectedDestination == TvDrawerDestination.LIBRARIES
|
||||
)
|
||||
destination = destination,
|
||||
label = library.name,
|
||||
icon = when (library.type) {
|
||||
LibraryKind.MOVIES -> Icons.Outlined.Movie
|
||||
LibraryKind.SERIES -> Icons.Outlined.Tv
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
viewModel.onResumed()
|
||||
onPauseOrDispose { }
|
||||
}
|
||||
|
||||
TvNavigationDrawer(
|
||||
destinations = destinations,
|
||||
selectedDestination = selectedDestination,
|
||||
onDestinationSelected = { destination ->
|
||||
selectedDestination = destination
|
||||
},
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
when (selectedDestination) {
|
||||
TvDrawerDestination.HOME -> {
|
||||
val tvEntryProvider = entryProvider {
|
||||
entry<Route.Home> {
|
||||
TvHomeScreen(
|
||||
libraries = libraries,
|
||||
libraryContent = latestLibraryContent,
|
||||
@@ -80,15 +81,44 @@ fun TvAppScreen(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
TvDrawerDestination.LIBRARIES -> {
|
||||
TvLibrariesOverviewScreen(
|
||||
libraries = libraries,
|
||||
onLibrarySelected = { library ->
|
||||
viewModel.onLibrarySelected(library.id, library.name)
|
||||
},
|
||||
entry<Route.LibraryRoute> { route ->
|
||||
TvLibraryScreen(
|
||||
library = route.library,
|
||||
onMediaSelected = viewModel::onMediaSelected,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TvNavigationDrawer(
|
||||
destinations = destinations,
|
||||
selectedDestination = selectedDestination,
|
||||
onDestinationSelected = { destination ->
|
||||
if (selectedDestination != destination) {
|
||||
backStack.clear()
|
||||
backStack.add(Route.Home)
|
||||
if (destination is Route.LibraryRoute) {
|
||||
backStack.add(destination)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = {
|
||||
if (backStack.size > 1) {
|
||||
backStack.removeLastOrNull()
|
||||
} else {
|
||||
navigationManager.pop()
|
||||
}
|
||||
},
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = tvEntryProvider,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
package hu.bbara.purefin.ui.screen.home.components
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
enum class TvDrawerDestination {
|
||||
HOME,
|
||||
LIBRARIES
|
||||
}
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
|
||||
data class TvDrawerDestinationItem(
|
||||
val destination: TvDrawerDestination,
|
||||
val destination: Route,
|
||||
val label: String,
|
||||
val icon: ImageVector,
|
||||
val selected: Boolean = false
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
@@ -30,9 +30,10 @@ import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.NavigationDrawer
|
||||
import androidx.tv.material3.NavigationDrawerItem
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.MaterialTheme as TvMaterialTheme
|
||||
import androidx.tv.material3.darkColorScheme
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.tv.R
|
||||
import androidx.tv.material3.MaterialTheme as TvMaterialTheme
|
||||
|
||||
internal const val TvDrawerItemTagPrefix = "tv-drawer-item-"
|
||||
internal const val TvDrawerTitleTag = "tv-drawer-title"
|
||||
@@ -43,8 +44,8 @@ private val TvDrawerExpandedWidth = 280.dp
|
||||
@Composable
|
||||
fun TvNavigationDrawer(
|
||||
destinations: List<TvDrawerDestinationItem>,
|
||||
selectedDestination: TvDrawerDestination,
|
||||
onDestinationSelected: (TvDrawerDestination) -> Unit,
|
||||
selectedDestination: Route,
|
||||
onDestinationSelected: (Route) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
@@ -132,15 +133,15 @@ fun TvDrawerHeader(
|
||||
private fun androidx.tv.material3.NavigationDrawerScope.TvNavigationDrawerRail(
|
||||
drawerValue: DrawerValue,
|
||||
destinations: List<TvDrawerDestinationItem>,
|
||||
selectedDestination: TvDrawerDestination,
|
||||
onDestinationSelected: (TvDrawerDestination) -> Unit,
|
||||
selectedDestination: Route,
|
||||
onDestinationSelected: (Route) -> Unit,
|
||||
) {
|
||||
val expanded = drawerValue == DrawerValue.Open
|
||||
val drawerWidth = animateDpAsState(
|
||||
targetValue = if (expanded) TvDrawerExpandedWidth else TvDrawerCollapsedWidth,
|
||||
label = "tv-drawer-width"
|
||||
)
|
||||
val scheme = androidx.tv.material3.MaterialTheme.colorScheme
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -4,15 +4,12 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
@@ -22,16 +19,15 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.navigation.LibraryDto
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||
import hu.bbara.purefin.navigation.LibraryDto
|
||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
|
||||
@Composable
|
||||
fun TvLibraryScreen(
|
||||
library: LibraryDto,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
viewModel: LibraryViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -45,15 +41,13 @@ fun TvLibraryScreen(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TvLibraryTopBar(
|
||||
title = library.name,
|
||||
onBack = viewModel::onBack
|
||||
title = library.name
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
TvLibraryContent(
|
||||
libraryItems = libraryItems.value,
|
||||
onMovieSelected = viewModel::onMovieSelected,
|
||||
onSeriesSelected = viewModel::onSeriesSelected,
|
||||
onMediaSelected = onMediaSelected,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
@@ -61,40 +55,23 @@ fun TvLibraryScreen(
|
||||
|
||||
@Composable
|
||||
private fun TvLibraryTopBar(
|
||||
title: String,
|
||||
onBack: () -> Unit
|
||||
title: String
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
PurefinIconButton(
|
||||
icon = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
onClick = onBack,
|
||||
focusedScale = 1.1f,
|
||||
focusedBorderWidth = 2.5.dp,
|
||||
focusedBorderColor = scheme.onPrimary,
|
||||
focusedBackgroundColor = scheme.primary
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvLibraryContent(
|
||||
libraryItems: List<MediaUiModel>,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onSeriesSelected: (UUID) -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -108,9 +85,9 @@ fun TvLibraryContent(
|
||||
items(libraryItems, key = { item -> item.id }) { item ->
|
||||
PosterCard(
|
||||
item = item,
|
||||
onMovieSelected = onMovieSelected,
|
||||
onSeriesSelected = onSeriesSelected,
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
onMovieSelected = { onMediaSelected(item) },
|
||||
onSeriesSelected = { onMediaSelected(item) },
|
||||
onEpisodeSelected = { _, _, _ -> onMediaSelected(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user