Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | 'use client' import { createContext, useContext, useEffect, useRef, type ReactNode } from 'react' import { TtsAudioManager } from '@/lib/audio/TtsAudioManager' import { SubtitleOverlay } from '@/components/audio/SubtitleOverlay' const LS_KEY_ENABLED = 'audio-help-enabled' const LS_KEY_VOLUME = 'audio-help-volume' const LS_KEY_SUBTITLE_SPEED = 'audio-subtitle-speed' function readLocalStorage(key: string, fallback: string): string { if (typeof window === 'undefined') return fallback try { return localStorage.getItem(key) ?? fallback } catch { return fallback } } const AudioManagerContext = createContext<TtsAudioManager | null>(null) export function AudioManagerProvider({ children }: { children: ReactNode }) { const managerRef = useRef<TtsAudioManager | null>(null) if (!managerRef.current) { managerRef.current = new TtsAudioManager() } const manager = managerRef.current // Apply persisted enabled / volume / subtitle speed on first render useEffect(() => { const enabled = readLocalStorage(LS_KEY_ENABLED, 'true') === 'true' const volume = Number(readLocalStorage(LS_KEY_VOLUME, '80')) / 100 const subtitleDurationMultiplier = Number(readLocalStorage(LS_KEY_SUBTITLE_SPEED, '1')) || 1 manager.configure({ enabled, volume, subtitleDurationMultiplier }) }, [manager]) // Flush collected clips on visibility change and before unload useEffect(() => { const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { manager.flush() } } const handleBeforeUnload = () => { manager.flush() } document.addEventListener('visibilitychange', handleVisibilityChange) window.addEventListener('beforeunload', handleBeforeUnload) return () => { document.removeEventListener('visibilitychange', handleVisibilityChange) window.removeEventListener('beforeunload', handleBeforeUnload) } }, [manager]) // Load pre-generated clip manifest on mount useEffect(() => { let cancelled = false fetch('/api/settings/voice-chain') .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (!cancelled && data?.voiceChain) { manager.loadPregenManifest(data.voiceChain) } }) .catch(() => { // Non-fatal — browser TTS fallback works without manifest }) return () => { cancelled = true } }, [manager]) // Cleanup on unmount useEffect(() => { return () => { manager.flush() manager.dispose() } }, [manager]) return ( <AudioManagerContext.Provider value={manager}> {children} <SubtitleOverlay /> </AudioManagerContext.Provider> ) } /** * Access the raw TtsAudioManager instance. * Prefer `useAudioManager()` for reactive state. */ export function useAudioManagerInstance(): TtsAudioManager { const ctx = useContext(AudioManagerContext) if (!ctx) { throw new Error('useAudioManagerInstance must be used within AudioManagerProvider') } return ctx } |