diff --git a/media_server/static/js/app.js b/media_server/static/js/app.js index 0362fb1..1b82c81 100644 --- a/media_server/static/js/app.js +++ b/media_server/static/js/app.js @@ -22,7 +22,7 @@ import { initTheme, toggleTheme, initAccentColor, applyAccentColor, renderAccentSwatches, selectAccentColor, toggleAccentPicker, lightenColor, toggleVinylMode, applyVinylMode, - visualizerEnabled, visualizerAvailable, + visualizerEnabled, visualizerAvailable, setVisualizerEnabled, checkVisualizerAvailability, toggleVisualizer, applyVisualizerMode, loadAudioDevices, onAudioDeviceChanged, setupProgressDrag, updateUI, updatePlaybackState, stopPositionInterpolation, @@ -186,14 +186,13 @@ window.addEventListener('DOMContentLoaded', async () => { // Initialize audio visualizer — auto-enable when supported so the // spectrum shows real audio out of the box. checkVisualizerAvailability().then(() => { - if (visualizerAvailable && !visualizerEnabled) { - // Auto-enable on first install if loopback capture works. - if (localStorage.getItem('visualizerEnabled') === null) { - localStorage.setItem('visualizerEnabled', 'true'); - } - } - if ((visualizerEnabled || localStorage.getItem('visualizerEnabled') === 'true') - && visualizerAvailable) { + if (!visualizerAvailable) return; + // First install: opt the user in by default since the spectrum + // is the centerpiece of the player view. + const stored = localStorage.getItem('visualizerEnabled'); + const shouldEnable = stored === null ? true : stored === 'true'; + if (shouldEnable) { + setVisualizerEnabled(true); // updates the let in player.js applyVisualizerMode(); } }); diff --git a/media_server/static/js/player.js b/media_server/static/js/player.js index f84b1a9..910b194 100644 --- a/media_server/static/js/player.js +++ b/media_server/static/js/player.js @@ -277,6 +277,10 @@ function updateVinylSpin() { // Audio Visualizer export let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true'; export let visualizerAvailable = false; +export function setVisualizerEnabled(value) { + visualizerEnabled = !!value; + localStorage.setItem('visualizerEnabled', visualizerEnabled); +} let visualizerCtx = null; let visualizerAnimFrame = null; export let frequencyData = null; @@ -506,13 +510,24 @@ export async function loadAudioDevices() { select.appendChild(opt); } - if (status.current_device) { + // Prefer server-reported device; fall back to the last user choice + // saved in localStorage (so reloads persist even if the server + // forgets between restarts). + const savedDevice = localStorage.getItem('audioDevice') || ''; + const targetDevice = status.current_device || savedDevice; + let pendingPushToServer = false; + if (targetDevice) { for (let i = 0; i < select.options.length; i++) { - if (select.options[i].value === status.current_device) { + if (select.options[i].value === targetDevice) { select.selectedIndex = i; break; } } + // If the saved device wasn't on the server, push it back so + // capture starts on the right one. + if (!status.current_device && savedDevice) { + pendingPushToServer = true; + } } // Enhance with icon grid @@ -545,6 +560,19 @@ export async function loadAudioDevices() { ws.send(JSON.stringify({ type: 'enable_visualizer' })); } } + + // If the user's previously-chosen device wasn't recognized by + // the server (e.g. server restart cleared in-memory state), + // push it back so capture lands on the right one. + if (pendingPushToServer && savedDevice) { + try { + await fetch('/api/media/visualizer/device', { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, + body: JSON.stringify({ device_name: savedDevice }) + }); + } catch (_) { /* best-effort */ } + } } catch (e) { section.style.display = 'none'; } @@ -572,6 +600,13 @@ export async function onAudioDeviceChanged() { const deviceName = select.value || null; + // Persist locally so reloads survive even if the server doesn't. + if (deviceName) { + localStorage.setItem('audioDevice', deviceName); + } else { + localStorage.removeItem('audioDevice'); + } + try { const resp = await fetch('/api/media/visualizer/device', { method: 'POST',