Checkpoint web-player design-system refactor and backend track genre tagging
Advances the ongoing token/componentization cleanup (anchored player dock, border-based panels instead of floating shadows, corrected light-mode accent/gradient tokens) alongside in-progress backend support for track genre tags.
This commit is contained in:
@@ -55,7 +55,7 @@
|
||||
{trackIds}
|
||||
{coverUrl}
|
||||
size="large"
|
||||
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
|
||||
class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
||||
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
|
||||
|
||||
type AlbumTrack = ApiTrackLike & { duration: number | string };
|
||||
type AlbumTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
|
||||
|
||||
interface Props {
|
||||
tracks: AlbumTrack[];
|
||||
@@ -24,7 +24,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<TrackTable index action={isOwner} class="mt-6 sm:mt-8">
|
||||
<TrackTable
|
||||
index
|
||||
action={isOwner}
|
||||
meta={[{ label: 'Escuchas', width: '100px' }]}
|
||||
class="mt-6 sm:mt-8"
|
||||
>
|
||||
{#each tracks as track, i (track.id)}
|
||||
{@const active = player.current.id === track.id}
|
||||
<TrackRow {active}>
|
||||
@@ -38,6 +43,7 @@
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
/>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
||||
{#if isOwner}
|
||||
<form
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
|
||||
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
||||
|
||||
type LibraryTrack = ApiTrackLike & { duration: number | string };
|
||||
type LibraryTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
|
||||
|
||||
interface Props {
|
||||
library: LibraryTrack[];
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<div class="mt-10 sm:mt-12">
|
||||
<SectionHeading title="Tus canciones subidas" />
|
||||
<TrackTable action>
|
||||
<TrackTable action meta={[{ label: 'Escuchas', width: '100px' }]}>
|
||||
{#each library as track, i (track.id)}
|
||||
{@const active = player.current.id === track.id}
|
||||
<TrackRow {active}>
|
||||
@@ -45,6 +45,7 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TrackTitleCell>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
||||
<form
|
||||
method="POST"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Play from '@lucide/svelte/icons/play';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { fetchAlbumQueueItems } from '$lib/albums';
|
||||
import { fmtTime } from '$lib/format';
|
||||
import { fmtTime, fmtPlays } from '$lib/format';
|
||||
import Page from '$lib/components/ui/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||
import Alert from '$lib/components/ui/Alert.svelte';
|
||||
@@ -30,6 +30,7 @@
|
||||
targetArtist,
|
||||
targetExplicit,
|
||||
targetId,
|
||||
targetListensCount,
|
||||
targetTitle,
|
||||
type TrackTarget
|
||||
} from '$lib/tracks';
|
||||
@@ -299,23 +300,23 @@
|
||||
trackId={targetId(topResult.target)}
|
||||
size="large"
|
||||
alt={targetTitle(topResult.target)}
|
||||
class="h-22 w-22 shrink-0 rounded-2xl shadow-art"
|
||||
class="h-22 w-22 shrink-0 rounded-2xl"
|
||||
/>
|
||||
{:else if topResult.kind === 'album'}
|
||||
<PlaylistArt
|
||||
trackIds={topResult.album.coverTrackIds}
|
||||
class="h-22 w-22 shrink-0 rounded-2xl shadow-art"
|
||||
class="h-22 w-22 shrink-0 rounded-2xl"
|
||||
/>
|
||||
{:else if topResult.kind === 'playlist'}
|
||||
<PlaylistArt
|
||||
playlistId={topResult.playlist.id}
|
||||
trackIds={topResult.playlist.coverTrackIds}
|
||||
version={topResult.playlist.updatedAt}
|
||||
class="h-22 w-22 shrink-0 rounded-2xl shadow-art"
|
||||
class="h-22 w-22 shrink-0 rounded-2xl"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="grid h-22 w-22 shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2 shadow-art"
|
||||
class="grid h-22 w-22 shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2"
|
||||
>
|
||||
{#if topResult.user.profilePictureUrl}
|
||||
<img
|
||||
@@ -385,7 +386,7 @@
|
||||
|
||||
{#if nothingFound}
|
||||
<div class="mx-auto max-w-115 py-[8vh] text-center">
|
||||
<h1 class="font-display text-xl font-semibold tracking-[-0.025em] text-fg">
|
||||
<h1 class="font-display text-xl font-semibold tracking-tight text-fg">
|
||||
Sin resultados para «{data.query}»
|
||||
</h1>
|
||||
<p class="mt-2.5 text-sm leading-[1.65] text-fg-2">
|
||||
@@ -411,14 +412,14 @@
|
||||
{#each genreTiles as genre, i (genre.query)}
|
||||
<a
|
||||
href={buildHref(genre.query)}
|
||||
class="animate-enter group relative block h-28 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-125"
|
||||
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-125"
|
||||
style="animation-delay:{Math.min(i, 10) *
|
||||
40}ms; background:linear-gradient(140deg, oklch(0.24 0.07 {genre.hue}), var(--mf-tile-shade))"
|
||||
40}ms; background:linear-gradient(140deg, oklch(var(--mf-tile-l) var(--mf-tile-c) {genre.hue}), oklch(var(--mf-tile-shade-l) var(--mf-tile-shade-c) {genre.hue}))"
|
||||
>
|
||||
<div class="font-display text-base font-semibold tracking-[-0.02em] text-fg">
|
||||
<div class="font-display text-base font-medium tracking-tight text-on-art">
|
||||
{genre.label}
|
||||
</div>
|
||||
<div class="mt-1.5 text-xs text-fg-3">{genre.tagline}</div>
|
||||
<div class="text-xs text-on-art-2">{genre.tagline}</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -445,7 +446,7 @@
|
||||
kind="track"
|
||||
title={targetTitle(item)}
|
||||
subtitle={targetArtist(item)}
|
||||
meta={fmtTime(seconds)}
|
||||
meta="{fmtTime(seconds)} · {fmtPlays(targetListensCount(item))}"
|
||||
explicit={targetExplicit(item)}
|
||||
onclick={() => togglePlay(i)}
|
||||
oncontextmenu={(e) => openContextMenu(e, item)}
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
index
|
||||
action
|
||||
meta={[
|
||||
{ label: 'Álbum', width: '160px' },
|
||||
{ label: 'Subida', width: '100px' },
|
||||
{ label: 'Escuchas', width: '100px' }
|
||||
{ label: 'Álbum', width: '76px' },
|
||||
{ label: 'Subida', width: '124px' },
|
||||
{ label: 'Escuchas', width: '106px' }
|
||||
]}
|
||||
class="mt-7 sm:mt-8"
|
||||
>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
>
|
||||
{#snippet cover()}
|
||||
<div
|
||||
class="grid h-36 w-36 shrink-0 place-items-center rounded-[18px] bg-[image:var(--mf-liked-grad)] shadow-cover-lg sm:h-41 sm:w-41"
|
||||
class="grid h-36 w-36 shrink-0 place-items-center rounded-art-lg bg-[image:var(--mf-liked-grad)] sm:h-41 sm:w-41"
|
||||
>
|
||||
<Heart class="h-10 w-10 text-on-art/90" fill="currentColor" strokeWidth={0} />
|
||||
</div>
|
||||
@@ -76,8 +76,9 @@
|
||||
index
|
||||
action
|
||||
meta={[
|
||||
{ label: 'Álbum', width: '160px' },
|
||||
{ label: 'Añadida', width: '140px' }
|
||||
{ label: 'Álbum', width: '76px' },
|
||||
{ label: 'Añadida', width: '124px' },
|
||||
{ label: 'Escuchas', width: '106px' }
|
||||
]}
|
||||
class="mt-6 sm:mt-8"
|
||||
>
|
||||
@@ -101,6 +102,7 @@
|
||||
/>
|
||||
<TrackMeta class="hidden sm:block">—</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{fmtDate(track.likedAt)}</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount ?? 0)}</TrackMeta>
|
||||
<TrackMeta>—</TrackMeta>
|
||||
<IconButton
|
||||
label="Quitar de Me gusta"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
queueItemForTarget,
|
||||
targetArtist,
|
||||
targetId,
|
||||
targetListensCount,
|
||||
targetTitle
|
||||
} from '$lib/tracks';
|
||||
|
||||
@@ -74,7 +75,7 @@
|
||||
meta="{items.length} {items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}"
|
||||
>
|
||||
{#snippet cover()}
|
||||
<MixArt {items} class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41" />
|
||||
<MixArt {items} class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41" />
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
|
||||
@@ -97,7 +98,7 @@
|
||||
<Alert tone="danger" class="mt-4">{form.message}</Alert>
|
||||
{/if}
|
||||
|
||||
<TrackTable index class="mt-6 sm:mt-8">
|
||||
<TrackTable index meta={[{ label: 'Escuchas', width: '100px' }]} class="mt-6 sm:mt-8">
|
||||
{#each items as item, i (mixItemKey(item))}
|
||||
{@const active = isTargetCurrent(targets[i])}
|
||||
<TrackRow {active} oncontextmenu={(e) => (contextMenu = contextMenuStateFor(e, targets[i]))}>
|
||||
@@ -109,6 +110,7 @@
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
/>
|
||||
<TrackMeta class="hidden sm:block">{Number(targetListensCount(targets[i]) ?? 0)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(item.durationSeconds))}</TrackMeta>
|
||||
</TrackRow>
|
||||
{/each}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
const items = $derived(data.playlists.items);
|
||||
const totals = $derived(data.totals);
|
||||
const summary = $derived(
|
||||
`${totals.playlistCount} ${totals.playlistCount === 1 ? 'playlist' : 'playlists'} · ${totals.trackCount} ${totals.trackCount === 1 ? 'canción' : 'canciones'} · ${fmtDurationLong(totals.durationSeconds)}`
|
||||
`${totals.playlistCount} ${totals.playlistCount === 1 ? 'Playlist' : 'Playlists'} · ${totals.trackCount} ${totals.trackCount === 1 ? 'Canción' : 'Canciones'} · ${fmtDurationLong(totals.durationSeconds)}`
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const meta = $derived(
|
||||
`${trackIds.length} ${trackIds.length === 1 ? 'canción' : 'canciones'} · ${
|
||||
`${trackIds.length} ${trackIds.length === 1 ? 'Canción' : 'Canciones'} · ${
|
||||
visibility === 'Public' ? 'Pública' : 'Privada'
|
||||
}`
|
||||
);
|
||||
@@ -49,16 +49,12 @@
|
||||
{trackIds}
|
||||
size="large"
|
||||
version={updatedAt}
|
||||
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
|
||||
class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
|
||||
{#if playing}
|
||||
Pausar
|
||||
{:else}
|
||||
Reproducir
|
||||
{/if}
|
||||
{playing ? 'Pausar' : 'Reproducir'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onclick={onEdit}>Editar</Button>
|
||||
<Button size="sm" variant="secondary" onclick={onDelete}>Eliminar</Button>
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
||||
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
|
||||
|
||||
type PlaylistTrack = ApiTrackLike & { createdAt: string; duration: number | string };
|
||||
type PlaylistTrack = ApiTrackLike & {
|
||||
createdAt: string;
|
||||
duration: number | string;
|
||||
listensCount: number | string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
tracks: PlaylistTrack[];
|
||||
@@ -27,8 +31,9 @@
|
||||
index
|
||||
action
|
||||
meta={[
|
||||
{ label: 'Álbum', width: '160px' },
|
||||
{ label: 'Añadida', width: '140px' }
|
||||
{ label: 'Álbum', width: '76px' },
|
||||
{ label: 'Añadida', width: '124px' },
|
||||
{ label: 'Escuchas', width: '106px' }
|
||||
]}
|
||||
class="mt-6 sm:mt-8"
|
||||
>
|
||||
@@ -47,6 +52,7 @@
|
||||
/>
|
||||
<TrackMeta class="hidden sm:block">—</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
||||
<form
|
||||
method="POST"
|
||||
|
||||
Reference in New Issue
Block a user