player components

This commit is contained in:
2026-03-29 06:40:45 +03:00
parent 02d65fbeeb
commit 9a4f5a4bf5
25 changed files with 4795 additions and 0 deletions
+97
View File
@@ -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>
)
}