// ============================================================ // Display Brightness, Power, Contrast, Input Source, Color Preset, // Picture Mode Control + Links Management // ============================================================ import { t, showToast, escapeHtml, closeDialog, showConfirm, resolveMdiIcons, fetchMdiIcon, getAuthHeaders, hasCredentials } from './core.js'; import { IconSelect } from './icon-select.js'; let displayBrightnessTimers = {}; let displayContrastTimers = {}; let _displayIconSelects = []; const DISPLAY_THROTTLE_MS = 50; // ─── Icon palette for the tuning IconSelects ─────────────────────────── // All SVGs are 24x24 monochrome — IconSelect's CSS fills them with currentColor. const ICON_PORT_GENERIC = ''; const ICON_PORT_HDMI = ''; const ICON_PORT_DP = ''; const ICON_PORT_DVI = ''; const ICON_PORT_VGA = ''; const ICON_PORT_USBC = ''; const ICON_THERMOMETER = ''; const ICON_MODE_MOVIE = ''; const ICON_MODE_GAME = ''; const ICON_MODE_SPORT = ''; const ICON_MODE_PRO = ''; const ICON_MODE_DOCS = ''; const ICON_MODE_USER = ''; const ICON_MODE_DEFAULT = ''; const ICON_MODE_MIXED = ''; function inputSourceIcon(src) { const s = String(src || '').toUpperCase(); if (s.startsWith('HDMI')) return ICON_PORT_HDMI; if (s.startsWith('DP')) return ICON_PORT_DP; if (s.startsWith('DVI')) return ICON_PORT_DVI; if (s.startsWith('VGA')) return ICON_PORT_VGA; if (s.startsWith('USB')) return ICON_PORT_USBC; return ICON_PORT_GENERIC; } function pictureModeIcon(label) { const k = String(label || '').toLowerCase(); if (k.includes('movie')) return ICON_MODE_MOVIE; if (k.includes('game')) return ICON_MODE_GAME; if (k.includes('sport')) return ICON_MODE_SPORT; if (k.includes('professional')) return ICON_MODE_PRO; if (k.includes('productivity')) return ICON_MODE_DOCS; if (k.includes('user')) return ICON_MODE_USER; if (k.includes('mixed')) return ICON_MODE_MIXED; return ICON_MODE_DEFAULT; } // Humanise enum-style identifiers returned by monitorcontrol so users // don't see SHOUT_CASE strings in the UI. function humanizeInputSource(raw) { if (!raw) return ''; // OFF / RESERVED → "Off" / "Reserved" // VGA1 → "VGA 1", HDMI1 → "HDMI 1", DP1 → "DisplayPort 1" const map = { DP: 'DisplayPort', DVI: 'DVI', HDMI: 'HDMI', VGA: 'VGA', USBC: 'USB-C' }; const m = String(raw).toUpperCase().match(/^(DP|DVI|HDMI|VGA|USBC|USB_C)(\d*)$/); if (m) { const key = m[1] === 'USB_C' ? 'USBC' : m[1]; return `${map[key]}${m[2] ? ' ' + m[2] : ''}`; } return String(raw) .replace(/_/g, ' ') .toLowerCase() .replace(/\b\w/g, c => c.toUpperCase()); } function humanizeColorPreset(raw) { if (!raw) return ''; // COLOR_TEMP_6500K → "6500 K", COLOR_TEMP_NATIVE → "Native", // COLOR_TEMP_USER1 → "User 1" const s = String(raw).replace(/^COLOR_TEMP_?/i, ''); const kelvin = s.match(/^(\d{4,5})K?$/); if (kelvin) return `${kelvin[1]} K`; const user = s.match(/^USER\s*_?(\d+)$/i); if (user) return `User ${user[1]}`; return s .replace(/_/g, ' ') .toLowerCase() .replace(/\b\w/g, c => c.toUpperCase()); } export async function loadDisplayMonitors() { if (!hasCredentials()) return; const container = document.getElementById('displayMonitors'); if (!container) return; try { const response = await fetch('/api/display/monitors', { headers: getAuthHeaders() }); if (!response.ok) { container.innerHTML = `

