refactor: replace NavigationDrawer with NavigationBar on HomeScreen

Converts the mobile home screen from a ModalNavigationDrawer to a
bottom NavigationBar with three tabs: Home, Libraries, and Downloads.
This commit is contained in:
2026-02-21 11:30:29 +01:00
parent 7d5eeaf7fa
commit b21454c764
5 changed files with 147 additions and 257 deletions

View File

@@ -2,31 +2,33 @@ package hu.bbara.purefin.app.home
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Movie import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Tv import androidx.compose.material.icons.outlined.Tv
import androidx.compose.material3.DrawerValue import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.NavigationBar
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import hu.bbara.purefin.app.home.ui.DownloadsContent
import hu.bbara.purefin.app.home.ui.HomeContent import hu.bbara.purefin.app.home.ui.HomeContent
import hu.bbara.purefin.app.home.ui.HomeDrawerContent
import hu.bbara.purefin.app.home.ui.HomeMockData
import hu.bbara.purefin.app.home.ui.HomeNavItem import hu.bbara.purefin.app.home.ui.HomeNavItem
import hu.bbara.purefin.app.home.ui.HomeTopBar import hu.bbara.purefin.app.home.ui.HomeTopBar
import hu.bbara.purefin.app.home.ui.LibrariesContent
import hu.bbara.purefin.feature.shared.home.HomePageViewModel import hu.bbara.purefin.feature.shared.home.HomePageViewModel
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
@Composable @Composable
@@ -34,8 +36,7 @@ fun HomePage(
viewModel: HomePageViewModel = hiltViewModel(), viewModel: HomePageViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val drawerState = rememberDrawerState(DrawerValue.Closed) var selectedTab by remember { mutableIntStateOf(0) }
val coroutineScope = rememberCoroutineScope()
val libraries = viewModel.libraries.collectAsState().value val libraries = viewModel.libraries.collectAsState().value
val isOfflineMode = viewModel.isOfflineMode.collectAsState().value val isOfflineMode = viewModel.isOfflineMode.collectAsState().value
@@ -59,41 +60,41 @@ fun HomePage(
onPauseOrDispose { } onPauseOrDispose { }
} }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet(
modifier = Modifier
.width(280.dp)
.fillMaxSize(),
drawerContainerColor = MaterialTheme.colorScheme.surface,
drawerContentColor = MaterialTheme.colorScheme.onBackground
) {
HomeDrawerContent(
title = "Jellyfin",
subtitle = "Library Dashboard",
primaryNavItems = libraryNavItems,
secondaryNavItems = HomeMockData.secondaryNavItems,
user = HomeMockData.user,
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
onLogout = viewModel::logout
)
}
}
) {
Scaffold( Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
topBar = { topBar = {
HomeTopBar( HomeTopBar(
onMenuClick = { coroutineScope.launch { drawerState.open() } },
isOfflineMode = isOfflineMode, isOfflineMode = isOfflineMode,
onToggleOfflineMode = viewModel::toggleOfflineMode onToggleOfflineMode = viewModel::toggleOfflineMode
) )
},
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = selectedTab == 0,
onClick = { selectedTab = 0 },
icon = { Icon(Icons.Outlined.Home, contentDescription = "Home") },
label = { Text("Home") }
)
NavigationBarItem(
selected = selectedTab == 1,
onClick = { selectedTab = 1 },
icon = { Icon(Icons.Outlined.Collections, contentDescription = "Libraries") },
label = { Text("Libraries") }
)
NavigationBarItem(
selected = selectedTab == 2,
onClick = { selectedTab = 2 },
icon = { Icon(Icons.Outlined.Download, contentDescription = "Downloads") },
label = { Text("Downloads") }
)
}
} }
) { innerPadding -> ) { innerPadding ->
HomeContent( when (selectedTab) {
0 -> HomeContent(
libraries = libraries, libraries = libraries,
libraryContent = latestLibraryContent.value, libraryContent = latestLibraryContent.value,
continueWatching = continueWatching.value, continueWatching = continueWatching.value,
@@ -103,6 +104,14 @@ fun HomePage(
onEpisodeSelected = viewModel::onEpisodeSelected, onEpisodeSelected = viewModel::onEpisodeSelected,
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) )
1 -> LibrariesContent(
items = libraryNavItems,
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
modifier = Modifier.padding(innerPadding)
)
2 -> DownloadsContent(
modifier = Modifier.padding(innerPadding)
)
} }
} }
} }

View File

@@ -0,0 +1,39 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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
@Composable
fun DownloadsContent(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Outlined.Download,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "No downloads yet",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
)
}
}

View File

