diff --git a/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte b/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte
index 9b84fea..85e3339 100644
--- a/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte
+++ b/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte
@@ -11,9 +11,10 @@
interface Props {
tracks: LibraryTrack[];
memberIds: string[];
+ onchange?: () => void;
}
- let { tracks, memberIds }: Props = $props();
+ let { tracks, memberIds, onchange }: Props = $props();
const [send, receive] = transferBetweenLists();
@@ -28,10 +29,12 @@
function add(track: LibraryTrack) {
currentIds = [...currentIds, track.id];
+ onchange?.();
}
function remove(track: LibraryTrack) {
currentIds = currentIds.filter((id) => id !== track.id);
+ onchange?.();
}
diff --git a/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte b/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte
index b2ae497..d76f2ea 100644
--- a/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte
+++ b/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte
@@ -1,4 +1,5 @@
-
- {#snippet aside()}
-
-
- {/snippet}
- {#snippet footer()}
- {#if album}
-
- {/if}
- {/snippet}
-
+
(dirty = true)} role="presentation">
+
+ {#snippet aside()}
+
+ (dirty = true)} />
+ {/snippet}
+ {#snippet footer()}
+ {#if album}
+
+ {/if}
+ {/snippet}
+
+
{#if album}
const FADE = { duration: 200 };
const SEARCH_DEBOUNCE_MS = 300;
+ const REFRESH_MIN_MS = 3000;
+ const REFRESH_MAX_MS = 30000;
+{announcement}
+
{#if errorMessage}
{errorMessage}
{:else if notice}
@@ -130,11 +224,7 @@
method="POST"
action="?/setPublished"
class="hidden"
- use:enhance={() =>
- async ({ update }) => {
- await update({ reset: false });
- selectedIds = [];
- }}
+ use:enhance={publishSubmit}
>
@@ -218,11 +308,16 @@
0}
- onClose={() => (pendingDelete = [])}
+ onClose={() => {
+ pendingDelete = [];
+ deleteError = '';
+ }}
title="¿Borrar {pendingTitle(pendingDelete)}?"
description="Esta acción no se puede deshacer: se eliminarán de tu biblioteca, de tus álbumes y de las playlists donde estén."
confirmLabel="Borrar"
action="?/deleteTracks"
+ submit={deleteSubmit}
+ error={deleteError}
fields={{ trackIds: pendingDelete.map((track) => track.id).join(',') }}
/>
@@ -233,5 +328,6 @@
description="Dejarán de verse en álbumes, playlists, búsquedas y perfiles. Solo tú podrás verlas y reproducirlas."
confirmLabel="Despublicar"
action="?/setPublished"
+ submit={publishSubmit}
fields={{ trackIds: pendingUnpublish.map((track) => track.id).join(','), isPublished: 'false' }}
/>
diff --git a/web-player/src/lib/components/ui/overlay/ConfirmDialog.svelte b/web-player/src/lib/components/ui/overlay/ConfirmDialog.svelte
index c006d11..6da293b 100644
--- a/web-player/src/lib/components/ui/overlay/ConfirmDialog.svelte
+++ b/web-player/src/lib/components/ui/overlay/ConfirmDialog.svelte
@@ -1,4 +1,7 @@
+{#snippet body()}
+ {#each Object.entries(fields) as [name, value] (name)}
+
+ {/each}
+
+{/snippet}
+
diff --git a/web-player/src/lib/player/player.svelte.ts b/web-player/src/lib/player/player.svelte.ts
index b35ea56..ca641fc 100644
--- a/web-player/src/lib/player/player.svelte.ts
+++ b/web-player/src/lib/player/player.svelte.ts
@@ -5,7 +5,15 @@ import { readStorage, writeStorage } from '$lib/utils/storage';
import { ListenTracker } from './listenTracker';
import { mediaSession } from './mediaSession';
import { ProgressClock } from './progressClock';
-import { append, insertNext, isLastIndex, move, nextIndex, previousIndex } from './queue';
+import {
+ append,
+ insertNext,
+ isLastIndex,
+ move,
+ nextIndex,
+ previousIndex,
+ removeTracks
+} from './queue';
import { resolveStream } from './stream';
const RECENT_LIMIT = 15;
@@ -281,6 +289,27 @@ class PlayerState {
this.tracks = move(this.tracks, from, insertAt);
}
+ removeTracks(ids: string[]) {
+ const gone = new Set(ids);
+ if (!this.tracks.some((track) => gone.has(track.id))) return;
+ const result = removeTracks(this.tracks, gone, this.#index());
+ this.tracks = result.queue;
+ if (!result.currentRemoved) return;
+ const next = result.queue[result.nextIndex] ?? result.queue[0];
+ if (next) {
+ this.currentId = next.id;
+ this.#loadCurrent();
+ return;
+ }
+ this.#audio?.pause();
+ this.#loadToken++;
+ this.currentId = null;
+ this.playing = false;
+ this.loading = false;
+ this.progress = 0;
+ mediaSession.setPlaying(false);
+ }
+
clearUpcoming() {
this.tracks = this.current ? [this.current] : [];
}
diff --git a/web-player/src/lib/player/queue.test.ts b/web-player/src/lib/player/queue.test.ts
index b1fb5de..3cd5cb8 100644
--- a/web-player/src/lib/player/queue.test.ts
+++ b/web-player/src/lib/player/queue.test.ts
@@ -1,5 +1,13 @@
import { describe, expect, it } from 'vitest';
-import { append, insertNext, isLastIndex, move, nextIndex, previousIndex } from './queue';
+import {
+ append,
+ insertNext,
+ isLastIndex,
+ move,
+ nextIndex,
+ previousIndex,
+ removeTracks
+} from './queue';
const t = (id: string) => ({ id });
const ids = (queue: { id: string }[]) => queue.map((item) => item.id);
@@ -83,3 +91,22 @@ describe('isLastIndex', () => {
expect(isLastIndex(3, 1)).toBe(false);
});
});
+
+describe('removeTracks', () => {
+ it('keeps the queue and current track when others are removed', () => {
+ const result = removeTracks(['a', 'b', 'c'].map(t), new Set(['a']), 1);
+ expect(ids(result.queue)).toEqual(['b', 'c']);
+ expect(result.currentRemoved).toBe(false);
+ });
+ it('points at the track that followed the removed current one', () => {
+ const result = removeTracks(['a', 'b', 'c', 'd'].map(t), new Set(['a', 'c']), 2);
+ expect(ids(result.queue)).toEqual(['b', 'd']);
+ expect(result.currentRemoved).toBe(true);
+ expect(result.nextIndex).toBe(1);
+ });
+ it('reports an empty queue when everything is removed', () => {
+ const result = removeTracks(['a'].map(t), new Set(['a']), 0);
+ expect(result.queue).toEqual([]);
+ expect(result.currentRemoved).toBe(true);
+ });
+});
diff --git a/web-player/src/lib/player/queue.ts b/web-player/src/lib/player/queue.ts
index 9ebe473..a24a19c 100644
--- a/web-player/src/lib/player/queue.ts
+++ b/web-player/src/lib/player/queue.ts
@@ -25,6 +25,18 @@ export function append(queue: T[], items: T[]): T[] {
return added.length > 0 ? [...queue, ...added] : queue;
}
+export function removeTracks(
+ queue: T[],
+ ids: ReadonlySet,
+ currentIndex: number
+): { queue: T[]; currentRemoved: boolean; nextIndex: number } {
+ const remaining = queue.filter((item) => !ids.has(item.id));
+ const currentRemoved = currentIndex >= 0 && ids.has(queue[currentIndex]?.id ?? '');
+ const before =
+ currentIndex < 0 ? 0 : queue.slice(0, currentIndex).filter((i) => !ids.has(i.id)).length;
+ return { queue: remaining, currentRemoved, nextIndex: before };
+}
+
export function move(queue: T[], from: number, insertAt: number): T[] {
if (from < 0 || from >= queue.length) return queue;
const target = insertAt > from ? insertAt - 1 : insertAt;
diff --git a/web-player/src/lib/state/pagedList.svelte.ts b/web-player/src/lib/state/pagedList.svelte.ts
index 992e8d7..c3247e2 100644
--- a/web-player/src/lib/state/pagedList.svelte.ts
+++ b/web-player/src/lib/state/pagedList.svelte.ts
@@ -30,6 +30,10 @@ export function createPagedList(
loading = false;
error = false;
},
+ merge(fresh: T[]) {
+ const byKey = new Map(fresh.map((item) => [keyOf(item), item]));
+ items = items.map((item) => byKey.get(keyOf(item)) ?? item);
+ },
async loadMore() {
if (loading) return;
loading = true;