选集屏蔽增加相反模式

This commit is contained in:
mtvpls
2026-04-18 10:34:34 +08:00
parent 3520fd1ed3
commit b08fe23eab
7 changed files with 185 additions and 67 deletions
+7 -24
View File
@@ -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 () => {
+80 -17
View File
@@ -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<EpisodeFilterConfig>({ rules: [] });
const [config, setConfig] = useState<EpisodeFilterConfig>(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({
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4 min-h-0">
{/* 添加规则 */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 space-y-3">
<div className="flex items-start justify-between gap-3 rounded-xl bg-white dark:bg-gray-700/60 border border-gray-200 dark:border-gray-600 px-4 py-3">
<div className="min-w-0">
<h3 className="text-sm font-medium text-gray-800 dark:text-gray-200">
</h3>
<p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
</p>
<p className="mt-1 text-xs leading-relaxed text-amber-600 dark:text-amber-400">
</p>
</div>
<button
onClick={handleToggleReverseMode}
className="flex-shrink-0 active:scale-95 transition-transform duration-150"
title={config.reverseMode ? '关闭相反模式' : '开启相反模式'}
>
{config.reverseMode ? (
<ToggleRight
size={28}
className="text-green-500 hover:text-green-400 transition-colors duration-150"
/>
) : (
<ToggleLeft
size={28}
className="text-gray-400 hover:text-gray-300 transition-colors duration-150"
/>
)}
</button>
</div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
</h3>
@@ -333,7 +395,8 @@ export default function EpisodeFilterSettings({
</div>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed">
💡 <br/>
💡 <br/>
🔄 <br/>
🔧 ^.*"预告"
</p>
</div>
+2 -23
View File
@@ -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<EpisodeSelectorProps> = ({
// 获取集数标题
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]
);
+5 -3
View File
@@ -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<EpisodeFilterConfig | nu
try {
const raw = localStorage.getItem('moontv_episode_filter_config');
if (!raw) return null;
return JSON.parse(raw) as EpisodeFilterConfig;
return normalizeEpisodeFilterConfig(JSON.parse(raw) as EpisodeFilterConfig);
} catch (err) {
console.error('读取集数过滤配置失败:', err);
return null;
@@ -2662,10 +2663,11 @@ export async function saveEpisodeFilterConfig(
}
try {
localStorage.setItem('moontv_episode_filter_config', JSON.stringify(config));
const normalizedConfig = normalizeEpisodeFilterConfig(config);
localStorage.setItem('moontv_episode_filter_config', JSON.stringify(normalizedConfig));
window.dispatchEvent(
new CustomEvent('episodeFilterConfigUpdated', {
detail: config,
detail: normalizedConfig,
})
);
} catch (err) {
+47
View File
@@ -0,0 +1,47 @@
import { EpisodeFilterConfig } from './types';
export function normalizeEpisodeFilterConfig(
config?: EpisodeFilterConfig | null
): EpisodeFilterConfig {
return {
rules: config?.rules ?? [],
reverseMode: config?.reverseMode ?? false,
};
}
export function doesEpisodeTitleMatchFilterRules(
title: string,
config?: EpisodeFilterConfig | null
): boolean {
const normalizedConfig = normalizeEpisodeFilterConfig(config);
for (const rule of normalizedConfig.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;
}
export function isEpisodeHiddenByFilter(
title: string,
config?: EpisodeFilterConfig | null
): boolean {
const normalizedConfig = normalizeEpisodeFilterConfig(config);
if (normalizedConfig.rules.length === 0) {
return false;
}
const isMatched = doesEpisodeTitleMatchFilterRules(title, normalizedConfig);
return normalizedConfig.reverseMode ? !isMatched : isMatched;
}
+1
View File
@@ -253,6 +253,7 @@ export interface EpisodeFilterRule {
// 集数过滤配置数据结构
export interface EpisodeFilterConfig {
rules: EpisodeFilterRule[]; // 过滤规则列表
reverseMode?: boolean; // 反向模式:开启后仅显示符合规则的集数
}
// 通知类型枚举
+43
View File
@@ -0,0 +1,43 @@
declare module 'sharp' {
interface ResizeOptions {
width?: number;
height?: number;
withoutEnlargement?: boolean;
}
interface PngOptions {
compressionLevel?: number;
palette?: boolean;
quality?: number;
effort?: number;
}
interface JpegOptions {
quality?: number;
mozjpeg?: boolean;
}
interface Metadata {
hasAlpha?: boolean;
}
interface Sharp {
rotate(): Sharp;
resize(options?: ResizeOptions): Sharp;
png(options?: PngOptions): Sharp;
jpeg(options?: JpegOptions): Sharp;
metadata(): Promise<Metadata>;
toBuffer(): Promise<Buffer>;
}
interface SharpOptions {
failOn?: string;
}
interface SharpConstructor {
(input?: Buffer, options?: SharpOptions): Sharp;
}
const sharp: SharpConstructor;
export default sharp;
}