Fix speech playback on iOS Safari
Build and publish frontend / publish (push) Successful in 44s

This commit is contained in:
Ein Anderssono
2026-10-15 18:24:45 +02:00
parent 4cb41cdeca
commit 09341e2486
5 changed files with 146 additions and 15 deletions
+33 -7
View File
@@ -37,7 +37,8 @@
<section id="auth" aria-labelledby="auth-heading">
<h1 id="auth-heading">Svensk livssimulator</h1>
<p id="auth-intro">Välkommen till en svensk, dialogbaserad livssimulator utan bilder. Världen beskrivs med ljud, dofter, temperatur, beröring och riktning. Skapa konto med ett användarnamn och ett lösenord på minst tolv tecken. Berätta sedan fritt om en vuxen person. Du får redigera och godkänna personens biografi och startplats. Spela genom att skriva eller tala vad du gör och frågar. Personer du möter kan minnas tidigare samtal. Spelets repliker läses automatiskt upp på svenska och finns kvar som text. Har du redan konto väljer du Logga in.</p>
<button type="button" id="auth-repeat-intro">Läs introduktionen igen</button>
<button type="button" id="auth-repeat-intro">Läs introduktionen</button>
<p id="auth-voice-status" role="status" aria-live="polite">På iPhone och iPad kan du behöva trycka på Läs introduktionen igen en gång för att aktivera uppläsningen.</p>
<p id="auth-status" role="status" aria-live="polite" hidden></p>
<form id="auth-form">
<label>Användarnamn <input name="username" required minlength="3" maxlength="40" autocomplete="username"></label>
@@ -91,6 +92,7 @@
const voiceSettingsKey = 'ai-spel.voice-settings.v2';
let voiceSettings = window.GameVoice.loadSettings(localStorage, voiceSettingsKey, 'ai-spel.voice-settings.v1');
let voiceController;
let speechActivatedByGesture = false;
let audioContext;
let voiceDisclosureAcknowledged = false;
let adminOffset = 0;
@@ -149,7 +151,11 @@
? 'Jag kunde inte behandla det just nu. Försök igen om en liten stund.'
: code || 'Ett tillfälligt fel uppstod. Försök igen om en liten stund.');
}
function setVoiceStatus(message = '') { document.querySelector('#voice-status').textContent = message; }
function setVoiceStatus(message = '') {
document.querySelector('#voice-status').textContent = message;
const authVoiceStatus = document.querySelector('#auth-voice-status');
if (authVoiceStatus && !auth.hidden && message) authVoiceStatus.textContent = message;
}
function refreshMicrophone() {
const button = document.querySelector('#microphone');
if (button) button.disabled = play.hidden || messageField.disabled || submitBusy;
@@ -191,6 +197,12 @@
return submitMessage(text, 'voice');
},
});
function primeSpeechFromGesture(event) {
if (speechActivatedByGesture || !event.isTrusted) return;
speechActivatedByGesture = voiceController.activateFromUserGesture();
}
document.addEventListener('pointerdown', primeSpeechFromGesture, true);
document.addEventListener('keydown', primeSpeechFromGesture, true);
persistVoiceSettings();
function addVoiceButton(parent, label, handler) {
@@ -319,18 +331,32 @@
adminView.hidden = false;
document.querySelector('#version-footer').hidden = true;
}
const introductionKey = 'ai-spel.introduction-heard.v1';
const introductionKey = 'ai-spel.introduction-heard.v2';
function playFirstVisitIntroduction() {
let alreadyHeard = false;
try { alreadyHeard = localStorage.getItem(introductionKey) === 'true'; } catch (_) { /* Keep the intro available for this visit. */ }
if (alreadyHeard) return;
const introduction = document.querySelector('#auth-intro').textContent;
if (voiceController.speak(introduction, {role: 'narrator', voiceIdentity: 'neutral'})) {
try { localStorage.setItem(introductionKey, 'true'); } catch (_) { /* The page text remains available. */ }
const isAppleMobile = /iPad|iPhone|iPod/i.test(navigator.userAgent)
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isAppleMobile) {
document.querySelector('#auth-repeat-intro').textContent = 'Läs introduktionen';
return;
}
const introduction = document.querySelector('#auth-intro').textContent;
voiceController.speak(introduction, {
role: 'narrator', voiceIdentity: 'neutral',
onStart() { try { localStorage.setItem(introductionKey, 'true'); } catch (_) { /* The page text remains available. */ } },
});
}
document.querySelector('#auth-repeat-intro').addEventListener('click', () => {
if (location.pathname !== '/admin') voiceController.speak(document.querySelector('#auth-intro').textContent, {role: 'narrator', voiceIdentity: 'neutral'});
if (location.pathname !== '/admin') {
voiceController.activateFromUserGesture();
const introduction = document.querySelector('#auth-intro').textContent;
voiceController.speak(introduction, {
role: 'narrator', voiceIdentity: 'neutral',
onStart() { try { localStorage.setItem(introductionKey, 'true'); } catch (_) { /* The page text remains available. */ } },
});
}
});
function explainAuthError(error, intent) {
if (intent === 'register' && error?.message === 'username_unavailable') return 'Det användarnamnet används redan. Välj ett annat och försök igen.';