fix(android): let list content reach the MiniPlayer — #2681
android / Build + lint + test (push) Successful in 4m28s
android / Build + lint + test (push) Successful in 4m28s
The shell is a Column (content weight(1f), then the bar), so the content viewport already ends at the MiniPlayer's top edge. But nothing owned the bottom navigation-bar inset under edge-to-edge: each in-shell screen's own Scaffold claimed it via the default contentWindowInsets and padded its content up by the nav-bar height a second time. That padding is the dead strip the operator sees between the last list row and the bar — and the bar's own bottom was drawing under the gesture pill. ShellScaffold now owns the inset end to end: the content region consumes it, and a Spacer below the MiniPlayer re-holds the space for the system bar (unconditional — MiniPlayer renders nothing when no track is loaded). Modifier.consumeWindowInsets alone can't fix it: ScaffoldLayout reads contentWindowInsets.asPaddingValues() directly, outside the modifier consumption chain, so every in-shell Scaffold is handed the new zero ShellContentWindowInsets. The full-screen routes (NowPlaying / Queue / Login / ServerUrl) keep the default — no shell sits above them. Also drops the hardcoded 140dp bottom contentPadding on Album and Playlist detail, a Flutter-era value for a player bar that overlaid its list; here the shell reserves that space in layout already.
This commit is contained in:
@@ -41,6 +41,7 @@ import com.fabledsword.minstrel.nav.AdminQuarantine
|
||||
import com.fabledsword.minstrel.nav.AdminRequests
|
||||
import com.fabledsword.minstrel.nav.AdminTagSources
|
||||
import com.fabledsword.minstrel.nav.AdminUsers
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
@@ -112,6 +113,7 @@ fun AdminLandingScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.models.AdminQuarantineItemRef
|
||||
import com.fabledsword.minstrel.nav.AdminQuarantine
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -42,6 +43,7 @@ fun AdminQuarantineScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.models.RequestRef
|
||||
import com.fabledsword.minstrel.nav.AdminRequests
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -41,6 +42,7 @@ fun AdminRequestsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.models.AdminTagSourceRef
|
||||
import com.fabledsword.minstrel.models.TagSourceTestResult
|
||||
import com.fabledsword.minstrel.nav.AdminTagSources
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -49,6 +50,7 @@ fun AdminTagSourcesScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -49,6 +49,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.models.AdminUserRef
|
||||
import com.fabledsword.minstrel.nav.AdminUsers
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -127,6 +128,7 @@ private fun AdminUsersScaffold(
|
||||
onRevokeInvite: (String) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.fabledsword.minstrel.models.LidarrRequestKind
|
||||
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
||||
import com.fabledsword.minstrel.models.SuggestionSnoozeRef
|
||||
import com.fabledsword.minstrel.nav.Discover
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
@@ -61,6 +62,7 @@ fun DiscoverScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -91,6 +91,7 @@ import com.fabledsword.minstrel.shared.VeilOutcome
|
||||
import com.fabledsword.minstrel.shared.VeilSessionResult
|
||||
import com.fabledsword.minstrel.shared.VeilSettleState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.ArtSettleTracker
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
@@ -564,6 +565,7 @@ fun HomeScreen(
|
||||
viewModel.transientMessages.collect { snackbarHostState.showSnackbar(it) }
|
||||
}
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -52,6 +51,7 @@ import com.fabledsword.minstrel.models.TrackRef
|
||||
import com.fabledsword.minstrel.nav.AlbumDetail
|
||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
import com.fabledsword.minstrel.shared.formatDuration
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.TrackRow
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LikeButton
|
||||
@@ -70,6 +70,7 @@ fun AlbumDetailScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -165,7 +166,6 @@ private fun AlbumBody(
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 140.dp),
|
||||
) {
|
||||
item {
|
||||
AlbumHeader(
|
||||
|
||||
@@ -56,6 +56,7 @@ import com.fabledsword.minstrel.models.albumCoverPath
|
||||
import com.fabledsword.minstrel.nav.AlbumDetail
|
||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
import com.fabledsword.minstrel.shared.formatDuration
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
|
||||
import com.fabledsword.minstrel.shared.widgets.LikeButton
|
||||
@@ -79,6 +80,7 @@ fun ArtistDetailScreen(
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.fabledsword.minstrel.nav.Library
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Shuffle
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
@@ -77,6 +78,7 @@ fun LibraryScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
Column {
|
||||
|
||||
+2
-2
@@ -7,7 +7,6 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -75,6 +74,7 @@ import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||
import com.fabledsword.minstrel.playlists.data.toPlayableTrackRefs
|
||||
import com.fabledsword.minstrel.shared.formatDuration
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.TrackRow
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LikeButton
|
||||
@@ -339,6 +339,7 @@ fun PlaylistDetailScreen(
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
@@ -430,7 +431,6 @@ private fun PlaylistDetailBody(
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 140.dp),
|
||||
) {
|
||||
item { PlaylistHeader(detail.playlist, onPlayAll, onShuffleAll, onRegenerate) }
|
||||
item { HorizontalDivider() }
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.fabledsword.minstrel.playlists.data.playPlaylistShuffled
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -149,6 +150,7 @@ fun PlaylistsListScreen(
|
||||
viewModel.transientMessages.collect { snackbar.showSnackbar(it) }
|
||||
}
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -45,6 +45,7 @@ import com.fabledsword.minstrel.nav.AlbumDetail
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
import com.fabledsword.minstrel.nav.Requests
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -58,6 +59,7 @@ fun RequestsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.fabledsword.minstrel.models.TrackRef
|
||||
import com.fabledsword.minstrel.nav.AlbumDetail
|
||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
import com.fabledsword.minstrel.nav.Search as SearchRoute
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.TrackRow
|
||||
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -76,6 +77,7 @@ fun SearchScreen(
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.fabledsword.minstrel.nav.Admin
|
||||
import com.fabledsword.minstrel.nav.Requests
|
||||
import com.fabledsword.minstrel.nav.Settings as SettingsRoute
|
||||
import com.fabledsword.minstrel.nav.ServerUrl
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
import com.fabledsword.minstrel.theme.ThemeMode
|
||||
import com.fabledsword.minstrel.theme.ThemePreferenceViewModel
|
||||
@@ -81,6 +82,7 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = ShellContentWindowInsets,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
MinstrelTopAppBar(
|
||||
|
||||
@@ -2,9 +2,14 @@ package com.fabledsword.minstrel.shared.widgets
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -20,6 +25,26 @@ import com.fabledsword.minstrel.update.ui.UpdateBanner
|
||||
import com.fabledsword.minstrel.update.ui.VersionTooOldBanner
|
||||
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
|
||||
|
||||
/**
|
||||
* `contentWindowInsets` for a screen's own Scaffold when that screen
|
||||
* renders inside [ShellScaffold].
|
||||
*
|
||||
* The shell owns the window insets for in-shell routes: it consumes the
|
||||
* status bar at the top and holds the navigation-bar space at the
|
||||
* bottom, below the MiniPlayer. A screen Scaffold's default
|
||||
* `contentWindowInsets` is the raw system bars, and Scaffold reads that
|
||||
* value directly (`contentWindowInsets.asPaddingValues()`) rather than
|
||||
* through the consumption-aware modifier chain — so the shell consuming
|
||||
* the inset does NOT reach it. Left at the default, every in-shell
|
||||
* screen pads its content up by the nav-bar height a second time: the
|
||||
* dead strip between the last list row and the MiniPlayer.
|
||||
*
|
||||
* Full-screen routes (NowPlaying / Queue / Login / ServerUrl) keep the
|
||||
* default — no shell sits above them, so their Scaffold is the only
|
||||
* thing that can inset them.
|
||||
*/
|
||||
val ShellContentWindowInsets: WindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
|
||||
/**
|
||||
* Outer shell wrapper for "in-app" routes — banners on top
|
||||
* (conditional, none implemented yet), routed screen filling the
|
||||
@@ -82,12 +107,31 @@ fun ShellScaffold(
|
||||
VersionTooOldBanner()
|
||||
UpdateBanner()
|
||||
ConnectionErrorBanner()
|
||||
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
||||
// The window is edge-to-edge, so something has to own the
|
||||
// bottom navigation-bar inset, and the shell claims it once
|
||||
// here: the content region consumes it so nothing inside a
|
||||
// screen pads for it a second time (see
|
||||
// [ShellContentWindowInsets] for the Scaffold half of that —
|
||||
// Scaffold reads its insets outside the consumption chain), and
|
||||
// the Spacer below re-holds the space for the system bar.
|
||||
// Un-owned, the inset re-appears inside each screen as a dead
|
||||
// strip between the last list row and the MiniPlayer.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.consumeWindowInsets(WindowInsets.navigationBars),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
MiniPlayer(
|
||||
onExpandClick = onExpandPlayer,
|
||||
)
|
||||
// Unconditional: the MiniPlayer renders nothing when no track
|
||||
// is loaded, and the content region has already given the inset
|
||||
// up, so this is what keeps a fresh install's last row off the
|
||||
// gesture pill.
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user