web-player: TypeScript helpers (partial)

Add src/lib/actions/pressable.ts and clickOutside.ts (Svelte actions;
role/tabindex stay static in markup for SSR accessibility, the action
only adds click + Enter/Space) and src/lib/toggle.svelte.ts
(createToggle), used by playlists.svelte.ts and queuePanel.svelte.ts.

format.ts: adds plural(n, one, many); migrates the repo's 15 manual
pluralizations, unifying them to lowercase (visible diff in
Playlists, Home, liked, Explore).

player.svelte.ts: adds toQueueItem, isQueueCurrent, playAllOrToggle,
playShuffled; renames addToQueue -> playNextItem (the old name was
misleading: it inserts next, not at the end). Migrates the 5 places
that repeated the isCurrentQueue/playAll pattern (album, playlist,
liked, mix, Home). Simplifies liked.svelte.ts and recentlyPlayed.ts
with object spread instead of rebuilding every field by hand.

navigation.svelte.ts: adds searchHref (migrates the duplicated
buildHref from TopBar and Explore) and appNavLinks (added; its
adoption in Sidebar/TabsBar is left for a later pass, as noted
there).

menus.svelte.ts: createTrackMenu() replaces the manual context-menu
state (contextMenu + contextMenuStateFor + addToQueue) in Home,
Explore and Mix.

Deliberately left pending: removing tracks.ts's TrackTarget wrapper
and its 8 accessors is deferred — it's the riskiest change in the
plan, touches types across Home/Explore/Mix/menus, and couldn't be
visually verified without Docker this session.

Verified with npm run check/build.
This commit is contained in:
2026-09-20 23:01:13 +02:00
parent aa20e70671
commit 1320b12699
25 changed files with 250 additions and 241 deletions
@@ -2,9 +2,8 @@
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import Shuffle from '@lucide/svelte/icons/shuffle';
import { player } from '$lib/player/player.svelte';
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
import { shuffle } from '$lib/collections';
import Page from '$lib/components/ui/Page.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
@@ -16,10 +15,8 @@
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 TrackContextMenu, {
contextMenuStateFor,
type ContextMenuState
} from '$lib/components/TrackContextMenu.svelte';
import TrackContextMenu from '$lib/components/TrackContextMenu.svelte';
import { createTrackMenu } from '$lib/menus.svelte';
import { mixItemKey, mixItemTarget } from '$lib/mixes';
import {
isTargetCurrent,
@@ -40,24 +37,13 @@
const totalSeconds = $derived(
items.reduce((total, item) => total + Number(item.durationSeconds), 0)
);
const isCurrentQueue = $derived(targets.some(isTargetCurrent));
const isCurrentQueue = $derived(isQueueCurrent(queue));
let contextMenu = $state<ContextMenuState | null>(null);
const trackMenu = createTrackMenu();
function playFrom(index: number) {
player.playOrToggle(queue, index);
}
function playAll() {
if (queue.length === 0) return;
if (isCurrentQueue) player.toggle();
else player.playQueue(queue, 0);
}
function playShuffled() {
if (queue.length === 0) return;
player.playQueue(shuffle(queue), 0);
}
</script>
<svelte:head>
@@ -83,7 +69,7 @@
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
<Button size="sm" onclick={() => playAllOrToggle(queue)} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
@@ -92,7 +78,12 @@
Reproducir
{/if}
</Button>
<Button variant="secondary" size="sm" onclick={playShuffled} disabled={queue.length === 0}>
<Button
variant="secondary"
size="sm"
onclick={() => playShuffled(queue)}
disabled={queue.length === 0}
>
<Shuffle class="h-4 w-4" />
Aleatorio
</Button>
@@ -106,7 +97,7 @@
<TrackTable index meta={[{ label: 'Escuchas', width: '100px' }]} class="mt-6 sm:mt-8">
{#each items as item, i (mixItemKey(item))}
{@const active = isTargetCurrent(targets[i])}
<TrackRow {active} oncontextmenu={(e) => (contextMenu = contextMenuStateFor(e, targets[i]))}>
<TrackRow {active} oncontextmenu={(e) => trackMenu.open(e, targets[i])}>
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
<TrackTitleCell
trackId={targetId(targets[i])}
@@ -122,18 +113,11 @@
</TrackTable>
</Page>
{#if contextMenu}
{#if trackMenu.state}
<TrackContextMenu
menu={contextMenu}
menu={trackMenu.state}
playlists={data.playlists}
onClose={() => (contextMenu = null)}
onAddToQueue={() => {
const menu = contextMenu;
if (menu) {
const index = targets.findIndex((target) => targetId(target) === targetId(menu));
if (index >= 0) player.addToQueue(queue[index]);
}
contextMenu = null;
}}
onClose={() => trackMenu.close()}
onAddToQueue={() => trackMenu.playNext()}
/>
{/if}