您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Wraps CivitAI versions into multiple rows, adds a search bar, and allows sorting by Date, Alphabetical, Popularity, and Downloads.
当前为
// ==UserScript== // @name Civitai Model Versions Wraparound + Search + Sort // @version 0.3.0 // @description Wraps CivitAI versions into multiple rows, adds a search bar, and allows sorting by Date, Alphabetical, Popularity, and Downloads. // @author // @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 --- let dateDict = {}; let generationDict = {}; // For popularity (generationCountAllTime) let downloadDict = {}; // For downloads (downloadCountAllTime) try { const nextData = JSON.parse(document.getElementById("__NEXT_DATA__").innerText); // find the query that contains "model","getById" const modelQuery = nextData?.props?.pageProps?.trpcState?.json?.queries ?.find(x => x.queryHash.includes('"model","getById"')); const modelVersions = modelQuery?.state?.data?.modelVersions ?? []; // Build dictionaries using version name as key (lowercase) for (const v of modelVersions) { const versionName = v.name.trim().toLowerCase(); dateDict[versionName] = new Date(v.publishedAt); generationDict[versionName] = v.rank?.generationCountAllTime ?? 0; downloadDict[versionName] = v.rank?.downloadCountAllTime ?? 0; } } 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'; searchInput.style.borderRadius = '4px'; searchInput.style.border = '1px solid #666'; searchInput.style.backgroundColor = '#2f2f2f'; searchInput.style.color = '#ddd'; // Clear button for search const clearBtn = document.createElement('button'); clearBtn.textContent = 'Clear'; styleButton(clearBtn); clearBtn.addEventListener('click', () => { searchInput.value = ''; searchInput.dispatchEvent(new Event('input')); }); // "Sort by:" label const sortLabel = document.createElement('span'); sortLabel.textContent = 'Sort by:'; // Sort dropdown (mode) const sortSelect = document.createElement('select'); sortSelect.style.padding = '4px'; sortSelect.style.borderRadius = '4px'; sortSelect.style.border = '1px solid #666'; sortSelect.style.backgroundColor = '#2f2f2f'; sortSelect.style.color = '#ddd'; const sortOptions = [ { value: 'default', text: 'Default' }, { value: 'date', text: 'Date' }, { value: 'alpha', text: 'Alphabetical' }, { value: 'pop', text: 'Popularity' }, { value: 'down', text: 'Downloads' }, ]; sortOptions.forEach(opt => { const optionEl = document.createElement('option'); optionEl.value = opt.value; optionEl.textContent = opt.text; sortSelect.appendChild(optionEl); }); // Asc/Desc toggle button let sortDirection = 'desc'; // default direction const toggleBtn = document.createElement('button'); styleButton(toggleBtn); // Mapping for toggle text based on mode and direction // Reversed the labeling so that ascending => "Most Overrated First" for pop/down // and descending => "Most Underrated First". const toggleTextMapping = { default: { asc: "Default", desc: "Default" }, date: { asc: "Newest First", desc: "Oldest First" }, alpha: { asc: "A–Z", desc: "Z–A" }, pop: { asc: "Most Overrated First", desc: "Most Underrated First" }, down: { asc: "Most Overrated First", desc: "Most Underrated First" }, }; // Function to update toggle button text based on current sort mode and direction function updateToggleText() { const mode = sortSelect.value; toggleBtn.textContent = toggleTextMapping[mode][sortDirection] || ""; } // Initialize toggle button text updateToggleText(); // Event for toggle button toggleBtn.addEventListener('click', () => { sortDirection = sortDirection === 'desc' ? 'asc' : 'desc'; updateToggleText(); applySorting(container, sortSelect.value, sortDirection); // Re-run search to maintain hidden items searchInput.dispatchEvent(new Event('input')); }); // Append controls controlPanel.appendChild(countLabel); controlPanel.appendChild(searchInput); controlPanel.appendChild(clearBtn); controlPanel.appendChild(sortLabel); controlPanel.appendChild(sortSelect); controlPanel.appendChild(toggleBtn); // 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 dropdown changed --- sortSelect.addEventListener('change', () => { updateToggleText(); // update toggle text when sort mode changes applySorting(container, sortSelect.value, sortDirection); // Re-run search filter to maintain hidden items searchInput.dispatchEvent(new Event('input')); }); } // Helper function to style buttons function styleButton(btn) { btn.style.padding = '4px 8px'; btn.style.borderRadius = '4px'; btn.style.border = '1px solid #666'; btn.style.backgroundColor = '#444'; btn.style.color = '#eee'; btn.style.cursor = 'pointer'; } // 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, direction) { if (!container) return; const items = [...container.children]; // Revert to original if "default" if (mode === 'default') { originalOrder.forEach(node => container.appendChild(node)); return; } // For sorting we use a multiplier: // In our sort functions, descending (default) is implemented as b - a. // Here, we multiply the natural comparison result by 1 if asc and -1 if desc. const multiplier = direction === 'asc' ? 1 : -1; items.sort((a, b) => { const aText = a.textContent.trim().toLowerCase(); const bText = b.textContent.trim().toLowerCase(); switch (mode) { case 'date': { const aDate = dateDict[aText] || new Date(0); const bDate = dateDict[bText] || new Date(0); // Default descending: newer first => (bDate - aDate) const diff = bDate - aDate; return multiplier * Math.sign(diff); } case 'alpha': { return multiplier * aText.localeCompare(bText); } case 'pop': { const aGen = generationDict[aText] || 0; const bGen = generationDict[bText] || 0; // Default descending: higher generation count first const diff = bGen - aGen; return multiplier * Math.sign(diff); } case 'down': { const aDown = downloadDict[aText] || 0; const bDown = downloadDict[bText] || 0; // Default descending: higher download count first const diff = bDown - aDown; return multiplier * Math.sign(diff); } default: 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或关注我们的公众号极客氢云获取最新地址