This commit is contained in:
truewhile
2026-09-15 22:05:34 +08:00
parent ff86c2f597
commit a9b3110b2c
5 changed files with 264 additions and 65 deletions
+9 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import { Link, useLocation } from 'react-router-dom'
import {
ArrowRight,
ChevronLeft,
@@ -20,6 +20,7 @@ import {
import { imageURL } from '../api/client'
import { useInViewOnce } from '../hooks/useInViewOnce'
import { useLazyPreviewBatch } from '../hooks/useLazyPreviewBatch'
import { useRememberedListPosition } from '../hooks/useListPositionMemory'
import { MediaCard } from '../components/MediaCard'
import type { HistoryItem } from '../api/playback'
import type { Library, Media } from '../types'
@@ -371,7 +372,13 @@ export function HomeLibrariesSection({
title?: string
}) {
const PAGE_SIZE = 20
const [currentPage, setCurrentPage] = useState(1)
// 分页位置按路由分别记忆:首页「媒体库」和 /libraries 的「媒体库入口」
// 各自保留自己的页码,进入媒体库详情再返回时不会掉回第一页。
const { pathname } = useLocation()
const [currentPage, setCurrentPage] = useRememberedListPosition(
`libraries-grid:${pathname}`,
1,
)
const totalPages = Math.max(1, Math.ceil(libraries.length / PAGE_SIZE))
const effectivePage = Math.min(currentPage, totalPages)
const queuePreview = useLazyPreviewBatch((ids) => onNeedPreviews?.(ids, 2))
+13 -3
View File
@@ -1,11 +1,16 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { Link } from 'react-router-dom'
import { Library as LibraryIcon, RefreshCw, Sparkles } from 'lucide-react'
import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle'
import { useRememberedListPosition } from '../hooks/useListPositionMemory'
import { HomeLibrariesSection, HomeLibraryRowSection } from './HomePageSections'
import type { LibraryPreview } from './librariesPageModel'
// 返回媒体库列表时最多一次性解锁的货架数量:滚动到很深的位置后回来
// 仍能落到原处,同时避免一次渲染过多货架。上限之外的继续按需解锁。
const MAX_RESTORED_SHELVES = 15
export function LibrariesHeader({
previewCount,
total,
@@ -100,7 +105,12 @@ export function LibrariesContent({
// 下方媒体库货架继续按需解锁:首屏先展示前 3 个,滚动接近底部再加载 2 个。
const INITIAL_SHELVES = 3
const STEP_SHELVES = 2
const [visibleCount, setVisibleCount] = useState(INITIAL_SHELVES)
// 货架解锁数量同样跨路由记忆:进入媒体库详情再返回不会从头解锁。
const [visibleCount, setVisibleCount] = useRememberedListPosition(
'libraries-shelves',
INITIAL_SHELVES,
MAX_RESTORED_SHELVES,
)
const sentinelRef = useRef<HTMLButtonElement | null>(null)
const userScrolledRef = useRef(false)
@@ -109,7 +119,7 @@ export function LibrariesContent({
if (prev >= previews.length) return prev
return Math.min(prev + STEP_SHELVES, previews.length)
})
}, [previews.length])
}, [previews.length, setVisibleCount])
useEffect(() => {
const currentTargets = previews.slice(0, visibleCount).map((preview) => preview.library.id)
+93 -60
View File
@@ -6,6 +6,7 @@ import type { Library, Media } from '../types'
import { peekLibrary, resolveLibrary } from '../utils/libraryCache'
import { groupSeries, isEpisodeLike, type SeriesCard } from '../utils/groupSeries'
import type { SortField, SortOrder } from '../utils/mediaSort'
import { MAX_RESTORE_PAGES, readListPosition, writeListPosition } from '../hooks/useListPositionMemory'
export function useLibraryData(
libraryID: string,
@@ -43,6 +44,84 @@ export function useLibraryData(
const modeRef = useRef<'media' | 'series'>('media')
const moreInFlightRef = useRef(false)
// 分页位置按「媒体库 + 排序」记忆:详情页返回、甚至换完排序再切回来,
// 都能回到上次加载到的页数。
const positionKey = `library:${libraryID}:${sortField}:${sortOrder}`
// 拉取并追加下一页。滚动哨兵、按钮和首屏分页恢复共用这一条路径,
// 避免两套分页逻辑各自算页码。
const appendNextPage = useCallback(async (options?: { remember?: boolean }) => {
const lib = libraryRef.current
if (!lib || !hasMoreRef.current) return false
const seq = requestSeqRef.current
const page = nextPageRef.current
try {
if (modeRef.current === 'series') {
const data = await libraryAPI.listSeries(libraryID, page, pageSizeFor(lib), {
sort: sortField,
order: sortOrder,
})
if (seq !== requestSeqRef.current) return false
const pageItems = data.items ?? []
setServerSeriesCards((prev) => [...prev, ...pageItems])
loadedCountRef.current += pageItems.length
totalRef.current = data.total ?? totalRef.current
nextPageRef.current = page + 1
hasMoreRef.current = pageItems.length > 0 && loadedCountRef.current < totalRef.current
} else {
const data = await libraryAPI.listMedia(libraryID, page, pageSizeFor(lib), {
sort: sortField,
order: sortOrder,
})
if (seq !== requestSeqRef.current) return false
const pageItems = data.items ?? []
setItems((prev) => [...prev, ...pageItems])
loadedCountRef.current += pageItems.length
totalRef.current = data.total ?? totalRef.current
nextPageRef.current = page + 1
hasMoreRef.current = pageItems.length > 0 && loadedCountRef.current < totalRef.current
}
setTotal(totalRef.current)
setHasMore(hasMoreRef.current)
if (options?.remember !== false) writeListPosition(positionKey, page)
return true
} catch {
if (seq === requestSeqRef.current) {
toast.error('媒体库加载失败')
hasMoreRef.current = false
setHasMore(false)
}
return false
}
}, [libraryID, positionKey, sortField, sortOrder])
const loadMore = useCallback(async (options?: { remember?: boolean }) => {
if (moreInFlightRef.current || !hasMoreRef.current) return
moreInFlightRef.current = true
setLoadingMore(true)
try {
await appendNextPage(options)
} finally {
moreInFlightRef.current = false
setLoadingMore(false)
}
}, [appendNextPage])
const loadAll = useCallback(async () => {
const seq = requestSeqRef.current
setLoadingMore(true)
try {
while (seq === requestSeqRef.current && hasMoreRef.current) {
// 一次拉全量(random 排序前的准备)不代表用户的分页位置,不写入记忆。
await loadMore({ remember: false })
if (seq !== requestSeqRef.current || !hasMoreRef.current) break
await yieldToBrowser()
}
} finally {
if (seq === requestSeqRef.current) setLoadingMore(false)
}
}, [loadMore])
useEffect(() => {
if (!libraryID) return
const seq = ++requestSeqRef.current
@@ -106,6 +185,19 @@ export function useLibraryData(
setTotal(totalRef.current)
hasMoreRef.current = loadedCountRef.current < totalRef.current
setHasMore(hasMoreRef.current)
// 按记忆的分页位置把后续页补回来:从详情页返回时直接回到上次翻到的
// 位置,而不是只剩第一页。超出上限的部分继续交给底部哨兵按需加载。
const restoreTarget = Math.min(readListPosition(positionKey, 1), MAX_RESTORE_PAGES)
while (
!cancelled &&
seq === requestSeqRef.current &&
hasMoreRef.current &&
nextPageRef.current <= restoreTarget
) {
const advanced = await appendNextPage()
if (!advanced) break
}
} catch {
if (!cancelled && seq === requestSeqRef.current) {
toast.error('媒体库加载失败')
@@ -122,7 +214,7 @@ export function useLibraryData(
cancelled = true
requestSeqRef.current += 1
}
}, [libraryID, reloadTick, sortField, sortOrder])
}, [appendNextPage, libraryID, positionKey, reloadTick, sortField, sortOrder])
useEffect(() => {
if (!libraryID || !isSeriesLibrary || !selectedSeries) {
@@ -146,65 +238,6 @@ export function useLibraryData(
return () => { cancelled = true }
}, [libraryID, isSeriesLibrary, selectedSeries])
const loadMore = useCallback(async () => {
const lib = libraryRef.current
if (!lib || moreInFlightRef.current || !hasMoreRef.current) return
const seq = requestSeqRef.current
moreInFlightRef.current = true
setLoadingMore(true)
try {
const page = nextPageRef.current
if (modeRef.current === 'series') {
const data = await libraryAPI.listSeries(libraryID, page, pageSizeFor(lib), {
sort: sortField,
order: sortOrder,
})
if (seq !== requestSeqRef.current) return
const pageItems = data.items ?? []
setServerSeriesCards((prev) => [...prev, ...pageItems])
loadedCountRef.current += pageItems.length
totalRef.current = data.total ?? totalRef.current
nextPageRef.current = page + 1
hasMoreRef.current = pageItems.length > 0 && loadedCountRef.current < totalRef.current
} else {
const data = await libraryAPI.listMedia(libraryID, page, pageSizeFor(lib), {
sort: sortField,
order: sortOrder,
})
if (seq !== requestSeqRef.current) return
const pageItems = data.items ?? []
setItems((prev) => [...prev, ...pageItems])
loadedCountRef.current += pageItems.length
totalRef.current = data.total ?? totalRef.current
nextPageRef.current = page + 1
hasMoreRef.current = pageItems.length > 0 && loadedCountRef.current < totalRef.current
}
setTotal(totalRef.current)
setHasMore(hasMoreRef.current)
} catch {
toast.error('媒体库加载失败')
hasMoreRef.current = false
setHasMore(false)
} finally {
moreInFlightRef.current = false
setLoadingMore(false)
}
}, [libraryID, sortField, sortOrder])
const loadAll = useCallback(async () => {
const seq = requestSeqRef.current
setLoadingMore(true)
try {
while (seq === requestSeqRef.current && hasMoreRef.current) {
await loadMore()
if (seq !== requestSeqRef.current || !hasMoreRef.current) break
await yieldToBrowser()
}
} finally {
if (seq === requestSeqRef.current) setLoadingMore(false)
}
}, [loadMore])
const reloadCurrentLibrary = useCallback(() => {
setReloadTick((tick) => tick + 1)
}, [])