|
|
|
@@ -35,6 +35,12 @@ let rafId = null; |
|
|
|
let lastVizAt = 0; // timestamp of last received viz frame |
|
|
|
let winampPlaying = false; |
|
|
|
|
|
|
|
// Canvas display mode — cycles on click: viz → actual → remaining → viz |
|
|
|
const VIZ_MODES = ['viz', 'actual', 'remaining']; |
|
|
|
let vizMode = 'viz'; |
|
|
|
let currentPosition = 0; |
|
|
|
let currentLength = 0; |
|
|
|
|
|
|
|
// ── WebSocket ───────────────────────────────────────────────────────────────── |
|
|
|
function connect() { |
|
|
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws'; |
|
|
|
@@ -94,6 +100,8 @@ function applyStatus(st) { |
|
|
|
} |
|
|
|
|
|
|
|
if (st.length > 0) { |
|
|
|
currentPosition = st.position; |
|
|
|
currentLength = st.length; |
|
|
|
progressFill.style.width = (st.position / st.length * 100).toFixed(1) + '%'; |
|
|
|
timeCurrent.textContent = fmtTime(st.position); |
|
|
|
timeLength.textContent = '-' + fmtTime(st.length - st.position); |
|
|
|
@@ -202,6 +210,10 @@ $('btn-close-killist').addEventListener('click', () => |
|
|
|
killistPanel.classList.add('hidden')); |
|
|
|
|
|
|
|
// ── Visualisation (Canvas) ──────────────────────────────────────────────────── |
|
|
|
canvas.addEventListener('click', () => { |
|
|
|
vizMode = VIZ_MODES[(VIZ_MODES.indexOf(vizMode) + 1) % VIZ_MODES.length]; |
|
|
|
}); |
|
|
|
|
|
|
|
function applyViz(bars) { |
|
|
|
if (!bars || bars.length === 0) return; |
|
|
|
lastBars = new Float32Array(bars); |
|
|
|
@@ -226,13 +238,37 @@ function renderFrame(ts = 0) { |
|
|
|
const h = cssH; |
|
|
|
if (w === 0 || h === 0) return; |
|
|
|
|
|
|
|
// Check if real viz data is fresh (< 1.5 s old) and Winamp is playing. |
|
|
|
const hasSignal = winampPlaying && (performance.now() - lastVizAt) < 1500; |
|
|
|
|
|
|
|
// Background |
|
|
|
ctx2d.fillStyle = '#000'; |
|
|
|
ctx2d.fillRect(0, 0, w, h); |
|
|
|
|
|
|
|
// ── Time display modes ────────────────────────────────────────────────────── |
|
|
|
if (vizMode === 'actual' || vizMode === 'remaining') { |
|
|
|
const isActual = vizMode === 'actual'; |
|
|
|
const secs = isActual ? currentPosition : Math.max(0, currentLength - currentPosition); |
|
|
|
const prefix = isActual ? '' : '-'; |
|
|
|
const timeStr = prefix + fmtTimeLong(secs); |
|
|
|
const label = isActual ? 'ELAPSED' : 'REMAINING'; |
|
|
|
|
|
|
|
ctx2d.textAlign = 'center'; |
|
|
|
ctx2d.textBaseline = 'middle'; |
|
|
|
|
|
|
|
// Large time |
|
|
|
ctx2d.font = `bold ${Math.round(h * 0.52)}px monospace`; |
|
|
|
ctx2d.fillStyle = isActual ? '#e0e0e0' : '#e94560'; |
|
|
|
ctx2d.fillText(timeStr, w / 2, h * 0.48); |
|
|
|
|
|
|
|
// Small label below |
|
|
|
ctx2d.font = `${Math.round(h * 0.18)}px monospace`; |
|
|
|
ctx2d.fillStyle = '#444'; |
|
|
|
ctx2d.fillText(label, w / 2, h * 0.82); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// ── Spectrum mode ─────────────────────────────────────────────────────────── |
|
|
|
// Check if real viz data is fresh (< 1.5 s old) and Winamp is playing. |
|
|
|
const hasSignal = winampPlaying && (performance.now() - lastVizAt) < 1500; |
|
|
|
|
|
|
|
const n = NUM_BARS; |
|
|
|
const gap = 1; |
|
|
|
const barW = Math.max(1, (w - gap * (n - 1)) / n); |
|
|
|
@@ -296,6 +332,15 @@ function fmtTime(secs) { |
|
|
|
return `${m}:${s}`; |
|
|
|
} |
|
|
|
|
|
|
|
// Like fmtTime but always zero-pads to hh:mm:ss for the canvas display. |
|
|
|
function fmtTimeLong(secs) { |
|
|
|
secs = Math.floor(secs); |
|
|
|
const h = Math.floor(secs / 3600); |
|
|
|
const m = Math.floor((secs % 3600) / 60); |
|
|
|
const s = secs % 60; |
|
|
|
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; |
|
|
|
} |
|
|
|
|
|
|
|
function parseTime(str) { |
|
|
|
const [m, s] = (str || '0:00').split(':').map(Number); |
|
|
|
return m * 60 + (s || 0); |
|
|
|
|