From a139ebe894e8e97e1891bb8a685d66f69d60efc0 Mon Sep 17 00:00:00 2001 From: claw1200 Date: Wed, 28 Oct 2026 17:36:49 +0000 Subject: [PATCH 1/5] Enable playlist tree drag and drop --- app/src/main/audio/native.ts | 1 + app/src/main/library.ts | 1 + app/src/preload/engine.ts | 1 + app/src/renderer/src/lib/api.ts | 2 + .../src/lib/components/Sidebar.svelte | 84 ++++++++++++++++++- app/src/renderer/src/lib/library.svelte.ts | 8 ++ es-rekordbox-sdk | 2 +- native/crates/es-engine/src/library.rs | 21 +++++ native/crates/es-node/src/library.rs | 10 +++ 9 files changed, 127 insertions(+), 3 deletions(-) diff --git a/app/src/main/audio/native.ts b/app/src/main/audio/native.ts index 63a327e..b6607b2 100644 --- a/app/src/main/audio/native.ts +++ b/app/src/main/audio/native.ts @@ -136,6 +136,7 @@ export interface NativeLibrary { playlistAdd(id: string, trackIds: string[]): Promise playlistRemove(id: string, trackIds: string[]): Promise playlistMove(id: string, trackIds: string[], index: number): Promise + playlistTreeMove(id: string, parent: string, index: number): Promise playlistCreate(name: string, parentId: string | null, folder: boolean, trackIds: string[]): Promise playlistRename(id: string, name: string): Promise playlistDelete(id: string): Promise diff --git a/app/src/main/library.ts b/app/src/main/library.ts index 1b9af1e..46df68f 100644 --- a/app/src/main/library.ts +++ b/app/src/main/library.ts @@ -73,6 +73,7 @@ const WRITES: CommandHandlers = { 'playlist.add': (lib, p) => lib.playlistAdd(text(p?.id), list(p?.trackIds)), 'playlist.remove': (lib, p) => lib.playlistRemove(text(p?.id), list(p?.trackIds)), 'playlist.move': (lib, p) => lib.playlistMove(text(p?.id), list(p?.trackIds), Number(p?.index ?? 0)), + 'playlist.treeMove': (lib, p) => lib.playlistTreeMove(text(p?.id), text(p?.parent), Number(p?.index ?? 0)), 'playlist.create': (lib, p) => lib.playlistCreate( text(p?.name), diff --git a/app/src/preload/engine.ts b/app/src/preload/engine.ts index aa068ab..c6e4511 100644 --- a/app/src/preload/engine.ts +++ b/app/src/preload/engine.ts @@ -109,6 +109,7 @@ export interface EngineCommandParams { 'playlist.add': { id: string; trackIds: string[] } 'playlist.remove': { id: string; trackIds: string[] } 'playlist.move': { id: string; trackIds: string[]; index: number } + 'playlist.treeMove': { id: string; parent: string; index: number } 'playlist.create': { name: string; parentId?: string | null; folder: boolean; trackIds: string[] } 'playlist.rename': { id: string; name: string } 'playlist.delete': { id: string } diff --git a/app/src/renderer/src/lib/api.ts b/app/src/renderer/src/lib/api.ts index 5c73e30..7e44f3f 100644 --- a/app/src/renderer/src/lib/api.ts +++ b/app/src/renderer/src/lib/api.ts @@ -67,6 +67,8 @@ export const api = { removeFromPlaylist: (id: string, trackIds: string[]) => call('playlist.remove', { id, trackIds }), moveInPlaylist: (id: string, trackIds: string[], index: number) => call('playlist.move', { id, trackIds, index }), + movePlaylistTree: (id: string, parent: string, index: number) => + call('playlist.treeMove', { id, parent, index }), createPlaylist: (name: string, parentId: string, folder: boolean, trackIds: string[]) => call<{ id: string; playlists: TreeItem[] }>('playlist.create', { name, parentId, folder, trackIds }), renamePlaylist: (id: string, name: string) => call('playlist.rename', { id, name }), diff --git a/app/src/renderer/src/lib/components/Sidebar.svelte b/app/src/renderer/src/lib/components/Sidebar.svelte index 01610b0..0863c24 100644 --- a/app/src/renderer/src/lib/components/Sidebar.svelte +++ b/app/src/renderer/src/lib/components/Sidebar.svelte @@ -26,6 +26,7 @@ } from '@lucide/svelte' import { getApp } from '$lib/context' import { droppedPaths, isFileDrag } from '$lib/library/drop' + import { PLAYLIST_TREE_DRAG_TYPE } from '$lib/library.svelte' import { buildTree, type TreeNode } from '$lib/library/tree' import { menuAt, tidy, type MenuEntry, type MenuRequest } from '$lib/menu' import { SEPARATOR, unlockEntry } from '$lib/trackMenu' @@ -75,6 +76,8 @@ const expanded = new SvelteSet() const collapsedSections = new SvelteSet(['histories']) + let treeDragId = $state(null) + let treeDrop = $state<{ key: string; placement: 'before' | 'after' | 'inside' } | null>(null) type SourceType = 'playlist' | 'history' | 'tag' @@ -106,6 +109,51 @@ return (type === 'playlist' && node.kind === 'playlist') || (type === 'tag' && node.kind === 'tag') } + function isPlaylistTreeDrag(e: DragEvent): boolean { + return !!e.dataTransfer?.types.includes(PLAYLIST_TREE_DRAG_TYPE) + } + + function playlistDragStart(e: DragEvent, type: SourceType, node: TreeNode): void { + if (type !== 'playlist') return + if (!library.writable) { + e.preventDefault() + return + } + treeDragId = node.id + const transfer = e.dataTransfer + if (!transfer) return + transfer.effectAllowed = 'move' + transfer.setData(PLAYLIST_TREE_DRAG_TYPE, node.id) + } + + function playlistDragEnd(): void { + treeDragId = null + treeDrop = null + } + + function playlistMoveTarget( + e: DragEvent, + node: TreeNode + ): { parent: string; index: number; placement: 'before' | 'after' | 'inside' } | null { + const draggedId = treeDragId ?? e.dataTransfer?.getData(PLAYLIST_TREE_DRAG_TYPE) + if (!draggedId || draggedId === node.id) return null + const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect() + const position = (e.clientY - bounds.top) / bounds.height + const placement = node.kind === 'folder' && position >= 0.25 && position <= 0.75 + ? 'inside' + : position < 0.5 + ? 'before' + : 'after' + const parent = placement === 'inside' ? node.id : node.parentId + const siblings = library.playlists + .filter((item) => item.parentId === parent && item.id !== draggedId) + .sort((a, b) => a.seq - b.seq) + const targetIndex = siblings.findIndex((item) => item.id === node.id) + const index = placement === 'inside' ? siblings.length : targetIndex + Number(placement === 'after') + if (targetIndex < 0 && placement !== 'inside') return null + return { parent, index, placement } + } + function expandSoon(key: string, expand: () => void): void { if (expandKey === key) return clearTimeout(expandTimer) @@ -123,6 +171,15 @@ } function dragOver(e: DragEvent, type: SourceType, node: TreeNode): void { + if (isPlaylistTreeDrag(e)) { + if (type !== 'playlist' || !library.writable) return + const target = playlistMoveTarget(e, node) + if (!target) return + e.preventDefault() + if (e.dataTransfer) e.dataTransfer.dropEffect = 'move' + treeDrop = { key: `${type}:${node.id}`, placement: target.placement } + return + } const files = !library.dragging && isFileDrag(e) if (!library.dragging && !files) return const key = `${type}:${node.id}` @@ -185,11 +242,21 @@ const into = e.relatedTarget if (into instanceof Node && (e.currentTarget as HTMLElement).contains(into)) return if (dropKey === key) dropKey = null + if (treeDrop?.key === key) treeDrop = null cancelExpand(key) } function drop(e: DragEvent, type: SourceType, node: TreeNode): void { dropKey = null + if (isPlaylistTreeDrag(e)) { + const draggedId = treeDragId ?? e.dataTransfer?.getData(PLAYLIST_TREE_DRAG_TYPE) + const target = playlistMoveTarget(e, node) + if (!draggedId || !target || type !== 'playlist' || !library.writable) return + e.preventDefault() + treeDrop = null + void library.movePlaylistTree(draggedId, target.parent, target.index) + return + } if (!library.dragging && isFileDrag(e)) { if (type !== 'playlist' || node.kind !== 'playlist' || !library.writable) return e.preventDefault() @@ -425,20 +492,33 @@ {:else}