Nexus No Wait ++

Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features.

// ==UserScript==
// @name        Nexus No Wait ++
// @description Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features.
// @namespace   NexusNoWaitPlusPlus
// @author      Torkelicious
// @version     1.1.6
// @include     https://*.nexusmods.com/*
// @run-at      document-idle
// @iconURL     https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
// @icon        https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @license MIT
// ==/UserScript==

/* global GM_getValue, GM_setValue, GM_deleteValue, GM_xmlhttpRequest, GM_info GM */

(function () {

    const DEFAULT_CONFIG = {
        autoCloseTab: true,        // Close tab after download starts
        skipRequirements: true,    // Skip requirements popup/tab
        showAlerts: true,          // Show errors as browser alerts
        refreshOnError: false,     // Refresh page on error
        requestTimeout: 30000,     // Request timeout (30 sec)
        closeTabTime: 1000,        // Wait before closing tab (1 sec)
        debug: false,              // Show debug messages as alerts
        playErrorSound: true,      // Play a sound on error
    };

    // === Configuration ===
    /**
     * @typedef {Object} Config
     * @property {boolean} autoCloseTab - Close tab after download starts
     * @property {boolean} skipRequirements - Skip requirements popup/tab
     * @property {boolean} showAlerts - Show errors as browser alerts
     * @property {boolean} refreshOnError - Refresh page on error
     * @property {number} requestTimeout - Request timeout in milliseconds
     * @property {number} closeTabTime - Wait before closing tab in milliseconds
     * @property {boolean} debug - Show debug messages as alerts
     * @property {boolean} playErrorSound - Play a sound on error
     */

    /**
     * @typedef {Object} SettingDefinition
     * @property {string} name - Display name of the setting
     * @property {string} description - Tooltip description
     */

    /**
     * @typedef {Object} UIStyles
     * @property {string} button - Button styles
     * @property {string} modal - Modal window styles
     * @property {string} settings - Settings header styles
     * @property {string} section - Section styles
     * @property {string} sectionHeader - Section header styles
     * @property {string} input - Input field styles
     * @property {Object} btn - Button variant styles
     */

    // === Settings Management ===
    /**
     * Validates settings object against default configuration
     * @param {Object} settings - Settings to validate
     * @returns {Config} Validated settings object
     */
    function validateSettings(settings) {
        if (!settings || typeof settings !== 'object') return {...DEFAULT_CONFIG};

        const validated = {...settings}; // Keep all existing settings

        // Settings validation
        for (const [key, defaultValue] of Object.entries(DEFAULT_CONFIG)) {
            if (typeof validated[key] !== typeof defaultValue) {
                validated[key] = defaultValue;
            }
        }

        return validated;
    }

    /**
     * Loads settings from storage with validation
     * @returns {Config} Loaded and validated settings
     */
    function loadSettings() {
        try {
            const saved = GM_getValue('nexusNoWaitConfig', null);
            const parsed = saved ? JSON.parse(saved) : DEFAULT_CONFIG;
            return validateSettings(parsed);
        } catch (error) {
            console.warn('GM storage load failed:', error);
            return {...DEFAULT_CONFIG};
        }
    }

    /**
     * Saves settings to storage
     * @param {Config} settings - Settings to save
     * @returns {void}
     */
    function saveSettings(settings) {
        try {
            GM_setValue('nexusNoWaitConfig', JSON.stringify(settings));
            logMessage('Settings saved to GM storage', false, true);
        } catch (e) {
            console.error('Failed to save settings:', e);
        }
    }
    const config = Object.assign({}, DEFAULT_CONFIG, loadSettings());

    // Create global sound instance

    const errorSound = new Audio('https://github.com/torkelicious/nexus-no-wait-pp/raw/refs/heads/main/errorsound.mp3');
    errorSound.load(); // Preload sound


     // Plays error sound if enabled

    function playErrorSound() {
        if (!config.playErrorSound) return;
        errorSound.play().catch(e => {
            console.warn("Error playing sound:", e);
        });
    }

    // === Error Handling ===

    /**
     * Centralized logging function
     * @param {string} message - Message to display/log
     * @param {boolean} [showAlert=false] - If true, shows browser alert
     * @param {boolean} [isDebug=false] - If true, handles debug logs
     * @returns {void}
     */
    function logMessage(message, showAlert = false, isDebug = false) {
        if (isDebug) {
            console.log("[Nexus No Wait ++]: " + message);
            if (config.debug) {
                alert("[Nexus No Wait ++] (Debug):\n" + message);
            }
            return;
        }

        playErrorSound();  // Play sound before alert
        console.error("[Nexus No Wait ++]: " + message);
        if (showAlert && config.showAlerts) {
            alert("[Nexus No Wait ++] \n" + message);
        }

        if (config.refreshOnError) {
            location.reload();
        }
    }

    // === URL and Navigation Handling ===
    /**
     * Auto-redirects from requirements to files
     */
    if (window.location.href.includes('tab=requirements') && config.skipRequirements)
    {
        const newUrl = window.location.href.replace('tab=requirements', 'tab=files');
        window.location.replace(newUrl);
        return;
    }

    // === AJAX Setup and Configuration ===
    let ajaxRequestRaw;
    if (typeof(GM_xmlhttpRequest) !== "undefined")
    {
        ajaxRequestRaw = GM_xmlhttpRequest;
    } else if (typeof(GM) !== "undefined" && typeof(GM.xmlHttpRequest) !== "undefined") {
        ajaxRequestRaw = GM.xmlHttpRequest;
    }

    // Wrapper for AJAX requests
    function ajaxRequest(obj) {
        if (!ajaxRequestRaw) {
            logMessage("AJAX functionality not available (Your browser or userscript manager may not support these requests!)", true);
            return;
        }
        ajaxRequestRaw({
            method: obj.type,
            url: obj.url,
            data: obj.data,
            headers: obj.headers,
            onload: function(response) {
                if (response.status >= 200 && response.status < 300) {
                    obj.success(response.responseText);
                } else {
                    obj.error(response);
                }
            },
            onerror: function(response) {
                obj.error(response);
            },
            ontimeout: function(response) {
                obj.error(response);
            }
        });
    }

    // === Button Management ===

    /**
     * Updates button appearance and shows errors
     * @param {HTMLElement} button - The button element
     * @param {Error|Object} error - Error details
     */
    function btnError(button, error) {
        button.style.color = "red";
        let errorMessage = "Download failed: " + (error.message);
        button.innerText = "ERROR: " + errorMessage;
        logMessage(errorMessage, true);
    }

    function btnSuccess(button) {
        button.style.color = "green";
        button.innerText = "Downloading!";
        logMessage("Download started.", false, true);
    }

    function btnWait(button) {
        button.style.color = "yellow";
        button.innerText = "Wait...";
        logMessage("Loading...", false, true);
    }


    // Closes tab after download starts
    function closeOnDL()
    {
        if (config.autoCloseTab && !isArchiveDownload) // Modified to check for archive downloads
        {
        setTimeout(() => window.close(), config.closeTabTime);
        }
    }

    // === Download Handling ===
    /**
     * Main click event handler for download buttons
     * Handles both manual and mod manager downloads
     * @param {Event} event - Click event object
     */
    function clickListener(event) {
        // Skip if this is an archive download
        if (isArchiveDownload) {
            isArchiveDownload = false; // Reset the flag
            return;
        }

        const href = this.href || window.location.href;
        const params = new URL(href).searchParams;

        if (params.get("file_id")) {
            let button = event;
            if (this.href) {
                button = this;
                event.preventDefault();
            }
            btnWait(button);

            const section = document.getElementById("section");
            const gameId = section ? section.dataset.gameId : this.current_game_id;

            let fileId = params.get("file_id");
            if (!fileId) {
                fileId = params.get("id");
            }

            const ajaxOptions = {
                type: "POST",
                url: "/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl",
                data: "fid=" + fileId + "&game_id=" + gameId,
                headers: {
                    Origin: "https://www.nexusmods.com",
                    Referer: href,
                    "Sec-Fetch-Site": "same-origin",
                    "X-Requested-With": "XMLHttpRequest",
                    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
                },
                success(data) {
                    if (data) {
                        try {
                            data = JSON.parse(data);
                            if (data.url) {
                                btnSuccess(button);
                                document.location.href = data.url;
                                closeOnDL();
                            }
                        } catch (e) {
                            btnError(button, e);
                        }
                    }
                },
                error(xhr) {
                    btnError(button, xhr);
                }
            };

            if (!params.get("nmm")) {
                ajaxRequest(ajaxOptions);
            } else {
                ajaxRequest({
                    type: "GET",
                    url: href,
                    headers: {
                        Origin: "https://www.nexusmods.com",
                        Referer: document.location.href,
                        "Sec-Fetch-Site": "same-origin",
                        "X-Requested-With": "XMLHttpRequest"
                    },
                    success(data) {
                        if (data) {
                            const xml = new DOMParser().parseFromString(data, "text/html");
                            const slow = xml.getElementById("slowDownloadButton");
                            if (slow && slow.getAttribute("data-download-url")) {
                                const downloadUrl = slow.getAttribute("data-download-url");
                                btnSuccess(button);
                                document.location.href = downloadUrl;
                                closeOnDL();
                            } else {
                                btnError(button);
                            }
                        }
                    },
                    error(xhr) {
                        btnError(button, xhr);
                    }
                });
            }

            const popup = this.parentNode;
            if (popup && popup.classList.contains("popup")) {
                popup.getElementsByTagName("button")[0].click();
                const popupButton = document.getElementById("popup" + fileId);
                if (popupButton) {
                    btnSuccess(popupButton);
                    closeOnDL();
                }
            }
        } else if (/ModRequirementsPopUp/.test(href)) {
            const fileId = params.get("id");

            if (fileId) {
                this.setAttribute("id", "popup" + fileId);
            }
        }
    }

    // === Event Listeners  ===
    /**
     * Attaches click event listener with proper context
     * @param {HTMLElement} el - the element to attach listener to
     */
    function addClickListener(el) {
        el.addEventListener("click", clickListener, true);
    }

    // Attaches click event listeners to multiple elements
    function addClickListeners(els) {
        for (let i = 0; i < els.length; i++) {
            addClickListener(els[i]);
        }
    }

    // === Automatic Downloading ===
    function autoStartFileLink() {
        if (/file_id=/.test(window.location.href)) {
            clickListener(document.getElementById("slowDownloadButton"));
        }
    }

    // Automatically skips file requirements popup and downloads
    function autoClickRequiredFileDownload() {
        const observer = new MutationObserver(() => {
            const downloadButton = document.querySelector(".popup-mod-requirements a.btn");
            if (downloadButton) {
                downloadButton.click();
                const popup = document.querySelector(".popup-mod-requirements");
                if (!popup) {
                    logMessage("Popup closed", false, true);
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'class']
        });
    }

    // === Archived Files Handling ===

    //SVG paths
    const ICON_PATHS = {
        nmm: 'https://www.nexusmods.com/assets/images/icons/icons.svg#icon-nmm',
        manual: 'https://www.nexusmods.com/assets/images/icons/icons.svg#icon-manual'
    };

    /**
     * Tracks if current download is from archives
     * @type {boolean}
     */
    let isArchiveDownload = false;

    function archivedFile() {
        try {
            // Only run in the archived category
            if (!window.location.href.includes('category=archived')) {
                return;
            }

            //  DOM queries and paths
            const path = `${location.protocol}//${location.host}${location.pathname}`;
            const downloadTemplate = (fileId) => `
                <li>
                    <a class="btn inline-flex download-btn"
                       href="${path}?tab=files&file_id=${fileId}&nmm=1"
                       data-fileid="${fileId}"
                       data-manager="true"
                       tabindex="0">
                        <svg title="" class="icon icon-nmm">
                            <use xlink:href="${ICON_PATHS.nmm}"></use>
                        </svg>
                        <span class="flex-label">Mod manager download</span>
                    </a>
                </li>
                <li>
                    <a class="btn inline-flex download-btn"
                       href="${path}?tab=files&file_id=${fileId}"
                       data-fileid="${fileId}"
                       data-manager="false"
                       tabindex="0">
                        <svg title="" class="icon icon-manual">
                            <use xlink:href="${ICON_PATHS.manual}"></use>
                        </svg>
                        <span class="flex-label">Manual download</span>
                    </a>
                </li>`;

            const downloadSections = Array.from(document.querySelectorAll('.accordion-downloads'));
            const fileHeaders = Array.from(document.querySelectorAll('.file-expander-header'));

            downloadSections.forEach((section, index) => {
                const fileId = fileHeaders[index]?.getAttribute('data-id');
                if (fileId) {
                    section.innerHTML = downloadTemplate(fileId);
                    const buttons = section.querySelectorAll('.download-btn');
                    buttons.forEach(btn => {
                        btn.addEventListener('click', function(e) {
                            e.preventDefault();
                            isArchiveDownload = true;
                            // Use existing download logic
                            clickListener.call(this, e);
                            // Reset flag after small delay
                            setTimeout(() => isArchiveDownload = false, 100);
                        });
                    });
                }
            });

        } catch (error) {
            logMessage('Error with archived file: ' + error.message, true);
            console.error('Archived file error:', error);
        }
    }
// --------------------------------------------- === UI === --------------------------------------------- //

    const SETTING_UI = {
        autoCloseTab: {
            name: 'Auto-Close tab on download',
            description: 'Automatically close tab after download starts'
        },
        skipRequirements: {
            name: 'Skip Requirements Popup/Tab',
            description: 'Skip requirements page and go straight to download'
        },
        showAlerts: {
            name: 'Show Error Alert messages',
            description: 'Show error messages as browser alerts'
        },
        refreshOnError: {
            name: 'Refresh page on error',
            description: 'Refresh the page when errors occur (may lead to infinite refresh loop!)'
        },
        requestTimeout: {
            name: 'Request Timeout',
            description: 'Time to wait for server response before timeout'
        },
        closeTabTime: {
            name: 'Auto-Close tab Delay',
            description: 'Delay before closing tab after download starts (Setting this too low may prevent download from starting!)'
        },
        debug: {
            name: "⚠️ Debug Alerts",
            description: "Show all console logs as alerts, don't enable unless you know what you are doing!"
        },
        playErrorSound: {
            name: 'Play Error Sound',
            description: 'Play a sound when errors occur'
        },
    };

    // Extract UI styles
    const STYLES = {
        button: `
            position:fixed;
            bottom:20px;
            right:20px;
            background:#2f2f2f;
            color:white;
            padding:10px 15px;
            border-radius:4px;
            cursor:pointer;
            box-shadow:0 2px 8px rgba(0,0,0,0.2);
            z-index:9999;
            font-family:-apple-system, system-ui, sans-serif;
            font-size:14px;
            transition:all 0.2s ease;
            border:none;`,
        modal: `
            position:fixed;
            top:50%;
            left:50%;
            transform:translate(-50%, -50%);
            background:#2f2f2f;
            color:#dadada;
            padding:25px;
            border-radius:4px;
            box-shadow:0 2px 20px rgba(0,0,0,0.3);
            z-index:10000;
            min-width:300px;
            max-width:90%;
            max-height:90vh;
            overflow-y:auto;
            font-family:-apple-system, system-ui, sans-serif;`,
        settings: `
            margin:0 0 20px 0;
            color:#da8e35;
            font-size:18px;
            font-weight:600;`,
        section: `
            background:#363636;
            padding:15px;
            border-radius:4px;
            margin-bottom:15px;`,
        sectionHeader: `
            color:#da8e35;
            margin:0 0 10px 0;
            font-size:16px;
            font-weight:500;`,
        input: `
            background:#2f2f2f;
            border:1px solid #444;
            color:#dadada;
            border-radius:3px;
            padding:5px;`,
        btn: {
            primary: `
                padding:8px 15px;
                border:none;
                background:#da8e35;
                color:white;
                border-radius:3px;
                cursor:pointer;
                transition:all 0.2s ease;`,
            secondary: `
                padding:8px 15px;
                border:1px solid #da8e35;
                background:transparent;
                color:#da8e35;
                border-radius:3px;
                cursor:pointer;
                transition:all 0.2s ease;`,
            advanced: `
                padding: 4px 8px;
                border: none;
                background: transparent;
                color: #666;
                font-size: 12px;
                cursor: pointer;
                transition: all 0.2s ease;
                opacity: 0.6;
                text-decoration: underline;
                &:hover {
                    opacity: 1;
                    color: #da8e35;
                }`
        }
    };

    function createSettingsUI() {
        const btn = document.createElement('div');
        btn.innerHTML = 'NexusNoWait++ ⚙️';
        btn.style.cssText = STYLES.button;

        btn.onmouseover = () => btn.style.transform = 'translateY(-2px)';
        btn.onmouseout = () => btn.style.transform = 'translateY(0)';
        btn.onclick = () => {
            if (activeModal) {
                activeModal.remove();
                activeModal = null;
                if (settingsChanged) {  // Only reload if settings were changed
                    location.reload();
                }
            } else {
                showSettingsModal();
            }
        };
        document.body.appendChild(btn);
    }

    //  settings UI
    /**
     * Creates settings UI HTML
     * @returns {string} Generated HTML
     */
    function generateSettingsHTML() {
        const normalBooleanSettings = Object.entries(SETTING_UI)
            .filter(([key]) => typeof config[key] === 'boolean' && !['debug'].includes(key))
            .map(([key, {name, description}]) => `
                <div style="margin-bottom:10px;">
                    <label title="${description}" style="display:flex;align-items:center;gap:8px;">
                        <input type="checkbox"
                               ${config[key] ? 'checked' : ''}
                               data-setting="${key}">
                        <span>${name}</span>
                    </label>
                </div>`).join('');

        const numberSettings = Object.entries(SETTING_UI)
            .filter(([key]) => typeof config[key] === 'number')
            .map(([key, {name, description}]) => `
                <div style="margin-bottom:10px;">
                    <label title="${description}" style="display:flex;align-items:center;justify-content:space-between;">
                        <span>${name}:</span>
                        <input type="number"
                               value="${config[key]}"
                               min="0"
                               step="100"
                               data-setting="${key}"
                               style="${STYLES.input};width:120px;">
                    </label>
                </div>`).join('');

        // debug section
        const advancedSection = `
            <div id="advancedSection" style="display:none;">
                <div style="${STYLES.section}">
                    <h4 style="${STYLES.sectionHeader}">Advanced Settings</h4>
                    <div style="margin-bottom:10px;">
                        <label title="${SETTING_UI.debug.description}" style="display:flex;align-items:center;gap:8px;">
                            <input type="checkbox"
                                   ${config.debug ? 'checked' : ''}
                                   data-setting="debug">
                            <span>${SETTING_UI.debug.name}</span>
                        </label>
                    </div>
                </div>
            </div>`;

        return `
            <h3 style="${STYLES.settings}">NexusNoWait++ Settings</h3>
            <div style="${STYLES.section}">
                <h4 style="${STYLES.sectionHeader}">Features</h4>
                ${normalBooleanSettings}
            </div>
            <div style="${STYLES.section}">
                <h4 style="${STYLES.sectionHeader}">Timing</h4>
                ${numberSettings}
            </div>
            ${advancedSection}
            <div style="margin-top:20px;display:flex;justify-content:center;gap:10px;">
                <button id="resetSettings" style="${STYLES.btn.secondary}">Reset</button>
                <button id="closeSettings" style="${STYLES.btn.primary}">Save & Close</button>
            </div>
            <div style="text-align: center; margin-top: 15px;">
                <button id="toggleAdvanced" style="${STYLES.btn.advanced}">⚙️ Advanced</button>
            </div>
            <div style="text-align: center; margin-top: 15px; color: #666; font-size: 12px;">
                Version ${GM_info.script.version}
                \n by Torkelicious
            </div>`;
    }

    let activeModal = null;
    let settingsChanged = false;  // Track settings changes

    /**
     * Shows settings and handles interactions
     * @returns {void}
     */
    function showSettingsModal() {
        if (activeModal) {
            activeModal.remove();
        }

        settingsChanged = false;  // Reset change tracker
        const modal = document.createElement('div');
        modal.style.cssText = STYLES.modal;

        modal.innerHTML = generateSettingsHTML();

        //  update function
        function updateSetting(element) {
            const setting = element.getAttribute('data-setting');
            const value = element.type === 'checkbox' ?
                element.checked :
                parseInt(element.value, 10);

            if (typeof value === 'number' && isNaN(value)) {
                element.value = config[setting];
                return;
            }

            if (config[setting] !== value) {
                settingsChanged = true;
                window.nexusConfig.setFeature(setting, value);
            }
        }

        modal.addEventListener('change', (e) => {
            if (e.target.hasAttribute('data-setting')) {
                updateSetting(e.target);
            }
        });

        modal.addEventListener('input', (e) => {
            if (e.target.type === 'number' && e.target.hasAttribute('data-setting')) {
                updateSetting(e.target);
            }
        });

        modal.querySelector('#closeSettings').onclick = () => {
            modal.remove();
            activeModal = null;
            // Only reload if settings were changed
            if (settingsChanged) {
                location.reload();
            }
        };

        modal.querySelector('#resetSettings').onclick = () => {
            settingsChanged = true;  // Reset counts as a change
            window.nexusConfig.reset();
            saveSettings(config);
            modal.remove();
            activeModal = null;
            location.reload();
        };

        // toggle handler for advanced section
        modal.querySelector('#toggleAdvanced').onclick = (e) => {
            const section = modal.querySelector('#advancedSection');
            const isHidden = section.style.display === 'none';
            section.style.display = isHidden ? 'block' : 'none';
            e.target.textContent = `Advanced ${isHidden ? '▲' : '▼'}`;
        };

        document.body.appendChild(modal);
        activeModal = modal;
    }

    // Override console when debug is enabled
    function setupDebugMode() {
        if (config.debug) {
            const originalConsole = {
                log: console.log,
                warn: console.warn,
                error: console.error
            };

            console.log = function() {
                originalConsole.log.apply(console, arguments);
                alert("[Debug Log]\n" + Array.from(arguments).join(' '));
            };

            console.warn = function() {
                originalConsole.warn.apply(console, arguments);
                alert("[Debug Warn]\n" + Array.from(arguments).join(' '));
            };

            console.error = function() {
                originalConsole.error.apply(console, arguments);
                alert("[Debug Error]\n" + Array.from(arguments).join(' '));
            };
        }
    }

    // === Configuration ===
    window.nexusConfig = {
        /**
         * Sets a feature setting
         * @param {string} name - Setting name
         * @param {any} value - Setting value
         */
        setFeature: (name, value) => {
            const oldValue = config[name];
            config[name] = value;
            saveSettings(config);

            // Only apply non-debug settings fast
            if (name !== 'debug') {
                applySettings();
            }

            // Mark settings as changed if value actually changed
            if (oldValue !== value) {
                settingsChanged = true;
            }
        },


         // Resets all settings to defaults

        reset: () => {
            GM_deleteValue('nexusNoWaitConfig');
            Object.assign(config, DEFAULT_CONFIG);
            saveSettings(config);
            applySettings();  // Apply changes
        },


        // Gets current configuration

        getConfig: () => config
    };

    function applySettings() {
        // Update AJAX timeout
        if (ajaxRequestRaw) {
            ajaxRequestRaw.timeout = config.requestTimeout;
        }
        setupDebugMode();
    }
// ------------------------------------------------------------------------------------------------ //

    // ===  Initialization ===
    /**
     * Checks if current URL is a mod page
     * @returns {boolean} True if URL matches mod pattern
     */
    function isModPage() {
        return /nexusmods\.com\/.*\/mods\//.test(window.location.href);
    }


    //Initializes UI components
    function initializeUI() {
        applySettings();
        createSettingsUI();
    }

    
    //Initializes main functions if on modpage
    function initMainFunctions() {
        if (!isModPage()) return;
        
        archivedFile();
        addClickListeners(document.querySelectorAll("a.btn"));
        autoStartFileLink();
        if (config.skipRequirements) {
            autoClickRequiredFileDownload();
        }
    }

    // Combined observer
    const mainObserver = new MutationObserver((mutations) => {
        if (!isModPage()) return;
        
        try {
            mutations.forEach(mutation => {
                if (!mutation.addedNodes) return;

                mutation.addedNodes.forEach(node => {
                    if (node.tagName === "A" && node.classList?.contains("btn")) {
                        addClickListener(node);
                    }

                    if (node.querySelectorAll) {
                        addClickListeners(node.querySelectorAll("a.btn"));
                    }
                });
            });
        } catch (error) {
            console.error("Error in mutation observer:", error);
        }
    });

    // Initialize everything
    initializeUI();
    initMainFunctions();

    // Start observing
    mainObserver.observe(document, {
        childList: true,
        subtree: true
    });

    // Cleanup on page unload
    window.addEventListener('unload', () => {
        mainObserver.disconnect();
    });
})();