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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
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.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -813,24 +816,64 @@ private fun MostPlayedRow(
|
|||||||
tracks: List<HomeTile<TrackRef>>,
|
tracks: List<HomeTile<TrackRef>>,
|
||||||
onTap: (List<TrackRef>, Int) -> Unit,
|
onTap: (List<TrackRef>, Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
HorizontalScrollRow(title = "Most played") {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
items(items = tracks, key = { it.id }) { tile ->
|
Text(
|
||||||
val track = tile.value
|
text = "Most played",
|
||||||
if (track == null) {
|
style = MaterialTheme.typography.titleLarge,
|
||||||
SkeletonCompactTrackTile()
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
} else {
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
CompactTrackTile(
|
)
|
||||||
track = track,
|
// Match web's chunked-rows-in-shared-scroller layout
|
||||||
onClick = {
|
// (web/src/routes/+page.svelte:196): tracks are dealt into
|
||||||
val playable = tracks.mapNotNull { it.value }
|
// MOST_PLAYED_ROWS rows of equal width, all stacked inside one
|
||||||
onTap(playable, playable.indexOfFirst { it.id == track.id })
|
// 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
|
@Composable
|
||||||
private fun SkeletonCompactTrackTile() {
|
private fun SkeletonCompactTrackTile() {
|
||||||
Column(modifier = Modifier.width(140.dp)) {
|
Column(modifier = Modifier.width(140.dp)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user