Release: web UX overhaul + Android native port + server polish #59

Merged
bvandeusen merged 298 commits from dev into main 2026-06-01 16:11:21 -04:00
2 changed files with 39 additions and 5 deletions
Showing only changes of commit bf61d6cfa3 - Show all commits
@@ -709,10 +709,18 @@ enum class OfflinePoolKind(val label: String) {
/** /**
* Builds the Home Playlists row. When [offline], the two cache-backed * Builds the Home Playlists row. When [offline], the two cache-backed
* pool cards (Recently played, Liked) lead the row — mirrors Flutter's * pool cards (Recently played, Liked) lead the row. Then For You +
* `_buildPlaylistsRow`. Then For You + Discover + 3× Songs-like fixed * Discover + 3× Songs-like fixed slots (real card when generated,
* slots (real card when generated, placeholder otherwise), then any * placeholder otherwise), then the secondary system kinds (deep cuts /
* user-owned playlists. * rediscover / new for you / on this day / first listens) when they
* exist — no placeholders for these since they're conditional on
* library shape, not guaranteed singletons. Finally user-owned
* playlists.
*
* Diverges from Flutter (`flutter_client/lib/library/home_screen.dart`
* `_buildPlaylistsRow`) which only shows the 5 fixed slots and never
* surfaces the secondary kinds on Home. Operator authorized the
* divergence on 2026-06-01; web UI catch-up tracked as task #53.
*/ */
private fun buildPlaylistsRow( private fun buildPlaylistsRow(
owned: List<PlaylistRef>, owned: List<PlaylistRef>,
@@ -736,6 +744,15 @@ private fun buildPlaylistsRow(
?.let { PlaylistRowItem.Real(it) } ?.let { PlaylistRowItem.Real(it) }
?: PlaylistRowItem.Placeholder("Songs like…", variantFor("songs-like", status)) ?: PlaylistRowItem.Placeholder("Songs like…", variantFor("songs-like", status))
} }
// Secondary system kinds in server-registry order. Only included
// when actually generated — these depend on library shape (Deep
// cuts needs deep albums, On this day needs prior history, etc.)
// so a missing one means "not enough data" rather than "still
// building".
for (variant in SECONDARY_SYSTEM_VARIANTS) {
owned.firstOrNull { it.systemVariant == variant }
?.let { out += PlaylistRowItem.Real(it) }
}
owned.filter { it.systemVariant == null }.forEach { out += PlaylistRowItem.Real(it) } owned.filter { it.systemVariant == null }.forEach { out += PlaylistRowItem.Real(it) }
return out return out
} }
@@ -749,6 +766,19 @@ private fun variantFor(slot: String, s: SystemPlaylistsStatus): String = when {
private const val SONGS_LIKE_SLOTS = 3 private const val SONGS_LIKE_SLOTS = 3
/**
* The 5 system playlist kinds the server generates that aren't pinned
* to Home's primary slots. Order matches `systemPlaylistRegistry` in
* `internal/playlists/system.go`.
*/
private val SECONDARY_SYSTEM_VARIANTS = listOf(
"deep_cuts",
"rediscover",
"new_for_you",
"on_this_day",
"first_listens",
)
@Composable @Composable
private fun ArtistsRow( private fun ArtistsRow(
title: String, title: String,
@@ -159,7 +159,11 @@ private fun systemLabelFor(variant: String?): String = when (variant) {
"for_you" -> "For You" "for_you" -> "For You"
"discover" -> "Discover" "discover" -> "Discover"
"songs_like_artist" -> "Songs like…" "songs_like_artist" -> "Songs like…"
"todays_mix" -> "Today's mix" "deep_cuts" -> "Deep cuts"
"rediscover" -> "Rediscover"
"new_for_you" -> "New for you"
"on_this_day" -> "On this day"
"first_listens" -> "First listens"
null -> "" null -> ""
else -> variant.replace('_', ' ').replaceFirstChar { it.uppercase() } else -> variant.replace('_', ' ').replaceFirstChar { it.uppercase() }
} }