Files
jellybloom/src/components/player/PlayerOverlays.tsx
T
2026-05-01 08:30:36 +03:00

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}
/>
</>
)
}