Civitai - Restore Censored Models

Automates clicking browsing mode options (X/XXX) on civitai.com, handles initial states correctly, waits for grid container & updates, and restores potentially missing grid items, ensuring X/XXX are ON at the end.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Civitai - Restore Censored Models
// @namespace    http://www.facebook.com/Tophness
// @version      1.2.5
// @description  Automates clicking browsing mode options (X/XXX) on civitai.com, handles initial states correctly, waits for grid container & updates, and restores potentially missing grid items, ensuring X/XXX are ON at the end.
// @author       Chris Malone
// @match        https://civitai.com/models*
// @match        https://civitai.com/user/*/models*
// @match        https://civitai.com/search/models*
// @match        https://www.civitai.com/models*
// @match        https://www.civitai.com/user/*/models*
// @match        https://www.civitai.com/search/models*
// @license      MIT
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';
    const FIRST_TARGET_SELECTOR = '.flex.items-center.gap-3 .mantine-UnstyledButton-root.mantine-ActionIcon-root:nth-child(3)';
    const BROWSING_MODE_CONTAINER_SELECTOR = '#browsing-mode';
    const GRID_CONTAINER_SELECTOR = 'div[style*="grid-template-columns"]';
    const ITEM_SELECTOR_INSIDE_CONTAINER = ':scope > div';
    const LABELS_TO_TARGET = ["X", "XXX"];
    const CONTAINER_WAIT_TIMEOUT_MS = 20000;
    const MUTATION_WAIT_TIMEOUT_MS = 20000;
    const MUTATION_INACTIVITY_DELAY_MS = 1750;
    const SCRIPT_START_DELAY_MS = 1500;
    const CLICK_DELAY_MS = 150;
    function waitForElement(selector, parent = document.body, timeout = 30000) {
        return new Promise((resolve) => {
            const existingElement = parent.querySelector(selector);
            if (existingElement) {
                return resolve(existingElement);
            }
            let observer;
            const timer = setTimeout(() => {
                if (observer) {
                    observer.disconnect();
                }
                console.error(`Timeout waiting for element: ${selector} within parent:`, parent);
                resolve(null);
            }, timeout);
            observer = new MutationObserver((mutations) => {
                const targetElement = parent.querySelector(selector);
                if (targetElement) {
                    clearTimeout(timer);
                    observer.disconnect();
                    resolve(targetElement);
                }
            });
            observer.observe(parent || document.body, { childList: true, subtree: true });
        });
    }
    function waitForGridUpdate(gridContainer) {
        console.log("Waiting for grid item updates to stabilize within container:", gridContainer);
        return new Promise((resolve) => {
            if (!gridContainer || !(gridContainer instanceof Element)) {
                 console.error("waitForGridUpdate called with invalid gridContainer:", gridContainer);
                 return resolve(false);
            }
            let Gtimer = null;
            let observer;
            const overallTimeout = setTimeout(() => {
                console.error(`Grid item update stabilization timed out after ${MUTATION_WAIT_TIMEOUT_MS / 1000}s.`);
                if (observer) observer.disconnect();
                clearTimeout(Gtimer);
                resolve(false);
            }, MUTATION_WAIT_TIMEOUT_MS);
            const mutationCallback = (mutationsList) => {
                 const relevantMutations = mutationsList.some(mutation => mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0));
                 if (!relevantMutations) return;
                clearTimeout(Gtimer);
                Gtimer = setTimeout(() => {
                    console.log(`Grid item updates stabilized after ${MUTATION_INACTIVITY_DELAY_MS}ms of inactivity.`);
                    if (observer) observer.disconnect();
                    clearTimeout(overallTimeout);
                    resolve(true);
                }, MUTATION_INACTIVITY_DELAY_MS);
            };
            observer = new MutationObserver(mutationCallback);
            observer.observe(gridContainer, { childList: true });
            Gtimer = setTimeout(() => {
                 console.log(`No relevant grid item mutations detected, assuming stable after initial ${MUTATION_INACTIVITY_DELAY_MS}ms wait.`);
                 if (observer) observer.disconnect();
                 clearTimeout(overallTimeout);
                 resolve(true);
            }, MUTATION_INACTIVITY_DELAY_MS);
        });
    }
    async function findAndSetBrowsingModeOptions(parentSelector, targetLabels, mode) {
        console.log(`Looking for browsing mode container: ${parentSelector} to set labels [${targetLabels.join(', ')}] to '${mode}'`);
        const parentElement = await waitForElement(parentSelector, document.body, 15000);
        if (!parentElement) {
            console.error('Browsing mode container not found:', parentSelector);
            return false;
        }
        const potentialLabels = parentElement.querySelectorAll('label');
        console.log(`Found ${potentialLabels.length} potential label elements under ${parentSelector}`);
        let clickedSomething = false;
        if (potentialLabels.length === 0) {
             console.error(`No labels found under ${parentSelector}. Cannot set options.`);
             return false;
        }
        for (const label of potentialLabels) {
            let labelText = '';
            const labelClone = label.cloneNode(true);
            const inputInClone = labelClone.querySelector('input');
            if (inputInClone) inputInClone.remove();
            labelText = labelClone.textContent?.trim();
            if (labelText && targetLabels.includes(labelText)) {
                console.log(`Found label with text "${labelText}"`);
                const isChecked = label.getAttribute('data-checked') === 'true';
                console.log(`Label "${labelText}" current state (data-checked): ${isChecked}`);
                let shouldClick = false;
                if (mode === 'ensure_on' && !isChecked) {
                    console.log(`Need to turn ON "${labelText}".`);
                    shouldClick = true;
                } else if (mode === 'ensure_off' && isChecked) {
                    console.log(`Need to turn OFF "${labelText}".`);
                    shouldClick = true;
                } else {
                    console.log(`Label "${labelText}" is already in the desired state ('${mode}' requires checked=${mode === 'ensure_on'}). No click needed.`);
                }
                if (shouldClick) {
                    console.log(`Clicking label for "${labelText}"`);
                    if (typeof label.click === 'function') {
                        label.click();
                        clickedSomething = true;
                        await new Promise(resolve => setTimeout(resolve, CLICK_DELAY_MS));
                    } else {
                        console.error(`Label element for "${labelText}" found, but cannot 'click' it:`, label);
                        const input = label.querySelector('input[type="checkbox"]') || document.getElementById(label.getAttribute('for'));
                         if (input && typeof input.click === 'function') {
                             console.log(`Falling back to clicking input for "${labelText}"`);
                             input.click();
                             clickedSomething = true;
                             await new Promise(resolve => setTimeout(resolve, CLICK_DELAY_MS));
                         } else {
                             console.error(`Fallback input click also not possible for "${labelText}".`);
                         }
                    }
                }
            }
        }
        if (!clickedSomething && mode === 'ensure_on') console.log("All target labels were already ON.");
        if (!clickedSomething && mode === 'ensure_off') console.log("All target labels were already OFF.");
        return true;
    }
    function findGridItemsInContainer(gridContainer) {
        if (!gridContainer || !(gridContainer instanceof Element)) {
            console.error('findGridItemsInContainer called with invalid container:', gridContainer);
            return null;
        }
        const items = gridContainer.querySelectorAll(ITEM_SELECTOR_INSIDE_CONTAINER);
        return items;
    }
    async function runCivitaiAutomation() {
        console.log("Civitai Enhancer v1.2.3 (State Aware): Starting automation sequence...");
        console.log("--- Step 1: Open Panel ---");
        const firstTarget = await waitForElement(FIRST_TARGET_SELECTOR);
        if (!firstTarget) {
            console.error("Initial target element (panel toggle) not found. Aborting script.");
            return;
        }
        console.log(`Waiting for grid container (${GRID_CONTAINER_SELECTOR}) after ensuring OFF...`);
        let gridContainerElement = await waitForElement(GRID_CONTAINER_SELECTOR, document.body, CONTAINER_WAIT_TIMEOUT_MS);
        if (!gridContainerElement) {
             console.error("Grid container did not appear after ensuring OFF within timeout. Aborting.");
             return;
        }
        console.log("Grid container found:", gridContainerElement);
        console.log("Clicking initial target to ensure panel is open:", firstTarget);
        firstTarget.click();
        await new Promise(resolve => setTimeout(resolve, 300));
        console.log("--- Step 2: Ensure X/XXX are OFF ---");
        const turnedOffOptions = await findAndSetBrowsingModeOptions(BROWSING_MODE_CONTAINER_SELECTOR, LABELS_TO_TARGET, 'ensure_off');
        if (!turnedOffOptions) {
             console.error("Failed to find browsing mode container to turn options OFF. Aborting.");
             return;
        }
        const gridStable1 = await waitForGridUpdate(gridContainerElement);
        if (!gridStable1) {
             console.warn("Grid items did not stabilize after ensuring OFF. Item capture might be unreliable.");
        } else {
             console.log("Grid items finished updating (All Items State).");
        }
        const allGridItemsNodelist = findGridItemsInContainer(gridContainerElement);
        const allGridItems = allGridItemsNodelist ? Array.from(allGridItemsNodelist) : [];
        const allItemIds = new Set(allGridItems.map(item => item.id).filter(id => id));
        console.log(`Stored ${allGridItems.length} grid items (potentially including hidden ones).`);
        if (allGridItems.length === 0) {
            console.warn("No grid items found while X/XXX were off. Restoration might not work correctly.");
        }
         allGridItems.forEach((item, index) => {
            if (!item.id) {
            }
        });
        console.log("--- Step 3: Ensure X/XXX are ON ---");
        const browsingPanelVisible = document.querySelector(BROWSING_MODE_CONTAINER_SELECTOR);
        if (!browsingPanelVisible) {
            console.log("Browsing panel seems closed, reopening...");
            const targetToReopen = await waitForElement(FIRST_TARGET_SELECTOR);
            if (targetToReopen) {
                 targetToReopen.click();
                 await new Promise(resolve => setTimeout(resolve, 300));
            } else {
                 console.error("Could not find button to reopen panel. Aborting.");
                 return;
            }
        }
        const turnedOnOptions = await findAndSetBrowsingModeOptions(BROWSING_MODE_CONTAINER_SELECTOR, LABELS_TO_TARGET, 'ensure_on');
         if (!turnedOnOptions) {
             console.error("Failed to find browsing mode container to turn options ON. Aborting restoration.");
             return;
         }
        console.log(`Waiting for grid container (${GRID_CONTAINER_SELECTOR}) after ensuring ON...`);
        gridContainerElement = await waitForElement(GRID_CONTAINER_SELECTOR, document.body, CONTAINER_WAIT_TIMEOUT_MS);
        if (!gridContainerElement) {
             console.error("Grid container did not appear after ensuring ON within timeout. Aborting comparison.");
             return;
        }
        console.log("Grid container found:", gridContainerElement);
        const gridStable2 = await waitForGridUpdate(gridContainerElement);
        if (!gridStable2) {
             console.warn("Grid items did not stabilize after ensuring ON. Item restoration might be unreliable.");
        } else {
             console.log("Grid items finished updating (Final State).");
        }
        const currentGridItemsNodelist = findGridItemsInContainer(gridContainerElement);
        const currentGridItems = currentGridItemsNodelist ? Array.from(currentGridItemsNodelist) : [];
        const currentItemIds = new Set(currentGridItems.map(item => item.id).filter(id => id));
        console.log(`Found ${currentGridItems.length} current grid items after ensuring ON.`);
        console.log("--- Step 4: Comparing and Restoring ---");
        if (allGridItems.length === 0) {
             console.log("No initial 'all items' were stored (X/XXX off state), skipping restoration.");
        } else {
            const missingItems = [];
            allGridItems.forEach(initialItem => {
                if (initialItem.id && !currentItemIds.has(initialItem.id)) {
                     const elementInDoc = document.getElementById(initialItem.id);
                     const elementInCurrentGrid = gridContainerElement.querySelector(`#${CSS.escape(initialItem.id)}`);
                     if (!elementInCurrentGrid) {
                         console.log(`Detected missing item (ID: ${initialItem.id}). Preparing to restore.`);
                         missingItems.push(initialItem);
                     } else {
                          console.warn(`Item (ID: ${initialItem.id}) seems present in grid container DOM but wasn't in the initial NodeList query. Skipping restore for safety.`);
                     }
                }
            });
            if (missingItems.length > 0) {
                console.log(`Found ${missingItems.length} items missing from the final grid state. Attempting to re-add them.`);
                if (gridContainerElement) {
                    missingItems.forEach(item => {
                        console.log(`Re-adding item (ID: ${item.id || 'No ID'})`);
                        if (item.parentElement) {
                            console.log(`Item ${item.id} is still attached to DOM (parent: ${item.parentElement.tagName}), moving to grid.`);
                            gridContainerElement.appendChild(item);
                        } else {
                             console.log(`Item ${item.id} seems detached from DOM, appending to grid.`);
                             gridContainerElement.appendChild(item);
                        }
                    });
                    console.log("Restoration attempt complete.");
                } else {
                    console.error("Cannot restore missing items because the grid container was not found at the restoration stage.");
                }
            } else {
                console.log("No missing items (with IDs) detected between the 'all items' state and the final state.");
            }
        }
        const finalTarget = document.querySelector(FIRST_TARGET_SELECTOR);
        const finalPanel = document.querySelector(BROWSING_MODE_CONTAINER_SELECTOR);
        if (finalTarget && finalPanel) {
            finalTarget.click();
        }
    }
    setTimeout(runCivitaiAutomation, SCRIPT_START_DELAY_MS);
})();