/** * Settings Page Controller */ class SettingsPage { constructor(app) { this.app = app; this.tabs = document.querySelectorAll('.tabs .tab'); this.tabContents = document.querySelectorAll('.tab-content'); this.init(); } init() { // Tab switching this.tabs.forEach(tab => { tab.addEventListener('click', () => this.switchTab(tab.dataset.tab)); }); // Player settings this.initPlayerSettings(); // Transcoding settings this.initTranscodingSettings(); // User management (admin only) this.initUserManagement(); } initPlayerSettings() { const arrowKeysToggle = document.getElementById('setting-arrow-keys'); const overlayDurationInput = document.getElementById('setting-overlay-duration'); const defaultVolumeSlider = document.getElementById('setting-default-volume'); const volumeValueDisplay = document.getElementById('volume-value'); const rememberVolumeToggle = document.getElementById('setting-remember-volume'); const autoPlayNextToggle = document.getElementById('setting-autoplay-next'); // Load current settings if (this.app.player?.settings) { arrowKeysToggle.checked = this.app.player.settings.arrowKeysChangeChannel; overlayDurationInput.value = this.app.player.settings.overlayDuration; defaultVolumeSlider.value = this.app.player.settings.defaultVolume; volumeValueDisplay.textContent = this.app.player.settings.defaultVolume + '%'; rememberVolumeToggle.checked = this.app.player.settings.rememberVolume; autoPlayNextToggle.checked = this.app.player.settings.autoPlayNextEpisode; } // Arrow keys toggle arrowKeysToggle.addEventListener('change', () => { this.app.player.settings.arrowKeysChangeChannel = arrowKeysToggle.checked; this.app.player.saveSettings(); }); // Overlay duration overlayDurationInput.addEventListener('change', () => { this.app.player.settings.overlayDuration = parseInt(overlayDurationInput.value) || 5; this.app.player.saveSettings(); }); // Default volume slider defaultVolumeSlider.addEventListener('input', () => { const value = defaultVolumeSlider.value; volumeValueDisplay.textContent = value + '%'; this.app.player.settings.defaultVolume = parseInt(value); this.app.player.saveSettings(); }); // Remember volume toggle rememberVolumeToggle.addEventListener('change', () => { this.app.player.settings.rememberVolume = rememberVolumeToggle.checked; this.app.player.saveSettings(); }); // Auto-play next episode toggle autoPlayNextToggle.addEventListener('change', () => { this.app.player.settings.autoPlayNextEpisode = autoPlayNextToggle.checked; this.app.player.saveSettings(); }); // EPG refresh interval const epgRefreshSelect = document.getElementById('epg-refresh-interval'); if (epgRefreshSelect && this.app.player?.settings) { // Load saved value from player settings epgRefreshSelect.value = this.app.player.settings.epgRefreshInterval || '24'; // Save on change - server will restart its sync timer via PUT /api/settings epgRefreshSelect.addEventListener('change', () => { this.app.player.settings.epgRefreshInterval = epgRefreshSelect.value; this.app.player.saveSettings(); }); } // Update last refreshed display this.updateEpgLastRefreshed(); } async initTranscodingSettings() { // Encoder settings const hwEncoderSelect = document.getElementById('setting-hw-encoder'); const maxResolutionSelect = document.getElementById('setting-max-resolution'); const qualitySelect = document.getElementById('setting-quality'); // Stream processing (use -tc suffix IDs from Transcoding tab) const forceProxyToggle = document.getElementById('setting-force-proxy-tc'); const autoTranscodeToggle = document.getElementById('setting-auto-transcode-tc'); const forceTranscodeToggle = document.getElementById('setting-force-transcode-tc'); const forceVideoTranscodeToggle = document.getElementById('setting-force-video-transcode-tc'); const forceRemuxToggle = document.getElementById('setting-force-remux-tc'); const streamFormatSelect = document.getElementById('setting-stream-format-tc'); // User-Agent (Transcoding tab versions) const userAgentSelect = document.getElementById('setting-user-agent-tc'); const userAgentCustomInput = document.getElementById('setting-user-agent-custom-tc'); const customUaContainer = document.getElementById('custom-user-agent-container-tc'); // Fetch settings directly from API to avoid race condition with VideoPlayer let s; try { s = await API.settings.get(); } catch (err) { console.warn('[Settings] Failed to load settings from API, using player defaults:', err); s = this.app.player?.settings || {}; } if (hwEncoderSelect) hwEncoderSelect.value = s.hwEncoder || 'auto'; if (maxResolutionSelect) maxResolutionSelect.value = s.maxResolution || '1080p'; if (qualitySelect) qualitySelect.value = s.quality || 'medium'; if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy === true; if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode !== false; if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode === true; if (forceVideoTranscodeToggle) forceVideoTranscodeToggle.checked = s.forceVideoTranscode === true; if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false; if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8'; if (userAgentSelect) userAgentSelect.value = s.userAgentPreset || 'chrome'; if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || ''; if (customUaContainer) { customUaContainer.style.display = userAgentSelect?.value === 'custom' ? 'flex' : 'none'; } // Event listeners for encoder settings hwEncoderSelect?.addEventListener('change', () => { this.app.player.settings.hwEncoder = hwEncoderSelect.value; this.app.player.saveSettings(); }); maxResolutionSelect?.addEventListener('change', () => { this.app.player.settings.maxResolution = maxResolutionSelect.value; this.app.player.saveSettings(); }); qualitySelect?.addEventListener('change', () => { this.app.player.settings.quality = qualitySelect.value; this.app.player.saveSettings(); }); // Audio Mix Preset const audioMixSelect = document.getElementById('setting-audio-mix'); if (audioMixSelect) { audioMixSelect.value = s.audioMixPreset || 'auto'; audioMixSelect.addEventListener('change', () => { this.app.player.settings.audioMixPreset = audioMixSelect.value; this.app.player.saveSettings(); }); } // Stream processing toggles forceProxyToggle?.addEventListener('change', () => { this.app.player.settings.forceProxy = forceProxyToggle.checked; this.app.player.saveSettings(); }); autoTranscodeToggle?.addEventListener('change', () => { this.app.player.settings.autoTranscode = autoTranscodeToggle.checked; this.app.player.saveSettings(); }); forceTranscodeToggle?.addEventListener('change', () => { this.app.player.settings.forceTranscode = forceTranscodeToggle.checked; this.app.player.saveSettings(); }); forceVideoTranscodeToggle?.addEventListener('change', () => { this.app.player.settings.forceVideoTranscode = forceVideoTranscodeToggle.checked; this.app.player.saveSettings(); }); forceRemuxToggle?.addEventListener('change', () => { this.app.player.settings.forceRemux = forceRemuxToggle.checked; this.app.player.saveSettings(); }); streamFormatSelect?.addEventListener('change', () => { this.app.player.settings.streamFormat = streamFormatSelect.value; this.app.player.saveSettings(); }); // User-Agent handlers const toggleCustomInput = () => { if (customUaContainer) { customUaContainer.style.display = userAgentSelect?.value === 'custom' ? 'flex' : 'none'; } }; userAgentSelect?.addEventListener('change', () => { this.app.player.settings.userAgentPreset = userAgentSelect.value; this.app.player.saveSettings(); toggleCustomInput(); }); userAgentCustomInput?.addEventListener('change', () => { this.app.player.settings.userAgentCustom = userAgentCustomInput.value; this.app.player.saveSettings(); }); } /** * Load and display hardware info in Transcoding tab */ async loadHardwareInfo() { const container = document.getElementById('hw-info-container'); if (!container) return; try { const response = await fetch('/api/settings/hw-info'); if (!response.ok) throw new Error('Failed to fetch hardware info'); const hwInfo = await response.json(); const detected = []; // Only show detected hardware if (hwInfo.nvidia?.available) { detected.push(`
✓ NVIDIA ${hwInfo.nvidia.name}
`); } if (hwInfo.amf?.available) { detected.push(`
✓ AMD ${hwInfo.amf.name || 'Available'}
`); } if (hwInfo.qsv?.available) { detected.push(`
✓ Intel QSV Available
`); } if (hwInfo.vaapi?.available) { detected.push(`
✓ VAAPI ${hwInfo.vaapi.device || 'Available'}
`); } let html; if (detected.length > 0) { html = `
${detected.join('')}
`; html += `

Recommended encoder: ${hwInfo.recommended}

`; } else { html = `

No GPU acceleration detected. Using software encoding.

`; } container.innerHTML = html; } catch (err) { console.error('Error loading hardware info:', err); container.innerHTML = '

Failed to load hardware info

'; } } initUserManagement() { // User tab visibility is handled in show() method // when currentUser is available // Handle add user form const addUserForm = document.getElementById('add-user-form'); if (addUserForm) { addUserForm.addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('new-username').value; const password = document.getElementById('new-password').value; const role = document.getElementById('new-role').value; try { await API.users.create({ username, password, role }); alert('User created successfully!'); addUserForm.reset(); this.loadUsers(); } catch (err) { alert('Error creating user: ' + err.message); } }); } } async loadUsers() { const userList = document.getElementById('user-list'); if (!userList) return; try { const users = await API.users.getAll(); // Store users in memory for easy access during edit this.users = users; if (users.length === 0) { userList.innerHTML = 'No users found'; return; } userList.innerHTML = users.map(user => { const isSSO = !!user.oidcId; const typeBadge = isSSO ? 'SSO' : 'Local'; const roleBadge = user.role === 'admin' ? 'Admin' : 'Viewer'; return `
${user.username} ${typeBadge}
${user.email || '-'} ${roleBadge} ${user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'} `}).join(''); } catch (err) { console.error('Error loading users:', err); userList.innerHTML = 'Error loading users'; } } openEditUserModal(userId) { console.log('openEditUserModal called with ID:', userId, 'Type:', typeof userId); console.log('Current users list:', this.users); const user = this.users.find(u => u.id === userId); if (!user) { console.error('User not found in this.users cache!'); console.log('Available IDs:', this.users.map(u => u.id)); return; } console.log('User found:', user); const modal = document.getElementById('edit-user-modal'); console.log('Modal element:', modal); if (!modal) { console.error('CRITICAL: Modal element #edit-user-modal not found in DOM!'); alert('Error: Modal not found. Please refresh the page.'); return; } const isSSO = !!user.oidcId; console.log('Is SSO user:', isSSO); // Populate form with null checks try { const editId = document.getElementById('edit-user-id'); const editUsername = document.getElementById('edit-username'); const editEmail = document.getElementById('edit-email'); const editRole = document.getElementById('edit-role'); const editPassword = document.getElementById('edit-password'); console.log('Form elements found:', { editId, editUsername, editEmail, editRole, editPassword }); if (editId) editId.value = user.id; if (editUsername) editUsername.value = user.username; if (editEmail) editEmail.value = user.email || ''; if (editRole) editRole.value = user.role; if (editPassword) editPassword.value = ''; // Handle SSO specific UI const passwordHint = document.getElementById('edit-password-hint'); const oidcGroup = document.getElementById('oidc-info-group'); const oidcIdDisplay = document.getElementById('edit-oidc-id'); if (isSSO) { if (editPassword) { editPassword.disabled = true; editPassword.placeholder = "Managed by SSO Provider"; } if (passwordHint) passwordHint.textContent = "Password cannot be changed for SSO users."; if (oidcGroup) oidcGroup.classList.remove('hidden'); if (oidcIdDisplay) oidcIdDisplay.textContent = user.oidcId; } else { if (editPassword) { editPassword.disabled = false; editPassword.placeholder = "Leave blank to keep current"; } if (passwordHint) passwordHint.textContent = "Optional. Leave blank to keep unchanged."; if (oidcGroup) oidcGroup.classList.add('hidden'); } // Show modal console.log('Adding active class to modal...'); modal.classList.add('active'); console.log('Modal classes after add:', modal.classList.toString()); // Setup Close/Cancel handlers (once) this.setupModalHandlers(modal); console.log('Modal should now be visible!'); } catch (err) { console.error('Error populating modal:', err); alert('Error opening edit modal: ' + err.message); } } setupModalHandlers(modal) { if (this.modalHandlersSetup) return; const closeBtn = document.getElementById('edit-user-close'); const cancelBtn = document.getElementById('edit-user-cancel'); const saveBtn = document.getElementById('edit-user-save'); const closeModal = () => modal.classList.remove('active'); closeBtn.onclick = closeModal; cancelBtn.onclick = closeModal; // Click outside to close modal.onclick = (e) => { if (e.target === modal) closeModal(); }; // Save Handler saveBtn.onclick = async () => { const userId = document.getElementById('edit-user-id').value; const updates = { username: document.getElementById('edit-username').value, role: document.getElementById('edit-role').value }; const newPassword = document.getElementById('edit-password').value; if (newPassword && !document.getElementById('edit-password').disabled) { updates.password = newPassword; } try { await API.users.update(userId, updates); // alert('User updated successfully!'); // Optional: Replace with toast? closeModal(); this.loadUsers(); } catch (err) { alert('Error updating user: ' + err.message); } }; this.modalHandlersSetup = true; } async deleteUser(userId, username) { if (!confirm(`Are you sure you want to delete user "${username}"?`)) { return; } try { await API.users.delete(userId); this.loadUsers(); } catch (err) { alert('Error deleting user: ' + err.message); } } switchTab(tabName) { this.tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabName)); this.tabContents.forEach(c => c.classList.toggle('active', c.id === `tab-${tabName}`)); // Load content browser when switching to that tab if (tabName === 'content') { this.app.sourceManager.loadContentSources(); } // Load users when switching to users tab if (tabName === 'users') { this.loadUsers(); } // Load hardware info when switching to transcode tab if (tabName === 'transcode') { this.loadHardwareInfo(); } } async show() { // Show users tab for admin if (this.app.currentUser && this.app.currentUser.role === 'admin') { const usersTab = document.getElementById('users-tab'); if (usersTab) { usersTab.style.display = 'block'; } } // Load sources when page is shown await this.app.sourceManager.loadSources(); // Refresh ALL player settings from server if (this.app.player?.settings) { const s = this.app.player.settings; // Player settings const arrowKeysToggle = document.getElementById('setting-arrow-keys'); const overlayDurationInput = document.getElementById('setting-overlay-duration'); const defaultVolumeSlider = document.getElementById('setting-default-volume'); const volumeValueDisplay = document.getElementById('volume-value'); const rememberVolumeToggle = document.getElementById('setting-remember-volume'); const autoPlayNextToggle = document.getElementById('setting-autoplay-next'); const forceProxyToggle = document.getElementById('setting-force-proxy'); const forceTranscodeToggle = document.getElementById('setting-force-transcode'); const forceRemuxToggle = document.getElementById('setting-force-remux'); const autoTranscodeToggle = document.getElementById('setting-auto-transcode'); const epgRefreshSelect = document.getElementById('epg-refresh-interval'); const streamFormatSelect = document.getElementById('setting-stream-format'); if (arrowKeysToggle) arrowKeysToggle.checked = s.arrowKeysChangeChannel; if (overlayDurationInput) overlayDurationInput.value = s.overlayDuration; if (defaultVolumeSlider) defaultVolumeSlider.value = s.defaultVolume; if (volumeValueDisplay) volumeValueDisplay.textContent = s.defaultVolume + '%'; if (rememberVolumeToggle) rememberVolumeToggle.checked = s.rememberVolume; if (autoPlayNextToggle) autoPlayNextToggle.checked = s.autoPlayNextEpisode; if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy || false; if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode || false; if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false; if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false; if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24'; if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8'; // User-Agent settings const userAgentSelect = document.getElementById('setting-user-agent'); const userAgentCustomInput = document.getElementById('setting-user-agent-custom'); const customUaContainer = document.getElementById('custom-user-agent-container'); if (userAgentSelect) { userAgentSelect.value = s.userAgentPreset || 'chrome'; if (customUaContainer) { customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none'; } } if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || ''; } // Update EPG last refreshed display this.updateEpgLastRefreshed(); } /** * Update the EPG last refreshed display */ async updateEpgLastRefreshed() { const display = document.getElementById('epg-last-refreshed'); if (!display) return; try { // Fetch last sync time from server const response = await fetch('/api/settings/sync-status'); if (!response.ok) throw new Error('Failed to fetch sync status'); const data = await response.json(); if (data.lastSyncTime) { const lastRefreshTime = new Date(data.lastSyncTime); // Format as relative time or absolute const now = new Date(); const diffMs = now - lastRefreshTime; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); let text; if (diffMins < 1) { text = 'Just now'; } else if (diffMins < 60) { text = `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`; } else if (diffHours < 24) { text = `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`; } else { // Use absolute time for older refreshes text = lastRefreshTime.toLocaleString(); } display.textContent = text; display.title = lastRefreshTime.toLocaleString(); // Full timestamp on hover } else { display.textContent = 'Never'; display.title = 'Sync has not run yet since server started'; } } catch (err) { console.error('Error fetching sync status:', err); display.textContent = 'Unknown'; display.title = 'Could not fetch sync status'; } } hide() { // Page is hidden } } window.SettingsPage = SettingsPage;