fix(scoring): PWA installierbar + Mikrofon auf Android/Chrome

Web-App-Manifest, Service Worker und Icons für Install-Prompt;
Permissions-Policy erlaubt Mikrofon (self); getUserMedia vor Speech API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cursor
2026-07-05 15:14:45 +00:00
parent 93850eb982
commit 271684ce85
7 changed files with 146 additions and 13 deletions
+78 -11
View File
@@ -2,7 +2,16 @@
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0f1117">
<meta name="description" content="Punktestand für zwei Spieler mit Spracheingabe">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Scoring">
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" href="icons/icon-192.png" type="image/png">
<link rel="apple-touch-icon" href="icons/icon-192.png">
<title>Scoring — zwei Spieler</title>
<style>
:root {
@@ -172,6 +181,16 @@
border-color: #7f1d1d;
}
#install-btn {
display: none;
border-color: var(--p2-dim);
color: var(--p2);
}
#install-btn.visible {
display: inline-flex;
}
.status-bar {
min-height: 2.75rem;
padding: 0.5rem 1.25rem 1rem;
@@ -248,6 +267,7 @@
<span aria-hidden="true">🎤</span>
<span id="mic-label">Spracheingabe</span>
</button>
<button type="button" id="install-btn">App installieren</button>
<button type="button" id="reset-btn">Zurücksetzen</button>
</div>
@@ -256,7 +276,7 @@
<p class="hint">
Sprachbefehle (de-DE): <code>eins plus drei</code>, <code>zwei minus eins</code>,
<code>spieler eins plus fünf</code>, <code>zurücksetzen</code>.
Mikrofon-Zugriff nötig; funktioniert in Chrome/Edge/Safari (nicht Firefox).
Mikrofon-Zugriff nötig; Chrome/Edge/Safari (nicht Firefox). Als App installierbar unter Android/Chrome.
</p>
<footer>
@@ -269,6 +289,8 @@
const statusEl = document.getElementById('status');
const micBtn = document.getElementById('mic-btn');
const micLabel = document.getElementById('mic-label');
const installBtn = document.getElementById('install-btn');
let installPrompt = null;
function getName(player) {
return document.querySelector(`#player-${player} .player-name`).value.trim()
@@ -387,6 +409,17 @@
adjust(cmd.player, cmd.delta);
}
async function ensureMicPermission() {
if (!navigator.mediaDevices?.getUserMedia) return true;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach((track) => track.stop());
return true;
} catch {
return false;
}
}
function initSpeech() {
if (!SpeechRecognition) {
micBtn.classList.add('unsupported');
@@ -400,7 +433,7 @@
recognition.lang = 'de-DE';
recognition.continuous = false;
recognition.interimResults = true;
recognition.maxAlternatives = 3;
recognition.maxAlternatives = 5;
recognition.addEventListener('start', () => {
listening = true;
@@ -439,21 +472,55 @@
if (msg) setStatus(msg);
});
micBtn.addEventListener('click', () => {
micBtn.addEventListener('click', async () => {
if (listening) {
recognition.stop();
} else {
try {
recognition.start();
} catch {
recognition.stop();
setTimeout(() => recognition.start(), 200);
}
return;
}
const allowed = await ensureMicPermission();
if (!allowed) {
setStatus('Mikrofon-Zugriff verweigert — in den Browser-Einstellungen erlauben.');
return;
}
try {
recognition.start();
} catch {
recognition.stop();
setTimeout(() => {
try { recognition.start(); } catch { /* ignore */ }
}, 250);
}
});
}
function initPwa() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js', { scope: './' }).catch(() => {});
}
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
installPrompt = event;
installBtn.classList.add('visible');
});
installBtn.addEventListener('click', async () => {
if (!installPrompt) return;
installPrompt.prompt();
const { outcome } = await installPrompt.userChoice;
installPrompt = null;
installBtn.classList.remove('visible');
if (outcome === 'accepted') setStatus('App installiert.');
});
window.addEventListener('appinstalled', () => {
installBtn.classList.remove('visible');
installPrompt = null;
});
}
initSpeech();
initPwa();
</script>
</body>
</html>