Better Rule34

A script to improve the use of rule34, now with API key support!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Better Rule34
// @name:fr      Meilleure règle 34
// @namespace    http://tampermonkey.net/
// @version      1.1.0
// @description  A script to improve the use of rule34, now with API key support!
// @description:fr Un script pour améliorer l'utilisation de rule34, maintenant avec le support de la clé API!
// @author       You
// @match        https://rule34.xxx/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM.xmlHttpRequest
// @license      MIT
// @require      https://unpkg.com/[email protected]
// ==/UserScript==

(async function() {
    'use strict';

    // --- NEW: API Key Management ---
    const API_KEY_NAME = 'br34_api_key';
    const USER_ID_NAME = 'br34_user_id';

    /**
     * Checks if the API key and User ID are stored. If not, it displays a prompt.
     * This function will "halt" the script by returning a never-resolving promise if keys are missing.
     */
    async function checkApiKey() {
        const apiKey = await GM_getValue(API_KEY_NAME);
        const userId = await GM_getValue(USER_ID_NAME);

        // If on the options page, don't show the prompt; let the page handler do its job.
        if (window.location.href.includes('page=account&s=options')) {
            return true;
        }

        if (!apiKey || !userId) {
            console.log("Better Rule34: API key or User ID not found. Displaying prompt.");
            showApiKeyPrompt();
            // Return a promise that never resolves to halt further script execution until the user provides keys.
            return new Promise(() => {});
        }
        console.log("Better Rule34: API key and User ID found.");
        return true;
    }

    /**
     * Displays a modal dialog prompting the user to generate or enter API credentials.
     */
    function showApiKeyPrompt() {
        GM_addStyle(`
            #br34-modal-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0,0,0,0.7); display: flex;
                justify-content: center; align-items: center; z-index: 9999; font-family: sans-serif;
            }
            #br34-modal-content {
                background-color: #1e1e1e; color: #eee; padding: 20px 30px;
                border-radius: 8px; text-align: center; max-width: 400px;
                border: 1px solid #555;
            }
            #br34-modal-content p { margin: 0 0 20px 0; line-height: 1.5; }
            #br34-modal-buttons button, #br34-manual-input button {
                background-color: #333; color: #fff; border: 1px solid #555;
                padding: 10px 15px; border-radius: 5px; cursor: pointer; margin: 0 10px;
            }
            #br34-modal-buttons button:hover, #br34-manual-input button:hover { background-color: #555; }
            #br34-manual-input { margin-top: 20px; }
            #br34-manual-input input {
                display: block; width: calc(100% - 20px); margin: 10px auto; padding: 8px;
                background-color: #333; border: 1px solid #555; color: #fff; border-radius: 4px;
            }
        `);

        const overlay = document.createElement('div');
        overlay.id = 'br34-modal-overlay';

        const modal = document.createElement('div');
        modal.id = 'br34-modal-content';
        modal.innerHTML = `
            <p>Due to a recent update, this script needs an API key! Either enter one manually, or generate a new one (recommended)!</p>
            <div id="br34-modal-buttons">
                <button id="br34-manual-btn">Enter Manually</button>
                <button id="br34-generate-btn">Generate New One</button>
            </div>
            <div id="br34-manual-input" style="display: none;">
                <p style="font-size: 0.9em;">Go to the options page, copy the full text from the 'API Access Credentials' box, and paste it here.</p>
                <input type="text" id="br34-credential-input" placeholder="&api_key=...&user_id=...">
                <button id="br34-save-manual-btn">Save</button>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        document.getElementById('br34-generate-btn').addEventListener('click', () => {
            window.location.href = 'https://rule34.xxx/index.php?page=account&s=options';
        });

        document.getElementById('br34-manual-btn').addEventListener('click', () => {
            document.getElementById('br34-modal-buttons').style.display = 'none';
            document.getElementById('br34-manual-input').style.display = 'block';
        });

        document.getElementById('br34-save-manual-btn').addEventListener('click', async () => {
            const credString = document.getElementById('br34-credential-input').value.trim();
            if (credString) {
                try {
                    const params = new URLSearchParams(credString.startsWith('?') ? credString : '?' + credString);
                    const apiKey = params.get('api_key');
                    const userId = params.get('user_id');

                    if (apiKey && userId) {
                        await GM_setValue(API_KEY_NAME, apiKey);
                        await GM_setValue(USER_ID_NAME, userId);
                        alert('API Key and User ID saved! The page will now reload.');
                        location.reload();
                    } else {
                        alert('Invalid format. Please paste the full string, e.g., &api_key=...&user_id=...');
                    }
                } catch (e) {
                    alert('Could not parse the provided string. Please check the format.');
                }
            }
        });
    }

    /**
     * On the account options page, finds the API credentials, saves them, and shows a confirmation banner.
     */
    async function handleOptionsPage() {
        if (!window.location.href.includes('page=account&s=options')) {
            return;
        }

        const textareas = document.querySelectorAll('textarea');
        let credTextarea = null;

        for (const textarea of textareas) {
            if (textarea.value.includes('&api_key=') && textarea.value.includes('&user_id=')) {
                credTextarea = textarea;
                break;
            }
        }

        if (credTextarea) {
            try {
                const credString = credTextarea.value.replace(/&amp;/g, '&');
                const params = new URLSearchParams(credString);
                const apiKey = params.get('api_key');
                const userId = params.get('user_id');

                if (apiKey && userId) {
                    await GM_setValue(API_KEY_NAME, apiKey);
                    await GM_setValue(USER_ID_NAME, userId);

                    // Show confirmation message
                    const banner = document.createElement('div');
                    banner.id = "br34-key-saved-banner";
                    banner.textContent = 'Better Rule34: API Key and User ID found and saved! You can now browse other pages.';
                    banner.style.cssText = `
                        background-color: #4CAF50; color: white; padding: 15px; text-align: center;
                        position: fixed; top: 0; left: 0; width: 100%; z-index: 10000; font-size: 16px;
                    `;
                    document.body.prepend(banner);
                    setTimeout(() => banner.remove(), 5000); // Remove after 5 seconds
                }
            } catch (e) {
                console.error("Better Rule34: Could not parse API credentials.", e);
            }
        } else {
            console.log("Better Rule34: Could not find API credentials textarea on this page.");
        }
    }


    const defaultConfig = {
        imageResizeNotice: "resize", // Changed default to "resize"
        theme: "dark",
        undeletePosts: true,
        clickAnywhereToStart: false, // Renamed to more descriptive "autoPlayVideo"
        htmlVideoPlayer: false,
        dynamicResizing: false,
        scrollPostsIntoView: false,
        downloadFullSizedImages: false,
        fitImageToScreen: false,
        hideAlerts: false,
        imageHover: true
    };

    // Initialize settings with default values if not already set
    function initializeSettings() {
        for (const key in defaultConfig) {
            if (GM_getValue(key) === undefined) {
                GM_setValue(key, defaultConfig[key]);
            }
        }
    }

    // Theme definitions
    const themes = {
        "dark": {
            "primary": "#121212",
            "secondary": "#000011",
            "contrast": "#4a4a4a",
            "complementary": "#666666",
            "tableBackground": "transparent",
            "linkColor": "#00f"
        }
    };

    // Apply dynamic resizing styles if enabled
    function applyDynamicResizing() {
        if (GM_getValue("dynamicResizing", false)) {
            GM_addStyle(`
                div.sidebar { max-width: 30%; }
                div.sidebar li { font-size: 120%; }
                div.content { width: 100%; }
                .thumb { height: 20%; width: auto; }
            `);
        }
    }

    applyDynamicResizing();

    const urlParams = new URLSearchParams(window.location.search);

    // Settings data structure
    const settingsData = {
        "tabs": [
            {
                "name": "General",
                "settings": [
                    {
                        "name": "imageResizeNotice",
                        "description": "Remove the image resize notice",
                        "type": "dropdown",
                        "options": ["resize", "no-resize"]
                    },
                    {
                        "name": "undeletePosts",
                        "description": "Display deleted posts",
                        "type": "checkbox"
                    },
                    {
                        "name": "hideAlerts",
                        "description": "Hide script warnings",
                        "type": "checkbox"
                    }
                ]
            },
            {
                "name": "Theme",
                "settings": [
                    {
                        "name": "theme",
                        "description": "Theme selection",
                        "type": "dropdown",
                        "options": Object.keys(themes), // Dynamically populate with available themes
                        "onChange": setTheme // Apply the theme immediately when changed
                    },
                    {
                        "name": "createNewTheme",
                        "description": "Create New Theme",
                        "type": "custom", // Use a custom type for more control
                        "render": (settingDiv, currentTheme) => {
                            const input = document.createElement('input');
                            input.type = 'text';
                            input.placeholder = 'Enter new theme name';
                            input.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 3px; margin-right: 5px;`;
                            settingDiv.appendChild(input);

                            const button = document.createElement('button');
                            button.classList.add('settings-button');
                            button.textContent = 'Create';
                            button.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 5px 10px; cursor: pointer; border-radius: 5px;`;
                            button.addEventListener('click', () => {
                                const newThemeName = input.value.trim();
                                if (newThemeName) {
                                    if (themes[newThemeName]) {
                                        alert('A theme with that name already exists!');
                                        return;
                                    }
                                    themes[newThemeName] = { ...themes.dark }; // Start with a copy of the dark theme
                                    GM_setValue("themes", themes); // Save the updated themes
                                    // Update the theme dropdown options
                                    const themeDropdown = document.querySelector('.settings-tab-content .settings-dropdown'); // Assuming you add a class to the dropdown
                                    if (themeDropdown) {
                                        const optionElement = document.createElement('option');
                                        optionElement.value = newThemeName;
                                        optionElement.textContent = newThemeName;
                                        themeDropdown.appendChild(optionElement);
                                    }
                                    // Refresh the settings panel to show the new theme
                                    openSettings();
                                } else {
                                    alert('Please enter a theme name.');
                                }
                            });
                            settingDiv.appendChild(button);
                        }
                    },
                    {
                        "name": "customizeTheme",
                        "description": "Customize Current Theme",
                        "type": "custom",
                        "render": (settingDiv, currentTheme) => {
                            const currentThemeName = GM_getValue("theme", "dark");
                            const currentThemeData = themes[currentThemeName];

                            const themeSettingsContainer = document.createElement('div');
                            themeSettingsContainer.style.cssText = `padding: 10px; border: 1px solid ${currentTheme.contrast}; margin-top: 10px;`;

                            for (const key in currentThemeData) {
                                const settingItemDiv = document.createElement('div');
                                settingItemDiv.style.marginBottom = '5px';

                                const label = document.createElement('label');
                                label.textContent = key;
                                label.style.cssText = 'display: block; margin-bottom: 2px;';
                                settingItemDiv.appendChild(label);

                                const input = document.createElement('input');
                                input.type = 'color';
                                input.value = currentThemeData[key];
                                input.addEventListener('change', () => {
                                    currentThemeData[key] = input.value;
                                    setTheme(); // Apply changes immediately
                                });
                                settingItemDiv.appendChild(input);

                                themeSettingsContainer.appendChild(settingItemDiv);
                            }

                            const saveButton = document.createElement('button');
                            saveButton.textContent = 'Save Changes';
                            saveButton.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 5px 10px; cursor: pointer; border-radius: 5px; margin-top: 5px;`;
                            saveButton.addEventListener('click', () => {
                                GM_setValue("themes", themes);
                                alert(`Theme "${currentThemeName}" updated!`);
                            });
                            themeSettingsContainer.appendChild(saveButton);

                            settingDiv.appendChild(themeSettingsContainer);
                        }
                    }
                ]
            },
            {
                "name": "Navigation",
                "settings": [
                    {
                        "name": "autoPlayVideo",
                        "description": "Click anywhere on the page to start the video",
                        "type": "checkbox"
                    },
                    {
                        "name": "scrollPostsIntoView",
                        "description": "Scroll posts into view",
                        "type": "checkbox"
                    }
                ]
            },
            {
                "name": "Posts",
                "settings": [
                    {
                        "name": "htmlVideoPlayer",
                        "description": "Use HTML video player instead of the Fluid Player",
                        "type": "checkbox"
                    },
                    {
                        "name": "dynamicResizing",
                        "description": "Dynamically resize the page for odd aspect ratios or large screens",
                        "type": "checkbox"
                    },
                    {
                        "name": "downloadFullSizedImages",
                        "description": "Download the full resolution image when saving the image",
                        "type": "checkbox"
                    },
                    {
                        "name": "fitImageToScreen",
                        "description": "Fit image to screen (buggy)",
                        "type": "checkbox"
                    },
                    {
                        "name": "imageHover",
                        "description": "Displays images on the search page when hovered",
                        "type": "checkbox"
                    }
                ]
            }
        ]
    };

    // Function to create and display the settings overlay
    function openSettings() {
        // Get the current theme for styling
        const currentTheme = themes[GM_getValue("theme", "dark")];

        // Create overlay
        const overlay = document.createElement('div');
        overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; z-index: 1000; /* Adjust the 0.5 for desired transparency */`;

        // Create settings container
        const settingsContainer = document.createElement('div');
        settingsContainer.style.cssText = `width: 40vw; background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; padding: 20px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); text-align: left; display: flex; flex-direction: column; align-items: stretch; border-radius: 5px;`;

        // Create tabs
        const tabsContainer = document.createElement('div');
        tabsContainer.style.cssText = 'display: flex; margin-bottom: 15px;';
        settingsContainer.appendChild(tabsContainer);

        settingsData.tabs.forEach(tab => {
            const tabButton = document.createElement('button');
            tabButton.classList.add('settings-tab-button');
            tabButton.textContent = tab.name;
            tabButton.style.cssText = `padding: 8px 15px; margin-right: 5px; background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; cursor: pointer; border-radius: 5px 5px 0 0;`;
            tabsContainer.appendChild(tabButton);

            const tabContent = document.createElement('div');
            tabContent.classList.add('settings-tab-content');
            tabContent.style.cssText = `display: none; border: 1px solid ${currentTheme.contrast}; padding: 10px; border-radius: 0 0 5px 5px;`;
            settingsContainer.appendChild(tabContent);

            tab.settings.forEach(setting => {
                const settingDiv = document.createElement('div');
                settingDiv.classList.add(`setting-${setting.name}`);
                settingDiv.style.marginBottom = '10px';

                const label = document.createElement('label');
                label.textContent = setting.description;
                label.style.cssText = 'display: block; margin-bottom: 5px; cursor: pointer;';
                settingDiv.appendChild(label);

                if (setting.type === "checkbox") {
                    const checkbox = document.createElement('input');
                    checkbox.type = 'checkbox';
                    checkbox.checked = GM_getValue(setting.name, false);
                    checkbox.addEventListener('change', () => GM_setValue(setting.name, checkbox.checked));
                    label.appendChild(checkbox);
                } else if (setting.type === "dropdown") {
                    const dropdown = document.createElement('select');
                    dropdown.classList.add("settings-dropdown")
                    dropdown.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 3px;`;
                    const currentValue = GM_getValue(setting.name, setting.options[0]);
                    setting.options.forEach(option => {
                        const optionElement = document.createElement('option');
                        optionElement.value = option;
                        optionElement.textContent = option;
                        optionElement.selected = option === currentValue;
                        dropdown.appendChild(optionElement);
                    });
                    dropdown.addEventListener('change', () => {
                        GM_setValue(setting.name, dropdown.value);
                        if (setting.onChange) {
                            setting.onChange(); // Call the onChange function if it exists
                        }
                    });
                    settingDiv.appendChild(dropdown);
                } else if (setting.type === "button") {
                    const button = document.createElement('button');
                    button.classList.add('settings-button');
                    button.textContent = setting.description;
                    button.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 5px 10px; cursor: pointer; border-radius: 5px;`;
                    button.addEventListener('click', setting.action);
                    settingDiv.appendChild(button);
                } else if (setting.type === "custom" && setting.render) {
                    setting.render(settingDiv, currentTheme); // Call the custom render function
                }

                tabContent.appendChild(settingDiv);
            });

            tabButton.addEventListener('click', () => {
                // Show the clicked tab content and hide others
                const allTabContents = document.querySelectorAll('.settings-tab-content');
                allTabContents.forEach(content => content.style.display = 'none');
                tabContent.style.display = 'block';

                // Highlight the active tab button
                const allTabButtons = document.querySelectorAll('.settings-tab-button');
                allTabButtons.forEach(button => button.style.backgroundColor = currentTheme.secondary);
                tabButton.style.backgroundColor = currentTheme.complementary;
            });
        });

        // Append to overlay and body
        overlay.appendChild(settingsContainer);
        document.body.appendChild(overlay);

        // Show the first tab by default
        const firstTabButton = document.querySelector('.settings-tab-button');
        if (firstTabButton) {
            firstTabButton.click();
        }

        // Close overlay when clicking outside
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                overlay.remove();
            }
        });
    }


    // Function to set the selected theme
    function setTheme() {
        // Load saved themes from GM storage
        const savedThemes = GM_getValue("themes");

        // Check if saved themes exist and are an object, otherwise use default
        if (typeof savedThemes === 'object' && savedThemes !== null) {
            Object.assign(themes, savedThemes);
        }

        const currentTheme = themes[GM_getValue("theme", "dark")];
        if (currentTheme) {
            const css = `
            table a:link, table a:visited { color: ${currentTheme.linkColor}; }
            body { background-color: ${currentTheme.primary}; }
            .flat-list, div#header ul#subnavbar, .current-page { background-color: ${currentTheme.secondary}; }
            div#header ul#navbar li.current-page { background-image: url(https://imgs.search.brave.com/77L3MmxBu09NuN5WiX4HlbmWjjUe7eAsmBbakS7-DTo/rs:fit:120:120:1/g:ce/aHR0cHM6Ly91cGxv/YWQud2lraW1lZGlh/Lm9yZy93aWtpcGVk/aWEvY29tbW9ucy90/aHVtYi8wLzAyL1Ry/YW5zcGFyZW50X3Nx/dWFyZS5zdmcvMTIw/cHgtVHJhbnNwYXJl/bnRfc3F1YXJlLnN2/Zy5wbmc); }
            .manual-page-chooser>input[type=text], .manual-page-chooser>input[type=submit], div.tag-search input[type=text], div.tag-search input[type=submit], button { background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; }
            .col2, h6, h5, .tag-count, b, li, ul, table.highlightable td, h2, table.form p, table, label { color: ${currentTheme.contrast}; }
            button { box-sizing: border-box; border: 1px solid; margin-top: 3px; border-color: ${currentTheme.contrast}; }
            table { background-color: ${currentTheme.tableBackground}; }
            div { color: ${currentTheme.contrast}; }
            .settings-tab-button { background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border-color: ${currentTheme.contrast}; }
            .settings-tab-button:hover, .settings-button:hover { background-color: ${currentTheme.complementary}; }
            .settings-tab-content { background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border-color: ${currentTheme.contrast}; }
            input[type="color"] { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: transparent; width: 100px; height: 40px; border: none; cursor: pointer; }
            input[type="color"]::-webkit-color-swatch { border: 1px solid ${currentTheme.contrast}; border-radius: 5px; }
            input[type="color"]::-moz-color-swatch { border: 1px solid ${currentTheme.contrast}; border-radius: 5px; }
        `;
            GM_addStyle(css);

            const userIndexElement = document.getElementById("user-index");
            if (userIndexElement) {
                Array.from(userIndexElement.getElementsByTagName("p")).forEach(element => element.style.color = currentTheme.contrast);
            }

            if (GM_getValue("resizePosts", false) && window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=view")) {
                GM_addStyle(".content{max-height: 45%; max-width: 45%; overflow: auto;}");
                const imageElement = document.getElementById("image");
                if (imageElement) {
                    imageElement.style.maxHeight = "50%";
                    imageElement.style.maxWidth = "fit-content";
                    imageElement.style.overflow = "auto";
                }
            }
        }
    }

    // Function to fetch data from Rule34 API
    async function getFromRule34(tags, index, limit, useBlacklist = false) {
        const apiKey = await GM_getValue(API_KEY_NAME);
        const userId = await GM_getValue(USER_ID_NAME);

        tags = tags === "all" ? "" : tags;
        let pid = index;
        if (useBlacklist) {
            const blacklist = decodeURIComponent(getCookie("tag_blacklist")).replaceAll("%20", " -").replaceAll("%2F", "/");
            tags += blacklist ? ` -${blacklist}` : "";
        }
        const url = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&tags=${encodeURIComponent(tags)}&limit=${limit}&pid=${pid}&json=1&api_key=${apiKey}&user_id=${userId}`;

        try {
            const response = await GM.xmlHttpRequest({
                method: 'GET',
                url: url
            });

            if (response.status >= 200 && response.status < 300) {
                return JSON.parse(response.responseText);
            } else {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
        } catch (error) {
            console.error(`Error fetching data: ${error}`);
            return []; // Return an empty array on error
        }
    }

    // Function to fetch data for a specific post ID
    async function getFromRule34WithId(id) {
        const apiKey = await GM_getValue(API_KEY_NAME);
        const userId = await GM_getValue(USER_ID_NAME);
        const url = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&id=${id}&json=1&api_key=${apiKey}&user_id=${userId}`;

        try {
            const response = await GM.xmlHttpRequest({
                method: 'GET',
                url: url
            });

            if (response.status >= 200 && response.status < 300) {
                const data = JSON.parse(response.responseText);
                return data[0] || null; // Return the post or null if not found
            } else {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
        } catch (error) {
            console.error(`Error fetching post: ${error}`);
            return null;
        }
    }

    // Helper function to get a cookie by name
    function getCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) return parts.pop().split(';').shift();
        return null;
    }

    // --- SCRIPT'S ORIGINAL FUNCTIONS (UNCHANGED, except where noted) ---
    // (The rest of your original script functions go here, from getTagsFromUrl down to the end)

    // Function to extract tags from the current URL
    function getTagsFromUrl(currentUrl) {
        if (currentUrl.startsWith("https://rule34.xxx/index.php?page=post&s=list&tags=")) {
            return currentUrl.replace("https://rule34.xxx/index.php?page=post&s=list&tags=", "");
        }
        return "";
    }

    // Function to create modified links for post navigation
    function createLinks() {
        try {
            if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=list&tags=")) {
                const imageList = document.getElementsByClassName("image-list")[0];
                if (!imageList) throw new Error("Image list not found.");

                const anchors = imageList.getElementsByTagName("a");
                if (anchors.length === 0) throw new Error("No anchor elements found in image list.");

                const urlParams = new URLSearchParams(window.location.search);
                let pageNum = parseInt(urlParams.get("pid")) || 0;

                for (let i = 0; i < anchors.length; i++) {
                    anchors[i].href = `${anchors[i].href}&srchTags=${getTagsFromUrl(window.location.href)}&index=${i + pageNum}`.replace(/[\?&]pid=\d*/g, '');
                }
            } else {
                // This is not an error, just means we are not on the search page.
            }
        } catch (error) {
            console.error(`Error in createLinks: ${error}`);
        }
    }

    let preloadedData;

    // Function to preload data for the next post
    async function preloadNextPost(srchTags, nextIndex, limit) {
        try {
            preloadedData = await getFromRule34(srchTags, nextIndex, limit, true);
        } catch (error) {
            console.error(`Error preloading next post: ${error}`);
        }
    }

    // Function to navigate to the next post
    function navigateToNextPost(srchTags, nextIndex) {
        if (!preloadedData || preloadedData.length === 0) {
            console.log(preloadedData)
            console.error("No preloaded data available.");
            return;
        }

        const nextPostId = preloadedData[0].id;
        const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${nextPostId}&srchTags=${encodeURIComponent(srchTags)}&index=${nextIndex}`;
        window.location.href = newUrl;
    }

    // Function to navigate to the previous post
    async function backPost() {
        const urlParams = new URLSearchParams(window.location.search);
        const srchTags = urlParams.get("srchTags");
        const currentIndex = parseInt(urlParams.get("index"));

        if (!srchTags || isNaN(currentIndex) || currentIndex <= 0) {
            console.error("Invalid URL parameters or no previous post.");
            return;
        }

        const nextIndex = currentIndex - 1;
        const limit = 1;

        try {
            const jsonInfo = await getFromRule34(srchTags, nextIndex, limit);
            if (!jsonInfo || jsonInfo.length === 0) {
                console.error("No data received from API.");
                return;
            }

            const nextPostId = jsonInfo[0].id;
            const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${nextPostId}&srchTags=${encodeURIComponent(srchTags)}&index=${nextIndex}`;
            window.location.href = newUrl;
        } catch (error) {
            console.error(`Error navigating to previous post: ${error}`);
        }
    }

    // Function to select a random post and navigate to it
    async function randomVideo() {
        const urlParams = new URLSearchParams(window.location.search);
        let srchTags = urlParams.get("tags");

        if (!srchTags) {
            const tagsInput = document.querySelector("input[name='tags']");
            srchTags = tagsInput ? tagsInput.value.replace(/ /g, "+") : "";
        }

        try {
            const posts = await getFromRule34(srchTags, 0, 1000);
            if (posts.length === 0) {
                console.error("No posts found for the given tags.");
                return;
            }

            const randNum = Math.floor(Math.random() * posts.length);
            const postId = posts[randNum].id;
            const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${postId}&tags=${encodeURIComponent(srchTags)}&index=${randNum}`;
            window.location.href = newUrl;
        } catch (error) {
            console.error(`Error in randomVideo: ${error}`);
        }
    }

    // Function to download all posts for the current search
    async function downloadAllPostFiles() {
        const urlParams = new URLSearchParams(window.location.search);
        let srchTags = urlParams.get("tags");

        if (!srchTags) {
            const tagsInput = document.querySelector("input[name='tags']");
            srchTags = tagsInput ? tagsInput.value.replace(/ /g, "+") : "";
        }

        try {
            const posts = await getFromRule34(srchTags, 0, 1000);
            if (posts.length === 0) {
                console.error("No posts found for the given tags.");
                return;
            }

            const zipFiles = {};
            const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg'];
            let postsDownloaded = 0;
            const totalPosts = posts.length;

            for (const post of posts) {
                const fileUrl = post.file_url;
                const sampleUrl = post.sample_url;
                const fileExtension = fileUrl.slice(fileUrl.lastIndexOf('.')).toLowerCase();
                const downloadUrl = videoExtensions.includes(fileExtension) ? sampleUrl : fileUrl;
                const fileName = downloadUrl.split("/").pop();

                try {
                    const fileData = await fetchFileAsBlob(downloadUrl);
                    const uint8Array = new Uint8Array(await fileData.arrayBuffer());
                    zipFiles[fileName] = uint8Array;
                    console.log(`Added file ${fileName} to zip`);
                    postsDownloaded++;
                    document.title = `${postsDownloaded}/${totalPosts}`;
                } catch (error) {
                    console.error(`Error fetching file ${downloadUrl}: ${error}`);
                }
            }

            const zipBlob = fflate.zipSync(zipFiles, {
                level: 0,
                mtime: new Date()
            });
            console.log("Zip finished");

            const a = document.createElement("a");
            const zipBlobUrl = URL.createObjectURL(new Blob([zipBlob], {
                type: "application/zip"
            }));
            a.href = zipBlobUrl;
            a.download = `${srchTags.replace(/\+/g, "_")}.zip`;
            a.click();
        } catch (error) {
            console.error(`Error in downloadAllPostFiles: ${error}`);
        }
    }

    // Helper function to fetch a file as a Blob
    async function fetchFileAsBlob(url) {
        try {
            const response = await GM.xmlHttpRequest({
                method: "GET",
                url: url,
                responseType: "blob"
            });

            if (response.status >= 200 && response.status < 300) {
                return response.response;
            } else {
                throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
            }
        } catch (error) {
            throw new Error(`Error fetching ${url}: ${error}`);
        }
    }

    // Function to create and append navigation buttons
    function createNavigationButtons() {
        const tagSearch = document.getElementsByClassName("tag-search")[0];
        if (tagSearch) {
            const randomButton = document.createElement("button");
            randomButton.textContent = "Random";
            randomButton.addEventListener("click", randomVideo);
            tagSearch.appendChild(randomButton);

            const downloadAllButton = document.createElement("button");
            downloadAllButton.textContent = "↓";
            downloadAllButton.addEventListener("click", downloadAllPostFiles);
            tagSearch.appendChild(downloadAllButton);
        }

        const imageSublinks = document.getElementsByClassName("image-sublinks")[0];
        if (imageSublinks) {
            const backButton = document.createElement("button");
            backButton.textContent = "Back";
            backButton.addEventListener("click", backPost);
            imageSublinks.appendChild(backButton);

            const nextButton = document.createElement("button");
            nextButton.id = "nextButton";
            nextButton.textContent = "Next";
            nextButton.addEventListener("click", () => {
                const urlParams = new URLSearchParams(window.location.search);
                const srchTags = urlParams.get("srchTags");
                const currentIndex = parseInt(urlParams.get("index"));
                navigateToNextPost(srchTags, currentIndex + 1);
            });
            imageSublinks.appendChild(nextButton);
        }
    }

    // Function to enable dynamic resizing of the search input field
    function enableDynamicInputResizing() {
        const awesompleteElement = document.querySelector(".awesomplete > input");
        if (!awesompleteElement) return;

        awesompleteElement.style.position = "relative";
        awesompleteElement.style.zIndex = "99";

        function resizeInput() {
            this.style.minWidth = "100%";
            this.style.width = `${this.value.length}ch`;
        }

        function restoreNormalSize() {
            this.style.width = "100%";
        }

        awesompleteElement.addEventListener('input', resizeInput);
        awesompleteElement.addEventListener('click', resizeInput);
        awesompleteElement.addEventListener('blur', restoreNormalSize);
    }

    // Add keyboard navigation for posts
    const imageSublinks = document.getElementsByClassName("image-sublinks")[0];
    if (imageSublinks) {
        document.addEventListener("keydown", function(event) {
            if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;

            const urlParams = new URLSearchParams(window.location.search);
            const srchTags = urlParams.get("srchTags");
            const currentIndex = parseInt(urlParams.get("index"));

            if (event.key === "ArrowRight") {
                navigateToNextPost(srchTags, currentIndex + 1);
            } else if (event.key === "ArrowLeft") {
                backPost();
            }
        });
    }

    // Function to handle deleted posts
    async function handleDeletedPosts(id) {
        const statusNotices = document.getElementsByClassName("status-notice");
        if (statusNotices.length === 0) return;

        let foundDeletedPost = false;
        for (const statusNotice of statusNotices) {
            if (statusNotice.firstChild.data.startsWith("This post was")) {
                foundDeletedPost = true;
                try {
                    const mediaJson = await getFromRule34WithId(id);
                    if (!mediaJson) throw new Error("Failed to retrieve post data.");

                    const mediaUrl = mediaJson.file_url;
                    const mediaType = mediaUrl.split('.').pop().toLowerCase();
                    const fitToScreen = document.getElementById("fit-to-screen");

                    const videoExtensions = ["mp4", "webm", "ogg", "mov", "avi", "wmv", "flv", "mkv", "3gp", "m4v", "mpg", "mpeg", "swf", "vob", "m2ts"];
                    const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "svg", "webp", "heic", "heif", "ico", "raw", "psd", "ai", "eps"];

                    if (videoExtensions.includes(mediaType)) {
                        const video = document.createElement("video");
                        video.src = mediaUrl;
                        video.controls = true;
                        video.style.cssText = "max-height: 70%; max-width: 70%; overflow: auto;";
                        fitToScreen.appendChild(video);
                    } else if (imageExtensions.includes(mediaType)) {
                        const image = document.createElement("img");
                        image.src = mediaUrl;
                        image.style.cssText = "max-height: 70%; max-width: 70%; overflow: auto;";
                        fitToScreen.appendChild(image);
                    }

                    statusNotice.remove();
                } catch (error) {
                    console.error(`Error handling deleted post: ${error}`);
                }
                break;
            }
        }

        if (!foundDeletedPost) {
            console.log("This post is not deleted.");
        }
    }

    // Function to download a file
    async function downloadFile(fileUrl, filename) {
        try {
            const response = await GM.xmlHttpRequest({
                method: 'GET',
                url: fileUrl,
                responseType: 'blob'
            });

            if (response.status >= 200 && response.status < 300) {
                const blob = response.response;
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = filename;
                link.click();
                URL.revokeObjectURL(link.href);
            } else {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
        } catch (error) {
            console.error(`Error downloading file: ${error}`);
        }
    }

    // Helper function to get the last link in a parent element
    function getLastLinkInParent(element) {
        const elements = element.parentNode.querySelectorAll("a");
        return elements[elements.length - 1];
    }

    // Function to handle the image resize popup
    function handleImageResizePopup() {
        try {
            if (GM_getValue("imageResizeNotice", "resize") === "no-resize") {
                const resizedNotice = document.getElementById('resized_notice');
                if (resizedNotice) resizedNotice.style.display = 'none';
            }
            // Removed "original" option as it was causing issues and is not typically necessary
        } catch (error) {
            console.error(`Error handling image resize popup: ${error}`);
        }
    }

    // Function to start the video on the first click
    function autoPlayVideo() {
        if (GM_getValue("autoPlayVideo", false)) {
            let isFirstClick = true;

            document.addEventListener("click", function() {
                if (isFirstClick) {
                    const videoPlayer = document.getElementById("gelcomVideoPlayer");
                    if (videoPlayer) {
                        videoPlayer.autoplay = true;
                        const playButton = document.getElementById("gelcomVideoPlayer_fluid_initial_play");
                        if (playButton) playButton.click();
                    }
                    isFirstClick = false;
                }
            });
        }
    }

    autoPlayVideo()

    // Function to add buttons for adding tags to the search
    function addTagButtons() {
        const tagTypes = ["tag-type-copyright", "tag-type-general", "tag-type-character", "tag-type-artist", "tag-type-metadata"];
        for (const tagType of tagTypes) {
            const elements = document.getElementsByClassName(tagType);
            for (const element of elements) {
                const button = document.createElement("button");
                button.textContent = "+";
                button.addEventListener("click", function() {
                    const tagsInput = document.querySelector("[name='tags']");
                    const tagToAdd = getLastLinkInParent(this).textContent.trim().replaceAll(" ", "_");
                    tagsInput.value += ` ${tagToAdd}`;
                });
                element.insertBefore(button, element.firstChild);
            }
        }
    }

    // Function to make the video/image container resizable
    function makePostResizable(isImage) {
        let div = isImage ? document.getElementById("image") : document.getElementById("fluid_video_wrapper_gelcomVideoPlayer");
        if (!div) return;

        if (isImage) {
            const newDiv = document.createElement("div");
            newDiv.style.position = "relative";
            div.parentNode.insertBefore(newDiv, div);
            newDiv.appendChild(div);
            div = newDiv;
            document.getElementById("image").style.maxHeight = 'none';
        }

        const resizer = document.createElement("div");
        resizer.style.cssText = "width: 10px; height: 10px; background-color: white; position: absolute; bottom: 0; right: 0; cursor: se-resize; z-index: 10;";

        let isResizing = false;
        let currentX, currentY, initialWidth, initialHeight;

        resizer.addEventListener("mousedown", function(e) {
            document.body.style.userSelect = 'none';
            isResizing = true;
            currentX = e.clientX;
            currentY = e.clientY;
            initialWidth = parseFloat(getComputedStyle(div).width);
            initialHeight = parseFloat(getComputedStyle(div).height);
        });

        document.addEventListener("mouseup", () => {
            document.body.style.userSelect = '';
            isResizing = false;
        });

        document.addEventListener("mousemove", function(e) {
            if (!isResizing) return;

            let newWidth = initialWidth + (e.clientX - currentX);
            let newHeight = initialHeight + (e.clientY - currentY);
            if (!e.shiftKey) {
                const ratio = initialWidth / initialHeight;
                newHeight = newWidth / ratio;
            }

            if (isImage) {
                const innerImage = div.querySelector("img");
                innerImage.style.width = `${newWidth}px`;
                innerImage.style.height = `${newHeight}px`;
            }

            div.style.width = `${newWidth}px`;
            div.style.height = `${newHeight}px`;
            div.style.maxHeight = "1000vh";

            const videoPlayer = document.getElementById("gelcomVideoPlayer");
            if (videoPlayer) {
                videoPlayer.style.maxHeight = "1000vh";
                videoPlayer.style.height = "100%";
            }

            const imageElement = document.getElementById("image");
            if (imageElement) {
                imageElement.style.width = `${newWidth}px`;
                imageElement.style.height = `${newHeight}px`;
                imageElement.style.maxHeight = "1000vh";
            }
        });

        div.appendChild(resizer);
    }

    // Add an input box after the tags input field
    function addInputBox() {
        const tagsElement = document.querySelector("[name='tags']");
        if (!tagsElement) return;

        const inputBox = document.createElement("input");
        inputBox.type = "text";
        tagsElement.after(inputBox);
    }

    // Function to set the value of the tags input field
    function setTags(tags) {
        const tagsInput = document.querySelector("[name='tags']");
        if (tagsInput) tagsInput.value = tags;
    }

    // Function to replace the Fluid Player with the native HTML5 video player
    async function replaceWithHtmlVideoPlayer(id) {
        const gelcomVideoContainer = document.getElementById("gelcomVideoContainer");
        if (!gelcomVideoContainer) return;

        try {
            const videoUrlData = await getFromRule34WithId(id);
            if (!videoUrlData) throw new Error("Failed to retrieve video URL.");

            const video = document.createElement("video");
            video.src = videoUrlData.file_url;
            video.controls = true;
            video.style.cssText = "max-height: 70%; max-width: 70%; overflow: auto;";

            gelcomVideoContainer.parentNode.insertBefore(video, gelcomVideoContainer.nextSibling);
            gelcomVideoContainer.remove();

            const statusNotices = document.getElementById("status-notices");
            if (statusNotices) statusNotices.remove();
        } catch (error) {
            console.error(`Error replacing video player: ${error}`);
        }
    }

    // Function to add a close button to status notice elements
    function addCloseButtonToStatusNotices() {
        const statusNoticeElements = document.querySelectorAll('.status-notice');
        statusNoticeElements.forEach(element => {
            const closeButton = document.createElement('button');
            closeButton.textContent = 'x';
            closeButton.style.cssText = 'background: none; border: none; cursor: pointer;';
            closeButton.addEventListener('click', () => element.remove());
            element.appendChild(closeButton);
        });
    }

    // Function to overlay the full-size image on top of the displayed image
    async function overlayFullSizeImage() {
        const urlParams = new URLSearchParams(window.location.search);
        const id = urlParams.get("id");
        if (!id) return;

        const postJson = await getFromRule34WithId(id);
        if (!postJson) return;

        const originalImage = document.getElementById("image");
        if (!originalImage) return;

        const newImage = document.createElement("img");
        newImage.src = postJson.file_url;
        newImage.style.cssText = `opacity: 0; width: ${originalImage.width}px; height: ${originalImage.height}px; position: absolute; top: ${originalImage.offsetTop}px; left: ${originalImage.offsetLeft}px; z-index: 1;`;

        originalImage.parentNode.insertBefore(newImage, originalImage);
    }

    // Function to convert the search button to a link
    function convertSearchToLink() {
        const commitButton = document.querySelector("[name='commit']");
        if (!commitButton) return;

        commitButton.innerHTML = `<a href="https://rule34.xxx/index.php?page=post&s=list&tags=all">${commitButton.innerHTML}</a>`;
    }

    // Function to fit the post (image or video) to the screen
    function fitPostToScreen() {
        if (GM_getValue("downloadFullSizedImages", false) && !GM_getValue("hideAlerts", false)) {
            alert(`downloadFullSizedImage and fitImageToScreen often cause bugs when used together. To disable this alert turn hide alerts on in settings.`);
        }

        const postElement = document.getElementById("fluid_video_wrapper_gelcomVideoPlayer") || document.getElementById("image");
        if (!postElement) return;

        postElement.style.maxHeight = "85vh";
        postElement.style.width = "auto";

        const gelcomVideoPlayer = document.getElementById("gelcomVideoPlayer");
        if (gelcomVideoPlayer) {
            gelcomVideoPlayer.style.maxHeight = "85vh";
            gelcomVideoPlayer.style.width = "auto";
        }
    }

    // Function to add download buttons to each post in the search results
    function addDownloadButtonsToPosts() {
        GM_addStyle(`
            .spinner {
                border: 2px solid #f3f3f3;
                border-top: 2px solid #3498db;
                border-radius: 50%;
                width: 12px;
                height: 12px;
                animation: spin 1s linear infinite;
                display: none;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            .loading .spinner {
                display: inline-block;
            }
            .loading .button-text {
                display: none;
            }
        `);

        const thumbElements = document.querySelectorAll('.thumb');
        thumbElements.forEach(thumb => {
            const button = document.createElement('button');
            button.style.cssText = 'position: absolute; top: 0; right: 0;';

            const buttonText = document.createElement('span');
            buttonText.classList.add('button-text');
            buttonText.textContent = '↓';
            button.appendChild(buttonText);

            const spinner = document.createElement('div');
            spinner.classList.add('spinner');
            button.appendChild(spinner);

            button.addEventListener('click', async () => {
                button.classList.add('loading');
                try {
                    const aElement = thumb.querySelector('a');
                    if (!aElement) {
                        alert('No <a> element found within this thumb element.');
                        return;
                    }

                    const id = aElement.id.substring(1);
                    const data = await getFromRule34WithId(id);
                    if (!data) return;

                    const fileUrl = data.file_url;
                    const sampleUrl = data.sample_url;
                    const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg'];
                    const fileExtension = fileUrl.split('.').pop().toLowerCase();
                    const downloadUrl = videoExtensions.includes(fileExtension) ? sampleUrl : fileUrl;
                    const filename = downloadUrl.split('/').pop();

                    await downloadFile(downloadUrl, filename);
                } catch (error) {
                    console.error('Error downloading file:', error);
                } finally {
                    button.classList.remove('loading');
                }
            });

            thumb.style.position = 'relative';
            thumb.appendChild(button);
        });
    }

    // Function to display media information on hover
    async function displayMediaData() {
        const urlParams = new URLSearchParams(window.location.search);
        const id = urlParams.get("id");
        if (!id) return;

        const postJson = await getFromRule34WithId(id);
        if (!postJson) return;

        let mediaURL = postJson.file_url;
        const mediaType = mediaURL.split('.').pop().toLowerCase();

        const tooltipContent = document.createElement('div');

        const handleImage = async () => {
            const img = new Image();
            img.src = mediaURL;

            img.onload = async () => {
                const width = img.width;
                const height = img.height;

                try {
                    const response = await GM.xmlHttpRequest({
                        method: 'GET',
                        url: mediaURL,
                        responseType: 'blob'
                    });

                    const imageSizeBytes = response.response.size;
                    const imageSizeKB = imageSizeBytes / 1024;
                    const imageSizeMB = imageSizeKB / 1024;
                    const sizeInfo = imageSizeMB >= 1 ? `${imageSizeMB.toFixed(2)} MB` : `${imageSizeKB.toFixed(2)} KB`;

                    tooltipContent.innerHTML = `
                        <div>Media URL: ${mediaURL}</div>
                        <div>Media Type: ${mediaType}</div>
                        <div>Width: ${width}px</div>
                        <div>Height: ${height}px</div>
                        <div>Size: ${sizeInfo}</div>
                    `;
                    appendTooltipToPage(tooltipContent);
                } catch (error) {
                    console.error('Failed to fetch image size:', error);
                }
            };

            img.onerror = (error) => console.error('Failed to load image:', error);
        };

        const handleVideo = async () => {
            const video = document.createElement('video');
            video.src = mediaURL;
            mediaURL = postJson.sample_url;

            video.onloadedmetadata = async () => {
                const width = video.videoWidth;
                const height = video.videoHeight;

                try {
                    const response = await GM.xmlHttpRequest({
                        method: 'GET',
                        url: mediaURL,
                        responseType: 'blob'
                    });

                    const videoSizeBytes = response.response.size;
                    const videoSizeKB = videoSizeBytes / 1024;
                    const videoSizeMB = videoSizeKB / 1024;
                    const sizeInfo = videoSizeMB >= 1 ? `${videoSizeMB.toFixed(2)} MB` : `${videoSizeKB.toFixed(2)} KB`;

                    video.currentTime = 1;
                    video.onseeked = () => {
                        tooltipContent.innerHTML = `
                            <div>Media URL: ${mediaURL}</div>
                            <div>Media Type: ${mediaType}</div>
                            <div>Width: ${width}px</div>
                            <div>Height: ${height}px</div>
                            <div>Size: ${sizeInfo}</div>
                        `;
                        appendTooltipToPage(tooltipContent);
                    };
                } catch (error) {
                    console.error('Failed to fetch video size:', error);
                }
            };

            video.onerror = (error) => console.error('Failed to load video:', error);
        };

        if (['jpg', 'jpeg', 'png', 'gif'].includes(mediaType)) {
            await handleImage();
        } else if (['mp4', 'webm', 'ogg'].includes(mediaType)) {
            await handleVideo();
        } else {
            console.error('Unsupported media type:', mediaType);
        }
    }

    // Helper function to append the tooltip to the page
    function appendTooltipToPage(tooltipContent) {
        const infoIcon = document.createElement('span');
        infoIcon.innerHTML = 'ℹ️';
        infoIcon.style.cssText = 'cursor: pointer; margin-left: 10px;';
        infoIcon.title = 'Media Information';

        const tooltip = document.createElement('div');
        tooltip.appendChild(tooltipContent);
        tooltip.style.cssText = 'position: absolute; background-color: #fff; border: 1px solid #ccc; padding: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); display: none; z-index: 1000;';

        infoIcon.appendChild(tooltip);
        infoIcon.onmouseover = () => tooltip.style.display = 'block';
        infoIcon.onmouseout = () => tooltip.style.display = 'none';

        const imageSublinks = document.querySelector('.image-sublinks');
        if (imageSublinks) imageSublinks.appendChild(infoIcon);
    }

    let activeImageContainer = null;

    // Function to add a hover effect to display full-size images on the search page
    async function addHoverEffect() {
        const thumbImages = document.querySelectorAll('.thumb a[id^="p"]'); // More specific selector

        thumbImages.forEach(thumbLink => {
            const parentThumb = thumbLink.closest('.thumb');
            if (!parentThumb) return;

            parentThumb.addEventListener('mouseenter', async function(event) {
                if (activeImageContainer) {
                    activeImageContainer.remove();
                }

                let id = thumbLink.id.replace(/\D/g, '');
                const postData = await getFromRule34WithId(id);

                if (!postData || !postData.file_url) {
                    console.error(`No file_url found for ID: ${id}`);
                    return;
                }

                const imageContainer = document.createElement('div');
                imageContainer.style.cssText = 'position: absolute; z-index: 1000; border: 2px solid black; padding: 10px; background-color: white;';

                const image = document.createElement('img');
                image.style.maxWidth = '300px';
                image.style.maxHeight = '300px';
                image.src = postData.file_url;

                imageContainer.appendChild(image);
                document.body.appendChild(imageContainer);
                activeImageContainer = imageContainer;

                function moveImageAtCursor(e) {
                    if (parentThumb.matches(':hover')) {
                        imageContainer.style.left = `${e.pageX + 10}px`;
                        imageContainer.style.top = `${e.pageY + 10}px`;
                    } else {
                        imageContainer.remove();
                        document.removeEventListener('mousemove', moveImageAtCursor);
                        activeImageContainer = null;
                    }
                }

                document.addEventListener('mousemove', moveImageAtCursor);

                parentThumb.addEventListener('mouseleave', function() {
                    if (activeImageContainer) {
                        activeImageContainer.remove();
                        activeImageContainer = null;
                    }
                    document.removeEventListener('mousemove', moveImageAtCursor);
                });
            });
        });
    }

    // Function to scroll the post into view
    function scrollPostIntoView() {
        if (GM_getValue("scrollPostsIntoView", false)) {
            const postElement = document.getElementById("image") || document.getElementById("gelcomVideoPlayer");
            if (postElement) {
                setTimeout(() => postElement.scrollIntoView({
                    behavior: 'smooth',
                    block: 'center',
                    inline: 'nearest'
                }), 250);
            }
        }
    }

    // Register the settings menu command
    GM_registerMenuCommand('Settings', openSettings);

    function executeScript() {
        const urlParams = new URLSearchParams(window.location.search);
        // Call functions based on the current page and settings
        createNavigationButtons();
        createLinks();
        addCloseButtonToStatusNotices();
        setTheme();
        handleImageResizePopup();
        addTagButtons();
        enableDynamicInputResizing();
        scrollPostIntoView();

        try {
            const idParam = urlParams.get("id");
            if (idParam) {
                if (GM_getValue("undeletePosts", false)) {
                    handleDeletedPosts(idParam);
                }
                if (GM_getValue("htmlVideoPlayer", false)) {
                    replaceWithHtmlVideoPlayer(idParam);
                }
            }
        } catch (error) {
            console.error(`Error in script execution: ${error}`);
        }

        if (GM_getValue("downloadFullSizedImages", false)) {
            overlayFullSizeImage();
        }
        if (GM_getValue("fitImageToScreen", false)) {
            fitPostToScreen();
        }
        if (GM_getValue("imageHover", false)) {
            addHoverEffect();
        }

        if (window.location.href.includes('page=post&s=view')) {
            displayMediaData();
        }
        if (window.location.href.includes('page=post&s=list')) {
            addDownloadButtonsToPosts();
        }


        // Set tags based on URL parameters
        if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=view")) {
            setTags(urlParams.get("srchTags"));
        } else if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=list")) {
            setTags(urlParams.get("tags"));
        }

        // Ensure note boxes are always on top
        const noteBoxes = document.querySelectorAll(".note-box");
        noteBoxes.forEach(noteBox => noteBox.style.zIndex = "999");

        // Call functions to make video/image resizable after a short delay
        setTimeout(() => {
            makePostResizable(false); // For video
            makePostResizable(true); // For image
        }, 300);

        if (document.readyState === "complete" && !window.betterRule34Initialized) {
            const srchTags = urlParams.get("srchTags");
            const currentIndex = parseInt(urlParams.get("index"));

            if (!srchTags || isNaN(currentIndex)) {
                // Not on a post page, which is fine.
                return;
            }

            const nextIndex = currentIndex + 1;
            const limit = 1; // Only need to preload the very next one

            // Preload data for the next post
            preloadNextPost(srchTags, nextIndex, limit);

            // Event listener for when the user tries to navigate to the next post
            const nextButton = document.getElementById("nextButton");
            if (nextButton) {
                nextButton.removeEventListener("click", navigateToNextPost)
                nextButton.addEventListener("click", () => {
                    navigateToNextPost(srchTags, nextIndex);
                });
            }
            window.betterRule34Initialized = true;
        }
    }


    // --- Main Execution ---
    // Initialize settings first, always.
    initializeSettings();

    // Handle the options page immediately if we're on it.
    await handleOptionsPage();

    // Check for API key. This will halt the script with a prompt if the key is missing.
    await checkApiKey();

    // If the key exists, proceed with the rest of the script.
    executeScript();

    // Listen for popstate event to re-run the script when navigating through history
    window.addEventListener('popstate', executeScript);

})().catch(console.error);