import { motion } from 'framer-motion' import { useEffect, useState } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' import { FileText, Heart, Play, RefreshCw, Sparkles, Trash2, Calendar, Database } from 'lucide-react' import toast from 'react-hot-toast' import { mediaAPI } from '../api/library' import { playbackAPI } from '../api/playback' import { recycleAPI } from '../api/recycle' import { imageURL } from '../api/client' import { useAuthStore } from '../stores/auth' import { api } from '../api/client' import type { Media } from '../types' import { confirmAction } from '../components/ConfirmDialog' import { ExternalPlayerButton } from '../components/ExternalPlayerButton' function fmtDuration(sec: number): string { if (!sec || sec <= 0) return '—' const h = Math.floor(sec / 3600) const m = Math.floor((sec % 3600) / 60) return h > 0 ? `${h}h ${m}m` : `${m}m` } function fmtSize(bytes: number): string { if (!bytes || bytes <= 0) return '—' const units = ['B', 'KB', 'MB', 'GB', 'TB'] let v = bytes let i = 0 while (v >= 1024 && i < units.length - 1) { v /= 1024 i++ } return `${v.toFixed(2)} ${units[i]}` } function parseCSV(s?: string): string[] { if (!s) return [] return s.split(',').map(x => x.trim()).filter(Boolean) } export function MediaDetailPage() { const { id = '' } = useParams() const navigate = useNavigate() const role = useAuthStore((s) => s.user?.role) const [media, setMedia] = useState(null) const [favourite, setFavourite] = useState(false) const [loading, setLoading] = useState(true) const refresh = async () => { if (!id) return setLoading(true) try { const m = await mediaAPI.get(id) setMedia(m) const favs = await playbackAPI.listFavourites().catch(() => []) setFavourite(favs.some((f) => f.id === m.id)) } finally { setLoading(false) } } useEffect(() => { refresh().catch(() => undefined) }, [id]) const toggleFav = async () => { if (!media) return const state = await playbackAPI.toggleFavourite(media.id) setFavourite(state) toast.success(state ? '已加入我的收藏' : '已取消收藏') } const rescrape = async () => { if (!media) return await api.post(`/media/${media.id}/scrape`) toast.success('已触发重新刮削') await refresh() } const reprobe = async () => { if (!media) return try { const r = await api.post(`/media/${media.id}/probe`) if (r.data?.code === 0) { toast.success('重新探测成功') } else { toast.error(r.data?.error || '探测失败') } await refresh() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '探测失败,请检查 ffprobe 是否已安装' toast.error(msg) } } const exportNFO = async () => { if (!media) return try { const r = await recycleAPI.exportNFO(media.id) toast.success(`NFO 已成功写入 ${r.path}`) } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '导出失败' toast.error(msg) } } const softDelete = async () => { if (!media) return if (!(await confirmAction({ title: '移入回收站', message: `将「${media.title}」移至回收站? (磁盘文件保留)`, confirmText: '移入回收站' }))) return await recycleAPI.softDelete(media.id) toast.success('已移至回收站') navigate(-1) } if (loading) { return (
) } if (!media) { return (

媒体资源已被移除或不存在

) } return (
{/* ── Cinematic Blurred Backdrop Glow ── */}
{media.poster_url ? ( ) : (
)}
{/* ── Main Details Layout Container ── */}
{/* Poster Card */}
{media.poster_url ? ( {media.title} ) : (
无海报
)} {/* Quick Play overlay button */}
{/* Detailed Metadata Body */}
{/* Title and Year Header */}

{media.title}

{media.year > 0 && ( {media.year} 年 )} {media.width > 0 && ( {media.width} × {media.height} )} {fmtSize(media.size_bytes)} {fmtDuration(media.duration_sec)} {media.container && ( {media.container} )}
{/* Description Card */} {media.overview && (

剧情简介

{media.overview}

)} {/* Tag Rows (Genres, Languages, Countries) */}
{/* Genres */} {parseCSV(media.genres).length > 0 && (
类型流派
{parseCSV(media.genres).map((g) => ( {g} ))}
)} {/* Languages */} {parseCSV(media.languages).length > 0 && (
语言
{parseCSV(media.languages).map((l) => ( {l} ))}
)} {/* Countries */} {parseCSV(media.countries).length > 0 && (
国家/地区
{parseCSV(media.countries).map((c) => ( {c} ))}
)}
{/* Action Buttons Panel */}
{/* Primary Direct Play */} 立即播放 {/* Transcode Playback */} HLS 兼容转码播放 {/* Toggle Favourites */}
{/* Admin Management Toolbar */} {role === 'admin' && (

系统后台高级控制面板

)}
) }