Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
19 changed files with 407 additions and 390 deletions
Showing only changes of commit fb4a8ccc0e - Show all commits
+1 -1
View File
@@ -779,7 +779,7 @@ Tareas:
| P4 | ✅ | (pendiente) | Decisión tomada sin bloquear: se elimina el botón "adelante" decorativo (TopBar) y el icono `AlignJustify` decorativo (PlayerExtras) por ser código muerto visual. `TransportControls` (shuffle/prev/next/repeat) se deja sin migrar a IconButton a propósito: son iconos sin chrome (sin fondo/hit-box), migrarlos habría añadido una píldora de hover no presente hoy — riesgo visual no verificable sin Docker | | P4 | ✅ | (pendiente) | Decisión tomada sin bloquear: se elimina el botón "adelante" decorativo (TopBar) y el icono `AlignJustify` decorativo (PlayerExtras) por ser código muerto visual. `TransportControls` (shuffle/prev/next/repeat) se deja sin migrar a IconButton a propósito: son iconos sin chrome (sin fondo/hit-box), migrarlos habría añadido una píldora de hover no presente hoy — riesgo visual no verificable sin Docker |
| P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas | | P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas |
| P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) | | P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) |
| P7 | ⬜ | | | | P7 | ✅ | (pendiente) | MediaIdentity queda "puro" (sin onClick) según spec; las 6 tablas de pista (P8) envuelven su propio `use:pressable` hasta que P8 las sustituya por TrackList. Deltas documentados: fila activa de Sidebar ahora usa bg-accent-tint/text-accent-soft (antes gris neutro, ahora consistente con el resto de filas activas); tarjeta "Tus playlists" de Inicio pierde la línea "por {tu nombre}" (redundante, siempre el propio usuario) y su radio pasa de rounded-panel a rounded-art (consistencia con la tarjeta "Continuar"); filas de Queue "A continuación" pierden el opacity-80 del arte (ListRow no expone esa personalización). Sin QA visual autenticada (Docker no disponible) |
| P8 | ⬜ | | | | P8 | ⬜ | | |
| P9 | ⬜ | | | | P9 | ⬜ | | |
| P10 | ⬜ | | | | P10 | ⬜ | | |
+7 -16
View File
@@ -4,7 +4,7 @@
import { liked } from '$lib/player/liked.svelte'; import { liked } from '$lib/player/liked.svelte';
import { createPlaylistModal } from '$lib/playlists.svelte'; import { createPlaylistModal } from '$lib/playlists.svelte';
import type { SessionUser } from '$lib/types'; import type { SessionUser } from '$lib/types';
import Artwork from './ui/Artwork.svelte'; import ListRow from './ui/ListRow.svelte';
import IconButton from './ui/IconButton.svelte'; import IconButton from './ui/IconButton.svelte';
import Home from '@lucide/svelte/icons/house'; import Home from '@lucide/svelte/icons/house';
import Compass from '@lucide/svelte/icons/compass'; import Compass from '@lucide/svelte/icons/compass';
@@ -92,26 +92,17 @@
<div class="flex flex-1 flex-col gap-px overflow-y-auto pb-4.5"> <div class="flex flex-1 flex-col gap-px overflow-y-auto pb-4.5">
{#each playlists as playlist (playlist.id)} {#each playlists as playlist (playlist.id)}
{@const active = page.url.pathname === `/playlists/${playlist.id}`} {@const active = page.url.pathname === `/playlists/${playlist.id}`}
<a <ListRow
href="/playlists/{playlist.id}" href="/playlists/{playlist.id}"
class="flex items-center gap-2.75 rounded-control px-3 py-1.75 transition-colors duration-150 {active {active}
? 'bg-surface-hover' size="xs"
: 'hover:bg-hover'}" title={playlist.name}
> coverSrc="/api/playlists/{playlist.id}/cover?size=small&v={encodeURIComponent(
<Artwork
src="/api/playlists/{playlist.id}/cover?size=small&v={encodeURIComponent(
playlist.updatedAt playlist.updatedAt
)}" )}"
trackIds={playlist.coverTrackIds} trackIds={playlist.coverTrackIds}
size="xs" class="px-3 py-1.75"
class="shrink-0 opacity-90"
/> />
<div class="min-w-0 flex-1">
<div class="truncate text-sm {active ? 'text-fg' : 'text-fg-2'}">
{playlist.name}
</div>
</div>
</a>
{:else} {:else}
<button <button
type="button" type="button"
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { player } from '$lib/player/player.svelte'; import { player } from '$lib/player/player.svelte';
import { queuePanel } from '$lib/player/queuePanel.svelte'; import { queuePanel } from '$lib/player/queuePanel.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import ArtistLink from '$lib/components/ui/ArtistLink.svelte'; import ListRow from '$lib/components/ui/ListRow.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte'; import IconButton from '$lib/components/ui/IconButton.svelte';
import { pressable } from '$lib/actions/pressable';
import { fmtTime } from '$lib/format'; import { fmtTime } from '$lib/format';
import X from '@lucide/svelte/icons/x'; import X from '@lucide/svelte/icons/x';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@@ -31,24 +30,15 @@
{#if player.current.id} {#if player.current.id}
<p class="mb-2.75 text-xs font-medium tracking-widest text-fg-3 uppercase">Reproduciendo</p> <p class="mb-2.75 text-xs font-medium tracking-widest text-fg-3 uppercase">Reproduciendo</p>
<div class="mb-6 flex items-center gap-3 rounded-xl bg-accent-tint p-2.5"> <div class="mb-6 rounded-control bg-accent-tint p-2.5">
<Artwork <MediaIdentity
trackIds={[player.current.id]} trackId={player.current.id}
size="sm" title={player.current.title}
alt={player.current.title} artist={player.current.artist || '—'}
class="shrink-0"
/>
<div class="min-w-0">
<div class="truncate text-sm text-accent-soft">
{player.current.title}
</div>
<ArtistLink
name={player.current.artist || '—'}
ownerUserId={player.current.ownerUserId} ownerUserId={player.current.ownerUserId}
class="mt-0.5 text-xs text-fg-3" active
/> />
</div> </div>
</div>
{/if} {/if}
<div class="mb-2.75 flex items-center justify-between gap-3"> <div class="mb-2.75 flex items-center justify-between gap-3">
@@ -69,25 +59,22 @@
<div class="flex flex-1 flex-col gap-px overflow-y-auto"> <div class="flex flex-1 flex-col gap-px overflow-y-auto">
{#each upcoming as track, i (track.id)} {#each upcoming as track, i (track.id)}
{@const index = player.tracks.length - upcoming.length + i} {@const index = player.tracks.length - upcoming.length + i}
<div <ListRow
role="button" onclick={() => player.playQueueIndex(index)}
tabindex="0" size="sm"
use:pressable={() => player.playQueueIndex(index)} title={track.title}
class="flex items-center gap-3 rounded-control p-2.25 text-left hover:bg-hover" subtitle={track.artist || '—'}
subtitleHref={track.ownerUserId}
trackId={track.id}
class="p-2.25"
> >
<Artwork trackIds={[track.id]} size="sm" alt={track.title} class="shrink-0 opacity-80" /> {#snippet trailing()}
<div class="min-w-0 flex-1">
<div class="truncate text-xs text-fg">{track.title}</div>
<ArtistLink
name={track.artist || '—'}
ownerUserId={track.ownerUserId}
class="mt-0.5 text-xs text-fg-3"
/>
</div>
{#if track.duration} {#if track.duration}
<span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(track.duration)}</span> <span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(track.duration)}</span
>
{/if} {/if}
</div> {/snippet}
</ListRow>
{:else} {:else}
<p class="px-2 py-6 text-center text-xs text-fg-3">No hay más canciones en la cola.</p> <p class="px-2 py-6 text-center text-xs text-fg-3">No hay más canciones en la cola.</p>
{/each} {/each}
@@ -21,7 +21,7 @@
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import type { LucideIcon } from '@lucide/svelte'; import type { LucideIcon } from '@lucide/svelte';
import ChevronRight from '@lucide/svelte/icons/chevron-right'; import ChevronRight from '@lucide/svelte/icons/chevron-right';
import Artwork from './Artwork.svelte'; import MediaIdentity from './MediaIdentity.svelte';
import GlassMenu from './GlassMenu.svelte'; import GlassMenu from './GlassMenu.svelte';
import MenuItem from './MenuItem.svelte'; import MenuItem from './MenuItem.svelte';
@@ -93,14 +93,14 @@
{/each} {/each}
<button <button
type="submit" type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover" class="flex w-full items-center rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
> >
<Artwork <MediaIdentity
src="/api/playlists/{playlist.id}/cover?size=small" title={playlist.name}
coverSrc="/api/playlists/{playlist.id}/cover?size=small"
size="xs" size="xs"
class="shrink-0" titleClass="text-fg-2"
/> />
<span class="truncate text-fg-2">{playlist.name}</span>
</button> </button>
</form> </form>
{/each} {/each}
+23 -3
View File
@@ -3,15 +3,34 @@
size?: number; size?: number;
barWidth?: number; barWidth?: number;
paused?: boolean; paused?: boolean;
overlay?: boolean;
class?: string; class?: string;
} }
let { size = 13, barWidth = 2, paused = false, class: klass = '' }: Props = $props(); let {
size = 13,
barWidth = 2,
paused = false,
overlay = false,
class: klass = ''
}: Props = $props();
const delays = [0, 0.3, 0.6]; const delays = [0, 0.3, 0.6];
</script> </script>
<div class="flex items-end justify-center gap-0.5 {klass}" style="height:{size}px"> {#if overlay}
<div class="absolute inset-0 flex items-end justify-center gap-0.75 bg-scrim/45 pb-2.5 {klass}">
{#each delays as delay (delay)}
<span
class="h-3.5 w-0.75 origin-bottom rounded-full bg-on-art"
style="animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
></span>
{/each}
</div>
{:else}
<div class="flex items-end justify-center gap-0.5 {klass}" style="height:{size}px">
{#each delays as delay (delay)} {#each delays as delay (delay)}
<span <span
class="rounded-full bg-accent" class="rounded-full bg-accent"
@@ -20,4 +39,5 @@
: 'running'}" : 'running'}"
></span> ></span>
{/each} {/each}
</div> </div>
{/if}
@@ -0,0 +1,90 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import MediaIdentity from './MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
interface Props {
href?: string;
onclick?: () => void;
oncontextmenu?: (event: MouseEvent) => void;
active?: boolean;
size?: 'xs' | 'sm' | 'lg';
variant?: 'row' | 'card';
title: string;
subtitle?: string | null;
subtitleHref?: string | number | null;
explicit?: boolean;
trackId?: string | number;
trackIds?: (string | number)[];
coverSrc?: string | null;
art?: Snippet;
overlay?: Snippet;
leading?: Snippet;
trailing?: Snippet;
class?: string;
style?: string;
}
let {
href,
onclick,
oncontextmenu,
active = false,
size = 'sm',
variant = 'row',
title,
subtitle,
subtitleHref,
explicit = false,
trackId,
trackIds,
coverSrc,
art,
overlay,
leading,
trailing,
class: klass = '',
style
}: Props = $props();
const classes = $derived(
`flex items-center gap-3 text-left transition-colors ${
variant === 'card' ? 'rounded-art p-2.25 pr-3.5' : 'rounded-control p-2'
} ${active ? 'bg-accent-tint' : 'hover:bg-hover'} ${klass}`
);
</script>
{#snippet content()}
{@render leading?.()}
<MediaIdentity
{title}
artist={subtitle}
ownerUserId={subtitleHref}
{trackId}
{trackIds}
{coverSrc}
{size}
{explicit}
{active}
{art}
{overlay}
/>
{@render trailing?.()}
{/snippet}
{#if href}
<a {href} {oncontextmenu} class={classes} {style}>
{@render content()}
</a>
{:else}
<div
role="button"
tabindex="0"
use:pressable={onclick ?? (() => {})}
{oncontextmenu}
class={classes}
{style}
>
{@render content()}
</div>
{/if}
@@ -3,25 +3,27 @@
import Artwork from './Artwork.svelte'; import Artwork from './Artwork.svelte';
import ExplicitBadge from './ExplicitBadge.svelte'; import ExplicitBadge from './ExplicitBadge.svelte';
import ArtistLink from './ArtistLink.svelte'; import ArtistLink from './ArtistLink.svelte';
import { pressable } from '$lib/actions/pressable';
interface Props { interface Props {
trackId: string | number; trackId?: string | number;
trackIds?: (string | number)[];
title: string; title: string;
artist?: string | null; artist?: string | null;
ownerUserId?: string | number | null; ownerUserId?: string | number | null;
coverSrc?: string | null; coverSrc?: string | null;
size?: 'sm' | 'lg'; size?: 'xs' | 'sm' | 'lg';
explicit?: boolean; explicit?: boolean;
active?: boolean; active?: boolean;
titleClass?: string; titleClass?: string;
onClick: () => void; class?: string;
art?: Snippet;
overlay?: Snippet; overlay?: Snippet;
badge?: Snippet; badge?: Snippet;
} }
let { let {
trackId, trackId,
trackIds,
title, title,
artist, artist,
ownerUserId, ownerUserId,
@@ -30,23 +32,27 @@
explicit = false, explicit = false,
active = false, active = false,
titleClass = '', titleClass = '',
onClick, class: klass = '',
art,
overlay, overlay,
badge badge
}: Props = $props(); }: Props = $props();
</script> </script>
<!-- svelte-ignore a11y_click_events_have_key_events --> <div class="flex min-w-0 flex-1 items-center gap-3.5 {klass}">
<div {#if art}
role="button" {@render art()}
tabindex="0" {:else if trackId !== undefined || coverSrc || trackIds?.length}
use:pressable={onClick} <Artwork
onclick={(event) => event.stopPropagation()} trackIds={trackIds ?? (trackId !== undefined ? [trackId] : [])}
class="flex min-w-0 items-center gap-3.5 text-left" src={coverSrc}
> {size}
<Artwork trackIds={[trackId]} src={coverSrc} {size} alt={title} class="shrink-0"> alt={title}
class="shrink-0"
>
{@render overlay?.()} {@render overlay?.()}
</Artwork> </Artwork>
{/if}
<div class="min-w-0"> <div class="min-w-0">
<div class="flex min-w-0 items-center gap-2"> <div class="flex min-w-0 items-center gap-2">
{#if explicit} {#if explicit}
@@ -1,20 +0,0 @@
<script lang="ts">
interface Props {
paused?: boolean;
}
let { paused = false }: Props = $props();
const delays = [0, 0.3, 0.6];
</script>
<div class="absolute inset-0 flex items-end justify-center gap-0.75 bg-scrim/45 pb-2.5">
{#each delays as delay (delay)}
<span
class="h-3.5 w-0.75 origin-bottom rounded-full bg-on-art"
style="animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
></span>
{/each}
</div>
@@ -1,65 +0,0 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import ExplicitBadge from './ExplicitBadge.svelte';
interface Props {
title: string;
subtitle?: string | null;
meta?: string;
explicit?: boolean;
href?: string;
onclick?: () => void;
oncontextmenu?: (event: MouseEvent) => void;
art: Snippet<[string]>;
}
let {
title,
subtitle,
meta,
explicit = false,
href,
onclick,
oncontextmenu,
art
}: Props = $props();
const artClass = 'relative shrink-0';
</script>
{#snippet content()}
{@render art(artClass)}
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-1.75">
{#if explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-sm font-medium text-fg">{title}</span>
</div>
{#if subtitle}
<div class="mt-0.75 truncate text-xs text-fg-3">{subtitle}</div>
{/if}
</div>
{#if meta}
<span class="shrink-0 text-xs text-muted tabular-nums">{meta}</span>
{/if}
{/snippet}
{#if href}
<a
{href}
{oncontextmenu}
class="group/row flex w-full min-w-0 items-center gap-3.5 rounded-control p-2 pr-4 text-left transition hover:bg-surface"
>
{@render content()}
</a>
{:else}
<button
type="button"
{onclick}
{oncontextmenu}
class="group/row flex w-full min-w-0 items-center gap-3.5 rounded-control p-2 pr-4 text-left transition hover:bg-surface focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none"
>
{@render content()}
</button>
{/if}
@@ -8,7 +8,8 @@
import TrackRow from '$lib/components/ui/TrackRow.svelte'; import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte'; import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
type AlbumTrack = ApiTrackLike & { duration: number | string; listensCount: number | string }; type AlbumTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
@@ -34,15 +35,23 @@
{@const active = player.current.id === track.id} {@const active = player.current.id === track.id}
<TrackRow {active}> <TrackRow {active}>
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} /> <TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
<TrackTitleCell <!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
use:pressable={() => playFrom(i)}
onclick={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity
trackId={track.id} trackId={track.id}
title={track.title} title={track.title}
artist={track.artist} artist={track.artist}
ownerUserId={track.ownerUserId} ownerUserId={track.ownerUserId}
explicit={track.isExplicit} explicit={track.isExplicit}
{active} {active}
onClick={() => playFrom(i)}
/> />
</div>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta> <TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta> <TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
{#if isOwner} {#if isOwner}
@@ -8,8 +8,9 @@
import TrackTable from '$lib/components/ui/TrackTable.svelte'; import TrackTable from '$lib/components/ui/TrackTable.svelte';
import TrackRow from '$lib/components/ui/TrackRow.svelte'; import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte'; import { pressable } from '$lib/actions/pressable';
import EqBars from '$lib/components/ui/EqBars.svelte';
type LibraryTrack = ApiTrackLike & { duration: number | string; listensCount: number | string }; type LibraryTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
@@ -30,21 +31,29 @@
{#each library as track, i (track.id)} {#each library as track, i (track.id)}
{@const active = player.current.id === track.id} {@const active = player.current.id === track.id}
<TrackRow {active}> <TrackRow {active}>
<TrackTitleCell <!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
use:pressable={() => playFrom(i)}
onclick={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity
trackId={track.id} trackId={track.id}
title={track.title} title={track.title}
artist={track.artist} artist={track.artist}
ownerUserId={track.ownerUserId} ownerUserId={track.ownerUserId}
explicit={track.isExplicit} explicit={track.isExplicit}
{active} {active}
onClick={() => playFrom(i)}
> >
{#snippet overlay()} {#snippet overlay()}
{#if player.current.id === track.id} {#if player.current.id === track.id}
<NowPlaying paused={!player.playing} /> <EqBars overlay paused={!player.playing} />
{/if} {/if}
{/snippet} {/snippet}
</TrackTitleCell> </MediaIdentity>
</div>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta> <TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta> <TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
<form <form
@@ -11,8 +11,8 @@
import Artwork from '$lib/components/ui/Artwork.svelte'; import Artwork from '$lib/components/ui/Artwork.svelte';
import Avatar from '$lib/components/ui/Avatar.svelte'; import Avatar from '$lib/components/ui/Avatar.svelte';
import PlayButton from '$lib/components/ui/PlayButton.svelte'; import PlayButton from '$lib/components/ui/PlayButton.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte'; import EqBars from '$lib/components/ui/EqBars.svelte';
import SearchResultRow from '$lib/components/ui/SearchResultRow.svelte'; import ListRow from '$lib/components/ui/ListRow.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte'; import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import { EXPLORE_PAGE_SIZE } from '$lib/config'; import { EXPLORE_PAGE_SIZE } from '$lib/config';
import { searchHref } from '$lib/navigation.svelte'; import { searchHref } from '$lib/navigation.svelte';
@@ -30,6 +30,7 @@
targetExplicit, targetExplicit,
targetId, targetId,
targetListensCount, targetListensCount,
targetOwnerUserId,
targetTitle, targetTitle,
type TrackTarget type TrackTarget
} from '$lib/tracks'; } from '$lib/tracks';
@@ -410,27 +411,28 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
{#each capped(songRows) as { target: item, seconds }, i (targetId(item))} {#each capped(songRows) as { target: item, seconds }, i (targetId(item))}
<li> <li>
<SearchResultRow <ListRow
title={targetTitle(item)} title={targetTitle(item)}
subtitle={targetArtist(item)} subtitle={targetArtist(item)}
meta="{fmtTime(seconds)} · {fmtPlays(targetListensCount(item))}" subtitleHref={targetOwnerUserId(item)}
explicit={targetExplicit(item)} explicit={targetExplicit(item)}
active={isTargetCurrent(item)}
size="lg"
trackId={targetId(item)}
onclick={() => togglePlay(i)} onclick={() => togglePlay(i)}
oncontextmenu={(e) => openContextMenu(e, item)} oncontextmenu={(e) => openContextMenu(e, item)}
> >
{#snippet art(artClass)} {#snippet overlay()}
<Artwork
trackIds={[targetId(item)]}
size="lg"
alt={targetTitle(item)}
class="{artClass} ring-1 ring-line ring-inset"
>
{#if isTargetCurrent(item)} {#if isTargetCurrent(item)}
<NowPlaying paused={!player.playing} /> <EqBars overlay paused={!player.playing} />
{/if} {/if}
</Artwork>
{/snippet} {/snippet}
</SearchResultRow> {#snippet trailing()}
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
{fmtTime(seconds)} · {fmtPlays(targetListensCount(item))}
</span>
{/snippet}
</ListRow>
</li> </li>
{/each} {/each}
</ul> </ul>
@@ -460,16 +462,19 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
{#each capped(albums) as album (album.id)} {#each capped(albums) as album (album.id)}
<li> <li>
<SearchResultRow <ListRow
title={album.title} title={album.title}
meta="{Number(album.trackCount)} canciones"
href="/albums/{album.id}" href="/albums/{album.id}"
size="lg"
trackIds={album.coverTrackIds}
oncontextmenu={(e) => openAlbumMenu(e, album.id)} oncontextmenu={(e) => openAlbumMenu(e, album.id)}
> >
{#snippet art(artClass)} {#snippet trailing()}
<Artwork trackIds={album.coverTrackIds} size="lg" class={artClass} /> <span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
{Number(album.trackCount)} canciones
</span>
{/snippet} {/snippet}
</SearchResultRow> </ListRow>
</li> </li>
{/each} {/each}
</ul> </ul>
@@ -489,11 +494,11 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
{#each capped(users) as u (u.id)} {#each capped(users) as u (u.id)}
<li> <li>
<SearchResultRow title={u.name} href="/u/{u.id}"> <ListRow title={u.name} href="/u/{u.id}" size="lg">
{#snippet art(artClass)} {#snippet art()}
<Avatar name={u.name} src={u.profilePictureUrl} size="md" class={artClass} /> <Avatar name={u.name} src={u.profilePictureUrl} size="md" />
{/snippet} {/snippet}
</SearchResultRow> </ListRow>
</li> </li>
{/each} {/each}
</ul> </ul>
@@ -513,22 +518,16 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
{#each capped(playlistMatches) as playlist (playlist.id)} {#each capped(playlistMatches) as playlist (playlist.id)}
<li> <li>
<SearchResultRow <ListRow
title={playlist.name} title={playlist.name}
subtitle={playlist.description} subtitle={playlist.description}
href="/playlists/{playlist.id}" href="/playlists/{playlist.id}"
> size="lg"
{#snippet art(artClass)} coverSrc="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
<Artwork
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
playlist.updatedAt playlist.updatedAt
)}" )}"
trackIds={playlist.coverTrackIds} trackIds={playlist.coverTrackIds}
size="lg"
class={artClass}
/> />
{/snippet}
</SearchResultRow>
</li> </li>
{/each} {/each}
</ul> </ul>
@@ -4,7 +4,7 @@
import X from '@lucide/svelte/icons/x'; import X from '@lucide/svelte/icons/x';
import Music from '@lucide/svelte/icons/music'; import Music from '@lucide/svelte/icons/music';
import { player, toQueueItems } from '$lib/player/player.svelte'; import { player, toQueueItems } from '$lib/player/player.svelte';
import { fmtTime, fmtDate } from '$lib/format'; import { fmtTime, fmtDate, plural } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte'; import Page from '$lib/components/ui/Page.svelte';
import PageHeader from '$lib/components/ui/PageHeader.svelte'; import PageHeader from '$lib/components/ui/PageHeader.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte'; import EmptyState from '$lib/components/ui/EmptyState.svelte';
@@ -14,7 +14,8 @@
import TrackRow from '$lib/components/ui/TrackRow.svelte'; import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte'; import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
let { data } = $props(); let { data } = $props();
@@ -34,7 +35,7 @@
<Page> <Page>
<PageHeader <PageHeader
title="Tus canciones subidas" title="Tus canciones subidas"
subtitle="{total} {total === 1 ? 'canción' : 'canciones'} subida{total === 1 ? '' : 's'}." subtitle="{plural(total, 'canción', 'canciones')} subida{total === 1 ? '' : 's'}."
> >
{#snippet actions()} {#snippet actions()}
{#if items.length > 0} {#if items.length > 0}
@@ -66,12 +67,16 @@
playing={player.playing} playing={player.playing}
onToggle={() => togglePlay(i)} onToggle={() => togglePlay(i)}
/> />
<TrackTitleCell <!-- svelte-ignore a11y_click_events_have_key_events -->
trackId={track.id} <div
title={track.title} role="button"
{active} tabindex="0"
onClick={() => togglePlay(i)} use:pressable={() => togglePlay(i)}
/> onclick={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity trackId={track.id} title={track.title} {active} />
</div>
<TrackMeta class="hidden sm:block">—</TrackMeta> <TrackMeta class="hidden sm:block">—</TrackMeta>
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta> <TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta> <TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
+14 -5
View File
@@ -3,7 +3,7 @@
import X from '@lucide/svelte/icons/x'; import X from '@lucide/svelte/icons/x';
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte'; import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
import { liked } from '$lib/player/liked.svelte'; import { liked } from '$lib/player/liked.svelte';
import { fmtDate, fmtTime } from '$lib/format'; import { fmtDate, fmtTime, plural } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte'; import Page from '$lib/components/ui/Page.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte'; import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
@@ -13,7 +13,8 @@
import TrackRow from '$lib/components/ui/TrackRow.svelte'; import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte'; import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
const tracks = $derived(liked.list); const tracks = $derived(liked.list);
const isCurrentQueue = $derived(isQueueCurrent(tracks)); const isCurrentQueue = $derived(isQueueCurrent(tracks));
@@ -36,7 +37,7 @@
<CollectionHeader <CollectionHeader
eyebrow="Colección" eyebrow="Colección"
title="Me gusta" title="Me gusta"
meta="{tracks.length} {tracks.length === 1 ? 'Canción' : 'Canciones'}" meta={plural(tracks.length, 'canción', 'canciones')}
align="center" align="center"
> >
{#snippet cover()} {#snippet cover()}
@@ -73,15 +74,23 @@
playing={player.playing} playing={player.playing}
onToggle={() => playFrom(i)} onToggle={() => playFrom(i)}
/> />
<TrackTitleCell <!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
use:pressable={() => playFrom(i)}
onclick={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity
trackId={track.id} trackId={track.id}
title={track.title} title={track.title}
artist={track.artist} artist={track.artist}
ownerUserId={track.ownerUserId} ownerUserId={track.ownerUserId}
explicit={track.explicit} explicit={track.explicit}
{active} {active}
onClick={() => playFrom(i)}
/> />
</div>
<TrackMeta class="hidden sm:block">—</TrackMeta> <TrackMeta class="hidden sm:block">—</TrackMeta>
<TrackMeta class="hidden sm:block">{fmtDate(track.likedAt)}</TrackMeta> <TrackMeta class="hidden sm:block">{fmtDate(track.likedAt)}</TrackMeta>
<TrackMeta class="hidden sm:block">{Number(track.listensCount ?? 0)}</TrackMeta> <TrackMeta class="hidden sm:block">{Number(track.listensCount ?? 0)}</TrackMeta>
@@ -3,7 +3,7 @@
import Pause from '@lucide/svelte/icons/pause'; import Pause from '@lucide/svelte/icons/pause';
import Shuffle from '@lucide/svelte/icons/shuffle'; import Shuffle from '@lucide/svelte/icons/shuffle';
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte'; import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format'; import { fmtTime, plural } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte'; import Page from '$lib/components/ui/Page.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte'; import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
@@ -14,7 +14,8 @@
import TrackRow from '$lib/components/ui/TrackRow.svelte'; import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte'; import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'; import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import ListPlus from '@lucide/svelte/icons/list-plus'; import ListPlus from '@lucide/svelte/icons/list-plus';
import { createTrackMenu } from '$lib/menus.svelte'; import { createTrackMenu } from '$lib/menus.svelte';
@@ -59,7 +60,7 @@
eyebrow="Mezcla" eyebrow="Mezcla"
title={mix.title} title={mix.title}
description={mix.subtitle ?? undefined} description={mix.subtitle ?? undefined}
meta="{items.length} {items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}" meta="{plural(items.length, 'canción', 'canciones')} · {fmtTime(totalSeconds)}"
> >
{#snippet cover()} {#snippet cover()}
<Artwork <Artwork
@@ -100,13 +101,21 @@
{@const active = isTargetCurrent(targets[i])} {@const active = isTargetCurrent(targets[i])}
<TrackRow {active} oncontextmenu={(e) => trackMenu.open(e, targets[i])}> <TrackRow {active} oncontextmenu={(e) => trackMenu.open(e, targets[i])}>
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} /> <TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
<TrackTitleCell <!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
use:pressable={() => playFrom(i)}
onclick={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity
trackId={targetId(targets[i])} trackId={targetId(targets[i])}
title={targetTitle(targets[i])} title={targetTitle(targets[i])}
artist={targetArtist(targets[i])} artist={targetArtist(targets[i])}
{active} {active}
onClick={() => playFrom(i)}
/> />
</div>
<TrackMeta class="hidden sm:block">{Number(targetListensCount(targets[i]) ?? 0)}</TrackMeta> <TrackMeta class="hidden sm:block">{Number(targetListensCount(targets[i]) ?? 0)}</TrackMeta>
<TrackMeta>{fmtTime(Number(item.durationSeconds))}</TrackMeta> <TrackMeta>{fmtTime(Number(item.durationSeconds))}</TrackMeta>
</TrackRow> </TrackRow>
@@ -7,14 +7,14 @@
import EmptyState from '$lib/components/ui/EmptyState.svelte'; import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
import { createPlaylistModal } from '$lib/playlists.svelte'; import { createPlaylistModal } from '$lib/playlists.svelte';
import { fmtDurationLong } from '$lib/format'; import { fmtDurationLong, plural } from '$lib/format';
let { data } = $props(); let { data } = $props();
const items = $derived(data.playlists.items); const items = $derived(data.playlists.items);
const totals = $derived(data.totals); const totals = $derived(data.totals);
const summary = $derived( const summary = $derived(
`${totals.playlistCount} ${totals.playlistCount === 1 ? 'Playlist' : 'Playlists'} · ${totals.trackCount} ${totals.trackCount === 1 ? 'Canción' : 'Canciones'} · ${fmtDurationLong(totals.durationSeconds)}` `${plural(totals.playlistCount, 'playlist', 'playlists')} · ${plural(totals.trackCount, 'canción', 'canciones')} · ${fmtDurationLong(totals.durationSeconds)}`
); );
</script> </script>
@@ -2,6 +2,7 @@
import Artwork from '$lib/components/ui/Artwork.svelte'; import Artwork from '$lib/components/ui/Artwork.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte'; import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import { plural } from '$lib/format';
interface Props { interface Props {
id: string; id: string;
@@ -30,7 +31,7 @@
}: Props = $props(); }: Props = $props();
const meta = $derived( const meta = $derived(
`${trackIds.length} ${trackIds.length === 1 ? 'Canción' : 'Canciones'} · ${ `${plural(trackIds.length, 'canción', 'canciones')} · ${
visibility === 'Public' ? 'Pública' : 'Privada' visibility === 'Public' ? 'Pública' : 'Privada'
}` }`
); );
@@ -8,7 +8,8 @@
import TrackRow from '$lib/components/ui/TrackRow.svelte'; import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte'; import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
type PlaylistTrack = ApiTrackLike & { type PlaylistTrack = ApiTrackLike & {
createdAt: string; createdAt: string;
@@ -41,15 +42,23 @@
{@const active = player.current.id === track.id} {@const active = player.current.id === track.id}
<TrackRow {active}> <TrackRow {active}>
<TrackIndexCell index={i} playing={player.playing} onToggle={() => playFrom(i)} {active} /> <TrackIndexCell index={i} playing={player.playing} onToggle={() => playFrom(i)} {active} />
<TrackTitleCell <!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
use:pressable={() => playFrom(i)}
onclick={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity
trackId={track.id} trackId={track.id}
title={track.title} title={track.title}
artist={track.artist} artist={track.artist}
ownerUserId={track.ownerUserId} ownerUserId={track.ownerUserId}
explicit={track.isExplicit} explicit={track.isExplicit}
{active} {active}
onClick={() => playFrom(i)}
/> />
</div>
<TrackMeta class="hidden sm:block">—</TrackMeta> <TrackMeta class="hidden sm:block">—</TrackMeta>
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta> <TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta> <TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
+48 -90
View File
@@ -6,7 +6,7 @@
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte'; import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
import { pressable } from '$lib/actions/pressable'; import { pressable } from '$lib/actions/pressable';
import { mergeRecentlyPlayed } from '$lib/recentlyPlayed'; import { mergeRecentlyPlayed } from '$lib/recentlyPlayed';
import { fmtTime, fmtDurationLong, fmtPlays } from '$lib/format'; import { fmtTime, fmtDurationLong, fmtPlays, plural } from '$lib/format';
import { import {
isTargetCurrent, isTargetCurrent,
queueItemForTarget, queueItemForTarget,
@@ -19,11 +19,10 @@
targetTitle, targetTitle,
type TrackTarget type TrackTarget
} from '$lib/tracks'; } from '$lib/tracks';
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte'; import Artwork from '$lib/components/ui/Artwork.svelte';
import MixTile from '$lib/components/ui/MixTile.svelte'; import MixTile from '$lib/components/ui/MixTile.svelte';
import EqBars from '$lib/components/ui/EqBars.svelte'; import EqBars from '$lib/components/ui/EqBars.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import ListRow from '$lib/components/ui/ListRow.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte'; import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte'; import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
@@ -72,7 +71,7 @@
spotlightTrackCount === 0 spotlightTrackCount === 0
? spotlightVisibilityLabel ? spotlightVisibilityLabel
: [ : [
`${spotlightTrackCount} ${spotlightTrackCount === 1 ? 'Canción' : 'Canciones'}`, plural(spotlightTrackCount, 'canción', 'canciones'),
fmtDurationLong(spotlightDurationSeconds), fmtDurationLong(spotlightDurationSeconds),
spotlightVisibilityLabel spotlightVisibilityLabel
].join(' · ') ].join(' · ')
@@ -176,26 +175,19 @@
<div class="grid grid-cols-2 gap-2.5 sm:grid-cols-3 lg:grid-cols-5"> <div class="grid grid-cols-2 gap-2.5 sm:grid-cols-3 lg:grid-cols-5">
{#each continueItems as item, i (item.id)} {#each continueItems as item, i (item.id)}
{@const isCurrent = player.current.id === item.id} {@const isCurrent = player.current.id === item.id}
<div <ListRow
role="button" onclick={() => playContinue(i)}
tabindex="0"
use:pressable={() => playContinue(i)}
class="animate-enter rounded-art p-2.25 pr-3.5 transition-colors {isCurrent
? 'bg-accent-tint'
: 'hover:bg-hover'}"
style="--i:{i}"
oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(item))} oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(item))}
>
<TrackTitleCell
trackId={item.id}
title={item.title}
artist={item.artist}
ownerUserId={item.ownerUserId}
size="lg"
active={isCurrent} active={isCurrent}
onClick={() => playContinue(i)} size="lg"
variant="card"
title={item.title}
subtitle={item.artist}
subtitleHref={item.ownerUserId}
trackId={item.id}
class="animate-enter"
style="--i:{i}"
/> />
</div>
{/each} {/each}
</div> </div>
{:else} {:else}
@@ -254,33 +246,22 @@
</div> </div>
<div class="flex flex-col gap-0.5 p-5.5 sm:p-6"> <div class="flex flex-col gap-0.5 p-5.5 sm:p-6">
{#each spotlightRows as { target, seconds }, i (targetId(target))} {#each spotlightRows as { target, seconds }, i (targetId(target))}
<div <ListRow
role="button" onclick={() => playSpotlightTrack(i)}
tabindex="0" active={isTargetCurrent(target)}
use:pressable={() => playSpotlightTrack(i)} size="sm"
class="flex items-center gap-4 rounded-control p-2 text-left transition-colors {isTargetCurrent( title={targetTitle(target)}
target subtitle={targetArtist(target)}
) subtitleHref={targetOwnerUserId(target)}
? 'bg-accent-tint'
: 'hover:bg-hover'}"
> >
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span> {#snippet leading()}
<div class="min-w-0 flex-1"> <span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span
<div
class="truncate text-sm {isTargetCurrent(target)
? 'text-accent-soft'
: 'text-fg'}"
> >
{targetTitle(target)} {/snippet}
</div> {#snippet trailing()}
<ArtistLink
name={targetArtist(target)}
ownerUserId={targetOwnerUserId(target)}
class="mt-0.5 text-xs text-fg-3"
/>
</div>
<span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(seconds)}</span> <span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(seconds)}</span>
</div> {/snippet}
</ListRow>
{:else} {:else}
<p class="p-2 text-sm text-fg-3">Esta playlist todavía no tiene canciones.</p> <p class="p-2 text-sm text-fg-3">Esta playlist todavía no tiene canciones.</p>
{/each} {/each}
@@ -314,15 +295,18 @@
<div class="grid grid-cols-1 gap-x-8.5 gap-y-1.5 lg:grid-cols-2"> <div class="grid grid-cols-1 gap-x-8.5 gap-y-1.5 lg:grid-cols-2">
{#each popularTargets as target, i (targetId(target))} {#each popularTargets as target, i (targetId(target))}
{@const isCurrent = isTargetCurrent(target)} {@const isCurrent = isTargetCurrent(target)}
<div <ListRow
role="button" onclick={() => playPopular(i)}
tabindex="0"
use:pressable={() => playPopular(i)}
class="flex items-center gap-3.5 rounded-control p-2 transition-colors {isCurrent
? 'bg-accent-tint'
: 'hover:bg-hover'}"
oncontextmenu={(e) => openContextMenu(e, target)} oncontextmenu={(e) => openContextMenu(e, target)}
active={isCurrent}
size="sm"
title={targetTitle(target)}
subtitle={targetArtist(target)}
subtitleHref={targetOwnerUserId(target)}
explicit={targetExplicit(target)}
trackId={targetId(target)}
> >
{#snippet leading()}
<span class="flex h-3.5 w-4.5 shrink-0 items-end justify-center"> <span class="flex h-3.5 w-4.5 shrink-0 items-end justify-center">
{#if isCurrent} {#if isCurrent}
<EqBars size={13} paused={!player.playing} /> <EqBars size={13} paused={!player.playing} />
@@ -330,21 +314,13 @@
<span class="text-xs text-muted tabular-nums">{i + 1}</span> <span class="text-xs text-muted tabular-nums">{i + 1}</span>
{/if} {/if}
</span> </span>
<div class="min-w-0 flex-1"> {/snippet}
<TrackTitleCell {#snippet trailing()}
trackId={targetId(target)}
title={targetTitle(target)}
artist={targetArtist(target)}
ownerUserId={targetOwnerUserId(target)}
explicit={targetExplicit(target)}
active={isCurrent}
onClick={() => playPopular(i)}
/>
</div>
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block"> <span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
{fmtPlays(targetListensCount(target))} {fmtPlays(targetListensCount(target))}
</span> </span>
</div> {/snippet}
</ListRow>
{/each} {/each}
</div> </div>
{:else} {:else}
@@ -377,36 +353,18 @@
{#if playlists.length > 0} {#if playlists.length > 0}
<div class="grid grid-cols-[repeat(auto-fill,minmax(258px,1fr))] gap-3.5"> <div class="grid grid-cols-[repeat(auto-fill,minmax(258px,1fr))] gap-3.5">
{#each playlists as playlist, i (playlist.id)} {#each playlists as playlist, i (playlist.id)}
<div <ListRow
role="button" onclick={() => goto(`/playlists/${playlist.id}`)}
tabindex="0" size="lg"
use:pressable={() => goto(`/playlists/${playlist.id}`)} variant="card"
class="group/card animate-enter flex items-center gap-3.5 rounded-panel p-3 transition-colors hover:bg-hover" title={playlist.name}
style="--i:{i}" subtitle={playlist.description}
> coverSrc="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
<Artwork
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
playlist.updatedAt playlist.updatedAt
)}" )}"
trackIds={playlist.coverTrackIds} trackIds={playlist.coverTrackIds}
size="lg" style="--i:{i}"
alt={playlist.name}
class="shrink-0"
/> />
<div class="min-w-0 flex-1">
<div class="truncate text-sm tracking-tight text-fg">
{playlist.name}
</div>
{#if playlist.description}
<div class="mt-1 truncate text-xs text-fg-3">{playlist.description}</div>
{/if}
<ArtistLink
name={data.user?.name}
ownerUserId={data.user?.sub}
class="mt-1 text-xs text-fg-3"
/>
</div>
</div>
{/each} {/each}
</div> </div>
{:else} {:else}