From 9928840577150a423daa696ac5f50553a53f9c1e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 25 Sep 2026 01:07:15 +0200 Subject: [PATCH] Add library manager with tracks, albums and uploads sections --- .../components/music/ManageTrackList.svelte | 143 +++++++++++++ .../music/library/LibraryAlbumEditor.svelte | 193 ++++++++++++++++++ .../music/library/LibraryAlbumsSection.svelte | 55 +++++ .../music/library/LibraryTracksSection.svelte | 124 +++++++++++ .../library/LibraryUploadsSection.svelte | 161 +++++++++++++++ .../music/library/TrackPickList.svelte | 38 ++++ .../music/library/UploadQueueList.svelte | 98 +++++++++ .../music/library/UploadSettings.svelte | 167 +++++++++++++++ .../src/lib/components/shell/TopBar.svelte | 39 +--- .../components/ui/forms/ImageDropzone.svelte | 7 +- .../lib/components/ui/media/MediaCard.svelte | 18 +- .../ui/media/MediaCreateCard.svelte | 29 +++ .../components/ui/media/StatusBadge.svelte | 21 ++ .../components/ui/primitives/Checkbox.svelte | 10 +- .../lib/components/ui/primitives/Chip.svelte | 1 + .../components/ui/primitives/ChipGroup.svelte | 30 +++ .../lib/components/ui/primitives/Input.svelte | 6 + .../ui/primitives/SearchInput.svelte | 55 +++++ web-player/src/lib/data/libraryTracks.test.ts | 42 ++++ web-player/src/lib/data/libraryTracks.ts | 41 ++++ web-player/src/lib/data/uploadQueue.test.ts | 45 ++++ web-player/src/lib/data/uploadQueue.ts | 98 +++++++++ web-player/src/lib/server/mappers.ts | 20 +- web-player/src/lib/types.ts | 9 + .../src/routes/(app)/albums/+page.svelte | 2 +- .../src/routes/(app)/library/+page.server.ts | 167 +++++++++++++-- .../src/routes/(app)/library/+page.svelte | 111 +++++----- web-player/src/routes/layout.css | 10 + 28 files changed, 1622 insertions(+), 118 deletions(-) create mode 100644 web-player/src/lib/components/music/ManageTrackList.svelte create mode 100644 web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte create mode 100644 web-player/src/lib/components/music/library/LibraryAlbumsSection.svelte create mode 100644 web-player/src/lib/components/music/library/LibraryTracksSection.svelte create mode 100644 web-player/src/lib/components/music/library/LibraryUploadsSection.svelte create mode 100644 web-player/src/lib/components/music/library/TrackPickList.svelte create mode 100644 web-player/src/lib/components/music/library/UploadQueueList.svelte create mode 100644 web-player/src/lib/components/music/library/UploadSettings.svelte create mode 100644 web-player/src/lib/components/ui/media/MediaCreateCard.svelte create mode 100644 web-player/src/lib/components/ui/media/StatusBadge.svelte create mode 100644 web-player/src/lib/components/ui/primitives/ChipGroup.svelte create mode 100644 web-player/src/lib/components/ui/primitives/SearchInput.svelte create mode 100644 web-player/src/lib/data/libraryTracks.test.ts create mode 100644 web-player/src/lib/data/libraryTracks.ts create mode 100644 web-player/src/lib/data/uploadQueue.test.ts create mode 100644 web-player/src/lib/data/uploadQueue.ts diff --git a/web-player/src/lib/components/music/ManageTrackList.svelte b/web-player/src/lib/components/music/ManageTrackList.svelte new file mode 100644 index 0000000..b5b72c4 --- /dev/null +++ b/web-player/src/lib/components/music/ManageTrackList.svelte @@ -0,0 +1,143 @@ + + + + +
+
+ allSelected, toggleAll} /> + Título + {#each COLUMNS as column (column.id)} + + {/each} + + +
+
+ {#each tracks as track, i (track.id)} + {@const active = player.currentId === track.id} +
+ selected.has(track.id), (checked) => toggle(track.id, checked)} + /> +
onPlay(i)} + class="flex min-w-0 flex-1 text-left" + > + + {#snippet overlay()} + {#if active} + + {/if} + {/snippet} + +
+ {#each COLUMNS as column (column.id)} + + {/each} + + {fmtTime(track.duration)} + + onDelete(track)}> + + +
+ {/each} +
+
diff --git a/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte b/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte new file mode 100644 index 0000000..b3aaaa9 --- /dev/null +++ b/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte @@ -0,0 +1,193 @@ + + + + +
+
{ + submitting = true; + return async ({ update, result }) => { + await update({ reset: false }); + submitting = false; + if (result.type === 'success') onClose(); + }; + }} + class="mt-6 grid gap-8 lg:grid-cols-[auto_1fr]" + > + + + + +
+ +

Las canciones del álbum usan esta portada.

+ + + + + + + + + + + + + + + + +
+ +
+
+
+

Canciones

+

+ {inAlbum.length > 0 ? plural(inAlbum.length, 'canción', 'canciones') : 'Sin canciones'} +

+
+ +
+ + {#if available.length > 0} +

Añadir desde tu biblioteca

+ + {/if} + +
+ {#if inAlbum.length > 0} +

En este álbum

+ + {:else} + + {/if} +
+ + {#if formMessage} + {formMessage} + {/if} + +
+ + {#if !isNew} + + {/if} +
+
+
+
+ +{#if album} + (confirmingDelete = false)} + title="¿Eliminar «{album.title}»?" + description="Las canciones no se borran: solo se elimina el álbum." + confirmLabel="Eliminar" + action="?/deleteAlbum" + fields={{ albumId: album.id }} + /> +{/if} diff --git a/web-player/src/lib/components/music/library/LibraryAlbumsSection.svelte b/web-player/src/lib/components/music/library/LibraryAlbumsSection.svelte new file mode 100644 index 0000000..bcfedec --- /dev/null +++ b/web-player/src/lib/components/music/library/LibraryAlbumsSection.svelte @@ -0,0 +1,55 @@ + + +{#if editing} +
+ {#key editing.album?.id} + (editing = null)} + /> + {/key} +
+{:else} +
+ (editing = { album: null })} + /> + {#each albums as album, i (album.id)} + { + event.preventDefault(); + editing = { album }; + }} + /> + {/each} +
+{/if} diff --git a/web-player/src/lib/components/music/library/LibraryTracksSection.svelte b/web-player/src/lib/components/music/library/LibraryTracksSection.svelte new file mode 100644 index 0000000..336b83d --- /dev/null +++ b/web-player/src/lib/components/music/library/LibraryTracksSection.svelte @@ -0,0 +1,124 @@ + + +{#if errorMessage} + {errorMessage} +{/if} + +{#if tracks.length > 0} + {#if selectedIds.length > 0} +
+ {plural(selectedIds.length, 'seleccionada', 'seleccionadas')} + + +
+ {:else} +
+ + +
+ {/if} + + {#if visible.length > 0} + (pendingDelete = [track])} + /> + {:else} + + {/if} +{:else} + + {#snippet actions()} + + {/snippet} + +{/if} + + 0} + onClose={() => (pendingDelete = [])} + title="¿Borrar {pendingTitle}?" + 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" + fields={{ trackIds: pendingDelete.map((track) => track.id).join(',') }} +/> diff --git a/web-player/src/lib/components/music/library/LibraryUploadsSection.svelte b/web-player/src/lib/components/music/library/LibraryUploadsSection.svelte new file mode 100644 index 0000000..78f4fc4 --- /dev/null +++ b/web-player/src/lib/components/music/library/LibraryUploadsSection.svelte @@ -0,0 +1,161 @@ + + +
+
+
{ + event.preventDefault(); + dragging = true; + }} + ondragleave={() => (dragging = false)} + ondrop={onDrop} + class="flex min-h-64 flex-col items-center justify-center rounded-panel border border-dashed p-6 text-center transition sm:p-10 {dragging + ? 'border-accent/60 bg-accent-tint' + : 'border-line-strong bg-surface'}" + > + + + +

+ Arrastra tus archivos aquí +

+

+ WAV, FLAC, AIFF o MP3 · hasta 500 MB por archivo · puedes soltar carpetas enteras +

+ + +
+ + single?.title ?? '', (value) => single && setTitle(single.id, value)} + titleDisabled={single === null} + /> +
+ +
+
+
+

Cola de subida

+

+ {queue.length > 0 ? queueMeta : 'Todavía no hay archivos'} +

+
+
+ {#if queue.length > 0} + + {/if} + +
+
+ {#if queue.length > 0} + + {:else} +

+ Los archivos que añadas aparecerán aquí. +

+ {/if} +
+
diff --git a/web-player/src/lib/components/music/library/TrackPickList.svelte b/web-player/src/lib/components/music/library/TrackPickList.svelte new file mode 100644 index 0000000..71ec02d --- /dev/null +++ b/web-player/src/lib/components/music/library/TrackPickList.svelte @@ -0,0 +1,38 @@ + + +
+ {#each tracks as track (track.id)} +
+ onPick(track)} + class="px-3 py-2" + > + {#snippet trailing()} + + {/snippet} + +
+ {/each} +
diff --git a/web-player/src/lib/components/music/library/UploadQueueList.svelte b/web-player/src/lib/components/music/library/UploadQueueList.svelte new file mode 100644 index 0000000..27f1665 --- /dev/null +++ b/web-player/src/lib/components/music/library/UploadQueueList.svelte @@ -0,0 +1,98 @@ + + + + +
+
+ Título + + + + + Estado + +
+
+ {#each items as item (item.id)} +
+
+ + {item.format} + +
+ onTitle(item.id, event.currentTarget.value)} + aria-label="Título de {item.file.name}" + class="w-full truncate rounded-tag bg-transparent text-sm text-fg transition-colors hover:bg-surface focus:bg-surface-2 focus:outline-none" + /> +

{item.file.name}

+
+
+ + + + + + {#if item.error} + + {:else} + + {/if} + + onRemove(item.id)}> + + +
+ {/each} +
+
diff --git a/web-player/src/lib/components/music/library/UploadSettings.svelte b/web-player/src/lib/components/music/library/UploadSettings.svelte new file mode 100644 index 0000000..180422f --- /dev/null +++ b/web-player/src/lib/components/music/library/UploadSettings.svelte @@ -0,0 +1,167 @@ + + + diff --git a/web-player/src/lib/components/shell/TopBar.svelte b/web-player/src/lib/components/shell/TopBar.svelte index 072d8a1..0491e4f 100644 --- a/web-player/src/lib/components/shell/TopBar.svelte +++ b/web-player/src/lib/components/shell/TopBar.svelte @@ -4,10 +4,9 @@ import type { SessionUser } from '$lib/types'; import { SEARCH_DEBOUNCE_MS, SEARCH_MIN_LENGTH } from '$lib/config'; import { searchHref } from '$lib/utils/hrefs'; - import Search from '@lucide/svelte/icons/search'; - import X from '@lucide/svelte/icons/x'; import ChevronLeft from '@lucide/svelte/icons/chevron-left'; import AccountMenu from '$lib/components/shell/AccountMenu.svelte'; + import SearchInput from '$lib/components/ui/primitives/SearchInput.svelte'; import IconButton from '$lib/components/ui/primitives/IconButton.svelte'; import Button from '$lib/components/ui/primitives/Button.svelte'; @@ -21,7 +20,6 @@ let query = $derived( page.url.pathname === '/explore' ? (page.url.searchParams.get('q') ?? '') : '' ); - let searchInput: HTMLInputElement | undefined = $state(); let searchTimeout: ReturnType | undefined; function runSearch(term: string) { @@ -33,16 +31,14 @@ }); } - function onInput() { + function oninput() { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => runSearch(query.trim()), SEARCH_DEBOUNCE_MS); } - function clearQuery() { - query = ''; + function onclear() { clearTimeout(searchTimeout); runSearch(''); - searchInput?.focus(); } function onSubmit(event: SubmitEvent) { @@ -58,29 +54,12 @@