// ============================================================ // Main: Initialization orchestrator (loaded last) // ============================================================ window.addEventListener('DOMContentLoaded', async () => { // Cache DOM references cacheDom(); // Initialize theme and accent color initTheme(); initAccentColor(); // Register service worker for PWA installability if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(() => {}); } // Initialize vinyl mode applyVinylMode(); // Initialize audio visualizer checkVisualizerAvailability().then(() => { if (visualizerEnabled && visualizerAvailable) { applyVisualizerMode(); } }); // Initialize locale (async - loads JSON file) await initLocale(); // Load version from health endpoint fetchVersion(); const token = localStorage.getItem('media_server_token'); if (token) { connectWebSocket(token); loadScripts(); loadScriptsTable(); loadCallbacksTable(); loadLinksTable(); loadAudioDevices(); } else { showAuthForm(); } // Shared volume slider setup (avoids duplicate handler code) function setupVolumeSlider(sliderId) { const slider = document.getElementById(sliderId); slider.addEventListener('input', (e) => { isUserAdjustingVolume = true; const volume = parseInt(e.target.value); // Sync both sliders and displays dom.volumeDisplay.textContent = `${volume}%`; dom.miniVolumeDisplay.textContent = `${volume}%`; dom.volumeSlider.value = volume; dom.miniVolumeSlider.value = volume; if (volumeUpdateTimer) clearTimeout(volumeUpdateTimer); volumeUpdateTimer = setTimeout(() => { setVolume(volume); volumeUpdateTimer = null; }, VOLUME_THROTTLE_MS); }); slider.addEventListener('change', (e) => { if (volumeUpdateTimer) { clearTimeout(volumeUpdateTimer); volumeUpdateTimer = null; } const volume = parseInt(e.target.value); setVolume(volume); setTimeout(() => { isUserAdjustingVolume = false; }, VOLUME_RELEASE_DELAY_MS); }); } setupVolumeSlider('volume-slider'); setupVolumeSlider('mini-volume-slider'); // Restore saved tab (migrate old tab names) let savedTab = localStorage.getItem('activeTab') || 'player'; if (['scripts', 'callbacks', 'links'].includes(savedTab)) savedTab = 'settings'; switchTab(savedTab); // Snap indicator to initial position without animation const initialActiveBtn = document.querySelector('.tab-btn.active'); if (initialActiveBtn) updateTabIndicator(initialActiveBtn, false); // Re-position tab indicator on window resize window.addEventListener('resize', () => { const activeBtn = document.querySelector('.tab-btn.active'); if (activeBtn) updateTabIndicator(activeBtn, false); }); // Mini Player: Intersection Observer to show/hide when main player scrolls out of view const playerContainer = document.querySelector('.player-container'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (activeTab !== 'player') return; setMiniPlayerVisible(!entry.isIntersecting); }); }, { threshold: 0.1 }); observer.observe(playerContainer); // Drag-to-seek for progress bars setupProgressDrag( document.getElementById('mini-progress-bar'), document.getElementById('mini-progress-fill') ); setupProgressDrag( document.getElementById('progress-bar'), document.getElementById('progress-fill') ); // Enter key in token input document.getElementById('token-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { authenticate(); } }); // Script form dirty state tracking const scriptForm = document.getElementById('scriptForm'); scriptForm.addEventListener('input', () => { scriptFormDirty = true; }); scriptForm.addEventListener('change', () => { scriptFormDirty = true; }); // Callback form dirty state tracking const callbackForm = document.getElementById('callbackForm'); callbackForm.addEventListener('input', () => { callbackFormDirty = true; }); callbackForm.addEventListener('change', () => { callbackFormDirty = true; }); // Script dialog backdrop click to close const scriptDialog = document.getElementById('scriptDialog'); scriptDialog.addEventListener('click', (e) => { // Check if click is on the backdrop (not the dialog content) if (e.target === scriptDialog) { closeScriptDialog(); } }); // Callback dialog backdrop click to close const callbackDialog = document.getElementById('callbackDialog'); callbackDialog.addEventListener('click', (e) => { // Check if click is on the backdrop (not the dialog content) if (e.target === callbackDialog) { closeCallbackDialog(); } }); // Delegated click handlers for script table actions (XSS-safe) document.getElementById('scriptsTableBody').addEventListener('click', (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; const name = btn.dataset.scriptName; if (action === 'execute') executeScriptDebug(name); else if (action === 'edit') showEditScriptDialog(name); else if (action === 'delete') deleteScriptConfirm(name); }); // Delegated click handlers for callback table actions (XSS-safe) document.getElementById('callbacksTableBody').addEventListener('click', (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; const name = btn.dataset.callbackName; if (action === 'execute') executeCallbackDebug(name); else if (action === 'edit') showEditCallbackDialog(name); else if (action === 'delete') deleteCallbackConfirm(name); }); // Link dialog backdrop click to close const linkDialog = document.getElementById('linkDialog'); linkDialog.addEventListener('click', (e) => { if (e.target === linkDialog) { closeLinkDialog(); } }); // Delegated click handlers for link table actions (XSS-safe) document.getElementById('linksTableBody').addEventListener('click', (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; const name = btn.dataset.linkName; if (action === 'edit') showEditLinkDialog(name); else if (action === 'delete') deleteLinkConfirm(name); }); // Track link form dirty state const linkForm = document.getElementById('linkForm'); linkForm.addEventListener('input', () => { linkFormDirty = true; }); linkForm.addEventListener('change', () => { linkFormDirty = true; }); // Initialize browser toolbar and load folders initBrowserToolbar(); if (token) { loadMediaFolders(); } // Icon preview for script and link dialogs setupIconPreview('scriptIcon', 'scriptIconPreview'); setupIconPreview('linkIcon', 'linkIconPreview'); // Settings sections: restore collapse state and persist on toggle document.querySelectorAll('.settings-section').forEach(details => { const key = `settings_section_${details.querySelector('summary')?.getAttribute('data-i18n') || ''}`; const saved = localStorage.getItem(key); if (saved === 'closed') details.removeAttribute('open'); else if (saved === 'open') details.setAttribute('open', ''); details.addEventListener('toggle', () => { localStorage.setItem(key, details.open ? 'open' : 'closed'); }); }); // Cleanup blob URLs on page unload window.addEventListener('beforeunload', () => { thumbnailCache.forEach(url => URL.revokeObjectURL(url)); thumbnailCache.clear(); }); // Tab bar keyboard navigation (WAI-ARIA Tabs pattern) document.getElementById('tabBar').addEventListener('keydown', (e) => { const tabs = Array.from(document.querySelectorAll('.tab-btn')); const currentIdx = tabs.indexOf(document.activeElement); if (currentIdx === -1) return; let newIdx; if (e.key === 'ArrowRight') { newIdx = (currentIdx + 1) % tabs.length; } else if (e.key === 'ArrowLeft') { newIdx = (currentIdx - 1 + tabs.length) % tabs.length; } else if (e.key === 'Home') { newIdx = 0; } else if (e.key === 'End') { newIdx = tabs.length - 1; } else { return; } e.preventDefault(); tabs[newIdx].focus(); switchTab(tabs[newIdx].dataset.tab); }); // Global keyboard shortcuts document.addEventListener('keydown', (e) => { // Skip when typing in inputs, textareas, selects, or when a dialog is open const tag = e.target.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; if (document.querySelector('dialog[open]')) return; switch (e.key) { case ' ': e.preventDefault(); togglePlayPause(); break; case 'ArrowLeft': e.preventDefault(); if (currentDuration > 0) seek(Math.max(0, currentPosition - 5)); break; case 'ArrowRight': e.preventDefault(); if (currentDuration > 0) seek(Math.min(currentDuration, currentPosition + 5)); break; case 'ArrowUp': e.preventDefault(); setVolume(Math.min(100, parseInt(dom.volumeSlider.value) + 5)); break; case 'ArrowDown': e.preventDefault(); setVolume(Math.max(0, parseInt(dom.volumeSlider.value) - 5)); break; case 'm': case 'M': toggleMute(); break; } }); });