248 lines
6.4 KiB
TypeScript
248 lines
6.4 KiB
TypeScript
import { motion, AnimatePresence } from 'framer-motion'
|
|
import UpNext from './UpNext'
|
|
import ResumePrompt from './ResumePrompt'
|
|
import RecapCard from './RecapCard'
|
|
import ChaptersPanel from './ChaptersPanel'
|
|
import BookmarksPanel from './BookmarksPanel'
|
|
import EndOfVideoCard from './EndOfVideoCard'
|
|
import KeyboardHints from './KeyboardHints'
|
|
import SubtitleSearchPanel from './SubtitleSearchPanel'
|
|
import SyncPlayPanel from './SyncPlayPanel'
|
|
import type { BaseItemDto } from '../../api/types'
|
|
|
|
export interface ChapterMarker {
|
|
StartPositionTicks?: number | null
|
|
Name?: string | null
|
|
ImageTag?: string | null
|
|
}
|
|
|
|
interface UpNextProps {
|
|
item: BaseItemDto | null
|
|
visible: boolean
|
|
countdown: number
|
|
onSkip: () => void
|
|
onDismiss: () => void
|
|
}
|
|
|
|
interface ResumeProps {
|
|
open: boolean
|
|
positionTicks: number
|
|
lastPlayedDate?: string | null
|
|
onResume: () => void
|
|
onRestart: () => void
|
|
}
|
|
|
|
interface RecapProps {
|
|
open: boolean
|
|
previousEpisodes: BaseItemDto[]
|
|
daysSinceLastWatch: number | null
|
|
onDismiss: () => void
|
|
}
|
|
|
|
interface ChaptersProps {
|
|
open: boolean
|
|
itemId: string
|
|
chapters: ChapterMarker[]
|
|
serverUrl: string
|
|
currentTime: number
|
|
onClose: () => void
|
|
onJump: (t: number) => void
|
|
}
|
|
|
|
interface BookmarksProps {
|
|
open: boolean
|
|
itemId: string
|
|
currentTime: number
|
|
refreshKey: number
|
|
onClose: () => void
|
|
onAdd: () => void
|
|
onJump: (t: number) => void
|
|
}
|
|
|
|
interface EndCardProps {
|
|
open: boolean
|
|
hasEpisodes: boolean
|
|
item: BaseItemDto | null | undefined
|
|
nextItem?: BaseItemDto | null | undefined
|
|
onReplay: () => void
|
|
onEpisodes: () => void
|
|
onBack: () => void
|
|
onPlayNext?: () => void
|
|
}
|
|
|
|
interface SkipPromptProps {
|
|
seriesId: string | null
|
|
onAccept: () => void
|
|
onNotNow: () => void
|
|
onDismiss: () => void
|
|
}
|
|
|
|
interface HintsProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
interface SubSearchProps {
|
|
open: boolean
|
|
subtitleUrl: string | null
|
|
onClose: () => void
|
|
onJump: (t: number) => void
|
|
}
|
|
|
|
interface SyncPlayProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
currentItemId: string | null
|
|
currentPositionTicks: number
|
|
}
|
|
|
|
interface Props {
|
|
upNext: UpNextProps
|
|
resume: ResumeProps
|
|
recap: RecapProps
|
|
chapters: ChaptersProps | null
|
|
bookmarks: BookmarksProps | null
|
|
endCard: EndCardProps
|
|
nextItem?: BaseItemDto | null
|
|
onPlayNext?: () => void
|
|
skipPrompt: SkipPromptProps
|
|
hints: HintsProps
|
|
subSearch: SubSearchProps
|
|
syncPlay: SyncPlayProps
|
|
}
|
|
|
|
/**
|
|
* All the modal / panel / card overlays that float above the video. Kept in
|
|
* one component so PlayerPage doesn't have to render 9 sibling overlays
|
|
* inline. State lives in PlayerPage; this is pure presentation.
|
|
*/
|
|
export default function PlayerOverlays({
|
|
upNext,
|
|
resume,
|
|
recap,
|
|
chapters,
|
|
bookmarks,
|
|
endCard,
|
|
nextItem,
|
|
onPlayNext,
|
|
skipPrompt,
|
|
hints,
|
|
subSearch,
|
|
syncPlay,
|
|
}: Props) {
|
|
return (
|
|
<>
|
|
<UpNext
|
|
nextItem={upNext.item}
|
|
visible={upNext.visible}
|
|
countdownSeconds={upNext.countdown}
|
|
onSkip={upNext.onSkip}
|
|
onDismiss={upNext.onDismiss}
|
|
/>
|
|
|
|
<ResumePrompt
|
|
open={resume.open}
|
|
positionTicks={resume.positionTicks}
|
|
lastPlayedDate={resume.lastPlayedDate}
|
|
onResume={resume.onResume}
|
|
onRestart={resume.onRestart}
|
|
/>
|
|
|
|
<RecapCard
|
|
open={recap.open}
|
|
previousEpisodes={recap.previousEpisodes}
|
|
daysSinceLastWatch={recap.daysSinceLastWatch}
|
|
onDismiss={recap.onDismiss}
|
|
/>
|
|
|
|
{chapters && (
|
|
<ChaptersPanel
|
|
open={chapters.open}
|
|
onClose={chapters.onClose}
|
|
chapters={chapters.chapters}
|
|
itemId={chapters.itemId}
|
|
serverUrl={chapters.serverUrl}
|
|
currentTime={chapters.currentTime}
|
|
onJump={chapters.onJump}
|
|
/>
|
|
)}
|
|
|
|
{bookmarks && (
|
|
<BookmarksPanel
|
|
open={bookmarks.open}
|
|
onClose={bookmarks.onClose}
|
|
itemId={bookmarks.itemId}
|
|
currentTime={bookmarks.currentTime}
|
|
onJump={bookmarks.onJump}
|
|
onAdd={bookmarks.onAdd}
|
|
refreshKey={bookmarks.refreshKey}
|
|
/>
|
|
)}
|
|
|
|
<EndOfVideoCard
|
|
open={endCard.open}
|
|
hasEpisodes={endCard.hasEpisodes}
|
|
item={endCard.item ?? undefined}
|
|
nextItem={endCard.nextItem ?? nextItem ?? undefined}
|
|
onReplay={endCard.onReplay}
|
|
onEpisodes={endCard.onEpisodes}
|
|
onBack={endCard.onBack}
|
|
onPlayNext={endCard.onPlayNext ?? onPlayNext}
|
|
/>
|
|
|
|
{/* Smart skip prompt - third manual skip on the same series triggers
|
|
a one-shot offer to auto-skip from now on. */}
|
|
<AnimatePresence>
|
|
{skipPrompt.seriesId && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: 16 }}
|
|
transition={{ duration: 0.24, ease: [0.16, 1, 0.3, 1] }}
|
|
className="absolute bottom-32 left-1/2 -translate-x-1/2 z-toast inline-flex items-center gap-2 h-11 pl-4 pr-2 rounded-full bg-black/90 backdrop-blur-xl border border-white/12 shadow-2xl"
|
|
role="alert"
|
|
>
|
|
<span className="text-[12px] text-white tracking-tight">
|
|
Auto-skip intros on this show?
|
|
</span>
|
|
<button
|
|
onClick={skipPrompt.onAccept}
|
|
className="h-8 px-3 rounded-full bg-accent text-void text-[11.5px] font-semibold hover:bg-accent-hover transition-colors focus-ring"
|
|
>
|
|
Yes
|
|
</button>
|
|
<button
|
|
onClick={skipPrompt.onNotNow}
|
|
className="h-8 px-3 rounded-full text-[11.5px] text-white/70 hover:text-white hover:bg-white/10 transition-colors focus-ring"
|
|
>
|
|
Not now
|
|
</button>
|
|
<button
|
|
onClick={skipPrompt.onDismiss}
|
|
className="h-8 px-3 rounded-full text-[11.5px] text-white/55 hover:text-white hover:bg-white/10 transition-colors focus-ring"
|
|
>
|
|
Don't ask
|
|
</button>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<KeyboardHints open={hints.open} onClose={hints.onClose} />
|
|
|
|
<SubtitleSearchPanel
|
|
open={subSearch.open}
|
|
onClose={subSearch.onClose}
|
|
subtitleUrl={subSearch.subtitleUrl}
|
|
onJump={subSearch.onJump}
|
|
/>
|
|
|
|
<SyncPlayPanel
|
|
open={syncPlay.open}
|
|
onClose={syncPlay.onClose}
|
|
currentItemId={syncPlay.currentItemId}
|
|
currentPositionTicks={syncPlay.currentPositionTicks}
|
|
/>
|
|
</>
|
|
)
|
|
}
|