mirror of
https://codeberg.org/armin/noiz.git
synced 2026-09-01 05:50:52 +02:00
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:
parent
e85de87728
commit
1a933585d2
1 changed files with 126 additions and 1 deletions
127
index.html
127
index.html
|
|
@ -17,7 +17,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
.brand { font-size: 15px; font-weight: 700; color: #6b7089; letter-spacing: 1px; user-select: none; }
|
.brand { font-size: 15px; font-weight: 700; color: #6b7089; letter-spacing: 1px; user-select: none; }
|
||||||
.brand span { color: #6b7089; }
|
.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; }
|
footer .brand { margin-left: auto; }
|
||||||
.content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
.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; }
|
.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 { 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)); }
|
#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; } }
|
@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; }
|
#timeRemaining { color: #6b7089; }
|
||||||
#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; }
|
||||||
|
|
@ -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-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; }
|
.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; }
|
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); }
|
#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) {
|
@media (max-width: 700px) {
|
||||||
.player { flex-direction: column; align-items: stretch; }
|
.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">
|
<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="infoBtn">Info</button>
|
||||||
<button class="btn" id="isolateBtn">Isolate</button>
|
<button class="btn" id="isolateBtn">Isolate</button>
|
||||||
<button class="btn" id="deleteBtn">Delete</button>
|
<button class="btn" id="deleteBtn">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<footer id="footer">
|
||||||
<span class="footer-track" id="footerTrack">No track</span>
|
<span class="footer-track" id="footerTrack">No track</span>
|
||||||
|
<span id="footerStatus"></span>
|
||||||
<span id="footerTime">0:00 / 0:00</span>
|
<span id="footerTime">0:00 / 0:00</span>
|
||||||
<span class="brand">noiz<span>://</span></span>
|
<span class="brand">noiz<span>://</span></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
@ -566,6 +601,7 @@ const timeRemaining = document.getElementById('timeRemaining');
|
||||||
const progressRow = document.getElementById('progressRow');
|
const progressRow = document.getElementById('progressRow');
|
||||||
const footerTrack = document.getElementById('footerTrack');
|
const footerTrack = document.getElementById('footerTrack');
|
||||||
const footerTime = document.getElementById('footerTime');
|
const footerTime = document.getElementById('footerTime');
|
||||||
|
const footerStatus = document.getElementById('footerStatus');
|
||||||
const volumeSlider = document.getElementById('volumeSlider');
|
const volumeSlider = document.getElementById('volumeSlider');
|
||||||
const volDisplay = document.getElementById('volDisplay');
|
const volDisplay = document.getElementById('volDisplay');
|
||||||
const audioInfo = document.getElementById('audioInfo');
|
const audioInfo = document.getElementById('audioInfo');
|
||||||
|
|
@ -574,6 +610,21 @@ const windowSelect = document.getElementById('windowSelect');
|
||||||
const decaySelect = document.getElementById('decaySelect');
|
const decaySelect = document.getElementById('decaySelect');
|
||||||
const barDecaySelect = document.getElementById('barDecaySelect');
|
const barDecaySelect = document.getElementById('barDecaySelect');
|
||||||
const crossfadeSelect = document.getElementById('crossfadeSelect');
|
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 AUDIO_EXTS = new Set(['.mp3','.wav','.ogg','.flac','.m4a','.aac','.wma','.aiff','.alac','.opus']);
|
||||||
const GENRES = [
|
const GENRES = [
|
||||||
'Blues','Classic Rock','Country','Dance','Disco','Funk','Grunge','Hip-Hop','Jazz','Metal',
|
'Blues','Classic Rock','Country','Dance','Disco','Funk','Grunge','Hip-Hop','Jazz','Metal',
|
||||||
|
|
@ -887,6 +938,10 @@ async function processFiles(files, paths) {
|
||||||
genre: tags.genre || '',
|
genre: tags.genre || '',
|
||||||
year: tags.year || '',
|
year: tags.year || '',
|
||||||
track: tags.track || '',
|
track: tags.track || '',
|
||||||
|
comment: tags.comment || '',
|
||||||
|
composer: tags.composer || '',
|
||||||
|
urlTag: tags.urlTag || '',
|
||||||
|
encodedBy: tags.encodedBy || '',
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
url,
|
url,
|
||||||
file,
|
file,
|
||||||
|
|
@ -1006,6 +1061,59 @@ function deleteTrack() {
|
||||||
renderTracks();
|
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) {
|
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);
|
||||||
|
|
@ -1300,6 +1408,23 @@ addFolderBtn.addEventListener('click', addFolder);
|
||||||
addFilesBtn.addEventListener('click', addFiles);
|
addFilesBtn.addEventListener('click', addFiles);
|
||||||
isolateBtn.addEventListener('click', isolateTracks);
|
isolateBtn.addEventListener('click', isolateTracks);
|
||||||
deleteBtn.addEventListener('click', deleteTrack);
|
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', () => {
|
sortSelect.addEventListener('change', () => {
|
||||||
if (sortSelect.value === sortField) { sortAsc = !sortAsc; }
|
if (sortSelect.value === sortField) { sortAsc = !sortAsc; }
|
||||||
else { sortField = sortSelect.value; sortAsc = true; }
|
else { sortField = sortSelect.value; sortAsc = true; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue