web-player: track tables (TrackList)

Add TrackList.svelte, replacing TrackTable+TrackRow+TrackIndexCell+
TrackMeta and the 6 track tables (library, liked, mix, album,
playlist, album's library picker): declarative columns
(added/uploaded/plays) with fixed widths as module constants, an
index with an EQ indicator for the active track (or an overlay on the
art when index={false}, for the library picker), and a rowAction that
builds the form+IconButton (removeTrack/addTrack/deleteTrack) or a
direct onclick (Like).

AlbumTrackTable, PlaylistTrackTable and LibraryPicker are inlined
into their pages and removed, along with the 4 components TrackList
replaces.

Decision (recommended by the plan itself, not blocking): remove the
"Album" column, which in all 3 tables that had it always showed "—"
(a placeholder with no real data).

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
This commit is contained in:
2026-09-21 00:21:15 +02:00
parent fb4a8ccc0e
commit 8cea298b2a
14 changed files with 285 additions and 535 deletions
@@ -1,13 +1,15 @@
<script lang="ts">
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
import X from '@lucide/svelte/icons/x';
import Plus from '@lucide/svelte/icons/plus';
import Page from '$lib/components/ui/Page.svelte';
import Modal from '$lib/components/ui/Modal.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import AlbumForm from '$lib/components/ui/AlbumForm.svelte';
import Button from '$lib/components/ui/Button.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import TrackList from '$lib/components/ui/TrackList.svelte';
import AlbumHeader from './components/AlbumHeader.svelte';
import AlbumTrackTable from './components/AlbumTrackTable.svelte';
import LibraryPicker from './components/LibraryPicker.svelte';
let { data, form } = $props();
@@ -24,6 +26,14 @@
function playAll() {
playAllOrToggle(toQueueItems(tracks));
}
function playTrackFrom(index: number) {
player.playOrToggle(toQueueItems(tracks), index);
}
function playLibraryFrom(index: number) {
player.playOrToggle(toQueueItems(library), index);
}
</script>
<svelte:head>
@@ -50,11 +60,26 @@
{/if}
{#if tracks.length > 0}
<AlbumTrackTable {tracks} {isOwner} />
<TrackList
{tracks}
onPlay={playTrackFrom}
rowAction={isOwner
? { action: '?/removeTrack', icon: X, label: 'Quitar del álbum' }
: undefined}
class="mt-6 sm:mt-8"
/>
{/if}
{#if isOwner && library.length > 0}
<LibraryPicker {library} />
<div class="mt-10 sm:mt-12">
<SectionHeading title="Tus canciones subidas" />
<TrackList
tracks={library}
index={false}
onPlay={playLibraryFrom}
rowAction={{ action: '?/addTrack', icon: Plus, label: 'Añadir al álbum' }}
/>
</div>
{/if}
</Page>
@@ -1,73 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
import X from '@lucide/svelte/icons/x';
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
import IconButton from '$lib/components/ui/IconButton.svelte';
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 TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
type AlbumTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
interface Props {
tracks: AlbumTrack[];
isOwner: boolean;
}
let { tracks, isOwner }: Props = $props();
function playFrom(index: number) {
player.playOrToggle(toQueueItems(tracks), index);
}
</script>
<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}>
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
<!-- 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}
/>
</div>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
{#if isOwner}
<form
method="POST"
action="?/removeTrack"
use:enhance={() =>
async ({ update }) =>
update()}
>
<input type="hidden" name="trackId" value={track.id} />
<IconButton type="submit" label="Quitar del álbum" size="xs" revealOnHover>
<X class="h-3.5 w-3.5" />
</IconButton>
</form>
{/if}
</TrackRow>
{/each}
</TrackTable>
@@ -1,74 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
import Plus from '@lucide/svelte/icons/plus';
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
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 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 };
interface Props {
library: LibraryTrack[];
}
let { library }: Props = $props();
function playFrom(index: number) {
player.playOrToggle(toQueueItems(library), index);
}
</script>
<div class="mt-10 sm:mt-12">
<SectionHeading title="Tus canciones subidas" />
<TrackTable action meta={[{ label: 'Escuchas', width: '100px' }]}>
{#each library as track, i (track.id)}
{@const active = player.current.id === track.id}
<TrackRow {active}>
<!-- 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}
>
{#snippet overlay()}
{#if player.current.id === track.id}
<EqBars overlay paused={!player.playing} />
{/if}
{/snippet}
</MediaIdentity>
</div>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
<form
method="POST"
action="?/addTrack"
use:enhance={() =>
async ({ update }) =>
update()}
>
<input type="hidden" name="trackId" value={track.id} />
<IconButton type="submit" label="Añadir al álbum" size="xs" revealOnHover>
<Plus class="h-3.5 w-3.5" />
</IconButton>
</form>
</TrackRow>
{/each}
</TrackTable>
</div>