web-player: rows (ListRow, MediaIdentity, EqBars)
Add MediaIdentity.svelte (renames and generalizes TrackTitleCell, purely presentational: art + title + subtitle, no click handling) and ListRow.svelte on top of it (href|onclick, active, size xs/sm/lg, variant row|card, leading/trailing/overlay). Also used by player/TrackInfo... (TrackInfo itself is left for a later pass, which already has an explicit task to touch that file). Migrated to ListRow: SearchResultRow (Explore, 4 groups — component removed), Home (Continue listening, Popular, spotlight, Your playlists), Queue (Up next), Sidebar (playlists). The 6 track tables (AlbumTrackTable, PlaylistTrackTable, library, liked, mixes, LibraryPicker) migrate to MediaIdentity with their own local use:pressable, since TrackList doesn't exist yet. EqBars gains an `overlay` prop (scrim/45 background, on-art color) replacing NowPlaying.svelte (removed). Along the way, fixes 6 plural() calls that a previous pass had left unmigrated (liked, library, mixes, playlists, PlaylistHeader, Home). Deltas documented in the plan's log: Sidebar's active row now uses the accent tint (previously neutral gray); Home's "Your playlists" card loses its redundant author line and its radius becomes rounded-art; Queue rows lose the art's opacity-80. Verified with npm run check/build; no authenticated visual QA (Docker unavailable this session).
This commit is contained in:
@@ -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 |
|
||||
| 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) |
|
||||
| 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 | ⬜ | | |
|
||||
| P9 | ⬜ | | |
|
||||
| P10 | ⬜ | | |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { liked } from '$lib/player/liked.svelte';
|
||||
import { createPlaylistModal } from '$lib/playlists.svelte';
|
||||
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 Home from '@lucide/svelte/icons/house';
|
||||
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">
|
||||
{#each playlists as playlist (playlist.id)}
|
||||
{@const active = page.url.pathname === `/playlists/${playlist.id}`}
|
||||
<a
|
||||
<ListRow
|
||||
href="/playlists/{playlist.id}"
|
||||
class="flex items-center gap-2.75 rounded-control px-3 py-1.75 transition-colors duration-150 {active
|
||||
? 'bg-surface-hover'
|
||||
: 'hover:bg-hover'}"
|
||||
>
|
||||
<Artwork
|
||||
src="/api/playlists/{playlist.id}/cover?size=small&v={encodeURIComponent(
|
||||
{active}
|
||||
size="xs"
|
||||
title={playlist.name}
|
||||
coverSrc="/api/playlists/{playlist.id}/cover?size=small&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
trackIds={playlist.coverTrackIds}
|
||||
size="xs"
|
||||
class="shrink-0 opacity-90"
|
||||
class="px-3 py-1.75"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm {active ? 'text-fg' : 'text-fg-2'}">
|
||||
{playlist.name}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
|
||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
||||
import ListRow from '$lib/components/ui/ListRow.svelte';
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
import { pressable } from '$lib/actions/pressable';
|
||||
import { fmtTime } from '$lib/format';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { fly } from 'svelte/transition';
|
||||
@@ -31,24 +30,15 @@
|
||||
|
||||
{#if player.current.id}
|
||||
<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">
|
||||
<Artwork
|
||||
trackIds={[player.current.id]}
|
||||
size="sm"
|
||||
alt={player.current.title}
|
||||
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 || '—'}
|
||||
<div class="mb-6 rounded-control bg-accent-tint p-2.5">
|
||||
<MediaIdentity
|
||||
trackId={player.current.id}
|
||||
title={player.current.title}
|
||||
artist={player.current.artist || '—'}
|
||||
ownerUserId={player.current.ownerUserId}
|
||||
class="mt-0.5 text-xs text-fg-3"
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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">
|
||||
{#each upcoming as track, i (track.id)}
|
||||
{@const index = player.tracks.length - upcoming.length + i}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
use:pressable={() => player.playQueueIndex(index)}
|
||||
class="flex items-center gap-3 rounded-control p-2.25 text-left hover:bg-hover"
|
||||
<ListRow
|
||||
onclick={() => player.playQueueIndex(index)}
|
||||
size="sm"
|
||||
title={track.title}
|
||||
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" />
|
||||
<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>
|
||||
{#snippet trailing()}
|
||||
{#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}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ListRow>
|
||||
{:else}
|
||||
<p class="px-2 py-6 text-center text-xs text-fg-3">No hay más canciones en la cola.</p>
|
||||
{/each}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import { enhance } from '$app/forms';
|
||||
import type { LucideIcon } from '@lucide/svelte';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import Artwork from './Artwork.svelte';
|
||||
import MediaIdentity from './MediaIdentity.svelte';
|
||||
import GlassMenu from './GlassMenu.svelte';
|
||||
import MenuItem from './MenuItem.svelte';
|
||||
|
||||
@@ -93,14 +93,14 @@
|
||||
{/each}
|
||||
<button
|
||||
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
|
||||
src="/api/playlists/{playlist.id}/cover?size=small"
|
||||
<MediaIdentity
|
||||
title={playlist.name}
|
||||
coverSrc="/api/playlists/{playlist.id}/cover?size=small"
|
||||
size="xs"
|
||||
class="shrink-0"
|
||||
titleClass="text-fg-2"
|
||||
/>
|
||||
<span class="truncate text-fg-2">{playlist.name}</span>
|
||||
</button>
|
||||
</form>
|
||||
{/each}
|
||||
|
||||
@@ -3,15 +3,34 @@
|
||||
size?: number;
|
||||
barWidth?: number;
|
||||
paused?: boolean;
|
||||
overlay?: boolean;
|
||||
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];
|
||||
</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)}
|
||||
<span
|
||||
class="rounded-full bg-accent"
|
||||
@@ -20,4 +39,5 @@
|
||||
: 'running'}"
|
||||
></span>
|
||||
{/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}
|
||||
+20
-14
@@ -3,25 +3,27 @@
|
||||
import Artwork from './Artwork.svelte';
|
||||
import ExplicitBadge from './ExplicitBadge.svelte';
|
||||
import ArtistLink from './ArtistLink.svelte';
|
||||
import { pressable } from '$lib/actions/pressable';
|
||||
|
||||
interface Props {
|
||||
trackId: string | number;
|
||||
trackId?: string | number;
|
||||
trackIds?: (string | number)[];
|
||||
title: string;
|
||||
artist?: string | null;
|
||||
ownerUserId?: string | number | null;
|
||||
coverSrc?: string | null;
|
||||
size?: 'sm' | 'lg';
|
||||
size?: 'xs' | 'sm' | 'lg';
|
||||
explicit?: boolean;
|
||||
active?: boolean;
|
||||
titleClass?: string;
|
||||
onClick: () => void;
|
||||
class?: string;
|
||||
art?: Snippet;
|
||||
overlay?: Snippet;
|
||||
badge?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
trackId,
|
||||
trackIds,
|
||||
title,
|
||||
artist,
|
||||
ownerUserId,
|
||||
@@ -30,23 +32,27 @@
|
||||
explicit = false,
|
||||
active = false,
|
||||
titleClass = '',
|
||||
onClick,
|
||||
class: klass = '',
|
||||
art,
|
||||
overlay,
|
||||
badge
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
use:pressable={onClick}
|
||||
onclick={(event) => event.stopPropagation()}
|
||||
class="flex min-w-0 items-center gap-3.5 text-left"
|
||||
>
|
||||
<Artwork trackIds={[trackId]} src={coverSrc} {size} alt={title} class="shrink-0">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3.5 {klass}">
|
||||
{#if art}
|
||||
{@render art()}
|
||||
{:else if trackId !== undefined || coverSrc || trackIds?.length}
|
||||
<Artwork
|
||||
trackIds={trackIds ?? (trackId !== undefined ? [trackId] : [])}
|
||||
src={coverSrc}
|
||||
{size}
|
||||
alt={title}
|
||||
class="shrink-0"
|
||||
>
|
||||
{@render overlay?.()}
|
||||
</Artwork>
|
||||
{/if}
|
||||
<div class="min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
{#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 TrackMeta from '$lib/components/ui/TrackMeta.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 };
|
||||
|
||||
@@ -34,15 +35,23 @@
|
||||
{@const active = player.current.id === track.id}
|
||||
<TrackRow {active}>
|
||||
<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}
|
||||
title={track.title}
|
||||
artist={track.artist}
|
||||
ownerUserId={track.ownerUserId}
|
||||
explicit={track.isExplicit}
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
/>
|
||||
</div>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
||||
{#if isOwner}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
||||
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
|
||||
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
||||
import { pressable } from '$lib/actions/pressable';
|
||||
import EqBars from '$lib/components/ui/EqBars.svelte';
|
||||
|
||||
type LibraryTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
|
||||
|
||||
@@ -30,21 +31,29 @@
|
||||
{#each library as track, i (track.id)}
|
||||
{@const active = player.current.id === track.id}
|
||||
<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}
|
||||
title={track.title}
|
||||
artist={track.artist}
|
||||
ownerUserId={track.ownerUserId}
|
||||
explicit={track.isExplicit}
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
>
|
||||
{#snippet overlay()}
|
||||
{#if player.current.id === track.id}
|
||||
<NowPlaying paused={!player.playing} />
|
||||
<EqBars overlay paused={!player.playing} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TrackTitleCell>
|
||||
</MediaIdentity>
|
||||
</div>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
||||
<form
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import Avatar from '$lib/components/ui/Avatar.svelte';
|
||||
import PlayButton from '$lib/components/ui/PlayButton.svelte';
|
||||
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
||||
import SearchResultRow from '$lib/components/ui/SearchResultRow.svelte';
|
||||
import EqBars from '$lib/components/ui/EqBars.svelte';
|
||||
import ListRow from '$lib/components/ui/ListRow.svelte';
|
||||
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
||||
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
||||
import { searchHref } from '$lib/navigation.svelte';
|
||||
@@ -30,6 +30,7 @@
|
||||
targetExplicit,
|
||||
targetId,
|
||||
targetListensCount,
|
||||
targetOwnerUserId,
|
||||
targetTitle,
|
||||
type TrackTarget
|
||||
} from '$lib/tracks';
|
||||
@@ -410,27 +411,28 @@
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(songRows) as { target: item, seconds }, i (targetId(item))}
|
||||
<li>
|
||||
<SearchResultRow
|
||||
<ListRow
|
||||
title={targetTitle(item)}
|
||||
subtitle={targetArtist(item)}
|
||||
meta="{fmtTime(seconds)} · {fmtPlays(targetListensCount(item))}"
|
||||
subtitleHref={targetOwnerUserId(item)}
|
||||
explicit={targetExplicit(item)}
|
||||
active={isTargetCurrent(item)}
|
||||
size="lg"
|
||||
trackId={targetId(item)}
|
||||
onclick={() => togglePlay(i)}
|
||||
oncontextmenu={(e) => openContextMenu(e, item)}
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<Artwork
|
||||
trackIds={[targetId(item)]}
|
||||
size="lg"
|
||||
alt={targetTitle(item)}
|
||||
class="{artClass} ring-1 ring-line ring-inset"
|
||||
>
|
||||
{#snippet overlay()}
|
||||
{#if isTargetCurrent(item)}
|
||||
<NowPlaying paused={!player.playing} />
|
||||
<EqBars overlay paused={!player.playing} />
|
||||
{/if}
|
||||
</Artwork>
|
||||
{/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>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -460,16 +462,19 @@
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(albums) as album (album.id)}
|
||||
<li>
|
||||
<SearchResultRow
|
||||
<ListRow
|
||||
title={album.title}
|
||||
meta="{Number(album.trackCount)} canciones"
|
||||
href="/albums/{album.id}"
|
||||
size="lg"
|
||||
trackIds={album.coverTrackIds}
|
||||
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<Artwork trackIds={album.coverTrackIds} size="lg" class={artClass} />
|
||||
{#snippet trailing()}
|
||||
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
|
||||
{Number(album.trackCount)} canciones
|
||||
</span>
|
||||
{/snippet}
|
||||
</SearchResultRow>
|
||||
</ListRow>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -489,11 +494,11 @@
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(users) as u (u.id)}
|
||||
<li>
|
||||
<SearchResultRow title={u.name} href="/u/{u.id}">
|
||||
{#snippet art(artClass)}
|
||||
<Avatar name={u.name} src={u.profilePictureUrl} size="md" class={artClass} />
|
||||
<ListRow title={u.name} href="/u/{u.id}" size="lg">
|
||||
{#snippet art()}
|
||||
<Avatar name={u.name} src={u.profilePictureUrl} size="md" />
|
||||
{/snippet}
|
||||
</SearchResultRow>
|
||||
</ListRow>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -513,22 +518,16 @@
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(playlistMatches) as playlist (playlist.id)}
|
||||
<li>
|
||||
<SearchResultRow
|
||||
<ListRow
|
||||
title={playlist.name}
|
||||
subtitle={playlist.description}
|
||||
href="/playlists/{playlist.id}"
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<Artwork
|
||||
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
size="lg"
|
||||
coverSrc="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
trackIds={playlist.coverTrackIds}
|
||||
size="lg"
|
||||
class={artClass}
|
||||
/>
|
||||
{/snippet}
|
||||
</SearchResultRow>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import Music from '@lucide/svelte/icons/music';
|
||||
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 PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
@@ -14,7 +14,8 @@
|
||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||
import TrackMeta from '$lib/components/ui/TrackMeta.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();
|
||||
|
||||
@@ -34,7 +35,7 @@
|
||||
<Page>
|
||||
<PageHeader
|
||||
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()}
|
||||
{#if items.length > 0}
|
||||
@@ -66,12 +67,16 @@
|
||||
playing={player.playing}
|
||||
onToggle={() => togglePlay(i)}
|
||||
/>
|
||||
<TrackTitleCell
|
||||
trackId={track.id}
|
||||
title={track.title}
|
||||
{active}
|
||||
onClick={() => togglePlay(i)}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
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">{fmtDate(track.createdAt)}</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.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 CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
@@ -13,7 +13,8 @@
|
||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||
import TrackMeta from '$lib/components/ui/TrackMeta.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 isCurrentQueue = $derived(isQueueCurrent(tracks));
|
||||
@@ -36,7 +37,7 @@
|
||||
<CollectionHeader
|
||||
eyebrow="Colección"
|
||||
title="Me gusta"
|
||||
meta="{tracks.length} {tracks.length === 1 ? 'Canción' : 'Canciones'}"
|
||||
meta={plural(tracks.length, 'canción', 'canciones')}
|
||||
align="center"
|
||||
>
|
||||
{#snippet cover()}
|
||||
@@ -73,15 +74,23 @@
|
||||
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}
|
||||
title={track.title}
|
||||
artist={track.artist}
|
||||
ownerUserId={track.ownerUserId}
|
||||
explicit={track.explicit}
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Pause from '@lucide/svelte/icons/pause';
|
||||
import Shuffle from '@lucide/svelte/icons/shuffle';
|
||||
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 BackLink from '$lib/components/ui/BackLink.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
@@ -14,7 +14,8 @@
|
||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||
import TrackMeta from '$lib/components/ui/TrackMeta.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 ListPlus from '@lucide/svelte/icons/list-plus';
|
||||
import { createTrackMenu } from '$lib/menus.svelte';
|
||||
@@ -59,7 +60,7 @@
|
||||
eyebrow="Mezcla"
|
||||
title={mix.title}
|
||||
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()}
|
||||
<Artwork
|
||||
@@ -100,13 +101,21 @@
|
||||
{@const active = isTargetCurrent(targets[i])}
|
||||
<TrackRow {active} oncontextmenu={(e) => trackMenu.open(e, targets[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])}
|
||||
title={targetTitle(targets[i])}
|
||||
artist={targetArtist(targets[i])}
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
/>
|
||||
</div>
|
||||
<TrackMeta class="hidden sm:block">{Number(targetListensCount(targets[i]) ?? 0)}</TrackMeta>
|
||||
<TrackMeta>{fmtTime(Number(item.durationSeconds))}</TrackMeta>
|
||||
</TrackRow>
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { createPlaylistModal } from '$lib/playlists.svelte';
|
||||
import { fmtDurationLong } from '$lib/format';
|
||||
import { fmtDurationLong, plural } from '$lib/format';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
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)}`
|
||||
`${plural(totals.playlistCount, 'playlist', 'playlists')} · ${plural(totals.trackCount, 'canción', 'canciones')} · ${fmtDurationLong(totals.durationSeconds)}`
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||
import { plural } from '$lib/format';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -30,7 +31,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const meta = $derived(
|
||||
`${trackIds.length} ${trackIds.length === 1 ? 'Canción' : 'Canciones'} · ${
|
||||
`${plural(trackIds.length, 'canción', 'canciones')} · ${
|
||||
visibility === 'Public' ? 'Pública' : 'Privada'
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||
import TrackMeta from '$lib/components/ui/TrackMeta.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 & {
|
||||
createdAt: string;
|
||||
@@ -41,15 +42,23 @@
|
||||
{@const active = player.current.id === track.id}
|
||||
<TrackRow {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}
|
||||
title={track.title}
|
||||
artist={track.artist}
|
||||
ownerUserId={track.ownerUserId}
|
||||
explicit={track.isExplicit}
|
||||
{active}
|
||||
onClick={() => playFrom(i)}
|
||||
/>
|
||||
</div>
|
||||
<TrackMeta class="hidden sm:block">—</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
|
||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
|
||||
import { pressable } from '$lib/actions/pressable';
|
||||
import { mergeRecentlyPlayed } from '$lib/recentlyPlayed';
|
||||
import { fmtTime, fmtDurationLong, fmtPlays } from '$lib/format';
|
||||
import { fmtTime, fmtDurationLong, fmtPlays, plural } from '$lib/format';
|
||||
import {
|
||||
isTargetCurrent,
|
||||
queueItemForTarget,
|
||||
@@ -19,11 +19,10 @@
|
||||
targetTitle,
|
||||
type TrackTarget
|
||||
} from '$lib/tracks';
|
||||
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import MixTile from '$lib/components/ui/MixTile.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 EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
@@ -72,7 +71,7 @@
|
||||
spotlightTrackCount === 0
|
||||
? spotlightVisibilityLabel
|
||||
: [
|
||||
`${spotlightTrackCount} ${spotlightTrackCount === 1 ? 'Canción' : 'Canciones'}`,
|
||||
plural(spotlightTrackCount, 'canción', 'canciones'),
|
||||
fmtDurationLong(spotlightDurationSeconds),
|
||||
spotlightVisibilityLabel
|
||||
].join(' · ')
|
||||
@@ -176,26 +175,19 @@
|
||||
<div class="grid grid-cols-2 gap-2.5 sm:grid-cols-3 lg:grid-cols-5">
|
||||
{#each continueItems as item, i (item.id)}
|
||||
{@const isCurrent = player.current.id === item.id}
|
||||
<div
|
||||
role="button"
|
||||
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}"
|
||||
<ListRow
|
||||
onclick={() => playContinue(i)}
|
||||
oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(item))}
|
||||
>
|
||||
<TrackTitleCell
|
||||
trackId={item.id}
|
||||
title={item.title}
|
||||
artist={item.artist}
|
||||
ownerUserId={item.ownerUserId}
|
||||
size="lg"
|
||||
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}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -254,33 +246,22 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 p-5.5 sm:p-6">
|
||||
{#each spotlightRows as { target, seconds }, i (targetId(target))}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
use:pressable={() => playSpotlightTrack(i)}
|
||||
class="flex items-center gap-4 rounded-control p-2 text-left transition-colors {isTargetCurrent(
|
||||
target
|
||||
)
|
||||
? 'bg-accent-tint'
|
||||
: 'hover:bg-hover'}"
|
||||
<ListRow
|
||||
onclick={() => playSpotlightTrack(i)}
|
||||
active={isTargetCurrent(target)}
|
||||
size="sm"
|
||||
title={targetTitle(target)}
|
||||
subtitle={targetArtist(target)}
|
||||
subtitleHref={targetOwnerUserId(target)}
|
||||
>
|
||||
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
class="truncate text-sm {isTargetCurrent(target)
|
||||
? 'text-accent-soft'
|
||||
: 'text-fg'}"
|
||||
{#snippet leading()}
|
||||
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span
|
||||
>
|
||||
{targetTitle(target)}
|
||||
</div>
|
||||
<ArtistLink
|
||||
name={targetArtist(target)}
|
||||
ownerUserId={targetOwnerUserId(target)}
|
||||
class="mt-0.5 text-xs text-fg-3"
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet trailing()}
|
||||
<span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(seconds)}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ListRow>
|
||||
{:else}
|
||||
<p class="p-2 text-sm text-fg-3">Esta playlist todavía no tiene canciones.</p>
|
||||
{/each}
|
||||
@@ -314,15 +295,18 @@
|
||||
<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))}
|
||||
{@const isCurrent = isTargetCurrent(target)}
|
||||
<div
|
||||
role="button"
|
||||
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'}"
|
||||
<ListRow
|
||||
onclick={() => playPopular(i)}
|
||||
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">
|
||||
{#if isCurrent}
|
||||
<EqBars size={13} paused={!player.playing} />
|
||||
@@ -330,21 +314,13 @@
|
||||
<span class="text-xs text-muted tabular-nums">{i + 1}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<TrackTitleCell
|
||||
trackId={targetId(target)}
|
||||
title={targetTitle(target)}
|
||||
artist={targetArtist(target)}
|
||||
ownerUserId={targetOwnerUserId(target)}
|
||||
explicit={targetExplicit(target)}
|
||||
active={isCurrent}
|
||||
onClick={() => playPopular(i)}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet trailing()}
|
||||
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
|
||||
{fmtPlays(targetListensCount(target))}
|
||||
</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ListRow>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -377,36 +353,18 @@
|
||||
{#if playlists.length > 0}
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(258px,1fr))] gap-3.5">
|
||||
{#each playlists as playlist, i (playlist.id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
use:pressable={() => goto(`/playlists/${playlist.id}`)}
|
||||
class="group/card animate-enter flex items-center gap-3.5 rounded-panel p-3 transition-colors hover:bg-hover"
|
||||
style="--i:{i}"
|
||||
>
|
||||
<Artwork
|
||||
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
<ListRow
|
||||
onclick={() => goto(`/playlists/${playlist.id}`)}
|
||||
size="lg"
|
||||
variant="card"
|
||||
title={playlist.name}
|
||||
subtitle={playlist.description}
|
||||
coverSrc="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
trackIds={playlist.coverTrackIds}
|
||||
size="lg"
|
||||
alt={playlist.name}
|
||||
class="shrink-0"
|
||||
style="--i:{i}"
|
||||
/>
|
||||
<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}
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user