Better Rule34

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

// ==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);