diff --git a/index.html b/index.html index f276680..0a628c1 100644 --- a/index.html +++ b/index.html @@ -191,6 +191,10 @@ + + diff --git a/src/audio/sounds.ts b/src/audio/sounds.ts index 8be1a96..18b62fa 100644 --- a/src/audio/sounds.ts +++ b/src/audio/sounds.ts @@ -1,5 +1,5 @@ -import { getSettings } from "../storage/db"; import { getDB } from "../app"; +import { getSettings } from "../storage/db"; let audioCtx: AudioContext | null = null; @@ -83,12 +83,12 @@ export async function playRewardSound(): Promise { const noteDuration = 0.15; const now = ctx.currentTime; - for (let i = 0; i < notes.length; i++) { + for (const [i, freq] of notes.entries()) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sine"; - osc.frequency.value = notes[i]!; + osc.frequency.value = freq; gain.gain.value = 0.12; osc.connect(gain); diff --git a/src/audio/speech.ts b/src/audio/speech.ts index b503972..11ebbb4 100644 --- a/src/audio/speech.ts +++ b/src/audio/speech.ts @@ -1,5 +1,5 @@ -import { getSettings } from "../storage/db"; import { getDB } from "../app"; +import { getSettings } from "../storage/db"; let cachedVoice: SpeechSynthesisVoice | null = null; diff --git a/src/main.ts b/src/main.ts index 97fe777..4f11eea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,7 @@ import "./styles/main.css"; -import { initApp } from "./app"; +import { getDB, initApp } from "./app"; +import { initAudioOnInteraction } from "./audio/sounds"; +import { getSettings, saveSettings } from "./storage/db"; window.addEventListener("error", (event) => { console.error("Global error:", event.error); @@ -13,4 +15,51 @@ window.addEventListener("unhandledrejection", (event) => { if (overlay) overlay.style.display = "flex"; }); -document.addEventListener("DOMContentLoaded", initApp); +document.addEventListener("DOMContentLoaded", async () => { + await initApp(); + + const db = getDB(); + + // Wire mute button + const muteBtn = document.getElementById("mute-btn"); + const muteIcon = document.getElementById("mute-icon"); + if (muteBtn && muteIcon) { + // Set initial state from settings + const settings = await getSettings(db); + const audioEnabled = settings?.audioEnabled !== false; + muteIcon.textContent = audioEnabled ? "\u{1F50A}" : "\u{1F507}"; + if (!audioEnabled) { + muteBtn.classList.add("mute-btn--muted"); + } + + muteBtn.onclick = async () => { + // Initialize AudioContext on first click (autoplay policy) + initAudioOnInteraction(); + + const current = await getSettings(db); + const nowEnabled = !(current?.audioEnabled !== false); + const updated = current ?? { + id: 1 as const, + audioEnabled: false, + speechEnabled: false, + apiKey: "", + apiCallsToday: { text: 0, image: 0 }, + lastApiCallDate: "", + }; + updated.audioEnabled = nowEnabled; + await saveSettings(db, updated); + + muteIcon.textContent = nowEnabled ? "\u{1F50A}" : "\u{1F507}"; + muteBtn.classList.toggle("mute-btn--muted", !nowEnabled); + }; + } + + // Initialize AudioContext on first user interaction (autoplay policy D-02) + const initAudioOnce = () => { + initAudioOnInteraction(); + document.removeEventListener("click", initAudioOnce); + document.removeEventListener("keydown", initAudioOnce); + }; + document.addEventListener("click", initAudioOnce); + document.addEventListener("keydown", initAudioOnce); +}); diff --git a/src/styles/main.css b/src/styles/main.css index ac564d0..34a1ccb 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -964,6 +964,37 @@ h3 { } } +/* Mute button — fixed position, always accessible */ +.mute-btn { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 100; + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--bg-cream); + border: 2px solid var(--accent-green); + cursor: pointer; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease; + padding: 0; + line-height: 1; +} + +.mute-btn:hover { + transform: scale(1.1); +} + +.mute-btn--muted { + border-color: var(--text-light); + opacity: 0.6; +} + /* Error overlay — kindgerechte Fehlermeldung */ .error-overlay { position: fixed;