- prevSlide() in usePresenterMode: springt zur vorherigen Folie, stoppt aktuelle Audio, startet Präsentation der vorherigen Folie - SkipBack Button in PresenterOverlay neben SkipForward - Beide Buttons springen zur korrekten Folie UND starten die Audio Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
202 lines
7.9 KiB
TypeScript
202 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { Play, Pause, Square, SkipForward, SkipBack, Volume2, VolumeX } from 'lucide-react'
|
|
import { Language } from '@/lib/types'
|
|
import { PresenterState } from '@/lib/presenter/types'
|
|
import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
|
|
import { t } from '@/lib/i18n'
|
|
|
|
interface PresenterOverlayProps {
|
|
state: PresenterState
|
|
currentIndex: number
|
|
totalSlides: number
|
|
progress: number
|
|
displayText: string
|
|
lang: Language
|
|
onPause: () => void
|
|
onResume: () => void
|
|
onStop: () => void
|
|
onSkip: () => void
|
|
onPrev?: () => void
|
|
isSpeaking?: boolean
|
|
ttsAvailable?: boolean
|
|
ttsEnabled?: boolean
|
|
onToggleTts?: () => void
|
|
}
|
|
|
|
export default function PresenterOverlay({
|
|
state,
|
|
currentIndex,
|
|
totalSlides,
|
|
progress,
|
|
displayText,
|
|
lang,
|
|
onPause,
|
|
onResume,
|
|
onStop,
|
|
onSkip,
|
|
onPrev,
|
|
isSpeaking,
|
|
ttsAvailable,
|
|
ttsEnabled = true,
|
|
onToggleTts,
|
|
}: PresenterOverlayProps) {
|
|
const i = t(lang)
|
|
const slideName = i.slideNames[currentIndex] || SLIDE_ORDER[currentIndex] || ''
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{state !== 'idle' && (
|
|
<motion.div
|
|
initial={{ y: 100, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: 100, opacity: 0 }}
|
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
|
className="fixed bottom-0 left-0 right-0 z-40 pointer-events-none"
|
|
>
|
|
<div className="mx-auto max-w-4xl px-4 pb-4 pointer-events-auto">
|
|
<div className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
|
|
{/* Progress bar */}
|
|
<div className="h-1 bg-white/5">
|
|
<motion.div
|
|
className="h-full bg-gradient-to-r from-indigo-500 to-purple-500"
|
|
style={{ width: `${Math.min(progress, 100)}%` }}
|
|
transition={{ duration: 0.3 }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="px-4 py-3">
|
|
{/* Top row: slide info + controls */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-3">
|
|
{/* State indicator */}
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
state === 'presenting' ? 'bg-green-400 animate-pulse' :
|
|
state === 'paused' ? 'bg-yellow-400' :
|
|
state === 'answering' ? 'bg-blue-400 animate-pulse' :
|
|
state === 'resuming' ? 'bg-indigo-400 animate-pulse' :
|
|
'bg-white/30'
|
|
}`} />
|
|
<span className="text-xs text-white/50 font-medium">
|
|
{lang === 'de' ? 'Folie' : 'Slide'} {currentIndex + 1}/{totalSlides} — {slideName}
|
|
</span>
|
|
<span className="text-xs text-white/30">
|
|
{Math.round(progress)}%
|
|
</span>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex items-center gap-1.5">
|
|
{/* TTS toggle */}
|
|
{ttsAvailable && onToggleTts && (
|
|
<button
|
|
onClick={onToggleTts}
|
|
className={`w-7 h-7 rounded-full flex items-center justify-center transition-colors ${
|
|
ttsEnabled
|
|
? 'bg-indigo-500/30 hover:bg-indigo-500/50'
|
|
: 'bg-white/10 hover:bg-white/20'
|
|
}`}
|
|
title={lang === 'de'
|
|
? (ttsEnabled ? 'Stimme ausschalten' : 'Stimme einschalten')
|
|
: (ttsEnabled ? 'Mute voice' : 'Enable voice')}
|
|
>
|
|
{ttsEnabled ? (
|
|
<Volume2 className={`w-3.5 h-3.5 ${isSpeaking ? 'text-indigo-300' : 'text-white/60'}`} />
|
|
) : (
|
|
<VolumeX className="w-3.5 h-3.5 text-white/40" />
|
|
)}
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={onPrev}
|
|
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
|
|
hover:bg-white/20 transition-colors"
|
|
title={lang === 'de' ? 'Vorherige Folie' : 'Previous slide'}
|
|
>
|
|
<SkipBack className="w-3.5 h-3.5 text-white/60" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onSkip}
|
|
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
|
|
hover:bg-white/20 transition-colors"
|
|
title={lang === 'de' ? 'Nächste Folie' : 'Next slide'}
|
|
>
|
|
<SkipForward className="w-3.5 h-3.5 text-white/60" />
|
|
</button>
|
|
|
|
{state === 'presenting' ? (
|
|
<button
|
|
onClick={onPause}
|
|
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
|
|
hover:bg-white/20 transition-colors"
|
|
title={lang === 'de' ? 'Pausieren' : 'Pause'}
|
|
>
|
|
<Pause className="w-3.5 h-3.5 text-white/60" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={onResume}
|
|
className="w-7 h-7 rounded-full bg-indigo-500/30 flex items-center justify-center
|
|
hover:bg-indigo-500/50 transition-colors"
|
|
title={lang === 'de' ? 'Fortsetzen' : 'Resume'}
|
|
>
|
|
<Play className="w-3.5 h-3.5 text-indigo-300" />
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={onStop}
|
|
className="w-7 h-7 rounded-full bg-red-500/20 flex items-center justify-center
|
|
hover:bg-red-500/30 transition-colors"
|
|
title={lang === 'de' ? 'Stoppen' : 'Stop'}
|
|
>
|
|
<Square className="w-3 h-3 text-red-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Subtitle text */}
|
|
<AnimatePresence mode="wait">
|
|
{displayText && (
|
|
<motion.p
|
|
key={displayText.slice(0, 30)}
|
|
initial={{ opacity: 0, y: 5 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -5 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="text-sm text-white/70 leading-relaxed"
|
|
>
|
|
{displayText}
|
|
</motion.p>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* State message */}
|
|
{state === 'paused' && (
|
|
<p className="text-xs text-yellow-400/60 mt-1">
|
|
{lang === 'de' ? 'Pausiert — stellen Sie eine Frage oder drücken Sie Play' : 'Paused — ask a question or press play'}
|
|
</p>
|
|
)}
|
|
{state === 'answering' && (
|
|
<p className="text-xs text-blue-400/60 mt-1">
|
|
{lang === 'de' ? 'Beantworte Ihre Frage...' : 'Answering your question...'}
|
|
</p>
|
|
)}
|
|
{state === 'resuming' && (
|
|
<p className="text-xs text-indigo-400/60 mt-1">
|
|
{lang === 'de' ? 'Setze Präsentation fort...' : 'Resuming presentation...'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|