From b51f18a910b91149b0d3f5027288c00c7f2cb02f Mon Sep 17 00:00:00 2001 From: Till JS Date: Sat, 21 Mar 2026 11:05:12 +0100 Subject: [PATCH] feat(mukke): add right-click context menu to library songs Uses the shared ContextMenu component from @manacore/shared-ui to provide quick actions (play, edit metadata, open in editor, toggle favorite, delete) on right-click in the song list. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/routes/(app)/library/+page.svelte | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte index 97ab73e58..3a3ff6f80 100644 --- a/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte @@ -5,6 +5,7 @@ import { authStore } from '$lib/stores/auth.svelte'; import { playerStore } from '$lib/stores/player.svelte'; import SongEditor from '$lib/components/SongEditor.svelte'; + import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui'; import type { Song } from '@mukke/shared'; const tabs = ['songs', 'albums', 'artists', 'genres'] as const; @@ -12,6 +13,63 @@ let editingSong = $state(null); let failedCovers = $state>(new Set()); + // Context menu state + let contextMenuVisible = $state(false); + let contextMenuX = $state(0); + let contextMenuY = $state(0); + let contextMenuSong = $state<{ song: Song; index: number } | null>(null); + + function handleContextMenu(e: MouseEvent, song: Song, index: number) { + e.preventDefault(); + e.stopPropagation(); + contextMenuX = e.clientX; + contextMenuY = e.clientY; + contextMenuSong = { song, index }; + contextMenuVisible = true; + } + + function getContextMenuItems(): ContextMenuItem[] { + if (!contextMenuSong) return []; + const { song } = contextMenuSong; + return [ + { + id: 'play', + label: playerStore.currentSong?.id === song.id && playerStore.isPlaying ? 'Pause' : 'Play', + action: () => handlePlaySong(contextMenuSong!.song, contextMenuSong!.index), + }, + { id: 'divider-1', label: '', type: 'divider' as const }, + { + id: 'edit', + label: 'Edit Metadata', + action: () => { + editingSong = contextMenuSong!.song; + }, + }, + { + id: 'editor', + label: 'Open in Editor', + action: () => openInEditor(contextMenuSong!.song.id, new MouseEvent('click')), + }, + { id: 'divider-2', label: '', type: 'divider' as const }, + { + id: 'favorite', + label: song.favorite ? 'Remove from Favorites' : 'Add to Favorites', + action: () => libraryStore.toggleFavorite(contextMenuSong!.song.id), + }, + { id: 'divider-3', label: '', type: 'divider' as const }, + { + id: 'delete', + label: 'Delete Song', + variant: 'danger' as const, + action: () => { + if (confirm(`Delete "${contextMenuSong!.song.title}"?`)) { + libraryStore.deleteSong(contextMenuSong!.song.id); + } + }, + }, + ]; + } + function getBackendUrl(): string { let baseUrl = 'http://localhost:3010'; if (typeof window !== 'undefined') { @@ -165,6 +223,7 @@ handlePlaySong(song, index); } }} + oncontextmenu={(e) => handleContextMenu(e, song, index)} class="grid grid-cols-[40px_1fr_1fr_1fr_80px_40px_40px_40px] gap-4 px-4 py-3 hover:bg-background transition-colors items-center cursor-pointer group {playerStore .currentSong?.id === song.id ? 'bg-primary/5' @@ -413,3 +472,14 @@ }} /> {/if} + + { + contextMenuVisible = false; + contextMenuSong = null; + }} +/>