From d5602aa88c1b0dd0ae9cda6decd0aa4d0e09a56e Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:03:43 -0500 Subject: [PATCH] Navigation drawer improvements (#842) ## Description This updates the navigation drawer with better behavior and UI. It is also a bit more optimized and is slightly faster. - When open, content is pushed to the right instead of being covered - When open, it uses the same background colors from the dynamic background - The icons no longer shift as the drawer opens & closes - The open drawer is not as wide - Focus highlight on items is less circular, now matching the other menus in the app - The animation is a bit faster too Dev notes: - This PR copies & modifies `tv-material` code, so there's a customized `ModalNavigationDrawer` implementation now - There are fewer `AnimatedVisibility` composables which improves performance - Icons are exactly sized - Content pages generally shouldn't add start/end padding anymore, the padding is now added by the nav drawer (b4f1b111edcd7e7ec05edc1100402b2851721fcd) and is consistent across every page ### Related issues Closes #699 Closes #384 ### Testing Tested on the emulator, nvidia shield, & google sabrina ## Screenshots ![nav_drawer_closed Large](https://github.com/user-attachments/assets/9047eb45-6025-4992-8740-3430f0a8f2a9) ![nav_drawer_open_backdrop_dim Large](https://github.com/user-attachments/assets/880f24a5-8018-469e-805d-18060c641dc5) ## AI or LLM usage None --- .../damontecres/wholphin/ui/cards/ItemRow.kt | 3 +- .../wholphin/ui/detail/CardGrid.kt | 4 +- .../ui/detail/CollectionFolderBoxSet.kt | 6 +- .../ui/detail/CollectionFolderGeneric.kt | 6 +- .../ui/detail/CollectionFolderLiveTv.kt | 6 +- .../ui/detail/CollectionFolderMovie.kt | 6 +- .../ui/detail/CollectionFolderPhotoAlbum.kt | 6 +- .../ui/detail/CollectionFolderPlaylist.kt | 6 +- .../ui/detail/CollectionFolderRecordings.kt | 6 +- .../wholphin/ui/detail/CollectionFolderTv.kt | 5 +- .../wholphin/ui/detail/PersonPage.kt | 4 +- .../wholphin/ui/detail/PlaylistDetails.kt | 3 +- .../ui/detail/episode/EpisodeDetails.kt | 2 +- .../wholphin/ui/detail/movie/MovieDetails.kt | 2 +- .../ui/detail/series/SeriesDetails.kt | 2 +- .../ui/detail/series/SeriesOverviewContent.kt | 6 +- .../damontecres/wholphin/ui/main/HomePage.kt | 5 +- .../wholphin/ui/nav/ApplicationContent.kt | 16 +- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 128 +++++----- .../ui/nav/NavigationDrawerAndroid.kt | 229 ++++++++++++++++++ 20 files changed, 334 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt index 314a7e1a..7ebf0faf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -58,7 +57,7 @@ fun ItemRow( text = title, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier, ) LazyRow( state = state, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index f028ed1e..8893246c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -274,7 +274,7 @@ fun CardGrid( horizontalArrangement = Arrangement.spacedBy(spacing), verticalArrangement = Arrangement.spacedBy(spacing), state = gridState, - contentPadding = PaddingValues(16.dp), + contentPadding = PaddingValues(vertical = 16.dp), modifier = Modifier .fillMaxSize() @@ -394,7 +394,7 @@ fun CardGrid( modifier = Modifier .align(Alignment.CenterVertically) - .padding(end = 16.dp), + .padding(start = 16.dp), // Add end padding to push away from edge letterClicked = { letter -> scope.launch(ExceptionHandler()) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt index 91cd66f7..07c24c37 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt @@ -1,13 +1,11 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.preferences.UserPreferences @@ -40,9 +38,7 @@ fun CollectionFolderBoxSet( recursive = recursive, sortOptions = BoxSetSortOptions, initialSortAndDirection = SortAndDirection(ItemSortBy.DEFAULT, SortOrder.ASCENDING), - modifier = - modifier - .padding(start = 16.dp), + modifier = modifier, positionCallback = { columns, position -> showHeader = position < columns }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt index 800c84c5..e0c15c26 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt @@ -1,13 +1,11 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions import com.github.damontecres.wholphin.data.filter.ItemFilterBy @@ -53,9 +51,7 @@ fun CollectionFolderGeneric( showTitle = showHeader, recursive = recursive, sortOptions = sortOptions, - modifier = - modifier - .padding(start = 16.dp), + modifier = modifier, positionCallback = { columns, position -> showHeader = position < columns }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt index 6d1fdd02..dc426409 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -134,7 +134,7 @@ fun CollectionFolderLiveTv( selectedTabIndex = selectedTabIndex, modifier = Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .padding(vertical = 16.dp) .focusRequester(firstTabFocusRequester), tabs = tabs.map { it.title }, onClick = { selectedTabIndex = it }, @@ -176,9 +176,7 @@ fun CollectionFolderLiveTv( showTitle = false, recursive = false, sortOptions = VideoSortOptions, - modifier = - Modifier - .padding(start = 16.dp), + modifier = Modifier, positionCallback = { columns, position -> showHeader = position < columns }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index c47aeb23..96ddbe94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -83,7 +83,7 @@ fun CollectionFolderMovie( selectedTabIndex = selectedTabIndex, modifier = Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .padding(vertical = 16.dp) .focusRequester(firstTabFocusRequester), tabs = tabs, onClick = { selectedTabIndex = it }, @@ -101,7 +101,6 @@ fun CollectionFolderMovie( }, modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), ) @@ -129,7 +128,6 @@ fun CollectionFolderMovie( defaultViewOptions = ViewOptionsPoster, modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), positionCallback = { columns, position -> @@ -162,7 +160,6 @@ fun CollectionFolderMovie( defaultViewOptions = ViewOptionsPoster, modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), positionCallback = { columns, position -> @@ -180,7 +177,6 @@ fun CollectionFolderMovie( includeItemTypes = listOf(BaseItemKind.MOVIE), modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt index c36da909..fd4d46d3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt @@ -1,13 +1,11 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions import com.github.damontecres.wholphin.data.filter.ItemFilterBy @@ -74,9 +72,7 @@ fun CollectionFolderPhotoAlbum( showTitle = showHeader, recursive = recursive, sortOptions = sortOptions, - modifier = - modifier - .padding(start = 16.dp), + modifier = modifier, positionCallback = { columns, position -> showHeader = position < columns }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPlaylist.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPlaylist.kt index 66ace2d7..e7e19066 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPlaylist.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPlaylist.kt @@ -1,13 +1,11 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.preferences.UserPreferences @@ -35,9 +33,7 @@ fun CollectionFolderPlaylist( showTitle = showHeader, recursive = recursive, sortOptions = PlaylistSortOptions, - modifier = - modifier - .padding(start = 16.dp), + modifier = modifier, positionCallback = { columns, position -> showHeader = position < columns }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderRecordings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderRecordings.kt index 5362ddb5..a58db1b9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderRecordings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderRecordings.kt @@ -1,13 +1,11 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.preferences.UserPreferences @@ -35,9 +33,7 @@ fun CollectionFolderRecordings( showTitle = showHeader, recursive = recursive, sortOptions = MovieSortOptions, - modifier = - modifier - .padding(start = 16.dp), + modifier = modifier, positionCallback = { columns, position -> showHeader = position < columns }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index c9fd5381..d72001ba 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -87,7 +87,7 @@ fun CollectionFolderTv( selectedTabIndex = selectedTabIndex, modifier = Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .padding(vertical = 16.dp) .focusRequester(firstTabFocusRequester), tabs = tabs, onClick = { selectedTabIndex = it }, @@ -105,7 +105,6 @@ fun CollectionFolderTv( }, modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), ) @@ -129,7 +128,6 @@ fun CollectionFolderTv( defaultViewOptions = ViewOptionsPoster, modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), positionCallback = { columns, position -> @@ -150,7 +148,6 @@ fun CollectionFolderTv( includeItemTypes = listOf(BaseItemKind.SERIES), modifier = Modifier - .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index 17535577..5a01d8a3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -298,9 +298,7 @@ fun PersonPageContent( LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), userScrollEnabled = !focusedOnHeader, - modifier = - modifier - .padding(start = 16.dp), + modifier = modifier, ) { item { PersonHeader( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 7e552525..57c8c5d6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -373,7 +373,6 @@ fun PlaylistDetailsContent( horizontalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier - .padding(horizontal = 8.dp) .fillMaxWidth(), ) { PlaylistDetailsHeader( @@ -386,7 +385,7 @@ fun PlaylistDetailsContent( getPossibleFilterValues = getPossibleFilterValues, modifier = Modifier - .padding(start = 16.dp, top = 80.dp) + .padding(top = 80.dp) .fillMaxWidth(.25f), ) when (loadingState) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index c46fe0cb..ead6cfd4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -303,7 +303,7 @@ fun EpisodeDetailsContent( Box(modifier = modifier) { LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp), + contentPadding = PaddingValues(vertical = 8.dp), modifier = Modifier.fillMaxSize(), ) { item { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index b4c4584d..a5d3a7c6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -410,7 +410,7 @@ fun MovieDetailsContent( Box(modifier = modifier) { LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp), + contentPadding = PaddingValues(vertical = 8.dp), modifier = Modifier.fillMaxSize(), ) { item { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 1db5bb61..cef5412b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -333,7 +333,7 @@ fun SeriesDetailsContent( Column( modifier = Modifier - .padding(horizontal = 24.dp, vertical = 16.dp) + .padding(vertical = 16.dp) .fillMaxSize(), ) { LazyColumn( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index b19e7900..86e2b398 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -127,7 +127,7 @@ fun SeriesOverviewContent( modifier = Modifier .fillMaxSize() - .padding(16.dp) + .padding(vertical = 16.dp) .focusGroup() .nestedScroll(scrollConnection) .verticalScroll(scrollState) @@ -142,9 +142,9 @@ fun SeriesOverviewContent( ) { val paddingValues = if (preferences.appPreferences.interfacePreferences.showClock) { - PaddingValues(start = 16.dp, end = 100.dp) + PaddingValues(start = 0.dp, end = 184.dp) } else { - PaddingValues(start = 16.dp, end = 16.dp) + PaddingValues(start = 0.dp, end = 16.dp) } TabRow( selectedTabIndex = selectedTabIndex, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 0e6e172b..ac946d8b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -236,7 +236,7 @@ fun HomePageContent( item = focusedItem, modifier = Modifier - .padding(top = 48.dp, bottom = 32.dp, start = 32.dp) + .padding(top = 48.dp, bottom = 32.dp, start = 8.dp) .fillMaxHeight(.33f), ) LazyColumn( @@ -244,9 +244,6 @@ fun HomePageContent( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues( - start = 24.dp, - end = 16.dp, - top = 0.dp, bottom = Cards.height2x3, ), modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt index 2ada8504..ff7643d8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt @@ -11,13 +11,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -32,7 +34,9 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.serialization.NavBackStackSerializer import androidx.navigation3.runtime.serialization.NavKeySerializer import androidx.navigation3.ui.NavDisplay +import androidx.tv.material3.DrawerValue import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.rememberDrawerState import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.transitionFactory @@ -91,6 +95,7 @@ fun ApplicationContent( navigationManager.backStack = backStack val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle() val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle + val drawerState = rememberDrawerState(DrawerValue.Closed) Box( modifier = modifier, ) { @@ -181,9 +186,15 @@ fun ApplicationContent( .align(Alignment.TopEnd) .fillMaxHeight(.7f) .fillMaxWidth(.7f) - .alpha(.95f) + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .drawWithContent { drawContent() + if (drawerState.isOpen) { + drawRect( + brush = SolidColor(Color.Black), + alpha = .75f, + ) + } // Subtle top scrim for system UI readability (clock, tabs) if (enableTopScrim) { drawRect( @@ -245,6 +256,7 @@ fun ApplicationContent( preferences = preferences, user = user, server = server, + drawerState = drawerState, onClearBackdrop = viewModel::clearBackdrop, modifier = Modifier.fillMaxSize(), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index f38d50bd..0b9c8135 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -2,7 +2,9 @@ package com.github.damontecres.wholphin.ui.nav import android.content.Context import androidx.activity.compose.BackHandler -import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.animateIntOffsetAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState @@ -12,7 +14,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -24,13 +28,13 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties @@ -44,6 +48,7 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -56,13 +61,10 @@ import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.ModalNavigationDrawer -import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.NavigationDrawerItemDefaults import androidx.tv.material3.NavigationDrawerScope import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text -import androidx.tv.material3.rememberDrawerState import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.NavDrawerItemRepository @@ -269,6 +271,7 @@ fun NavDrawer( preferences: UserPreferences, user: JellyfinUser, server: JellyfinServer, + drawerState: DrawerState, onClearBackdrop: () -> Unit, modifier: Modifier = Modifier, viewModel: NavDrawerViewModel = @@ -277,13 +280,11 @@ fun NavDrawer( key = "${server.id}_${user.id}", // Keyed to the server & user to ensure its reset when switching either ), ) { - val drawerState = rememberDrawerState(DrawerValue.Closed) - val listState = rememberLazyListState() val scope = rememberCoroutineScope() val context = LocalContext.current + val density = LocalDensity.current val focusRequester = remember { FocusRequester() } - val searchFocusRequester = remember { FocusRequester() } // If the user presses back while on the home page, open the nav drawer, another back press will quit the app BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Home)) { @@ -302,51 +303,59 @@ fun NavDrawer( viewModel.setShowMore(false) } - val closedDrawerWidth = NavigationDrawerItemDefaults.CollapsedDrawerItemWidth - val drawerBackground by animateColorAsState( - if (drawerState.isOpen) { - MaterialTheme.colorScheme.surface - } else { - Color.Transparent - }, + val closedDrawerWidth = CollapsedDrawerItemWidth + val openDrawerWidth = ExpandedDrawerItemWidth + val offset by animateIntOffsetAsState( + targetValue = + IntOffset( + x = + with(density) { + if (drawerState.isOpen) (openDrawerWidth - closedDrawerWidth).roundToPx() else 0 + }, + y = 0, + ), + animationSpec = + spring( + stiffness = DrawerAnimationStiffness, + visibilityThreshold = IntOffset.VisibilityThreshold, + ), ) - val spacedBy = 4.dp val config = LocalConfiguration.current - val density = LocalDensity.current val heightInPx = remember { with(density) { config.screenHeightDp.dp.roundToPx() } } - suspend fun scrollToSelected() { - val target = selectedIndex + 2 - try { - if (target !in - listState.firstVisibleItemIndex.. + val isOpen = drawerValue.isOpen + val spacedBy = 4.dp + val listState = rememberLazyListState() + val searchFocusRequester = remember { FocusRequester() } + + suspend fun scrollToSelected() { + val target = selectedIndex + 2 + try { + if (target !in + listState.firstVisibleItemIndex.. Unit, + modifier: Modifier = Modifier, + drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + DrawerSheet( + modifier = + Modifier + .align(Alignment.CenterStart), + drawerState = drawerState, + content = drawerContent, + ) + + content() + } +} + +@Composable +private fun DrawerSheet( + modifier: Modifier = Modifier, + drawerState: DrawerState = remember { DrawerState() }, + content: @Composable NavigationDrawerScope.(DrawerValue) -> Unit, +) { + // indicates that the drawer has been set to its initial state and has grabbed focus if + // necessary. Controls whether focus is used to decide the state of the drawer going forward. + var initializationComplete: Boolean by remember { mutableStateOf(false) } + var focusState by remember { mutableStateOf(null) } + val focusRequester = remember { FocusRequester() } + LaunchedEffect(key1 = drawerState.currentValue) { + if (drawerState.currentValue == DrawerValue.Open && focusState?.hasFocus == false) { + // used to grab focus if the drawer state is set to Open on start. + focusRequester.requestFocus() + } + initializationComplete = true + } + + val internalModifier = + Modifier + .focusRequester(focusRequester) + .animateContentSize( + animationSpec = + spring( + stiffness = DrawerAnimationStiffness, + visibilityThreshold = IntSize.VisibilityThreshold, + ), + ).fillMaxHeight() + // adding passed-in modifier here to ensure animateContentSize is called before other + // size based modifiers. + .then(modifier) + .onFocusChanged { + focusState = it + + if (initializationComplete) { + drawerState.setValue(if (it.hasFocus) DrawerValue.Open else DrawerValue.Closed) + } + }.focusGroup() + + Box(modifier = internalModifier) { + NavigationDrawerScopeImpl(drawerState.currentValue == DrawerValue.Open).apply { + content(drawerState.currentValue) + } + } +} + +internal class NavigationDrawerScopeImpl( + override val hasFocus: Boolean, +) : NavigationDrawerScope + +internal val CollapsedDrawerItemWidth = 64.dp +internal val ExpandedDrawerItemWidth = 224.dp +internal val DrawerIconSize = 24.dp +internal val DrawerIconPadding = (ListItemDefaults.IconSize - DrawerIconSize) / 2 +internal val DrawerAnimationStiffness = Spring.StiffnessMedium + +@Composable +internal fun NavigationDrawerScope.NavigationDrawerItem( + selected: Boolean, + onClick: () -> Unit, + leadingContent: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onLongClick: (() -> Unit)? = null, + supportingContent: (@Composable () -> Unit)? = null, + trailingContent: (@Composable () -> Unit)? = null, + tonalElevation: Dp = NavigationDrawerItemDefaults.NavigationDrawerItemElevation, + colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(), + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + val animatedWidth by + animateDpAsState( + targetValue = + if (hasFocus) { + ExpandedDrawerItemWidth + } else { + CollapsedDrawerItemWidth + }, + label = "NavigationDrawerItem width open/closed state of the drawer item", + ) + val navDrawerItemHeight = + if (supportingContent == null) { + NavigationDrawerItemDefaults.ContainerHeightOneLine + } else { + NavigationDrawerItemDefaults.ContainerHeightTwoLine + } + + ListItem( + selected = selected, + onClick = onClick, + headlineContent = content, + leadingContent = { + Box( + Modifier + .padding(horizontal = DrawerIconPadding) + .size(DrawerIconSize), + ) { leadingContent() } + }, + trailingContent = trailingContent, + supportingContent = supportingContent, + modifier = + modifier + .layout { measurable, constraints -> + val width = animatedWidth.roundToPx() + val height = navDrawerItemHeight.roundToPx() + val placeable = + measurable.measure( + constraints.copy( + minWidth = width, + maxWidth = width, + minHeight = height, + maxHeight = height, + ), + ) + layout(placeable.width, placeable.height) { placeable.place(0, 0) } + }, + enabled = enabled, + onLongClick = onLongClick, + tonalElevation = tonalElevation, + colors = colors.toToggleableListItemColors(hasFocus), + scale = ListItemDefaults.scale(1f, 1f), + interactionSource = interactionSource, + ) +} + +@Composable +private fun NavigationDrawerItemColors.toToggleableListItemColors(doesNavigationDrawerHaveFocus: Boolean) = + ListItemDefaults.colors( + containerColor = containerColor, + contentColor = if (doesNavigationDrawerHaveFocus) contentColor else inactiveContentColor, + focusedContainerColor = focusedContainerColor, + focusedContentColor = focusedContentColor, + pressedContainerColor = pressedContainerColor, + pressedContentColor = pressedContentColor, + selectedContainerColor = selectedContainerColor, + selectedContentColor = selectedContentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = + if (doesNavigationDrawerHaveFocus) { + disabledContentColor + } else { + disabledInactiveContentColor + }, + focusedSelectedContainerColor = focusedSelectedContainerColor, + focusedSelectedContentColor = focusedSelectedContentColor, + pressedSelectedContainerColor = pressedSelectedContainerColor, + pressedSelectedContentColor = pressedSelectedContentColor, + )