/* Réglages matériels de webcam : curseurs générés selon ce que V4L2 expose. */ (() => { 'use strict'; const pane = document.getElementById('audioSettingsPanel'); const tabAudio = document.getElementById('audioTab'); const tabCamera = document.getElementById('cameraTab'); const sectionAudio = document.getElementById('audioSettingsSection'); const sectionCamera = document.getElementById('cameraSettingsSection'); const items = document.getElementById('cameraSettingsItems'); const message = document.getElementById('cameraSettingsMessage'); const reload = document.getElementById('cameraSettingsReload'); const reset = document.getElementById('cameraSettingsReset'); if (!pane || !tabAudio || !tabCamera || !items) return; const labels = { brightness: 'Luminosité', contrast: 'Contraste', saturation: 'Saturation', hue: 'Teinte', gamma: 'Gamma', gain: 'Gain vidéo', sharpness: 'Netteté', backlight_compensation: 'Compensation contre-jour', power_line_frequency: 'Fréquence anti-scintillement', white_balance_automatic: 'Balance des blancs automatique', white_balance_temperature: 'Température des blancs (K)', auto_exposure: 'Exposition', exposure_time_absolute: 'Temps d’exposition', exposure_dynamic_framerate: 'Fréquence d’image dynamique' }; const groups = [ {name: 'Image', keys: ['brightness','contrast','saturation','sharpness','hue','gamma','gain','backlight_compensation']}, {name: 'Éclairage et couleurs', keys: ['white_balance_automatic','white_balance_temperature','power_line_frequency']}, {name: 'Exposition', keys: ['auto_exposure','exposure_time_absolute','exposure_dynamic_framerate']} ]; const exposureChoices = {1: 'Manuelle', 3: 'Automatique'}; const powerChoices = {0: 'Désactivé', 1: '50 Hz', 2: '60 Hz'}; let working = false; function showTab(which) { const camera = which === 'camera'; tabCamera.setAttribute('aria-selected', String(camera)); tabAudio.setAttribute('aria-selected', String(!camera)); sectionCamera.hidden = !camera; sectionAudio.hidden = camera; if (camera) load(); } tabAudio.addEventListener('click', () => showTab('audio')); tabCamera.addEventListener('click', () => showTab('camera')); document.getElementById('audioSettingsButton')?.addEventListener('click', () => { if (!pane.hidden) showTab('audio'); }); async function api(path, payload) { const opts = {cache: 'no-store'}; if (payload !== undefined) { opts.method = 'POST'; opts.headers = {'Content-Type':'application/json', 'X-Alfred-Request':'1'}; opts.body = JSON.stringify(payload); } const res = await fetch(path, opts); const data = await res.json(); if (!res.ok) throw new Error(data.error || `Erreur HTTP ${res.status}`); return data; } function elt(tag, klass, text) { const e = document.createElement(tag); if (klass) e.className = klass; if (text !== undefined) e.textContent = text; return e; } function controlRow(name, meta) { const row = elt('div', 'camera-control'); if (meta.inactive) row.classList.add('is-inactive'); const header = elt('div','setting-row'); const label = elt('label', null, labels[name] || name); const output = elt('output','camera-control-value',String(meta.value ?? '—')); header.append(label, output); row.appendChild(header); let input; if (meta.kind === 'bool' || meta.kind === 'menu') { input = elt('select','camera-select'); let opts; if (meta.kind === 'bool') opts = [{value:1,label:'Activé'},{value:0,label:'Désactivé'}]; else if (name === 'auto_exposure') opts = [{value:3,label:'Automatique'},{value:1,label:'Manuelle'}]; else if (name === 'power_line_frequency') opts = Object.entries(powerChoices).map(([value,label])=>({value:Number(value),label})); else opts = meta.menu || []; const allowed = meta.menu?.length && meta.kind === 'menu' ? new Set(meta.menu.map(i=>i.value)) : null; for (const opt of opts) { if (allowed && !allowed.has(opt.value)) continue; const option = elt('option',null,opt.label); option.value = String(opt.value); input.appendChild(option); } input.value = String(meta.value); const updateLabel = () => { output.textContent = input.options[input.selectedIndex]?.text || String(input.value); }; updateLabel(); input.addEventListener('change', () => save(name, Number(input.value))); } else { input = elt('input','camera-range'); input.type = 'range'; input.min = String(meta.min); input.max = String(meta.max); input.step = String(meta.step || 1); input.value = String(meta.value); input.addEventListener('input', () => { output.textContent = input.value; }); // Appliquer au relâchement : évite des centaines de commandes shell. input.addEventListener('change', () => save(name, Number(input.value))); } input.id = `v4l-${name}`; label.htmlFor = input.id; input.disabled = Boolean(meta.inactive || working); row.appendChild(input); if (meta.inactive) row.appendChild(elt('div','camera-disabled-hint', 'Désactive le mode automatique pour régler cette valeur.')); return row; } function render(data) { items.replaceChildren(); const available = data.controls || {}; for (const group of groups) { const found = group.keys.filter(name => available[name]); if (!found.length) continue; items.appendChild(elt('h3','camera-group-title',group.name)); for (const name of found) items.appendChild(controlRow(name,available[name])); } if (!items.children.length) items.appendChild(elt('p','camera-settings-note','Aucun réglage V4L2 disponible.')); } async function load() { if (working) return; message.textContent = 'Lecture des réglages caméra…'; try { render(await api('/api/camera/settings')); message.textContent = ''; } catch (err) { message.textContent = err.message; items.replaceChildren(); } } async function save(control, value) { if (working) return; working = true; message.textContent = 'Application…'; try { const result = await api('/api/camera/settings', {control, value}); working = false; render(result); message.textContent = 'Réglage appliqué et mémorisé.'; } catch (err) { message.textContent = err.message; working = false; await load(); return; } working = false; } reload.addEventListener('click', load); reset.addEventListener('click', async () => { if (working || !window.confirm('Rétablir tous les réglages caméra d’origine ?')) return; working = true; message.textContent = 'Restauration en cours…'; try { const result = await api('/api/camera/reset', {}); working = false; render(result); message.textContent = 'Valeurs d’usine restaurées.'; } catch (err) { message.textContent = err.message; } finally { working = false; } }); window.alfredSocket?.on('camera_settings', data => { if (!sectionCamera.hidden && !working) render(data); }); })();