131 lines
6.7 KiB
HTML
131 lines
6.7 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Alfred — Configuration audio</title>
|
||
<style>
|
||
:root { color-scheme: dark; font-family: Inter, system-ui, -apple-system, sans-serif; background:#15171a; color:#f3f4f6; }
|
||
* { box-sizing:border-box; }
|
||
body { margin:0; min-height:100vh; display:grid; place-items:center; padding:28px; }
|
||
main { width:min(100%,640px); }
|
||
header { margin-bottom:30px; }
|
||
h1 { font-size:1.8rem; font-weight:550; margin:0 0 8px; }
|
||
.subtitle { color:#a5a9b0; margin:0; }
|
||
.panel { background:#202328; border:1px solid #34383f; border-radius:18px; padding:26px; }
|
||
.section { padding:20px 0; }
|
||
.section:first-child { padding-top:0; }
|
||
.section:last-child { padding-bottom:0; }
|
||
.section + .section { border-top:1px solid #353940; }
|
||
.line { display:flex; align-items:center; justify-content:space-between; gap:18px; }
|
||
.label { display:flex; align-items:center; gap:12px; font-size:1.05rem; }
|
||
.icon { width:24px; height:24px; color:#c9cdd3; flex-shrink:0; }
|
||
.value { font-variant-numeric:tabular-nums; color:#c8cbd1; }
|
||
input[type=range] { width:100%; margin:23px 0 2px; accent-color:#e5e7eb; cursor:pointer; }
|
||
input[type=range]:disabled { opacity:.35; cursor:not-allowed; }
|
||
.switch { position:relative; width:54px; height:30px; display:inline-block; flex-shrink:0; }
|
||
.switch input { opacity:0; width:0; height:0; position:absolute; }
|
||
.track { position:absolute; inset:0; background:#525760; border-radius:99px; transition:background .2s; cursor:pointer; }
|
||
.track:before { content:""; position:absolute; width:22px; height:22px; top:4px; left:4px; background:white; border-radius:50%; transition:transform .2s; }
|
||
.switch input:checked + .track { background:#ad5555; }
|
||
.switch input:checked + .track:before { transform:translateX(24px); }
|
||
.switch input:focus-visible + .track { outline:2px solid white; outline-offset:3px; }
|
||
.hint { margin:9px 0 0 36px; color:#9298a2; font-size:.88rem; }
|
||
#status { min-height:1.3em; margin:18px 0 0; color:#a5a9b0; font-size:.86rem; }
|
||
a { color:#cdd2da; text-decoration:none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<header>
|
||
<h1>Configuration</h1>
|
||
<p class="subtitle">Alfred · Paramètres audio</p>
|
||
</header>
|
||
<div class="panel">
|
||
<section class="section">
|
||
<div class="line">
|
||
<label class="label" for="volume">
|
||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M19 5a10 10 0 0 1 0 14"/></svg>
|
||
Volume de sortie
|
||
</label>
|
||
<output class="value" id="volumeValue" for="volume">50 %</output>
|
||
</div>
|
||
<input id="volume" type="range" min="0" max="100" value="50" step="1" aria-label="Volume de sortie">
|
||
</section>
|
||
<section class="section">
|
||
<div class="line">
|
||
<span class="label" id="speakerLabel">
|
||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="m17 9 5 6m0-6-5 6"/></svg>
|
||
Couper le haut-parleur
|
||
</span>
|
||
<label class="switch"><input type="checkbox" id="speakerMute" aria-labelledby="speakerLabel"><span class="track"></span></label>
|
||
</div>
|
||
<p class="hint">Désactive les réponses vocales d’Alfred.</p>
|
||
</section>
|
||
<section class="section">
|
||
<div class="line">
|
||
<span class="label" id="micLabel">
|
||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5m-4 0h8"/></svg>
|
||
Couper le microphone
|
||
</span>
|
||
<label class="switch"><input type="checkbox" id="micMute" aria-labelledby="micLabel"><span class="track"></span></label>
|
||
</div>
|
||
<p class="hint">Suspend l’écoute et la détection du mot « Alfred ».</p>
|
||
</section>
|
||
</div>
|
||
<p id="status" role="status" aria-live="polite"></p>
|
||
</main>
|
||
<script>
|
||
'use strict';
|
||
// API attendue côté Flask :
|
||
// GET /api/audio/settings -> {"volume": 50, "speaker_muted": false, "microphone_muted": false}
|
||
// PATCH /api/audio/settings <- mêmes clés, partielles acceptées.
|
||
// Le navigateur n'agit PAS directement sur ALSA ou Vosk.
|
||
const volume = document.getElementById('volume');
|
||
const volumeValue = document.getElementById('volumeValue');
|
||
const speakerMute = document.getElementById('speakerMute');
|
||
const micMute = document.getElementById('micMute');
|
||
const status = document.getElementById('status');
|
||
let volumeTimer;
|
||
function setStatus(message) { status.textContent = message; }
|
||
function render(data) {
|
||
if (typeof data.volume === 'number') volume.value = data.volume;
|
||
if (typeof data.speaker_muted === 'boolean') speakerMute.checked = data.speaker_muted;
|
||
if (typeof data.microphone_muted === 'boolean') micMute.checked = data.microphone_muted;
|
||
volumeValue.textContent = `${volume.value} %`;
|
||
}
|
||
async function save(change) {
|
||
try {
|
||
const response = await fetch('/api/audio/settings', {
|
||
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(change)
|
||
});
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
render(await response.json());
|
||
setStatus('Paramètres appliqués.');
|
||
} catch (error) {
|
||
setStatus('Impossible d’appliquer le réglage : API Flask non disponible ou erreur serveur.');
|
||
console.error(error);
|
||
}
|
||
}
|
||
volume.addEventListener('input', () => {
|
||
volumeValue.textContent = `${volume.value} %`;
|
||
clearTimeout(volumeTimer);
|
||
volumeTimer = setTimeout(() => save({ volume: Number(volume.value) }), 180);
|
||
});
|
||
speakerMute.addEventListener('change', () => save({ speaker_muted: speakerMute.checked }));
|
||
micMute.addEventListener('change', () => save({ microphone_muted: micMute.checked }));
|
||
(async () => {
|
||
try {
|
||
const response = await fetch('/api/audio/settings');
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
render(await response.json());
|
||
setStatus('');
|
||
} catch (_) {
|
||
setStatus('Interface prête. Connexion à l’API audio Flask nécessaire pour contrôler le matériel.');
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|