refactor: split library page details

This commit is contained in:
ShukeBta
2026-06-25 11:50:39 +08:00
parent e16fecb3ec
commit 13cca43813
3 changed files with 234 additions and 81 deletions
+35 -81
View File
@@ -1,16 +1,14 @@
import { useState } from 'react'
import { useLocation, useParams, useSearchParams } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { motion } from 'framer-motion'
import type { Media } from '../types'
import { useAuthStore } from '../stores/auth'
import { seriesTitle, type SeriesCard } from '../utils/groupSeries'
import { ManualScrapeDialog } from '../components/ManualScrapeDialog'
import { MetadataEditDialog } from '../components/MetadataEditDialog'
import { LibrarySeriesEpisodes } from './LibrarySeriesEpisodes'
import { LibrarySeriesDetailHeader } from './LibrarySeriesDetailHeader'
import type { SeriesCard } from '../utils/groupSeries'
import { LibraryPageDialogs } from './LibraryPageDialogs'
import { LibraryPageHeader } from './LibraryPageHeader'
import { LibraryMediaSections } from './LibraryMediaSections'
import { LibrarySeriesDetailSection } from './LibrarySeriesDetailSection'
import { useLibraryData } from './useLibraryData'
import { useLibraryScanStatus } from './useLibraryScanStatus'
import { useLibrarySeriesSelection } from './useLibrarySeriesSelection'
@@ -141,84 +139,40 @@ export function LibraryPage() {
onSeriesClick={handleSeriesClick}
/>
{/* 剧集详情:季/集选择器 */}
<AnimatePresence mode="wait">
{selectedSeries && (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="space-y-6"
>
<LibrarySeriesDetailHeader
series={selectedSeries}
visibleEpisodes={visibleEpisodes}
allEpisodes={selectedSeriesEpisodes}
playbackFrom={`${location.pathname}${location.search}`}
isAdmin={role === 'admin'}
seriesToolBusy={seriesToolBusy}
onBack={clearSelectedSeries}
onSmartScrape={handleSeriesSmartScrape}
onManualScrape={() => setManualSeriesScrapeOpen(true)}
onMetadataEdit={() => setSeriesMetadataEditOpen(true)}
onProbe={handleSeriesProbe}
onNFO={handleSeriesNFO}
onOrganize={handleSeriesOrganize}
onSoftDelete={handleSeriesSoftDelete}
/>
{/* 季 / 集列表 */}
<div className="space-y-6">
<LibrarySeriesEpisodes
loading={loadingSeriesEpisodes}
selectedEpisodes={selectedEpisodes}
selectedSeason={selectedSeason}
visibleEpisodes={visibleEpisodes}
playbackFrom={`${location.pathname}${location.search}`}
onSeasonChange={setSelectedSeason}
/>
</div>
</motion.div>
)}
</AnimatePresence>
<ManualScrapeDialog
open={manualSeriesScrapeOpen}
media={selectedSeries?.rep ?? null}
mediaIds={selectedSeriesMediaIDs}
defaultQuery={selectedSeries ? seriesTitle(selectedSeries.rep) : ''}
mediaType={selectedSeries ? scrapeMediaType(library?.type, selectedSeries.rep) : 'tv'}
scopeLabel={selectedSeries ? seriesTitle(selectedSeries.rep) : '当前剧集'}
episodeArtwork={scrapeEpisodeArtwork}
onClose={() => setManualSeriesScrapeOpen(false)}
onApplied={reloadCurrentLibrary}
<LibrarySeriesDetailSection
selectedSeries={selectedSeries}
selectedEpisodes={selectedEpisodes}
selectedSeason={selectedSeason}
visibleEpisodes={visibleEpisodes}
allEpisodes={selectedSeriesEpisodes}
loadingEpisodes={loadingSeriesEpisodes}
playbackFrom={`${location.pathname}${location.search}`}
isAdmin={role === 'admin'}
seriesToolBusy={seriesToolBusy}
onBack={clearSelectedSeries}
onSmartScrape={handleSeriesSmartScrape}
onManualScrape={() => setManualSeriesScrapeOpen(true)}
onMetadataEdit={() => setSeriesMetadataEditOpen(true)}
onProbe={handleSeriesProbe}
onNFO={handleSeriesNFO}
onOrganize={handleSeriesOrganize}
onSoftDelete={handleSeriesSoftDelete}
onSeasonChange={setSelectedSeason}
/>
<MetadataEditDialog
open={seriesMetadataEditOpen}
media={selectedSeries?.rep ?? null}
mediaIds={selectedSeriesMediaIDs}
mode="series"
scopeLabel={selectedSeries ? seriesTitle(selectedSeries.rep) : '当前剧集'}
onClose={() => setSeriesMetadataEditOpen(false)}
onSaved={reloadCurrentLibrary}
/>
<ManualScrapeDialog
open={!!manualMovie}
media={manualMovie}
defaultQuery={manualMovie?.title ?? ''}
mediaType={manualMovie ? scrapeMediaType(library?.type, manualMovie) : library?.type || 'movie'}
scopeLabel={manualMovie?.title ?? '当前电影'}
episodeArtwork={scrapeEpisodeArtwork}
onClose={() => setManualMovie(null)}
<LibraryPageDialogs
manualSeriesScrapeOpen={manualSeriesScrapeOpen}
seriesMetadataEditOpen={seriesMetadataEditOpen}
manualMovie={manualMovie}
selectedSeries={selectedSeries}
selectedSeriesMediaIDs={selectedSeriesMediaIDs}
libraryType={library?.type}
scrapeEpisodeArtwork={scrapeEpisodeArtwork}
onCloseManualSeriesScrape={() => setManualSeriesScrapeOpen(false)}
onCloseSeriesMetadataEdit={() => setSeriesMetadataEditOpen(false)}
onCloseManualMovie={() => setManualMovie(null)}
onApplied={reloadCurrentLibrary}
/>
</div>
)
}
function scrapeMediaType(libraryType: string | undefined, media: Media): string {
if ((media.season_num ?? 0) > 0 || (media.episode_num ?? 0) > 0) {
return 'tv'
}
return libraryType || 'movie'
}
+76
View File
@@ -0,0 +1,76 @@
import { ManualScrapeDialog } from '../components/ManualScrapeDialog'
import { MetadataEditDialog } from '../components/MetadataEditDialog'
import type { Media } from '../types'
import { seriesTitle, type SeriesCard } from '../utils/groupSeries'
type LibraryPageDialogsProps = {
manualSeriesScrapeOpen: boolean
seriesMetadataEditOpen: boolean
manualMovie: Media | null
selectedSeries: SeriesCard | null
selectedSeriesMediaIDs: string[]
libraryType?: string
scrapeEpisodeArtwork: boolean
onCloseManualSeriesScrape: () => void
onCloseSeriesMetadataEdit: () => void
onCloseManualMovie: () => void
onApplied: () => void
}
export function LibraryPageDialogs({
manualSeriesScrapeOpen,
seriesMetadataEditOpen,
manualMovie,
selectedSeries,
selectedSeriesMediaIDs,
libraryType,
scrapeEpisodeArtwork,
onCloseManualSeriesScrape,
onCloseSeriesMetadataEdit,
onCloseManualMovie,
onApplied,
}: LibraryPageDialogsProps) {
const selectedSeriesTitle = selectedSeries ? seriesTitle(selectedSeries.rep) : ''
return (
<>
<ManualScrapeDialog
open={manualSeriesScrapeOpen}
media={selectedSeries?.rep ?? null}
mediaIds={selectedSeriesMediaIDs}
defaultQuery={selectedSeriesTitle}
mediaType={selectedSeries ? scrapeMediaType(libraryType, selectedSeries.rep) : 'tv'}
scopeLabel={selectedSeriesTitle || '当前剧集'}
episodeArtwork={scrapeEpisodeArtwork}
onClose={onCloseManualSeriesScrape}
onApplied={onApplied}
/>
<MetadataEditDialog
open={seriesMetadataEditOpen}
media={selectedSeries?.rep ?? null}
mediaIds={selectedSeriesMediaIDs}
mode="series"
scopeLabel={selectedSeriesTitle || '当前剧集'}
onClose={onCloseSeriesMetadataEdit}
onSaved={onApplied}
/>
<ManualScrapeDialog
open={!!manualMovie}
media={manualMovie}
defaultQuery={manualMovie?.title ?? ''}
mediaType={manualMovie ? scrapeMediaType(libraryType, manualMovie) : libraryType || 'movie'}
scopeLabel={manualMovie?.title ?? '当前电影'}
episodeArtwork={scrapeEpisodeArtwork}
onClose={onCloseManualMovie}
onApplied={onApplied}
/>
</>
)
}
function scrapeMediaType(libraryType: string | undefined, media: Media): string {
if ((media.season_num ?? 0) > 0 || (media.episode_num ?? 0) > 0) {
return 'tv'
}
return libraryType || 'movie'
}
@@ -0,0 +1,123 @@
import { AnimatePresence, motion } from 'framer-motion'
import type { Media } from '../types'
import type { SeriesCard } from '../utils/groupSeries'
import { LibrarySeriesDetailHeader } from './LibrarySeriesDetailHeader'
import { LibrarySeriesEpisodes } from './LibrarySeriesEpisodes'
type SeasonEpisodes = {
season: number
episodes: Media[]
}
type LibrarySeriesDetailSectionProps = {
selectedSeries: SeriesCard | null
selectedEpisodes: SeasonEpisodes[]
selectedSeason: number | null
visibleEpisodes: Media[]
allEpisodes: Media[]
loadingEpisodes: boolean
playbackFrom: string
isAdmin: boolean
seriesToolBusy: string
onBack: () => void
onSmartScrape: () => void
onManualScrape: () => void
onMetadataEdit: () => void
onProbe: () => void
onNFO: () => void
onOrganize: () => void
onSoftDelete: () => void
onSeasonChange: (season: number) => void
}
export function LibrarySeriesDetailSection({
selectedSeries,
selectedEpisodes,
selectedSeason,
visibleEpisodes,
allEpisodes,
loadingEpisodes,
playbackFrom,
isAdmin,
seriesToolBusy,
onBack,
onSmartScrape,
onManualScrape,
onMetadataEdit,
onProbe,
onNFO,
onOrganize,
onSoftDelete,
onSeasonChange,
}: LibrarySeriesDetailSectionProps) {
return (
<AnimatePresence mode="wait">
{selectedSeries && (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="space-y-6"
>
<LibrarySeriesDetailHeader
series={selectedSeries}
visibleEpisodes={visibleEpisodes}
allEpisodes={allEpisodes}
playbackFrom={playbackFrom}
isAdmin={isAdmin}
seriesToolBusy={seriesToolBusy}
onBack={onBack}
onSmartScrape={onSmartScrape}
onManualScrape={onManualScrape}
onMetadataEdit={onMetadataEdit}
onProbe={onProbe}
onNFO={onNFO}
onOrganize={onOrganize}
onSoftDelete={onSoftDelete}
/>
<LibrarySeriesEpisodesPanel
loading={loadingEpisodes}
selectedEpisodes={selectedEpisodes}
selectedSeason={selectedSeason}
visibleEpisodes={visibleEpisodes}
playbackFrom={playbackFrom}
onSeasonChange={onSeasonChange}
/>
</motion.div>
)}
</AnimatePresence>
)
}
type LibrarySeriesEpisodesPanelProps = {
loading: boolean
selectedEpisodes: SeasonEpisodes[]
selectedSeason: number | null
visibleEpisodes: Media[]
playbackFrom: string
onSeasonChange: (season: number) => void
}
function LibrarySeriesEpisodesPanel({
loading,
selectedEpisodes,
selectedSeason,
visibleEpisodes,
playbackFrom,
onSeasonChange,
}: LibrarySeriesEpisodesPanelProps) {
return (
<div className="space-y-6">
<LibrarySeriesEpisodes
loading={loading}
selectedEpisodes={selectedEpisodes}
selectedSeason={selectedSeason}
visibleEpisodes={visibleEpisodes}
playbackFrom={playbackFrom}
onSeasonChange={onSeasonChange}
/>
</div>
)
}