lots of cleanups
This commit is contained in:
parent
a9f69752ed
commit
62c7fb9e19
|
|
@ -121,7 +121,7 @@
|
||||||
return { track: track || { id, title: 'Unknown track', duration: 0 }, originalIndex: i };
|
return { track: track || { id, title: 'Unknown track', duration: 0 }, originalIndex: i };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
canRemove: isMine,
|
isPlaylistOwner: isMine,
|
||||||
playlistId: selectedPlaylistId
|
playlistId: selectedPlaylistId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -300,8 +300,10 @@ button:hover { background: #333; }
|
||||||
.empty-playlist-tracks { color: #555; font-size: 0.8rem; padding: 1rem; text-align: center; font-style: italic; }
|
.empty-playlist-tracks { color: #555; font-size: 0.8rem; padding: 1rem; text-align: center; font-style: italic; }
|
||||||
.context-menu-separator { height: 1px; background: #333; margin: 0.2rem 0; }
|
.context-menu-separator { height: 1px; background: #333; margin: 0.2rem 0; }
|
||||||
.context-menu-item.has-submenu { position: relative; }
|
.context-menu-item.has-submenu { position: relative; }
|
||||||
.context-submenu { position: absolute; display: none; top: 0; left: 100%; margin-left: 2px; background: #222; border: 1px solid #444; border-radius: 4px; min-width: 120px; z-index: 1001; }
|
.context-submenu { position: absolute; display: none; top: -4px; left: calc(100% - 8px); padding-left: 8px; background: transparent; min-width: 120px; z-index: 1001; }
|
||||||
|
.context-submenu-inner { background: #222; border: 1px solid #444; border-radius: 4px; padding: 0.2rem 0; }
|
||||||
.context-menu-item.has-submenu:hover > .context-submenu { display: block; }
|
.context-menu-item.has-submenu:hover > .context-submenu { display: block; }
|
||||||
|
.context-submenu .context-menu-item { padding: 0.4rem 0.75rem; }
|
||||||
.context-menu-item.disabled { color: #666; cursor: default; }
|
.context-menu-item.disabled { color: #666; cursor: default; }
|
||||||
.context-menu-item.disabled:hover { background: transparent; }
|
.context-menu-item.disabled:hover { background: transparent; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,9 @@
|
||||||
* @param {HTMLElement} config.element - Container DOM element
|
* @param {HTMLElement} config.element - Container DOM element
|
||||||
* @param {Function} config.getTracks - Returns array of tracks to render
|
* @param {Function} config.getTracks - Returns array of tracks to render
|
||||||
* @param {Function} [config.getFilteredTracks] - Returns filtered tracks (for library search)
|
* @param {Function} [config.getFilteredTracks] - Returns filtered tracks (for library search)
|
||||||
* @param {boolean} [config.canEdit] - Whether user can modify queue
|
* @param {boolean} [config.canEditQueue] - Whether user can modify queue
|
||||||
* @param {boolean} [config.canReorder] - Whether tracks can be reordered (queue only)
|
* @param {boolean} [config.canReorder] - Whether tracks can be reordered (queue only)
|
||||||
* @param {boolean} [config.canRemove] - Whether tracks can be removed from playlist
|
* @param {boolean} [config.isPlaylistOwner] - Whether user owns the playlist (can remove/reorder)
|
||||||
* @param {string} [config.playlistId] - Playlist ID (for playlist type)
|
* @param {string} [config.playlistId] - Playlist ID (for playlist type)
|
||||||
* @param {Function} [config.onRender] - Callback after render
|
* @param {Function} [config.onRender] - Callback after render
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,15 +50,15 @@
|
||||||
getTracks,
|
getTracks,
|
||||||
getFilteredTracks,
|
getFilteredTracks,
|
||||||
canReorder = false,
|
canReorder = false,
|
||||||
canRemove = false,
|
isPlaylistOwner = false,
|
||||||
playlistId = null,
|
playlistId = null,
|
||||||
onRender
|
onRender
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
let currentTracks = [];
|
let currentTracks = [];
|
||||||
|
|
||||||
// Get canEdit dynamically (permissions may change)
|
// Get canEditQueue dynamically (permissions may change)
|
||||||
const getCanEdit = () => config.canEdit ?? M.canControl();
|
const getCanEditQueue = () => config.canEditQueue ?? M.canControl();
|
||||||
|
|
||||||
// Track if this container needs a render after drag ends
|
// Track if this container needs a render after drag ends
|
||||||
let pendingRender = false;
|
let pendingRender = false;
|
||||||
|
|
@ -70,17 +70,17 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingRender = false;
|
pendingRender = false;
|
||||||
const canEdit = getCanEdit();
|
const canEditQueue = getCanEditQueue();
|
||||||
element.innerHTML = "";
|
element.innerHTML = "";
|
||||||
|
|
||||||
// Get tracks (filtered for library, direct for queue/playlist)
|
// Get tracks (filtered for library, direct for queue/playlist)
|
||||||
currentTracks = getFilteredTracks ? getFilteredTracks() : getTracks();
|
currentTracks = getFilteredTracks ? getFilteredTracks() : getTracks();
|
||||||
|
|
||||||
// Always wire up container drop handlers first (even for empty containers)
|
// Always wire up container drop handlers first (even for empty containers)
|
||||||
if (canEdit && type === 'queue') {
|
if (canEditQueue && type === 'queue') {
|
||||||
wireQueueContainerDrop(element);
|
wireQueueContainerDrop(element);
|
||||||
}
|
}
|
||||||
if (canRemove && type === 'playlist' && playlistId) {
|
if (isPlaylistOwner && type === 'playlist' && playlistId) {
|
||||||
wirePlaylistContainerDrop(element);
|
wirePlaylistContainerDrop(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@
|
||||||
const isActive = type === 'queue' && index === M.currentIndex;
|
const isActive = type === 'queue' && index === M.currentIndex;
|
||||||
|
|
||||||
// Library tracks are always draggable, queue tracks need edit permission
|
// Library tracks are always draggable, queue tracks need edit permission
|
||||||
const isDraggable = type === 'library' || (type === 'queue' && canEdit) || (type === 'playlist' && canRemove);
|
const isDraggable = type === 'library' || (type === 'queue' && canEditQueue) || (type === 'playlist' && isPlaylistOwner);
|
||||||
|
|
||||||
const div = M.trackComponent.render(track, {
|
const div = M.trackComponent.render(track, {
|
||||||
view: type,
|
view: type,
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wire up event handlers
|
// Wire up event handlers
|
||||||
wireTrackEvents(div, track, filteredIndex, index, canEdit);
|
wireTrackEvents(div, track, filteredIndex, index, canEditQueue);
|
||||||
|
|
||||||
element.appendChild(div);
|
element.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
@ -160,7 +160,7 @@
|
||||||
const targetPos = dropTargetIndex !== null ? dropTargetIndex : currentTracks.length;
|
const targetPos = dropTargetIndex !== null ? dropTargetIndex : currentTracks.length;
|
||||||
|
|
||||||
if (dragSource === 'playlist') {
|
if (dragSource === 'playlist') {
|
||||||
reorderPlaylist(draggedTrackIds, targetPos);
|
reorderPlaylist(draggedIndices, targetPos);
|
||||||
} else if (dragSource === 'queue' || dragSource === 'library') {
|
} else if (dragSource === 'queue' || dragSource === 'library') {
|
||||||
if (currentTracks.length === 0) {
|
if (currentTracks.length === 0) {
|
||||||
addTracksToPlaylist(draggedTrackIds);
|
addTracksToPlaylist(draggedTrackIds);
|
||||||
|
|
@ -189,14 +189,13 @@
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''} to playlist`);
|
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''} to playlist`);
|
||||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to add to playlist", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addTracksToPlaylistAt(trackIds, position) {
|
async function addTracksToPlaylistAt(trackIds, position) {
|
||||||
if (!playlistId) {
|
if (!playlistId) return;
|
||||||
console.error("[addTracksToPlaylistAt] No playlistId");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current tracks and insert at position
|
// Get current tracks and insert at position
|
||||||
const current = currentTracks.map(t => (t.track || t).id);
|
const current = currentTracks.map(t => (t.track || t).id);
|
||||||
|
|
@ -212,31 +211,37 @@
|
||||||
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
|
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
|
||||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||||
} else {
|
} else {
|
||||||
console.error("[addTracksToPlaylistAt] Failed:", await res.text());
|
M.showToast("Failed to add to playlist", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reorderPlaylist(trackIds, targetIndex) {
|
async function reorderPlaylist(indices, targetIndex) {
|
||||||
if (!playlistId) return;
|
if (!playlistId) return;
|
||||||
|
|
||||||
// Get current track IDs
|
// Get current track IDs
|
||||||
const current = currentTracks.map(t => (t.track || t).id);
|
const current = currentTracks.map(t => (t.track || t).id);
|
||||||
|
|
||||||
// Remove the dragged tracks from their current positions
|
// Sort indices descending for safe removal
|
||||||
const remaining = current.filter(id => !trackIds.includes(id));
|
const sortedIndices = [...indices].sort((a, b) => b - a);
|
||||||
|
|
||||||
|
// Get track IDs being moved
|
||||||
|
const movedTrackIds = indices.map(i => current[i]);
|
||||||
|
|
||||||
// Calculate insertion position (adjusted for removed items before target)
|
// Calculate insertion position (adjusted for removed items before target)
|
||||||
let insertAt = targetIndex;
|
let insertAt = targetIndex;
|
||||||
for (const id of trackIds) {
|
for (const idx of indices) {
|
||||||
const originalPos = current.indexOf(id);
|
if (idx < targetIndex) {
|
||||||
if (originalPos < targetIndex) {
|
|
||||||
insertAt--;
|
insertAt--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove tracks at indices (from end to preserve indices)
|
||||||
|
const remaining = current.filter((_, i) => !indices.includes(i));
|
||||||
|
|
||||||
insertAt = Math.max(0, Math.min(insertAt, remaining.length));
|
insertAt = Math.max(0, Math.min(insertAt, remaining.length));
|
||||||
|
|
||||||
// Insert at new position
|
// Insert at new position
|
||||||
const newList = [...remaining.slice(0, insertAt), ...trackIds, ...remaining.slice(insertAt)];
|
const newList = [...remaining.slice(0, insertAt), ...movedTrackIds, ...remaining.slice(insertAt)];
|
||||||
|
|
||||||
const res = await fetch(`/api/playlists/${playlistId}/tracks`, {
|
const res = await fetch(`/api/playlists/${playlistId}/tracks`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
|
@ -248,17 +253,19 @@
|
||||||
selection.playlist.clear();
|
selection.playlist.clear();
|
||||||
lastSelected.playlist = null;
|
lastSelected.playlist = null;
|
||||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to reorder playlist", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireTrackEvents(div, track, filteredIndex, originalIndex, canEdit) {
|
function wireTrackEvents(div, track, filteredIndex, originalIndex, canEditQueue) {
|
||||||
const trackId = track.id || track.filename;
|
const trackId = track.id || track.filename;
|
||||||
const index = type === 'queue' ? originalIndex : filteredIndex;
|
const index = type === 'queue' ? originalIndex : filteredIndex;
|
||||||
|
|
||||||
// Click - toggle selection
|
// Click - handle selection (Ctrl = toggle, Shift = range, plain = select only this)
|
||||||
div.onclick = (e) => {
|
div.onclick = (e) => {
|
||||||
if (e.target.closest('.track-actions')) return;
|
if (e.target.closest('.track-actions')) return;
|
||||||
toggleSelection(index, trackId, e.shiftKey);
|
toggleSelection(index, trackId, e.shiftKey, e.ctrlKey || e.metaKey);
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -283,81 +290,38 @@
|
||||||
// Context menu
|
// Context menu
|
||||||
div.oncontextmenu = (e) => {
|
div.oncontextmenu = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showContextMenu(e, track, originalIndex, canEdit);
|
showContextMenu(e, track, originalIndex, canEditQueue);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag start/end handlers - library always, queue/playlist with permissions
|
// Drag start/end handlers - library always, queue/playlist with permissions
|
||||||
const canDrag = type === 'library' || (type === 'queue' && canEdit) || (type === 'playlist' && canRemove);
|
const canDrag = type === 'library' || (type === 'queue' && canEditQueue) || (type === 'playlist' && isPlaylistOwner);
|
||||||
if (canDrag) {
|
if (canDrag) {
|
||||||
div.ondragstart = (e) => handleDragStart(e, track, originalIndex, div);
|
div.ondragstart = (e) => handleDragStart(e, track, originalIndex, div);
|
||||||
div.ondragend = (e) => handleDragEnd(e, div);
|
div.ondragend = (e) => handleDragEnd(e, div);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop handlers - only queue accepts drops (from library/playlist)
|
// Drop handlers - queue and playlist accept drops
|
||||||
if (canEdit && type === 'queue') {
|
if (canEditQueue && type === 'queue') {
|
||||||
div.ondragover = (e) => handleDragOver(e, div, originalIndex);
|
div.ondragover = (e) => handleDragOver(e, div, originalIndex);
|
||||||
div.ondragleave = (e) => handleDragLeave(e, div);
|
div.ondragleave = (e) => handleDragLeave(e, div);
|
||||||
div.ondrop = (e) => handleDrop(e, div, originalIndex);
|
div.ondrop = (e) => handleDrop(e, div, originalIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For playlist tracks, allow reordering and insertion (separate from canEdit)
|
if (type === 'playlist' && playlistId && isPlaylistOwner) {
|
||||||
if (type === 'playlist' && playlistId && canRemove) {
|
div.ondragover = (e) => handleDragOver(e, div, filteredIndex);
|
||||||
div.ondragover = (e) => {
|
div.ondragleave = (e) => handleDragLeave(e, div);
|
||||||
e.preventDefault();
|
div.ondrop = (e) => handleDrop(e, div, filteredIndex);
|
||||||
e.dataTransfer.dropEffect = dragSource === 'playlist' ? "move" : "copy";
|
|
||||||
|
|
||||||
const rect = div.getBoundingClientRect();
|
|
||||||
const midY = rect.top + rect.height / 2;
|
|
||||||
const isAbove = e.clientY < midY;
|
|
||||||
|
|
||||||
// Clear other indicators
|
|
||||||
element.querySelectorAll(".drop-above, .drop-below").forEach(el => {
|
|
||||||
el.classList.remove("drop-above", "drop-below");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't show indicator on dragged items (for reorder)
|
|
||||||
if (dragSource === 'playlist' && draggedTrackIds.includes(trackId)) return;
|
|
||||||
|
|
||||||
div.classList.add(isAbove ? "drop-above" : "drop-below");
|
|
||||||
dropTargetIndex = isAbove ? filteredIndex : filteredIndex + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
div.ondragleave = () => {
|
|
||||||
div.classList.remove("drop-above", "drop-below");
|
|
||||||
};
|
|
||||||
|
|
||||||
div.ondrop = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
div.classList.remove("drop-above", "drop-below");
|
|
||||||
element.classList.remove("drop-target");
|
|
||||||
|
|
||||||
if (draggedTrackIds.length > 0 && dropTargetIndex !== null) {
|
|
||||||
if (dragSource === 'playlist') {
|
|
||||||
// Reorder within playlist
|
|
||||||
reorderPlaylist(draggedTrackIds, dropTargetIndex);
|
|
||||||
} else if (dragSource === 'queue' || dragSource === 'library') {
|
|
||||||
// Insert at position
|
|
||||||
addTracksToPlaylistAt(draggedTrackIds, dropTargetIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draggedTrackIds = [];
|
|
||||||
draggedIndices = [];
|
|
||||||
dragSource = null;
|
|
||||||
dropTargetIndex = null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelection(index, trackId, shiftKey) {
|
function toggleSelection(index, trackId, shiftKey, ctrlKey) {
|
||||||
// Queue and playlist use indices, library uses trackIds
|
// Queue and playlist use indices, library uses trackIds
|
||||||
const key = type === 'library' ? trackId : index;
|
const key = type === 'library' ? trackId : index;
|
||||||
const sel = selection[type];
|
const sel = selection[type];
|
||||||
|
const currentIdx = type === 'library' ? getFilteredIndex(trackId) : index;
|
||||||
|
|
||||||
if (shiftKey && lastSelected[type] !== null) {
|
if (shiftKey && lastSelected[type] !== null) {
|
||||||
// Range select
|
// Shift+click: Range select (add to existing selection)
|
||||||
const currentIdx = type === 'library' ? getFilteredIndex(trackId) : index;
|
|
||||||
const start = Math.min(lastSelected[type], currentIdx);
|
const start = Math.min(lastSelected[type], currentIdx);
|
||||||
const end = Math.max(lastSelected[type], currentIdx);
|
const end = Math.max(lastSelected[type], currentIdx);
|
||||||
|
|
||||||
|
|
@ -373,13 +337,19 @@
|
||||||
sel.add(i);
|
sel.add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (ctrlKey) {
|
||||||
|
// Ctrl+click: Toggle single item
|
||||||
if (sel.has(key)) {
|
if (sel.has(key)) {
|
||||||
sel.delete(key);
|
sel.delete(key);
|
||||||
} else {
|
} else {
|
||||||
sel.add(key);
|
sel.add(key);
|
||||||
}
|
}
|
||||||
lastSelected[type] = type === 'library' ? getFilteredIndex(trackId) : index;
|
lastSelected[type] = currentIdx;
|
||||||
|
} else {
|
||||||
|
// Plain click: Select only this item (clear others)
|
||||||
|
sel.clear();
|
||||||
|
sel.add(key);
|
||||||
|
lastSelected[type] = currentIdx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +405,13 @@
|
||||||
|
|
||||||
function handleDragOver(e, div, index) {
|
function handleDragOver(e, div, index) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = dragSource === 'queue' ? "move" : "copy";
|
|
||||||
|
// Set drop effect based on source
|
||||||
|
if (type === 'queue') {
|
||||||
|
e.dataTransfer.dropEffect = dragSource === 'queue' ? "move" : "copy";
|
||||||
|
} else if (type === 'playlist') {
|
||||||
|
e.dataTransfer.dropEffect = dragSource === 'playlist' ? "move" : "copy";
|
||||||
|
}
|
||||||
|
|
||||||
const rect = div.getBoundingClientRect();
|
const rect = div.getBoundingClientRect();
|
||||||
const midY = rect.top + rect.height / 2;
|
const midY = rect.top + rect.height / 2;
|
||||||
|
|
@ -447,7 +423,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Don't show indicator on dragged items
|
// Don't show indicator on dragged items
|
||||||
if (dragSource === 'queue' && draggedIndices.includes(index)) return;
|
if (dragSource === type && draggedIndices.includes(index)) return;
|
||||||
|
|
||||||
div.classList.add(isAbove ? "drop-above" : "drop-below");
|
div.classList.add(isAbove ? "drop-above" : "drop-below");
|
||||||
dropTargetIndex = isAbove ? index : index + 1;
|
dropTargetIndex = isAbove ? index : index + 1;
|
||||||
|
|
@ -459,20 +435,32 @@
|
||||||
|
|
||||||
function handleDrop(e, div, index) {
|
function handleDrop(e, div, index) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
div.classList.remove("drop-above", "drop-below");
|
div.classList.remove("drop-above", "drop-below");
|
||||||
|
element.classList.remove("drop-target");
|
||||||
|
|
||||||
if (dropTargetIndex === null) return;
|
if (dropTargetIndex === null) return;
|
||||||
|
|
||||||
if (dragSource === 'queue' && draggedIndices.length > 0) {
|
if (type === 'queue') {
|
||||||
// Reorder within queue
|
if (dragSource === 'queue' && draggedIndices.length > 0) {
|
||||||
const minDragged = Math.min(...draggedIndices);
|
// Reorder within queue
|
||||||
const maxDragged = Math.max(...draggedIndices);
|
const minDragged = Math.min(...draggedIndices);
|
||||||
if (dropTargetIndex < minDragged || dropTargetIndex > maxDragged + 1) {
|
const maxDragged = Math.max(...draggedIndices);
|
||||||
reorderQueue(draggedIndices, dropTargetIndex);
|
if (dropTargetIndex < minDragged || dropTargetIndex > maxDragged + 1) {
|
||||||
|
reorderQueue(draggedIndices, dropTargetIndex);
|
||||||
|
}
|
||||||
|
} else if ((dragSource === 'library' || dragSource === 'playlist') && draggedTrackIds.length > 0) {
|
||||||
|
// Insert tracks from library or playlist
|
||||||
|
insertTracksAtPosition(draggedTrackIds, dropTargetIndex);
|
||||||
|
}
|
||||||
|
} else if (type === 'playlist') {
|
||||||
|
if (dragSource === 'playlist' && draggedIndices.length > 0) {
|
||||||
|
// Reorder within playlist
|
||||||
|
reorderPlaylist(draggedIndices, dropTargetIndex);
|
||||||
|
} else if ((dragSource === 'queue' || dragSource === 'library') && draggedTrackIds.length > 0) {
|
||||||
|
// Insert at position
|
||||||
|
addTracksToPlaylistAt(draggedTrackIds, dropTargetIndex);
|
||||||
}
|
}
|
||||||
} else if ((dragSource === 'library' || dragSource === 'playlist') && draggedTrackIds.length > 0) {
|
|
||||||
// Insert tracks from library or playlist
|
|
||||||
insertTracksAtPosition(draggedTrackIds, dropTargetIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draggedIndices = [];
|
draggedIndices = [];
|
||||||
|
|
@ -524,6 +512,8 @@
|
||||||
else if (res.ok) {
|
else if (res.ok) {
|
||||||
selection.queue.clear();
|
selection.queue.clear();
|
||||||
lastSelected.queue = null;
|
lastSelected.queue = null;
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to reorder queue", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,6 +530,8 @@
|
||||||
else if (res.ok) {
|
else if (res.ok) {
|
||||||
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
|
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to add tracks", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,13 +580,16 @@
|
||||||
M.localTimestamp = 0;
|
M.localTimestamp = 0;
|
||||||
M.audio.play();
|
M.audio.play();
|
||||||
|
|
||||||
if (M.synced) {
|
// Desync and disable auto-resync
|
||||||
|
if (M.synced || M.wantSync) {
|
||||||
M.synced = false;
|
M.synced = false;
|
||||||
|
M.wantSync = false;
|
||||||
M.showToast("Previewing track (desynced)");
|
M.showToast("Previewing track (desynced)");
|
||||||
|
M.updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showContextMenu(e, track, index, canEdit) {
|
function showContextMenu(e, track, index, canEditQueue) {
|
||||||
const trackId = track.id || track.filename;
|
const trackId = track.id || track.filename;
|
||||||
const title = track.title?.trim() || (track.filename || trackId || "Unknown").replace(/\.[^.]+$/, "");
|
const title = track.title?.trim() || (track.filename || trackId || "Unknown").replace(/\.[^.]+$/, "");
|
||||||
|
|
||||||
|
|
@ -618,16 +613,16 @@
|
||||||
|
|
||||||
const menuItems = [];
|
const menuItems = [];
|
||||||
|
|
||||||
// Play (queue only, single track)
|
// Play (queue only, single track or single selection)
|
||||||
if (type === 'queue' && !hasSelection) {
|
if (type === 'queue' && selectedCount === 1) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: "▶ Play",
|
label: "▶ Play",
|
||||||
action: () => playTrack(track, index)
|
action: () => playTrack(track, index)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview (all views, single track)
|
// Preview (all views, single track or single selection)
|
||||||
if (!hasSelection) {
|
if (selectedCount === 1) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: "⏵ Preview",
|
label: "⏵ Preview",
|
||||||
action: () => previewTrack(track)
|
action: () => previewTrack(track)
|
||||||
|
|
@ -635,7 +630,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue actions
|
// Queue actions
|
||||||
if (type === 'queue' && canEdit) {
|
if (type === 'queue' && canEditQueue) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: selectedCount > 1 ? `+ Add ${selectedCount} again` : "+ Add again",
|
label: selectedCount > 1 ? `+ Add ${selectedCount} again` : "+ Add again",
|
||||||
action: () => addToQueue(idsForAction)
|
action: () => addToQueue(idsForAction)
|
||||||
|
|
@ -651,8 +646,8 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Library actions
|
// Library actions - can add to queue if user has queue edit permission
|
||||||
if (type === 'library' && canEdit) {
|
if (type === 'library' && canEditQueue) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: selectedCount > 1 ? `+ Add ${selectedCount} to queue` : "+ Add to queue",
|
label: selectedCount > 1 ? `+ Add ${selectedCount} to queue` : "+ Add to queue",
|
||||||
action: () => addToQueue(idsForAction)
|
action: () => addToQueue(idsForAction)
|
||||||
|
|
@ -665,7 +660,8 @@
|
||||||
|
|
||||||
// Playlist actions
|
// Playlist actions
|
||||||
if (type === 'playlist') {
|
if (type === 'playlist') {
|
||||||
if (canEdit) {
|
// Can add to queue if user has queue edit permission
|
||||||
|
if (canEditQueue) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: selectedCount > 1 ? `+ Add ${selectedCount} to queue` : "+ Add to queue",
|
label: selectedCount > 1 ? `+ Add ${selectedCount} to queue` : "+ Add to queue",
|
||||||
action: () => addToQueue(idsForAction)
|
action: () => addToQueue(idsForAction)
|
||||||
|
|
@ -675,7 +671,8 @@
|
||||||
action: () => addToQueue(idsForAction, true)
|
action: () => addToQueue(idsForAction, true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (canRemove && playlistId) {
|
// Can remove if user owns the playlist
|
||||||
|
if (isPlaylistOwner && playlistId) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: selectedCount > 1 ? `🗑️ Remove ${selectedCount}` : "🗑️ Remove",
|
label: selectedCount > 1 ? `🗑️ Remove ${selectedCount}` : "🗑️ Remove",
|
||||||
danger: true,
|
danger: true,
|
||||||
|
|
@ -753,6 +750,8 @@
|
||||||
M.showToast(playNext ? "Playing next" : "Added to queue");
|
M.showToast(playNext ? "Playing next" : "Added to queue");
|
||||||
clearSelection();
|
clearSelection();
|
||||||
render();
|
render();
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to add to queue", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -769,6 +768,8 @@
|
||||||
else if (res.ok) {
|
else if (res.ok) {
|
||||||
M.showToast("Removed");
|
M.showToast("Removed");
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to remove from queue", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -785,6 +786,8 @@
|
||||||
M.showToast("Removed from playlist");
|
M.showToast("Removed from playlist");
|
||||||
clearSelection();
|
clearSelection();
|
||||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||||
|
} else {
|
||||||
|
M.showToast("Failed to remove from playlist", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue