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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();