From b8b2bcddd3c838345d93d0cda057df280f89635a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Sun, 21 Jun 2026 01:51:37 +0200 Subject: [PATCH] =?UTF-8?q?ui(tts):=20"Im=20Ger=C3=A4t"=20ausgrauen=20stat?= =?UTF-8?q?t=20ausblenden,=20wenn=20keine=20Sprachstimmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refreshDevicePreset(): graut das "Im Gerät"-Preset aus (disabled + opacity + Hinweis "keine Sprachstimmen im Browser"), wenn getVoices() leer ist; entgraut automatisch bei voiceschanged (+ 1,5s-Backstop). - Kein Aufzwingen: device wird nur (wieder) gewählt, wenn es die gespeicherte Präferenz ist; ohne Stimmen wird die aktive Wahl auf piper gesetzt (Präferenz bleibt erhalten). - Server-Fallback bleibt für fehlende Einzelsprachen. Co-Authored-By: Claude Opus 4.8 --- app/web/app.js | 35 +++++++++++++++++++++++++++-------- app/web/index.html | 4 ++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/web/app.js b/app/web/app.js index d3d6ae5..a7ce676 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -60,7 +60,9 @@ let _voices = []; function loadVoices() { _voices = TTS_SUPPORTED ? speechSynthesis.getVoices() : []; } if (TTS_SUPPORTED) { loadVoices(); - speechSynthesis.addEventListener("voiceschanged", loadVoices); + // Stimmen laden asynchron -> bei Eintreffen Preset-Zustand neu bewerten (entgrauen). + speechSynthesis.addEventListener("voiceschanged", () => { loadVoices(); refreshDevicePreset(); }); + setTimeout(() => refreshDevicePreset(), 1500); // Backstop, falls kein voiceschanged kommt } function pickVoice(bcp) { @@ -139,13 +141,6 @@ const PLAYBACK_KEY = "va-playback"; // geräte-lokal: "device" wenn Geräte-TTS // Vorrang: 1. lokale Wahl (localStorage), 2. Server-Pref (tts_provider), 3. Mobil-Default = Gerät. function applyPlaybackChoice(serverProvider) { if (!ttsSel) return; - // "Im Gerät"-Preset ausblenden, wenn der Browser keine Web Speech API hat. - if (!TTS_SUPPORTED) { - const opt = ttsSel.querySelector('option[value="device"]'); - if (opt) opt.remove(); - const btn = document.querySelector('.tts-preset[data-tts="device"]'); - if (btn) btn.classList.add("hidden"); - } const local = localStorage.getItem(PLAYBACK_KEY); if (local === "device" && TTS_SUPPORTED) { ttsSel.value = "device"; @@ -157,6 +152,30 @@ function applyPlaybackChoice(serverProvider) { } else if (!ttsSel.value) { ttsSel.value = "piper"; // Fallback: Server } + refreshDevicePreset(); // graut „Im Gerät" aus, wenn keine Stimmen + syncPresetUI +} + +// Graut „Im Gerät" aus, wenn keine Sprachstimmen vorhanden sind; entgraut automatisch, +// sobald welche da sind. Kein Aufzwingen: device wird nur (wieder) gewählt, wenn es die +// gespeicherte Präferenz ist. Ohne Stimmen NICHT auf der deaktivierten Option sitzen bleiben. +function refreshDevicePreset() { + const btn = document.querySelector('.tts-preset[data-tts="device"]'); + if (!btn || !ttsSel) return; + const ready = deviceVoicesReady(); + btn.disabled = !ready; + btn.classList.toggle("opacity-50", !ready); + btn.classList.toggle("cursor-not-allowed", !ready); + const sub = btn.querySelector(".tts-sub"); + if (sub) sub.textContent = ready + ? "Das Gerät liest vor · spart Daten" + : "keine Sprachstimmen im Browser"; + if (ready) { + if (localStorage.getItem(PLAYBACK_KEY) === "device" && ttsSel.value !== "device") { + ttsSel.value = "device"; // gespeicherte Präferenz wieder aktivieren + } + } else if (ttsSel.value === "device") { + ttsSel.value = "piper"; // Server statt deaktivierter Option (Präferenz bleibt) + } syncPresetUI(); } diff --git a/app/web/index.html b/app/web/index.html index f0c5a89..fafb5b0 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -261,7 +261,7 @@ class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"> 📱 Im Gerät - Das Gerät liest vor · spart Daten + Das Gerät liest vor · spart Daten