All files / web/src/contexts AudioManagerContext.tsx

0% Statements 0/101
0% Branches 0/1
0% Functions 0/1
0% Lines 0/101

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
}