DMM Top Filter Buttons (Stable + Cached)

Adds filter buttons to DMM with robust InstantRD and Cached filtering. Persistent active background for all buttons. Buttons remain first children in correct order.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         DMM Top Filter Buttons (Stable + Cached)
// @namespace    http://tampermonkey.net/
// @version      1.61
// @description  Adds filter buttons to DMM with robust InstantRD and Cached filtering. Persistent active background for all buttons. Buttons remain first children in correct order.
// @author       Waseem
// @match        https://debridmediamanager.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    function setReactValue(element, value) {
        const nativeSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype, 'value'
        ).set;
        nativeSetter.call(element, value);
        element.dispatchEvent(new Event('input', { bubbles: true }));
    }

    function normalizeText(txt) {
        return (txt || '').toLowerCase().replace(/\s+/g, ' ').trim();
    }

    function escapeRegex(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    let instantRDActive = false;
    let cachedActive = false;

    const buttons = [
        { text: '⚡instantRD', value: '__bgonly__', class: 'dmm-bgonly', instantRD: true, color: '#166534' },
        { text: 'Cached', class: 'dmm-cached', cached: true, color: '#f59e0b' }, // amber
        { text: '4K', value: '2160p|4k', class: 'dmm-4k', color: '#1e40af' },
        { text: '1080p', value: '1080p', class: 'dmm-1080p', color: '#1e40af' },
        { text: '720p', value: '720p', class: 'dmm-720p', color: '#1e40af' },
        { text: 'Dolby Vision', value: 'dovi|dv|dolby|vision', class: 'dmm-dolbyvision', color: '#1e40af' },
        { text: 'HDR', value: 'hdr', class: 'dmm-hdr', color: '#1e40af' },
        { text: 'Remux', value: 'remux', class: 'dmm-remux', color: '#1e40af' }
    ];
    const qualityValues = ['2160p|4k', '1080p', '720p'];

    function getGrid() {
        return document.querySelector(
            '#__next > div > div.mx-1.my-1.grid.grid-cols-1.gap-2.overflow-x-auto.sm\\:grid-cols-2.md\\:grid-cols-3.lg\\:grid-cols-4.xl\\:grid-cols-6'
        );
    }

    function cardHasInstantRD(card) {
        const btns = card.querySelectorAll('button');
        for (const b of btns) {
            const t = normalizeText(b.textContent);
            if (t.includes('instant rd')) return true;
            if (t.includes('rd (100%)')) return true;
        }
        return false;
    }

    function cardHasCached(card) {
        const btns = card.querySelectorAll('button');
        for (const b of btns) {
            const t = normalizeText(b.textContent);
            if (t.includes('rd (100%)')) return true;
        }
        return false;
    }

    function applyFilters() {
        const grid = getGrid();
        if (!grid) return;

        const cards = grid.querySelectorAll(':scope > div');
        cards.forEach(card => {
            const instantRDKeep = !instantRDActive || cardHasInstantRD(card);
            const cachedKeep = !cachedActive || cardHasCached(card);
            card.style.display = (instantRDKeep && cachedKeep) ? '' : 'none';
        });

        updateButtonActiveStates();
    }

    function showAllCards() {
        const grid = getGrid();
        if (!grid) return;
        const cards = grid.querySelectorAll(':scope > div');
        cards.forEach(card => (card.style.display = ''));
    }

    function updateButtonActiveStates() {
        const input = document.querySelector('#query');
        if (!input) return;
        const val = input.value.trim();

        buttons.forEach(btn => {
            const span = document.querySelector(`span.${btn.class}`);
            if (!span) return;
            let active = false;
            if (btn.instantRD) active = instantRDActive;
            else if (btn.cached) active = cachedActive;
            else if (val.includes(btn.value)) active = true;

            if (active) {
                span.classList.add('active');
                span.style.setProperty('background-color', btn.color, 'important');
            } else {
                span.classList.remove('active');
                span.style.removeProperty('background-color');
            }
        });
    }

    function addFilterButtons() {
        const container = document.querySelector(
            '#__next > div > div.mb-2.flex.items-center.gap-2.overflow-x-auto.p-2 > div'
        );
        const input = document.querySelector('#query');
        if (!container || !input) return;

        const reference = container.firstChild;

        buttons.forEach(btn => {
            if (container.querySelector(`span.${btn.class}`)) return;

            const span = document.createElement('span');
            span.textContent = btn.text;
            span.className =
                `${btn.class} cursor-pointer whitespace-nowrap rounded border border-blue-500 bg-blue-900/30 px-2 py-0.5 text-xs text-blue-100 transition-colors hover:bg-blue-800/50`;
            span.dataset.color = btn.color;

            if (reference) container.insertBefore(span, reference);
            else container.appendChild(span);

            span.addEventListener('click', () => {
                if (btn.instantRD) {
                    instantRDActive = !instantRDActive;
                    if (instantRDActive) applyFilters();
                    else {
                        showAllCards();
                        applyFilters();
                    }
                    updateButtonActiveStates();
                    return;
                }

                if (btn.cached) {
                    cachedActive = !cachedActive;
                    applyFilters();
                    return;
                }

                let current = input.value.trim();
                if (qualityValues.includes(btn.value)) {
                    const clickedQuality = btn.value;
                    const escaped = escapeRegex(clickedQuality);
                    const regex = new RegExp(`(^|\\||\\s)${escaped}($|\\||\\s)`);
                    if (current.match(regex)) {
                        const parts = current.split(' ');
                        const filteredParts = parts.map(part => {
                            const qualityRegex = new RegExp(`(^|\\|)${escaped}($|\\|)`);
                            let cleaned = part.replace(qualityRegex, (match, before, after) => {
                                if (before === '|' && after === '|') return '|';
                                return '';
                            });
                            cleaned = cleaned.replace(/\|{2,}/g, '|').replace(/^\|+|\|+$/g, '');
                            return cleaned;
                        }).filter(Boolean);
                        current = filteredParts.join(' ').trim();
                        setReactValue(input, current);
                        applyFilters();
                        updateButtonActiveStates();
                        return;
                    }

                    let lastFound = null;
                    qualityValues.forEach(q => {
                        const idx = current.lastIndexOf(q);
                        if (idx !== -1) lastFound = { value: q, index: idx };
                    });

                    if (lastFound) {
                        const before = current.slice(0, lastFound.index + lastFound.value.length);
                        const after = current.slice(lastFound.index + lastFound.value.length);
                        current = before + '|' + clickedQuality + after;
                    } else current = current ? current + ' ' + clickedQuality : clickedQuality;

                    current = current.replace(/\|{2,}/g, '|').replace(/^\|+|\|+$/g, '');
                    current = current.replace(/\s+/g, ' ').trim();
                    setReactValue(input, current);
                    applyFilters();
                    updateButtonActiveStates();
                    return;
                }

                if (current.includes(btn.value)) {
                    const regex = new RegExp(`\\s*\\b${escapeRegex(btn.value)}\\b\\s*`, 'g');
                    current = current.replace(regex, ' ').trim();
                    current = current.replace(/\s+/g, ' ');
                    setReactValue(input, current);
                } else {
                    current = current ? current + ' ' + btn.value : btn.value;
                    setReactValue(input, current);
                }
                applyFilters();
                updateButtonActiveStates();
            });
        });

        updateButtonActiveStates();
        applyFilters();
    }

    function setupGlobalListeners() {
        const input = document.querySelector('#query');
        if (!input) return;

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') setReactValue(input, '');
        });

        input.addEventListener('input', () => {
            applyFilters();
        });
    }

    const observer = new MutationObserver(() => {
        addFilterButtons();
        setupGlobalListeners();
        applyFilters();
    });
    observer.observe(document.body, { childList: true, subtree: true });

    addFilterButtons();
    setupGlobalListeners();
    applyFilters();
})();