From adf53cadaa7c88ceeeaafbdf8cbc0e972ca7a87b Mon Sep 17 00:00:00 2001 From: peterino2 Date: Thu, 5 Feb 2026 23:50:01 -0800 Subject: [PATCH] added ability to right click -> queue next --- AGENTS.md | 61 +++++++++++++++++++++++++++++++++++++++++ channel.ts | 14 ++++++++++ public/queue.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++ routes/channels.ts | 8 ++++-- 4 files changed, 148 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a80943c..611999b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -216,3 +216,64 @@ Default port 3001 (override with `PORT` env var). Track durations read from file - **Username**: test - **Password**: testuser + +## UI/UX Design Principles + +### No Alerts/Prompts +Never use `alert()` or `prompt()`. Use inline editing, toasts, or modals instead. +- Rename actions → inline text input that appears in place +- Confirmations → toast notifications +- Errors → toast with error styling + +### Inline Editing +Editable fields should transform in-place: +- Click edit icon → field becomes input +- Enter to save, Escape to cancel +- Blur (click away) saves changes + +### Drag and Drop +Support drag-and-drop for reordering and moving items: +- Tracks in queue can be reordered via drag +- Tracks from library can be dragged to queue +- Visual feedback with drop indicators + +### Context Menus +Right-click context menus for all actionable items: +- Tracks (both library and queue) have context menus +- Context menu options are consistent across views +- Disabled options are hidden, not grayed out + +### Track Behavior Consistency +Tracks in Library and Queue should behave identically where applicable: + +| Feature | Library | Queue | +|---------|---------|-------| +| Click to select | ✓ | ✓ | +| Shift+click range select | ✓ | ✓ | +| Right-click context menu | ✓ | ✓ | +| Drag to reorder | ✗ | ✓ | +| Cache status indicator | ✓ | ✓ | + +### Context Menu Options by View + +**Library tracks:** +- ▶ Play track (local mode, single track) +- ⏭ Play next (insert after current) +- + Add to queue (append to end) + +**Queue tracks:** +- ▶ Play track (jump to track) +- ⏭ Play next (re-add after current) +- + Add again (duplicate at end) +- ✕ Remove from queue + +### Mobile/Touch Support +- Larger touch targets (min 44px) +- No hover-dependent features (always show action buttons) +- Tab-based navigation for panels +- Sticky headers for scrollable lists + +### Responsive Layout +- Desktop: side-by-side panels (Channels | Library | Queue) +- Mobile (<768px): tab-switched single panel view +- Player bar adapts: horizontal on desktop, stacked on mobile diff --git a/channel.ts b/channel.ts index 8076cf7..78d191a 100644 --- a/channel.ts +++ b/channel.ts @@ -247,6 +247,20 @@ export class Channel { this.broadcast(); } + insertTracksAt(tracks: Track[], position: number) { + if (tracks.length === 0) return; + // Clamp position to valid range + const insertPos = Math.max(0, Math.min(position, this.queue.length)); + this.queue.splice(insertPos, 0, ...tracks); + // Adjust currentIndex if insertion is at or before current + if (insertPos <= this.currentIndex) { + this.currentIndex += tracks.length; + } + this.queueDirty = true; + this.persistQueue(); + this.broadcast(); + } + removeTracksByIndex(indices: number[]) { if (indices.length === 0) return; diff --git a/public/queue.js b/public/queue.js index e9da53b..06d3372 100644 --- a/public/queue.js +++ b/public/queue.js @@ -593,6 +593,50 @@ // Remove track(s) option (if user can edit) if (canEdit) { + // Get track IDs for the selected indices + const idsToAdd = hasSelection + ? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean) + : [trackId]; + + // Add again option - duplicate tracks at end of queue + const addAgainLabel = selectedCount > 1 ? `+ Add ${selectedCount} again` : "+ Add again"; + menuItems.push({ + label: addAgainLabel, + action: async () => { + if (!M.currentChannelId) return; + const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ add: idsToAdd }) + }); + if (res.status === 403) M.flashPermissionDenied(); + else if (res.ok) { + M.showToast(selectedCount > 1 ? `Added ${selectedCount} tracks again` : "Track added again"); + M.clearSelections(); + } + } + }); + + // Play next option - insert after current track + const playNextLabel = selectedCount > 1 ? `⏭ Play ${selectedCount} next` : "⏭ Play next"; + menuItems.push({ + label: playNextLabel, + action: async () => { + if (!M.currentChannelId) return; + const insertAt = (M.currentIndex ?? 0) + 1; + const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ add: idsToAdd, insertAt }) + }); + if (res.status === 403) M.flashPermissionDenied(); + else if (res.ok) { + M.showToast(selectedCount > 1 ? `${selectedCount} tracks playing next` : "Track playing next"); + M.clearSelections(); + } + } + }); + const label = selectedCount > 1 ? `✕ Remove ${selectedCount} tracks` : "✕ Remove track"; menuItems.push({ label, @@ -820,6 +864,29 @@ } } }); + + // Play next option - insert after current track + const playNextLabel = selectedCount > 1 ? `⏭ Play ${selectedCount} next` : "⏭ Play next"; + menuItems.push({ + label: playNextLabel, + action: async () => { + if (!M.currentChannelId) { + M.showToast("No channel selected"); + return; + } + const insertAt = (M.currentIndex ?? 0) + 1; + const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ add: idsToAdd, insertAt }) + }); + if (res.status === 403) M.flashPermissionDenied(); + else if (res.ok) { + M.showToast(selectedCount > 1 ? `${selectedCount} tracks playing next` : "Track playing next"); + M.clearSelections(); + } + } + }); } // Preload track(s) option - only show if not in stream-only mode diff --git a/routes/channels.ts b/routes/channels.ts index 29c29d2..71ec552 100644 --- a/routes/channels.ts +++ b/routes/channels.ts @@ -221,7 +221,7 @@ export async function handleModifyQueue(req: Request, server: any, channelId: st try { const body = await req.json(); - const { add, remove, set } = body; + const { add, remove, set, insertAt } = body; if (Array.isArray(set)) { const tracks = buildTracksFromIds(set, state.library); @@ -236,7 +236,11 @@ export async function handleModifyQueue(req: Request, server: any, channelId: st if (Array.isArray(add) && add.length > 0) { const tracks = buildTracksFromIds(add, state.library); - channel.addTracks(tracks); + if (typeof insertAt === "number") { + channel.insertTracksAt(tracks, insertAt); + } else { + channel.addTracks(tracks); + } } return Response.json({ success: true, queueLength: channel.queue.length });