web-player: fix playlist ownership, sidebar counts, home spotlight and assorted page bugs

This commit is contained in:
2026-09-24 05:02:26 +02:00
parent dda8c30d45
commit bc43b4b6eb
21 changed files with 216 additions and 232 deletions
@@ -31,12 +31,6 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
}
});
const playlistsPromise = user
? api.GET('/playlists/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
})
: Promise.resolve(null);
const albumsPromise = query
? api.GET('/albums', {
params: { query: { title: query, pageNumber: 1, pageSize: EXPLORE_ALBUMS_PAGE_SIZE } }
@@ -49,9 +43,8 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
})
: Promise.resolve(null);
const [tracksRes, playlistsRes, albumsRes, usersRes] = await Promise.all([
const [tracksRes, albumsRes, usersRes] = await Promise.all([
tracksPromise,
playlistsPromise,
albumsPromise,
usersPromise
]);
@@ -64,9 +57,10 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
genres,
tracks,
albums: (albumsRes?.data?.items ?? []).map((item) => item.album),
albumsTotal: Number(albumsRes?.data?.totalItemCount ?? 0),
users: usersRes?.data?.items ?? [],
viewerId: user?.sub ?? null,
playlists: playlistsRes?.data?.items ?? []
usersTotal: Number(usersRes?.data?.totalItemCount ?? 0),
viewerId: user?.sub ?? null
};
};
@@ -83,7 +83,7 @@
const hasAlbums = $derived(albums.length > 0);
const hasUsers = $derived(users.length > 0);
const playlistMatches = $derived(matchPlaylists(data.playlists, data.query));
const playlistMatches = $derived(matchPlaylists(data.userPlaylists, data.query));
const hasPlaylists = $derived(playlistMatches.length > 0);
const nothingFound = $derived(
@@ -92,10 +92,10 @@
const counts = $derived(
searchCounts({
tracks: songRows.length,
albums: albums.length,
tracks: Number(data.tracks.totalItemCount),
albums: data.albumsTotal,
playlists: playlistMatches.length,
users: users.length
users: data.usersTotal
})
);
const chips = $derived(searchChips(counts));
@@ -400,7 +400,7 @@
>
{#snippet trailing()}
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
{Number(album.trackCount)} canciones
{plural(Number(album.trackCount), 'canción', 'canciones')}
</span>
{/snippet}
</ListRow>
@@ -462,7 +462,7 @@
fields: { trackId: String(trackMenu.state.track.id) },
label: 'Añadir a una playlist'
}}
playlists={data.playlists}
playlists={data.userPlaylists}
/>
{/if}
@@ -481,6 +481,6 @@
fields: { albumId: albumMenu.albumId },
label: 'Añadir álbum a una playlist'
}}
playlists={data.playlists}
playlists={data.userPlaylists}
/>
{/if}
@@ -8,6 +8,7 @@
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
import Button from '$lib/components/ui/primitives/Button.svelte';
import ConfirmDialog from '$lib/components/ui/overlay/ConfirmDialog.svelte';
import TrackList from '$lib/components/ui/media/TrackList.svelte';
let { data } = $props();
@@ -16,6 +17,8 @@
const total = $derived(Number(data.tracks.totalItemCount));
const listTracks = $derived(items.map((track) => ({ ...track, uploadedAt: track.createdAt })));
let deleting = $state<(typeof items)[number] | null>(null);
function togglePlay(index: number) {
player.playOrToggle(toQueueItems(items), index);
}
@@ -46,7 +49,11 @@
tracks={listTracks}
columns={['uploaded', 'plays']}
onPlay={togglePlay}
rowAction={{ action: '?/deleteTrack', icon: X, label: 'Borrar canción' }}
rowAction={{
onclick: (track) => (deleting = items.find((item) => item.id === track.id) ?? null),
icon: X,
label: 'Borrar canción'
}}
/>
{:else}
<EmptyState
@@ -63,3 +70,13 @@
</EmptyState>
{/if}
</Page>
<ConfirmDialog
open={deleting !== null}
onClose={() => (deleting = null)}
title="¿Borrar «{deleting?.title ?? ''}»?"
description="Esta acción no se puede deshacer, la canción se eliminará de tu biblioteca, de tus álbumes y de las playlists donde esté."
confirmLabel="Borrar"
action="?/deleteTrack"
fields={{ trackId: deleting?.id ?? '' }}
/>
@@ -1,25 +1,14 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, requireUser, unwrapOrError } from '$lib/server/api';
import { addTrackAction } from '$lib/server/playlistActions';
import { PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config';
export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
const user = requireUser(locals, url);
requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const [mixRes, playlistsRes] = await Promise.all([
api.GET('/mixes/{id}', { params: { path: { id: params.id } } }),
api.GET('/playlists/users/{userId}', {
params: {
path: { userId: user.sub },
query: { pageNumber: 1, pageSize: PLAYLIST_PICKER_PAGE_SIZE }
}
})
]);
const mixRes = await api.GET('/mixes/{id}', { params: { path: { id: params.id } } });
const mix = unwrapOrError(mixRes, 'Mezcla no encontrada.', 404);
return { mix, playlists: playlistsRes.data?.items ?? [] };
return { mix: unwrapOrError(mixRes, 'Mezcla no encontrada.', 404) };
};
export const actions: Actions = {
@@ -116,6 +116,6 @@
fields: { trackId: String(trackMenu.state.track.id) },
label: 'Añadir a una playlist'
}}
playlists={data.playlists}
playlists={data.userPlaylists}
/>
{/if}
@@ -13,7 +13,7 @@ const MAX_NAME = 100;
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
const { allowAnonymousListening } = await parent();
optionalUser(locals, url, allowAnonymousListening);
const user = optionalUser(locals, url, allowAnonymousListening);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const [playlistRes, tracksRes] = await Promise.all([
@@ -26,7 +26,9 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
const playlist = unwrapOrError(playlistRes, 'Playlist no encontrada.', 404);
const tracks = tracksRes.data?.items ?? [];
return { playlist, tracks };
const isOwner = user !== null && String(playlist.ownerUserId) === user.sub;
return { playlist, tracks, isOwner, section: isOwner ? '/playlists' : null };
};
export const actions: Actions = {
@@ -9,6 +9,7 @@
import Artwork from '$lib/components/ui/media/Artwork.svelte';
import PlaylistForm from '$lib/components/ui/forms/PlaylistForm.svelte';
import Button from '$lib/components/ui/primitives/Button.svelte';
import BackLink from '$lib/components/ui/primitives/BackLink.svelte';
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
import TrackList from '$lib/components/ui/media/TrackList.svelte';
import { playlistMeta } from '$lib/utils/format';
@@ -17,6 +18,7 @@
const playlist = $derived(data.playlist);
const tracks = $derived(data.tracks);
const isOwner = $derived(data.isOwner);
const listTracks = $derived(tracks.map((track) => ({ ...track, addedAt: track.createdAt })));
let editing = $state(false);
@@ -39,8 +41,13 @@
</svelte:head>
<Page>
<BackLink
href={isOwner ? '/playlists' : '/'}
label={isOwner ? 'Volver a tus playlists' : 'Volver al inicio'}
/>
<PageHeader
eyebrow="Lista"
eyebrow="Playlist"
title={playlist.name}
description={playlist.description ?? undefined}
meta="{playlistMeta(tracks.length)} · {playlist.visibility === 'Public'
@@ -67,10 +74,12 @@
Reproducir
{/if}
</Button>
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
Eliminar
</Button>
{#if isOwner}
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
Eliminar
</Button>
{/if}
{/snippet}
</PageHeader>
@@ -79,7 +88,9 @@
tracks={listTracks}
columns={['added', 'plays']}
onPlay={playFrom}
rowAction={{ action: '?/removeTrack', icon: X, label: 'Quitar de la playlist' }}
rowAction={isOwner
? { action: '?/removeTrack', icon: X, label: 'Quitar de la playlist' }
: undefined}
/>
{:else}
<EmptyState
@@ -299,14 +299,9 @@
</Surface>
<Checkbox bind:checked={acceptedTerms}>
Acepto los
<a
href="/terms"
class="text-accent-soft underline underline-offset-2 transition hover:brightness-110"
>
términos y condiciones
</a>
al subir {title.trim() !== '' ? `«${title}»` : 'esta canción'}.
Acepto los términos y condiciones al subir {title.trim() !== ''
? `«${title}»`
: 'esta canción'}.
</Checkbox>
{#if status === 'uploading'}