player components
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Play, X } from '../../lib/icons'
|
||||
import type { BaseItemDto } from '../../api/types'
|
||||
import { getBestImage, getStoredServerUrl } from '../../api/jellyfin'
|
||||
import { formatRuntime } from '../../lib/format'
|
||||
|
||||
interface Props {
|
||||
nextItem?: BaseItemDto | null
|
||||
visible: boolean
|
||||
countdownSeconds: number
|
||||
onSkip: () => void
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export default function UpNext({
|
||||
nextItem,
|
||||
visible,
|
||||
countdownSeconds,
|
||||
onSkip,
|
||||
onDismiss,
|
||||
}: Props) {
|
||||
const navigate = useNavigate()
|
||||
const serverUrl = getStoredServerUrl()
|
||||
|
||||
if (!nextItem) return null
|
||||
|
||||
const thumb = getBestImage(serverUrl, nextItem, 'thumb', 480)
|
||||
const epLabel =
|
||||
nextItem.ParentIndexNumber != null && nextItem.IndexNumber != null
|
||||
? `S${nextItem.ParentIndexNumber} · E${nextItem.IndexNumber}`
|
||||
: ''
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30, y: 0 }}
|
||||
animate={{ opacity: 1, x: 0, y: 0 }}
|
||||
exit={{ opacity: 0, x: 30 }}
|
||||
transition={{ duration: 0.36, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="absolute bottom-32 right-6 w-[360px] glass-strong rounded-xl border border-white/15 overflow-hidden shadow-2xl pointer-events-auto"
|
||||
>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="absolute top-2 right-2 z-10 w-7 h-7 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur grid place-items-center text-white/70 hover:text-white transition-colors focus-ring"
|
||||
aria-label="Dismiss up next"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
|
||||
<div className="relative aspect-video bg-elevated">
|
||||
{thumb && <img src={thumb} alt="" className="w-full h-full object-cover" />}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-3">
|
||||
<p className="text-[10px] uppercase tracking-[0.14em] text-accent font-semibold mb-0.5">
|
||||
Up next in {countdownSeconds}s
|
||||
</p>
|
||||
{epLabel && (
|
||||
<p className="text-[11px] text-white/65 font-mono mb-0.5">{epLabel}</p>
|
||||
)}
|
||||
<p className="text-[14px] text-white font-semibold tracking-tight line-clamp-1">
|
||||
{nextItem.Name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-2">
|
||||
{nextItem.Overview && (
|
||||
<p className="text-[11.5px] text-white/65 line-clamp-2 leading-relaxed">{nextItem.Overview}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<button
|
||||
onClick={onSkip}
|
||||
className="flex-1 h-9 px-3 bg-white text-void rounded-md flex items-center justify-center gap-1.5 text-[12.5px] font-semibold transition-all duration-150 hover:scale-[1.02] active:scale-[0.98] focus-ring"
|
||||
>
|
||||
<Play size={13} fill="currentColor" />
|
||||
Play now
|
||||
</button>
|
||||
<button
|
||||
onClick={() => nextItem.Id && navigate(`/item/${nextItem.Id}`)}
|
||||
className="h-9 px-3 bg-white/10 hover:bg-white/15 text-white border border-white/15 rounded-md text-[12.5px] font-medium transition-colors focus-ring"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
{nextItem.RunTimeTicks && (
|
||||
<span className="text-[11px] text-white/55 tabular-nums">
|
||||
{formatRuntime(nextItem.RunTimeTicks)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user