'use strict'; const api = (path, opts = {}) => fetch(path, opts).then(r => r.json()).catch(() => null); // ── State ───────────────────────────────────────────────────────────────────── let currentVolume = 50; // 0–100 (Windows system volume) let pollTimer = null; // ── DOM refs ────────────────────────────────────────────────────────────────── const $ = id => document.getElementById(id); const statusDot = $('winamp-status'); const stateLabel = $('state-label'); const trackTitle = $('track-title'); const playlistPos = $('playlist-pos'); const progressFill = $('progress-fill'); const timeCurrent = $('time-current'); const timeLength = $('time-length'); const volumeFill = $('volume-fill'); const volumePct = $('volume-pct'); const btnMute = $('btn-mute'); const btnPlay = $('btn-play'); const killistPanel = $('killist-panel'); const killistItems = $('killist-items'); // ── Playback controls ───────────────────────────────────────────────────────── btnPlay.addEventListener('click', async () => { const st = await api('/api/status'); if (!st) return; if (st.state === 'playing') { await api('/api/pause', { method: 'POST' }); } else { await api('/api/play', { method: 'POST' }); } poll(); }); $('btn-stop').addEventListener('click', async () => { await api('/api/stop', { method: 'POST' }); poll(); }); $('btn-next').addEventListener('click', async () => { await api('/api/next', { method: 'POST' }); poll(); }); $('btn-prev').addEventListener('click', async () => { await api('/api/prev', { method: 'POST' }); poll(); }); // ── Seek buttons ────────────────────────────────────────────────────────────── document.querySelectorAll('.btn-seek').forEach(btn => { btn.addEventListener('click', async () => { const delta = parseInt(btn.dataset.delta, 10); await api(`/api/seek?delta=${delta}`, { method: 'POST' }); poll(); }); }); // ── Progress bar click-to-seek ──────────────────────────────────────────────── $('progress-bar').addEventListener('click', async e => { const rect = e.currentTarget.getBoundingClientRect(); const frac = (e.clientX - rect.left) / rect.width; const st = await api('/api/status'); if (!st || !st.length) return; const target = Math.round(frac * st.length); const delta = target - st.position; await api(`/api/seek?delta=${delta}`, { method: 'POST' }); poll(); }); // ── Volume ──────────────────────────────────────────────────────────────────── $('btn-vol-up').addEventListener('click', async () => { currentVolume = Math.min(100, currentVolume + 5); await api(`/api/volume?level=${currentVolume}`, { method: 'POST' }); updateVolumeFill(); }); $('btn-vol-down').addEventListener('click', async () => { currentVolume = Math.max(0, currentVolume - 5); await api(`/api/volume?level=${currentVolume}`, { method: 'POST' }); updateVolumeFill(); }); $('volume-bar').addEventListener('click', async e => { const rect = e.currentTarget.getBoundingClientRect(); currentVolume = Math.round((e.clientX - rect.left) / rect.width * 100); await api(`/api/volume?level=${currentVolume}`, { method: 'POST' }); updateVolumeFill(); }); btnMute.addEventListener('click', async () => { const cur = await api('/api/mute'); const newMuted = !(cur?.muted); await api(`/api/mute?muted=${newMuted}`, { method: 'POST' }); updateMuteBtn(newMuted); }); function updateVolumeFill(muted = false) { volumeFill.style.width = currentVolume + '%'; volumePct.textContent = currentVolume + ' %'; volumeFill.classList.toggle('muted', muted); } function updateMuteBtn(muted) { btnMute.textContent = muted ? '🔇' : '🔊'; btnMute.classList.toggle('muted', muted); volumeFill.classList.toggle('muted', muted); } // ── KillList ────────────────────────────────────────────────────────────────── $('btn-kill').addEventListener('click', async () => { const res = await api('/api/killist', { method: 'POST' }); if (res?.added) { showToast(`🚫 ${res.added}`); } }); $('btn-show-killist').addEventListener('click', async () => { await refreshKillist(); killistPanel.classList.remove('hidden'); }); $('btn-close-killist').addEventListener('click', () => { killistPanel.classList.add('hidden'); }); async function refreshKillist() { const list = await api('/api/killist'); if (!list) return; killistItems.innerHTML = ''; list.forEach(title => { const li = document.createElement('li'); li.innerHTML = `${escHtml(title)}`; const btn = document.createElement('button'); btn.textContent = '✕'; btn.onclick = async () => { await api(`/api/killist?title=${encodeURIComponent(title)}`, { method: 'DELETE' }); await refreshKillist(); }; li.appendChild(btn); killistItems.appendChild(li); }); } // ── Polling ─────────────────────────────────────────────────────────────────── async function poll() { const st = await api('/api/status'); if (!st) { statusDot.className = 'err'; statusDot.textContent = '●'; stateLabel.textContent = 'Keine Verbindung'; trackTitle.textContent = '–'; return; } if (!st.running) { statusDot.className = 'err'; stateLabel.textContent = 'Winamp nicht gestartet'; trackTitle.textContent = '–'; return; } statusDot.className = 'ok'; const stateMap = { playing: '▶ Spielt', paused: '⏸ Pause', stopped: '⏹ Stop' }; stateLabel.textContent = stateMap[st.state] ?? st.state; trackTitle.textContent = st.title || '–'; playlistPos.textContent = st.playlist_length ? `${st.playlist_pos} / ${st.playlist_length}` : ''; if (st.length > 0) { progressFill.style.width = (st.position / st.length * 100).toFixed(1) + '%'; timeCurrent.textContent = fmtTime(st.position); timeLength.textContent = fmtTime(st.length); } else { progressFill.style.width = '0%'; } // Reflect play/pause state on button btnPlay.textContent = st.state === 'playing' ? '⏸' : '▶'; // System volume (always present in status response) if (typeof st.volume === 'number') { currentVolume = st.volume; updateVolumeFill(st.muted); updateMuteBtn(st.muted); } } function startPolling(intervalMs = 2000) { if (pollTimer) clearInterval(pollTimer); poll(); pollTimer = setInterval(poll, intervalMs); } // ── Helpers ─────────────────────────────────────────────────────────────────── function fmtTime(secs) { const m = Math.floor(secs / 60); const s = String(Math.floor(secs % 60)).padStart(2, '0'); return `${m}:${s}`; } function escHtml(str) { return str.replace(/&/g,'&').replace(//g,'>'); } let toastTimer; function showToast(msg) { let el = document.getElementById('toast'); if (!el) { el = document.createElement('div'); el.id = 'toast'; el.style.cssText = ` position:fixed;bottom:24px;left:50%;transform:translateX(-50%); background:#333;color:#fff;padding:10px 20px;border-radius:8px; font-size:14px;z-index:999;opacity:0;transition:opacity .2s; `; document.body.appendChild(el); } el.textContent = msg; el.style.opacity = '1'; clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.style.opacity = '0'; }, 2500); } // ── Boot ────────────────────────────────────────────────────────────────────── startPolling(2000);