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:
@@ -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">🔊</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-3
@@ -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
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user