This commit is contained in:
truewhile
2026-08-30 18:50:15 +08:00
parent 51a64f41b9
commit 42b8805e94
2 changed files with 68 additions and 50 deletions
+2 -1
View File
@@ -42,6 +42,7 @@ export function ExternalPlayerButton({
<> <>
<button <button
type="button" type="button"
title={compact ? '使用外部播放器播放' : undefined}
disabled={loading} disabled={loading}
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
@@ -50,7 +51,7 @@ export function ExternalPlayerButton({
}} }}
className={ className={
compact compact
? 'rounded-lg border border-primary-400/35 bg-white px-2 py-1 text-xs font-semibold text-brand-500 hover:bg-primary-400/10 disabled:opacity-50' ? 'shrink-0 inline-flex items-center rounded-lg border border-primary-400/35 bg-white px-2 py-1 text-xs font-semibold text-brand-500 hover:bg-primary-400/10 disabled:opacity-50 transition-colors whitespace-nowrap'
: 'btn-outline border-brand-500/30 px-5 text-[#c9954a] hover:border-brand-500 hover:bg-brand-50' : 'btn-outline border-brand-500/30 px-5 text-[#c9954a] hover:border-brand-500 hover:bg-brand-50'
} }
> >
+66 -49
View File
@@ -62,48 +62,53 @@ export function LibrarySeriesEpisodes({
<h3 className="mb-3 font-display text-lg font-semibold text-ink-600"> <h3 className="mb-3 font-display text-lg font-semibold text-ink-600">
{displaySeason === 0 ? '特别篇' : `第 ${displaySeason} 季`} {displaySeason === 0 ? '特别篇' : `第 ${displaySeason} 季`}
</h3> </h3>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"> <div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
{visibleEpisodes.map((ep) => ( {visibleEpisodes.map((ep) => {
<div const displayTitle = episodeDisplayTitle(ep, visibleEpisodes)
key={ep.id} return (
className="group flex items-center gap-3 rounded-xl border border-sand-200 bg-white p-3 shadow-card transition-all hover:border-brand-300 hover:shadow-card-hover" <div
> key={ep.id}
<Link to={`/play/${ep.id}`} state={{ from: playbackFrom }} className="flex min-w-0 flex-1 items-center gap-3"> className="group flex items-center justify-between gap-3 rounded-xl border border-sand-200 bg-white p-2.5 shadow-card transition-all hover:border-brand-300 hover:shadow-card-hover"
<div >
className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-brand-50" <Link to={`/play/${ep.id}`} state={{ from: playbackFrom }} className="flex min-w-0 flex-1 items-center gap-3">
title={ep.episode_num > 0 ? `第 ${ep.episode_num} 集` : episodeDisplayTitle(ep, visibleEpisodes)} <div
> className="relative flex h-11 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-brand-50/70 border border-sand-200/60"
{ep.backdrop_url || ep.poster_url ? ( title={displayTitle}
<img >
src={imageURL(ep.backdrop_url || ep.poster_url || '', ep.updated_at)} {ep.backdrop_url || ep.poster_url ? (
alt="" <img
className="h-full w-full object-cover" src={imageURL(ep.backdrop_url || ep.poster_url || '', ep.updated_at)}
referrerPolicy="no-referrer" alt=""
/> className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
) : ( referrerPolicy="no-referrer"
<span className="text-brand-600 font-semibold text-sm">{ep.episode_num || '—'}</span> />
)} ) : (
{ep.episode_num > 0 && ( <span className="text-brand-600 font-bold text-sm">{ep.episode_num || '—'}</span>
<span className="absolute bottom-0 right-0 rounded-tl bg-black/70 px-1 py-px text-[9px] font-bold text-white"> )}
{ep.episode_num} <div className="absolute inset-0 flex items-center justify-center bg-black/25 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
</span> <Play size={15} className="fill-white text-white drop-shadow-sm" />
)} </div>
</div> {ep.episode_num > 0 && (
<div className="min-w-0 flex-1"> <span className="absolute bottom-0 right-0 rounded-tl bg-black/75 px-1 py-0.5 text-[9px] font-bold leading-none text-white backdrop-blur-[2px]">
<p className="truncate text-sm font-medium text-ink-600"> {ep.episode_num}
{episodeDisplayTitle(ep, visibleEpisodes)} </span>
</p> )}
<p className="text-xs text-sand-500"> </div>
{ep.duration_sec > 0 <div className="min-w-0 flex-1">
? `${Math.floor(ep.duration_sec / 60)} 分钟` <p className="truncate text-sm font-medium text-ink-600 transition-colors group-hover:text-brand-600" title={displayTitle}>
: formatSize(ep.size_bytes)} {displayTitle}
</p> </p>
</div> <p className="text-xs text-sand-500 whitespace-nowrap">
<Play size={14} className="shrink-0 text-gray-500 opacity-0 transition-opacity group-hover:opacity-100 group-hover:text-brand-500" /> {ep.duration_sec > 0
</Link> ? `${Math.floor(ep.duration_sec / 60)} 分钟`
<ExternalPlayerButton mediaId={ep.id} label="外部" compact /> : formatSize(ep.size_bytes)}
</div> </p>
))} </div>
</Link>
<ExternalPlayerButton mediaId={ep.id} label="外部" compact />
</div>
)
})}
</div> </div>
</div> </div>
</> </>
@@ -111,17 +116,29 @@ export function LibrarySeriesEpisodes({
} }
function episodeDisplayTitle(ep: Media, siblings: Media[]): string { function episodeDisplayTitle(ep: Media, siblings: Media[]): string {
const title = ep.episode_title?.trim() let mainTitle = ''
if (title && !looksLikeSeriesTitle(ep, title, siblings)) { const epTitle = ep.episode_title?.trim()
return title if (epTitle && !looksLikeSeriesTitle(ep, epTitle, siblings)) {
mainTitle = epTitle
} else {
const mediaTitle = ep.title?.trim()
if (mediaTitle && !looksLikeSeriesTitle(ep, mediaTitle, siblings)) {
mainTitle = mediaTitle
}
} }
const mediaTitle = ep.title?.trim() if (ep.episode_num > 0) {
if (mediaTitle && !looksLikeSeriesTitle(ep, mediaTitle, siblings)) { if (!mainTitle) {
return mediaTitle return `第 ${ep.episode_num} 集`
}
const prefixRegex = new RegExp(`^(第\\s*0*${ep.episode_num}\\s*集|ep?\\.?\\s*0*${ep.episode_num}\\b)`, 'i')
if (prefixRegex.test(mainTitle)) {
return mainTitle
}
return `第 ${ep.episode_num} 集 · ${mainTitle}`
} }
return ep.episode_num > 0 ? `第 ${ep.episode_num} 集` : mediaTitle || title || '未命名' return mainTitle || '未命名'
} }
function looksLikeSeriesTitle(ep: Media, title: string, siblings: Media[]): boolean { function looksLikeSeriesTitle(ep: Media, title: string, siblings: Media[]): boolean {