mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
|
import { Link } from 'react-router-dom'
|
|
import { ArrowLeft, Database, FileText, Film, FolderInput, Pencil, Play, Search, Sparkles, Trash2 } from 'lucide-react'
|
|
|
|
import { imageURL } from '../api/client'
|
|
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
|
|
import { MediaFavouriteButton } from '../components/MediaFavouriteButton'
|
|
import type { Media } from '../types'
|
|
import { isTheatricalFeature, seriesTitle, type SeriesCard } from '../utils/groupSeries'
|
|
|
|
type LibrarySeriesDetailHeaderProps = {
|
|
series: SeriesCard
|
|
allEpisodes: Media[]
|
|
playbackFrom: string
|
|
isAdmin: boolean
|
|
canFavorite: boolean
|
|
favourite: boolean
|
|
favouriteBusy: boolean
|
|
onToggleFavourite: () => void
|
|
seriesToolBusy: string
|
|
onBack: () => void
|
|
onSmartScrape: () => void
|
|
onManualScrape: () => void
|
|
onMetadataEdit: () => void
|
|
onProbe: () => void
|
|
onNFO: () => void
|
|
onOrganize: () => void
|
|
onDelete: () => void
|
|
}
|
|
|
|
export function LibrarySeriesDetailHeader({
|
|
series,
|
|
allEpisodes,
|
|
playbackFrom,
|
|
isAdmin,
|
|
canFavorite,
|
|
favourite,
|
|
favouriteBusy,
|
|
onToggleFavourite,
|
|
seriesToolBusy,
|
|
onBack,
|
|
onSmartScrape,
|
|
onManualScrape,
|
|
onMetadataEdit,
|
|
onProbe,
|
|
onNFO,
|
|
onOrganize,
|
|
onDelete,
|
|
}: LibrarySeriesDetailHeaderProps) {
|
|
const tvEpisodes = allEpisodes.filter((media) => !isTheatricalFeature(media))
|
|
const theatricalCount = allEpisodes.length - tvEpisodes.length
|
|
const firstEpisode = firstPlayableEpisode(tvEpisodes)
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center gap-4">
|
|
<button onClick={onBack} className="btn-ghost gap-2">
|
|
<ArrowLeft size={16} />
|
|
返回列表
|
|
</button>
|
|
<h2 className="truncate font-display text-2xl font-bold text-ink-600">
|
|
{seriesTitle(series.rep)}
|
|
</h2>
|
|
<span className="text-sm text-sand-500">
|
|
共 {tvEpisodes.length} 集{theatricalCount > 0 ? ` · ${theatricalCount} 部剧场版` : ''}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-6 sm:flex-row">
|
|
<div className="w-40 shrink-0 overflow-hidden rounded-xl bg-sand-200 shadow-card">
|
|
{series.rep.poster_url ? (
|
|
<img
|
|
src={imageURL(series.rep.poster_url, series.rep.updated_at)}
|
|
alt={series.rep.title}
|
|
loading="lazy"
|
|
decoding="async"
|
|
className="aspect-[2/3] w-full object-cover"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
) : (
|
|
<div className="flex aspect-[2/3] items-center justify-center text-gray-500">
|
|
<Film size={40} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 space-y-3">
|
|
<p className="text-sm leading-relaxed text-ink-50">
|
|
{series.rep.overview || '暂无简介'}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{firstEpisode && (
|
|
<>
|
|
<Link to={`/play/${firstEpisode.id}`} state={{ from: playbackFrom }} className="btn-primary inline-flex">
|
|
<Play size={16} fill="currentColor" />
|
|
从第一集开始播放
|
|
</Link>
|
|
<ExternalPlayerButton mediaId={firstEpisode.id} label="外部播放器播放" />
|
|
</>
|
|
)}
|
|
{canFavorite && (
|
|
<MediaFavouriteButton
|
|
favourite={favourite}
|
|
disabled={favouriteBusy}
|
|
onToggle={onToggleFavourite}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{isAdmin && allEpisodes.length > 0 && !isRemoteEmbyID(series.rep.id) && (
|
|
<div className="rounded-2xl border border-sand-200 bg-white/80 p-4 shadow-sm">
|
|
<p className="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-[#c9954a]">系统后台高级控制面板</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
<button onClick={onSmartScrape} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5">
|
|
<Sparkles size={13} className="text-[#c9954a]" />
|
|
<span>{seriesToolBusy === 'scrape' ? '刮削中…' : '整剧智能刮削'}</span>
|
|
</button>
|
|
<button onClick={onManualScrape} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5">
|
|
<Search size={13} className="text-[#c9954a]" />
|
|
<span>{theatricalCount > 0 ? '手动匹配 TV 版' : '手动匹配整剧'}</span>
|
|
</button>
|
|
<button onClick={onMetadataEdit} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5">
|
|
<Pencil size={13} />
|
|
<span>编辑元数据</span>
|
|
</button>
|
|
<button onClick={onProbe} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5">
|
|
<Database size={13} />
|
|
<span>{seriesToolBusy === 'probe' ? '探测中…' : '探测媒体轨'}</span>
|
|
</button>
|
|
<button onClick={onNFO} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5">
|
|
<FileText size={13} />
|
|
<span>{seriesToolBusy === 'nfo' ? '写出中…' : '写出本地 NFO'}</span>
|
|
</button>
|
|
<button onClick={onOrganize} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5">
|
|
<FolderInput size={13} />
|
|
<span>{seriesToolBusy === 'organize' ? '整理中…' : '整理当前合集'}</span>
|
|
</button>
|
|
<button onClick={onDelete} disabled={!!seriesToolBusy} className="btn-outline px-3.5 py-2 text-xs gap-1.5 !border-red-100 !text-red-500 hover:!border-red-200 hover:!bg-red-50">
|
|
<Trash2 size={13} />
|
|
<span>{seriesToolBusy === 'delete' ? '处理中…' : '删除'}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function firstPlayableEpisode(episodes: Media[]): Media | null {
|
|
const sorted = [...episodes]
|
|
sorted.sort((a, b) =>
|
|
(a.season_num || 0) - (b.season_num || 0)
|
|
|| (a.episode_num || 0) - (b.episode_num || 0),
|
|
)
|
|
return sorted[0] ?? null
|
|
}
|