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 };
|
||||
});
|
||||
},
|
||||
canRemove: isMine,
|
||||
isPlaylistOwner: isMine,
|
||||
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; }
|
||||
.context-menu-separator { height: 1px; background: #333; margin: 0.2rem 0; }
|
||||
.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-submenu .context-menu-item { padding: 0.4rem 0.75rem; }
|
||||
.context-menu-item.disabled { color: #666; cursor: default; }
|
||||
.context-menu-item.disabled:hover { background: transparent; }
|
||||
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@
|
|||
* @param {HTMLElement} config.element - Container DOM element
|
||||
* @param {Function} config.getTracks - Returns array of tracks to render
|
||||
* @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.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 {Function} [config.onRender] - Callback after render
|
||||
*/
|
||||
|
|
@ -50,15 +50,15 @@
|
|||
getTracks,
|
||||
getFilteredTracks,
|
||||
canReorder = false,
|
||||
canRemove = false,
|
||||
isPlaylistOwner = false,
|
||||
playlistId = null,
|
||||
onRender
|
||||
} = config;
|
||||
|
||||
let currentTracks = [];
|
||||
|
||||
// Get canEdit dynamically (permissions may change)
|
||||
const getCanEdit = () => config.canEdit ?? M.canControl();
|
||||
// Get canEditQueue dynamically (permissions may change)
|
||||
const getCanEditQueue = () => config.canEditQueue ?? M.canControl();
|
||||
|
||||
// Track if this container needs a render after drag ends
|
||||
let pendingRender = false;
|
||||
|
|
@ -70,17 +70,17 @@
|
|||
return;
|
||||
}
|
||||
pendingRender = false;
|
||||
const canEdit = getCanEdit();
|
||||
const canEditQueue = getCanEditQueue();
|
||||
element.innerHTML = "";
|
||||
|
||||
// Get tracks (filtered for library, direct for queue/playlist)
|
||||
currentTracks = getFilteredTracks ? getFilteredTracks() : getTracks();
|
||||
|
||||
// Always wire up container drop handlers first (even for empty containers)
|
||||
if (canEdit && type === 'queue') {
|
||||
if (canEditQueue && type === 'queue') {
|
||||
wireQueueContainerDrop(element);
|
||||
}
|
||||
if (canRemove && type === 'playlist' && playlistId) {
|
||||
if (isPlaylistOwner && type === 'playlist' && playlistId) {
|
||||
wirePlaylistContainerDrop(element);
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
const isActive = type === 'queue' && index === M.currentIndex;
|
||||
|
||||
// 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, {
|
||||
view: type,
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
});
|
||||
|
||||
// Wire up event handlers
|
||||
wireTrackEvents(div, track, filteredIndex, index, canEdit);
|
||||
wireTrackEvents(div, track, filteredIndex, index, canEditQueue);
|
||||
|
||||
element.appendChild(div);
|
||||
});
|
||||
|
|
@ -160,7 +160,7 @@
|
|||
const targetPos = dropTargetIndex !== null ? dropTargetIndex : currentTracks.length;
|
||||
|
||||
if (dragSource === 'playlist') {
|
||||
reorderPlaylist(draggedTrackIds, targetPos);
|
||||
reorderPlaylist(draggedIndices, targetPos);
|
||||
} else if (dragSource === 'queue' || dragSource === 'library') {
|
||||
if (currentTracks.length === 0) {
|
||||
addTracksToPlaylist(draggedTrackIds);
|
||||
|
|
@ -189,14 +189,13 @@
|
|||
if (res.ok) {
|
||||
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''} to playlist`);
|
||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||
} else {
|
||||
M.showToast("Failed to add to playlist", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function addTracksToPlaylistAt(trackIds, position) {
|
||||
if (!playlistId) {
|
||||
console.error("[addTracksToPlaylistAt] No playlistId");
|
||||
return;
|
||||
}
|
||||
if (!playlistId) return;
|
||||
|
||||
// Get current tracks and insert at position
|
||||
const current = currentTracks.map(t => (t.track || t).id);
|
||||
|
|
@ -212,31 +211,37 @@
|
|||
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
|
||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||
} 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;
|
||||
|
||||
// Get current track IDs
|
||||
const current = currentTracks.map(t => (t.track || t).id);
|
||||
|
||||
// Remove the dragged tracks from their current positions
|
||||
const remaining = current.filter(id => !trackIds.includes(id));
|
||||
// Sort indices descending for safe removal
|
||||
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)
|
||||
let insertAt = targetIndex;
|
||||
for (const id of trackIds) {
|
||||
const originalPos = current.indexOf(id);
|
||||
if (originalPos < targetIndex) {
|
||||
for (const idx of indices) {
|
||||
if (idx < targetIndex) {
|
||||
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));
|
||||
|
||||
// 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`, {
|
||||
method: "PATCH",
|
||||
|
|
@ -248,17 +253,19 @@
|
|||
selection.playlist.clear();
|
||||
lastSelected.playlist = null;
|
||||
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 index = type === 'queue' ? originalIndex : filteredIndex;
|
||||
|
||||
// Click - toggle selection
|
||||
// Click - handle selection (Ctrl = toggle, Shift = range, plain = select only this)
|
||||
div.onclick = (e) => {
|
||||
if (e.target.closest('.track-actions')) return;
|
||||
toggleSelection(index, trackId, e.shiftKey);
|
||||
toggleSelection(index, trackId, e.shiftKey, e.ctrlKey || e.metaKey);
|
||||
render();
|
||||
};
|
||||
|
||||
|
|
@ -283,81 +290,38 @@
|
|||
// Context menu
|
||||
div.oncontextmenu = (e) => {
|
||||
e.preventDefault();
|
||||
showContextMenu(e, track, originalIndex, canEdit);
|
||||
showContextMenu(e, track, originalIndex, canEditQueue);
|
||||
};
|
||||
|
||||
// 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) {
|
||||
div.ondragstart = (e) => handleDragStart(e, track, originalIndex, div);
|
||||
div.ondragend = (e) => handleDragEnd(e, div);
|
||||
}
|
||||
|
||||
// Drop handlers - only queue accepts drops (from library/playlist)
|
||||
if (canEdit && type === 'queue') {
|
||||
// Drop handlers - queue and playlist accept drops
|
||||
if (canEditQueue && type === 'queue') {
|
||||
div.ondragover = (e) => handleDragOver(e, div, originalIndex);
|
||||
div.ondragleave = (e) => handleDragLeave(e, div);
|
||||
div.ondrop = (e) => handleDrop(e, div, originalIndex);
|
||||
}
|
||||
|
||||
// For playlist tracks, allow reordering and insertion (separate from canEdit)
|
||||
if (type === 'playlist' && playlistId && canRemove) {
|
||||
div.ondragover = (e) => {
|
||||
e.preventDefault();
|
||||
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;
|
||||
};
|
||||
if (type === 'playlist' && playlistId && isPlaylistOwner) {
|
||||
div.ondragover = (e) => handleDragOver(e, div, filteredIndex);
|
||||
div.ondragleave = (e) => handleDragLeave(e, div);
|
||||
div.ondrop = (e) => handleDrop(e, div, filteredIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelection(index, trackId, shiftKey) {
|
||||
function toggleSelection(index, trackId, shiftKey, ctrlKey) {
|
||||
// Queue and playlist use indices, library uses trackIds
|
||||
const key = type === 'library' ? trackId : index;
|
||||
const sel = selection[type];
|
||||
const currentIdx = type === 'library' ? getFilteredIndex(trackId) : index;
|
||||
|
||||
if (shiftKey && lastSelected[type] !== null) {
|
||||
// Range select
|
||||
const currentIdx = type === 'library' ? getFilteredIndex(trackId) : index;
|
||||
// Shift+click: Range select (add to existing selection)
|
||||
const start = Math.min(lastSelected[type], currentIdx);
|
||||
const end = Math.max(lastSelected[type], currentIdx);
|
||||
|
||||
|
|
@ -373,13 +337,19 @@
|
|||
sel.add(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (ctrlKey) {
|
||||
// Ctrl+click: Toggle single item
|
||||
if (sel.has(key)) {
|
||||
sel.delete(key);
|
||||
} else {
|
||||
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) {
|
||||
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 midY = rect.top + rect.height / 2;
|
||||
|
|
@ -447,7 +423,7 @@
|
|||
});
|
||||
|
||||
// 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");
|
||||
dropTargetIndex = isAbove ? index : index + 1;
|
||||
|
|
@ -459,20 +435,32 @@
|
|||
|
||||
function handleDrop(e, div, index) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
div.classList.remove("drop-above", "drop-below");
|
||||
element.classList.remove("drop-target");
|
||||
|
||||
if (dropTargetIndex === null) return;
|
||||
|
||||
if (dragSource === 'queue' && draggedIndices.length > 0) {
|
||||
// Reorder within queue
|
||||
const minDragged = Math.min(...draggedIndices);
|
||||
const maxDragged = Math.max(...draggedIndices);
|
||||
if (dropTargetIndex < minDragged || dropTargetIndex > maxDragged + 1) {
|
||||
reorderQueue(draggedIndices, dropTargetIndex);
|
||||
if (type === 'queue') {
|
||||
if (dragSource === 'queue' && draggedIndices.length > 0) {
|
||||
// Reorder within queue
|
||||
const minDragged = Math.min(...draggedIndices);
|
||||
const maxDragged = Math.max(...draggedIndices);
|
||||
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 = [];
|
||||
|
|
@ -524,6 +512,8 @@
|
|||
else if (res.ok) {
|
||||
selection.queue.clear();
|
||||
lastSelected.queue = null;
|
||||
} else {
|
||||
M.showToast("Failed to reorder queue", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,6 +530,8 @@
|
|||
else if (res.ok) {
|
||||
M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
|
||||
clearSelection();
|
||||
} else {
|
||||
M.showToast("Failed to add tracks", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -588,13 +580,16 @@
|
|||
M.localTimestamp = 0;
|
||||
M.audio.play();
|
||||
|
||||
if (M.synced) {
|
||||
// Desync and disable auto-resync
|
||||
if (M.synced || M.wantSync) {
|
||||
M.synced = false;
|
||||
M.wantSync = false;
|
||||
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 title = track.title?.trim() || (track.filename || trackId || "Unknown").replace(/\.[^.]+$/, "");
|
||||
|
||||
|
|
@ -618,16 +613,16 @@
|
|||
|
||||
const menuItems = [];
|
||||
|
||||
// Play (queue only, single track)
|
||||
if (type === 'queue' && !hasSelection) {
|
||||
// Play (queue only, single track or single selection)
|
||||
if (type === 'queue' && selectedCount === 1) {
|
||||
menuItems.push({
|
||||
label: "▶ Play",
|
||||
action: () => playTrack(track, index)
|
||||
});
|
||||
}
|
||||
|
||||
// Preview (all views, single track)
|
||||
if (!hasSelection) {
|
||||
// Preview (all views, single track or single selection)
|
||||
if (selectedCount === 1) {
|
||||
menuItems.push({
|
||||
label: "⏵ Preview",
|
||||
action: () => previewTrack(track)
|
||||
|
|
@ -635,7 +630,7 @@
|
|||
}
|
||||
|
||||
// Queue actions
|
||||
if (type === 'queue' && canEdit) {
|
||||
if (type === 'queue' && canEditQueue) {
|
||||
menuItems.push({
|
||||
label: selectedCount > 1 ? `+ Add ${selectedCount} again` : "+ Add again",
|
||||
action: () => addToQueue(idsForAction)
|
||||
|
|
@ -651,8 +646,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Library actions
|
||||
if (type === 'library' && canEdit) {
|
||||
// Library actions - can add to queue if user has queue edit permission
|
||||
if (type === 'library' && canEditQueue) {
|
||||
menuItems.push({
|
||||
label: selectedCount > 1 ? `+ Add ${selectedCount} to queue` : "+ Add to queue",
|
||||
action: () => addToQueue(idsForAction)
|
||||
|
|
@ -665,7 +660,8 @@
|
|||
|
||||
// Playlist actions
|
||||
if (type === 'playlist') {
|
||||
if (canEdit) {
|
||||
// Can add to queue if user has queue edit permission
|
||||
if (canEditQueue) {
|
||||
menuItems.push({
|
||||
label: selectedCount > 1 ? `+ Add ${selectedCount} to queue` : "+ Add to queue",
|
||||
action: () => addToQueue(idsForAction)
|
||||
|
|
@ -675,7 +671,8 @@
|
|||
action: () => addToQueue(idsForAction, true)
|
||||
});
|
||||
}
|
||||
if (canRemove && playlistId) {
|
||||
// Can remove if user owns the playlist
|
||||
if (isPlaylistOwner && playlistId) {
|
||||
menuItems.push({
|
||||
label: selectedCount > 1 ? `🗑️ Remove ${selectedCount}` : "🗑️ Remove",
|
||||
danger: true,
|
||||
|
|
@ -753,6 +750,8 @@
|
|||
M.showToast(playNext ? "Playing next" : "Added to queue");
|
||||
clearSelection();
|
||||
render();
|
||||
} else {
|
||||
M.showToast("Failed to add to queue", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -769,6 +768,8 @@
|
|||
else if (res.ok) {
|
||||
M.showToast("Removed");
|
||||
clearSelection();
|
||||
} else {
|
||||
M.showToast("Failed to remove from queue", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -785,6 +786,8 @@
|
|||
M.showToast("Removed from playlist");
|
||||
clearSelection();
|
||||
if (M.playlists) M.playlists.reloadCurrentPlaylist();
|
||||
} else {
|
||||
M.showToast("Failed to remove from playlist", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue