From d99de1af27b6bd816ed9943c484d9c74374cc7c6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 1 Jun 2026 19:30:17 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20Recently=20Added=20=E2=86=92?= =?UTF-8?q?=20single=20LazyHorizontalGrid=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously Recently Added chunked into rows of 25 and rendered each chunk as its own LazyRow, so chunks scrolled independently. Same pattern Most Played uses (one LazyHorizontalGrid where all rows scroll as a single panel) now applies to Recently Added. - New RECENTLY_ADDED_GRID_ROWS = 2 + RECENTLY_ADDED_GRID_HEIGHT_DP = 440 (matches a 200dp tile × 2 rows + the 8dp inter-row gap). - New RecentlyAddedGrid composable owns the section header + the LazyHorizontalGrid. Column-major re-flattening matches Web's row-major reading order on screen (top row first, then bottom). - recentlyAddedSection collapses from itemsIndexed-over-chunks to a single item { RecentlyAddedGrid(...) } in the outer LazyColumn. - AlbumsRow stays untouched (still used by Rediscover, etc.). The other multi-section helpers (Rediscover, Last Played, Playlists) are single-row by design and don't need this treatment. --- .../minstrel/home/ui/HomeScreen.kt | 81 ++++++++++++++++--- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt index c2b03bc4..ae5a0ed9 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt @@ -101,7 +101,12 @@ import javax.inject.Inject private const val SHARE_STOP_TIMEOUT_MS = 5_000L private const val PLAYLIST_FETCH_TIMEOUT_MS = 8_000L private const val BOTTOM_PADDING_FOR_MINIPLAYER_DP = 140 -private const val RECENTLY_ADDED_CHUNK = 25 +// Recently Added is laid out in a multi-row LazyHorizontalGrid that +// scrolls as one panel (same pattern as Most Played). Two rows trades +// a tall block for fewer columns to scroll past — a 200dp tile × 2 = +// ~440dp section still leaves room for the rest of Home. +private const val RECENTLY_ADDED_GRID_ROWS = 2 +private const val RECENTLY_ADDED_GRID_HEIGHT_DP = 440 // ─── State ─────────────────────────────────────────────────────────── @@ -512,23 +517,75 @@ private fun LazyListScope.recentlyAddedSection( } return } - // Chunk into stacked carousels of 25 so a large library reads as - // multiple rows instead of one unwieldy LazyRow. Only the first - // chunk carries the section title; the rest are continuation rows. - val chunks = albums.chunked(RECENTLY_ADDED_CHUNK) - itemsIndexed( - items = chunks, - key = { index, chunk -> "recent-$index-${chunk.first().id}" }, - ) { index, chunk -> - AlbumsRow( - title = if (index == 0) "Recently added" else "", - albums = chunk, + item { + RecentlyAddedGrid( + albums = albums, onAlbumClick = onAlbumClick, onPlayAlbum = onPlayAlbum, ) } } +/** + * Multi-row horizontal grid for Recently Added, mirroring the + * Most Played pattern: tiles laid out column-major across + * [RECENTLY_ADDED_GRID_ROWS] rows inside one LazyHorizontalGrid so + * every row scrolls together as one panel. Replaces the previous + * stacked-LazyRows layout which scrolled each chunk independently. + */ +@Composable +private fun RecentlyAddedGrid( + albums: List>, + onAlbumClick: (String) -> Unit, + onPlayAlbum: suspend (String) -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Recently added", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + // Re-flatten column-major to match Web's "ranks across the + // top row, then across the middle, then bottom" reading + // order. LazyHorizontalGrid otherwise fills column-major + // directly from a flat list and would scramble per-rank + // expectations. + val perRow = (albums.size + RECENTLY_ADDED_GRID_ROWS - 1) / RECENTLY_ADDED_GRID_ROWS + val rows = albums.chunked(perRow.coerceAtLeast(1)) + val cols = rows.maxOfOrNull { it.size } ?: 0 + val ordered = buildList { + for (c in 0 until cols) { + for (r in 0 until RECENTLY_ADDED_GRID_ROWS) { + rows.getOrNull(r)?.getOrNull(c)?.let { add(it) } + } + } + } + LazyHorizontalGrid( + rows = GridCells.Fixed(RECENTLY_ADDED_GRID_ROWS), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(RECENTLY_ADDED_GRID_HEIGHT_DP.dp), + ) { + gridItems(items = ordered, key = { it.id }) { tile -> + val album = tile.value + if (album == null) { + SkeletonAlbumTile() + } else { + AlbumCard( + album = album, + onClick = { onAlbumClick(album.id) }, + onPlay = { onPlayAlbum(album.id) }, + ) + } + } + } + } +} + private fun LazyListScope.rediscoverSection( albums: List>, artists: List>,