Add tag editor with 10 fields in 2-column layout and footer status display

- Add tag editor overlay/modal with Name, Artist, Album, Genre, Year,
  Track No., Comment, Composer, URL, and Encoded By fields in 2-column grid
- Add Info button to open tag editor
- Add footer status display with success/error messaging
- Extend track model with comment, composer, urlTag, encodedBy fields
- Rename Track field to Track No.
This commit is contained in:
Armin 2026-07-08 04:11:59 +02:00
commit 1a933585d2

View file

@ -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
<div class="add-bar">
<button class="btn btn-primary" id="addFolderBtn">+ Folder</button>
<button class="btn" id="addFilesBtn">+ Files</button>
<button class="btn" id="infoBtn">Info</button>
<button class="btn" id="isolateBtn">Isolate</button>
<button class="btn" id="deleteBtn">Delete</button>
</div>
</div>
<div id="tagEditorOverlay">
<div id="tagEditorModal">
<h3>Edit Tags</h3>
<div class="tag-editor-fields">
<label>Name <input type="text" id="tagName"></label>
<label>Artist <input type="text" id="tagArtist"></label>
<label>Album <input type="text" id="tagAlbum"></label>
<label>Genre <input type="text" id="tagGenre" list="genreList"></label>
<label>Year <input type="text" id="tagYear"></label>
<label>Track No. <input type="text" id="tagTrack"></label>
<label>Comment <input type="text" id="tagComment"></label>
<label>Composer <input type="text" id="tagComposer"></label>
<label>URL <input type="text" id="tagUrl"></label>
<label>Encoded By <input type="text" id="tagEncodedBy"></label>
</div>
<datalist id="genreList"></datalist>
<div class="tag-editor-buttons">
<button class="btn btn-primary" id="tagSaveBtn">Save</button>
<button class="btn" id="tagCancelBtn">Cancel</button>
</div>
</div>
</div>
<footer id="footer">
<span class="footer-track" id="footerTrack">No track</span>
<span id="footerStatus"></span>
<span id="footerTime">0:00 / 0:00</span>
<span class="brand">noiz<span>://</span></span>
</footer>
@ -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; }