overlay a translucent header onto the library content

This commit is contained in:
austinried
2026-10-04 00:29:57 +00:00
parent 40f20eaa40
commit ab39fc259e
6 changed files with 125 additions and 81 deletions
@@ -43,6 +43,7 @@ fun AlbumsContent(
filtered: Boolean = false,
onClearFilters: () -> Unit = {},
resetKey: Any? = null,
topInset: Dp = 0.dp,
modifier: Modifier = Modifier,
) {
when {
@@ -65,7 +66,7 @@ fun AlbumsContent(
LazyVerticalGrid(
state = gridState,
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = bottomInset + 8.dp),
contentPadding = PaddingValues(start = 8.dp, top = topInset + 8.dp, end = 8.dp, bottom = bottomInset + 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier.fillMaxSize(),
@@ -41,6 +41,7 @@ fun ArtistsContent(
filtered: Boolean = false,
onClearFilters: () -> Unit = {},
resetKey: Any? = null,
topInset: Dp = 0.dp,
modifier: Modifier = Modifier,
) {
when {
@@ -63,7 +64,7 @@ fun ArtistsContent(
LazyColumn(
state = listState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomInset),
contentPadding = PaddingValues(top = topInset, bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
val artist = items[index]
@@ -91,6 +91,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.Constraints
@@ -312,87 +313,96 @@ fun LibraryScreen(
val filtersActive = listQuery.starred != StarredFilter.Any || search.isNotEmpty()
val listBottomInset = bottomInset + if (searchActive) SEARCH_BAR_CLEARANCE else FAB_CLEARANCE
Box(modifier.fillMaxSize()) {
var headerHeightPx by remember { mutableFloatStateOf(0f) }
val listTopInset = with(density) { headerHeightPx.toDp() }
Box(
modifier =
modifier
.fillMaxSize()
.background(artwork?.let(::librarySurfaceColor) ?: MaterialTheme.colorScheme.background),
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { page ->
val pageTab = LibraryTab.entries[page]
val resetKey = resetKeys[pageTab]
when (pageTab) {
LibraryTab.Albums -> {
AlbumsContent(
albums,
coverArt,
listBottomInset,
onAlbumClick,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
topInset = listTopInset,
)
}
LibraryTab.Artists -> {
ArtistsContent(
artists,
coverArt,
listBottomInset,
onArtistClick,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
topInset = listTopInset,
)
}
LibraryTab.Songs -> {
SongsContent(
songs,
coverArt,
listBottomInset,
onSongClick,
playingSongId,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
topInset = listTopInset,
)
}
LibraryTab.Playlists -> {
PlaylistsContent(
playlists,
coverArt,
listBottomInset,
onPlaylistClick,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
topInset = listTopInset,
)
}
}
}
Column(
modifier =
Modifier
.fillMaxSize()
.background(artwork?.let(::librarySurfaceColor) ?: MaterialTheme.colorScheme.background),
.fillMaxWidth()
.align(Alignment.TopStart)
.onSizeChanged { headerHeightPx = it.height.toFloat() }
.background(
(artwork?.let(::playerSurfaceColor) ?: MaterialTheme.colorScheme.background)
.copy(alpha = HEADER_ALPHA),
).padding(top = statusBarTop + 2.dp, bottom = 2.dp),
) {
Column(
modifier =
Modifier
.fillMaxWidth()
.background(artwork?.let(::playerSurfaceColor) ?: MaterialTheme.colorScheme.background)
.padding(top = statusBarTop + 2.dp, bottom = 2.dp),
) {
LibraryTabs(
pagerState = pagerState,
onTabSelected = onTabSelected,
syncing = syncing,
onSync = onSync,
onOpenSettings = onOpenSettings,
artwork = artwork,
)
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxWidth().weight(1f),
) { page ->
val pageTab = LibraryTab.entries[page]
val resetKey = resetKeys[pageTab]
when (pageTab) {
LibraryTab.Albums -> {
AlbumsContent(
albums,
coverArt,
listBottomInset,
onAlbumClick,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
)
}
LibraryTab.Artists -> {
ArtistsContent(
artists,
coverArt,
listBottomInset,
onArtistClick,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
)
}
LibraryTab.Songs -> {
SongsContent(
songs,
coverArt,
listBottomInset,
onSongClick,
playingSongId,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
)
}
LibraryTab.Playlists -> {
PlaylistsContent(
playlists,
coverArt,
listBottomInset,
onPlaylistClick,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
)
}
}
}
LibraryTabs(
pagerState = pagerState,
onTabSelected = onTabSelected,
syncing = syncing,
onSync = onSync,
onOpenSettings = onOpenSettings,
artwork = artwork,
)
}
if (searchActive) {
@@ -582,6 +592,7 @@ private fun ListOptionsSheet(
}
private val FAB_CLEARANCE = 80.dp
private const val HEADER_ALPHA = 0.72f
private val SEARCH_BAR_CLEARANCE = 80.dp
@Composable
@@ -40,6 +40,7 @@ fun PlaylistsContent(
filtered: Boolean = false,
onClearFilters: () -> Unit = {},
resetKey: Any? = null,
topInset: Dp = 0.dp,
modifier: Modifier = Modifier,
) {
when {
@@ -62,7 +63,7 @@ fun PlaylistsContent(
LazyColumn(
state = listState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomInset),
contentPadding = PaddingValues(top = topInset, bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
val playlist = items[index]
@@ -52,6 +52,7 @@ fun SongsContent(
filtered: Boolean = false,
onClearFilters: () -> Unit = {},
resetKey: Any? = null,
topInset: Dp = 0.dp,
modifier: Modifier = Modifier,
) {
when {
@@ -74,7 +75,7 @@ fun SongsContent(
LazyColumn(
state = listState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomInset),
contentPadding = PaddingValues(top = topInset, bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
val item = items[index]
@@ -96,6 +96,35 @@ class AppScreenshotTest {
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_albums.png")
}
@Test
fun libraryScrolledUnderHeader() {
val albums = (1..60).map { Fixtures.albums.first().copy(id = "scrolled-$it", name = "Album $it") }
composeRule.setContent {
SubtracksTheme {
LibraryScreen(
selectedTab = LibraryTab.Albums,
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums)) }.collectAsLazyPagingItems(),
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(),
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(),
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(),
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
)
}
}
awaitTag(ALBUM_COVER_TAG)
composeRule.onRoot().performTouchInput { swipeUp(startY = centerY + 600f, endY = centerY - 600f, durationMillis = 400) }
composeRule.waitForIdle()
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_scrolled_under_header.png")
}
@Test
fun libraryArtists() {
setLibraryContent(LibraryTab.Artists)