feat: persönlichere Profilseite + iOS-App-Setup
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>
This commit is contained in:
@@ -3,25 +3,55 @@ 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'
|
||||
const onPlay = () => setPlaying(true)
|
||||
const onStop = () => setPlaying(false)
|
||||
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', 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', onStop)
|
||||
audio.removeEventListener('ended', onEnded)
|
||||
audio.removeEventListener('error', onError)
|
||||
if (currentAudio === audio) currentAudio = null
|
||||
audio.src = ''
|
||||
audioRef.current = null
|
||||
@@ -30,13 +60,16 @@ export default function usePairAudio(url) {
|
||||
|
||||
function play() {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return false
|
||||
if (!audio || failedRef.current) return false
|
||||
if (currentAudio && currentAudio !== audio) currentAudio.pause()
|
||||
currentAudio = audio
|
||||
audio.currentTime = 0
|
||||
audio.play().catch(() => {})
|
||||
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 }
|
||||
return { play, playing, currentTime, failed }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user