import { useEffect, useRef, useState } from 'react'; import { Animated, Easing, Text, TouchableOpacity, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import i18n from '../../lib/i18n'; import { useTranslation } from 'react-i18next'; import { RiveAvatar, type Emotion } from '../RiveAvatar'; import { useColors } from '../../lib/theme'; import { playLyraSpeech, stopLyraSpeech, type LyraSpeechStatus, } from '../../lib/lyraSpeech'; import { useLyraVoiceStore } from '../../stores/lyraVoice'; /** * Lyra-Mascot (animiertes Rive-Avatar) + Speech-Bubble + TTS-Audio-Button. * * Auto-Play-Pattern (User-Preference, persistent via useLyraVoiceStore): * - Voice OFF (default): kein Auto-Play, Audio-Button-Tap aktiviert Voice * + spielt aktuellen Text. * - Voice ON: Bubble spielt sich auf jeder Slide automatisch ab beim * Text-Change. Audio-Button-Tap stoppt aktuelles Playback + disabled * die Preference (= kein Auto-Play mehr). * * Cleanup beim Unmount + bei text-change (Slide-Switch → vorheriger Sound * wird abgebrochen). */ export function LyraBubble({ text, emotion = 'idle', }: { text: string; emotion?: Emotion; }) { const { t } = useTranslation(); const colors = useColors(); const opacity = useRef(new Animated.Value(0)).current; const translateX = useRef(new Animated.Value(-12)).current; const [speech, setSpeech] = useState('idle'); const voiceEnabled = useLyraVoiceStore((s) => s.enabled); const voiceReady = useLyraVoiceStore((s) => s.ready); const toggleVoice = useLyraVoiceStore((s) => s.toggle); // Fade-in beim Mount + text-change useEffect(() => { opacity.setValue(0); translateX.setValue(-12); Animated.parallel([ Animated.timing(opacity, { toValue: 1, duration: 400, useNativeDriver: true, easing: Easing.out(Easing.cubic), }), Animated.spring(translateX, { toValue: 0, useNativeDriver: true, friction: 7, tension: 80, }), ]).start(); }, [text, opacity, translateX]); // Auto-Play wenn voice enabled + Bubble-Text wechselt (= neue Slide). // Stoppt vorheriges Playback automatisch via stopLyraSpeech() in playLyraSpeech. useEffect(() => { if (!voiceReady) return; // warte bis AsyncStorage geladen if (!voiceEnabled) { // Falls voice grad disabled wurde während etwas spielte: stoppen. setSpeech('idle'); void stopLyraSpeech(); return; } void playLyraSpeech(text, i18n.language || 'de', setSpeech); }, [text, voiceEnabled, voiceReady]); // Cleanup beim Unmount — sonst spielt der Sound weiter wenn /onboarding entladen wird useEffect(() => { return () => { void stopLyraSpeech(); }; }, []); async function togglePlayback() { // Wenn voice OFF: enable (auto-trigger Auto-Play-Effect oben) // Wenn voice ON: disable + stop current if (voiceEnabled) { setSpeech('idle'); await stopLyraSpeech(); } await toggleVoice(); // Wenn jetzt enabled: useEffect oben startet das playback automatisch } const a11yLabel = voiceEnabled ? speech === 'playing' ? t('onboarding.lyra.audio_stop') : t('onboarding.lyra.audio_disable') : t('onboarding.lyra.audio_play'); const iconName: keyof typeof Ionicons.glyphMap = !voiceEnabled ? 'volume-mute-outline' : speech === 'playing' ? 'stop' : speech === 'loading' ? 'hourglass' : 'volume-medium'; return ( {text} {/* Audio-Toggle rechts oben — bestimmt globale Voice-Preference */} ); }