From 95e723a931b9dfa30999cb4ae294e046d199e1dd Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:55:06 +0800 Subject: [PATCH] refactor: split libraries page sections --- web/src/pages/LibrariesPage.tsx | 229 ++-------------------- web/src/pages/LibrariesPageSections.tsx | 244 ++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 209 deletions(-) create mode 100644 web/src/pages/LibrariesPageSections.tsx diff --git a/web/src/pages/LibrariesPage.tsx b/web/src/pages/LibrariesPage.tsx index d62e6ef..2341b0a 100644 --- a/web/src/pages/LibrariesPage.tsx +++ b/web/src/pages/LibrariesPage.tsx @@ -1,38 +1,14 @@ -import { useEffect, useMemo, useState, type ReactNode } from 'react' -import { Link } from 'react-router-dom' -import { motion } from 'framer-motion' -import { ArrowRight, Film, FolderOpen, Library as LibraryIcon, Music, PlayCircle, RefreshCw, Tv } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' -import { imageURL } from '../api/client' import { libraryAPI } from '../api/library' import { toolsAPI } from '../api/tools' -import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle' -import { MediaCard } from '../components/MediaCard' -import type { Library, Media } from '../types' -import { artworkScore, groupSeries, seriesCardLink, type SeriesCard } from '../utils/groupSeries' - -type LibraryPreview = { - library: Library - items: Media[] - total: number - cards: SeriesCard[] -} - -const TYPE_ICONS: Record = { - movie: , - tv: , - anime: , - variety: , - music: , -} - -const TYPE_LABELS: Record = { - movie: '电影', - tv: '剧集', - anime: '动漫', - variety: '综艺', - music: '音乐', -} +import { + LibrariesContent, + LibrariesEmptyState, + LibrariesHeader, + latestLibraryCards, + type LibraryPreview, +} from './LibrariesPageSections' export function LibrariesPage() { const [previews, setPreviews] = useState([]) @@ -64,7 +40,7 @@ export function LibrariesPage() { const rows = await Promise.all(libs.map(async (library) => { try { const page = await libraryAPI.listMedia(library.id, 1, 160, { groupVersions: false }) - const cards = latestCards(page.items) + const cards = latestLibraryCards(page.items) return { library, items: page.items, total: page.total, cards } satisfies LibraryPreview } catch { return { library, items: [], total: 0, cards: [] } satisfies LibraryPreview @@ -87,186 +63,21 @@ export function LibrariesPage() { return (
-
-
-

媒体库

-

- 共 {previews.length} 个目录 · {total.toLocaleString()} 个条目。每个目录直接展示最新入库内容。 -

-
-
- {repairMsg && {repairMsg}} - - - - 管理媒体库 - - -
-
+ {previews.length === 0 ? ( -
- -

暂无媒体库,请到管理后台添加目录。

-
+ ) : ( - <> -
-
-

媒体库入口

-

按目录进入完整媒体库;下方每个目录也会直接展示最新内容。

-
-
- {previews.map((preview, index) => ( - - - - ))} -
-
- -
- {previews.map((preview, index) => ( - - - - ))} -
- + )}
) } - -function LibraryEntryCard({ preview }: { preview: LibraryPreview }) { - const library = preview.library - const artwork = preview.cards - .sort((a, b) => artworkScore(b.rep) - artworkScore(a.rep) || mediaTime(b.rep) - mediaTime(a.rep)) - .map((card) => ({ - src: card.rep.poster_url || card.rep.backdrop_url, - version: card.rep.updated_at, - })) - .filter((item) => Boolean(item.src)) - .slice(0, 4) - - return ( - -
- {artwork.length > 0 ? ( - artwork.map(({ src, version }, index) => ( - { event.currentTarget.style.visibility = 'hidden' }} - /> - )) - ) : ( -
- {TYPE_ICONS[library.type] ?? } -
- )} -
-
-
-
- {TYPE_LABELS[library.type] ?? library.type} -
-

- {library.name} -

-

{library.path}

-
-
- {preview.total.toLocaleString()} 个条目 - 浏览全部 -
-
- - ) -} - -function LibraryShelf({ preview }: { preview: LibraryPreview }) { - const library = preview.library - const cards = preview.cards.slice(0, 10) - - return ( -
-
-
-
- {TYPE_ICONS[library.type] ?? } - {TYPE_LABELS[library.type] ?? library.type} -
-

{library.name}

-

- {library.path} · {preview.total.toLocaleString()} 个条目 · 最新 {cards.length} 部 -

-
- - 浏览全部 - - -
- - {cards.length > 0 ? ( -
- {cards.map((card) => ( -
- -
- ))} -
- ) : ( -
- 该目录暂无可展示内容,扫描媒体库后会出现在这里。 -
- )} -
- ) -} - -function latestCards(items: Media[]): SeriesCard[] { - return groupSeries(items) - .sort((a, b) => mediaTime(b.rep) - mediaTime(a.rep) || artworkScore(b.rep) - artworkScore(a.rep)) - .slice(0, 10) -} - -function mediaTime(media: Media): number { - return Date.parse(media.updated_at || media.created_at || '') || 0 -} diff --git a/web/src/pages/LibrariesPageSections.tsx b/web/src/pages/LibrariesPageSections.tsx new file mode 100644 index 0000000..335c891 --- /dev/null +++ b/web/src/pages/LibrariesPageSections.tsx @@ -0,0 +1,244 @@ +import type { ReactNode } from 'react' +import { Link } from 'react-router-dom' +import { motion } from 'framer-motion' +import { ArrowRight, Film, FolderOpen, Library as LibraryIcon, Music, PlayCircle, RefreshCw, Tv } from 'lucide-react' + +import { imageURL } from '../api/client' +import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle' +import { MediaCard } from '../components/MediaCard' +import type { Library, Media } from '../types' +import { artworkScore, groupSeries, seriesCardLink, type SeriesCard } from '../utils/groupSeries' + +export type LibraryPreview = { + library: Library + items: Media[] + total: number + cards: SeriesCard[] +} + +const TYPE_ICONS: Record = { + movie: , + tv: , + anime: , + variety: , + music: , +} + +const TYPE_LABELS: Record = { + movie: '电影', + tv: '剧集', + anime: '动漫', + variety: '综艺', + music: '音乐', +} + +export function LibrariesHeader({ + previewCount, + total, + repairMsg, + repairEpisodeArtwork, + repairing, + onRepairEpisodeArtworkChange, + onRepairRescrape, +}: { + previewCount: number + total: number + repairMsg: string + repairEpisodeArtwork: boolean + repairing: boolean + onRepairEpisodeArtworkChange: (value: boolean) => void + onRepairRescrape: () => void +}) { + return ( +
+
+

媒体库

+

+ 共 {previewCount} 个目录 · {total.toLocaleString()} 个条目。每个目录直接展示最新入库内容。 +

+
+
+ {repairMsg && {repairMsg}} + + + + 管理媒体库 + + +
+
+ ) +} + +export function LibrariesEmptyState() { + return ( +
+ +

暂无媒体库,请到管理后台添加目录。

+
+ ) +} + +export function LibrariesContent({ previews }: { previews: LibraryPreview[] }) { + return ( + <> +
+
+

媒体库入口

+

按目录进入完整媒体库;下方每个目录也会直接展示最新内容。

+
+
+ {previews.map((preview, index) => ( + + + + ))} +
+
+ +
+ {previews.map((preview, index) => ( + + + + ))} +
+ + ) +} + +function LibraryEntryCard({ preview }: { preview: LibraryPreview }) { + const library = preview.library + const artwork = libraryArtworkItems(preview.cards) + + return ( + +
+ {artwork.length > 0 ? ( + artwork.map(({ src, version }, index) => ( + { event.currentTarget.style.visibility = 'hidden' }} + /> + )) + ) : ( +
+ {TYPE_ICONS[library.type] ?? } +
+ )} +
+
+
+
+ {TYPE_LABELS[library.type] ?? library.type} +
+

+ {library.name} +

+

{library.path}

+
+
+ {preview.total.toLocaleString()} 个条目 + 浏览全部 +
+
+ + ) +} + +function LibraryShelf({ preview }: { preview: LibraryPreview }) { + const library = preview.library + const cards = preview.cards.slice(0, 10) + + return ( +
+
+
+
+ {TYPE_ICONS[library.type] ?? } + {TYPE_LABELS[library.type] ?? library.type} +
+

{library.name}

+

+ {library.path} · {preview.total.toLocaleString()} 个条目 · 最新 {cards.length} 部 +

+
+ + 浏览全部 + + +
+ + {cards.length > 0 ? ( +
+ {cards.map((card) => ( +
+ +
+ ))} +
+ ) : ( +
+ 该目录暂无可展示内容,扫描媒体库后会出现在这里。 +
+ )} +
+ ) +} + +export function latestLibraryCards(items: Media[]): SeriesCard[] { + return groupSeries(items) + .sort((a, b) => mediaTime(b.rep) - mediaTime(a.rep) || artworkScore(b.rep) - artworkScore(a.rep)) + .slice(0, 10) +} + +function libraryArtworkItems(cards: SeriesCard[]): Array<{ src: string; version?: string }> { + return [...cards] + .sort((a, b) => artworkScore(b.rep) - artworkScore(a.rep) || mediaTime(b.rep) - mediaTime(a.rep)) + .map((card) => ({ + src: card.rep.poster_url || card.rep.backdrop_url || '', + version: card.rep.updated_at, + })) + .filter((item) => Boolean(item.src)) + .slice(0, 4) +} + +function mediaTime(media: Media): number { + return Date.parse(media.updated_at || media.created_at || '') || 0 +}