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} + + {#if error} + {error} + {/if} {#snippet actions()} -
- {#each Object.entries(fields) as [name, value] (name)} - - {/each} - -
+ {#if submit} +
+ {@render body()} +
+ {:else} +
+ {@render body()} +
+ {/if} {/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;