feat(04-01): add mute button with CSS, wire audio toggle in main.ts

- Add mute-btn element to index.html outside all screen sections
- Add .mute-btn CSS styles (fixed position, bottom-right, circular)
- Wire mute button click handler to toggle audioEnabled in Settings
- Initialize AudioContext on first user interaction (autoplay policy)
- Fix lint issues: import ordering, non-null assertion
This commit is contained in:
2026-03-29 18:18:31 +02:00
parent 370b76725b
commit 52596c57b0
5 changed files with 90 additions and 6 deletions
+4
View File
@@ -191,6 +191,10 @@
</div> </div>
</div> </div>
<button id="mute-btn" class="mute-btn" aria-label="Audio stumm schalten" title="Audio">
<span class="mute-btn__icon" id="mute-icon">&#x1F50A;</span>
</button>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+3 -3
View File
@@ -1,5 +1,5 @@
import { getSettings } from "../storage/db";
import { getDB } from "../app"; import { getDB } from "../app";
import { getSettings } from "../storage/db";
let audioCtx: AudioContext | null = null; let audioCtx: AudioContext | null = null;
@@ -83,12 +83,12 @@ export async function playRewardSound(): Promise<void> {
const noteDuration = 0.15; const noteDuration = 0.15;
const now = ctx.currentTime; const now = ctx.currentTime;
for (let i = 0; i < notes.length; i++) { for (const [i, freq] of notes.entries()) {
const osc = ctx.createOscillator(); const osc = ctx.createOscillator();
const gain = ctx.createGain(); const gain = ctx.createGain();
osc.type = "sine"; osc.type = "sine";
osc.frequency.value = notes[i]!; osc.frequency.value = freq;
gain.gain.value = 0.12; gain.gain.value = 0.12;
osc.connect(gain); osc.connect(gain);
+1 -1
View File
@@ -1,5 +1,5 @@
import { getSettings } from "../storage/db";
import { getDB } from "../app"; import { getDB } from "../app";
import { getSettings } from "../storage/db";
let cachedVoice: SpeechSynthesisVoice | null = null; let cachedVoice: SpeechSynthesisVoice | null = null;
+51 -2
View File
@@ -1,5 +1,7 @@
import "./styles/main.css"; 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) => { window.addEventListener("error", (event) => {
console.error("Global error:", event.error); console.error("Global error:", event.error);
@@ -13,4 +15,51 @@ window.addEventListener("unhandledrejection", (event) => {
if (overlay) overlay.style.display = "flex"; 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);
});
+31
View File
@@ -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 — kindgerechte Fehlermeldung */
.error-overlay { .error-overlay {
position: fixed; position: fixed;