diff --git a/web/src/lib/components/HomeHeroCard.svelte b/web/src/lib/components/HomeHeroCard.svelte new file mode 100644 index 00000000..4c4e6445 --- /dev/null +++ b/web/src/lib/components/HomeHeroCard.svelte @@ -0,0 +1,75 @@ + + + +
+ +
+
+
+
{eyebrow}
+
+ {title} +
+ {#if subtitle} +
{subtitle}
+ {/if} +
+ +
+
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 80b8d6db..60cf8d5a 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -3,6 +3,13 @@ import { createHomeQuery } from '$lib/api/home'; import HorizontalScrollRow from '$lib/components/HorizontalScrollRow.svelte'; import AlbumCard from '$lib/components/AlbumCard.svelte'; + import HomeHeroCard from '$lib/components/HomeHeroCard.svelte'; + import { systemShuffle, getPlaylist } from '$lib/api/playlists'; + import { playlistTrackToRef } from '$lib/playlists/playlistTrackToRef'; + import { playQueue } from '$lib/player/store.svelte'; + import { api } from '$lib/api/client'; + import { coverUrl as albumCoverUrl } from '$lib/media/covers'; + import type { AlbumDetail, TrackRef as TrackRefType } from '$lib/api/types'; import ArtistCard from '$lib/components/ArtistCard.svelte'; import CompactTrackCard from '$lib/components/CompactTrackCard.svelte'; import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte'; @@ -75,6 +82,46 @@ const userPlaylists = $derived(userPlaylistsQ.data?.owned ?? []); + // Hero row: For-You + most-recently-added album. Anchors the page + // so the eye lands on these instead of cascading down a wall of + // identical tiles. + const latestAlbum = $derived(data?.recently_added_albums?.[0] ?? null); + + function albumCoverFor(albumId: string | undefined | null): string { + return albumId ? albumCoverUrl(albumId) : ''; + } + + let heroStarting = $state(false); + + async function playForYou(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (heroStarting || !forYouPlaylist?.system_variant) return; + heroStarting = true; + try { + const detail = await systemShuffle(forYouPlaylist.system_variant); + const refs = detail.tracks + .map((r) => playlistTrackToRef(r)) + .filter((t): t is TrackRefType => t !== null); + if (refs.length > 0) playQueue(refs, 0, { source: forYouPlaylist.system_variant }); + } finally { + heroStarting = false; + } + } + + async function playLatestAlbum(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (heroStarting || !latestAlbum) return; + heroStarting = true; + try { + const detail = await api.get(`/api/albums/${latestAlbum.id}`); + if (detail.tracks.length > 0) playQueue(detail.tracks, 0); + } finally { + heroStarting = false; + } + } + type PlaceholderVariant = 'building' | 'failed' | 'pending' | 'seed-needed'; function placeholderVariant(slot: 'for-you' | 'discover' | 'songs-like'): PlaceholderVariant { const s = systemStatusQ.data; @@ -134,6 +181,40 @@ {pageTitle('Home')}
+ + {#if forYouPlaylist || latestAlbum} +
+ {#if forYouPlaylist} + 0 + ? `${forYouPlaylist.track_count} tracks for you` + : 'Building your mix'} + onPlay={playForYou} + playDisabled={heroStarting || forYouPlaylist.track_count === 0} + /> + {/if} + {#if latestAlbum} + + {/if} +
+ {/if} +