@@ -1,204 +0,0 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun HomeDrawerContent(
title: String,
subtitle: String,
primaryNavItems: List<HomeNavItem>,
secondaryNavItems: List<HomeNavItem>,
user: HomeUser,
onLibrarySelected: (HomeNavItem) -> Unit,
onLogout: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxSize()) {
HomeDrawerHeader(
title = title,
subtitle = subtitle
)
HomeDrawerNav(
primaryItems = primaryNavItems,
secondaryItems = secondaryNavItems,
onLibrarySelected = onLibrarySelected
)
Spacer(modifier = Modifier.weight(1f))
HomeDrawerFooter(user = user, onLogout = onLogout)
}
}
@Composable
fun HomeDrawerHeader(
title: String,
subtitle: String,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 16.dp, top = 24.dp, bottom = 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.size(40.dp)
.background(scheme.primary, RoundedCornerShape(12.dp)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = scheme.onPrimary
)
}
Column(modifier = Modifier.padding(start = 12.dp)) {
Text(
text = title,
color = scheme.onBackground,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
Text(
text = subtitle,
color = scheme.onSurfaceVariant,
fontSize = 12.sp
)
}
}
HorizontalDivider(color = scheme.onSurfaceVariant.copy(alpha = 0.2f))
}
@Composable
fun HomeDrawerNav(
primaryItems: List<HomeNavItem>,
secondaryItems: List<HomeNavItem>,
onLibrarySelected: (HomeNavItem) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
primaryItems.forEach { item ->
HomeDrawerNavItem(item = item, onLibrarySelected = onLibrarySelected)
}
if (secondaryItems.isNotEmpty()) {
HorizontalDivider(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
secondaryItems.forEach { item ->
HomeDrawerNavItem(item = item, onLibrarySelected = onLibrarySelected)
}
}
}
}
@Composable
fun HomeDrawerNavItem(
item: HomeNavItem,
modifier: Modifier = Modifier,
onLibrarySelected: (HomeNavItem) -> Unit
) {
val scheme = MaterialTheme.colorScheme
val background = if (item.selected) scheme.primary.copy(alpha = 0.12f) else Color.Transparent
val tint = if (item.selected) scheme.primary else scheme.onSurfaceVariant
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.background(background, RoundedCornerShape(12.dp))
.clickable { onLibrarySelected(item) }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = item.icon,
contentDescription = item.label,
tint = tint
)
Text(
text = item.label,
color = if (item.selected) scheme.primary else scheme.onBackground,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 12.dp)
)
}
}
@Composable
fun HomeDrawerFooter (
user: HomeUser,
onLogout: () -> Unit,
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.background(scheme.surfaceVariant, RoundedCornerShape(12.dp))
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
HomeAvatar(
size = 32.dp,
borderWidth = 1.dp,
borderColor = scheme.outlineVariant,
backgroundColor = scheme.primaryContainer,
icon = Icons.Outlined.Person,
iconTint = scheme.onBackground
)
Column(modifier = Modifier.padding(start = 12.dp)
.clickable { onLogout() }) {
Text(
text = user.name,
color = scheme.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = user.plan,
color = scheme.onSurfaceVariant,
fontSize = 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}

View File

@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cloud import androidx.compose.material.icons.outlined.Cloud
import androidx.compose.material.icons.outlined.CloudOff import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -22,7 +21,6 @@ import hu.bbara.purefin.common.ui.components.SearchField
@Composable @Composable
fun HomeTopBar( fun HomeTopBar(
onMenuClick: () -> Unit,
isOfflineMode: Boolean, isOfflineMode: Boolean,
onToggleOfflineMode: () -> Unit, onToggleOfflineMode: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -43,11 +41,6 @@ fun HomeTopBar(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
) { ) {
PurefinIconButton(
icon = Icons.Outlined.Menu,
contentDescription = "Menu",
onClick = onMenuClick,
)
SearchField( SearchField(
value = "", value = "",
onValueChange = {}, onValueChange = {},
@@ -65,5 +58,3 @@ fun HomeTopBar(
} }
} }
} }

View File

@@ -0,0 +1,55 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun LibrariesContent(
items: List<HomeNavItem>,
onLibrarySelected: (HomeNavItem) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
items(items, key = { it.id }) { item ->
ListItem(
headlineContent = {
Text(
text = item.label,
style = MaterialTheme.typography.titleMedium
)
},
leadingContent = {
Icon(
imageVector = item.icon,
contentDescription = item.label,
tint = MaterialTheme.colorScheme.primary
)
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(12.dp))
.clickable { onLibrarySelected(item) }
)
}
}
}