mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
refactor: split library page details
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user