Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions api/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,22 @@
}
.load-btn .arrow { color: var(--orange); margin-right: 4px; }
.load-btn:hover .arrow { color: var(--black); }
/* Per-row "remove from playlist" × — only rendered when the active
source is a regular playlist (see renderLibrary's reorderMode). */
.pl-rm-btn {
background: transparent;
border: 1px solid var(--wire);
color: var(--text3);
width: 22px; height: 22px; padding: 0;
margin-right: 6px;
vertical-align: middle;
font-family: inherit;
font-size: 12px;
line-height: 1;
cursor: pointer;
transition: all .12s ease;
}
.pl-rm-btn:hover { background: var(--red); color: var(--black); border-color: var(--red); }

.load-menu {
position: fixed;
Expand Down Expand Up @@ -5902,6 +5918,7 @@
<button id="bulk-tag-add">+ TAG…</button>
<button id="bulk-tag-remove">− TAG…</button>
<button id="bulk-playlist">+ PLAYLIST…</button>
${activeRegularPlaylist() ? '<button id="bulk-pl-remove" title="Remove selected from this playlist">− PLAYLIST</button>' : ''}
<button id="bulk-export">EXPORT…</button>
<button class="danger" id="bulk-delete">DELETE</button>
<span class="spacer"></span>
Expand All @@ -5913,6 +5930,8 @@
bar.querySelector('#bulk-tag-add').onclick = e => openBulkTagPicker(e.currentTarget, 'add');
bar.querySelector('#bulk-tag-remove').onclick = e => openBulkTagPicker(e.currentTarget, 'remove');
bar.querySelector('#bulk-playlist').onclick = e => openBulkPlaylistPicker(e.currentTarget);
const plRemove = bar.querySelector('#bulk-pl-remove');
if (plRemove) plRemove.onclick = bulkRemoveFromPlaylist;
bar.querySelector('#bulk-export').onclick = () => {
const sel = Array.from(bulkSelectedIDs);
openExportModal('selection:' + sel.join(','), `${sel.length} SELECTED TRACK${sel.length === 1 ? '' : 'S'}`);
Expand Down Expand Up @@ -6279,13 +6298,17 @@
// row at once — now that the cap is gone every filtered row is rendered,
// so this matches the full filtered total shown in the stats chip.
const head2 = `<th class="sel-col"><input type="checkbox" id="lib-sel-all" title="Select all"></th>` + head + '<th></th>';
// Source-aware row extras: when viewing a regular playlist, rows can be
// drag-reordered and get a per-row × that removes them from the playlist.
const reorderMode = activeRegularPlaylist();
const body = visible.map(t => {
const cells = cols.map(c => `<td class="${c.tdClass || ''}">${c.cell(t)}</td>`).join('');
const bulkSelected = bulkSelectedIDs.has(t.id);
return `<tr data-track-row="${t.id}" draggable="true" class="${selectedTrackID === t.id ? 'selected' : ''} ${bulkSelected ? 'selected-bulk' : ''} ${t.file_missing ? 'file-missing' : ''}">` +
`<td class="sel-col"><input type="checkbox" data-sel-row="${t.id}" ${bulkSelected ? 'checked' : ''}></td>` +
cells +
`<td class="load-cell">
${reorderMode ? `<button class="pl-rm-btn" data-pl-remove="${t.id}" title="Remove from playlist">×</button>` : ''}
<button class="load-btn ${currentPlayers.length === 0 ? '' : 'ready'}" data-track="${t.id}">
<span class="arrow">▶</span>LOAD
</button>
Expand All @@ -6306,6 +6329,16 @@
el.querySelectorAll('.load-btn').forEach(btn => {
btn.addEventListener('click', e => handleLoadClick(e, btn));
});
// Per-row × (playlist view only) → drop the track from this playlist.
// Looks the index up by ID so an active search/sort can't misalign it.
el.querySelectorAll('[data-pl-remove]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const tid = parseInt(btn.getAttribute('data-pl-remove'), 10);
const idx = plSelectedTracks.findIndex(t => t.id === tid);
if (idx !== -1) removePlaylistTrack(idx);
});
});
// Bulk-selection checkboxes (per-row + the header sel-all) — wires
// toggleBulkSelection on change so the bottom bulk-action bar shows.
wireBulkSelectionHandlers();
Expand Down Expand Up @@ -6374,11 +6407,6 @@
// playlist, dragging a row reorders within that playlist; otherwise
// (Collection, or a smart playlist) the drag pops up the floating
// playlist drop overlay so the user can add the track elsewhere.
const reorderMode = (typeof selectedSource === 'number')
&& (() => {
const sel = playlistsCache.find(p => p.id === selectedSource);
return !!sel && !sel.is_folder && !sel.is_smart;
})();
el.querySelectorAll('tr[data-track-row]').forEach((row, rowIdx) => {
const tid = parseInt(row.getAttribute('data-track-row'), 10);
row.addEventListener('click', (e) => {
Expand Down Expand Up @@ -11301,15 +11329,39 @@ <h4>TAGS <span class="count" id="tags-count">${drawerTrackTags.length}</span></h
await postPlaylistTrackIDs(plSelectedID, ids);
}

// True when the active library source is a regular (non-smart, non-folder)
// playlist — membership is user-managed, so rows can be reordered/removed.
function activeRegularPlaylist() {
if (typeof selectedSource !== 'number') return false;
const sel = playlistsCache.find(p => p.id === selectedSource);
return !!sel && !sel.is_folder && !sel.is_smart;
}

// Bulk-bar "− PLAYLIST": drop every selected track from the current
// playlist. Library rows, tags, cues, and other playlists are untouched.
async function bulkRemoveFromPlaylist() {
if (plSelectedID == null) return;
const drop = new Set(plSelectedTracks.filter(t => bulkSelectedIDs.has(t.id)).map(t => t.id));
if (drop.size === 0) { toast('SELECTION IS NOT IN THIS PLAYLIST', 'error'); return; }
const keep = plSelectedTracks.map(t => t.id).filter(id => !drop.has(id));
const n = plSelectedTracks.length - keep.length;
if (!await postPlaylistTrackIDs(plSelectedID, keep)) return;
// Deselect just the removed tracks — a cross-source selection survives.
drop.forEach(id => bulkSelectedIDs.delete(id));
updateBulkBar();
toast(`REMOVED ${n} TRACK${n === 1 ? '' : 'S'} FROM PLAYLIST`, 'success');
}

async function postPlaylistTrackIDs(playlistID, trackIDs) {
const r = await fetch(`/api/playlists/${playlistID}/tracks`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ track_ids: trackIDs }),
});
if (!r.ok) { toast('UPDATE FAILED', 'error'); return; }
if (!r.ok) { toast('UPDATE FAILED', 'error'); return false; }
await loadPlaylistTracks(playlistID);
// Also refresh the cache so the track count in the tree updates.
await loadPlaylists(true);
return true;
}

// Add-tracks picker — a modal that lists every library track, filterable
Expand Down
Loading