Profil: Begrüßung in Zielsprache, Kategorie-Punkte-Übersicht, ruhigerer Header (kein rotierender Avatar/Online-Dot), Notch-Fix und kompaktere Aktivitäts-Heatmap. Außerdem Capacitor-iOS-Projekt und diverse Auth/Feed/Audio-Verbesserungen aus dem Premium-Redesign. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.9 KiB
JavaScript
76 lines
2.9 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
|
|
|
// Nur eine Stimme gleichzeitig im Feed
|
|
let currentAudio = null
|
|
|
|
// Reines Vorlesen wird gedrosselt (User-Wunsch). Pitch bleibt durch den
|
|
// Browser-Default `preservesPitch` erhalten.
|
|
const PLAYBACK_RATE = 0.7
|
|
|
|
export default function usePairAudio(url) {
|
|
const audioRef = useRef(null)
|
|
const rafRef = useRef(null)
|
|
const failedRef = useRef(false)
|
|
const [playing, setPlaying] = useState(false)
|
|
const [currentTime, setCurrentTime] = useState(0)
|
|
const [failed, setFailed] = useState(false)
|
|
const markFailed = () => { failedRef.current = true; setFailed(true) }
|
|
|
|
useEffect(() => {
|
|
if (!url) { audioRef.current = null; return }
|
|
failedRef.current = false
|
|
setFailed(false)
|
|
const audio = new Audio(url)
|
|
audio.preload = 'auto'
|
|
audio.playbackRate = PLAYBACK_RATE
|
|
|
|
// currentTime ~30fps mitschreiben, solange das Audio läuft (für Karaoke-Sync).
|
|
let lastTick = 0
|
|
const tick = (ts) => {
|
|
if (ts - lastTick >= 33) { lastTick = ts; setCurrentTime(audio.currentTime) }
|
|
rafRef.current = requestAnimationFrame(tick)
|
|
}
|
|
const stopLoop = () => {
|
|
if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null }
|
|
}
|
|
|
|
const onPlay = () => { setPlaying(true); stopLoop(); rafRef.current = requestAnimationFrame(tick) }
|
|
const onStop = () => { setPlaying(false); stopLoop(); setCurrentTime(audio.currentTime) }
|
|
const onEnded = () => { setPlaying(false); stopLoop(); setCurrentTime(0) }
|
|
// Netz-/Decode-Fehler eines vorhandenen Files → File gilt als nicht abspielbar,
|
|
// damit play() den TTS-Fallback der Karte greifen lässt.
|
|
const onError = () => { setPlaying(false); stopLoop(); markFailed() }
|
|
audio.addEventListener('play', onPlay)
|
|
audio.addEventListener('pause', onStop)
|
|
audio.addEventListener('ended', onEnded)
|
|
audio.addEventListener('error', onError)
|
|
audioRef.current = audio
|
|
return () => {
|
|
stopLoop()
|
|
audio.pause()
|
|
audio.removeEventListener('play', onPlay)
|
|
audio.removeEventListener('pause', onStop)
|
|
audio.removeEventListener('ended', onEnded)
|
|
audio.removeEventListener('error', onError)
|
|
if (currentAudio === audio) currentAudio = null
|
|
audio.src = ''
|
|
audioRef.current = null
|
|
}
|
|
}, [url])
|
|
|
|
function play() {
|
|
const audio = audioRef.current
|
|
if (!audio || failedRef.current) return false
|
|
if (currentAudio && currentAudio !== audio) currentAudio.pause()
|
|
currentAudio = audio
|
|
audio.currentTime = 0
|
|
audio.playbackRate = PLAYBACK_RATE
|
|
// AbortError = von einem pause() unterbrochen (harmlos); echte Quell-/Decode-Fehler
|
|
// markieren das File als nicht abspielbar → Karte fällt auf TTS zurück.
|
|
audio.play().catch((err) => { if (err?.name !== 'AbortError') markFailed() })
|
|
return true
|
|
}
|
|
|
|
return { play, playing, currentTime, failed }
|
|
}
|