优化支持0.5集

This commit is contained in:
truewhile
2026-09-27 13:52:17 +08:00
parent cb539afd2a
commit bad07f7add
32 changed files with 537 additions and 158 deletions
+2 -1
View File
@@ -28,6 +28,7 @@ import type { SeriesCard } from '../utils/groupSeries'
import { seriesCardLink } from '../utils/groupSeries'
import { isRemoteEmbyID } from '../utils/remoteEmby'
import { getLibraryArtworks } from './librariesPageModel'
import { formatEpisodeNumber } from '../utils/episodeNumber'
const TYPE_ICONS: Record<string, ReactNode> = {
movie: <Film size={18} />,
@@ -773,7 +774,7 @@ function ContinueCard({ media, progress }: { media: Media; progress: number }) {
{media.year > 0 && <span>{media.year}</span>}
{media.season_num !== undefined && media.episode_num !== undefined && (
<span>
S{media.season_num}E{media.episode_num}
S{media.season_num}E{formatEpisodeNumber(media)}
</span>
)}
</div>
+2 -1
View File
@@ -7,6 +7,7 @@ import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
import { MediaFavouriteButton } from '../components/MediaFavouriteButton'
import type { Media } from '../types'
import { isTheatricalFeature, seriesTitle, type SeriesCard } from '../utils/groupSeries'
import { episodeNumberValue } from '../utils/episodeNumber'
type LibrarySeriesDetailHeaderProps = {
series: SeriesCard
@@ -152,7 +153,7 @@ 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),
|| episodeNumberValue(a) - episodeNumberValue(b),
)
return sorted[0] ?? null
}
+10 -6
View File
@@ -11,6 +11,7 @@ import {
THEATRICAL_SEASON,
} from '../utils/groupSeries'
import { formatSize } from './libraryPageModel'
import { formatEpisodeLabel, formatEpisodeNumber } from '../utils/episodeNumber'
type SeasonGroup = {
season: number
@@ -95,14 +96,14 @@ export function LibrarySeriesEpisodes({
referrerPolicy="no-referrer"
/>
) : (
<span className="text-brand-600 font-bold text-sm">{ep.episode_num || '—'}</span>
<span className="text-brand-600 font-bold text-sm">{formatEpisodeNumber(ep) || '—'}</span>
)}
<div className="absolute inset-0 flex items-center justify-center bg-black/25 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<Play size={15} className="fill-white text-white drop-shadow-sm" />
</div>
{ep.episode_num > 0 && (
<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]">
{ep.episode_num}
{formatEpisodeNumber(ep)}
</span>
)}
</div>
@@ -153,15 +154,18 @@ function episodeDisplayTitle(ep: Media, siblings: Media[]): string {
}
}
if (ep.episode_num > 0) {
const episodeLabel = formatEpisodeLabel(ep)
if (episodeLabel) {
if (!mainTitle) {
return `第 ${ep.episode_num} 集`
return episodeLabel
}
const prefixRegex = new RegExp(`^(第\\s*0*${ep.episode_num}\\s*集|ep?\\.?\\s*0*${ep.episode_num}\\b)`, 'i')
// 标题里已经以「第 11 集 / 第 11.5 集 / ep 11」开头时直接用它,避免重复前缀。
const episodeNumber = formatEpisodeNumber(ep).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const prefixRegex = new RegExp(`^(第\\s*0*${episodeNumber}\\s*集|ep?\\.?\\s*0*${episodeNumber}\\b)`, 'i')
if (prefixRegex.test(mainTitle)) {
return mainTitle
}
return `第 ${ep.episode_num} 集 · ${mainTitle}`
return `${episodeLabel} · ${mainTitle}`
}
return mainTitle || '未命名'
+3 -1
View File
@@ -19,6 +19,7 @@ import { profileAPI } from '../api/profile'
import { useAuthStore } from '../stores/auth'
import type { Media, PlaybackInfo, PlaybackQuality, PlaybackSegment, PlaybackSegmentKind } from '../types'
import { getSeriesKey, seriesTitleFromPath } from '../utils/groupSeries'
import { formatEpisodeLabel } from '../utils/episodeNumber'
import { mediaVersionMatches, mediaVersionsOf } from '../utils/mediaVersion'
import { DEFAULT_PLAYBACK_RATE, normalizePlaybackRate } from '../utils/playbackRate'
import { resolveActiveSkip, skippedNoticeText, toSkipSegments, type SkipPrompt } from '../utils/skipSegments'
@@ -2347,7 +2348,8 @@ function formatEpisodeDisplay(ep: Media, siblings: Media[]): string {
return mediaTitle
}
return ep.episode_num > 0 ? `第 ${ep.episode_num} 集` : mediaTitle || title || '未命名'
const episodeLabel = formatEpisodeLabel(ep)
return episodeLabel || mediaTitle || title || '未命名'
}
function findPlaybackQualityById(info: PlaybackInfo | null, id: string): PlaybackQuality | undefined {
+2 -1
View File
@@ -10,6 +10,7 @@ import {
type SeriesCard,
} from '../utils/groupSeries'
import { resolveSeriesCardByKey } from '../utils/seriesCardResolve'
import { episodeNumberValue } from '../utils/episodeNumber'
type SeasonEpisodes = {
season: number
@@ -72,7 +73,7 @@ export function useLibrarySeriesSelection({
seasons.get(s)!.push(ep)
}
for (const [, list] of seasons) {
list.sort((a, b) => (a.episode_num || 0) - (b.episode_num || 0))
list.sort((a, b) => episodeNumberValue(a) - episodeNumberValue(b))
}
return Array.from(seasons.entries())
.sort(([a], [b]) => seasonSortOrder(a) - seasonSortOrder(b))
+2 -1
View File
@@ -9,6 +9,7 @@ import { confirmActionResult } from '../components/confirmAction'
import { useEpisodeArtworkPreference } from '../hooks/useEpisodeArtworkPreference'
import type { Media } from '../types'
import { seasonSortOrder } from '../utils/groupSeries'
import { episodeNumberValue } from '../utils/episodeNumber'
import { mediaLibraryBackTarget } from './MediaDetailPageModel'
type MediaDetailLocationState = { from?: string } | null
@@ -81,7 +82,7 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa
seasons.get(s)!.push(ep)
}
for (const [, list] of seasons) {
list.sort((a, b) => (a.episode_num || 0) - (b.episode_num || 0))
list.sort((a, b) => episodeNumberValue(a) - episodeNumberValue(b))
}
return Array.from(seasons.entries())
.sort(([a], [b]) => seasonSortOrder(a) - seasonSortOrder(b))