Design Prot Merge #1
@@ -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,13 +27,8 @@
|
|||||||
Musify
|
Musify
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="relative">
|
|
||||||
{#if user}
|
{#if user}
|
||||||
<AccountMenu
|
<AccountMenu {user} {accountUrl} width="md">
|
||||||
{user}
|
|
||||||
{accountUrl}
|
|
||||||
panelClass="absolute top-full right-0 mt-2 w-56 overflow-hidden"
|
|
||||||
>
|
|
||||||
{#snippet trigger({ toggle, open })}
|
{#snippet trigger({ toggle, open })}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -55,5 +50,4 @@
|
|||||||
<LogIn class="size-icon-lg" strokeWidth={2} />
|
<LogIn class="size-icon-lg" strokeWidth={2} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
+35
-31
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user