feat(frontend): redesign playlist pages with cover image support

Create and edit playlist modals now upload/preview a cover image
(reusing the presigned-upload flow already used for tracks) and share
identical layout and coloring. Adds a cover proxy route and wires
PlaylistArt to render a playlist's real cover before falling back to
the track mosaic/gradient.

Also reworks the playlist detail page: full-width borderless "Añadir
de tu biblioteca" table with listens count and click-to-play, drops
the empty-playlist placeholder message, and restyles the playlists
grid to match the rest of the app.
This commit is contained in:
2026-07-15 16:10:04 +02:00
parent a1f7756fc9
commit 3bbbd52d43
6 changed files with 390 additions and 110 deletions
@@ -3,12 +3,14 @@
import { gradientForHue, HUES } from '$lib/theme/color';
interface Props {
playlistId?: string;
trackIds: (string | number)[];
hue: number;
size?: 'small' | 'medium' | 'large';
class?: string;
}
let { trackIds, hue, class: klass = '' }: Props = $props();
let { playlistId, trackIds, hue, size = 'medium', class: klass = '' }: Props = $props();
function hueFor(id: string | number) {
const text = String(id);
@@ -17,11 +19,23 @@
return HUES[hash % HUES.length];
}
let coverFailed = $state(false);
const mosaic = $derived(trackIds.length >= 4);
const single = $derived(trackIds.length >= 1 && trackIds.length < 4 ? trackIds[0] : null);
</script>
{#if mosaic}
{#if playlistId && !coverFailed}
<div class="relative overflow-hidden {klass}" style="background:{gradientForHue(hue)}">
<img
src="/api/playlists/{playlistId}/cover?size={size}"
alt=""
loading="lazy"
onerror={() => (coverFailed = true)}
class="h-full w-full object-cover"
/>
</div>
{:else if mosaic}
<div class="grid grid-cols-2 grid-rows-2 overflow-hidden {klass}">
{#each trackIds.slice(0, 4) as id (id)}
<Cover trackId={id} hue={hueFor(id)} size="small" class="h-full w-full" />