Civitai Better Galleries: Larger Video/Image & Preload

Hides UI elements when viewing galleries and attempts to preload upcoming videos/images by simulating key presses

// ==UserScript==
// @name        Civitai Better Galleries: Larger Video/Image & Preload
// @namespace   Violentmonkey Scripts
// @match       *://*.civitai.com/*
// @grant       GM_addStyle
// @version     1.0
// @author      rainlizard
// @license     MIT
// @description Hides UI elements when viewing galleries and attempts to preload upcoming videos/images by simulating key presses
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const SELECTORS_TO_HIDE = [
        ".p-3.gap-8.justify-between.flex", // Original top bar
        ".flex.flex-col.gap-3.p-3",        // Container for reaction buttons
        // Target the main sidebar container using its specific width and layout classes
        ".\\@md\\:w-\\[450px\\].\\@md\\:min-w-\\[450px\\].flex-col"
    ];
    const SELECTOR_TO_HIDE_DURING_SIMULATION = ".flex.min-h-0.flex-1.items-stretch.justify-stretch"; // Main image/video container
    const TOGGLE_VISIBILITY_KEY = 'Backspace';
    const RIGHT_KEY = 'ArrowRight';
    const RIGHT_CODE = 'ArrowRight';
    const LEFT_KEY = 'ArrowLeft';
    const LEFT_CODE = 'ArrowLeft';
    const PRESS_COUNT = 3; // Number of times to press the first key in the sequence
    const DELAY_BETWEEN_PRESSES_MS = 0; // Delay between simulated keystrokes
    // --- End Configuration ---

    const HIDE_STYLE_ID = 'toggle-hide-elements-style-' + Math.random().toString(36).substring(7);
    const TEMP_HIDE_STYLE_ID = 'temp-hide-during-sim-style-' + Math.random().toString(36).substring(7); // ID for temporary style
    const TOGGLE_BUTTON_ID = 'civitai-toggle-ui-button-' + Math.random().toString(36).substring(7); // ID for the toggle button
    let isHidden = true; // Start hidden by default
    let styleElement = null;
    let tempHideStyleElement = null; // Style element for temporary hiding
    let isSimulating = false; // Flag to prevent rapid re-triggering
    let toggleButton = null; // Reference to the toggle button
    let urlObserver = null; // Observer for URL changes

    // URL pattern to match for showing the button
    const SHOW_BUTTON_URL_PATTERN = 'https://civitai.com/images/';

    // Combine selectors into a single CSS rule string for hiding
    const hideRule = `
        ${SELECTORS_TO_HIDE.join(',\n')} {
            display: none !important;
        }
    `;

    // CSS rule for temporarily hiding the main content
    const tempHideRule = `
        ${SELECTOR_TO_HIDE_DURING_SIMULATION} {
            opacity: 0 !important; /* Make transparent but keep interactable */
            pointer-events: none !important; /* Prevent accidental clicks on the invisible element */
        }
    `;

    // Style for the toggle button
    const buttonStyleRule = `
        #${TOGGLE_BUTTON_ID} {
            position: fixed;
            bottom: 10px; /* Changed from top */
            right: 10px;  /* Changed from left */
            z-index: 9999; /* Ensure it's on top */
            padding: 5px 10px;
            background-color: #4CAF50; /* Green */
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            opacity: 0.7;
            transition: opacity 0.2s ease-in-out;
        }
        #${TOGGLE_BUTTON_ID}:hover {
            opacity: 1;
        }
    `;

    // Function to check the URL and show/hide the toggle button
    function checkUrlAndToggleButton() {
        if (!toggleButton) return; // Button not initialized yet

        const currentUrl = window.location.href;
        if (currentUrl.startsWith(SHOW_BUTTON_URL_PATTERN)) {
            // console.log("Civitai Script: URL matches, showing toggle button."); // Optional: for debugging
            toggleButton.style.display = 'block'; // Or 'inline-block' or '' depending on original display
        } else {
            // console.log("Civitai Script: URL does not match, hiding toggle button."); // Optional: for debugging
            toggleButton.style.display = 'none';
        }
    }

    // Function to update the style element based on the isHidden state
    function updateVisibilityStyle() {
        if (!styleElement) return;
        if (isHidden) {
            styleElement.textContent = hideRule;
            if (toggleButton) toggleButton.textContent = "Show UI"; // Update button text
            console.log("Civitai Script: Hiding elements.");
        } else {
            styleElement.textContent = '';
            if (toggleButton) toggleButton.textContent = "Hide UI"; // Update button text
            console.log("Civitai Script: Showing elements.");
        }
    }

    // Function to toggle the visibility state
    function toggleVisibility() {
        isHidden = !isHidden;
        updateVisibilityStyle();
    }

    // Function to simulate a specific key press
    function simulateKeyPress(key, code) {
        // console.log(`Civitai Script: Simulating '${key}'...`); // Very verbose at 1ms
        const keyEvent = new KeyboardEvent('keydown', {
            key: key,
            code: code,
            bubbles: true,
            cancelable: true,
        });
        document.body.dispatchEvent(keyEvent);
    }

    // Recursive function to simulate multiple key presses with delay
    function simulateMultipleKeyPresses(key, code, remainingCount, callback) {
        if (remainingCount <= 0) {
            if (callback) callback();
            return;
        }
        simulateKeyPress(key, code);
        setTimeout(() => {
            simulateMultipleKeyPresses(key, code, remainingCount - 1, callback);
        }, DELAY_BETWEEN_PRESSES_MS);
    }

    // Function to start the simulation sequence (N times first key, N-1 times second key)
    function startSimulationSequence(firstKey, firstCode, secondKey, secondCode, count) {
        if (isSimulating) {
            console.log("Civitai Script: Simulation already in progress, ignoring trigger.");
            return;
        }
        isSimulating = true;
        const secondCount = Math.max(0, count - 1);
        console.log(`Civitai Script: Starting simulation: ${count}x ${firstKey} -> ${secondCount}x ${secondKey}`);

        // Apply temporary hide style
        if (tempHideStyleElement) {
            tempHideStyleElement.textContent = tempHideRule;
            console.log("Civitai Script: Temporarily hiding main content.");
        }

        simulateMultipleKeyPresses(firstKey, firstCode, count, () => {
            console.log(`Civitai Script: Finished ${firstKey} sequence. Starting ${secondKey} sequence...`);
            simulateMultipleKeyPresses(secondKey, secondCode, secondCount, () => {
                console.log("Civitai Script: Simulation sequence finished.");
                 // Remove temporary hide style
                if (tempHideStyleElement) {
                    tempHideStyleElement.textContent = '';
                     console.log("Civitai Script: Restoring main content visibility.");
                }
                isSimulating = false;
            });
        });
    }

    // Function to handle the initial setup and potential simulation trigger
    function initialize() {
        console.log(`Civitai Script: Loading for ${window.location.href}`);
        console.log(`Civitai Script: Press '${TOGGLE_VISIBILITY_KEY}' or click the top-left button (if visible) to toggle UI visibility.`);
        console.log(`Civitai Script: Press '${RIGHT_KEY}' or '${LEFT_KEY}' to trigger ${PRESS_COUNT}/${PRESS_COUNT - 1} simulations and temporarily hide content.`);

        try {
            // Add styles for hiding/showing UI elements
            styleElement = GM_addStyle('');
            styleElement.id = HIDE_STYLE_ID;

            // Initialize the temporary style element (initially empty)
            tempHideStyleElement = GM_addStyle('');
            tempHideStyleElement.id = TEMP_HIDE_STYLE_ID;

            // Add style for the toggle button
            GM_addStyle(buttonStyleRule);

            // Create the toggle button
            toggleButton = document.createElement('button');
            toggleButton.id = TOGGLE_BUTTON_ID;
            toggleButton.textContent = isHidden ? "Show UI" : "Hide UI"; // Set initial text
            toggleButton.addEventListener('click', toggleVisibility);
            toggleButton.style.display = 'none'; // Initially hide the button
            document.body.appendChild(toggleButton);

            updateVisibilityStyle(); // Apply initial hidden state & set initial button text
            checkUrlAndToggleButton(); // Check URL and set initial button visibility

            // --- URL Change Monitoring ---

            // 1. Listen for browser back/forward navigation
            window.addEventListener('popstate', checkUrlAndToggleButton);

            // 2. Observe DOM changes (common in SPAs for navigation)
            urlObserver = new MutationObserver((mutations) => {
                // We don't need to inspect mutations, just re-check the URL
                // Add a small debounce/throttle if performance becomes an issue
                checkUrlAndToggleButton();
            });

            urlObserver.observe(document.body, {
                childList: true, // Detect when elements are added/removed from body
                subtree: true    // Detect changes within the body's descendants
                // We don't need 'attributes' or 'characterData' for typical SPA navigation detection
            });

            // --- End URL Change Monitoring ---

            console.log(`Civitai Script: Style elements, toggle button, and URL observer initialized.`);
        } catch (e) {
             console.error(`Civitai Script: Error initializing:`, e);
             // Clean up observer if initialization failed partway
             if (urlObserver) urlObserver.disconnect();
             window.removeEventListener('popstate', checkUrlAndToggleButton);
             return; // Don't proceed if setup fails
        }
    }

    // --- Event Listener ---
    document.addEventListener('keydown', function(event) {
         if (isSimulating) {
             if (event.key === TOGGLE_VISIBILITY_KEY) {
                 if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;
                 const targetTagName = event.target.tagName.toUpperCase();
                 const isEditable = event.target.isContentEditable;
                 if (!(targetTagName === 'INPUT' || targetTagName === 'TEXTAREA' || targetTagName === 'SELECT' || isEditable)) {
                     toggleVisibility();
                 }
             }
            return;
        }

        if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;

        const targetTagName = event.target.tagName.toUpperCase();
        const isEditable = event.target.isContentEditable;
        if (targetTagName === 'INPUT' || targetTagName === 'TEXTAREA' || targetTagName === 'SELECT' || isEditable) {
             if (event.key === TOGGLE_VISIBILITY_KEY || event.key === RIGHT_KEY || event.key === LEFT_KEY) {
                 console.log(`Civitai Script: Key '${event.key}' pressed in editable field, ignoring.`);
             }
            return;
        }

        if (event.key === TOGGLE_VISIBILITY_KEY) {
            toggleVisibility();
        }
        else if (event.key === RIGHT_KEY) {
            event.preventDefault();
            event.stopPropagation();
            startSimulationSequence(RIGHT_KEY, RIGHT_CODE, LEFT_KEY, LEFT_CODE, PRESS_COUNT);
        }
        else if (event.key === LEFT_KEY) {
            event.preventDefault();
            event.stopPropagation();
            startSimulationSequence(LEFT_KEY, LEFT_CODE, RIGHT_KEY, RIGHT_CODE, PRESS_COUNT);
        }

    }, true);
    // --- End Event Listener ---

    // --- Run Initialization ---
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize(); // DOM is already ready
    }
    // --- End Run Initialization ---

})();