/** * 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'); const groupLiveChannelsToggle = document.getElementById('setting-group-live-channels'); // 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; groupLiveChannelsToggle.checked = this.app.player.settings.liveChannelListMode !== 'flat'; } // 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(); }); groupLiveChannelsToggle.addEventListener('change', () => { const mode = groupLiveChannelsToggle.checked ? 'grouped' : 'flat'; this.app.player.settings.liveChannelListMode = mode; this.app.channelList.setListMode(mode); this.app.player.saveSettings(); }); this.initLayaSettings(); this.initLiveAnalysis(); // 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(); } initLiveAnalysis() { document.getElementById('live-check-new-button')?.addEventListener('click', () => void this.runLiveAnalysis('check-new')); document.getElementById('live-rebuild-catalog-button')?.addEventListener('click', async () => { if (await this.confirmFullRefresh()) void this.runLiveAnalysis('rebuild'); }); // Compatibility with pages cached before the actions were split. document.getElementById('live-analysis-button')?.addEventListener('click', () => void this.runLiveAnalysis('rebuild')); document.getElementById('live-analysis-details-toggle')?.addEventListener('click', () => { const button = document.getElementById('live-analysis-details-toggle'); const details = document.getElementById('live-analysis-error-details'); const expanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', String(!expanded)); details.hidden = expanded; }); void this.loadLiveAnalysisStatus(); } async loadLiveAnalysisStatus() { try { this.liveAnalysisStatus = await API.live.getProjectionStatus(); } catch (err) { this.liveAnalysisStatus = { status: 'error', error: err.message }; } this.renderLiveAnalysisStatus(this.liveAnalysisStatus); const presentation = LiveAnalysisPresentation.present(this.liveAnalysisStatus); if (presentation.busy) this.startLiveAnalysisPolling(); return this.liveAnalysisStatus; } renderLiveAnalysisStatus(status = {}) { const buttons = [ document.getElementById('live-check-new-button'), document.getElementById('live-rebuild-catalog-button'), document.getElementById('live-analysis-button') ].filter(Boolean); const statusElement = document.getElementById('live-analysis-status'); const progress = document.getElementById('live-analysis-progress'); const track = document.getElementById('live-analysis-progress-track'); const bar = document.getElementById('live-analysis-progress-bar'); const progressText = document.getElementById('live-analysis-progress-text'); const fallback = document.getElementById('live-analysis-fallback'); const detailsButton = document.getElementById('live-analysis-details-toggle'); const details = document.getElementById('live-analysis-error-details'); if (!buttons.length || !statusElement) return; const presentation = LiveAnalysisPresentation.present(status); buttons.forEach(button => { button.disabled = presentation.busy; }); const rebuildButton = document.getElementById('live-rebuild-catalog-button'); const fullRefreshPhases = new Set([ 'refreshing_live_sources', 'refreshing_epg_sources', 'matching_epg', 'laya_epg_matching', 'writing_epg_matches' ]); const fullRefreshBusy = presentation.busy && (this.currentLiveAnalysisAction === 'rebuild' || fullRefreshPhases.has(presentation.phase)); if (rebuildButton) rebuildButton.textContent = fullRefreshBusy ? 'Refreshing…' : 'Run full refresh'; statusElement.textContent = presentation.statusLabel; progress.hidden = !presentation.busy; track.classList.toggle('indeterminate', presentation.progressIndeterminate); if (presentation.progressPercent === null) { track.removeAttribute('aria-valuemin'); track.removeAttribute('aria-valuemax'); track.removeAttribute('aria-valuenow'); bar.style.width = ''; } else { track.setAttribute('aria-valuemin', '0'); track.setAttribute('aria-valuemax', '100'); track.setAttribute('aria-valuenow', String(presentation.progressPercent)); bar.style.width = `${presentation.progressPercent}%`; } progressText.textContent = presentation.progressText; track.setAttribute('aria-valuetext', presentation.progressText); const mode = this.app.player?.settings?.languageDetectionMode || 'algo'; fallback.textContent = LiveAnalysisPresentation.layaFallbackMessage(mode, this.layaStatus); detailsButton.hidden = !presentation.errorDetail; detailsButton.setAttribute('aria-expanded', 'false'); details.hidden = true; details.textContent = presentation.errorDetail; } async runLiveAnalysis(action = 'rebuild') { const buttons = ['live-check-new-button', 'live-rebuild-catalog-button', 'live-analysis-button'] .map(id => document.getElementById(id)).filter(Boolean); if (buttons.some(button => button.disabled)) return; this.currentLiveAnalysisAction = action; this.liveAnalysisStatus = { ...(this.liveAnalysisStatus || {}), status: 'queued', processed: 0 }; this.renderLiveAnalysisStatus(this.liveAnalysisStatus); try { const response = action === 'check-new' ? await API.live.checkNewStreams() : await API.live.rebuildProjection(); this.liveAnalysisStatus = response.projection || (response.started ? { status: 'queued', processed: 0 } : response); this.renderLiveAnalysisStatus(this.liveAnalysisStatus); this.startLiveAnalysisPolling(); } catch (err) { this.liveAnalysisStatus = { status: 'error', error: err.message }; this.currentLiveAnalysisAction = ''; this.renderLiveAnalysisStatus(this.liveAnalysisStatus); } } confirmFullRefresh() { const titleText = 'Run a full refresh?'; const bodyText = 'Nodecast will refresh IPTV sources, re-analyze every channel and language, refresh EPG data, and rebuild all EPG matches. This may take several minutes.'; const modal = document.getElementById('modal'); const title = document.getElementById('modal-title'); const body = document.getElementById('modal-body'); const footer = document.getElementById('modal-footer'); const close = modal?.querySelector('.modal-close'); if (!modal || !title || !body || !footer || !close) { return Promise.resolve(confirm(`${titleText}\n\n${bodyText}`)); } return new Promise(resolve => { title.textContent = titleText; body.textContent = bodyText; footer.innerHTML = '' + ''; modal.classList.add('active'); const finish = confirmed => { modal.classList.remove('active'); close.onclick = null; resolve(confirmed); }; close.onclick = () => finish(false); document.getElementById('full-refresh-cancel').onclick = () => finish(false); document.getElementById('full-refresh-confirm').onclick = () => finish(true); }); } startLiveAnalysisPolling() { if (this.liveAnalysisPollTimer) return; this.liveAnalysisPollTimer = setInterval(async () => { if (this.liveAnalysisPollInFlight) return; this.liveAnalysisPollInFlight = true; try { const status = await this.loadLiveAnalysisStatus(); const presentation = LiveAnalysisPresentation.present(status); if (!presentation.busy) { clearInterval(this.liveAnalysisPollTimer); this.liveAnalysisPollTimer = null; if (presentation.complete) { await this.app.channelList?.loadChannels(); this.currentLiveAnalysisAction = ''; this.liveAnalysisStatus = status; this.renderLiveAnalysisStatus(status); } } } finally { this.liveAnalysisPollInFlight = false; } }, 1000); } initLayaSettings() { const radios = document.querySelectorAll('input[name="language-detection-mode"]'); const currentMode = this.app.player?.settings?.languageDetectionMode || 'algo'; radios.forEach(radio => { radio.checked = radio.value === currentMode; radio.addEventListener('change', () => { if (!radio.checked) return; this.app.player.settings.languageDetectionMode = radio.value; this.app.player.saveSettings(); this.app.channelList.setLanguageDetectionMode(radio.value); this.renderLayaStatus(this.layaStatus); this.renderLiveAnalysisStatus(this.liveAnalysisStatus); }); }); document.getElementById('laya-download-model')?.addEventListener('click', () => this.runLayaModelAction('download')); document.getElementById('laya-remove-model')?.addEventListener('click', () => { if (confirm('Remove local AI model?\n\nYour sources, favorites, and hidden channels will not be deleted.')) { this.runLayaModelAction('remove'); } }); document.getElementById('laya-use-standard')?.addEventListener('click', () => this.useStandardLanguageDetection()); document.getElementById('laya-check-again')?.addEventListener('click', () => void this.loadLayaStatus()); document.getElementById('laya-copy-command')?.addEventListener('click', () => void this.copyLayaStartCommand()); document.getElementById('laya-show-details')?.addEventListener('click', () => { const details = document.getElementById('laya-error-details'); const button = document.getElementById('laya-show-details'); const expanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', String(!expanded)); details.hidden = expanded; }); void this.loadLayaStatus(); } async loadLayaStatus() { try { this.layaStatus = await API.laya.getStatus(); } catch (err) { this.layaStatus = { runtimeAvailable: false, modelPresent: false, error: err.message }; } this.renderLayaStatus(this.layaStatus); if (this.layaStatus?.downloading) this.startLayaStatusPolling(); return this.layaStatus; } renderLayaStatus(status = {}) { const statusElement = document.getElementById('laya-model-status'); const modelStateElement = document.getElementById('laya-model-state'); const serviceStateElement = document.getElementById('laya-service-state'); const sizeElement = document.getElementById('laya-model-size'); const messageElement = document.getElementById('laya-model-message'); const downloadButton = document.getElementById('laya-download-model'); const removeButton = document.getElementById('laya-remove-model'); const useStandardButton = document.getElementById('laya-use-standard'); const checkAgainButton = document.getElementById('laya-check-again'); const copyCommandButton = document.getElementById('laya-copy-command'); const startCommand = document.getElementById('laya-start-command'); const progress = document.getElementById('laya-download-progress'); const progressTrack = document.getElementById('laya-progress-track'); const progressBar = document.getElementById('laya-progress-bar'); const progressText = document.getElementById('laya-progress-text'); const detailsButton = document.getElementById('laya-show-details'); const details = document.getElementById('laya-error-details'); if (!statusElement) return; const mode = this.app.player?.settings?.languageDetectionMode || 'algo'; const presentation = LayaStatusPresentation.presentLayaStatus(status, mode); statusElement.textContent = presentation.statusLabel; modelStateElement.textContent = presentation.modelLabel; serviceStateElement.textContent = presentation.serviceLabel; const size = status.sizeBytes || status.runtime?.modelSizeBytes; sizeElement.textContent = size ? `· ${this.formatBytes(size)}` : ''; downloadButton.hidden = !presentation.showDownload; downloadButton.textContent = presentation.downloadLabel; removeButton.hidden = !presentation.showRemove; useStandardButton.hidden = !presentation.showUseStandard; checkAgainButton.hidden = !presentation.showCheckAgain; copyCommandButton.hidden = !presentation.showStartCommand; startCommand.hidden = !presentation.showStartCommand; downloadButton.disabled = presentation.busy || Boolean(this.layaOperationPending); removeButton.disabled = presentation.busy || Boolean(this.layaOperationPending); progress.hidden = !presentation.showProgress; progressTrack.classList.toggle('indeterminate', presentation.progressIndeterminate); if (presentation.progressPercent !== null) { progressTrack.setAttribute('aria-valuemin', '0'); progressTrack.setAttribute('aria-valuemax', '100'); progressTrack.setAttribute('aria-valuenow', String(presentation.progressPercent)); progressBar.style.width = `${presentation.progressPercent}%`; progressText.textContent = `${presentation.progressPercent}%${presentation.bytesDownloaded !== null ? ` · ${this.formatBytes(presentation.bytesDownloaded)} downloaded` : ''}`; progressTrack.setAttribute('aria-valuetext', progressText.textContent); } else { progressTrack.removeAttribute('aria-valuemin'); progressTrack.removeAttribute('aria-valuemax'); progressTrack.removeAttribute('aria-valuenow'); progressBar.style.width = ''; progressText.textContent = presentation.bytesDownloaded !== null ? `${this.formatBytes(presentation.bytesDownloaded)} downloaded` : 'Downloading…'; progressTrack.setAttribute('aria-valuetext', progressText.textContent); } detailsButton.hidden = !presentation.errorDetail; detailsButton.setAttribute('aria-expanded', 'false'); details.hidden = true; details.textContent = presentation.errorDetail; if (presentation.errorMessage) messageElement.textContent = presentation.errorMessage; else if (presentation.availabilityMessage) messageElement.textContent = presentation.availabilityMessage; else if (this.layaOperationPending) messageElement.textContent = this.layaOperationPending; else if (mode !== 'algo' && (!status.modelPresent || !status.runtimeAvailable)) { messageElement.textContent = 'Local AI is not ready. Download the model and ensure the runtime is running. Live TV will remain available.'; } else messageElement.textContent = ''; if (this.liveAnalysisStatus) this.renderLiveAnalysisStatus(this.liveAnalysisStatus); } formatBytes(bytes) { const value = Number(bytes); if (!Number.isFinite(value) || value <= 0) return ''; const units = ['B', 'KB', 'MB', 'GB']; const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1); return `${(value / (1024 ** index)).toFixed(index > 1 ? 1 : 0)} ${units[index]}`; } async runLayaModelAction(action) { if (this.layaOperationPending) return; this.layaOperationPending = action === 'download' ? 'Downloading model…' : 'Removing model…'; this.layaStatus = { ...(this.layaStatus || {}), phase: action === 'download' ? 'preparing' : 'deleting', downloading: action === 'download' }; this.renderLayaStatus(this.layaStatus); try { this.layaStatus = action === 'download' ? await API.laya.downloadModel('multilingual') : await API.laya.removeModel(); this.layaOperationPending = ''; this.renderLayaStatus(this.layaStatus); if (this.layaStatus?.downloading) this.startLayaStatusPolling(); else await this.loadLayaStatus(); } catch (err) { this.layaStatus = { ...(this.layaStatus || {}), error: err.message }; this.layaOperationPending = ''; await this.loadLayaStatus(); } } startLayaStatusPolling() { if (this.layaPollTimer) return; this.layaPollTimer = setInterval(async () => { if (this.layaPollInFlight) return; this.layaPollInFlight = true; try { const status = await this.loadLayaStatus(); if (!status?.downloading && ['ready', 'failed', 'idle'].includes(status?.phase)) { clearInterval(this.layaPollTimer); this.layaPollTimer = null; if (status.phase === 'ready' && this.app.player.settings.languageDetectionMode !== 'algo') { this.app.channelList.setLanguageDetectionMode(this.app.player.settings.languageDetectionMode); } } } finally { this.layaPollInFlight = false; } }, 1000); } useStandardLanguageDetection() { const radio = document.querySelector('input[name="language-detection-mode"][value="algo"]'); if (radio) radio.checked = true; this.app.player.settings.languageDetectionMode = 'algo'; this.app.player.saveSettings(); this.app.channelList.setLanguageDetectionMode('algo'); this.renderLayaStatus(this.layaStatus); } async copyLayaStartCommand() { const command = 'docker compose --profile laya up -d'; const button = document.getElementById('laya-copy-command'); try { await navigator.clipboard.writeText(command); button.textContent = 'Copied'; } catch { const code = document.querySelector('#laya-start-command code'); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(code); selection.removeAllRanges(); selection.addRange(range); button.textContent = 'Command selected'; } setTimeout(() => { button.textContent = 'Copy start command'; }, 2000); } 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'); const maxDirectBitrateSelect = document.getElementById('setting-max-direct-bitrate'); const allowHardwareFallbackToggle = document.getElementById('setting-allow-hardware-fallback'); // 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 (maxDirectBitrateSelect) maxDirectBitrateSelect.value = String(s.maxDirectBitrateKbps || 0); if (allowHardwareFallbackToggle) allowHardwareFallbackToggle.checked = s.allowHardwareFallback !== false; 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(); }); } // Upscaling Settings const upscaleEnabledToggle = document.getElementById('setting-upscale-enabled'); const upscaleMethodSelect = document.getElementById('setting-upscale-method'); const upscaleTargetSelect = document.getElementById('setting-upscale-target'); const upscaleMethodContainer = document.getElementById('upscale-method-container'); const upscaleTargetContainer = document.getElementById('upscale-target-container'); // Helper to toggle upscale options visibility const toggleUpscaleOptions = (enabled) => { if (upscaleMethodContainer) upscaleMethodContainer.style.display = enabled ? 'flex' : 'none'; if (upscaleTargetContainer) upscaleTargetContainer.style.display = enabled ? 'flex' : 'none'; }; // Load upscaling settings if (upscaleEnabledToggle) { upscaleEnabledToggle.checked = s.upscaleEnabled || false; toggleUpscaleOptions(upscaleEnabledToggle.checked); } if (upscaleMethodSelect) upscaleMethodSelect.value = s.upscaleMethod || 'hardware'; if (upscaleTargetSelect) upscaleTargetSelect.value = s.upscaleTarget || '1080p'; // Upscaling event handlers upscaleEnabledToggle?.addEventListener('change', () => { this.app.player.settings.upscaleEnabled = upscaleEnabledToggle.checked; this.app.player.saveSettings(); toggleUpscaleOptions(upscaleEnabledToggle.checked); }); upscaleMethodSelect?.addEventListener('change', () => { this.app.player.settings.upscaleMethod = upscaleMethodSelect.value; this.app.player.saveSettings(); }); upscaleTargetSelect?.addEventListener('change', () => { this.app.player.settings.upscaleTarget = upscaleTargetSelect.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(); }); maxDirectBitrateSelect?.addEventListener('change', () => { this.app.player.settings.maxDirectBitrateKbps = parseInt(maxDirectBitrateSelect.value, 10) || 0; this.app.player.saveSettings(); }); allowHardwareFallbackToggle?.addEventListener('change', () => { this.app.player.settings.allowHardwareFallback = allowHardwareFallbackToggle.checked; 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(`
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 = '