From 60f05129c980595869616fd35bf6133d1289d279 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:56:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/HomePageSections.tsx | 212 +++++++++------ web/src/pages/LibrariesPageSections.tsx | 337 ++++-------------------- 2 files changed, 174 insertions(+), 375 deletions(-) diff --git a/web/src/pages/HomePageSections.tsx b/web/src/pages/HomePageSections.tsx index 31d548b..f1bc396 100644 --- a/web/src/pages/HomePageSections.tsx +++ b/web/src/pages/HomePageSections.tsx @@ -9,6 +9,7 @@ import { FolderOpen, Library as LibraryIcon, Music, + Pin, Play, PlayCircle, Sparkles, @@ -355,11 +356,19 @@ export function HomeLibrariesSection({ libraryData, libraryCounts, onNeedPreviews, + pinnedIds = [], + onTogglePin, + showAllLink = true, + title = '媒体库', }: { libraries: Library[] libraryData?: Record libraryCounts: Record onNeedPreviews?: (ids: string[], limit?: number) => void + pinnedIds?: string[] + onTogglePin?: (libraryId: string) => void + showAllLink?: boolean + title?: string }) { const PAGE_SIZE = 20 const [currentPage, setCurrentPage] = useState(1) @@ -382,7 +391,7 @@ export function HomeLibrariesSection({

- 媒体库 + {title}

{libraries.length > PAGE_SIZE && ( @@ -396,41 +405,45 @@ export function HomeLibrariesSection({
-
- {totalPages > 1 && ( -
- - - {effectivePage} / {totalPages} - - -
- )} + {(totalPages > 1 || showAllLink) && ( +
+ {totalPages > 1 && ( +
+ + + {effectivePage} / {totalPages} + + +
+ )} - - 全部媒体库 - - -
+ {showAllLink && ( + + 全部媒体库 + + + )} +
+ )}
@@ -440,6 +453,8 @@ export function HomeLibrariesSection({ library={lib} count={libraryCounts[lib.id] ?? 0} cards={libraryData?.[lib.id]?.cards ?? []} + pinned={pinnedIds.includes(lib.id)} + onTogglePin={onTogglePin ? () => onTogglePin(lib.id) : undefined} onVisible={() => { if (!lib.cover_url) { queuePreview(lib.id) @@ -452,73 +467,100 @@ export function HomeLibrariesSection({ ) } -function HomeLibraryCard({ +export function HomeLibraryCard({ library, count, cards, + pinned = false, + onTogglePin, onVisible, }: { library: Library count: number cards: SeriesCard[] + pinned?: boolean + onTogglePin?: () => void onVisible: () => void }) { - const ref = useInViewOnce(onVisible) + const ref = useInViewOnce(onVisible) const artwork = getLibraryArtworks(library, cards) return ( - -
1 ? 'grid grid-cols-2 gap-0.5' : '' - }`} + - {artwork.length > 0 ? ( - artwork.map(({ src, version }, index) => ( - { - event.currentTarget.style.visibility = 'hidden' - }} - /> - )) - ) : ( -
- {TYPE_ICONS[library.type] || } -
- )} - -
- {TYPE_LABELS[library.type] || '自定义'} -
-
- -
-

1 ? 'grid grid-cols-2 gap-0.5' : '' + }`} > - {library.name} -

-

- {count > 0 ? `${count} 部媒体` : '暂无条目'} -

-
- + {artwork.length > 0 ? ( + artwork.map(({ src, version }, index) => ( + { + event.currentTarget.style.visibility = 'hidden' + }} + /> + )) + ) : ( +
+ {TYPE_ICONS[library.type] || } +
+ )} + +
+ {TYPE_LABELS[library.type] || '自定义'} +
+
+ +
+

+ {library.name} +

+

+ {count > 0 ? `${count} 部媒体` : '暂无条目'} +

+
+ + + {onTogglePin && ( + + )} + ) } diff --git a/web/src/pages/LibrariesPageSections.tsx b/web/src/pages/LibrariesPageSections.tsx index c15d54a..d9c4f46 100644 --- a/web/src/pages/LibrariesPageSections.tsx +++ b/web/src/pages/LibrariesPageSections.tsx @@ -1,33 +1,10 @@ -import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Link } from 'react-router-dom' -import { ArrowRight, ChevronLeft, ChevronRight, Film, FolderOpen, Library as LibraryIcon, Music, Pin, PlayCircle, RefreshCw, Sparkles, Tv } from 'lucide-react' +import { Library as LibraryIcon, RefreshCw, Sparkles } from 'lucide-react' -import { imageURL } from '../api/client' import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle' -import { MediaCard } from '../components/MediaCard' -import { useInViewOnce } from '../hooks/useInViewOnce' -import { useLazyPreviewBatch } from '../hooks/useLazyPreviewBatch' -import { seriesCardLink } from '../utils/groupSeries' -import { libraryDisplayPath } from './libraryDisplayModel' -import { libraryArtworkItems, type LibraryPreview } from './librariesPageModel' -import { isLibraryPinned } from '../utils/pinnedLibraries' - -const TYPE_ICONS: Record = { - movie: , - tv: , - anime: , - variety: , - music: , - adult: , -} -const TYPE_LABELS: Record = { - movie: '电影', - tv: '剧集', - anime: '动漫', - variety: '综艺', - music: '音乐', - adult: '成人', -} +import { HomeLibrariesSection, HomeLibraryRowSection } from './HomePageSections' +import type { LibraryPreview } from './librariesPageModel' export function LibrariesHeader({ previewCount, @@ -96,9 +73,9 @@ export function LibrariesHeader({ export function LibrariesEmptyState() { return ( -
- -

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

+
+ +

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

) } @@ -114,22 +91,17 @@ export function LibrariesContent({ onTogglePin: (libraryId: string) => void onNeedPreviews?: (ids: string[], limit?: number) => void }) { - const pinnedCount = previews.filter((preview) => isLibraryPinned(preview.library.id, pinnedIds)).length - const queuePreview = useLazyPreviewBatch((ids) => onNeedPreviews?.(ids, 2)) - - // 下方媒体库货架支持向下滑动渐进流式加载:默认先展示前 3 个库货架, - // 随着用户向下滑动接近底部,通过 IntersectionObserver 动态解锁后续媒体库货架。 + // 下方媒体库货架继续按需解锁:首屏先展示前 3 个,滚动接近底部再加载 2 个。 const INITIAL_SHELVES = 3 const STEP_SHELVES = 2 const [visibleCount, setVisibleCount] = useState(INITIAL_SHELVES) const sentinelRef = useRef(null) useEffect(() => { - const currentTargets = previews.slice(0, visibleCount).map((p) => p.library.id) + const currentTargets = previews.slice(0, visibleCount).map((preview) => preview.library.id) onNeedPreviews?.(currentTargets, 10) }, [previews, visibleCount, onNeedPreviews]) - // 底部哨兵监听与滚动双保险(触底解锁后续媒体库货架) useEffect(() => { const scrollParent = document.getElementById('app-main-scroll') if (!scrollParent) return @@ -172,111 +144,53 @@ export function LibrariesContent({ return () => { scrollParent.removeEventListener('scroll', handleCheckBottom) - if (observer) { - observer.disconnect() - } + if (observer) observer.disconnect() } }, [visibleCount, previews.length]) - const visiblePreviews = useMemo(() => { - return previews.slice(0, visibleCount) - }, [previews, visibleCount]) - - const ENTRY_PAGE_SIZE = 20 - const [entryPage, setEntryPage] = useState(1) - const totalEntryPages = Math.max(1, Math.ceil(previews.length / ENTRY_PAGE_SIZE)) - const effectiveEntryPage = Math.min(entryPage, totalEntryPages) - - const pagedPreviews = useMemo(() => { - const start = (effectiveEntryPage - 1) * ENTRY_PAGE_SIZE - return previews.slice(start, start + ENTRY_PAGE_SIZE) - }, [previews, effectiveEntryPage]) - - // 入口网格首屏展示前 20 个库;没有自定义封面的库需要立即拉预览,否则卡片会先渲染占位图标再补图。 - useEffect(() => { - const ids = pagedPreviews.filter((preview) => !preview.library.cover_url).map((preview) => preview.library.id) - if (ids.length > 0) { - onNeedPreviews?.(ids, 2) - } - }, [pagedPreviews, onNeedPreviews]) - + const visiblePreviews = useMemo( + () => previews.slice(0, visibleCount), + [previews, visibleCount], + ) + const libraries = useMemo(() => previews.map((preview) => preview.library), [previews]) + const libraryData = useMemo( + () => + Object.fromEntries( + previews.map((preview) => [ + preview.library.id, + { cards: preview.cards, items: [], total: preview.total }, + ]), + ), + [previews], + ) + const libraryCounts = useMemo( + () => + Object.fromEntries( + previews.map((preview) => [preview.library.id, preview.total]), + ), + [previews], + ) return ( <> -
-
-
-
-

媒体库入口

- {previews.length > ENTRY_PAGE_SIZE && ( - - 共 {previews.length} 个 · 每页 {ENTRY_PAGE_SIZE} 个 - - )} -
-

- 按目录进入完整媒体库;下方每个目录也会直接展示最新内容。 - {pinnedCount > 0 ? ` 已置顶 ${pinnedCount} 个媒体库。` : ' 点击卡片右上角图钉可置顶常用媒体库。'} -

-
- - {totalEntryPages > 1 && ( -
- - - {effectiveEntryPage} / {totalEntryPages} - - -
- )} -
-
- {pagedPreviews.map((preview) => ( -
- onTogglePin(preview.library.id)} - onVisible={() => { - if (!preview.library.cover_url) { - queuePreview(preview.library.id) - } - }} - /> -
- ))} -
-
+ {visiblePreviews.length > 0 && ( -
+
{visiblePreviews.map((preview) => ( -
- +
))} @@ -294,160 +208,3 @@ export function LibrariesContent({ ) } - -function LibraryEntryCard({ - preview, - pinned, - onTogglePin, - onVisible, -}: { - preview: LibraryPreview - pinned: boolean - onTogglePin: () => void - onVisible?: () => void -}) { - const library = preview.library - const ref = useInViewOnce(onVisible ?? (() => {})) - const artwork = library.cover_url - ? [{ src: library.cover_url, version: library.updated_at }] - : libraryArtworkItems(preview.cards) - const displayPath = libraryDisplayPath(library.path) - - return ( -
- - -
1 ? 'grid-cols-2' : 'grid-cols-1'}`}> - {artwork.length > 0 ? ( - artwork.map(({ src, version }, index) => ( - { event.currentTarget.style.visibility = 'hidden' }} - /> - )) - ) : ( -
- {TYPE_ICONS[library.type] ?? } -
- )} -
-
-
-
- - {TYPE_LABELS[library.type] ?? library.type} - - - {preview.total.toLocaleString()} 个条目 - -
-

- {library.name} - {pinned ? 置顶 : null} -

-

- {displayPath} -

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

- {library.name} - {pinned ? 置顶 : null} -

-

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

-
- - 浏览全部 - - -
- - {cards.length > 0 ? ( -
- {cards.map((card) => ( -
- -
- ))} -
- ) : ( -
- 该目录暂无可展示内容,扫描媒体库后会出现在这里。 -
- )} -
- ) -}