web-player: unify the context menu (ContextMenu) + AccountMenu

Add src/lib/components/ui/ContextMenu.svelte, replacing
TrackContextMenu and AlbumContextMenu (109 and 119 lines, 90%
identical): same positioning function (contextMenuPosition, exported
from the module, using MENU_WIDTH/SUBMENU_WIDTH moved to config.ts),
same backdrop, same Escape handling, same generic "Add to playlist"
submenu via {items, playlistAction, playlists}. Uses MenuItem for
actions (previously each reimplemented its own classes by hand) and
Artwork size="xs" for playlist thumbnails.

menus.svelte.ts (createTrackMenu) adapts to the new
contextMenuPosition; Home, Explore (track and album menus) and Mix
are migrated.

AccountMenu: panelClass (free-form Tailwind classes) becomes prop
width: 'sm'|'md'; adds Escape-to-close (accessibility improvement, it
didn't close before); TopBar and MobileHeader no longer wrap it in a
duplicate div.relative (its own root already is one). The panel's
z-index becomes z-(--z-menu), matching ContextMenu (previously z-20
in TopBar / no explicit z in MobileHeader).

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
This commit is contained in:
2026-09-20 23:05:53 +02:00
parent 1320b12699
commit 14ee1dc45d
12 changed files with 144 additions and 227 deletions
+1 -1
View File
@@ -778,7 +778,7 @@ Tareas:
| P3 | ✅ | (pendiente) | Se quitó el nombre bajo el avatar de perfil (decisión tomada: redundante con el título de CollectionHeader). Se añadió `size="fill"` a Artwork (no está en la spec) para las tarjetas de grid fluidas (AlbumCard/PlaylistCard/MixTile), ya que P9 (MediaCard) aún no existe. SearchResultRow perdió su prop `kind`/radio-por-tipo (ahora el radio lo decide Artwork/Avatar). Sin QA visual autenticada (Docker no disponible) | | P3 | ✅ | (pendiente) | Se quitó el nombre bajo el avatar de perfil (decisión tomada: redundante con el título de CollectionHeader). Se añadió `size="fill"` a Artwork (no está en la spec) para las tarjetas de grid fluidas (AlbumCard/PlaylistCard/MixTile), ya que P9 (MediaCard) aún no existe. SearchResultRow perdió su prop `kind`/radio-por-tipo (ahora el radio lo decide Artwork/Avatar). Sin QA visual autenticada (Docker no disponible) |
| P4 | ✅ | (pendiente) | Decisión tomada sin bloquear: se elimina el botón "adelante" decorativo (TopBar) y el icono `AlignJustify` decorativo (PlayerExtras) por ser código muerto visual. `TransportControls` (shuffle/prev/next/repeat) se deja sin migrar a IconButton a propósito: son iconos sin chrome (sin fondo/hit-box), migrarlos habría añadido una píldora de hover no presente hoy — riesgo visual no verificable sin Docker | | P4 | ✅ | (pendiente) | Decisión tomada sin bloquear: se elimina el botón "adelante" decorativo (TopBar) y el icono `AlignJustify` decorativo (PlayerExtras) por ser código muerto visual. `TransportControls` (shuffle/prev/next/repeat) se deja sin migrar a IconButton a propósito: son iconos sin chrome (sin fondo/hit-box), migrarlos habría añadido una píldora de hover no presente hoy — riesgo visual no verificable sin Docker |
| P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas | | P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas |
| P6 | ⬜ | | | | P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) |
| P7 | ⬜ | | | | P7 | ⬜ | | |
| P8 | ⬜ | | | | P8 | ⬜ | | |
| P9 | ⬜ | | | | P9 | ⬜ | | |
@@ -1,118 +0,0 @@
<script lang="ts" module>
const MENU_WIDTH = 240;
const SUBMENU_WIDTH = 256;
export type AlbumMenuState = { x: number; y: number; openLeft: boolean; albumId: string };
export function albumContextMenuStateFor(event: MouseEvent, albumId: string): AlbumMenuState {
event.preventDefault();
return {
albumId,
x: Math.max(8, Math.min(event.clientX, window.innerWidth - MENU_WIDTH - 8)),
y: Math.max(8, Math.min(event.clientY, window.innerHeight - 60)),
openLeft: event.clientX + MENU_WIDTH + SUBMENU_WIDTH + 16 > window.innerWidth
};
}
</script>
<script lang="ts">
import { enhance } from '$app/forms';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import ListPlus from '@lucide/svelte/icons/list-plus';
import ListEnd from '@lucide/svelte/icons/list-end';
import Artwork from '$lib/components/ui/Artwork.svelte';
import GlassMenu from '$lib/components/ui/GlassMenu.svelte';
interface Props {
menu: AlbumMenuState;
playlists: { id: string; name: string }[];
onClose: () => void;
onPlayNext: () => void;
onAddToQueue: () => void;
}
let { menu, playlists, onClose, onPlayNext, onAddToQueue }: Props = $props();
function onWindowKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') onClose();
}
</script>
<svelte:window onkeydown={onWindowKeydown} />
<div
class="fixed inset-0 z-30"
role="presentation"
onclick={onClose}
oncontextmenu={(e) => {
e.preventDefault();
onClose();
}}
></div>
<GlassMenu class="fixed z-40 w-60" style="left:{menu.x}px; top:{menu.y}px;">
<button
type="button"
onclick={onPlayNext}
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover"
>
<ListPlus class="h-4.5 w-4.5 shrink-0" strokeWidth={2} />
<span>Reproducir a continuación</span>
</button>
<button
type="button"
onclick={onAddToQueue}
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover"
>
<ListEnd class="h-4.5 w-4.5 shrink-0" strokeWidth={2} />
<span>Añadir a la cola</span>
</button>
{#if playlists.length > 0}
<div class="group/addmenu relative">
<div
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
>
<span class="flex-1 cursor-default">Añadir álbum a una playlist</span>
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
</div>
<div
class="invisible absolute top-0 z-50 opacity-0 transition group-hover/addmenu:visible group-hover/addmenu:opacity-100 max-sm:top-full max-sm:right-0 max-sm:left-0 max-sm:px-0 max-sm:pt-1.5 {menu.openLeft
? 'right-full pr-1.5'
: 'left-full pl-1.5'}"
>
<div
class="w-64 overflow-hidden rounded-panel border border-line bg-elevated p-2 max-sm:w-full"
>
<div class="max-h-80 overflow-y-auto">
{#each playlists as playlist (playlist.id)}
<form
method="POST"
action="?/addAlbumToPlaylist"
use:enhance={() =>
({ update }) => {
onClose();
return update({ reset: false, invalidateAll: false });
}}
>
<input type="hidden" name="playlistId" value={playlist.id} />
<input type="hidden" name="albumId" value={menu.albumId} />
<button
type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
>
<Artwork
src="/api/playlists/{playlist.id}/cover?size=small"
size="xs"
class="shrink-0"
/>
<span class="truncate text-fg-2">{playlist.name}</span>
</button>
</form>
{/each}
</div>
</div>
</div>
</div>
{:else}
<p class="px-2 py-2.5 text-sm text-fg-3">Todavía no tienes playlists.</p>
{/if}
</GlassMenu>
@@ -27,33 +27,27 @@
Musify Musify
</a> </a>
<div class="relative"> {#if user}
{#if user} <AccountMenu {user} {accountUrl} width="md">
<AccountMenu {#snippet trigger({ toggle, open })}
{user} <button
{accountUrl} type="button"
panelClass="absolute top-full right-0 mt-2 w-56 overflow-hidden" onclick={toggle}
> aria-label="Tu cuenta"
{#snippet trigger({ toggle, open })} aria-expanded={open}
<button class="flex items-center rounded-full"
type="button" >
onclick={toggle} <Avatar name={user.name} src={user.picture} size="sm" />
aria-label="Tu cuenta" </button>
aria-expanded={open} {/snippet}
class="flex items-center rounded-full" {#snippet extraItems({ close })}
> <MenuItem icon={Folder} label="Canciones subidas" href="/library" onclick={close} />
<Avatar name={user.name} src={user.picture} size="sm" /> <MenuItem icon={Upload} label="Subir música" href="/upload" onclick={close} />
</button> {/snippet}
{/snippet} </AccountMenu>
{#snippet extraItems({ close })} {:else}
<MenuItem icon={Folder} label="Canciones subidas" href="/library" onclick={close} /> <IconButton href="/auth/login" reload label="Iniciar sesión" shape="round" size="md">
<MenuItem icon={Upload} label="Subir música" href="/upload" onclick={close} /> <LogIn class="size-icon-lg" strokeWidth={2} />
{/snippet} </IconButton>
</AccountMenu> {/if}
{:else}
<IconButton href="/auth/login" reload label="Iniciar sesión" shape="round" size="md">
<LogIn class="size-icon-lg" strokeWidth={2} />
</IconButton>
{/if}
</div>
</header> </header>
+2 -6
View File
@@ -93,13 +93,9 @@
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="relative shrink-0"> <div class="shrink-0">
{#if user} {#if user}
<AccountMenu <AccountMenu {user} {accountUrl}>
{user}
{accountUrl}
panelClass="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden"
>
{#snippet trigger({ toggle, open })} {#snippet trigger({ toggle, open })}
<button <button
type="button" type="button"
@@ -14,18 +14,14 @@
interface Props { interface Props {
user: SessionUser; user: SessionUser;
accountUrl?: string | null; accountUrl?: string | null;
panelClass?: string; width?: 'sm' | 'md';
extraItems?: Snippet<[{ close: () => void }]>; extraItems?: Snippet<[{ close: () => void }]>;
trigger: Snippet<[{ toggle: () => void; open: boolean }]>; trigger: Snippet<[{ toggle: () => void; open: boolean }]>;
} }
let { let { user, accountUrl, width = 'sm', extraItems, trigger }: Props = $props();
user,
accountUrl, const WIDTH: Record<'sm' | 'md', string> = { sm: 'w-52', md: 'w-56' };
panelClass = 'absolute top-full right-0 mt-2 w-52 overflow-hidden',
extraItems,
trigger
}: Props = $props();
let open = $state(false); let open = $state(false);
@@ -36,13 +32,19 @@
function close() { function close() {
open = false; open = false;
} }
function onWindowKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') close();
}
</script> </script>
<svelte:window onkeydown={onWindowKeydown} />
<div class="relative" use:clickOutside={close}> <div class="relative" use:clickOutside={close}>
{@render trigger({ toggle, open })} {@render trigger({ toggle, open })}
{#if open} {#if open}
<GlassMenu class={panelClass}> <GlassMenu class="absolute top-full right-0 z-(--z-menu) mt-2 overflow-hidden {WIDTH[width]}">
<MenuItem icon={User} label="Ver perfil" href="/u/{user.sub}" onclick={close} /> <MenuItem icon={User} label="Ver perfil" href="/u/{user.sub}" onclick={close} />
{@render extraItems?.({ close })} {@render extraItems?.({ close })}
{#if accountUrl} {#if accountUrl}
@@ -1,15 +1,15 @@
<script lang="ts" module> <script lang="ts" module>
import type { TrackTarget } from '$lib/tracks'; import { MENU_WIDTH, SUBMENU_WIDTH } from '$lib/config';
export type ContextMenuState = TrackTarget & { x: number; y: number; openLeft: boolean }; export interface MenuPosition {
x: number;
y: number;
openLeft: boolean;
}
const MENU_WIDTH = 240; export function contextMenuPosition(event: MouseEvent): MenuPosition {
const SUBMENU_WIDTH = 256;
export function contextMenuStateFor(event: MouseEvent, target: TrackTarget): ContextMenuState {
event.preventDefault(); event.preventDefault();
return { return {
...target,
x: Math.max(8, Math.min(event.clientX, window.innerWidth - MENU_WIDTH - 8)), x: Math.max(8, Math.min(event.clientX, window.innerWidth - MENU_WIDTH - 8)),
y: Math.max(8, Math.min(event.clientY, window.innerHeight - 60)), y: Math.max(8, Math.min(event.clientY, window.innerHeight - 60)),
openLeft: event.clientX + MENU_WIDTH + SUBMENU_WIDTH + 16 > window.innerWidth openLeft: event.clientX + MENU_WIDTH + SUBMENU_WIDTH + 16 > window.innerWidth
@@ -19,19 +19,23 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import ListPlus from '@lucide/svelte/icons/list-plus'; import type { LucideIcon } from '@lucide/svelte';
import ChevronRight from '@lucide/svelte/icons/chevron-right'; import ChevronRight from '@lucide/svelte/icons/chevron-right';
import Artwork from '$lib/components/ui/Artwork.svelte'; import Artwork from './Artwork.svelte';
import GlassMenu from '$lib/components/ui/GlassMenu.svelte'; import GlassMenu from './GlassMenu.svelte';
import MenuItem from './MenuItem.svelte';
interface Props { interface Props {
menu: ContextMenuState; x: number;
playlists: { id: string; name: string }[]; y: number;
openLeft: boolean;
onClose: () => void; onClose: () => void;
onAddToQueue: () => void; items: { icon: LucideIcon; label: string; onclick: () => void }[];
playlistAction?: { action: string; fields: Record<string, string>; label: string };
playlists?: { id: string; name: string }[];
} }
let { menu, playlists, onClose, onAddToQueue }: Props = $props(); let { x, y, openLeft, onClose, items, playlistAction, playlists = [] }: Props = $props();
function onWindowKeydown(event: KeyboardEvent) { function onWindowKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') onClose(); if (event.key === 'Escape') onClose();
@@ -41,7 +45,7 @@
<svelte:window onkeydown={onWindowKeydown} /> <svelte:window onkeydown={onWindowKeydown} />
<div <div
class="fixed inset-0 z-30" class="fixed inset-0 z-(--z-backdrop)"
role="presentation" role="presentation"
onclick={onClose} onclick={onClose}
oncontextmenu={(e) => { oncontextmenu={(e) => {
@@ -49,36 +53,34 @@
onClose(); onClose();
}} }}
></div> ></div>
<GlassMenu class="fixed z-40 w-60" style="left:{menu.x}px; top:{menu.y}px;"> <GlassMenu class="fixed z-(--z-menu) w-(--mf-menu-w)" style="left:{x}px; top:{y}px;">
<button {#each items as item (item.label)}
type="button" <MenuItem icon={item.icon} label={item.label} onclick={item.onclick} />
onclick={onAddToQueue} {/each}
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover" {#if playlistAction}
>
<ListPlus class="h-4.5 w-4.5 shrink-0" strokeWidth={2} />
<span>Reproducir a continuación</span>
</button>
{#if playlists.length > 0}
<div class="group/addmenu relative"> <div class="group/addmenu relative">
<div <div
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover" class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
> >
<span class="flex-1 cursor-default">Añadir a una playlist</span> <span class="flex-1 cursor-default">{playlistAction.label}</span>
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" /> <ChevronRight class="size-icon-sm shrink-0 text-fg-3" />
</div> </div>
<div <div
class="invisible absolute top-0 z-50 opacity-0 transition group-hover/addmenu:visible group-hover/addmenu:opacity-100 max-sm:top-full max-sm:right-0 max-sm:left-0 max-sm:px-0 max-sm:pt-1.5 {menu.openLeft class="invisible absolute top-0 z-(--z-submenu) opacity-0 transition group-hover/addmenu:visible group-hover/addmenu:opacity-100 max-sm:top-full max-sm:right-0 max-sm:left-0 max-sm:px-0 max-sm:pt-1.5 {openLeft
? 'right-full pr-1.5' ? 'right-full pr-1.5'
: 'left-full pl-1.5'}" : 'left-full pl-1.5'}"
> >
<div <div
class="w-64 overflow-hidden rounded-panel border border-line bg-elevated p-2 max-sm:w-full" class="w-(--mf-submenu-w) overflow-hidden rounded-panel border border-line bg-elevated p-2 max-sm:w-full"
> >
<div class="max-h-80 overflow-y-auto"> <div class="max-h-80 overflow-y-auto">
{#if playlists.length === 0}
<p class="px-2 py-2.5 text-sm text-fg-3">Todavía no tienes playlists.</p>
{/if}
{#each playlists as playlist (playlist.id)} {#each playlists as playlist (playlist.id)}
<form <form
method="POST" method="POST"
action="?/addTrack" action={playlistAction.action}
use:enhance={() => use:enhance={() =>
({ update }) => { ({ update }) => {
onClose(); onClose();
@@ -86,7 +88,9 @@
}} }}
> >
<input type="hidden" name="playlistId" value={playlist.id} /> <input type="hidden" name="playlistId" value={playlist.id} />
<input type="hidden" name="trackId" value={menu.track.id} /> {#each Object.entries(playlistAction.fields) as [name, value] (name)}
<input type="hidden" {name} {value} />
{/each}
<button <button
type="submit" type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover" class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
@@ -37,12 +37,12 @@
{onclick} {onclick}
class={classes} class={classes}
> >
<Icon class="h-4.5 w-4.5 shrink-0" strokeWidth={1.8} /> <Icon class="size-icon-md shrink-0" />
{label} {label}
</a> </a>
{:else} {:else}
<button {type} {onclick} class={classes}> <button {type} {onclick} class={classes}>
<Icon class="h-4.5 w-4.5 shrink-0" strokeWidth={1.8} /> <Icon class="size-icon-md shrink-0" />
{label} {label}
</button> </button>
{/if} {/if}
+2
View File
@@ -17,3 +17,5 @@ export const HOME_POPULAR_MAX = 8;
export const PLAYLIST_PICKER_PAGE_SIZE = 50; export const PLAYLIST_PICKER_PAGE_SIZE = 50;
export const LIKED_TRACKS_PAGE_SIZE = 500; export const LIKED_TRACKS_PAGE_SIZE = 500;
export const EXPLORE_USERS_LIMIT = 12; export const EXPLORE_USERS_LIMIT = 12;
export const MENU_WIDTH = 240;
export const SUBMENU_WIDTH = 256;
+5 -6
View File
@@ -1,19 +1,18 @@
import { import { contextMenuPosition, type MenuPosition } from '$lib/components/ui/ContextMenu.svelte';
contextMenuStateFor,
type ContextMenuState
} from '$lib/components/TrackContextMenu.svelte';
import { player } from './player/player.svelte'; import { player } from './player/player.svelte';
import { queueItemForTarget, type TrackTarget } from './tracks'; import { queueItemForTarget, type TrackTarget } from './tracks';
export type TrackMenuState = TrackTarget & MenuPosition;
export function createTrackMenu() { export function createTrackMenu() {
let state = $state<ContextMenuState | null>(null); let state = $state<TrackMenuState | null>(null);
return { return {
get state() { get state() {
return state; return state;
}, },
open(event: MouseEvent, target: TrackTarget) { open(event: MouseEvent, target: TrackTarget) {
state = contextMenuStateFor(event, target); state = { ...target, ...contextMenuPosition(event) };
}, },
close() { close() {
state = null; state = null;
@@ -17,11 +17,11 @@
import { EXPLORE_PAGE_SIZE } from '$lib/config'; import { EXPLORE_PAGE_SIZE } from '$lib/config';
import { searchHref } from '$lib/navigation.svelte'; import { searchHref } from '$lib/navigation.svelte';
import { createTrackMenu } from '$lib/menus.svelte'; import { createTrackMenu } from '$lib/menus.svelte';
import TrackContextMenu from '$lib/components/TrackContextMenu.svelte'; import ContextMenu, { contextMenuPosition } from '$lib/components/ui/ContextMenu.svelte';
import AlbumContextMenu, { import ListPlus from '@lucide/svelte/icons/list-plus';
albumContextMenuStateFor, import ListEnd from '@lucide/svelte/icons/list-end';
type AlbumMenuState
} from '$lib/components/AlbumContextMenu.svelte'; type AlbumMenuState = { x: number; y: number; openLeft: boolean; albumId: string };
import { appendUnique } from '$lib/collections'; import { appendUnique } from '$lib/collections';
import { import {
isTargetCurrent, isTargetCurrent,
@@ -190,7 +190,7 @@
} }
function openAlbumMenu(event: MouseEvent, albumId: string) { function openAlbumMenu(event: MouseEvent, albumId: string) {
albumMenu = albumContextMenuStateFor(event, albumId); albumMenu = { ...contextMenuPosition(event), albumId };
} }
async function albumPlayNext() { async function albumPlayNext() {
@@ -539,20 +539,38 @@
</Page> </Page>
{#if trackMenu.state} {#if trackMenu.state}
<TrackContextMenu <ContextMenu
menu={trackMenu.state} x={trackMenu.state.x}
playlists={data.playlists} y={trackMenu.state.y}
openLeft={trackMenu.state.openLeft}
onClose={() => trackMenu.close()} onClose={() => trackMenu.close()}
onAddToQueue={() => trackMenu.playNext()} items={[
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
]}
playlistAction={{
action: '?/addTrack',
fields: { trackId: String(trackMenu.state.track.id) },
label: 'Añadir a una playlist'
}}
playlists={data.playlists}
/> />
{/if} {/if}
{#if albumMenu} {#if albumMenu}
<AlbumContextMenu <ContextMenu
menu={albumMenu} x={albumMenu.x}
playlists={data.playlists} y={albumMenu.y}
openLeft={albumMenu.openLeft}
onClose={() => (albumMenu = null)} onClose={() => (albumMenu = null)}
onPlayNext={albumPlayNext} items={[
onAddToQueue={albumAddToQueue} { 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.playlists}
/> />
{/if} {/if}
@@ -15,7 +15,8 @@
import TrackMeta from '$lib/components/ui/TrackMeta.svelte'; import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte'; import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte'; import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
import TrackContextMenu from '$lib/components/TrackContextMenu.svelte'; import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import ListPlus from '@lucide/svelte/icons/list-plus';
import { createTrackMenu } from '$lib/menus.svelte'; import { createTrackMenu } from '$lib/menus.svelte';
import { mixItemKey, mixItemTarget } from '$lib/mixes'; import { mixItemKey, mixItemTarget } from '$lib/mixes';
import { import {
@@ -114,10 +115,19 @@
</Page> </Page>
{#if trackMenu.state} {#if trackMenu.state}
<TrackContextMenu <ContextMenu
menu={trackMenu.state} x={trackMenu.state.x}
playlists={data.playlists} y={trackMenu.state.y}
openLeft={trackMenu.state.openLeft}
onClose={() => trackMenu.close()} onClose={() => trackMenu.close()}
onAddToQueue={() => trackMenu.playNext()} items={[
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
]}
playlistAction={{
action: '?/addTrack',
fields: { trackId: String(trackMenu.state.track.id) },
label: 'Añadir a una playlist'
}}
playlists={data.playlists}
/> />
{/if} {/if}
+15 -5
View File
@@ -27,7 +27,8 @@
import SectionHeading from '$lib/components/ui/SectionHeading.svelte'; import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte'; import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
import TrackContextMenu from '$lib/components/TrackContextMenu.svelte'; import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import ListPlus from '@lucide/svelte/icons/list-plus';
import { createTrackMenu } from '$lib/menus.svelte'; import { createTrackMenu } from '$lib/menus.svelte';
import { HOME_CONTINUE_LIMIT, HOME_POPULAR_MAX } from '$lib/config'; import { HOME_CONTINUE_LIMIT, HOME_POPULAR_MAX } from '$lib/config';
@@ -423,10 +424,19 @@
</div> </div>
{#if trackMenu.state} {#if trackMenu.state}
<TrackContextMenu <ContextMenu
menu={trackMenu.state} x={trackMenu.state.x}
{playlists} y={trackMenu.state.y}
openLeft={trackMenu.state.openLeft}
onClose={() => trackMenu.close()} onClose={() => trackMenu.close()}
onAddToQueue={() => trackMenu.playNext()} items={[
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
]}
playlistAction={{
action: '?/addTrack',
fields: { trackId: String(trackMenu.state.track.id) },
label: 'Añadir a una playlist'
}}
{playlists}
/> />
{/if} {/if}