This commit is contained in:
truewhile
2026-09-04 09:06:15 +08:00
parent fc84291346
commit cd720ae879
8 changed files with 250 additions and 82 deletions
+13 -6
View File
@@ -5,6 +5,7 @@ import { historyAPI } from '../api/history'
import type { HistoryItem } from '../api/playback'
import type { Library, Media } from '../types'
import type { SeriesCard } from '../utils/groupSeries'
import { fetchLibraries, peekLibraries } from '../utils/libraryCache'
import { usePinnedLibraries } from '../hooks/usePinnedLibraries'
import { sortByPinnedIds } from '../utils/pinnedLibraries'
import {
@@ -18,7 +19,6 @@ import {
} from './HomePageSections'
const hasArtwork = (media?: Media | null) => !!(media?.poster_url || media?.backdrop_url)
const asArray = <T,>(value: unknown): T[] => (Array.isArray(value) ? (value as T[]) : [])
export function HomePage() {
const [libraries, setLibraries] = useState<Library[]>([])
@@ -30,17 +30,24 @@ export function HomePage() {
const [historyLoading, setHistoryLoading] = useState(true)
const { pinnedIds } = usePinnedLibraries()
// 1. 媒体库元数据极速加载(不带 preview,毫秒级秒开首屏)
// 1. 媒体库元数据极速加载(不带 preview,毫秒级秒开首屏)。
// 会话内已有缓存则先用缓存立即渲染,后台仍刷新一次兜底。
useEffect(() => {
let cancelled = false
libraryAPI
.list()
.then((rows) => asArray<Library>(rows))
const cached = peekLibraries()
if (cached && cached.length > 0) {
setLibraries(cached)
setLibrariesLoading(false)
}
fetchLibraries()
.then((rows) => (Array.isArray(rows) ? rows : []))
.catch(() => [] as Library[])
.then((libs) => {
if (cancelled) return
setLibraries(libs)
// 失败兜底返回空数组时,不要覆盖已先行渲染的缓存数据
setLibraries((prev) => (libs.length === 0 && prev.length > 0 ? prev : libs))
setLibrariesLoading(false)
})
+13 -4
View File
@@ -12,6 +12,7 @@ import {
import type { LibraryPreview } from './librariesPageModel'
import type { Library } from '../types'
import type { SeriesCard } from '../utils/groupSeries'
import { fetchLibraries, invalidateLibraries, peekLibraries } from '../utils/libraryCache'
import { sortLibraryPreviews } from '../utils/pinnedLibraries'
export function LibrariesPage() {
@@ -53,10 +54,18 @@ export function LibrariesPage() {
}
}, [])
const loadLibraries = useCallback(async () => {
setLoading(true)
const loadLibraries = useCallback(async (options?: { force?: boolean }) => {
if (options?.force) invalidateLibraries()
// 会话缓存命中时先行渲染,避免每次进入都白等一轮请求
const cached = peekLibraries()
if (cached) {
setLibraries(cached)
setLoading(false)
} else {
setLoading(true)
}
try {
const libs = await libraryAPI.list()
const libs = await fetchLibraries()
setLibraries(libs)
// 优先拉取入口卡片网格当前页(前 20 个库)的预览
const topIds = libs.slice(0, 20).map((l) => l.id)
@@ -82,7 +91,7 @@ export function LibrariesPage() {
const handleManageLibraries = async () => {
await openManageLibrariesDialog()
await loadLibraries()
await loadLibraries({ force: true })
}
useEffect(() => {
+7 -2
View File
@@ -3,6 +3,7 @@ import toast from 'react-hot-toast'
import { libraryAPI } from '../api/library'
import type { Library, LibraryRoot } from '../types'
import { invalidateLibraries } from '../utils/libraryCache'
import { confirmAction } from '../components/confirmAction'
import { apiErrorMessage, createRootPayload, displayLibraryRootName, displayLibraryRootPath, emptyRootDraft, rootDraftKey, type RootDraft } from './adminLibraryPanelModel'
@@ -18,10 +19,14 @@ export function useAdminLibraryPanel() {
function useAdminLibraryList() {
const [libs, setLibs] = useState<Library[]>([])
const refresh = () =>
libraryAPI
const refresh = () => {
// 后台任何库变更都会走到这里;顺带清掉前台会话缓存,
// 避免返回首页/媒体库页后 30 秒 TTL 内还显示旧列表。
invalidateLibraries()
return libraryAPI
.list({ includeHidden: true })
.then((libs) => setLibs(libs.filter((l) => !l.is_remote_emby))) // 远程挂载库只读,不在后台管理列表内
}
useEffect(() => {
refresh().catch(() => undefined)
+52 -46
View File
@@ -3,6 +3,7 @@ import toast from 'react-hot-toast'
import { libraryAPI } from '../api/library'
import type { Library, Media } from '../types'
import { peekLibrary, resolveLibrary } from '../utils/libraryCache'
import { groupSeries, isEpisodeLike, type SeriesCard } from '../utils/groupSeries'
export function useLibraryData(libraryID: string, selectedSeries: SeriesCard | null) {
@@ -25,74 +26,79 @@ export function useLibraryData(libraryID: string, selectedSeries: SeriesCard | n
return groupSeries(items)
}, [isSeries, isSeriesLibrary, items, serverSeriesCards])
// reloadCurrentLibrary 通过自增 tick 重跑加载(原实现靠克隆 library 对象
// 触发第二个 effect,这里合并成单个 bootstrap effect 后改用显式信号)。
const [reloadTick, setReloadTick] = useState(0)
useEffect(() => {
if (!libraryID) return
let cancelled = false
setLoading(true)
setLoadingAll(false)
setLibrary(null)
setItems([])
setServerSeriesCards([])
setSeriesEpisodeItems([])
libraryAPI.get(libraryID)
.then((lib) => {
if (!cancelled) setLibrary(lib)
})
.catch(() => {
const bootstrap = async () => {
// 库信息先查会话缓存(首页/全部媒体库页已拉过全量列表),命中则
// 同步就绪,内容请求在挂载当帧即发出;未命中才退回单独请求,
// 消除原先"先等库信息、再等内容"的两段串行首屏等待。
let lib = peekLibrary(libraryID)
if (lib) setLibrary(lib)
try {
const resolved = await resolveLibrary(libraryID)
if (cancelled) return
if (!lib) {
lib = resolved
setLibrary(lib)
}
} catch {
if (!cancelled) {
setLibrary(null)
setLoading(false)
toast.error('媒体库不存在或无权限')
}
})
return () => { cancelled = true }
}, [libraryID])
useEffect(() => {
if (!libraryID || !library) return
let cancelled = false
setLoading(true)
setLoadingAll(true)
setItems([])
setServerSeriesCards([])
setSeriesEpisodeItems([])
const loadAll = async () => {
if (isSeriesLibrary) {
const collected = await loadAllSeriesCards(libraryID, library.is_remote_emby, (next) => {
if (cancelled) return
setTotal(next.total)
if (next.firstPage) {
setServerSeriesCards(next.items)
setLoading(false)
}
})
if (!cancelled) setServerSeriesCards(collected.items)
return
}
const collected = await loadAllMedia(libraryID, library.is_remote_emby, (next) => {
if (cancelled) return
setTotal(next.total)
if (next.firstPage) {
setItems(next.items)
setLoading(false)
const seriesLibrary = isSeriesLibraryType(lib.type)
setLoadingAll(true)
try {
if (seriesLibrary) {
const collected = await loadAllSeriesCards(libraryID, lib.is_remote_emby, (next) => {
if (cancelled) return
setTotal(next.total)
if (next.firstPage) {
setServerSeriesCards(next.items)
setLoading(false)
}
})
if (!cancelled) setServerSeriesCards(collected.items)
return
}
})
if (!cancelled) setItems(collected.items)
}
loadAll()
.catch(() => {
const collected = await loadAllMedia(libraryID, lib.is_remote_emby, (next) => {
if (cancelled) return
setTotal(next.total)
if (next.firstPage) {
setItems(next.items)
setLoading(false)
}
})
if (!cancelled) setItems(collected.items)
} catch {
if (!cancelled) toast.error('媒体库加载失败')
})
.finally(() => {
} finally {
if (!cancelled) {
setLoading(false)
setLoadingAll(false)
}
})
}
}
void bootstrap()
return () => { cancelled = true }
}, [libraryID, library, isSeriesLibrary])
}, [libraryID, reloadTick])
useEffect(() => {
if (!libraryID || !isSeriesLibrary || !selectedSeries) {
@@ -117,7 +123,7 @@ export function useLibraryData(libraryID: string, selectedSeries: SeriesCard | n
}, [libraryID, isSeriesLibrary, selectedSeries])
const reloadCurrentLibrary = useCallback(() => {
setLibrary((current) => (current ? { ...current } : current))
setReloadTick((tick) => tick + 1)
}, [])
const loadingAllText = loadingAll && !loading && (isSeriesLibrary ? total > serverSeriesCards.length : total > items.length)
+17 -11
View File
@@ -143,21 +143,27 @@ function useMediaDetailRefresh({
setLoading(true)
setLoadingEpisodes(true)
try {
const nextMedia = await mediaAPI.get(id)
// 三个请求并行发出;详情一到就解锁整页渲染,收藏状态与分集列表
// 到达后各自补齐(原先完全串行,首屏要排队等满三个往返)。
const nextMediaPromise = mediaAPI.get(id)
const favouritesPromise = playbackAPI.listFavourites().catch(() => [])
const episodesPromise = mediaAPI
.getEpisodes(id)
.then((r) => r.items ?? [])
.catch(() => [])
const nextMedia = await nextMediaPromise
setMedia(nextMedia)
const favourites = await playbackAPI.listFavourites().catch(() => [])
setLoading(false)
const favourites = await favouritesPromise
setFavourite(favourites.some((item) => item.id === nextMedia.id))
// 异步加载分集列表
try {
const epRes = await mediaAPI.getEpisodes(id)
setEpisodes(epRes.items ?? [])
} catch {
setEpisodes([])
} finally {
setLoadingEpisodes(false)
}
const episodes = await episodesPromise
setEpisodes(episodes)
} finally {
setLoading(false)
setLoadingEpisodes(false)
}
}, [id, setFavourite, setLoading, setMedia, setEpisodes, setLoadingEpisodes])
}