ui(tts): "Im Gerät" ausgrauen statt ausblenden, wenn keine Sprachstimmen

- 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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-21 01:51:37 +02:00
commit b8b2bcddd3
2 changed files with 29 additions and 10 deletions

View file

@ -60,7 +60,9 @@ let _voices = [];
function loadVoices() { _voices = TTS_SUPPORTED ? speechSynthesis.getVoices() : []; } function loadVoices() { _voices = TTS_SUPPORTED ? speechSynthesis.getVoices() : []; }
if (TTS_SUPPORTED) { if (TTS_SUPPORTED) {
loadVoices(); 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) { 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. // Vorrang: 1. lokale Wahl (localStorage), 2. Server-Pref (tts_provider), 3. Mobil-Default = Gerät.
function applyPlaybackChoice(serverProvider) { function applyPlaybackChoice(serverProvider) {
if (!ttsSel) return; 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); const local = localStorage.getItem(PLAYBACK_KEY);
if (local === "device" && TTS_SUPPORTED) { if (local === "device" && TTS_SUPPORTED) {
ttsSel.value = "device"; ttsSel.value = "device";
@ -157,6 +152,30 @@ function applyPlaybackChoice(serverProvider) {
} else if (!ttsSel.value) { } else if (!ttsSel.value) {
ttsSel.value = "piper"; // Fallback: Server 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(); syncPresetUI();
} }

View file

@ -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"> 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">
<span class="text-lg shrink-0">📱</span> <span class="text-lg shrink-0">📱</span>
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Im Gerät</span> <span class="flex-1 min-w-0"><span class="block text-sm font-medium">Im Gerät</span>
<span class="block text-xs text-slate-500 dark:text-slate-400">Das Gerät liest vor · spart Daten</span></span> <span class="tts-sub block text-xs text-slate-500 dark:text-slate-400">Das Gerät liest vor · spart Daten</span></span>
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0"></span> <span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0"></span>
</button> </button>
<button type="button" data-tts="piper" <button type="button" data-tts="piper"
@ -326,6 +326,6 @@
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div> <div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
</footer> </footer>
<script src="/app.js?v=41"></script> <script src="/app.js?v=42"></script>
</body> </body>
</html> </html>