Files
Alfredv2/alfred/app/static/js/alfred-camera-settings.js
T
2026-10-26 00:06:24 +01:00

176 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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);
});
})();