mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
Change Navigation Drawler to Top Tabs
This commit is contained in:
@@ -2,102 +2,124 @@ package hu.bbara.purefin.tv.home
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.Search
|
||||
import androidx.compose.material.icons.outlined.Tv
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import hu.bbara.purefin.feature.shared.home.AppViewModel
|
||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeDrawerContent
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeMockData
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeNavItem
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeTabDestination
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeTabItem
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeTopBar
|
||||
import kotlinx.coroutines.launch
|
||||
import hu.bbara.purefin.tv.library.ui.TvLibraryContent
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
|
||||
@Composable
|
||||
fun TvHomePage(
|
||||
viewModel: AppViewModel = hiltViewModel(),
|
||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectedTabIndex by remember { mutableIntStateOf(1) }
|
||||
val libraries by viewModel.libraries.collectAsState()
|
||||
val isOfflineMode by viewModel.isOfflineMode.collectAsState()
|
||||
val continueWatching by viewModel.continueWatching.collectAsState()
|
||||
val nextUp by viewModel.nextUp.collectAsState()
|
||||
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
|
||||
val selectedLibraryItems by libraryViewModel.contents.collectAsState()
|
||||
|
||||
val libraries = viewModel.libraries.collectAsState().value
|
||||
val isOfflineMode = viewModel.isOfflineMode.collectAsState().value
|
||||
val libraryNavItems = libraries.map {
|
||||
TvHomeNavItem(
|
||||
id = it.id,
|
||||
val tabs = remember(libraries) {
|
||||
buildList {
|
||||
add(
|
||||
TvHomeTabItem(
|
||||
destination = TvHomeTabDestination.SEARCH,
|
||||
label = "Search",
|
||||
icon = Icons.Outlined.Search,
|
||||
)
|
||||
)
|
||||
add(
|
||||
TvHomeTabItem(
|
||||
destination = TvHomeTabDestination.HOME,
|
||||
label = "Home",
|
||||
icon = Icons.Outlined.Home,
|
||||
)
|
||||
)
|
||||
addAll(libraries.map {
|
||||
TvHomeTabItem(
|
||||
destination = TvHomeTabDestination.LIBRARY,
|
||||
label = it.name,
|
||||
icon = when (it.type) {
|
||||
CollectionType.MOVIES -> Icons.Outlined.Movie
|
||||
CollectionType.TVSHOWS -> Icons.Outlined.Tv
|
||||
else -> Icons.Outlined.Collections
|
||||
},
|
||||
libraryId = it.id
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
val continueWatching = viewModel.continueWatching.collectAsState()
|
||||
val nextUp = viewModel.nextUp.collectAsState()
|
||||
val latestLibraryContent = viewModel.latestLibraryContent.collectAsState()
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
viewModel.onResumed()
|
||||
onPauseOrDispose { }
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier
|
||||
.width(280.dp)
|
||||
.fillMaxSize(),
|
||||
drawerContainerColor = MaterialTheme.colorScheme.surface,
|
||||
drawerContentColor = MaterialTheme.colorScheme.onBackground
|
||||
) {
|
||||
TvHomeDrawerContent(
|
||||
title = "Jellyfin",
|
||||
subtitle = "Library Dashboard",
|
||||
primaryNavItems = libraryNavItems,
|
||||
secondaryNavItems = TvHomeMockData.secondaryNavItems,
|
||||
user = TvHomeMockData.user,
|
||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
||||
onLogout = viewModel::logout
|
||||
)
|
||||
val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, (tabs.size - 1).coerceAtLeast(0))
|
||||
val selectedTab = tabs.getOrNull(safeSelectedTabIndex)
|
||||
|
||||
LaunchedEffect(selectedTab?.destination, selectedTab?.libraryId) {
|
||||
if (selectedTab?.destination == TvHomeTabDestination.LIBRARY) {
|
||||
val libraryId = selectedTab.libraryId ?: return@LaunchedEffect
|
||||
libraryViewModel.selectLibrary(libraryId)
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
topBar = {
|
||||
TvHomeTopBar(
|
||||
onMenuClick = { coroutineScope.launch { drawerState.open() } },
|
||||
tabs = tabs,
|
||||
selectedTabIndex = safeSelectedTabIndex,
|
||||
onTabSelected = { index, _ ->
|
||||
selectedTabIndex = index
|
||||
},
|
||||
isOfflineMode = isOfflineMode,
|
||||
onToggleOfflineMode = viewModel::toggleOfflineMode
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
when (selectedTab?.destination) {
|
||||
TvHomeTabDestination.LIBRARY -> {
|
||||
TvLibraryContent(
|
||||
libraryItems = selectedLibraryItems,
|
||||
onMovieSelected = viewModel::onMovieSelected,
|
||||
onSeriesSelected = viewModel::onSeriesSelected,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
|
||||
TvHomeTabDestination.SEARCH,
|
||||
TvHomeTabDestination.HOME,
|
||||
null -> {
|
||||
TvHomeContent(
|
||||
libraries = libraries,
|
||||
libraryContent = latestLibraryContent.value,
|
||||
continueWatching = continueWatching.value,
|
||||
nextUp = nextUp.value,
|
||||
libraryContent = latestLibraryContent,
|
||||
continueWatching = continueWatching,
|
||||
nextUp = nextUp,
|
||||
onMovieSelected = viewModel::onMovieSelected,
|
||||
onSeriesSelected = viewModel::onSeriesSelected,
|
||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||
@@ -105,4 +127,5 @@ fun TvHomePage(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,19 @@ data class TvHomeNavItem(
|
||||
val selected: Boolean = false
|
||||
)
|
||||
|
||||
enum class TvHomeTabDestination {
|
||||
SEARCH,
|
||||
HOME,
|
||||
LIBRARY
|
||||
}
|
||||
|
||||
data class TvHomeTabItem(
|
||||
val destination: TvHomeTabDestination,
|
||||
val label: String,
|
||||
val icon: ImageVector,
|
||||
val libraryId: UUID? = null
|
||||
)
|
||||
|
||||
data class TvHomeUser(
|
||||
val name: String,
|
||||
val plan: String
|
||||
|
||||
@@ -10,24 +10,31 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Cloud
|
||||
import androidx.compose.material.icons.outlined.CloudOff
|
||||
import androidx.compose.material.icons.outlined.Menu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
||||
import hu.bbara.purefin.common.ui.components.SearchField
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TvHomeTopBar(
|
||||
onMenuClick: () -> Unit,
|
||||
tabs: List<TvHomeTabItem>,
|
||||
selectedTabIndex: Int,
|
||||
onTabSelected: (Int, TvHomeTabItem) -> Unit,
|
||||
isOfflineMode: Boolean,
|
||||
onToggleOfflineMode: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, tabs.lastIndex.coerceAtLeast(0))
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -41,19 +48,33 @@ fun TvHomeTopBar(
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
PurefinIconButton(
|
||||
icon = Icons.Outlined.Menu,
|
||||
contentDescription = "Menu",
|
||||
onClick = onMenuClick,
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = safeSelectedTabIndex,
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = scheme.surface,
|
||||
contentColor = scheme.onSurface
|
||||
) {
|
||||
tabs.forEachIndexed { index, tab ->
|
||||
Tab(
|
||||
selected = index == safeSelectedTabIndex,
|
||||
onClick = { onTabSelected(index, tab) },
|
||||
text = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = tab.icon,
|
||||
contentDescription = null
|
||||
)
|
||||
SearchField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = "Search",
|
||||
modifier = Modifier.weight(1.0f, true),
|
||||
Text(text = tab.label)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
PurefinIconButton(
|
||||
icon = if (isOfflineMode) Icons.Outlined.CloudOff else Icons.Outlined.Cloud,
|
||||
contentDescription = if (isOfflineMode) "Switch to Online" else "Switch to Offline",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package hu.bbara.purefin.tv.library.ui
|
||||
|
||||
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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.common.ui.PosterCard
|
||||
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
||||
import hu.bbara.purefin.core.data.navigation.LibraryDto
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
|
||||
@Composable
|
||||
fun TvLibraryScreen(
|
||||
library: LibraryDto,
|
||||
viewModel: LibraryViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LaunchedEffect(library) {
|
||||
viewModel.selectLibrary(libraryId = library.id)
|
||||
}
|
||||
|
||||
val libraryItems = viewModel.contents.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TvLibraryTopBar(
|
||||
title = library.name,
|
||||
onBack = viewModel::onBack
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
TvLibraryContent(
|
||||
libraryItems = libraryItems.value,
|
||||
onMovieSelected = viewModel::onMovieSelected,
|
||||
onSeriesSelected = viewModel::onSeriesSelected,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvLibraryTopBar(
|
||||
title: String,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
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
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvLibraryContent(
|
||||
libraryItems: List<PosterItem>,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onSeriesSelected: (UUID) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 140.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
items(libraryItems) { item ->
|
||||
PosterCard(
|
||||
item = item,
|
||||
onMovieSelected = onMovieSelected,
|
||||
onSeriesSelected = onSeriesSelected,
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,10 @@ object TvNavigationModule {
|
||||
fun provideTvPlayerEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||
tvPlayerSection()
|
||||
}
|
||||
|
||||
@IntoSet
|
||||
@Provides
|
||||
fun provideTvLibraryEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||
tvLibrarySection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.login.ui.LoginScreen
|
||||
import hu.bbara.purefin.tv.home.TvHomePage
|
||||
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen
|
||||
import hu.bbara.purefin.tv.player.TvPlayerScreen
|
||||
|
||||
fun EntryProviderScope<Route>.tvHomeSection() {
|
||||
@@ -49,3 +50,9 @@ fun EntryProviderScope<Route>.tvPlayerSection() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun EntryProviderScope<Route>.tvLibrarySection() {
|
||||
entry<Route.LibraryRoute> { route ->
|
||||
TvLibraryScreen(library = route.library)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user