mirror of
https://codeberg.org/armin/noiz.git
synced 2026-09-01 14:00:54 +02:00
Add excessive playlist management (dragging files around, move marker, ability to select multiple files at once (ctrl/shift)
This commit is contained in:
parent
852b8f76a1
commit
4d273f085b
1 changed files with 172 additions and 21 deletions
193
index.html
193
index.html
|
|
@ -19,8 +19,10 @@
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #111; color: #e0e0e0; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #111; color: #e0e0e0; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
.btn { background: linear-gradient(to bottom, #3a3a3a, #282828); border: 1px solid #484848; color: #e0e0e0; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; white-space: nowrap; box-shadow: 0 3px 0 #181818, 0 4px 6px rgba(0,0,0,0.4); }
|
.btn { background: linear-gradient(to bottom, #3a3a3a, #282828); border: 1px solid #484848; color: #e0e0e0; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; white-space: nowrap; box-shadow: 0 3px 0 #181818, 0 4px 6px rgba(0,0,0,0.4); }
|
||||||
.btn:hover { background: linear-gradient(to bottom, #404040, #2e2e2e); }
|
.btn:hover { background: linear-gradient(to bottom, #404040, #2e2e2e); }
|
||||||
|
.btn:active { background: linear-gradient(to bottom, #282828, #3a3a3a); box-shadow: 0 1px 0 #181818, 0 2px 4px rgba(0,0,0,0.4); transform: translateY(2px); }
|
||||||
.btn-primary { background: linear-gradient(to bottom, #2a7dff, #185dd5); border-color: #1a6df5; color: #fff; box-shadow: 0 3px 0 #0f4ab0, 0 4px 6px rgba(0,0,0,0.4); }
|
.btn-primary { background: linear-gradient(to bottom, #2a7dff, #185dd5); border-color: #1a6df5; color: #fff; box-shadow: 0 3px 0 #0f4ab0, 0 4px 6px rgba(0,0,0,0.4); }
|
||||||
.btn-primary:hover { background: linear-gradient(to bottom, #3a8dff, #1a6de5); }
|
.btn-primary:hover { background: linear-gradient(to bottom, #3a8dff, #1a6de5); }
|
||||||
|
.btn-primary:active { background: linear-gradient(to bottom, #185dd5, #2a7dff); box-shadow: 0 1px 0 #0f4ab0, 0 2px 4px rgba(0,0,0,0.4); transform: translateY(2px); }
|
||||||
.btn-sm { padding: 4px 10px; font-size: 11px; }
|
.btn-sm { padding: 4px 10px; font-size: 11px; }
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
.brand { font-size: 15px; font-weight: 700; color: #999; letter-spacing: 1px; user-select: none; }
|
.brand { font-size: 15px; font-weight: 700; color: #999; letter-spacing: 1px; user-select: none; }
|
||||||
|
|
@ -43,16 +45,18 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
|
||||||
.progress-row .progress-bar { flex: 1; height: 6px; background: #333; border-radius: 3px; cursor: pointer; }
|
.progress-row .progress-bar { flex: 1; height: 6px; background: #333; border-radius: 3px; cursor: pointer; }
|
||||||
.progress-row .progress-bar .fill { height: 100%; background: #1a6df5; border-radius: 3px; width: 0%; }
|
.progress-row .progress-bar .fill { height: 100%; background: #1a6df5; border-radius: 3px; width: 0%; }
|
||||||
.progress-row .progress-bar:hover .fill { background: #3a8df5; }
|
.progress-row .progress-bar:hover .fill { background: #3a8df5; }
|
||||||
#noiz-overlay { position: fixed; inset: 0; display: flex; justify-content: center; align-items: center; background: transparent; pointer-events: none; z-index: 9999; animation: fadeout 1s ease 2s forwards; }
|
#noiz-overlay { position: fixed; opacity: 0.75; inset: 0; display: flex; justify-content: center; align-items: center; background: transparent; pointer-events: none; z-index: 9999; animation: fadeout 750ms ease 750ms forwards; }
|
||||||
#noiz-overlay img { max-width: 50vw; max-height: 50vh; }
|
#noiz-overlay img { max-width: 50vw; max-height: 50vh; filter: drop-shadow(0 8px 16px rgba(0,0,0,0.5)); }
|
||||||
@keyframes fadeout { from { opacity: 1; } to { opacity: 0; visibility: hidden; } }
|
@keyframes fadeout { from { opacity: 0.75; transform: scale(1); } to { opacity: 0; transform: scale(1.75); visibility: hidden; } }
|
||||||
#timeRemaining { color: #555; }
|
#timeRemaining { color: #555; }
|
||||||
#vuMeter { width: 100%; height: 20px; border-radius: 2px; display: block; }
|
#vuMeter { width: 100%; height: 20px; border-radius: 2px; display: block; }
|
||||||
.controls { display: flex; align-items: center; gap: 16px; align-self: flex-end; }
|
.controls { display: flex; align-items: center; gap: 16px; align-self: flex-end; }
|
||||||
.controls button { background: linear-gradient(to bottom, #3a3a3a, #222); border: none; color: #ccc; cursor: pointer; padding: 6px; font-size: 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 0 #1a1a1a, 0 3px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06); }
|
.controls button { background: linear-gradient(to bottom, #3a3a3a, #222); border: none; color: #ccc; cursor: pointer; padding: 6px; font-size: 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 0 #1a1a1a, 0 3px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06); }
|
||||||
.controls button:hover { color: #fff; background: linear-gradient(to bottom, #444, #2a2a2a); }
|
.controls button:hover { color: #fff; background: linear-gradient(to bottom, #444, #2a2a2a); }
|
||||||
|
.controls button:active { background: linear-gradient(to bottom, #222, #3a3a3a); box-shadow: 0 1px 0 #1a1a1a, 0 1px 2px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06); transform: translateY(1px); }
|
||||||
.controls .play-btn { width: 44px; height: 44px; background: linear-gradient(to bottom, #2a7dff, #185dd5); color: #fff; font-size: 16px; border: 1px solid #1a6df5; box-shadow: 0 3px 0 #0f4ab0, 0 4px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15); }
|
.controls .play-btn { width: 44px; height: 44px; background: linear-gradient(to bottom, #2a7dff, #185dd5); color: #fff; font-size: 16px; border: 1px solid #1a6df5; box-shadow: 0 3px 0 #0f4ab0, 0 4px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15); }
|
||||||
.controls .play-btn:hover { background: linear-gradient(to bottom, #3a8dff, #2a6de5); }
|
.controls .play-btn:hover { background: linear-gradient(to bottom, #3a8dff, #2a6de5); }
|
||||||
|
.controls .play-btn:active { background: linear-gradient(to bottom, #185dd5, #2a7dff); box-shadow: 0 1px 0 #0f4ab0, 0 2px 4px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15); transform: translateY(2px); }
|
||||||
.volume-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; width: 100%; max-width: 200px; align-self: flex-end; }
|
.volume-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; width: 100%; max-width: 200px; align-self: flex-end; }
|
||||||
.volume-row input[type="range"] { flex: 1; height: 3px; -webkit-appearance: none; appearance: none; background: #333; border-radius: 2px; outline: none; }
|
.volume-row input[type="range"] { flex: 1; height: 3px; -webkit-appearance: none; appearance: none; background: #333; border-radius: 2px; outline: none; }
|
||||||
.volume-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #1a6df5; cursor: pointer; }
|
.volume-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #1a6df5; cursor: pointer; }
|
||||||
|
|
@ -74,15 +78,25 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
|
||||||
.playlist-body::-webkit-scrollbar { width: 6px; }
|
.playlist-body::-webkit-scrollbar { width: 6px; }
|
||||||
.playlist-body::-webkit-scrollbar-track { background: transparent; }
|
.playlist-body::-webkit-scrollbar-track { background: transparent; }
|
||||||
.playlist-body::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
.playlist-body::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
||||||
.track-item { display: grid; grid-template-columns: 36px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 6px 24px; cursor: pointer; font-size: 12px; border-left: 3px solid transparent; transition: background .08s; align-items: center; }
|
.track-item { display: grid; grid-template-columns: 36px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 6px 24px; cursor: pointer; font-size: 12px; border-left: 3px solid transparent; transition: background .08s; align-items: center; user-select: none; }
|
||||||
.track-item:hover { background: #1a1a1a; }
|
.track-item:hover { background: #1a1a1a; }
|
||||||
.track-item.active { background: linear-gradient(to bottom, #0f3a0f, #082208); border-left-color: #33ff55; }
|
.track-item.active { background: linear-gradient(to bottom, #0f3a0f, #082208); border-left-color: #33ff55; }
|
||||||
.track-item.active .col-title { color: #55ff77; }
|
.track-item.active .col-title { color: #55ff77; font-weight: 700; }
|
||||||
.track-item.active .col-artist { color: #33dd55; }
|
.track-item.active .col-artist { color: #33dd55; font-weight: 600; }
|
||||||
.track-item.active .col-idx { color: #55ff77; }
|
.track-item.active .col-idx { color: #55ff77; font-weight: 700; }
|
||||||
.track-item.active .col-time { color: #33dd55; }
|
.track-item.active .col-time { color: #33dd55; font-weight: 600; }
|
||||||
.track-item.active .col-genre { color: #33dd55; }
|
.track-item.active .col-genre { color: #33dd55; }
|
||||||
.track-item.active .col-file { color: #33cc55; }
|
.track-item.active .col-file { color: #33cc55; font-weight: 600; }
|
||||||
|
.track-item.selected { background: linear-gradient(to bottom, #1a3a5a, #0f2440); border-left-color: #3399ff; }
|
||||||
|
.track-item.selected .col-title { color: #77bbff; font-weight: 600; }
|
||||||
|
.track-item.selected .col-artist { color: #5599dd; }
|
||||||
|
.track-item.selected .col-idx { color: #5599dd; }
|
||||||
|
.track-item.selected .col-time { color: #5599dd; }
|
||||||
|
.track-item.selected .col-file { color: #4488cc; }
|
||||||
|
.track-item.active.selected { background: linear-gradient(to bottom, #1a6a1a, #0d3d0d); border-left-color: #66ff88; }
|
||||||
|
.track-item.active.selected .col-title { color: #88ffaa; }
|
||||||
|
.track-item.dragging { opacity: 0.4; }
|
||||||
|
.track-item.drag-over { border-top: 2px solid #22bb44; }
|
||||||
.track-item .col-idx { color: #444; text-align: right; font-size: 10px; }
|
.track-item .col-idx { color: #444; text-align: right; font-size: 10px; }
|
||||||
.track-item .col-title { color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.track-item .col-title { color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.track-item .col-artist { color: #999; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.track-item .col-artist { color: #999; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
@ -223,7 +237,8 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
|
||||||
<div class="add-bar">
|
<div class="add-bar">
|
||||||
<button class="btn btn-primary" id="addFolderBtn">+ Folder</button>
|
<button class="btn btn-primary" id="addFolderBtn">+ Folder</button>
|
||||||
<button class="btn" id="addFilesBtn">+ Files</button>
|
<button class="btn" id="addFilesBtn">+ Files</button>
|
||||||
<button class="btn" id="clearBtn">Isolate</button>
|
<button class="btn" id="isolateBtn">Isolate</button>
|
||||||
|
<button class="btn" id="deleteBtn">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -538,12 +553,16 @@ drawSpectrum();
|
||||||
|
|
||||||
let tracks = [];
|
let tracks = [];
|
||||||
let currentIndex = -1;
|
let currentIndex = -1;
|
||||||
|
let selectedIndices = new Set();
|
||||||
|
let anchorTrack = null;
|
||||||
|
let dragIndices = null;
|
||||||
let dirHandle = null;
|
let dirHandle = null;
|
||||||
|
|
||||||
const addFolderBtn = document.getElementById('addFolderBtn');
|
const addFolderBtn = document.getElementById('addFolderBtn');
|
||||||
const addFilesBtn = document.getElementById('addFilesBtn');
|
const addFilesBtn = document.getElementById('addFilesBtn');
|
||||||
const folderPicker = document.getElementById('folderPicker');
|
const folderPicker = document.getElementById('folderPicker');
|
||||||
const clearBtn = document.getElementById('clearBtn');
|
const isolateBtn = document.getElementById('isolateBtn');
|
||||||
|
const deleteBtn = document.getElementById('deleteBtn');
|
||||||
const sortSelect = document.getElementById('sortSelect');
|
const sortSelect = document.getElementById('sortSelect');
|
||||||
const scanning = document.getElementById('scanning');
|
const scanning = document.getElementById('scanning');
|
||||||
const scanningText = scanning.querySelector('span');
|
const scanningText = scanning.querySelector('span');
|
||||||
|
|
@ -925,15 +944,11 @@ function addTracks(newTracks) {
|
||||||
showError('No audio files found.');
|
showError('No audio files found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wasEmpty = tracks.length === 0;
|
|
||||||
tracks = tracks.concat(newTracks);
|
tracks = tracks.concat(newTracks);
|
||||||
renderTracks();
|
renderTracks();
|
||||||
if (wasEmpty) {
|
|
||||||
playTrack(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTracks() {
|
function isolateTracks() {
|
||||||
tracks.forEach((t, i) => {
|
tracks.forEach((t, i) => {
|
||||||
if (i !== currentIndex) URL.revokeObjectURL(t.url);
|
if (i !== currentIndex) URL.revokeObjectURL(t.url);
|
||||||
});
|
});
|
||||||
|
|
@ -941,12 +956,45 @@ function clearTracks() {
|
||||||
const current = tracks[currentIndex];
|
const current = tracks[currentIndex];
|
||||||
tracks = [current];
|
tracks = [current];
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
|
selectedIndices.clear();
|
||||||
} else {
|
} else {
|
||||||
tracks = [];
|
tracks = [];
|
||||||
|
selectedIndices.clear();
|
||||||
}
|
}
|
||||||
renderTracks();
|
renderTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteTrack() {
|
||||||
|
if (selectedIndices.size === 0) return;
|
||||||
|
if (selectedIndices.size === tracks.length) {
|
||||||
|
tracks.forEach(t => URL.revokeObjectURL(t.url));
|
||||||
|
tracks = [];
|
||||||
|
currentIndex = -1;
|
||||||
|
selectedIndices.clear();
|
||||||
|
renderTracks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sortedSel = [...selectedIndices].sort((a, b) => a - b);
|
||||||
|
const willDeleteCurrent = selectedIndices.has(currentIndex);
|
||||||
|
const playingTrack = currentIndex >= 0 ? tracks[currentIndex] : null;
|
||||||
|
for (const idx of sortedSel) URL.revokeObjectURL(tracks[idx].url);
|
||||||
|
for (let j = sortedSel.length - 1; j >= 0; j--) tracks.splice(sortedSel[j], 1);
|
||||||
|
if (willDeleteCurrent) {
|
||||||
|
if (tracks.length > 0) {
|
||||||
|
currentIndex = Math.min(sortedSel[0], tracks.length - 1);
|
||||||
|
playTrack(currentIndex);
|
||||||
|
} else {
|
||||||
|
currentIndex = -1;
|
||||||
|
renderTracks();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const removedBefore = sortedSel.filter(idx => idx < currentIndex).length;
|
||||||
|
currentIndex -= removedBefore;
|
||||||
|
renderTracks();
|
||||||
|
}
|
||||||
|
selectedIndices.clear();
|
||||||
|
}
|
||||||
|
|
||||||
function getSortFn(field) {
|
function getSortFn(field) {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'idx': return (a, b) => tracks.indexOf(a) - tracks.indexOf(b);
|
case 'idx': return (a, b) => tracks.indexOf(a) - tracks.indexOf(b);
|
||||||
|
|
@ -966,10 +1014,93 @@ function renderTracks() {
|
||||||
trackCount.textContent = tracks.length;
|
trackCount.textContent = tracks.length;
|
||||||
sorted.forEach((t, i) => {
|
sorted.forEach((t, i) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'track-item' + (t === tracks[currentIndex] ? ' active' : '');
|
|
||||||
const realIdx = tracks.indexOf(t);
|
const realIdx = tracks.indexOf(t);
|
||||||
|
const classes = ['track-item'];
|
||||||
|
if (t === tracks[currentIndex]) classes.push('active');
|
||||||
|
if (selectedIndices.has(realIdx)) classes.push('selected');
|
||||||
|
div.className = classes.join(' ');
|
||||||
div.innerHTML = `<span class="col-idx">${realIdx + 1}</span><span class="col-title">${escHtml(t.name)}</span><span class="col-artist">${escHtml(t.artist || '-')}</span><span class="col-file">${escHtml(t.filename)}</span><span class="col-time">${t.playtime ? fmtTime(t.playtime) : '-'}</span><span class="col-genre">${escHtml(t.genre || '-')}</span>`;
|
div.innerHTML = `<span class="col-idx">${realIdx + 1}</span><span class="col-title">${escHtml(t.name)}</span><span class="col-artist">${escHtml(t.artist || '-')}</span><span class="col-file">${escHtml(t.filename)}</span><span class="col-time">${t.playtime ? fmtTime(t.playtime) : '-'}</span><span class="col-genre">${escHtml(t.genre || '-')}</span>`;
|
||||||
div.addEventListener('click', () => playTrack(realIdx));
|
div.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.detail > 1) return;
|
||||||
|
const ctrl = e.ctrlKey || e.metaKey;
|
||||||
|
const shift = e.shiftKey;
|
||||||
|
if (shift && anchorTrack) {
|
||||||
|
const sorted2 = [...tracks].sort(getSortFn(sortField));
|
||||||
|
if (!sortAsc) sorted2.reverse();
|
||||||
|
const anchorPos = sorted2.indexOf(anchorTrack);
|
||||||
|
const clickPos = i;
|
||||||
|
const lo = Math.min(anchorPos, clickPos);
|
||||||
|
const hi = Math.max(anchorPos, clickPos);
|
||||||
|
selectedIndices.clear();
|
||||||
|
for (let j = lo; j <= hi; j++) selectedIndices.add(tracks.indexOf(sorted2[j]));
|
||||||
|
renderTracks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctrl) {
|
||||||
|
if (selectedIndices.has(realIdx)) selectedIndices.delete(realIdx);
|
||||||
|
else selectedIndices.add(realIdx);
|
||||||
|
anchorTrack = t;
|
||||||
|
renderTracks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedIndices.has(realIdx)) {
|
||||||
|
selectedIndices.clear();
|
||||||
|
selectedIndices.add(realIdx);
|
||||||
|
anchorTrack = t;
|
||||||
|
renderTracks();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
div.addEventListener('dblclick', () => {
|
||||||
|
selectedIndices.clear();
|
||||||
|
selectedIndices.add(realIdx);
|
||||||
|
anchorTrack = t;
|
||||||
|
playTrack(realIdx);
|
||||||
|
});
|
||||||
|
div.draggable = true;
|
||||||
|
div.addEventListener('dragstart', (e) => {
|
||||||
|
if (!selectedIndices.has(realIdx)) {
|
||||||
|
selectedIndices.clear();
|
||||||
|
selectedIndices.add(realIdx);
|
||||||
|
anchorTrack = t;
|
||||||
|
renderTracks();
|
||||||
|
}
|
||||||
|
dragIndices = new Set(selectedIndices);
|
||||||
|
isInternalDrag = true;
|
||||||
|
div.classList.add('dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
});
|
||||||
|
div.addEventListener('dragend', () => {
|
||||||
|
div.classList.remove('dragging');
|
||||||
|
dragIndices = null;
|
||||||
|
isInternalDrag = false;
|
||||||
|
document.querySelectorAll('.track-item.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||||
|
});
|
||||||
|
div.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
document.querySelectorAll('.track-item.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||||
|
div.classList.add('drag-over');
|
||||||
|
});
|
||||||
|
div.addEventListener('dragleave', () => {
|
||||||
|
div.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
div.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
div.classList.remove('drag-over');
|
||||||
|
if (!dragIndices || dragIndices.size === 0 || dragIndices.has(realIdx)) return;
|
||||||
|
const indices = [...dragIndices].sort((a, b) => a - b);
|
||||||
|
const draggedTracks = indices.map(idx => tracks[idx]);
|
||||||
|
const wasPlaying = currentIndex >= 0 ? tracks[currentIndex] : null;
|
||||||
|
for (let j = indices.length - 1; j >= 0; j--) tracks.splice(indices[j], 1);
|
||||||
|
const targetIdx = tracks.indexOf(t);
|
||||||
|
tracks.splice(targetIdx, 0, ...draggedTracks);
|
||||||
|
if (wasPlaying) currentIndex = tracks.indexOf(wasPlaying);
|
||||||
|
selectedIndices.clear();
|
||||||
|
for (const tr of draggedTracks) selectedIndices.add(tracks.indexOf(tr));
|
||||||
|
anchorTrack = draggedTracks[0];
|
||||||
|
dragIndices = null;
|
||||||
|
renderTracks();
|
||||||
|
});
|
||||||
trackList.appendChild(div);
|
trackList.appendChild(div);
|
||||||
});
|
});
|
||||||
const hasT = tracks.length > 0;
|
const hasT = tracks.length > 0;
|
||||||
|
|
@ -1105,7 +1236,19 @@ audioElB.addEventListener('pause', onPause);
|
||||||
|
|
||||||
playBtn.addEventListener('click', () => {
|
playBtn.addEventListener('click', () => {
|
||||||
if (tracks.length === 0) return;
|
if (tracks.length === 0) return;
|
||||||
if (audio.paused) audio.play().catch(() => {}); else audio.pause();
|
const selArr = [...selectedIndices];
|
||||||
|
const sel = selArr.length > 0 ? selArr[0] : -1;
|
||||||
|
if (sel >= 0 && sel !== currentIndex) {
|
||||||
|
playTrack(sel);
|
||||||
|
} else if (currentIndex < 0 && sel >= 0) {
|
||||||
|
playTrack(sel);
|
||||||
|
} else if (currentIndex < 0) {
|
||||||
|
playTrack(0);
|
||||||
|
} else if (audio.paused) {
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
} else {
|
||||||
|
audio.pause();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
prevBtn.addEventListener('click', prevTrack);
|
prevBtn.addEventListener('click', prevTrack);
|
||||||
|
|
@ -1151,7 +1294,8 @@ volumeSlider.addEventListener('input', () => {
|
||||||
|
|
||||||
addFolderBtn.addEventListener('click', addFolder);
|
addFolderBtn.addEventListener('click', addFolder);
|
||||||
addFilesBtn.addEventListener('click', addFiles);
|
addFilesBtn.addEventListener('click', addFiles);
|
||||||
clearBtn.addEventListener('click', clearTracks);
|
isolateBtn.addEventListener('click', isolateTracks);
|
||||||
|
deleteBtn.addEventListener('click', deleteTrack);
|
||||||
|
|
||||||
sortSelect.addEventListener('change', () => {
|
sortSelect.addEventListener('change', () => {
|
||||||
if (sortSelect.value === sortField) { sortAsc = !sortAsc; }
|
if (sortSelect.value === sortField) { sortAsc = !sortAsc; }
|
||||||
|
|
@ -1168,9 +1312,11 @@ shuffleSelect.addEventListener('change', () => { shuffleOn = shuffleSelect.value
|
||||||
volDisplay.textContent = Math.round(parseFloat(volumeSlider.value) * 100);
|
volDisplay.textContent = Math.round(parseFloat(volumeSlider.value) * 100);
|
||||||
|
|
||||||
let dragCounter = 0;
|
let dragCounter = 0;
|
||||||
|
let isInternalDrag = false;
|
||||||
const content = document.querySelector('.content');
|
const content = document.querySelector('.content');
|
||||||
|
|
||||||
content.addEventListener('dragenter', (e) => {
|
content.addEventListener('dragenter', (e) => {
|
||||||
|
if (isInternalDrag) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragCounter++;
|
dragCounter++;
|
||||||
content.style.outline = '2px dashed #1a6df5';
|
content.style.outline = '2px dashed #1a6df5';
|
||||||
|
|
@ -1178,14 +1324,19 @@ content.addEventListener('dragenter', (e) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
content.addEventListener('dragleave', (e) => {
|
content.addEventListener('dragleave', (e) => {
|
||||||
|
if (isInternalDrag) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragCounter--;
|
dragCounter--;
|
||||||
if (dragCounter <= 0) { dragCounter = 0; content.style.outline = 'none'; }
|
if (dragCounter <= 0) { dragCounter = 0; content.style.outline = 'none'; }
|
||||||
});
|
});
|
||||||
|
|
||||||
content.addEventListener('dragover', (e) => e.preventDefault());
|
content.addEventListener('dragover', (e) => {
|
||||||
|
if (isInternalDrag) { e.preventDefault(); return; }
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
content.addEventListener('drop', async (e) => {
|
content.addEventListener('drop', async (e) => {
|
||||||
|
if (isInternalDrag) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragCounter = 0;
|
dragCounter = 0;
|
||||||
content.style.outline = 'none';
|
content.style.outline = 'none';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue