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 0de419dd..47b5524a 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 @@ -18,6 +18,9 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items as gridItems import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape @@ -813,24 +816,64 @@ private fun MostPlayedRow( tracks: List>, onTap: (List, Int) -> Unit, ) { - HorizontalScrollRow(title = "Most played") { - items(items = tracks, key = { it.id }) { tile -> - val track = tile.value - if (track == null) { - SkeletonCompactTrackTile() - } else { - CompactTrackTile( - track = track, - onClick = { - val playable = tracks.mapNotNull { it.value } - onTap(playable, playable.indexOfFirst { it.id == track.id }) - }, - ) + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Most played", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + // Match web's chunked-rows-in-shared-scroller layout + // (web/src/routes/+page.svelte:196): tracks are dealt into + // MOST_PLAYED_ROWS rows of equal width, all stacked inside one + // horizontal scroller. Web is row-major (ranks 0..24 across the + // top row, 25..49 across the middle, 50..74 across the bottom); + // LazyHorizontalGrid fills column-major, so we re-flatten the + // chunked input column-by-column to match the web order. + val perRow = (tracks.size + MOST_PLAYED_ROWS - 1) / MOST_PLAYED_ROWS + val rows = tracks.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 MOST_PLAYED_ROWS) { + rows.getOrNull(r)?.getOrNull(c)?.let { add(it) } + } + } + } + LazyHorizontalGrid( + rows = GridCells.Fixed(MOST_PLAYED_ROWS), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(MOST_PLAYED_GRID_HEIGHT_DP.dp), + ) { + gridItems(items = ordered, key = { it.id }) { tile -> + val track = tile.value + if (track == null) { + SkeletonCompactTrackTile() + } else { + CompactTrackTile( + track = track, + onClick = { + val playable = tracks.mapNotNull { it.value } + onTap(playable, playable.indexOfFirst { it.id == track.id }) + }, + ) + } } } } } +private const val MOST_PLAYED_ROWS = 3 + +// 3 rows of CompactTrackTile (~188dp intrinsic each: 140dp cover + +// 8dp spacer + 2 lines of text) + 2 * 8dp inter-row spacing = ~580dp; +// 600dp gives breathing room without leaving an obvious gap. +private const val MOST_PLAYED_GRID_HEIGHT_DP = 600 + @Composable private fun SkeletonCompactTrackTile() { Column(modifier = Modifier.width(140.dp)) {