diff --git a/index.html b/index.html index f92c588..681ec30 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- @keyframes spin { to { transform: rotate(360deg); } } .brand { font-size: 15px; font-weight: 700; color: #6b7089; letter-spacing: 1px; user-select: none; } .brand span { color: #6b7089; } -footer { display: flex !important; align-items: center; gap: 16px; padding: 10px 24px; background: linear-gradient(to bottom, #1e2132, #161821); border-top: 1px solid #272c42; font-size: 14px; color: #6b7089; flex-shrink: 0; } +footer { display: flex !important; align-items: center; gap: 16px; padding: 10px 24px; background: linear-gradient(to bottom, #1e2132, #161821); border-top: 1px solid #272c42; font-size: 14px; color: #6b7089; flex-shrink: 0; position: relative; } footer .brand { margin-left: auto; } .content { flex: 1; overflow: hidden; display: flex; flex-direction: column; } .pinned { flex-shrink: 0; background: linear-gradient(to bottom, #1e2132, #161821); padding: 16px 24px; display: flex; flex-direction: row; gap: 20px; align-items: center; } @@ -38,6 +38,14 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; } #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; } } +#tagEditorOverlay { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(22,24,33,0.85); z-index: 10000; } +#tagEditorModal { background: #1e2132; border: 1px solid #6b7089; border-radius: 8px; padding: 24px; min-width: 640px; max-width: 700px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); } +#tagEditorModal h3 { margin-bottom: 16px; color: #c6c8d1; font-size: 14px; font-weight: 600; } +.tag-editor-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.tag-editor-fields label { display: flex; flex-direction: column; gap: 2px; margin-bottom: 0; font-size: 11px; color: #6b7089; text-transform: uppercase; letter-spacing: .4px; } +#tagEditorModal input, #tagEditorModal select { background: #161821; border: 1px solid #272c42; color: #c6c8d1; padding: 6px 8px; border-radius: 4px; font-size: 12px; outline: none; } +#tagEditorModal input:focus, #tagEditorModal select:focus { border-color: #84a0c6; } +.tag-editor-buttons { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; } #timeRemaining { color: #6b7089; } #vuMeter { width: 100%; height: 20px; border-radius: 2px; display: block; } .controls { display: flex; align-items: center; gap: 16px; align-self: flex-end; } @@ -95,6 +103,9 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; } .track-item .col-time { color: #6b7089; font-size: 11px; font-variant-numeric: tabular-nums; overflow: hidden; white-space: nowrap; } .track-item .col-genre { color: #6b7089; font-size: 10px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#footerStatus { font-size: 12px; font-weight: 600; white-space: nowrap; position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; } +#footerStatus.success { color: #87be82; } +#footerStatus.error { color: #e27878; } #error { display: none; background: rgba(226,120,120,0.15); color: #e27878; padding: 8px 16px; font-size: 13px; text-align: center; border-bottom: 1px solid rgba(226,120,120,0.3); } @media (max-width: 700px) { .player { flex-direction: column; align-items: stretch; } @@ -232,12 +243,36 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
+
+
+
+

Edit Tags

+
+ + + + + + + + + + +
+ +
+ + +
+
+
@@ -566,6 +601,7 @@ const timeRemaining = document.getElementById('timeRemaining'); const progressRow = document.getElementById('progressRow'); const footerTrack = document.getElementById('footerTrack'); const footerTime = document.getElementById('footerTime'); +const footerStatus = document.getElementById('footerStatus'); const volumeSlider = document.getElementById('volumeSlider'); const volDisplay = document.getElementById('volDisplay'); const audioInfo = document.getElementById('audioInfo'); @@ -574,6 +610,21 @@ const windowSelect = document.getElementById('windowSelect'); const decaySelect = document.getElementById('decaySelect'); const barDecaySelect = document.getElementById('barDecaySelect'); const crossfadeSelect = document.getElementById('crossfadeSelect'); +const infoBtn = document.getElementById('infoBtn'); +const tagEditorOverlay = document.getElementById('tagEditorOverlay'); +const tagName = document.getElementById('tagName'); +const tagArtist = document.getElementById('tagArtist'); +const tagAlbum = document.getElementById('tagAlbum'); +const tagGenre = document.getElementById('tagGenre'); +const tagYear = document.getElementById('tagYear'); +const tagTrack = document.getElementById('tagTrack'); +const tagComment = document.getElementById('tagComment'); +const tagComposer = document.getElementById('tagComposer'); +const tagUrl = document.getElementById('tagUrl'); +const tagEncodedBy = document.getElementById('tagEncodedBy'); +const tagSaveBtn = document.getElementById('tagSaveBtn'); +const tagCancelBtn = document.getElementById('tagCancelBtn'); +const genreList = document.getElementById('genreList'); const AUDIO_EXTS = new Set(['.mp3','.wav','.ogg','.flac','.m4a','.aac','.wma','.aiff','.alac','.opus']); const GENRES = [ 'Blues','Classic Rock','Country','Dance','Disco','Funk','Grunge','Hip-Hop','Jazz','Metal', @@ -887,6 +938,10 @@ async function processFiles(files, paths) { genre: tags.genre || '', year: tags.year || '', track: tags.track || '', + comment: tags.comment || '', + composer: tags.composer || '', + urlTag: tags.urlTag || '', + encodedBy: tags.encodedBy || '', filename: file.name, url, file, @@ -1006,6 +1061,59 @@ function deleteTrack() { renderTracks(); } } +function openTagEditor() { + let idx = -1; + if (selectedIndices.size > 0) { + idx = [...selectedIndices][0]; + } else if (currentIndex >= 0) { + idx = currentIndex; + } + if (idx < 0) { showStatus('No track selected.', 'error'); return; } + const t = tracks[idx]; + tagEditorOverlay.dataset.idx = idx; + tagName.value = t.name || ''; + tagArtist.value = t.artist || ''; + tagAlbum.value = t.album || ''; + tagGenre.value = t.genre || ''; + tagYear.value = t.year || ''; + tagTrack.value = t.track || ''; + tagComment.value = t.comment || ''; + tagComposer.value = t.composer || ''; + tagUrl.value = t.urlTag || ''; + tagEncodedBy.value = t.encodedBy || ''; + tagEditorOverlay.style.display = 'flex'; +} +function showStatus(msg, type) { + footerStatus.textContent = msg; + footerStatus.className = type; + clearTimeout(footerStatus._timer); + footerStatus._timer = setTimeout(() => { footerStatus.textContent = ''; footerStatus.className = ''; }, 2000); +} +function saveTagEditor() { + const idx = parseInt(tagEditorOverlay.dataset.idx, 10); + if (isNaN(idx) || idx < 0 || idx >= tracks.length) { + showStatus('Error saving tags', 'error'); + return; + } + const t = tracks[idx]; + t.name = tagName.value.trim(); + t.artist = tagArtist.value.trim(); + t.album = tagAlbum.value.trim(); + t.genre = tagGenre.value.trim(); + t.year = tagYear.value.trim(); + t.track = tagTrack.value.trim(); + t.comment = tagComment.value.trim(); + t.composer = tagComposer.value.trim(); + t.urlTag = tagUrl.value.trim(); + t.encodedBy = tagEncodedBy.value.trim(); + if (idx === currentIndex) updateUI(); + renderTracks(); + tagEditorOverlay.style.display = 'none'; + showStatus('Tag information saved.', 'success'); +} +function cancelTagEditor() { + tagEditorOverlay.style.display = 'none'; +} function getSortFn(field) { switch (field) { case 'idx': return (a, b) => tracks.indexOf(a) - tracks.indexOf(b); @@ -1300,6 +1408,23 @@ addFolderBtn.addEventListener('click', addFolder); addFilesBtn.addEventListener('click', addFiles); isolateBtn.addEventListener('click', isolateTracks); deleteBtn.addEventListener('click', deleteTrack); +infoBtn.addEventListener('click', openTagEditor); +tagSaveBtn.addEventListener('click', saveTagEditor); +tagCancelBtn.addEventListener('click', cancelTagEditor); +tagEditorOverlay.addEventListener('click', (e) => { + if (e.target === tagEditorOverlay) cancelTagEditor(); +}); +tagName.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagArtist.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagAlbum.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagGenre.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagYear.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagTrack.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagComment.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagComposer.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagUrl.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +tagEncodedBy.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveTagEditor(); }); +GENRES.forEach(g => { const opt = document.createElement('option'); opt.value = g; genreList.appendChild(opt); }); sortSelect.addEventListener('change', () => { if (sortSelect.value === sortField) { sortAsc = !sortAsc; } else { sortField = sortSelect.value; sortAsc = true; }