Civitai Model Versions Wraparound + Search + Sort

Wraps CivitAI versions into multiple rows, adds a search bar, and allows for sorting alphabetically.

当前为 2025-02-21 提交的版本,查看 最新版本

// ==UserScript==
// @name         Civitai Model Versions Wraparound + Search + Sort
// @version      0.2.1
// @description  Wraps CivitAI versions into multiple rows, adds a search bar, and allows for sorting alphabetically.
// @author       redtvpe
// @match        https://civitai.com/models/*
// @grant        none
// @namespace https://gf.qytechs.cn/users/1418032
// ==/UserScript==

(function () {
    'use strict';

    // --- 1) SELECT THE VERSION CONTAINER ---
    const scrollAreaSelector = '.mantine-ScrollArea-viewport .mantine-Group-root';
    let scrollArea = null;

    // --- 2) PARSE __NEXT_DATA__ FOR MODEL VERSIONS & BUILD A DATE DICTIONARY ---
    let dateDict = {};
    try {
        const nextData = JSON.parse(document.getElementById("__NEXT_DATA__").innerText);
        // Look in the queries array for the one that contains "model","getById"
        const modelQuery = nextData.props.pageProps.trpcState.json.queries
            .filter(x => x.queryHash.includes('"model","getById"'))[0];
        const modelVersions = modelQuery.state.data.modelVersions;
        // Build the dictionary: { versionName (lowercase) -> Date }
        dateDict = modelVersions.reduce((acc, v) => {
            acc[v.name.trim().toLowerCase()] = new Date(v.publishedAt);
            return acc;
        }, {});
    } catch (err) {
        console.warn("[Civitai] Could not parse modelVersions from __NEXT_DATA__:", err);
    }

    // --- 3) SAVE ORIGINAL ORDER ---
    let originalOrder = [];

    // --- 4) CREATE / UPDATE THE CONTROL PANEL ---
    function injectControls(container) {
        // Prevent duplicate insertion
        if (document.getElementById('civitaiVersionControls')) return;

        const controlPanel = document.createElement('div');
        controlPanel.id = 'civitaiVersionControls';
        controlPanel.style.marginBottom = '10px';
        controlPanel.style.display = 'flex';
        controlPanel.style.flexWrap = 'wrap';
        controlPanel.style.alignItems = 'center';
        controlPanel.style.gap = '10px';

        // Count label for visible versions
        const countLabel = document.createElement('span');
        countLabel.style.fontWeight = 'bold';
        updateCountLabel(countLabel, container);

        // Search input
        const searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.placeholder = 'Search versions...';
        searchInput.style.padding = '4px';

        // Clear button for search
        const clearBtn = document.createElement('button');
        clearBtn.textContent = 'Clear';
        clearBtn.style.padding = '4px 8px';
        clearBtn.addEventListener('click', () => {
            searchInput.value = '';
            searchInput.dispatchEvent(new Event('input'));
        });

        // "Sort by:" label
        const sortLabel = document.createElement('span');
        sortLabel.textContent = 'Sort by:';

        // Sort dropdown
        const sortSelect = document.createElement('select');
        sortSelect.style.padding = '4px';
        const options = [
            { value: 'default', text: 'Default' },
            { value: 'date-desc', text: 'Date (Newest First)' },
            { value: 'date-asc', text: 'Date (Oldest First)' },
            { value: 'alpha-asc', text: 'Alphabetical (A–Z)' },
            { value: 'alpha-desc', text: 'Alphabetical (Z–A)' },
        ];
        options.forEach(opt => {
            const optionEl = document.createElement('option');
            optionEl.value = opt.value;
            optionEl.textContent = opt.text;
            sortSelect.appendChild(optionEl);
        });

        // Append controls
        controlPanel.appendChild(countLabel);
        controlPanel.appendChild(searchInput);
        controlPanel.appendChild(clearBtn);
        controlPanel.appendChild(sortLabel);
        controlPanel.appendChild(sortSelect);

        // Insert control panel before the container
        container.parentNode.insertBefore(controlPanel, container);

        // --- Event: Search ---
        searchInput.addEventListener('input', () => {
            const query = searchInput.value.toLowerCase();
            const items = [...container.children];
            items.forEach(item => {
                const text = item.textContent.toLowerCase();
                item.style.display = text.includes(query) ? '' : 'none';
            });
            updateCountLabel(countLabel, container);
        });

        // --- Event: Sort ---
        sortSelect.addEventListener('change', () => {
            applySorting(container, sortSelect.value);
            // Re-run search filter to maintain hidden items
            searchInput.dispatchEvent(new Event('input'));
        });
    }

    // Helper: Update version count label based on visible buttons
    function updateCountLabel(labelEl, container) {
        const items = [...container.children];
        const visibleCount = items.filter(item => item.style.display !== 'none').length;
        labelEl.textContent = `Total Versions: ${visibleCount}`;
    }

    // --- 5) SORTING LOGIC ---
    function applySorting(container, mode) {
        if (!container) return;
        const items = [...container.children];

        if (mode === 'default') {
            // Revert to original order
            originalOrder.forEach(node => container.appendChild(node));
            return;
        }

        items.sort((a, b) => {
            const aText = a.textContent.trim().toLowerCase();
            const bText = b.textContent.trim().toLowerCase();
            if (mode === 'date-desc' || mode === 'date-asc') {
                const aDate = dateDict[aText] || new Date(0);
                const bDate = dateDict[bText] || new Date(0);
                const diff = bDate - aDate;
                return mode === 'date-desc' ? diff : -diff;
            } else if (mode === 'alpha-asc') {
                if (aText < bText) return -1;
                if (aText > bText) return 1;
                return 0;
            } else if (mode === 'alpha-desc') {
                if (aText > bText) return -1;
                if (aText < bText) return 1;
                return 0;
            }
            return 0;
        });

        items.forEach(item => container.appendChild(item));
    }

    // --- 6) MAIN LAYOUT ADJUSTMENT FUNCTION ---
    function adjustLayout() {
        scrollArea = document.querySelector(scrollAreaSelector);
        if (!scrollArea) return;

        // Wrap versions into multiple rows
        scrollArea.style.display = 'flex';
        scrollArea.style.flexWrap = 'wrap';
        scrollArea.style.gap = '8px';
        scrollArea.style.overflowX = 'visible';

        // Save original order on first run
        if (originalOrder.length === 0 && scrollArea.children.length > 0) {
            originalOrder = [...scrollArea.children];
        }

        injectControls(scrollArea);
    }

    // --- 7) SETUP MUTATION OBSERVER ---
    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                adjustLayout();
            }
        }
    });
    const body = document.querySelector('body');
    if (body) {
        observer.observe(body, { childList: true, subtree: true });
    }

    // --- 8) INITIAL RUN ---
    adjustLayout();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址