feat(android): Most Played to 3-row dense layout matching web
Web Home (web/src/routes/+page.svelte:196) stacks the 75 most-played tracks into 3 rows of 25 inside a single horizontal scroller; Android was rendering them as one long LazyRow which makes the section feel sparse and forces a lot of horizontal scrolling to reach mid-rank tracks. Operator request 2026-06-01. Replaces MostPlayedRow's LazyRow with a LazyHorizontalGrid where rows=Fixed(MOST_PLAYED_ROWS=3). Cards stay 140dp (matching web's w-36); density gain comes purely from stacking. Web is row-major (ranks 0..24 across the top row); LazyHorizontalGrid fills column-major. To match web's visual order, the input list is pre-chunked into 3 row-major rows then re-flattened column-by-column before being handed to the grid. With 75 tracks and 3 rows that's 25 columns, ranks 0/25/50 in column 0, 1/26/51 in column 1, etc. MOST_PLAYED_GRID_HEIGHT_DP=600 budgets ~188dp per row (140 cover + 8 spacer + 2 lines of text), plus 2 * 8dp inter-row spacing. Diverges from Flutter (still single-row); intentional, parity-map updated. Backflow to Flutter not tracked yet because the Flutter client is being retired.
This commit is contained in:
@@ -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<HomeTile<TrackRef>>,
|
||||
onTap: (List<TrackRef>, 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)) {
|
||||
|
||||
Reference in New Issue
Block a user