basic drag-and-drop, fixed end of scan behavior

This commit is contained in:
2026-02-19 15:25:49 -05:00
parent d9ebd38382
commit 193f65bd98
12 changed files with 1043 additions and 31 deletions
+153 -12
View File
@@ -1,6 +1,8 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { DragActiveDetail } from '@utils/drag-controller';
type View = 'home' | 'libraries' | 'playlists' | 'artists' | 'albums' | 'tracks';
interface NavItem {
@@ -69,14 +71,35 @@ export class AppSidebar extends LitElement {
overflow: hidden;
text-overflow: ellipsis;
}
li.drag-hover {
background-color: rgba(255, 212, 59, 0.15);
outline: 1px dashed #ffd43b;
outline-offset: -1px;
}
`;
/** Delay in ms before a drag-hover triggers navigation. */
private static readonly HOVER_NAV_DELAY = 600;
@state()
private activeView: View = 'tracks';
@state()
private isDragging = false;
/** Whether a track drag is in progress somewhere in the app. */
@state()
private trackDragActive = false;
/** The nav item ID being hovered during a drag. */
@state()
private dragHoverView: View | null = null;
private dragHoverTimer: ReturnType<
typeof setTimeout
> | null = null;
private navItems: NavItem[] = [
{ id: 'home', label: 'Home' },
{ id: 'libraries', label: 'Libraries' },
@@ -89,14 +112,35 @@ export class AppSidebar extends LitElement {
override connectedCallback() {
super.connectedCallback();
this.style.width = `${DEFAULT_WIDTH}px`;
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener(
'mousemove',
this.handleMouseMove,
);
document.addEventListener(
'mouseup',
this.handleMouseUp,
);
document.addEventListener(
'yj-drag-active',
this.onDragActive as EventListener,
);
}
override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
document.removeEventListener(
'mousemove',
this.handleMouseMove,
);
document.removeEventListener(
'mouseup',
this.handleMouseUp,
);
document.removeEventListener(
'yj-drag-active',
this.onDragActive as EventListener,
);
this.clearDragHoverTimer();
}
override render() {
@@ -106,14 +150,39 @@ export class AppSidebar extends LitElement {
@mousedown=${this.handleMouseDown}
></div>
<ul>
${this.navItems.map(item => html`
<li
class="${this.activeView === item.id ? 'active' : ''}"
@click=${() => this.navigate(item.id)}
>
<p>${item.label}</p>
</li>
`)}
${this.navItems.map((item) => {
const classes = [
this.activeView === item.id
? 'active'
: '',
this.dragHoverView === item.id
? 'drag-hover'
: '',
]
.filter(Boolean)
.join(' ');
return html`
<li
class=${classes}
@click=${() =>
this.navigate(item.id)}
@dragover=${(e: DragEvent) =>
this.onNavDragOver(
e,
item.id,
)}
@dragleave=${() =>
this.onNavDragLeave(
item.id,
)}
@drop=${(e: DragEvent) =>
this.onNavDrop(e)}
>
<p>${item.label}</p>
</li>
`;
})}
</ul>
`;
}
@@ -137,6 +206,78 @@ export class AppSidebar extends LitElement {
this.isDragging = false;
};
// =================================================================
// Drag-hover navigation
// =================================================================
/** Views that accept track drops. */
private static readonly DROP_VIEWS: Set<View> =
new Set(['playlists']);
private onDragActive = (
e: CustomEvent<DragActiveDetail>,
) => {
this.trackDragActive = e.detail.active;
if (!e.detail.active) {
this.clearDragHoverTimer();
this.dragHoverView = null;
}
};
private onNavDragOver = (
e: DragEvent,
view: View,
) => {
if (!this.trackDragActive) return;
if (!AppSidebar.DROP_VIEWS.has(view)) return;
// Prevent default so that `drop` can fire.
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
// Already hovering this item — no-op.
if (this.dragHoverView === view) return;
this.clearDragHoverTimer();
this.dragHoverView = view;
this.dragHoverTimer = setTimeout(() => {
this.dragHoverTimer = null;
if (this.dragHoverView === view) {
this.navigate(view);
}
}, AppSidebar.HOVER_NAV_DELAY);
};
private onNavDragLeave = (view: View) => {
if (this.dragHoverView !== view) return;
this.clearDragHoverTimer();
this.dragHoverView = null;
};
private onNavDrop = (e: DragEvent) => {
// The drop target is the playlist-view, not
// the sidebar itself — just prevent the
// default browser action.
e.preventDefault();
this.clearDragHoverTimer();
this.dragHoverView = null;
};
private clearDragHoverTimer() {
if (this.dragHoverTimer !== null) {
clearTimeout(this.dragHoverTimer);
this.dragHoverTimer = null;
}
}
private navigate(view: View) {
this.activeView = view;
this.dispatchEvent(new CustomEvent('navigate', {