From 96aa2407d9aca83898e90724207c9250fbcad8e1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 11 May 2026 23:41:01 -0400 Subject: [PATCH] feat(home): surface Discover system playlist as a tile (web + flutter) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server has been generating system_variant='discover' playlists since M6a (internal/playlists/system.go and POST /api/playlists/system/discover/refresh) but neither client surfaced it. The Flutter home filtered system playlists by exact match on 'for_you' and 'songs_like_artist', dropping Discover. The web home did the same. Tiles were generated server-side and silently discarded by both clients. Add a Discover slot to the playlists row in both: - Flutter: 5-slot row now (For You, Discover, 3× Songs-like) with matching placeholder state machine. - Web: same shape, same placeholderVariant() call. When the engine has built it, the real playlist tile renders; otherwise a placeholder with the same building/pending/failed status semantics as the other system tiles. --- flutter_client/lib/library/home_screen.dart | 11 ++++++++++- web/src/routes/+page.svelte | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/flutter_client/lib/library/home_screen.dart b/flutter_client/lib/library/home_screen.dart index bf455251..41dc5d04 100644 --- a/flutter_client/lib/library/home_screen.dart +++ b/flutter_client/lib/library/home_screen.dart @@ -135,7 +135,16 @@ List<_PlaylistRowItem> _buildPlaylistsRow( ? _RealPlaylist(forYou) : _PlaceholderPlaylist('For You', _variantFor('for-you', status))); - // Slots 2-4: Songs-like (real first, padded to 3). + // Slot 2: Discover. Server emits this with system_variant='discover' + // when the recommendation engine has eligible tracks; before then, + // show a placeholder in the same "building/pending" state machine + // as For-You. + final discover = findFirst((p) => p.systemVariant == 'discover'); + out.add(discover != null + ? _RealPlaylist(discover) + : _PlaceholderPlaylist('Discover', _variantFor('discover', status))); + + // Slots 3-5: Songs-like (real first, padded to 3). final songsLike = ownedAll .where((p) => p.systemVariant == 'songs_like_artist') .take(3) diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 1ecdfb48..5649a904 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -39,6 +39,10 @@ (systemPlaylistsQ.data?.owned ?? []).find((p) => p.system_variant === 'for_you') ?? null ); + const discoverPlaylist = $derived( + (systemPlaylistsQ.data?.owned ?? []).find((p) => p.system_variant === 'discover') ?? null + ); + const songsLikePlaylists = $derived( (systemPlaylistsQ.data?.owned ?? []) .filter((p) => p.system_variant === 'songs_like_artist') @@ -48,7 +52,7 @@ const userPlaylists = $derived(userPlaylistsQ.data?.owned ?? []); type PlaceholderVariant = 'building' | 'failed' | 'pending' | 'seed-needed'; - function placeholderVariant(slot: 'for-you' | 'songs-like'): PlaceholderVariant { + function placeholderVariant(slot: 'for-you' | 'discover' | 'songs-like'): PlaceholderVariant { const s = systemStatusQ.data; if (!s) return 'pending'; if (s.in_flight) return 'building'; @@ -71,7 +75,16 @@ out.push({ kind: 'placeholder', label: 'For You', variant: placeholderVariant('for-you') }); } - // Slots 2-4: Songs-like (real first, padded with placeholders). + // Slot 2: Discover (real or placeholder). Server emits this with + // system_variant='discover' once the recommendation engine has + // eligible tracks. + if (discoverPlaylist) { + out.push({ kind: 'real', playlist: discoverPlaylist }); + } else { + out.push({ kind: 'placeholder', label: 'Discover', variant: placeholderVariant('discover') }); + } + + // Slots 3-5: Songs-like (real first, padded with placeholders). for (let i = 0; i < 3; i++) { if (songsLikePlaylists[i]) { out.push({ kind: 'real', playlist: songsLikePlaylists[i] });