Failed to load monitors

`; return; } const monitors = await response.json(); if (monitors.length === 0) { container.innerHTML = `

No monitors detected

`; return; } // Destroy IconSelects from a previous render so listeners + popups // don't pile up. _displayIconSelects.forEach(inst => { try { inst.destroy(); } catch (_) {} }); _displayIconSelects = []; container.innerHTML = ''; const pendingIconSelects = []; monitors.forEach(monitor => { const card = document.createElement('div'); card.className = 'display-monitor-card'; card.id = `monitor-card-${monitor.id}`; const brightnessValue = monitor.brightness !== null ? monitor.brightness : 0; const brightnessDisabled = monitor.brightness === null ? 'disabled' : ''; let powerBtn = ''; if (monitor.power_supported) { // Inline onclick with string-interpolated monitor.name is a DOM-XSS // foot-gun if the OS ever reports a name containing quotes / angle // brackets. Use a delegated click handler bound to data-* attrs. powerBtn = ` `; } const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' \u00B7 '); const detailsHtml = details ? `${escapeHtml(details)}` : ''; const primaryBadge = monitor.is_primary ? ` ` : ''; // Contrast (DDC/CI) — render only if the monitor reports it. let contrastRow = ''; if (monitor.contrast_supported) { const contrastValue = monitor.contrast !== null && monitor.contrast !== undefined ? monitor.contrast : 50; contrastRow = `
${t('display.contrast')} ${contrastValue}%
`; } // Build the picture-tuning selects (input source / color preset / picture mode). const tuningRows = []; // Each tuning field renders a hidden ${options} `); tuningTargets.push({ kind: 'input', monitorId: monitor.id, items: monitor.available_input_sources.map(src => ({ value: src, icon: inputSourceIcon(src), label: humanizeInputSource(src), })), }); } if (monitor.color_preset_supported && monitor.available_color_presets.length > 0) { const current = monitor.color_preset; const options = monitor.available_color_presets.map(p => { const selected = p === current ? 'selected' : ''; return ``; }).join(''); tuningRows.push(`
${t('display.color_preset')}
`); tuningTargets.push({ kind: 'color', monitorId: monitor.id, items: monitor.available_color_presets.map(p => ({ value: p, icon: ICON_THERMOMETER, label: humanizeColorPreset(p), })), }); } if (monitor.picture_mode_supported && monitor.available_picture_modes.length > 0) { const current = monitor.picture_mode_code; const options = monitor.available_picture_modes.map(m => { const selected = m.code === current ? 'selected' : ''; return ``; }).join(''); tuningRows.push(`
${t('display.picture_mode')}
`); tuningTargets.push({ kind: 'mode', monitorId: monitor.id, items: monitor.available_picture_modes.map(m => ({ value: String(m.code), icon: pictureModeIcon(m.label), label: m.label, })), }); } pendingIconSelects.push(...tuningTargets); const tuningBlock = tuningRows.length > 0 ? `
${t('display.tuning')}
${tuningRows.join('')}
` : ''; card.innerHTML = `
${escapeHtml(monitor.name)}${primaryBadge} ${detailsHtml}
${powerBtn}
${t('display.brightness')} ${brightnessValue}%
${contrastRow} ${tuningBlock}`; container.appendChild(card); }); // Bind a single delegated click handler for the power buttons, // plus input/change handlers for the brightness & contrast sliders. // Avoids inline on* attributes (blocked by script-src 'self' CSP). container.removeEventListener('click', _onPowerButtonClick); container.addEventListener('click', _onPowerButtonClick); container.removeEventListener('input', _onDisplaySliderInput); container.addEventListener('input', _onDisplaySliderInput); container.removeEventListener('change', _onDisplaySliderChange); container.addEventListener('change', _onDisplaySliderChange); // Enhance every tuning