diff --git a/index.html b/index.html index b1bb44d..001b9f2 100644 --- a/index.html +++ b/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; } .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: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: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; } @keyframes spin { to { transform: rotate(360deg); } } .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 .fill { height: 100%; background: #1a6df5; border-radius: 3px; width: 0%; } .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 img { max-width: 50vw; max-height: 50vh; } -@keyframes fadeout { from { opacity: 1; } to { opacity: 0; visibility: hidden; } } +#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; filter: drop-shadow(0 8px 16px rgba(0,0,0,0.5)); } +@keyframes fadeout { from { opacity: 0.75; transform: scale(1); } to { opacity: 0; transform: scale(1.75); visibility: hidden; } } #timeRemaining { color: #555; } #vuMeter { width: 100%; height: 20px; border-radius: 2px; display: block; } .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: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: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 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; } @@ -74,15 +78,25 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; } .playlist-body::-webkit-scrollbar { width: 6px; } .playlist-body::-webkit-scrollbar-track { background: transparent; } .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.active { background: linear-gradient(to bottom, #0f3a0f, #082208); border-left-color: #33ff55; } -.track-item.active .col-title { color: #55ff77; } -.track-item.active .col-artist { color: #33dd55; } -.track-item.active .col-idx { color: #55ff77; } -.track-item.active .col-time { color: #33dd55; } +.track-item.active .col-title { color: #55ff77; font-weight: 700; } +.track-item.active .col-artist { color: #33dd55; font-weight: 600; } +.track-item.active .col-idx { color: #55ff77; font-weight: 700; } +.track-item.active .col-time { color: #33dd55; font-weight: 600; } .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-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; } @@ -223,7 +237,8 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
@@ -538,12 +553,16 @@ drawSpectrum(); let tracks = []; let currentIndex = -1; +let selectedIndices = new Set(); +let anchorTrack = null; +let dragIndices = null; let dirHandle = null; const addFolderBtn = document.getElementById('addFolderBtn'); const addFilesBtn = document.getElementById('addFilesBtn'); 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 scanning = document.getElementById('scanning'); const scanningText = scanning.querySelector('span'); @@ -925,15 +944,11 @@ function addTracks(newTracks) { showError('No audio files found.'); return; } - const wasEmpty = tracks.length === 0; tracks = tracks.concat(newTracks); renderTracks(); - if (wasEmpty) { - playTrack(0); - } } -function clearTracks() { +function isolateTracks() { tracks.forEach((t, i) => { if (i !== currentIndex) URL.revokeObjectURL(t.url); }); @@ -941,12 +956,45 @@ function clearTracks() { const current = tracks[currentIndex]; tracks = [current]; currentIndex = 0; + selectedIndices.clear(); } else { tracks = []; + selectedIndices.clear(); } 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) { switch (field) { case 'idx': return (a, b) => tracks.indexOf(a) - tracks.indexOf(b); @@ -966,10 +1014,93 @@ function renderTracks() { trackCount.textContent = tracks.length; sorted.forEach((t, i) => { const div = document.createElement('div'); - div.className = 'track-item' + (t === tracks[currentIndex] ? ' active' : ''); 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 = `${realIdx + 1}${escHtml(t.name)}${escHtml(t.artist || '-')}${escHtml(t.filename)}${t.playtime ? fmtTime(t.playtime) : '-'}${escHtml(t.genre || '-')}`; - 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); }); const hasT = tracks.length > 0; @@ -1105,7 +1236,19 @@ audioElB.addEventListener('pause', onPause); playBtn.addEventListener('click', () => { 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); @@ -1151,7 +1294,8 @@ volumeSlider.addEventListener('input', () => { addFolderBtn.addEventListener('click', addFolder); addFilesBtn.addEventListener('click', addFiles); -clearBtn.addEventListener('click', clearTracks); +isolateBtn.addEventListener('click', isolateTracks); +deleteBtn.addEventListener('click', deleteTrack); sortSelect.addEventListener('change', () => { if (sortSelect.value === sortField) { sortAsc = !sortAsc; } @@ -1168,9 +1312,11 @@ shuffleSelect.addEventListener('change', () => { shuffleOn = shuffleSelect.value volDisplay.textContent = Math.round(parseFloat(volumeSlider.value) * 100); let dragCounter = 0; +let isInternalDrag = false; const content = document.querySelector('.content'); content.addEventListener('dragenter', (e) => { + if (isInternalDrag) return; e.preventDefault(); dragCounter++; content.style.outline = '2px dashed #1a6df5'; @@ -1178,14 +1324,19 @@ content.addEventListener('dragenter', (e) => { }); content.addEventListener('dragleave', (e) => { + if (isInternalDrag) return; e.preventDefault(); dragCounter--; 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) => { + if (isInternalDrag) return; e.preventDefault(); dragCounter = 0; content.style.outline = 'none';