Greasy Fork

动漫花园标题过滤器

为动漫花园增加标题过滤功能,支持正则表达式和复杂过滤规则

// ==UserScript==
// @name         动漫花园标题过滤器
// @name:zh-CN   动漫花园标题过滤器
// @name:zh-TW   動漫花園標題過濾器
// @name:en      DMHY Title Filter
// @namespace    https://github.com/xkbkx5904
// @version      0.1
// @description  为动漫花园增加标题过滤功能,支持正则表达式和复杂过滤规则
// @description:zh-CN  为动漫花园增加标题过滤功能,支持正则表达式、简繁体匹配和复杂过滤规则
// @description:zh-TW  為動漫花園增加標題過濾功能,支持正則表達式、簡繁體匹配和複雜過濾規則
// @description:en  Add title filtering functionality to DMHY, supporting regex, Chinese variants matching and complex filtering rules
// @author       xkbkx5904
// @match        https://share.dmhy.org/*
// @icon         https://share.dmhy.org/favicon.ico
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/full.js
// @license      MIT
// @run-at       document-start
// @supportURL   https://github.com/xkbkx5904/dmhy-filter/issues
// @homepageURL  https://github.com/xkbkx5904/dmhy-filter
// ==/UserScript==

(function() {
    'use strict';

    // 等待页面加载完成
    function waitForElements() {
        return new Promise((resolve) => {
            const observer = new MutationObserver((mutations, obs) => {
                const quickSearch = document.querySelector('.quick_search');
                const table = document.querySelector('#topic_list');
                
                if (quickSearch && table) {
                    obs.disconnect();
                    resolve({ quickSearch, table });
                }
            });

            observer.observe(document, {
                childList: true,
                subtree: true
            });

            // 立即检查元素是否已存在
            const quickSearch = document.querySelector('.quick_search');
            const table = document.querySelector('#topic_list');
            if (quickSearch && table) {
                observer.disconnect();
                resolve({ quickSearch, table });
            }
        });
    }

    // 缓存 OpenCC 实例
    const converters = {
        toTraditional: null,
        toSimplified: null
    };

    // 初始化转换器
    function initConverters() {
        if (!converters.toTraditional || !converters.toSimplified) {
            converters.toTraditional = OpenCC.Converter({ from: 'cn', to: 'twp' });
            converters.toSimplified = OpenCC.Converter({ from: 'tw', to: 'cn' });
        }
    }

    // 过滤标题
    function filterTitles(keywords, table) {
        if (!keywords || !keywords.trim()) {
            // 如果关键词为空,显示所有行
            Array.from(table.querySelectorAll('tbody tr')).forEach(row => {
                row.style.display = '';
            });
            return;
        }

        // 确保转换器已初始化
        initConverters();

        // 分割多个过滤条件(支持中英文分号)
        const filterGroups = keywords
            .split(/[;;]/)
            .map(group => group.trim())
            .filter(Boolean);

        const tbody = table.querySelector('tbody');
        const fragment = document.createDocumentFragment();
        const rows = Array.from(tbody.children);

        // 从 DOM 中移除所有行
        rows.forEach(row => tbody.removeChild(row));

        // 在 fragment 中处理行
        rows.forEach(row => {
            const title = row.querySelector('.title').textContent;
            
            // 生成标题的简繁体变体
            const titleVariants = [
                title.toLowerCase(),
                converters.toTraditional(title).toLowerCase(),
                converters.toSimplified(title).toLowerCase()
            ];

            // 检查是否满足所有过滤组中的至少一个条件
            const matchesAllGroups = filterGroups.every(group => {
                // 检查是否是正则表达式
                if (group.startsWith('/') && group.endsWith('/') && group.length > 2) {
                    try {
                        const regex = new RegExp(group.slice(1, -1), 'i');
                        return titleVariants.some(variant => regex.test(variant));
                    } catch (e) {
                        console.warn('Invalid regex:', group);
                        // 如果正则表达式无效,将其作为普通文本搜索
                        return titleVariants.some(variant => 
                            variant.includes(group.toLowerCase())
                        );
                    }
                }
                
                // 处理普通关键词(支持 OR 操作符 |)
                const terms = group.split('|').map(term => term.trim()).filter(Boolean);
                
                // 生成每个关键词的简繁体变体
                const termVariants = terms.flatMap(term => [
                    term.toLowerCase(),
                    converters.toTraditional(term).toLowerCase(),
                    converters.toSimplified(term).toLowerCase()
                ]);

                // 检查标题变体是否包含任何关键词变体
                return titleVariants.some(titleVariant =>
                    termVariants.some(termVariant =>
                        titleVariant.includes(termVariant)
                    )
                );
            });

            row.style.display = matchesAllGroups ? '' : 'none';
            fragment.appendChild(row);
        });

        // 一次性将所有行添加回 DOM
        tbody.appendChild(fragment);
    }

    // 设置 UI
    function setupUI({ quickSearch, table }) {
        // 创建过滤器输入框
        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = '输入过滤条件(不区分简繁体字、支持正则)';
        input.title = `过滤语法说明:
1. 分号(;或;)表示"与",优先级高于"或",例如:
   1080p|2160p;简体|繁体
   表示:(1080p或2160p) 且 (简体或繁体)

2. 竖线(|)表示"或",在分号分隔的每组条件内生效,例如:
   简体|繁体;1080p|2160p;HEVC|x265
   表示:(简体或繁体) 且 (1080p或2160p) 且 (HEVC或x265)

3. 支持正则表达式,使用 /pattern/ 格式:
   /\\[第\\d+集\\]/;1080p
   表示:(标题含[第n集]) 且 (含1080p)

示例组合:HEVC|x265;简体;/\\[01-24集\\]/
匹配说明:必须同时满足以下条件:
1. 包含HEVC或x265
2. 包含简体(或簡體)
3. 包含[01-24集]格式的集数`;
        
        // 复制原始搜索框的属性
        input.className = 'quick_input ac_input';
        input.setAttribute('maxlength', '50');
        input.setAttribute('x-webkit-speech', '');
        input.setAttribute('lang', 'zh-tw');
        input.setAttribute('x-webkit-grammar', 'builtin:translate');
        input.setAttribute('autocomplete', 'off');

        // 创建包装容器以对齐
        const wrapper = document.createElement('span');
        wrapper.style.cssText = `
            display: inline-flex;
            align-items: center;
            margin-left: 10px;
            white-space: nowrap;
        `;

        // 组装并添加到页面
        wrapper.appendChild(input);
        quickSearch.appendChild(wrapper);

        // 调整输入框宽度
        function adjustInputWidth() {
            const originalInput = quickSearch.querySelector('#keyword');
            if (originalInput) {
                // 复制原始搜索框的所有计算样式
                const computedStyle = window.getComputedStyle(originalInput);
                input.style.cssText = Array.from(computedStyle)
                    .filter(prop => !prop.startsWith('margin'))
                    .map(prop => `${prop}: ${computedStyle.getPropertyValue(prop)}`)
                    .join(';');
                
                // 设置宽度
                input.style.width = computedStyle.width;
                
                // 保持自定义的 margin
                input.style.marginLeft = '10px';
            }
        }

        // 创建 ResizeObserver 来监听原始搜索框的大小变化
        const resizeObserver = new ResizeObserver(() => {
            adjustInputWidth();
        });

        // 监听原始搜索框的大小变化
        const originalInput = quickSearch.querySelector('#keyword');
        if (originalInput) {
            resizeObserver.observe(originalInput);
        }

        // 初始调整
        adjustInputWidth();

        // 监听窗口大小变化
        window.addEventListener('resize', adjustInputWidth);

        // 添加防抖处理
        let debounceTimer;
        input.addEventListener('input', (e) => {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                // 规范化分号显示
                const value = e.target.value;
                const normalizedValue = value.replace(/;/g, ';');
                if (value !== normalizedValue) {
                    input.value = normalizedValue;
                }
                filterTitles(value, table);
            }, 300);
        });
    }

    // 初始化
    async function initialize() {
        try {
            const elements = await waitForElements();
            if (!elements.quickSearch || !elements.table) {
                throw new Error('Required elements not found');
            }
            setupUI(elements);
        } catch (error) {
            console.error('初始化失败:', error);
        }
    }

    // 启动初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();