From b08fe23eabbab78672956bdce9e1c4c18f26df6b Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 18 Apr 2026 10:34:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=89=E9=9B=86=E5=B1=8F=E8=94=BD=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=9B=B8=E5=8F=8D=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/play/page.tsx | 31 ++------ src/components/EpisodeFilterSettings.tsx | 97 +++++++++++++++++++----- src/components/EpisodeSelector.tsx | 25 +----- src/lib/db.client.ts | 8 +- src/lib/episode-filter.ts | 47 ++++++++++++ src/lib/types.ts | 1 + src/types/sharp.d.ts | 43 +++++++++++ 7 files changed, 185 insertions(+), 67 deletions(-) create mode 100644 src/lib/episode-filter.ts create mode 100644 src/types/sharp.d.ts diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 9571692..9c4d655 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -46,13 +46,14 @@ import { saveSkipConfig, subscribeToDataUpdates, } from '@/lib/db.client'; +import { getDoubanDetail } from '@/lib/douban.client'; +import { isEpisodeHiddenByFilter, normalizeEpisodeFilterConfig } from '@/lib/episode-filter'; import { buildEpisodeProgressContentKey, loadLocalEpisodeProgress, pruneLocalEpisodeProgressStorage, saveLocalEpisodeProgress, } from '@/lib/episode-progress'; -import { getDoubanDetail } from '@/lib/douban.client'; import { getTMDBImageUrl } from '@/lib/tmdb.search'; import { getRecommendationCache, @@ -543,10 +544,11 @@ function PlayPageClient() { // 加载集数过滤配置 const episodeConfig = await getEpisodeFilterConfig(); if (episodeConfig) { - setEpisodeFilterConfig(episodeConfig); - episodeFilterConfigRef.current = episodeConfig; + const normalizedEpisodeConfig = normalizeEpisodeFilterConfig(episodeConfig); + setEpisodeFilterConfig(normalizedEpisodeConfig); + episodeFilterConfigRef.current = normalizedEpisodeConfig; } else { - const defaultEpisodeConfig: EpisodeFilterConfig = { rules: [] }; + const defaultEpisodeConfig: EpisodeFilterConfig = normalizeEpisodeFilterConfig(); setEpisodeFilterConfig(defaultEpisodeConfig); episodeFilterConfigRef.current = defaultEpisodeConfig; } @@ -4592,26 +4594,7 @@ function PlayPageClient() { // 检查集数是否被过滤 const isEpisodeFilteredByTitle = (title: string): boolean => { - const filterConfig = episodeFilterConfigRef.current; - if (!filterConfig || filterConfig.rules.length === 0) { - return false; - } - - for (const rule of filterConfig.rules) { - if (!rule.enabled) continue; - - try { - if (rule.type === 'normal' && title.includes(rule.keyword)) { - return true; - } - if (rule.type === 'regex' && new RegExp(rule.keyword).test(title)) { - return true; - } - } catch (e) { - console.error('集数过滤规则错误:', e); - } - } - return false; + return isEpisodeHiddenByFilter(title, episodeFilterConfigRef.current); }; const handleNextEpisode = async () => { diff --git a/src/components/EpisodeFilterSettings.tsx b/src/components/EpisodeFilterSettings.tsx index 99564e9..501041f 100644 --- a/src/components/EpisodeFilterSettings.tsx +++ b/src/components/EpisodeFilterSettings.tsx @@ -6,6 +6,7 @@ import { useEffect, useRef,useState } from 'react'; import { createPortal } from 'react-dom'; import { getEpisodeFilterConfig, saveEpisodeFilterConfig } from '@/lib/db.client'; +import { normalizeEpisodeFilterConfig } from '@/lib/episode-filter'; import { EpisodeFilterConfig, EpisodeFilterRule } from '@/lib/types'; interface EpisodeFilterSettingsProps { @@ -21,7 +22,7 @@ export default function EpisodeFilterSettings({ onConfigUpdate, onShowToast, }: EpisodeFilterSettingsProps) { - const [config, setConfig] = useState({ rules: [] }); + const [config, setConfig] = useState(normalizeEpisodeFilterConfig()); const [newKeyword, setNewKeyword] = useState(''); const [newType, setNewType] = useState<'normal' | 'regex'>('normal'); const [loading, setLoading] = useState(false); @@ -130,9 +131,9 @@ export default function EpisodeFilterSettings({ try { const loadedConfig = await getEpisodeFilterConfig(); if (loadedConfig) { - setConfig(loadedConfig); + setConfig(normalizeEpisodeFilterConfig(loadedConfig)); } else { - setConfig({ rules: [] }); + setConfig(normalizeEpisodeFilterConfig()); } } catch (error) { console.error('加载集数过滤配置失败:', error); @@ -141,13 +142,31 @@ export default function EpisodeFilterSettings({ } }; + const handleToggleReverseMode = () => { + setConfig((prev) => { + const normalizedConfig = normalizeEpisodeFilterConfig(prev); + return { + ...normalizedConfig, + reverseMode: !normalizedConfig.reverseMode, + }; + }); + }; + // 保存配置 const handleSave = async () => { + const normalizedConfig = normalizeEpisodeFilterConfig(config); + if (normalizedConfig.reverseMode && normalizedConfig.rules.length === 0) { + if (onShowToast) { + onShowToast('启用相反模式时,至少需要添加一条规则', 'info'); + } + return; + } + setSaving(true); try { - await saveEpisodeFilterConfig(config); + await saveEpisodeFilterConfig(normalizedConfig); if (onConfigUpdate) { - onConfigUpdate(config); + onConfigUpdate(normalizedConfig); } if (onShowToast) { onShowToast('保存成功!', 'success'); @@ -182,9 +201,13 @@ export default function EpisodeFilterSettings({ id: Date.now().toString(), }; - setConfig((prev) => ({ - rules: [...prev.rules, newRule], - })); + setConfig((prev) => { + const normalizedConfig = normalizeEpisodeFilterConfig(prev); + return { + ...normalizedConfig, + rules: [...normalizedConfig.rules, newRule], + }; + }); // 清空输入框并强制重新渲染 setNewKeyword(''); @@ -202,19 +225,27 @@ export default function EpisodeFilterSettings({ // 删除规则 const handleDeleteRule = (id: string | undefined) => { if (!id) return; - setConfig((prev) => ({ - rules: prev.rules.filter((rule) => rule.id !== id), - })); + setConfig((prev) => { + const normalizedConfig = normalizeEpisodeFilterConfig(prev); + return { + ...normalizedConfig, + rules: normalizedConfig.rules.filter((rule) => rule.id !== id), + }; + }); }; // 切换规则启用状态 const handleToggleRule = (id: string | undefined) => { if (!id) return; - setConfig((prev) => ({ - rules: prev.rules.map((rule) => - rule.id === id ? { ...rule, enabled: !rule.enabled } : rule - ), - })); + setConfig((prev) => { + const normalizedConfig = normalizeEpisodeFilterConfig(prev); + return { + ...normalizedConfig, + rules: normalizedConfig.rules.map((rule) => + rule.id === id ? { ...rule, enabled: !rule.enabled } : rule + ), + }; + }); }; if (!isVisible || !mounted) return null; @@ -294,6 +325,37 @@ export default function EpisodeFilterSettings({
{/* 添加规则 */}
+
+
+

+ 相反模式 +

+

+ 开启后,将屏蔽改为仅显示符合规则的集数。 +

+

+ 启用时必须至少保留一条规则才能保存。 +

+
+ +
+

添加屏蔽规则

@@ -333,7 +395,8 @@ export default function EpisodeFilterSettings({

- 💡 普通模式:集数标题包含关键字即屏蔽
+ 💡 普通模式:集数标题包含关键字即命中规则
+ 🔄 相反模式:仅显示命中规则的集数
🔧 正则模式:支持正则表达式匹配(如:^预告.*匹配以"预告"开头的集数)

diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx index 4f5c746..bf2f100 100644 --- a/src/components/EpisodeSelector.tsx +++ b/src/components/EpisodeSelector.tsx @@ -12,6 +12,7 @@ import React, { import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types'; import { generateStorageKey, getCachedPlayRecordsSnapshot } from '@/lib/db.client'; +import { isEpisodeHiddenByFilter } from '@/lib/episode-filter'; import { loadAllLocalEpisodeProgressRecords } from '@/lib/episode-progress'; import { EpisodeFilterConfig,SearchResult } from '@/lib/types'; import { getVideoResolutionFromM3u8 } from '@/lib/utils'; @@ -249,29 +250,7 @@ const EpisodeSelector: React.FC = ({ // 获取集数标题 const title = episodes_titles?.[episodeNumber - 1]; if (!title) return false; - - // 检查每个启用的规则 - for (const rule of episodeFilterConfig.rules) { - if (!rule.enabled) continue; - - try { - if (rule.type === 'normal') { - // 普通模式:字符串包含匹配 - if (title.includes(rule.keyword)) { - return true; - } - } else if (rule.type === 'regex') { - // 正则模式:正则表达式匹配 - if (new RegExp(rule.keyword).test(title)) { - return true; - } - } - } catch (e) { - console.error('集数过滤规则错误:', e); - } - } - - return false; + return isEpisodeHiddenByFilter(title, episodeFilterConfig); }, [episodeFilterConfig, episodes_titles] ); diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index 7913bd7..27828ff 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -15,6 +15,7 @@ */ import { getAuthInfoFromBrowserCookie, clearAuthCookie } from './auth'; +import { normalizeEpisodeFilterConfig } from './episode-filter'; import { MangaReadRecord, MangaShelfItem } from './manga.types'; import { DanmakuFilterConfig, EpisodeFilterConfig,SkipConfig } from './types'; @@ -2643,7 +2644,7 @@ export async function getEpisodeFilterConfig(): Promise; + toBuffer(): Promise; + } + + interface SharpOptions { + failOn?: string; + } + + interface SharpConstructor { + (input?: Buffer, options?: SharpOptions): Sharp; + } + + const sharp: SharpConstructor; + export default sharp; +}