remove blank linez ;-)

This commit is contained in:
Armin 2026-07-07 14:20:13 +02:00
commit 4cd4649c0a

View file

@ -5,16 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>noiz://</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #161821; color: #c6c8d1; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
.btn { background: linear-gradient(to bottom, #272c42, #1e2132); border: 1px solid #6b7089; color: #c6c8d1; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; white-space: nowrap; box-shadow: 0 3px 0 #161821, 0 4px 6px rgba(0,0,0,0.4); }
@ -71,7 +61,6 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
.playlist-toolbar .spacer { flex: 1; }
.playlist-toolbar select { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #272c42, #1e2132); border: 1px solid #6b7089; color: #c6c8d1; padding: 4px 20px 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; outline: none; box-shadow: 0 2px 0 #161821, 0 3px 4px rgba(0,0,0,0.3); transition: transform .1s, box-shadow .1s; -webkit-appearance: none; appearance: none; }
.playlist-toolbar select:hover { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #2e3348, #272c42); }
.playlist-header { display: grid; grid-template-columns: 36px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 8px 24px; font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: #6b7089; border-bottom: 1px solid #272c42; flex-shrink: 0; background: linear-gradient(to bottom, #1e2132, #161821); }
.playlist-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 2px 0; position: relative; background: #101219; }
#scanning { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(22,24,33,0.85); z-index: 10; font-size: 14px; color: #c6c8d1; flex-direction: column; gap: 10px; }
@ -122,14 +111,11 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
</style>
</head>
<body>
<div id="noiz-overlay">
<img src="noizmainlogo.png" alt="noiz://">
</div>
<div id="error"></div>
<input type="file" id="folderPicker" webkitdirectory multiple style="display:none">
<div class="content">
<div class="pinned" id="pinned">
<div class="player" id="player">
@ -250,13 +236,11 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
<button class="btn" id="deleteBtn">Delete</button>
</div>
</div>
<footer id="footer">
<span class="footer-track" id="footerTrack">No track</span>
<span id="footerTime">0:00 / 0:00</span>
<span class="brand">noiz<span>://</span></span>
</footer>
<script>
const audioElA = new Audio();
const audioElB = new Audio();
@ -274,7 +258,6 @@ let crossfadeTimer = null;
let isCrossfading = false;
let repeatMode = 'off';
let shuffleOn = false;
let audioCtx = null;
let analyser = null;
let analyserL = null;
@ -285,8 +268,6 @@ let sortAsc = true;
let windowType = 'hanning';
let peakDecay = 0.985;
let barDecay = 0.96;
function initAudioCtx() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
@ -318,16 +299,13 @@ function initAudioCtx() {
analyser.connect(audioCtx.destination);
} catch (e) {}
}
const canvas = document.getElementById('spectrum');
const ctx = canvas.getContext('2d');
const bigTime = document.getElementById('bigTime');
let peakData = [];
let smoothedData = [];
let vuPeakL = 0, vuPeakR = 0;
let vuSmoothL = 0, vuSmoothR = 0;
function bessi0(x) {
let s = 1, t = 1;
for (let k = 1; k <= 20; k++) {
@ -337,7 +315,6 @@ function bessi0(x) {
}
return s;
}
function applyWindow(data, type) {
const n = data.length;
let kaiserI0;
@ -380,7 +357,6 @@ function applyWindow(data, type) {
data[i] *= w;
}
}
function fft(re, im) {
const n = re.length;
for (let i = 1, j = 0; i < n; i++) {
@ -413,9 +389,7 @@ function fft(re, im) {
}
}
}
const SCALE_EXP = { none: 1, hanning: 0.5, hamming: 0.48, blackman: 0.4, bartlett: 0.55, gaussian: 0.43, kaiser: 0.33, flatTop: 0.3 };
function drawSpectrum() {
if (!analyser) { animId = requestAnimationFrame(drawSpectrum); return; }
const fftSize = analyser.fftSize;
@ -559,16 +533,13 @@ function drawSpectrum() {
}
animId = requestAnimationFrame(drawSpectrum);
}
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');
@ -603,9 +574,7 @@ const windowSelect = document.getElementById('windowSelect');
const decaySelect = document.getElementById('decaySelect');
const barDecaySelect = document.getElementById('barDecaySelect');
const crossfadeSelect = document.getElementById('crossfadeSelect');
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',
'New Age','Oldies','Other','Pop','R&B','Rap','Reggae','Rock','Techno','Industrial',
@ -627,20 +596,17 @@ const GENRES = [
'Christian Gangsta Rap','Heavy Metal','Black Metal','Crossover','Contemporary Christian',
'Christian Rock','Merengue','Salsa','Thrash Metal','Anime','JPop','Synthpop'
];
function fmtTime(s) {
if (!isFinite(s) || s < 0) return '0:00';
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${sec.toString().padStart(2, '0')}`;
}
function showError(msg) {
errorEl.textContent = msg;
errorEl.style.display = 'block';
setTimeout(() => { errorEl.style.display = 'none'; }, 5000);
}
async function* walkDir(dirHandle, path = '') {
for await (const entry of dirHandle.values()) {
const fullPath = path ? `${path}/${entry.name}` : entry.name;
@ -651,13 +617,11 @@ async function* walkDir(dirHandle, path = '') {
}
}
}
function isAudioFile(name) {
const dot = name.lastIndexOf('.');
if (dot === -1) return false;
return AUDIO_EXTS.has(name.slice(dot).toLowerCase());
}
function readBytes(file, start, length) {
const blob = file.slice(start, start + length);
return new Promise((resolve) => {
@ -666,7 +630,6 @@ function readBytes(file, start, length) {
r.readAsArrayBuffer(blob);
});
}
function decodeText(arr, offset, len, enc) {
const end = Math.min(offset + len, arr.length);
if (end <= offset) return '';
@ -687,7 +650,6 @@ function decodeText(arr, offset, len, enc) {
return new TextDecoder('utf-8').decode(slice).replace(/\0/g, '').trim();
} catch { return ''; }
}
function parseSyncsafeInt(bytes, off, len) {
let val = 0;
for (let i = 0; i < len; i++) {
@ -695,7 +657,6 @@ function parseSyncsafeInt(bytes, off, len) {
}
return val;
}
function parseGenre(text) {
if (!text) return '';
const m = text.match(/\((\d+)\)/);
@ -703,7 +664,6 @@ function parseGenre(text) {
if (!isNaN(text) && text !== '') return GENRES[parseInt(text)] || text;
return text;
}
async function readId3v2(file) {
try {
const hdr = await readBytes(file, 0, 10);
@ -744,7 +704,6 @@ async function readId3v2(file) {
return tags;
} catch { return null; }
}
async function readId3v1(file) {
try {
if (file.size < 128) return null;
@ -765,7 +724,6 @@ async function readId3v1(file) {
return tags;
} catch { return null; }
}
async function readTags(file) {
let tags = await readId3v2(file);
if (tags && (tags.title || tags.artist)) return tags;
@ -781,7 +739,6 @@ async function readTags(file) {
}
return tags || {};
}
const MP3_BITRATES = [
[0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,0],
[0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,0],
@ -789,22 +746,17 @@ const MP3_BITRATES = [
[0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,0],
[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0]
];
const MP3_SAMPLERATES = [44100, 48000, 32000, 0, 22050, 24000, 16000, 0, 11025, 12000, 8000, 0];
const MP3_CHANNELS = ['Stereo', 'Joint Stereo', 'Dual Channel', 'Mono'];
function isMp3Sync(b) {
return b[0] === 0xFF && (b[1] & 0xE0) === 0xE0 && ((b[1] >> 3) & 3) !== 1 && ((b[1] >> 1) & 3) !== 0;
}
function findMp3Header(data, off) {
for (let i = off; i < data.length - 4; i++) {
if (isMp3Sync(data.slice(i, i + 4))) return i;
}
return -1;
}
async function readAudioInfo(file) {
const info = {};
const ext = file.name.toLowerCase().slice(file.name.lastIndexOf('.'));
@ -860,7 +812,6 @@ async function readAudioInfo(file) {
} catch {}
return info;
}
async function processFiles(files, paths) {
scanning.style.display = 'flex';
const total = files.length;
@ -895,7 +846,6 @@ async function processFiles(files, paths) {
setTimeout(() => { scanning.style.display = 'none'; }, 800);
return found;
}
async function addFolder() {
if ('showDirectoryPicker' in window) {
try {
@ -924,7 +874,6 @@ async function addFolder() {
}
folderPicker.click();
}
folderPicker.addEventListener('change', async (e) => {
const fileList = e.target.files;
if (!fileList || fileList.length === 0) return;
@ -934,7 +883,6 @@ folderPicker.addEventListener('change', async (e) => {
addTracks(processed);
folderPicker.value = '';
});
async function addFiles() {
const input = document.createElement('input');
input.type = 'file';
@ -950,7 +898,6 @@ async function addFiles() {
});
input.click();
}
function addTracks(newTracks) {
if (newTracks.length === 0) {
showError('No audio files found.');
@ -959,7 +906,6 @@ function addTracks(newTracks) {
tracks = tracks.concat(newTracks);
renderTracks();
}
function isolateTracks() {
tracks.forEach((t, i) => {
if (i !== currentIndex) URL.revokeObjectURL(t.url);
@ -975,7 +921,6 @@ function isolateTracks() {
}
renderTracks();
}
function deleteTrack() {
if (selectedIndices.size === 0) return;
if (selectedIndices.size === tracks.length) {
@ -1006,7 +951,6 @@ function deleteTrack() {
renderTracks();
}
}
function getSortFn(field) {
switch (field) {
case 'idx': return (a, b) => tracks.indexOf(a) - tracks.indexOf(b);
@ -1018,7 +962,6 @@ function getSortFn(field) {
default: return (a, b) => tracks.indexOf(a) - tracks.indexOf(b);
}
}
function renderTracks() {
const sorted = [...tracks].sort(getSortFn(sortField));
if (!sortAsc) sorted.reverse();
@ -1120,13 +1063,11 @@ function renderTracks() {
canvas.style.display = hasT ? 'block' : 'none';
progressRow.style.display = hasT ? 'flex' : 'none';
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function playTrack(i) {
if (i < 0 || i >= tracks.length) return;
const prevIdx = currentIndex;
@ -1173,7 +1114,6 @@ function playTrack(i) {
renderTracks();
}
}
function updateUI() {
const t = tracks[currentIndex];
if (!t) return;
@ -1191,7 +1131,6 @@ function updateUI() {
else if (ai.channels) parts.push(ai.channels === 1 ? 'Mono' : 'Stereo');
audioInfo.textContent = parts.join(' \u00B7 ');
}
function onLoadedmetadata(e) {
if (e.target !== audio) return;
const t = tracks[currentIndex];
@ -1245,7 +1184,6 @@ audioElA.addEventListener('play', onPlay);
audioElB.addEventListener('play', onPlay);
audioElA.addEventListener('pause', onPause);
audioElB.addEventListener('pause', onPause);
playBtn.addEventListener('click', () => {
if (tracks.length === 0) return;
const selArr = [...selectedIndices];
@ -1262,10 +1200,8 @@ playBtn.addEventListener('click', () => {
audio.pause();
}
});
prevBtn.addEventListener('click', prevTrack);
nextBtn.addEventListener('click', nextTrack);
function prevTrack() {
if (tracks.length === 0) return;
if (audio.currentTime > 3) { audio.currentTime = 0; return; }
@ -1277,7 +1213,6 @@ function prevTrack() {
playTrack(currentIndex <= 0 ? tracks.length - 1 : currentIndex - 1);
}
}
function nextTrack() {
if (tracks.length === 0) return;
if (shuffleOn) {
@ -1290,31 +1225,26 @@ function nextTrack() {
playTrack(currentIndex >= tracks.length - 1 ? 0 : currentIndex + 1);
}
}
progressBar.addEventListener('click', (e) => {
if (!audio.duration) return;
const rect = progressBar.getBoundingClientRect();
audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration;
});
volumeSlider.addEventListener('input', () => {
const v = parseFloat(volumeSlider.value);
audioElA.volume = v;
audioElB.volume = v;
volDisplay.textContent = Math.round(v * 100);
});
addFolderBtn.addEventListener('click', addFolder);
addFilesBtn.addEventListener('click', addFiles);
isolateBtn.addEventListener('click', isolateTracks);
deleteBtn.addEventListener('click', deleteTrack);
sortSelect.addEventListener('change', () => {
if (sortSelect.value === sortField) { sortAsc = !sortAsc; }
else { sortField = sortSelect.value; sortAsc = true; }
renderTracks();
});
windowSelect.addEventListener('change', () => { windowType = windowSelect.value; });
decaySelect.addEventListener('change', () => { peakDecay = parseFloat(decaySelect.value); });
barDecaySelect.addEventListener('change', () => { barDecay = parseFloat(barDecaySelect.value); });
@ -1323,11 +1253,9 @@ crossfadeDuration = parseFloat(crossfadeSelect.value);
repeatSelect.addEventListener('change', () => { repeatMode = repeatSelect.value; });
shuffleSelect.addEventListener('change', () => { shuffleOn = shuffleSelect.value === 'on'; });
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();
@ -1335,19 +1263,16 @@ content.addEventListener('dragenter', (e) => {
content.style.outline = '2px dashed #84a0c6';
content.style.outlineOffset = '-12px';
});
content.addEventListener('dragleave', (e) => {
if (isInternalDrag) return;
e.preventDefault();
dragCounter--;
if (dragCounter <= 0) { dragCounter = 0; content.style.outline = 'none'; }
});
content.addEventListener('dragover', (e) => {
if (isInternalDrag) { e.preventDefault(); return; }
e.preventDefault();
});
content.addEventListener('drop', async (e) => {
if (isInternalDrag) return;
e.preventDefault();
@ -1386,7 +1311,6 @@ content.addEventListener('drop', async (e) => {
const processed = await processFiles(files, paths);
addTracks(processed);
});
async function collectFiles(entry, files, paths, path, onFile) {
if (entry.isFile) {
const file = await new Promise((resolve) => entry.file(resolve));
@ -1409,14 +1333,12 @@ async function collectFiles(entry, files, paths, path, onFile) {
await readAll();
}
}
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
if (e.code === 'Space') { e.preventDefault(); playBtn.click(); }
if (e.code === 'ArrowRight') nextBtn.click();
if (e.code === 'ArrowLeft') prevBtn.click();
});
window.addEventListener('focus', () => {
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
});