From 1cad36b9297e290011253d89f74ce710c676a09c Mon Sep 17 00:00:00 2001
From: XShine555
Date: Thu, 24 Sep 2026 11:51:14 +0200
Subject: [PATCH] web-player: extract menu, paged list and play-all components
from pages
---
.../components/ui/media/PlayAllButton.svelte | 35 +++++
.../ui/overlay/AlbumContextMenu.svelte | 46 ++++++
.../ui/overlay/TrackContextMenu.svelte | 36 +++++
.../ui/primitives/InfiniteScroll.svelte | 17 ++-
web-player/src/lib/state/menu.svelte.ts | 22 +++
web-player/src/lib/state/menus.svelte.ts | 27 ----
web-player/src/lib/state/pagedList.svelte.ts | 50 ++++++
.../src/routes/(app)/albums/[id]/+page.svelte | 16 +-
.../src/routes/(app)/explore/+page.svelte | 142 +++++-------------
.../src/routes/(app)/liked/+page.svelte | 11 +-
.../src/routes/(app)/mixes/[id]/+page.svelte | 40 ++---
.../routes/(app)/playlists/[id]/+page.svelte | 16 +-
.../user/[id]/[list=followList]/+page.svelte | 50 +++---
web-player/src/routes/+page.svelte | 95 ++++--------
14 files changed, 306 insertions(+), 297 deletions(-)
create mode 100644 web-player/src/lib/components/ui/media/PlayAllButton.svelte
create mode 100644 web-player/src/lib/components/ui/overlay/AlbumContextMenu.svelte
create mode 100644 web-player/src/lib/components/ui/overlay/TrackContextMenu.svelte
create mode 100644 web-player/src/lib/state/menu.svelte.ts
delete mode 100644 web-player/src/lib/state/menus.svelte.ts
create mode 100644 web-player/src/lib/state/pagedList.svelte.ts
diff --git a/web-player/src/lib/components/ui/media/PlayAllButton.svelte b/web-player/src/lib/components/ui/media/PlayAllButton.svelte
new file mode 100644
index 0000000..846f243
--- /dev/null
+++ b/web-player/src/lib/components/ui/media/PlayAllButton.svelte
@@ -0,0 +1,35 @@
+
+
+
diff --git a/web-player/src/lib/components/ui/overlay/AlbumContextMenu.svelte b/web-player/src/lib/components/ui/overlay/AlbumContextMenu.svelte
new file mode 100644
index 0000000..12b044c
--- /dev/null
+++ b/web-player/src/lib/components/ui/overlay/AlbumContextMenu.svelte
@@ -0,0 +1,46 @@
+
+
+{#if menu.state}
+ {@const { target: albumId, x, y, openLeft } = menu.state}
+ menu.close()}
+ items={[
+ { icon: ListPlus, label: 'Reproducir a continuación', onclick: () => playNext(albumId) },
+ { icon: ListEnd, label: 'Añadir a la cola', onclick: () => addToQueue(albumId) }
+ ]}
+ playlistAction={{
+ action: '?/addAlbumToPlaylist',
+ fields: { albumId },
+ label: 'Añadir álbum a una playlist'
+ }}
+ {playlists}
+ />
+{/if}
diff --git a/web-player/src/lib/components/ui/overlay/TrackContextMenu.svelte b/web-player/src/lib/components/ui/overlay/TrackContextMenu.svelte
new file mode 100644
index 0000000..4e4bcb1
--- /dev/null
+++ b/web-player/src/lib/components/ui/overlay/TrackContextMenu.svelte
@@ -0,0 +1,36 @@
+
+
+{#if menu.state}
+ {@const { target: track, x, y, openLeft } = menu.state}
+ menu.close()}
+ items={[{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => playNext(track) }]}
+ playlistAction={{
+ action: '?/addTrack',
+ fields: { trackId: track.id },
+ label: 'Añadir a una playlist'
+ }}
+ {playlists}
+ />
+{/if}
diff --git a/web-player/src/lib/components/ui/primitives/InfiniteScroll.svelte b/web-player/src/lib/components/ui/primitives/InfiniteScroll.svelte
index e4e0cb8..7074c58 100644
--- a/web-player/src/lib/components/ui/primitives/InfiniteScroll.svelte
+++ b/web-player/src/lib/components/ui/primitives/InfiniteScroll.svelte
@@ -1,15 +1,23 @@
{#if loading}
+ {:else if error}
+
+ No se pudo cargar más.
+
+
{/if}
diff --git a/web-player/src/lib/state/menu.svelte.ts b/web-player/src/lib/state/menu.svelte.ts
new file mode 100644
index 0000000..1b84a96
--- /dev/null
+++ b/web-player/src/lib/state/menu.svelte.ts
@@ -0,0 +1,22 @@
+import { contextMenuPosition, type MenuPosition } from '$lib/utils/menuPosition';
+
+export type MenuState = { target: T } & MenuPosition;
+
+export function createMenu() {
+ let state = $state | null>(null);
+
+ return {
+ get state() {
+ return state;
+ },
+ open(event: MouseEvent, target: T) {
+ event.preventDefault();
+ state = { target, ...contextMenuPosition(event) };
+ },
+ close() {
+ state = null;
+ }
+ };
+}
+
+export type Menu = ReturnType>;
diff --git a/web-player/src/lib/state/menus.svelte.ts b/web-player/src/lib/state/menus.svelte.ts
deleted file mode 100644
index 42db1be..0000000
--- a/web-player/src/lib/state/menus.svelte.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { contextMenuPosition, type MenuPosition } from '$lib/utils/menuPosition';
-import { player } from '$lib/player/player.svelte';
-import type { Track } from '$lib/types';
-
-export type TrackMenuState = { track: Track } & MenuPosition;
-
-export function createTrackMenu() {
- let state = $state(null);
-
- return {
- get state() {
- return state;
- },
- open(event: MouseEvent, track: Track) {
- event.preventDefault();
- state = { track, ...contextMenuPosition(event) };
- },
- close() {
- state = null;
- },
- playNext() {
- if (!state) return;
- player.playNext([state.track]);
- state = null;
- }
- };
-}
diff --git a/web-player/src/lib/state/pagedList.svelte.ts b/web-player/src/lib/state/pagedList.svelte.ts
new file mode 100644
index 0000000..b6b9580
--- /dev/null
+++ b/web-player/src/lib/state/pagedList.svelte.ts
@@ -0,0 +1,50 @@
+import type { Paged } from '$lib/types';
+
+export function createPagedList(
+ fetchPage: (pageNumber: number) => Promise>,
+ keyOf: (item: T) => string | number
+) {
+ let items = $state([]);
+ let pageNumber = 1;
+ let hasMore = $state(false);
+ let loading = $state(false);
+ let error = $state(false);
+
+ return {
+ get items() {
+ return items;
+ },
+ get hasMore() {
+ return hasMore;
+ },
+ get loading() {
+ return loading;
+ },
+ get error() {
+ return error;
+ },
+ reset(initial: Paged | null) {
+ items = initial?.items ?? [];
+ pageNumber = initial?.pageNumber ?? 1;
+ hasMore = initial?.hasNextPage ?? false;
+ loading = false;
+ error = false;
+ },
+ async loadMore() {
+ if (loading) return;
+ loading = true;
+ error = false;
+ try {
+ const next = await fetchPage(pageNumber + 1);
+ const seen = new Set(items.map(keyOf));
+ items = [...items, ...next.items.filter((item) => !seen.has(keyOf(item)))];
+ pageNumber = next.pageNumber;
+ hasMore = next.hasNextPage;
+ } catch {
+ error = true;
+ } finally {
+ loading = false;
+ }
+ }
+ };
+}
diff --git a/web-player/src/routes/(app)/albums/[id]/+page.svelte b/web-player/src/routes/(app)/albums/[id]/+page.svelte
index d0e5bdf..fb24a80 100644
--- a/web-player/src/routes/(app)/albums/[id]/+page.svelte
+++ b/web-player/src/routes/(app)/albums/[id]/+page.svelte
@@ -1,7 +1,7 @@
@@ -334,7 +291,7 @@
active={player.currentId === track.id}
size="lg"
trackId={track.id}
- onclick={() => togglePlay(i)}
+ onclick={() => player.playOrToggle(items, i)}
oncontextmenu={(e) => trackMenu.open(e, track)}
>
{#snippet overlay()}
@@ -352,8 +309,13 @@
{/each}
- {#if sfilter === 'tracks' && hasMore}
-
+ {#if sfilter === 'tracks' && tracksList.hasMore}
+ tracksList.loadMore()}
+ hasMore={tracksList.hasMore}
+ loading={tracksList.loading}
+ error={tracksList.error}
+ />
{/if}
{:else}
openAlbumMenu(e, album.id)}
+ oncontextmenu={(e) => albumMenu.open(e, album.id)}
>
{#snippet trailing()}
@@ -427,39 +389,5 @@
{/if}
-{#if trackMenu.state}
- trackMenu.close()}
- items={[
- { icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
- ]}
- playlistAction={{
- action: '?/addTrack',
- fields: { trackId: trackMenu.state.track.id },
- label: 'Añadir a una playlist'
- }}
- playlists={data.userPlaylists}
- />
-{/if}
-
-{#if albumMenu}
- (albumMenu = null)}
- items={[
- { icon: ListPlus, label: 'Reproducir a continuación', onclick: albumPlayNext },
- { icon: ListEnd, label: 'Añadir a la cola', onclick: albumAddToQueue }
- ]}
- playlistAction={{
- action: '?/addAlbumToPlaylist',
- fields: { albumId: albumMenu.albumId },
- label: 'Añadir álbum a una playlist'
- }}
- playlists={data.userPlaylists}
- />
-{/if}
+
+
diff --git a/web-player/src/routes/(app)/liked/+page.svelte b/web-player/src/routes/(app)/liked/+page.svelte
index 94882f5..81629c6 100644
--- a/web-player/src/routes/(app)/liked/+page.svelte
+++ b/web-player/src/routes/(app)/liked/+page.svelte
@@ -2,7 +2,7 @@
import Heart from '@lucide/svelte/icons/heart';
import X from '@lucide/svelte/icons/x';
import { player } from '$lib/player/player.svelte';
- import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
+ import PlayAllButton from '$lib/components/ui/media/PlayAllButton.svelte';
import { liked } from '$lib/player/liked.svelte';
import { plural } from '$lib/utils/format';
import Page from '$lib/components/ui/layout/Page.svelte';
@@ -13,15 +13,10 @@
const tracks = $derived(liked.list);
const listTracks = $derived(tracks.map((track) => ({ ...track, date: track.likedAt })));
- const isCurrentQueue = $derived(isQueueCurrent(tracks));
function playFrom(index: number) {
player.playOrToggle(tracks, index);
}
-
- function playAll() {
- playAllOrToggle(tracks);
- }
@@ -44,9 +39,7 @@
{/snippet}
{#snippet actions()}
-
+
{/snippet}
diff --git a/web-player/src/routes/(app)/mixes/[id]/+page.svelte b/web-player/src/routes/(app)/mixes/[id]/+page.svelte
index ccc5ed8..903002c 100644
--- a/web-player/src/routes/(app)/mixes/[id]/+page.svelte
+++ b/web-player/src/routes/(app)/mixes/[id]/+page.svelte
@@ -1,7 +1,7 @@
@@ -67,7 +52,7 @@
title="Seguidores privados"
description="Solo puedes ver los seguidores de {data.profile.name} si os seguís mutuamente."
/>
- {:else if items.length === 0}
+ {:else if list.items.length === 0}
{:else}
- {#each items as user (user.id)}
+ {#each list.items as user (user.id)}
-
{/each}
-
+ list.loadMore()}
+ hasMore={list.hasMore}
+ loading={list.loading}
+ error={list.error}
+ />
{/if}
diff --git a/web-player/src/routes/+page.svelte b/web-player/src/routes/+page.svelte
index e11d642..a8468a4 100644
--- a/web-player/src/routes/+page.svelte
+++ b/web-player/src/routes/+page.svelte
@@ -1,11 +1,10 @@
@@ -90,11 +66,14 @@
{#if spotlight}
- {#await data.spotlightTracks then { items: previewItems }}
-
+ {#await data.spotlightTracks then { items }}
+
fetchPlaylistTracks(spotlight.id)}
+ size="md"
+ idleLabel="Reanudar {spotlight.name}"
+ activeLabel="Pausar {spotlight.name}"
+ />
{/await}
{/if}
@@ -130,7 +109,7 @@
{#each continueItems as item, i (item.id)}
{@const isCurrent = player.currentId === item.id}
playContinue(i)}
+ onclick={() => player.playOrToggle(continueItems, i)}
oncontextmenu={(e) => trackMenu.open(e, item)}
active={isCurrent}
size="lg"
@@ -185,16 +164,19 @@
{spotlight.description || 'Tu colección, siempre a mano.'}
-
+
fetchPlaylistTracks(spotlight.id)}
+ variant="accent"
+ size="md"
+ />
{#each previewItems as track, i (track.id)}
playSpotlightTrack(previewItems, i)}
+ onclick={() => player.playOrToggle(previewItems, i)}
active={player.currentId === track.id}
size="sm"
title={track.title}
@@ -224,15 +206,18 @@
- {#if mixes.length > 0}
+ {#if data.mixes.length > 0}
- {#each mixes as mix, i (mix.id)}
+ {#each data.mixes as mix, i (mix.id)}
track.id)}
- onPlay={(event) => playMix(mix, event)}
+ onPlay={(event) => {
+ event.preventDefault();
+ player.playQueue(mix.tracks, 0);
+ }}
index={i}
/>
{/each}
@@ -252,11 +237,11 @@
subtitle="Creadas y guardadas por ti"
href={data.playlistsHasMore ? '/playlists' : undefined}
/>
- {#if playlists.length > 0}
+ {#if data.playlists.length > 0}
- {#each playlists as playlist, i (playlist.id)}
+ {#each data.playlists as playlist, i (playlist.id)}
goto(`/playlists/${playlist.id}`)}
+ href="/playlists/{playlist.id}"
size="lg"
variant="card"
title={playlist.name}
@@ -282,20 +267,4 @@
-{#if trackMenu.state}
- trackMenu.close()}
- items={[
- { icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
- ]}
- playlistAction={{
- action: '?/addTrack',
- fields: { trackId: trackMenu.state.track.id },
- label: 'Añadir a una playlist'
- }}
- playlists={data.userPlaylists}
- />
-{/if}
+