diff --git a/web/src/pages/LibraryPage.tsx b/web/src/pages/LibraryPage.tsx index 2448868..c211dba 100644 --- a/web/src/pages/LibraryPage.tsx +++ b/web/src/pages/LibraryPage.tsx @@ -132,6 +132,7 @@ export function LibraryPage() { selectedEpisodes, visibleEpisodes, selectedSeriesEpisodes, + selectedSeriesAllEpisodes, selectedSeriesMediaIDs, handleSeriesClick, clearSelectedSeries, @@ -172,7 +173,7 @@ export function LibraryPage() { role, library, selectedSeries, - selectedSeriesEpisodes, + selectedSeriesEpisodes: selectedSeriesAllEpisodes, reloadCurrentLibrary, clearSelectedSeries, setManualMovie, diff --git a/web/src/pages/useLibrarySeriesSelection.ts b/web/src/pages/useLibrarySeriesSelection.ts index 12764e3..7d55086 100644 --- a/web/src/pages/useLibrarySeriesSelection.ts +++ b/web/src/pages/useLibrarySeriesSelection.ts @@ -2,6 +2,7 @@ import { useEffect, useMemo } from 'react' import type { Media } from '../types' import { + expandSeriesMediaVersions, getSeriesKey, isTheatricalFeature, seasonSortOrder, @@ -77,11 +78,17 @@ export function useLibrarySeriesSelection({ [selectedEpisodes], ) - const selectedSeriesMediaIDs = useMemo( - () => selectedSeriesEpisodes.filter((ep) => !isTheatricalFeature(ep)).map((ep) => ep.id), + // 批量/整剧操作必须覆盖被折叠进 versions 的每一行, 否则只作用到代表行。 + const selectedSeriesAllEpisodes = useMemo( + () => expandSeriesMediaVersions(selectedSeriesEpisodes), [selectedSeriesEpisodes], ) + const selectedSeriesMediaIDs = useMemo( + () => selectedSeriesAllEpisodes.filter((ep) => !isTheatricalFeature(ep)).map((ep) => ep.id), + [selectedSeriesAllEpisodes], + ) + useEffect(() => { if (loading) return if (!isSeries) { @@ -131,6 +138,7 @@ export function useLibrarySeriesSelection({ selectedEpisodes, visibleEpisodes, selectedSeriesEpisodes, + selectedSeriesAllEpisodes, selectedSeriesMediaIDs, handleSeriesClick, clearSelectedSeries, diff --git a/web/src/utils/groupSeries.ts b/web/src/utils/groupSeries.ts index 19100eb..916f5fa 100644 --- a/web/src/utils/groupSeries.ts +++ b/web/src/utils/groupSeries.ts @@ -127,6 +127,31 @@ export function isTheatricalFeature(media: Media): boolean { return THEATRICAL_TITLE_RE.test(`${media.title || ''} ${path}`) || THEATRICAL_FOLDER_RE.test(path) } +/** + * 把剧集列表展开成"每一行 media"。 + * + * 剧集接口会把同一集的多版本(含被误判折叠的行)合成一个 item, 真实的行放在 + * `versions` 里, 顶层只保留代表行。批量操作(整剧匹配 / 整剧刮削 / 编辑整剧 / + * 删除整剧)必须作用到每一行, 否则只会更新代表行, 折叠掉的版本被漏掉。 + * + * 注意 `versions` 已包含代表行本身, 因此命中 versions 时不要再额外加回 item, + * 否则会重复。仅用于「操作」而非展示——展示仍按折叠后的剧集渲染版本切换器。 + */ +export function expandSeriesMediaVersions(episodes: Media[] = []): Media[] { + const out: Media[] = [] + const seen = new Set() + for (const ep of episodes) { + if (!ep) continue + const rows = ep.versions && ep.versions.length > 0 ? ep.versions : [ep] + for (const row of rows) { + if (!row || !row.id || seen.has(row.id)) continue + seen.add(row.id) + out.push(row) + } + } + return out +} + const SPECIAL_SECTION_PATTERNS: Array<[number, RegExp]> = [ [OVA_SEASON, /(?:^|[^a-z0-9])ovas?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i], [OAD_SEASON, /(?:^|[^a-z0-9])oads?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],