Nyaa - Highlight & Block

Highlight and block releases on nyaa.si

// ==UserScript==
// @name         Nyaa - Highlight & Block
// @version      1.03
// @description  Highlight and block releases on nyaa.si
// @author       Animorphs
// @namespace    https://github.com/Animorphs/Nyaa-Highlight-and-Block
// @match        https://nyaa.si/*
// @match        https://sukebei.nyaa.si/*
// @exclude      https://nyaa.si/view/*
// @exclude      https://nyaa.si/profile
// @exclude      https://nyaa.si/upload
// @exclude      https://nyaa.si/rules
// @exclude      https://nyaa.si/help
// @exclude      https://nyaa.si/?page=rss*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================================
    // CONSTANTS & CONFIGURATION
    // ============================================================================

    const isSukebei = window.location.hostname === 'sukebei.nyaa.si';
    const CONFIG = {
        STORAGE_KEYS: {
            HIGHLIGHT: isSukebei ? 'keywordsHighlight_sukebei' : 'keywordsHighlight',
            BLOCK: isSukebei ? 'keywordsBlock_sukebei' : 'keywordsBlock',
            FANSUBBERS_TOGGLE: 'fansubbersToggle',
            MINIS_TOGGLE: 'minisToggle',
            FANSUBBERS_BLACKLIST: 'fansubbersBlacklist',
            MINIS_BLACKLIST: 'minisBlacklist',
            BLOCKED_CATEGORIES: isSukebei ? 'blockedCategories_sukebei' : 'blockedCategories'
        },
        DEFAULTS: {
            HIGHLIGHT: isSukebei ? [
                [["[SakuraCircle]"], []]
            ] : [
                [["[Animorphs]"], []],
                [["[whomst]"], []]
            ],
            BLOCK: isSukebei ? [] : [
                [["[Raze]"], []],
                [["[SubsPlease]"], ["1080p"]],
                [["[Erai-raws]"], ["1080p"]]
            ]
        },
        DEBOUNCE_DELAY: 100,
        MAX_HEIGHT_VH: 90
    };

    const PRESETS = {
        FANSUBBERS: [
            [["[9volt]"], []],
            [["[Animorphs]"], []],
            [["[Arid]"], []],
            [["[Asakura]"], []],
            [["[Baws]"], []],
            [["[Blasphemboys]"], []],
            [["[BlurayDesuYo]"], []],
            [["[cappybara]"], []],
            [["[Chihiro]"], []],
            [["[Commie]"], []],
            [["[Cyan]"], []],
            [["[DameDesuYo]"], []],
            [["[DarkWispers]"], []],
            [["[derpie]"], []],
            [["[FLE]"], []],
            [["[Freehold]"], []],
            [["[GHS]"], []],
            [["[Glue]"], []],
            [["[GJM]"], []],
            [["[h-b]"], []],
            [["[Half-Baked]"], []],
            [["[Inka-Subs]"], []],
            [["[Kaleido-subs]"], []],
            [["[LonelyChaser]"], []],
            [["[MaruChanSubs]"], []],
            [["[McBalls]"], []],
            [["[Mocha]"], []],
            [["[MTBB]"], []],
            [["[Noiy]"], []],
            [["[Ny]"], []],
            [["[Okay-Subs]"], []],
            [["[Orphan]"], []],
            [["[P9]"], []],
            [["[Paradise]"], []],
            [["[Perevodildo]"], []],
            [["[Piyoko]"], []],
            [["[Pizza]"], []],
            [["[poop]"], []],
            [["[Reza]"], []],
            [["[Saizen]"], []],
            [["[sam]"], []],
            [["[Seigyoku]"], []],
            [["(shiteater)"], []],
            [["[Some-Stuffs]"], ["Pocket Monsters","Poké"]], // Exclude pokemon due to volume that probably isn't relevant to most
            [["[sgt]"], []],
            [["[Starbez]"], []],
            [["[Stardust]"], []],
            [["[Vodes]"], []],
            [["[WakuTomete]"], []],
            [["[WastedChaser]"], []],
            [["[WasteOfBlindness]"], []],
            [["[washed]"], []],
            [["[whomst]"], []]
        ],
        MINIS: [
            [["[Anime Time]"], []],
            [["[ARR]"], []],
            [["[ASW]"], []],
            [["AV1"], []],
            [["[Judas]"], []],
            [["[DB]"], []],
            [["[DKB]"], []],
            [["[EMBER]"], []],
            [["[Erai-raws]","WEBRip"], []],
            [["[JacobSwaggedUp]"], []],
            [["[MiniMTBB]"], []],
            [["[neoDESU]"], []],
            [["[neoHEVC]"], []],
            [["[Tenrai-Sensei]"], []],
            [["[TRC]"], []]
        ]
    };

    const CATEGORIES = isSukebei ? {
        'art-anime': 'Art - Anime',
        'art-doujinshi': 'Art - Doujinshi',
        'art-games': 'Art - Games',
        'art-manga': 'Art - Manga',
        'art-pictures': 'Art - Pictures',
        'real-life-photobooks': 'Real Life - Photobooks and Pictures',
        'real-life-videos': 'Real Life - Videos'
    } : {
        'anime-amv': 'Anime - AMV',
        'anime-english': 'Anime - English-translated',
        'anime-non-english': 'Anime - Non-English-translated',
        'anime-raw': 'Anime - Raw',
        'audio-lossless': 'Audio - Lossless',
        'audio-lossy': 'Audio - Lossy',
        'literature-english': 'Literature - English-translated',
        'literature-non-english': 'Literature - Non-English-translated',
        'literature-raw': 'Literature - Raw',
        'live-action-english': 'Live Action - English-translated',
        'live-action-idol': 'Live Action - Idol/Promotional Video',
        'live-action-non-english': 'Live Action - Non-English-translated',
        'live-action-raw': 'Live Action - Raw',
        'pictures-graphics': 'Pictures - Graphics',
        'pictures-photos': 'Pictures - Photos',
        'software-applications': 'Software - Applications',
        'software-games': 'Software - Games'
    };

    const SELECTORS = {
        TORRENT_ROWS: 'tbody tr',
        TITLE_ELEMENT: 'td[colspan="2"] a',
        GUI_CONTAINER: 'guiContainer'
    };

    // ============================================================================
    // FUNCTIONAL UTILITIES
    // ============================================================================

    /**
     * Functional utility for chaining operations like "chips"
     * Provides a fluent interface for combining predicates
     */
    const Chips = {
        from: (value) => new ChipChain(value),
        all: (predicates) => (value) => predicates.every(pred => pred(value)),
        any: (predicates) => (value) => predicates.some(pred => pred(value)),
        contains: (keyword) => (text) => text.toLowerCase().includes(keyword.toLowerCase()),
        not: (predicate) => (value) => !predicate(value)
    };

    /**
     * Chain class for fluent predicate composition
     */
    class ChipChain {
        constructor(value) {
            this.value = value;
            this.operations = [];
        }

        and(operation) {
            this.operations.push({ type: 'and', op: operation });
            return this;
        }

        or(operation) {
            this.operations.push({ type: 'or', op: operation });
            return this;
        }

        not(operation) {
            this.operations.push({ type: 'not', op: operation });
            return this;
        }

        execute() {
            if (this.operations.length === 0) return this.value;

            return this.operations.reduce((result, { type, op }) => {
                switch (type) {
                    case 'and':
                        return result && op(this.value);
                    case 'or':
                        return result || op(this.value);
                    case 'not':
                        return result && !op(this.value);
                    default:
                        return result;
                }
            }, true);
        }

        result() {
            return this.execute();
        }
    }

    // ============================================================================
    // UTILITY FUNCTIONS
    // ============================================================================

    /**
     * Debounce function to limit rapid successive calls
     * @param {Function} func - Function to debounce
     * @param {number} wait - Wait time in milliseconds
     * @returns {Function} Debounced function
     */
    const debounce = (func, wait) => {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    };

    /**
     * Sanitize input text by trimming and removing dangerous characters
     * @param {string} text - Text to sanitize
     * @returns {string} Sanitized text
     */
    const sanitizeInput = (text) => {
        if (typeof text !== 'string') return '';
        return text.trim().replace(/[<>]/g, '');
    };

    /**
     * Check if two arrays are deeply equal
     * @param {Array} arr1 - First array
     * @param {Array} arr2 - Second array
     * @returns {boolean} True if arrays are equal
     */
    const arraysEqual = (arr1, arr2) => {
        return JSON.stringify(arr1) === JSON.stringify(arr2);
    };

    // ============================================================================
    // AUTO-IMPORT FUNCTIONALITY
    // ============================================================================

    /**
     * Auto-import fansubbers if toggle is enabled
     */
    const autoImportFansubbers = () => {
        if (!AppState.fansubbersToggle) return;

        let addedCount = 0;
        PRESETS.FANSUBBERS.forEach(newRule => {
            // Check if rule already exists
            const isDuplicate = AppState.keywordsHighlight.some(existingRule =>
                arraysEqual(existingRule[0], newRule[0]) &&
                arraysEqual(existingRule[1], newRule[1])
            );

            // Check if rule is blacklisted
            const isBlacklisted = AppState.fansubbersBlacklist.some(blacklistedRule =>
                arraysEqual(blacklistedRule[0], newRule[0]) &&
                arraysEqual(blacklistedRule[1], newRule[1])
            );

            if (!isDuplicate && !isBlacklisted) {
                AppState.keywordsHighlight.push([...newRule]);
                addedCount++;
            }
        });

        if (addedCount > 0) {
            sortRules(AppState.keywordsHighlight);
            AppState.saveHighlightKeywords();
        }

        return addedCount;
    };

    /**
     * Auto-import minis if toggle is enabled
     */
    const autoImportMinis = () => {
        if (!AppState.minisToggle) return;

        let addedCount = 0;
        PRESETS.MINIS.forEach(newRule => {
            // Check if rule already exists
            const isDuplicate = AppState.keywordsBlock.some(existingRule =>
                arraysEqual(existingRule[0], newRule[0]) &&
                arraysEqual(existingRule[1], newRule[1])
            );

            // Check if rule is blacklisted
            const isBlacklisted = AppState.minisBlacklist.some(blacklistedRule =>
                arraysEqual(blacklistedRule[0], newRule[0]) &&
                arraysEqual(blacklistedRule[1], newRule[1])
            );

            if (!isDuplicate && !isBlacklisted) {
                AppState.keywordsBlock.push([...newRule]);
                addedCount++;
            }
        });

        if (addedCount > 0) {
            sortRules(AppState.keywordsBlock);
            AppState.saveBlockKeywords();
        }

        return addedCount;
    };


    // ============================================================================
    // STATE MANAGEMENT
    // ============================================================================

    /**
     * Application state manager
     */
    const AppState = {
        keywordsHighlight: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.HIGHLIGHT)) || CONFIG.DEFAULTS.HIGHLIGHT,
        keywordsBlock: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.BLOCK)) || CONFIG.DEFAULTS.BLOCK,
        isEnabled: JSON.parse(localStorage.getItem('isEnabled')) ?? true,
        fansubbersToggle: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.FANSUBBERS_TOGGLE)) ?? false,
        minisToggle: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.MINIS_TOGGLE)) ?? false,
        fansubbersBlacklist: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.FANSUBBERS_BLACKLIST)) || [],
        minisBlacklist: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.MINIS_BLACKLIST)) || [],
        blockedCategories: JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.BLOCKED_CATEGORIES)) || [],
        editMode: {
            isEditing: false,
            type: null,
            index: null,
            originalData: null
        },

        /**
         * Save highlight keywords to localStorage
         */
        saveHighlightKeywords() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.HIGHLIGHT, JSON.stringify(this.keywordsHighlight));
        },

        /**
         * Save block keywords to localStorage
         */
        saveBlockKeywords() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.BLOCK, JSON.stringify(this.keywordsBlock));
        },

        /**
         * Save enabled state to localStorage
         */
        saveEnabledState() {
            localStorage.setItem('isEnabled', JSON.stringify(this.isEnabled));
        },

        /**
         * Save toggle states to localStorage
         */
        saveToggleStates() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.FANSUBBERS_TOGGLE, JSON.stringify(this.fansubbersToggle));
            localStorage.setItem(CONFIG.STORAGE_KEYS.MINIS_TOGGLE, JSON.stringify(this.minisToggle));
        },

        /**
         * Save blacklists to localStorage
         */
        saveBlacklists() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.FANSUBBERS_BLACKLIST, JSON.stringify(this.fansubbersBlacklist));
            localStorage.setItem(CONFIG.STORAGE_KEYS.MINIS_BLACKLIST, JSON.stringify(this.minisBlacklist));
        },

        /**
         * Reset edit mode to default state
         */
        resetEditMode() {
            this.editMode = {
                isEditing: false,
                type: null,
                index: null,
                originalData: null
            };
        },

        /**
         * Color settings
         */
        colors: {
            light: localStorage.getItem('highlightColorLight') || '#F0E68C', // khaki
            dark: localStorage.getItem('highlightColorDark') || '#330033' // purple
        },

        /**
         * Save highlight colors to localStorage
         */
        saveColors() {
            localStorage.setItem('highlightColorLight', this.colors.light);
            localStorage.setItem('highlightColorDark', this.colors.dark);
        },

        /**
         * Get current highlight color based on theme
         */
        getCurrentHighlightColor() {
            return this.isDarkMode() ? this.colors.dark : this.colors.light;
        },

        /**
         * Detect if current theme is dark mode
         */
        isDarkMode() {
            const body = document.body;
            return body.classList.contains('dark');
        },

        /**
         * Save fansubbers toggle state
         */
        saveFansubbersToggle() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.FANSUBBERS_TOGGLE, JSON.stringify(this.fansubbersToggle));
        },

        /**
         * Save minis toggle state
         */
        saveMinisToggle() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.MINIS_TOGGLE, JSON.stringify(this.minisToggle));
        },

        /**
         * Save fansubbers blacklist
         */
        saveFansubbersBlacklist() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.FANSUBBERS_BLACKLIST, JSON.stringify(this.fansubbersBlacklist));
        },

        /**
         * Save minis blacklist
         */
        saveMinisBlacklist() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.MINIS_BLACKLIST, JSON.stringify(this.minisBlacklist));
        },

        /**
         * Save blocked categories to localStorage
        */
        saveBlockedCategories() {
            localStorage.setItem(CONFIG.STORAGE_KEYS.BLOCKED_CATEGORIES, JSON.stringify(this.blockedCategories));
        }
    };

    // ============================================================================
    // CHIP INPUT COMPONENT
    // ============================================================================

    /**
     * Chip input component for managing keyword tags
     */
    class ChipInput {
        constructor(containerId, placeholder) {
            this.containerId = containerId;
            this.placeholder = placeholder;
            this.chips = [];
            this.container = null;
            this.input = null;
            this.display = null;
            this.init();
        }

        /**
         * Initialize the chip input component
         */
        init() {
            this.container = document.getElementById(this.containerId);
            if (!this.container) {
                console.error(`Container with ID ${this.containerId} not found`);
                return;
            }

            this.container.innerHTML = `
                <div class="chip-container">
                    <div class="chips-display"></div>
                    <input type="text" class="chip-input" placeholder="${this.placeholder}">
                </div>
            `;

            this.input = this.container.querySelector('.chip-input');
            this.display = this.container.querySelector('.chips-display');

            this.bindEvents();
            this.updateDisplay();
        }

        /**
         * Bind event listeners
         */
        bindEvents() {
            if (!this.input) return;

            this.input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' && this.input.value.trim()) {
                    e.preventDefault();
                    this.addChip(sanitizeInput(this.input.value));
                    this.input.value = '';
                } else if (e.key === 'Backspace' && !this.input.value && this.chips.length > 0) {
                    this.removeChip(this.chips.length - 1);
                }
            });
        }

        /**
         * Add a new chip
         * @param {string} text - Chip text
         */
        addChip(text) {
            if (text && !this.chips.includes(text)) {
                this.chips.push(text);
                this.updateDisplay();
            }
        }

        /**
         * Remove chip at index
         * @param {number} index - Chip index
         */
        removeChip(index) {
            if (index >= 0 && index < this.chips.length) {
                this.chips.splice(index, 1);
                this.updateDisplay();
            }
        }

        /**
         * Set chips from array
         * @param {Array} chips - Array of chip texts
         */
        setChips(chips) {
            this.chips = Array.isArray(chips) ? [...chips] : [];
            this.updateDisplay();
        }

        /**
         * Get current chips
         * @returns {Array} Array of chip texts
         */
        getChips() {
            return [...this.chips];
        }

        /**
         * Clear all chips
         */
        clear() {
            this.chips = [];
            this.updateDisplay();
            if (this.input) {
                this.input.value = '';
            }
        }

        /**
         * Edit chip at index
         * @param {number} index - Chip index
         */
        editChip(index) {
            if (index >= 0 && index < this.chips.length) {
                const chipText = this.chips[index];
                this.chips.splice(index, 1);
                if (this.input) {
                    this.input.value = chipText;
                    this.input.focus();
                }
                this.updateDisplay();
            }
        }

        /**
         * Update the visual display of chips
         */
        updateDisplay() {
            if (!this.display) return;

            this.display.innerHTML = this.chips.map((chip, index) =>
                `<span class="chip" data-index="${index}">
                    ${chip}
                    <span class="chip-remove" data-index="${index}">×</span>
                </span>`
            ).join('');

            this.bindChipEvents();
            this.updatePlaceholder();
        }

        /**
         * Bind events for chip elements
         */
        bindChipEvents() {
            // Chip click for editing
            this.display.querySelectorAll('.chip').forEach(chipElement => {
                chipElement.addEventListener('click', (e) => {
                    if (e.target.classList.contains('chip-remove')) return;
                    e.stopPropagation();
                    const index = parseInt(chipElement.dataset.index);
                    this.editChip(index);
                });
            });

            // Remove button click
            this.display.querySelectorAll('.chip-remove').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    e.preventDefault();
                    const index = parseInt(e.target.dataset.index);
                    this.removeChip(index);
                });
            });
        }

        /**
         * Update input placeholder text
         */
        updatePlaceholder() {
            if (this.input) {
                this.input.placeholder = this.chips.length === 0
                    ? this.placeholder
                    : 'Press Enter to add more...';
            }
        }
    }

    // ============================================================================
    // FILTERING LOGIC
    // ============================================================================

    /**
     * Check if title should be highlighted
     * @param {string} title - Title to check
     * @returns {boolean} True if should be highlighted
     */
    const shouldHighlightTitle = (title) => {
        if (!title || typeof title !== 'string') return false;

        return AppState.keywordsHighlight.some(([keywordParts, exceptionParts]) => {
            const keywordPredicates = keywordParts
                .filter(keyword => keyword && keyword.trim())
                .map(keyword => Chips.contains(keyword.trim()));

            const filteredExceptions = Array.isArray(exceptionParts)
                ? exceptionParts.filter(exception => exception && exception.trim())
                : [];

            const exceptionPredicates = filteredExceptions
                .map(exception => Chips.contains(exception.trim()));

            if (keywordPredicates.length === 0) return false;

            let chain = Chips.from(title).and(Chips.all(keywordPredicates));

            if (exceptionPredicates.length > 0) {
                chain = chain.not(Chips.any(exceptionPredicates));
            }

            return chain.result();
        });
    };

    /**
     * Check if title should be blocked
     * @param {string} title - Title to check
     * @returns {boolean} True if should be blocked
     */
    const shouldBlockTitle = (title) => {
        if (!title || typeof title !== 'string') return false;

        return AppState.keywordsBlock.some(([keywordParts, exceptionParts]) => {
            const keywordPredicates = keywordParts
                .filter(keyword => keyword && keyword.trim())
                .map(keyword => Chips.contains(keyword.trim()));

            const filteredExceptions = Array.isArray(exceptionParts)
                ? exceptionParts.filter(exception => exception && exception.trim())
                : [];

            const exceptionPredicates = filteredExceptions
                .map(exception => Chips.contains(exception.trim()));

            if (keywordPredicates.length === 0) return false;

            let chain = Chips.from(title).and(Chips.all(keywordPredicates));

            if (exceptionPredicates.length > 0) {
                chain = chain.not(Chips.any(exceptionPredicates));
            }

            return chain.result();
        });
    };

    // ============================================================================
    // DOM MANIPULATION
    // ============================================================================

    /**
     * Extract title from torrent row element
     * @param {Element} titleElement - Title element
     * @returns {string} Extracted title
     */
    const getTitleFromElement = (titleElement) => {
        if (!titleElement) return '';

        let title = titleElement.getAttribute('title') || titleElement.textContent.trim();

        if (titleElement.classList.contains('comments')) {
            const nextSibling = titleElement.nextElementSibling;
            if (nextSibling && nextSibling.tagName === 'A') {
                title = getTitleFromElement(nextSibling);
            }
        }

        return title || '';
    };

    /**
     * Get category from torrent row
     * @param {Element} row - Torrent row element
     * @returns {string} Category identifier
     */
    const getCategoryFromRow = (row) => {
        const categoryImg = row.querySelector('img[alt]');
        if (!categoryImg) return '';

        const alt = categoryImg.getAttribute('alt') || '';

        // Direct mapping from alt text to category IDs
        const categoryMap = isSukebei ? {
            'Art - Anime': 'art-anime',
            'Art - Doujinshi': 'art-doujinshi',
            'Art - Games': 'art-games',
            'Art - Manga': 'art-manga',
            'Art - Pictures': 'art-pictures',
            'Real Life - Photobooks and Pictures': 'real-life-photobooks',
            'Real Life - Videos': 'real-life-videos'
        } : {
            'Anime - AMV': 'anime-amv',
            'Anime - English-translated': 'anime-english',
            'Anime - Non-English-translated': 'anime-non-english',
            'Anime - Raw': 'anime-raw',
            'Audio - Lossless': 'audio-lossless',
            'Audio - Lossy': 'audio-lossy',
            'Literature - English-translated': 'literature-english',
            'Literature - Non-English-translated': 'literature-non-english',
            'Literature - Raw': 'literature-raw',
            'Live Action - English-translated': 'live-action-english',
            'Live Action - Idol/Promotional Video': 'live-action-idol',
            'Live Action - Non-English-translated': 'live-action-non-english',
            'Live Action - Raw': 'live-action-raw',
            'Pictures - Graphics': 'pictures-graphics',
            'Pictures - Photos': 'pictures-photos',
            'Software - Applications': 'software-applications',
            'Software - Games': 'software-games'
        };

        return categoryMap[alt] || '';
    };

    /**
     * Get the current category being browsed from URL
     * @returns {string|null} Current category ID or null if not browsing a specific category
     */
    const getCurrentBrowsingCategory = () => {
        const urlParams = new URLSearchParams(window.location.search);
        const categoryParam = urlParams.get('c');

        if (!categoryParam) return null;

        // Map URL category codes to our category IDs
        const categoryCodeMap = isSukebei ? {
            '1_1': 'art-anime',
            '1_2': 'art-doujinshi',
            '1_3': 'art-games',
            '1_4': 'art-manga',
            '1_5': 'art-pictures',
            '2_1': 'real-life-photobooks',
            '2_2': 'real-life-videos'
        } : {
            '1_1': 'anime-amv',
            '1_2': 'anime-english',
            '1_3': 'anime-non-english',
            '1_4': 'anime-raw',
            '2_1': 'audio-lossless',
            '2_2': 'audio-lossy',
            '3_1': 'literature-english',
            '3_2': 'literature-non-english',
            '3_3': 'literature-raw',
            '4_1': 'live-action-english',
            '4_2': 'live-action-idol',
            '4_3': 'live-action-non-english',
            '4_4': 'live-action-raw',
            '5_1': 'pictures-graphics',
            '5_2': 'pictures-photos',
            '6_1': 'software-applications',
            '6_2': 'software-games'
        };

        return categoryCodeMap[categoryParam] || null;
    };

    /**
     * Update display of torrent rows based on current filters
     */
    const updateDisplay = debounce(() => {
        const rows = document.querySelectorAll(SELECTORS.TORRENT_ROWS);
        const currentBrowsingCategory = getCurrentBrowsingCategory();

        rows.forEach(row => {
            const titleElement = row.querySelector(SELECTORS.TITLE_ELEMENT);
            const title = getTitleFromElement(titleElement);
            const category = getCategoryFromRow(row);

            // Reset row if disabled
            if (!AppState.isEnabled) {
                resetRowAppearance(row);
                return;
            }

            // Check if category is blocked, but skip if it's the current browsing category
            if (category &&
                AppState.blockedCategories.includes(category) &&
                category !== currentBrowsingCategory) {
                row.style.display = 'none';
                return;
            }

            const shouldHighlight = shouldHighlightTitle(title);
            const shouldBlock = shouldBlockTitle(title);

            // Apply blocking first (higher priority)
            if (shouldBlock) {
                row.style.display = 'none';
            } else {
                row.style.display = '';

                // Apply highlighting
                if (shouldHighlight) {
                    applyHighlight(row);
                } else {
                    resetRowAppearance(row);
                }
            }
        });

        // Check if everything is blocked and show message if needed
        checkIfEverythingBlocked();
    }, CONFIG.DEBOUNCE_DELAY);

    /**
     * Apply highlight styling to row
     * @param {Element} row - Row element
     */
    const applyHighlight = (row) => {
        // Don't highlight rows with class="info"
        if (row.classList.contains('info')) {
            return;
        }

        if (!row.hasAttribute('data-old-class')) {
            row.setAttribute('data-old-class', row.className);
        }
        row.className = "default";

        // Use the current theme-appropriate highlight color
        row.style.backgroundColor = AppState.getCurrentHighlightColor();
    };

    /**
     * Reset row appearance to original state
     * @param {Element} row - Row element
     */
    const resetRowAppearance = (row) => {
        // Don't modify rows with class="info", for nyaablue
        if (row.classList.contains('info')) {
            return;
        }

        if (row.hasAttribute('data-old-class')) {
            row.className = row.getAttribute('data-old-class');
            row.removeAttribute('data-old-class');
        }
        row.style.backgroundColor = '';
        row.style.display = '';
    };

    /**
     * Check if all rows are hidden and show appropriate message
     */
    const checkIfEverythingBlocked = () => {
        const rows = document.querySelectorAll(SELECTORS.TORRENT_ROWS);
        const visibleRows = Array.from(rows).filter(row => {
            const computedStyle = window.getComputedStyle(row);
            return computedStyle.display !== 'none';
        });

        let messageElement = document.getElementById('everything-blocked-message');

        // Only show message if script is enabled and there are rows but none are visible
        if (AppState.isEnabled && rows.length > 0 && visibleRows.length === 0) {
            if (!messageElement) {
                // Create the message element
                messageElement = document.createElement('div');
                messageElement.id = 'everything-blocked-message';
                messageElement.className = 'everything-blocked-message';
                messageElement.innerHTML = `
                    🚫 Everything's blocked! Maybe your blocking is a little too aggressive...
                    <div class="subtitle">Try adjusting your block rules or category filters in the <a href="#" id="open-settings-link" style="color: #007bff; text-decoration: underline; cursor: pointer;">settings</a>.</div>
                `;

                // Insert after the torrent table
                const torrentTable = document.querySelector('.torrent-list');
                if (torrentTable) {
                    torrentTable.parentNode.insertBefore(messageElement, torrentTable.nextSibling);
                } else {
                    // Fallback: insert after the first table found
                    const firstTable = document.querySelector('table');
                    if (firstTable) {
                        firstTable.parentNode.insertBefore(messageElement, firstTable.nextSibling);
                    }
                }
            }

            // Always rebind the click event (in case the element was recreated)
            const settingsLink = document.getElementById('open-settings-link');
            if (settingsLink) {
                // Remove any existing event listeners by cloning the element
                const newSettingsLink = settingsLink.cloneNode(true);
                settingsLink.parentNode.replaceChild(newSettingsLink, settingsLink);

                // Add the event listener to the new element
                newSettingsLink.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    console.log('Settings link clicked'); // Debug log

                    // Make sure GUI is visible first
                    if (UI.guiContainer.style.display !== 'flex') {
                        UI.toggleGUI();
                    }
                });
            }

            messageElement.style.display = 'block';
        } else {
            // Hide message if it exists
            if (messageElement) {
                messageElement.style.display = 'none';
            }
        }
    };

    // ============================================================================
    // EDIT MODE MANAGEMENT
    // ============================================================================

    /**
     * Enter edit mode for a rule
     * @param {string} type - 'highlight' or 'block'
     * @param {number} index - Rule index
     */
    const enterEditMode = (type, index) => {
        // Exit current edit mode if different rule
        if (AppState.editMode.isEditing &&
            (AppState.editMode.type !== type || AppState.editMode.index !== index)) {
            exitEditMode();
        }

        AppState.editMode.isEditing = true;
        AppState.editMode.type = type;
        AppState.editMode.index = index;

        const keywords = type === 'highlight' ? AppState.keywordsHighlight : AppState.keywordsBlock;
        AppState.editMode.originalData = [...keywords[index]];

        const [keywordParts, exceptionParts] = keywords[index];

        if (type === 'highlight') {
            UI.chipInputs.highlightKeyword.setChips(keywordParts);
            UI.chipInputs.highlightException.setChips(exceptionParts);
            updateEditButton('add-keyword-highlight-btn', 'Update Highlight Rule');
            addCancelButton('highlight');
        } else {
            UI.chipInputs.blockKeyword.setChips(keywordParts);
            UI.chipInputs.blockException.setChips(exceptionParts);
            updateEditButton('add-keyword-block-btn', 'Update Block Rule');
            addCancelButton('block');
        }

        updateRuleLists();
    };

    /**
     * Exit edit mode
     */
    const exitEditMode = () => {
        AppState.resetEditMode();

        // Reset buttons
        updateEditButton('add-keyword-highlight-btn', 'Add Highlight Rule', '#555');
        updateEditButton('add-keyword-block-btn', 'Add Block Rule', '#555');

        // Remove cancel buttons
        removeCancelButtons();

        // Clear validation errors
        hideValidationError('highlight');
        hideValidationError('block');

        // Clear inputs
        if (UI.chipInputs) {
            Object.values(UI.chipInputs).forEach(input => input.clear());
        }

        updateRuleLists();
    };

    /**
     * Update edit button appearance
     * @param {string} buttonId - Button ID
     * @param {string} text - Button text
     * @param {string} color - Button color
     */
    const updateEditButton = (buttonId, text, color = '#28a745') => {
        const button = document.getElementById(buttonId);
        if (button) {
            button.textContent = text;
            button.style.backgroundColor = color;
        }
    };

    /**
     * Add cancel button for edit mode
     * @param {string} type - 'highlight' or 'block'
     */
    const addCancelButton = (type) => {
        const containerId = `${type}-cancel-container`;
        const targetButtonId = `add-keyword-${type}-btn`;
        const targetButton = document.getElementById(targetButtonId);

        if (!document.getElementById(containerId) && targetButton) {
            const cancelBtn = document.createElement('button');
            cancelBtn.id = containerId;
            cancelBtn.textContent = 'Cancel Edit';
            cancelBtn.className = 'action-button';
            cancelBtn.style.backgroundColor = '#dc3545';
            cancelBtn.style.marginLeft = '8px';
            cancelBtn.addEventListener('click', exitEditMode);

            targetButton.parentNode.insertBefore(cancelBtn, targetButton.nextSibling);
        }
    };

    /**
     * Remove cancel buttons
     */
    const removeCancelButtons = () => {
        ['highlight-cancel-container', 'block-cancel-container'].forEach(id => {
            const button = document.getElementById(id);
            if (button) button.remove();
        });
    };

    // ============================================================================
    // RULE MANAGEMENT
    // ============================================================================

    /**
     * Add or update highlight rule
     */
    const addKeywordToHighlight = () => {
        if (!UI.chipInputs) return;

        // Hide any existing validation errors
        hideValidationError('highlight');

        let keywordParts = UI.chipInputs.highlightKeyword.getChips();
        let exceptionParts = UI.chipInputs.highlightException.getChips();

        // Check for text in input fields that hasn't been converted to chips
        const keywordInputText = UI.chipInputs.highlightKeyword.input.value.trim();
        const exceptionInputText = UI.chipInputs.highlightException.input.value.trim();

        if (keywordInputText || exceptionInputText) {
            let warningMessage = 'Warning: You have text that hasn\'t been converted to chips.\nPress "Enter" in each field to convert text to chips, then try again.';
            showValidationError('highlight', warningMessage);
            return;
        }

        // Validation: Check if we have keywords
        if (keywordParts.length === 0) {
            showValidationError('highlight', 'Error: Please add at least one keyword.');
            return;
        }

        // Validation: Check if only exceptions without keywords (redundant but kept for clarity)
        if (keywordParts.length === 0 && exceptionParts.length > 0) {
            showValidationError('highlight', 'Error: Cannot create a rule with only exceptions. Please add at least one keyword.');
            return;
        }

        if (AppState.editMode.isEditing && AppState.editMode.type === 'highlight') {
            // Update existing rule
            AppState.keywordsHighlight[AppState.editMode.index] = [keywordParts, exceptionParts];
            exitEditMode();
        } else {
            // Add new rule if not duplicate
            const isDuplicate = AppState.keywordsHighlight.some(([existingKeywords, existingExceptions]) => {
                return arraysEqual(existingKeywords, keywordParts) &&
                       arraysEqual(existingExceptions, exceptionParts);
            });

            if (!isDuplicate) {
                AppState.keywordsHighlight.push([keywordParts, exceptionParts]);
                sortRules(AppState.keywordsHighlight);
            }

            UI.chipInputs.highlightKeyword.clear();
            UI.chipInputs.highlightException.clear();
        }

        AppState.saveHighlightKeywords();
        updateRuleLists();
        updateDisplay();
    };

    /**
     * Add or update block rule
     */
    const addKeywordToBlock = () => {
        if (!UI.chipInputs) return;

        // Hide any existing validation errors
        hideValidationError('block');

        let keywordParts = UI.chipInputs.blockKeyword.getChips();
        let exceptionParts = UI.chipInputs.blockException.getChips();

        // Check for text in input fields that hasn't been converted to chips
        const keywordInputText = UI.chipInputs.blockKeyword.input.value.trim();
        const exceptionInputText = UI.chipInputs.blockException.input.value.trim();

        if (keywordInputText || exceptionInputText) {
            let warningMessage = 'Warning: You have text that hasn\'t been converted to chips.\nPress "Enter" in each field to convert text to chips, then try again.';
            showValidationError('block', warningMessage);
            return;
        }

        // Validation: Check if we have keywords
        if (keywordParts.length === 0) {
            showValidationError('block', 'Error: Please add at least one keyword.');
            return;
        }

        // Validation: Check if only exceptions without keywords (redundant but kept for clarity)
        if (keywordParts.length === 0 && exceptionParts.length > 0) {
            showValidationError('block', 'Error: Cannot create a rule with only exceptions. Please add at least one keyword.');
            return;
        }

        if (AppState.editMode.isEditing && AppState.editMode.type === 'block') {
            // Update existing rule
            AppState.keywordsBlock[AppState.editMode.index] = [keywordParts, exceptionParts];
            exitEditMode();
        } else {
            // Add new rule if not duplicate
            const isDuplicate = AppState.keywordsBlock.some(([existingKeywords, existingExceptions]) => {
                return arraysEqual(existingKeywords, keywordParts) &&
                       arraysEqual(existingExceptions, exceptionParts);
            });

            if (!isDuplicate) {
                AppState.keywordsBlock.push([keywordParts, exceptionParts]);
                sortRules(AppState.keywordsBlock);
            }

            UI.chipInputs.blockKeyword.clear();
            UI.chipInputs.blockException.clear();
        }

        AppState.saveBlockKeywords();
        updateRuleLists();
        updateDisplay();
    };

    /**
     * Sort rules alphabetically by first keyword, ignoring leading brackets and parentheses
     * @param {Array} rules - Rules array to sort
     */
    const sortRules = (rules) => {
        rules.sort((a, b) => {
            // Get the first keyword from each rule and remove leading brackets and parentheses
            const keywordA = a[0].join(' ').toLowerCase().replace(/^[\[\(]/, '');
            const keywordB = b[0].join(' ').toLowerCase().replace(/^[\[\(]/, '');
            return keywordA.localeCompare(keywordB);
        });
    };

    /**
     * Show validation error message
     * @param {string} type - 'highlight' or 'block'
     * @param {string} message - Error message to display
     */
    const showValidationError = (type, message) => {
        const errorElement = document.getElementById(`${type}-validation-error`);
        if (errorElement) {
            errorElement.textContent = message;
            errorElement.classList.add('show');

            // Auto-hide after 10 seconds
            setTimeout(() => {
                hideValidationError(type);
            }, 10000);
        }
    };

    /**
     * Hide validation error message
     * @param {string} type - 'highlight' or 'block'
     */
    const hideValidationError = (type) => {
        const errorElement = document.getElementById(`${type}-validation-error`);
        if (errorElement) {
            errorElement.classList.remove('show');
            errorElement.textContent = '';
        }
    };

    /**
     * Remove rule by index
     * @param {string} type - 'highlight' or 'block'
     * @param {number} index - Rule index
     */
    const removeRule = (type, index) => {
        if (type === 'highlight') {
            const removedRule = AppState.keywordsHighlight[index];

            // Check if this rule came from fansubbers preset and add to blacklist
            const isFromFansubbers = PRESETS.FANSUBBERS.some(presetRule =>
                arraysEqual(presetRule[0], removedRule[0]) &&
                arraysEqual(presetRule[1], removedRule[1])
            );

            if (isFromFansubbers) {
                AppState.fansubbersBlacklist.push([...removedRule]);
                AppState.saveBlacklists();
            }

            AppState.keywordsHighlight.splice(index, 1);
            AppState.saveHighlightKeywords();
        } else {
            const removedRule = AppState.keywordsBlock[index];

            // Check if this rule came from minis preset and add to blacklist
            const isFromMinis = PRESETS.MINIS.some(presetRule =>
                arraysEqual(presetRule[0], removedRule[0]) &&
                arraysEqual(presetRule[1], removedRule[1])
            );

            if (isFromMinis) {
                AppState.minisBlacklist.push([...removedRule]);
                AppState.saveBlacklists();
            }

            AppState.keywordsBlock.splice(index, 1);
            AppState.saveBlockKeywords();
        }

        updateRuleLists();
        updateDisplay();
    };


        /**
     * Export configuration to JSON file
     */
    const exportConfiguration = () => {
        const config = {
            version: "1.0",
            timestamp: new Date().toISOString(),
            keywordsHighlight: AppState.keywordsHighlight,
            keywordsBlock: AppState.keywordsBlock,
            isEnabled: AppState.isEnabled,
            colors: AppState.colors
        };


        const dataStr = JSON.stringify(config, null, 2);
        const dataBlob = new Blob([dataStr], { type: 'application/json' });

        const link = document.createElement('a');
        link.href = URL.createObjectURL(dataBlob);
        link.download = `nyaa-highlight-block-config-${new Date().toISOString().split('T')[0]}.json`;
        link.click();

        URL.revokeObjectURL(link.href);
    };

    /**
     * Import configuration from JSON file
     */
    const importConfiguration = () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';

        input.onchange = (event) => {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const config = JSON.parse(e.target.result);

                    // Validate the configuration structure
                    if (!config.keywordsHighlight || !config.keywordsBlock) {
                        alert('Error: Invalid configuration file format.');
                        return;
                    }

                    // Show confirmation dialog
                    const confirmed = confirm(
                        'Warning: This will replace ALL your current settings.\n\n' +
                        `Import contains:\n` +
                        `• ${config.keywordsHighlight.length} highlight rules\n` +
                        `• ${config.keywordsBlock.length} block rules\n` +
                        `• Custom colors: ${config.colors ? 'Yes' : 'No'}\n\n` +
                        'Do you want to continue?'
                    );

                    if (confirmed) {
                        // Import the configuration
                        AppState.keywordsHighlight = config.keywordsHighlight || [];
                        AppState.keywordsBlock = config.keywordsBlock || [];
                        AppState.isEnabled = config.isEnabled !== undefined ? config.isEnabled : true;

                        // Import colors if available
                        if (config.colors) {
                            AppState.colors = {
                                light: config.colors.light || '#F0E68C',
                                dark: config.colors.dark || '#330033'
                            };
                        }

                        // Save everything
                        AppState.saveHighlightKeywords();
                        AppState.saveBlockKeywords();
                        AppState.saveEnabledState();
                        AppState.saveColors();

                        // Update UI
                        updateRuleLists();
                        updateDisplay();

                        // Update color pickers
                        const lightPicker = document.getElementById('light-color-picker');
                        const darkPicker = document.getElementById('dark-color-picker');
                        if (lightPicker) lightPicker.value = AppState.colors.light;
                        if (darkPicker) darkPicker.value = AppState.colors.dark;

                        // Update toggle switch
                        const toggleSwitch = document.getElementById('toggle-highlight-block-switch');
                        if (toggleSwitch) {
                            toggleSwitch.checked = AppState.isEnabled;
                        }

                        alert('Configuration imported successfully!');
                    }
                } catch (error) {
                    alert('Error: Could not parse configuration file. Please ensure it\'s a valid JSON file.');
                    console.error('Import error:', error);
                }
            };
            reader.readAsText(file);
        };

        input.click();
    };

    // ============================================================================
    // COLOR PICKER HANDLERS
    // ============================================================================

        /**
     * Handle light mode color change
     */
    const handleLightColorChange = (event) => {
        AppState.colors.light = event.target.value;
        AppState.saveColors();
        updateDisplay(); // Refresh highlights with new color
    };

    /**
     * Handle dark mode color change
     */
    const handleDarkColorChange = (event) => {
        AppState.colors.dark = event.target.value;
        AppState.saveColors();
        updateDisplay(); // Refresh highlights with new color
    };

    /**
     * Reset light mode color to default
     */
    const resetLightColor = () => {
        AppState.colors.light = '#F0E68C'; // khaki
        AppState.saveColors();
        const lightPicker = document.getElementById('light-color-picker');
        if (lightPicker) {
            lightPicker.value = AppState.colors.light;
        }
        updateDisplay();
    };

    /**
     * Reset dark mode color to default
     */
    const resetDarkColor = () => {
        AppState.colors.dark = '#330033'; // purple
        AppState.saveColors();
        const darkPicker = document.getElementById('dark-color-picker');
        if (darkPicker) {
            darkPicker.value = AppState.colors.dark;
        }
        updateDisplay();
    };

    // ============================================================================
    // THEME MONITORING
    // ============================================================================

    /**
     * Monitor body class changes for theme switching
     */
    const initThemeMonitoring = () => {
        // Create a MutationObserver to watch for class changes on body
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                    // Body class changed, update highlights with new theme colors
                    updateDisplay();
                }
            });
        });

        // Start observing body element for class attribute changes
        observer.observe(document.body, {
            attributes: true,
            attributeFilter: ['class']
        });
    };

    // ============================================================================
    // UI MANAGEMENT
    // ============================================================================

    /**
     * UI management object
     */
    const UI = {
        guiContainer: null,
        openButton: null,
        chipInputs: null,

        /**
         * Initialize the user interface
         */
        init() {
            this.createStyles();
            this.createGUIContainer();
            this.createOpenButton();
            this.bindEvents();
            this.initializeChipInputs();
            updateRuleLists();
        },

        /**
         * Create CSS styles
         */
        createStyles() {
            const style = document.createElement('style');
            style.textContent = `
                .chip-container {
                    display: flex;
                    flex-wrap: wrap;
                    align-items: center;
                    min-height: 32px;
                    padding: 4px;
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    background-color: #f5f5f5;
                    margin-bottom: 8px;
                    gap: 4px;
                }

                .chips-display {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 4px;
                }

                .chip {
                    display: inline-flex;
                    align-items: center;
                    background-color: #28a745;
                    color: white;
                    padding: 4px 8px;
                    border-radius: 16px;
                    font-size: 12px;
                    white-space: nowrap;
                    cursor: pointer;
                    transition: background-color 0.2s;
                }

                .chip:hover {
                    background-color: #1e7e34;
                }

                #highlight-exception-container .chip,
                #block-exception-container .chip {
                    background-color: #dc3545;
                }

                #highlight-exception-container .chip:hover,
                #block-exception-container .chip:hover {
                    background-color: #c82333;
                }

                .chip-remove {
                    margin-left: 6px;
                    cursor: pointer;
                    font-size: 14px;
                    user-select: none;
                    z-index: 1;
                    position: relative;
                }

                .chip-remove:hover {
                    color: #ff6b6b;
                }

                .chip-input {
                    border: none !important;
                    outline: none !important;
                    background: transparent !important;
                    color: #333 !important;
                    flex: 1;
                    min-width: 80px;
                    padding: 2px !important;
                    margin: 0 !important;
                    font-size: 14px;
                }

                #keywords-highlight-list, #keywords-block-list {
                    overflow-y: auto;
                    padding: 5px;
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    max-height: calc(90vh - 300px);
                    min-height: 50px;
                }

                .side-by-side {
                    display: flex;
                    justify-content: space-between;
                    gap: 20px;
                }

                .side-by-side > div {
                    width: 49%;
                    display: flex;
                    flex-direction: column;
                }

                .keyword-input {
                    display: flex;
                    flex-direction: column;
                    margin-bottom: 15px;
                    flex-shrink: 0;
                }

                .action-button {
                    border: 1px solid #333;
                    border-radius: 5px;
                    cursor: pointer;
                    background-color: #555;
                    color: white;
                    padding: 8px 12px;
                    transition: background-color 0.2s;
                }

                .action-button:hover:not(:disabled) {
                    background-color: #666;
                }

                .action-button:disabled {
                    background-color: #888;
                    cursor: not-allowed;
                    opacity: 0.6;
                }

                .switch {
                    position: relative;
                    display: inline-block;
                    width: 34px;
                    height: 20px;
                }

                .switch input {
                    opacity: 0;
                    width: 0;
                    height: 0;
                }

                .slider {
                    position: absolute;
                    cursor: pointer;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background-color: #dc3545;
                    transition: .4s;
                    border-radius: 34px;
                }

                .slider:before {
                    position: absolute;
                    content: "";
                    height: 12px;
                    width: 12px;
                    border-radius: 50%;
                    left: 4px;
                    bottom: 4px;
                    background-color: white;
                    transition: .4s;
                }

                input:checked + .slider {
                    background-color: #4CAF50;
                }

                input:checked + .slider:before {
                    transform: translateX(14px);
                }

                #guiContainer h3 {
                    margin-top: 0px;
                }

                .rule-item {
                    display: flex;
                    align-items: center;
                    padding: 4px;
                    margin-bottom: 4px;
                    border: 1px solid #555;
                    border-radius: 8px;
                    background-color: #2a2a2a;
                    min-height: 32px;
                }

                .rule-item.editing {
                    border: 2px solid #ffc107;
                    background-color: #3a3a2a;
                }

                .rule-buttons {
                    flex-shrink: 0;
                    margin-right: 8px;
                }

                .rule-keywords {
                    flex-grow: 1;
                    margin-right: 8px;
                }

                .rule-exceptions {
                    flex-shrink: 0;
                }

                .colored-chip {
                    color: white;
                    padding: 2px 6px;
                    border-radius: 12px;
                    margin: 2px;
                    display: inline-block;
                    font-size: 12px;
                }

                .green-chip {
                    background-color: #28a745;
                }

                .red-chip {
                    background-color: #dc3545;
                }

                .button-group {
                    display: flex;
                    gap: 4px;
                }

                .rule-button {
                    padding: 4px 8px;
                    font-size: 12px;
                }

                .validation-error {
                    color: #dc3545;
                    font-size: 12px;
                    margin-top: 4px;
                    display: none;
                    white-space: pre-line;
                    line-height: 1.4;
                }

                .validation-error.show {
                    display: block;
                }

                .section-header {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    margin-bottom: 15px;
                }

                .section-title {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                    margin: 0;
                }

                .top-right-controls {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                }

                .help-icon {
                    width: 20px;
                    height: 20px;
                    border-radius: 50%;
                    background-color: #666;
                    color: white;
                    border: none;
                    cursor: pointer;
                    font-size: 12px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: background-color 0.2s;
                    margin-bottom:10px
                }

                .help-icon:hover {
                    background-color: #777;
                }

                .close-x {
                    background: none;
                    border: none;
                    color: #ccc;
                    font-size: 20px;
                    cursor: pointer;
                    margin-top: -5px;
                    margin-bottom: 7px;
                }

                .close-x:hover {
                    color: white;
                }

                .help-popup, .settings-overlay {
                    position: fixed;
                    top: 5vh;
                    bottom: 5vh;
                    left: 50%;
                    transform: translateX(-50%);
                    background-color: #333;
                    color: white;
                    padding: 20px;
                    z-index: 10002;
                    border: 2px solid #ccc;
                    border-radius: 10px;
                    display: none;
                    min-width: 60%;
                    overflow-y: auto;
                    box-shadow: 0 4px 20px rgba(0,0,0,0.5);
                }

                .help-popup ul {
                    margin: 10px 0;
                    padding-left: 20px;
                }

                .help-popup li {
                    margin-bottom: 8px;
                    line-height: 1.4;
                }

                .help-close {
                    float: right;
                    background: none;
                    border: none;
                    color: #ccc;
                    font-size: 20px;
                    cursor: pointer;
                    margin-top: -5px;
                }

                .help-close:hover {
                    color: white;
                }

                .toggle-container {
                    display: flex;
                    align-items: center;
                    gap: 6px;
                }

                .toggle-label {
                    font-size: 12px;
                    color: #ccc;
                    white-space: nowrap;
                }

                .import-export-buttons {
                    display: flex;
                    gap: 8px;
                }

                .import-export-btn {
                    padding: 4px 8px;
                    font-size: 11px;
                    background-color: #6c757d;
                    border: 1px solid #5a6268;
                    color: white;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: background-color 0.2s;
                }

                .import-export-btn:hover {
                    background-color: #5a6268;
                }

                .preset-btn {
                    padding: 8px 16px;
                    font-size: 12px;
                    background-color: #28a745;
                    border: 1px solid #1e7e34;
                    color: white;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: background-color 0.2s;
                }

                .preset-btn:hover {
                    background-color: #218838;
                }

                .preset-btn.minis-btn {
                    background-color: #dc3545;
                    border: 1px solid #c82333;
                }

                .preset-btn.minis-btn:hover {
                    background-color: #c82333;
                }

                .top-right-controls {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                    flex-wrap: wrap;
                }

                .color-picker-row {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                    margin-bottom: 8px;
                }

                .color-picker-row:last-child {
                    margin-bottom: 0;
                }

                .color-label {
                    font-size: 12px;
                    color: #ccc;
                    min-width: 100px;
                    flex-shrink: 0;
                }

                .color-picker {
                    width: 40px;
                    height: 25px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    cursor: pointer;
                    background: none;
                    padding: 0;
                }

                .color-picker::-webkit-color-swatch-wrapper {
                    padding: 0;
                    border: none;
                    border-radius: 4px;
                }

                .color-picker::-webkit-color-swatch {
                    border: none;
                    border-radius: 4px;
                }

                .reset-color-btn {
                    width: 25px;
                    height: 25px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    background-color: #555;
                    color: #ccc;
                    cursor: pointer;
                    font-size: 14px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: background-color 0.2s;
                }

                .reset-color-btn:hover {
                    background-color: #666;
                    color: white;
                }

                .color-preview {
                    margin-left: 8px;
                    font-size: 11px;
                    color: #999;
                    font-style: italic;
                }

                .settings-cog {
                    background: none;
                    border: none;
                    color: #ccc;
                    font-size: 20px;
                    cursor: pointer;
                    margin-top: -7px;
                    transition: color 0.2s, transform 0.2s;
                }

                .settings-cog:hover {
                    color: white;
                    transform: rotate(90deg);
                }

                .settings-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 20px;
                    border-bottom: 1px solid #555;
                    padding-bottom: 10px;
                }

                .settings-header h3 {
                    margin: 0;
                }

                .settings-content {
                    display: flex;
                    flex-direction: column;
                    gap: 20px;
                }

                .settings-section {
                    border: 1px solid #555;
                    border-radius: 8px;
                    padding: 15px;
                    background-color: #2a2a2a;
                }

                .settings-section h4 {
                    margin: 0 0 15px 0;
                    color: #ccc;
                    font-size: 14px;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }

                .settings-buttons {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 10px;
                }

                .settings-button {
                    padding: 8px 16px;
                    font-size: 12px;
                    background-color: #6c757d;
                    border: 1px solid #5a6268;
                    color: white;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: background-color 0.2s;
                }

                .settings-button:hover {
                    background-color: #5a6268;
                }

                .color-picker-section {
                    display: flex;
                    flex-direction: column;
                    gap: 10px;
                }

                .color-picker-row {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                }

                .color-label {
                    font-size: 12px;
                    color: #ccc;
                    min-width: 120px;
                    flex-shrink: 0;
                }

                .color-picker {
                    width: 50px;
                    height: 30px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    cursor: pointer;
                    background: none;
                    padding: 0;
                }

                .color-picker::-webkit-color-swatch-wrapper {
                    padding: 0;
                    border: none;
                    border-radius: 4px;
                }

                .color-picker::-webkit-color-swatch {
                    border: none;
                    border-radius: 4px;
                }

                .reset-color-btn {
                    width: 30px;
                    height: 30px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    background-color: #555;
                    color: #ccc;
                    cursor: pointer;
                    font-size: 16px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: background-color 0.2s;
                }

                .reset-color-btn:hover {
                    background-color: #666;
                    color: white;
                }

                /* Hide main overlay when color picker is active */
                .color-picker-active #guiContainer {
                    display: none !important;
                }

                .color-picker-active .settings-overlay {
                    display: block !important;
                }

                .category-controls {
                    display: flex;
                    gap: 10px;
                    margin-bottom: 15px;
                }

                .category-control-btn {
                    padding: 6px 12px;
                    font-size: 11px;
                    background-color: #6c757d;
                    border: 1px solid #5a6268;
                    color: white;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: background-color 0.2s;
                }

                .category-control-btn:hover {
                    background-color: #5a6268;
                }

                .category-checkboxes {
                    display: flex;
                    flex-direction: column;
                    gap: 8px;
                    max-height: 200px;
                    overflow-y: auto;
                }

                .category-checkbox-row {
                    display: flex;
                    align-items: center;
                }

                .category-checkbox-label {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                    font-size: 12px;
                    color: #ccc;
                    cursor: pointer;
                    width: 100%;
                }

                .category-checkbox {
                    margin: 0;
                    cursor: pointer;
                }

                .category-name {
                    flex-grow: 1;
                }

                .category-checkbox-label:hover .category-name {
                    color: white;
                }

                .everything-blocked-message {
                    text-align: center;
                    padding: 40px 20px;
                    color: #dc3545;
                    font-size: 16px;
                    background-color: rgba(220, 53, 69, 0.1);
                    border: 2px dashed #dc3545;
                    border-radius: 8px;
                    margin: 20px 0;
                    display: none;
                }

                .everything-blocked-message .subtitle {
                    font-size: 14px;
                    color: #666;
                    margin-top: 8px;
                }
            `;
            document.head.appendChild(style);
        },

        /**
         * Create main GUI container
         */
        createGUIContainer() {
            this.guiContainer = document.createElement('div');
            this.guiContainer.id = SELECTORS.GUI_CONTAINER;
            this.guiContainer.style.cssText = `
                position: fixed;
                top: 5vh;
                left: 50%;
                transform: translateX(-50%);
                background-color: #333;
                color: white;
                padding: 20px;
                z-index: 10000;
                border: 2px solid #ccc;
                border-radius: 10px;
                display: none;
                height: 90vh;
                max-width: 95vw;
                overflow-y: auto;
                flex-direction: column;
            `;

            this.guiContainer.innerHTML = this.getGUIHTML();
            document.body.appendChild(this.guiContainer);
            this.updateContainerWidth();
        },

        /**
         * Update container width based on screen size - narrower design
         */
        updateContainerWidth() {
            if (!this.guiContainer) return;
            const screenWidth = window.innerWidth;

            if (screenWidth < 1200) {
                this.guiContainer.style.width = '80vw';
            } else {
                this.guiContainer.style.width = '50vw';
            }
        },



        /**
         * Get HTML content for GUI
         * @returns {string} HTML content
         */
        getGUIHTML() {
            return `
                <div class="side-by-side">
                    <div>
                        <div class="section-header">
                            <div class="section-title">
                                <h3>Highlight</h3>
                                <button class="help-icon" id="help-btn" title="Click for help">?</button>
                            </div>
                        </div>
                        <div class="keyword-input">
                            <p>Must contain:</p>
                            <div id="highlight-keyword-container"></div>
                            <p>But not if it contains:</p>
                            <div id="highlight-exception-container"></div>
                            <div class="button-group">
                                <button id="add-keyword-highlight-btn" class="action-button">Add Highlight Rule</button>
                            </div>
                            <div id="highlight-validation-error" class="validation-error"></div>
                        </div>
                        <ul id="keywords-highlight-list"></ul>
                    </div>
                    <div>
                        <div class="section-header">
                            <div class="section-title">
                                <h3>Block</h3>
                            </div>
                            <div class="top-right-controls">
                                <div class="toggle-container">
                                    <span class="toggle-label">Enable:</span>
                                    <label class="switch">
                                        <input type="checkbox" id="toggle-highlight-block-switch" ${AppState.isEnabled ? 'checked' : ''}>
                                        <span class="slider"></span>
                                    </label>
                                </div>
                                <button class="settings-cog" id="settings-btn" title="Settings">⚙</button>
                                <button class="close-x" id="close-x-btn" title="Close">×</button>
                            </div>
                        </div>
                        <div class="keyword-input">
                            <p>Must contain:</label>
                            <div id="block-keyword-container"></div>
                            <p>But not if it contains:</p>
                            <div id="block-exception-container"></div>
                            <div class="button-group">
                                <button id="add-keyword-block-btn" class="action-button">Add Block Rule</button>
                            </div>
                            <div id="block-validation-error" class="validation-error"></div>
                        </div>
                        <ul id="keywords-block-list"></ul>
                    </div>
                </div>

                <!-- Settings Overlay -->
                <div class="settings-overlay" id="settings-overlay">
                    <div class="settings-header">
                        <h3>Settings</h3>
                        <button class="close-x" id="settings-close-btn" title="Close">×</button>
                    </div>

                    <div class="settings-content">
                         <div class="settings-section">
                            <h4>Highlight Colors</h4>
                            <div class="color-picker-row">
                                <label class="color-label">Light mode color:</label>
                                <input type="color" id="light-color-picker" value="${AppState.colors.light}" class="color-picker">
                                <button id="reset-light-color" class="reset-color-btn" title="Reset to default">↺</button>
                            </div>
                            <div class="color-picker-row">
                                <label class="color-label">Dark mode color:</label>
                                <input type="color" id="dark-color-picker" value="${AppState.colors.dark}" class="color-picker">
                                <button id="reset-dark-color" class="reset-color-btn" title="Reset to default">↺</button>
                            </div>
                        </div>

                        ${!isSukebei ? `
                        <div class="settings-section">
                            <h4>Auto-Import Lists</h4>
                            <div class="settings-buttons" style="flex-direction: column; align-items: flex-start;">
                                <div class="toggle-container">
                                    <span class="toggle-label">Auto-highlight fansubbers:</span>
                                    <label class="switch">
                                        <input type="checkbox" id="fansubbers-toggle" ${AppState.fansubbersToggle ? 'checked' : ''}>
                                        <span class="slider"></span>
                                    </label>
                                </div>
                                <div class="toggle-container">
                                    <span class="toggle-label">Auto-block minis:</span>
                                    <label class="switch">
                                        <input type="checkbox" id="minis-toggle" ${AppState.minisToggle ? 'checked' : ''}>
                                        <span class="slider"></span>
                                    </label>
                                </div>
                            </div>
                        </div>
                        ` : ''}

                        <div class="settings-section">
                            <h4>Block Categories</h4>
                            <div class="category-controls">
                                <button id="select-all-categories" class="category-control-btn">Select All</button>
                                <button id="select-none-categories" class="category-control-btn">Select None</button>
                            </div>
                            <div class="category-checkboxes">
                                ${Object.entries(CATEGORIES).map(([key, label]) => `
                                    <div class="category-checkbox-row">
                                        <label class="category-checkbox-label">
                                            <input type="checkbox" class="category-checkbox" data-category="${key}" ${AppState.blockedCategories.includes(key) ? 'checked' : ''}>
                                            <span class="category-name">${label}</span>
                                        </label>
                                    </div>
                                `).join('')}
                            </div>
                        </div>

                        <div class="settings-section">
                            <h4>Custom Import/Export</h4>
                            <div class="settings-buttons">
                                <button id="import-btn" class="settings-button" title="Import configuration from file">Import Configuration</button>
                                <button id="export-btn" class="settings-button" title="Export configuration to file">Export Configuration</button>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="help-popup" id="help-popup">
                    <button class="help-close" id="help-close">×</button>
                    <h3>How to Use This Tool</h3>

                    <p>What This Tool Does:</p>
                    <ul>
                        <li>Highlight: Makes matching titles stand out with a highlighted background</li>
                        <li>Block: Completely hides matching titles from view</li>
                    </ul>

                    <p>How to Create a Rule:</p>
                    <ul>
                        <li>Type each word or phrase and press "Enter" to create a separate "chip"</li>
                        <li>You can click on any chip to edit it</li>
                        <li>Click the × on any chip to remove it</li>
                    </ul>

                    <p>How Rule Logic Works:</p>
                    <ul>
                        <li>Must contain: ALL words/phrases must be found in a title (uses AND logic)</li>
                        <li>But not if it contains: If ANY of these words/phrases are found, the rule won't apply (uses OR logic)</li>
                        <li>Note: Non-latin characters are (hopefully) fully supported. This includes spaces, meaning you could things like block "CR WEB-DL AVC" if so desired</li>
                        <li>Note: Matching is case-insensitive ("SubsPlease" is the same as "subsplease")</li>
                    </ul>

                    <p>Example:</p>
                    <ul>
                        <li>If you wanted to highlight all 1080p SubsPlease releases except for unofficial batches, you could configure it as:</li>
                        <li style="margin-left: 20px; margin-top: 8px;">
                            Must contain:
                            <span class="colored-chip green-chip">SubsPlease</span>
                            <span class="colored-chip green-chip">1080p</span>
                        </li>
                        <li style="margin-left: 20px;">
                            But not if it contains:
                            <span class="colored-chip red-chip">Unofficial Batch</span>
                        </li>
                        <li>If you wanted to block all VARYG releases except for Netflix, Amazon, or Disney+ (i.e. block all of VARYG's Crunchyroll and Hidive releases):</li>
                        <li style="margin-left: 20px; margin-top: 8px;">
                            Must contain:
                            <span class="colored-chip green-chip">VARYG</span>
                        </li>
                        <li style="margin-left: 20px;">
                            But not if it contains:
                            <span class="colored-chip red-chip">NF</span>
                            <span class="colored-chip red-chip">AMZN</span>
                            <span class="colored-chip red-chip">DSNP</span>
                        </li>
                    </ul>

                    <p>Further settings:</p>
                    <ul>
                        <li>The "Enable" toggle in the top right turns the entire script on or off until turned back on</li>
                        <li>Click the "⚙" (gear icon) next to the close button to access the following additional settings:</li>
                        <li>Highlight Colors: Customize the highlight colors for both light and dark themes</li>
                        <li>Auto-Import Lists: Instantly highlight common fansubber groups or block mini release groups, and receive automatic updates. Undesired groups can be removed with the "Remove" button</li>
                        <li>Block Categories: Block undesired categories (e.g. "Anime - Anime Music Video") when browsing the homepage or a category group</li>
                        <li>Custom Import/Export: Save your rules to a file or load previously saved configurations</li>
                    </ul>
                </div>

            `;
        },

        /**
         * Create navbar button
         */
        createOpenButton() {
            // Add button to main navbar (left side)
            const navbar = document.querySelector(".navbar-nav");
            if (navbar) {
                const settingsItem = document.createElement("li");
                const settingsLink = document.createElement("a");
                settingsLink.innerHTML = ' <i class="fa fa-filter fa-fw" aria-hidden="true"></i>H&B';
                settingsLink.title = "Highlight & Block Settings";
                settingsLink.style.cursor = "pointer";
                settingsLink.id = "highlight-block-link";
                settingsLink.addEventListener("click", (e) => {
                    e.preventDefault();
                    this.toggleGUI();
                });
                settingsItem.appendChild(settingsLink);
                navbar.appendChild(settingsItem);

                this.openButton = settingsLink; // Store reference for event handling
            }
        },

        /**
         * Bind event listeners
         */
        bindEvents() {
            // Window resize handler
            window.addEventListener('resize', () => {
                this.updateContainerWidth();
                this.syncListHeights();
            });

            // Document click handler for closing GUI
            document.addEventListener('click', (event) => {
                this.handleDocumentClick(event);
            });

            // Initialize button event listeners after DOM is ready
            setTimeout(() => {
                this.bindButtonEvents();
            }, 100);
        },

        /**
         * Toggle GUI visibility
         */
        toggleGUI() {
            const isVisible = this.guiContainer.style.display === 'flex';
            this.guiContainer.style.display = isVisible ? 'none' : 'flex';

            if (!isVisible && !this.chipInputs) {
                this.initializeChipInputs();
            }

            if (!isVisible) {
                this.syncListHeights();
            }
        },

        /**
         * Handle document click events
         * @param {Event} event - Click event
         */
        handleDocumentClick(event) {
            // Don't close if clicking on chip remove buttons
            if (event.target.classList.contains('chip-remove')) {
                return;
            }

            // Don't close if clicking on cancel edit buttons
            if (event.target.id === 'highlight-cancel-container' ||
                event.target.id === 'block-cancel-container') {
                return;
            }

            // Close GUI if clicking outside
            if (!this.guiContainer.contains(event.target) &&
                this.openButton && !this.openButton.contains(event.target)) {
                this.guiContainer.style.display = 'none';
            }
        },

        /**
         * Bind button event listeners
         */
        bindButtonEvents() {
            const highlightBtn = document.getElementById('add-keyword-highlight-btn');
            const blockBtn = document.getElementById('add-keyword-block-btn');
            const closeBtn = document.getElementById('close-x-btn');

            if (highlightBtn) {
                highlightBtn.addEventListener('click', addKeywordToHighlight);
            }

            if (blockBtn) {
                blockBtn.addEventListener('click', addKeywordToBlock);
            }

            if (closeBtn) {
                closeBtn.addEventListener('click', () => {
                    this.guiContainer.style.display = 'none';
                });
            }

            const helpBtn = document.getElementById('help-btn');
            const helpClose = document.getElementById('help-close');
            const helpPopup = document.getElementById('help-popup');

            if (helpBtn) {
                helpBtn.addEventListener('click', () => {
                    if (helpPopup.style.display === 'block') {
                        helpPopup.style.display = 'none';
                    } else {
                        helpPopup.style.display = 'block';
                    }
                });
            }

            if (helpClose) {
                helpClose.addEventListener('click', () => {
                    helpPopup.style.display = 'none';
                });
            }

            // Close help popup when clicking outside
            if (helpPopup) {
                document.addEventListener('click', (event) => {
                    if (!helpPopup.contains(event.target) && !helpBtn.contains(event.target)) {
                        helpPopup.style.display = 'none';
                    }
                });
            }

            const toggleSwitch = document.getElementById('toggle-highlight-block-switch');

            if (toggleSwitch) {
                toggleSwitch.addEventListener('change', (event) => {
                    AppState.isEnabled = event.target.checked;
                    AppState.saveEnabledState();
                    updateDisplay();
                });
            }

            const importBtn = document.getElementById('import-btn');
            const exportBtn = document.getElementById('export-btn');

            if (importBtn) {
                importBtn.addEventListener('click', importConfiguration);
            }

            if (exportBtn) {
                exportBtn.addEventListener('click', exportConfiguration);
            }

            // Toggle event listeners
            const fansubbersToggle = document.getElementById('fansubbers-toggle');
            const minisToggle = document.getElementById('minis-toggle');

            if (fansubbersToggle) {
                fansubbersToggle.addEventListener('change', (event) => {
                    AppState.fansubbersToggle = event.target.checked;
                    AppState.saveToggleStates();
                    if (AppState.fansubbersToggle) {
                        autoImportFansubbers();
                        updateRuleLists();
                        updateDisplay();
                    }
                });
            }

            if (minisToggle) {
                minisToggle.addEventListener('change', (event) => {
                    AppState.minisToggle = event.target.checked;
                    AppState.saveToggleStates();
                    if (AppState.minisToggle) {
                        autoImportMinis();
                        updateRuleLists();
                        updateDisplay();
                    }
                });
            }


            const lightColorPicker = document.getElementById('light-color-picker');
            const darkColorPicker = document.getElementById('dark-color-picker');
            const resetLightBtn = document.getElementById('reset-light-color');
            const resetDarkBtn = document.getElementById('reset-dark-color');

            if (lightColorPicker) {
                lightColorPicker.addEventListener('click', () => {
                    const guiContainer = document.getElementById('guiContainer');
                    const settingsOverlay = document.getElementById('settings-overlay');
                    if (guiContainer) guiContainer.style.display = 'none';
                    if (settingsOverlay) settingsOverlay.style.display = 'none';
                });
                lightColorPicker.addEventListener('input', handleLightColorChange);
                lightColorPicker.addEventListener('change', handleLightColorChange);
            }

            if (darkColorPicker) {
                darkColorPicker.addEventListener('click', () => {
                    const guiContainer = document.getElementById('guiContainer');
                    const settingsOverlay = document.getElementById('settings-overlay');
                    if (guiContainer) guiContainer.style.display = 'none';
                    if (settingsOverlay) settingsOverlay.style.display = 'none';
                });
                darkColorPicker.addEventListener('input', handleDarkColorChange);
                darkColorPicker.addEventListener('change', handleDarkColorChange);
            }

            if (resetLightBtn) {
                resetLightBtn.addEventListener('click', resetLightColor);
            }

            if (resetDarkBtn) {
                resetDarkBtn.addEventListener('click', resetDarkColor);
            }

            // Settings button
            const settingsBtn = document.getElementById('settings-btn');
            const settingsCloseBtn = document.getElementById('settings-close-btn');
            const settingsOverlay = document.getElementById('settings-overlay');

            if (settingsBtn) {
                settingsBtn.addEventListener('click', () => {
                    if (settingsOverlay.style.display === 'block') {
                        settingsOverlay.style.display = 'none';
                    } else {
                        settingsOverlay.style.display = 'block';
                    }
                });
            }

            if (settingsCloseBtn) {
                settingsCloseBtn.addEventListener('click', () => {
                    settingsOverlay.style.display = 'none';
                    document.body.classList.remove('color-picker-active');
                });
            }

            // Close settings when clicking outside
            document.addEventListener('click', (event) => {
                if (settingsOverlay &&
                    settingsOverlay.style.display === 'block' &&
                    !settingsOverlay.contains(event.target) &&
                    !settingsBtn.contains(event.target)) {
                    settingsOverlay.style.display = 'none';
                    document.body.classList.remove('color-picker-active');
                }
            });

            // Category checkbox event listeners
            const categoryCheckboxes = document.querySelectorAll('.category-checkbox');
            categoryCheckboxes.forEach(checkbox => {
                checkbox.addEventListener('change', (event) => {
                    const category = event.target.dataset.category;
                    if (event.target.checked) {
                        if (!AppState.blockedCategories.includes(category)) {
                            AppState.blockedCategories.push(category);
                        }
                    } else {
                        const index = AppState.blockedCategories.indexOf(category);
                        if (index > -1) {
                            AppState.blockedCategories.splice(index, 1);
                        }
                    }
                    AppState.saveBlockedCategories();
                    updateDisplay();
                });
            });

            // Select all categories button
            const selectAllBtn = document.getElementById('select-all-categories');
            if (selectAllBtn) {
                selectAllBtn.addEventListener('click', () => {
                    AppState.blockedCategories = Object.keys(CATEGORIES);
                    AppState.saveBlockedCategories();

                    // Update all checkboxes
                    categoryCheckboxes.forEach(checkbox => {
                        checkbox.checked = true;
                    });

                    updateDisplay();
                });
            }

            // Select none categories button
            const selectNoneBtn = document.getElementById('select-none-categories');
            if (selectNoneBtn) {
                selectNoneBtn.addEventListener('click', () => {
                    AppState.blockedCategories = [];
                    AppState.saveBlockedCategories();

                    // Update all checkboxes
                    categoryCheckboxes.forEach(checkbox => {
                        checkbox.checked = false;
                    });

                    updateDisplay();
                });
            }

            updateRuleLists();
            updateDisplay();
        },

        /**
         * Initialize chip input components
         */
        initializeChipInputs() {
            this.chipInputs = {
                highlightKeyword: new ChipInput('highlight-keyword-container', 'Add required word or phrase'),
                highlightException: new ChipInput('highlight-exception-container', 'Add word or phrase to exclude'),
                blockKeyword: new ChipInput('block-keyword-container', 'Add required word or phrase'),
                blockException: new ChipInput('block-exception-container', 'Add word or phrase to exclude')
            };
        },

        /**
         * Synchronize list heights for better visual balance
         */
        syncListHeights() {
            const highlightList = document.getElementById('keywords-highlight-list');
            const blockList = document.getElementById('keywords-block-list');

            if (!highlightList || !blockList) return;

            // Reset heights
            highlightList.style.height = 'auto';
            blockList.style.height = 'auto';

            // Get natural heights
            const highlightHeight = highlightList.offsetHeight;
            const blockHeight = blockList.offsetHeight;

            // Set both to the maximum height
            const maxHeight = Math.max(highlightHeight, blockHeight);
            highlightList.style.height = `${maxHeight}px`;
            blockList.style.height = `${maxHeight}px`;
        }
    };

    // ============================================================================
    // RULE LIST MANAGEMENT
    // ============================================================================

    /**
     * Update both rule lists
     */
    const updateRuleLists = () => {
        updateHighlightRuleList();
        updateBlockRuleList();
        UI.syncListHeights();
    };

    /**
     * Update highlight rules list display
     */
    const updateHighlightRuleList = () => {
        const list = document.getElementById('keywords-highlight-list');
        if (!list) return;

        list.innerHTML = '';

        AppState.keywordsHighlight.forEach(([keywordParts, exceptionParts], index) => {
            const listItem = createRuleListItem('highlight', index, keywordParts, exceptionParts);
            list.appendChild(listItem);
        });
    };

    /**
     * Update block rules list display
     */
    const updateBlockRuleList = () => {
        const list = document.getElementById('keywords-block-list');
        if (!list) return;

        list.innerHTML = '';

        AppState.keywordsBlock.forEach(([keywordParts, exceptionParts], index) => {
            const listItem = createRuleListItem('block', index, keywordParts, exceptionParts);
            list.appendChild(listItem);
        });
    };

    /**
     * Create a rule list item element
     * @param {string} type - 'highlight' or 'block'
     * @param {number} index - Rule index
     * @param {Array} keywordParts - Keyword parts array
     * @param {Array} exceptionParts - Exception parts array
     * @returns {Element} List item element
     */
    const createRuleListItem = (type, index, keywordParts, exceptionParts) => {
        const isEditing = AppState.editMode.isEditing &&
                         AppState.editMode.type === type &&
                         AppState.editMode.index === index;

        const keywordChips = keywordParts
            .filter(k => k && k.trim())
            .map(k => `<span class="colored-chip green-chip">${sanitizeInput(k)}</span>`)
            .join('');

        const exceptionChips = exceptionParts
            .filter(e => e && e.trim())
            .map(e => `<span class="colored-chip red-chip">${sanitizeInput(e)}</span>`)
            .join('');

        const li = document.createElement('li');
        li.className = `rule-item ${isEditing ? 'editing' : ''}`;

        li.innerHTML = `
            <div class="rule-buttons">
                <div class="button-group">
                    <button class="${type}-edit-btn action-button rule-button"
                            data-index="${index}"
                            ${isEditing ? 'disabled' : ''}>
                        ${isEditing ? 'Editing...' : 'Edit'}
                    </button>
                    <button class="${type}-remove-btn action-button rule-button"
                            data-index="${index}"
                            ${isEditing ? 'disabled' : ''}>
                        Remove
                    </button>
                </div>
            </div>
            <div class="rule-content">
                ${keywordChips}
                ${exceptionChips}
            </div>
        `;

        // Bind event listeners
        bindRuleItemEvents(li, type);

        return li;
    };

    /**
     * Bind event listeners for rule item buttons
     * @param {Element} listItem - List item element
     * @param {string} type - 'highlight' or 'block'
     */
    const bindRuleItemEvents = (listItem, type) => {
        const editBtn = listItem.querySelector(`.${type}-edit-btn`);
        const removeBtn = listItem.querySelector(`.${type}-remove-btn`);

        if (editBtn) {
            editBtn.addEventListener('click', (event) => {
                event.stopPropagation();
                if (!editBtn.disabled) {
                    const index = parseInt(editBtn.dataset.index);
                    enterEditMode(type, index);
                }
            });
        }

        if (removeBtn) {
            removeBtn.addEventListener('click', (event) => {
                event.stopPropagation();
                if (!removeBtn.disabled) {
                    const index = parseInt(removeBtn.dataset.index);
                    removeRule(type, index);
                }
            });
        }
    };

    // ============================================================================
    // INITIALIZATION
    // ============================================================================

    /**
     * Initialize the application
     */
    const init = () => {
        try {
            UI.init();

            // Auto-import on startup if toggles are enabled
            autoImportFansubbers();
            autoImportMinis();

            updateDisplay();
            initThemeMonitoring();

            console.log('Nyaa Highlight & Block script initialized successfully');
        } catch (error) {
            console.error('Failed to initialize Nyaa Highlight & Block script:', error);
        }
    };


    // Start the application when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址