mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
bug处理
This commit is contained in:
@@ -124,6 +124,7 @@ export function LibraryPage() {
|
||||
})
|
||||
|
||||
const {
|
||||
resolvingSeries,
|
||||
selectedEpisodes,
|
||||
visibleEpisodes,
|
||||
selectedSeriesEpisodes,
|
||||
@@ -132,6 +133,7 @@ export function LibraryPage() {
|
||||
handleSeriesClick,
|
||||
clearSelectedSeries,
|
||||
} = useLibrarySeriesSelection({
|
||||
libraryID: id,
|
||||
items,
|
||||
seriesEpisodeItems,
|
||||
isSeriesLibrary,
|
||||
@@ -209,7 +211,9 @@ export function LibraryPage() {
|
||||
return <>{actions}</>
|
||||
}, [canFavorite, favouriteBusyID, handleToggleFavourite, isFavourite, movieActions])
|
||||
|
||||
if (loading) {
|
||||
// resolvingSeries:深链目标剧集不在已加载的一页卡片里,正在按 key
|
||||
// 单独解析。此时先显示加载态,避免先渲染整个媒体库列表再跳走。
|
||||
if (loading || resolvingSeries) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-32">
|
||||
<motion.div animate={{ opacity: [0.4, 1, 0.4] }} transition={{ repeat: Infinity, duration: 2 }} className="flex items-center gap-3">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import type { Media } from '../types'
|
||||
import {
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
specialSectionForMedia,
|
||||
type SeriesCard,
|
||||
} from '../utils/groupSeries'
|
||||
import { resolveSeriesCardByKey } from '../utils/seriesCardResolve'
|
||||
|
||||
type SeasonEpisodes = {
|
||||
season: number
|
||||
@@ -16,6 +17,7 @@ type SeasonEpisodes = {
|
||||
}
|
||||
|
||||
type UseLibrarySeriesSelectionOptions = {
|
||||
libraryID: string
|
||||
items: Media[]
|
||||
seriesEpisodeItems: Media[]
|
||||
isSeriesLibrary: boolean
|
||||
@@ -32,6 +34,7 @@ type UseLibrarySeriesSelectionOptions = {
|
||||
}
|
||||
|
||||
export function useLibrarySeriesSelection({
|
||||
libraryID,
|
||||
items,
|
||||
seriesEpisodeItems,
|
||||
isSeriesLibrary,
|
||||
@@ -46,6 +49,14 @@ export function useLibrarySeriesSelection({
|
||||
setSelectedSeason,
|
||||
onClearSeriesState,
|
||||
}: UseLibrarySeriesSelectionOptions) {
|
||||
// 深链(首页最新条目 / 海报墙 ?series=<key>)指向的剧集不在已加载的
|
||||
// 一页卡片里时,需要单独按 key 解析;resolvingSeries 让页面在这段时间
|
||||
// 显示加载态,而不是先闪一下整个媒体库列表。
|
||||
const [resolvingSeries, setResolvingSeries] = useState(false)
|
||||
const [pendingSeriesKey, setPendingSeriesKey] = useState('')
|
||||
// 当前 URL 请求的剧集 key(非剧集库 / 加载中 / 没带参数时为空)。
|
||||
const requestedSeriesKey = !loading && isSeries ? (searchParams.get('series') ?? '') : ''
|
||||
|
||||
const selectedEpisodes = useMemo(() => {
|
||||
const sourceItems = isSeriesLibrary ? seriesEpisodeItems : items
|
||||
if (!selectedSeries || sourceItems.length === 0) return []
|
||||
@@ -92,20 +103,53 @@ export function useLibrarySeriesSelection({
|
||||
useEffect(() => {
|
||||
if (loading) return
|
||||
if (!isSeries) {
|
||||
setPendingSeriesKey('')
|
||||
setSelectedSeries(null)
|
||||
setSelectedSeason(null)
|
||||
return
|
||||
}
|
||||
|
||||
const key = searchParams.get('series')
|
||||
if (!key) {
|
||||
if (!requestedSeriesKey) {
|
||||
setPendingSeriesKey('')
|
||||
setSelectedSeries(null)
|
||||
return
|
||||
}
|
||||
|
||||
const next = seriesCards.find((card) => card.key === key)
|
||||
setSelectedSeries(next ?? null)
|
||||
}, [isSeries, loading, searchParams, seriesCards, setSelectedSeason, setSelectedSeries])
|
||||
const next = seriesCards.find((card) => card.key === requestedSeriesKey)
|
||||
if (next) {
|
||||
setPendingSeriesKey('')
|
||||
setSelectedSeries(next)
|
||||
return
|
||||
}
|
||||
|
||||
// 目标剧集尚未加载到(远程库分页 / 大库分页):登记待解析 key。
|
||||
setPendingSeriesKey((prev) => (prev === requestedSeriesKey ? prev : requestedSeriesKey))
|
||||
}, [isSeries, loading, requestedSeriesKey, seriesCards, setSelectedSeason, setSelectedSeries])
|
||||
|
||||
useEffect(() => {
|
||||
// pendingSeriesKey 必须仍然是 URL 上请求的 key:切换媒体库/清空参数
|
||||
// 时直接丢弃待解析状态,避免把别的库的剧集解析进来。
|
||||
if (!pendingSeriesKey || pendingSeriesKey !== requestedSeriesKey) {
|
||||
setResolvingSeries(false)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
setResolvingSeries(true)
|
||||
resolveSeriesCardByKey(libraryID, pendingSeriesKey)
|
||||
.then((card) => {
|
||||
if (cancelled) return
|
||||
setResolvingSeries(false)
|
||||
setSelectedSeries(card)
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setResolvingSeries(false)
|
||||
setSelectedSeries(null)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [libraryID, pendingSeriesKey, requestedSeriesKey, setSelectedSeries])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSeries || selectedEpisodes.length === 0) {
|
||||
@@ -135,6 +179,7 @@ export function useLibrarySeriesSelection({
|
||||
}
|
||||
|
||||
return {
|
||||
resolvingSeries,
|
||||
selectedEpisodes,
|
||||
visibleEpisodes,
|
||||
selectedSeriesEpisodes,
|
||||
|
||||
@@ -36,6 +36,7 @@ export type SeriesCard = {
|
||||
linkMedia?: Media
|
||||
linkLibraryId?: string
|
||||
count: number
|
||||
is_series?: boolean
|
||||
last_added_at?: string
|
||||
}
|
||||
|
||||
@@ -223,6 +224,7 @@ export function seasonLabel(season: number): string {
|
||||
export function isSeriesCard(card: SeriesCard): boolean {
|
||||
const linkMedia = card.linkMedia ?? card.rep
|
||||
return (
|
||||
card.is_series === true ||
|
||||
card.count > 1 ||
|
||||
isEpisodeLike(card.rep) ||
|
||||
isEpisodeLike(linkMedia) ||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { libraryAPI, mediaAPI } from '../api/library'
|
||||
import type { Media } from '../types'
|
||||
import { groupSeries, type SeriesCard } from './groupSeries'
|
||||
import { isRemoteEmbyID } from './remoteEmby'
|
||||
|
||||
/**
|
||||
* 按 series key 反解出剧集卡片。
|
||||
*
|
||||
* 首页「最新条目」、海报墙等入口点击后带 ?series=<key> 深链进媒体库页,
|
||||
* 但媒体库页每次只加载一页剧集卡片(远程 Emby 库 100 条),目标剧集不在
|
||||
* 这一页时列表里找不到对应卡片,页面就退化成整个媒体库列表。这里按 key
|
||||
* 单独解析目标剧集,与分页无关:
|
||||
*
|
||||
* - 远程 Emby 的 key 本身就是(伪装后的)剧集 ID,直接取该条目详情;
|
||||
* - 本地 key 是分组 hash,改取该剧集的集列表,再折叠出代表条目。
|
||||
*
|
||||
* key 失效(剧集已删除等)时返回 null,调用方保持"整库列表"原状态。
|
||||
*/
|
||||
export async function resolveSeriesCardByKey(libraryID: string, key: string): Promise<SeriesCard | null> {
|
||||
if (!libraryID || !key) return null
|
||||
|
||||
if (isRemoteEmbyID(key)) {
|
||||
const rep = await fetchRemoteSeriesMedia(key)
|
||||
if (rep) {
|
||||
return { key, rep, linkMedia: rep, count: 0, is_series: true }
|
||||
}
|
||||
}
|
||||
|
||||
const episodes = await fetchSeriesEpisodes(libraryID, key)
|
||||
if (episodes.length === 0) return null
|
||||
// 与列表页同一套折叠规则,选出海报/元数据最好的那一集做代表卡片。
|
||||
const [grouped] = groupSeries(episodes)
|
||||
const rep = grouped?.rep ?? episodes[0]
|
||||
return {
|
||||
key,
|
||||
rep,
|
||||
linkMedia: grouped?.linkMedia ?? rep,
|
||||
count: episodes.length,
|
||||
is_series: true,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRemoteSeriesMedia(id: string): Promise<Media | null> {
|
||||
try {
|
||||
const media = await mediaAPI.get(id)
|
||||
return media?.id ? media : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSeriesEpisodes(libraryID: string, key: string): Promise<Media[]> {
|
||||
try {
|
||||
const page = await libraryAPI.listSeriesEpisodes(libraryID, key)
|
||||
return Array.isArray(page.items) ? page.items : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user