您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enlarges thumbnails and adds Up/Down arrow key navigation to instantly go to the next thumbnail. No more tired eyes from constantly moving your eyes from thumbnail to thumbnail.
// ==UserScript== // @name Civitai Larger Thumbnails + Arrow Key Navigation // @namespace Violentmonkey Scripts // @match *://*.civitai.com/* // @grant GM_addStyle // @version 1.1 // @author rainlizard // @license MIT // @description Enlarges thumbnails and adds Up/Down arrow key navigation to instantly go to the next thumbnail. No more tired eyes from constantly moving your eyes from thumbnail to thumbnail. // ==/UserScript== (function() { 'use strict'; let currentItemIndex = 0; let items = []; let gridContainer = null; let isInitialized = false; let headerObserver = null; // Function to wait for elements to appear function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver((mutations, obs) => { const element = document.querySelector(selector); if (element) { obs.disconnect(); resolve(element); } }); observer.observe(document, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); }); } // Function to calculate optimal thumbnail height function calculateThumbnailHeight() { // Get the actual viewport height const viewportHeight = window.innerHeight; // Check if header is visible by looking for common header selectors const headerSelectors = [ 'header', '[role="banner"]', '.header', '.navbar', '.nav-bar', '.top-bar', 'nav:first-of-type' ]; let headerHeight = 0; for (const selector of headerSelectors) { const header = document.querySelector(selector); if (header) { const rect = header.getBoundingClientRect(); // Only count the header if it's visible (not hidden/transformed off-screen) if (rect.top >= -10 && rect.height > 0) { headerHeight = Math.max(headerHeight, rect.height); } } } // Calculate available height (use full space) const availableHeight = viewportHeight - headerHeight; // Use 100% of available height (100vh) return availableHeight; } // Function to update thumbnail heights function updateThumbnailHeights() { const newHeight = calculateThumbnailHeight(); const heightValue = `${newHeight}px`; // Update CSS custom property document.documentElement.style.setProperty('--civitai-thumbnail-height', heightValue); // Also update existing cards directly items.forEach(item => { if (item.classList.contains('civitai-fullscreen-card')) { item.style.height = heightValue; item.style.minHeight = heightValue; } }); } // Function to find the grid container dynamically function findGridContainer() { // Look for common grid patterns const possibleSelectors = [ '[class*="grid"]', '[class*="Grid"]', '[style*="grid"]', '[style*="display: grid"]', '[style*="display:grid"]', 'div[class*="container"] > div[class*="grid"]', '#main [class*="grid"]', '.flex [class*="grid"]' ]; for (const selector of possibleSelectors) { const elements = document.querySelectorAll(selector); for (const element of elements) { // Check if this element contains what looks like model cards const children = element.children; if (children.length > 2) { // Must have multiple items // Look for image elements within children let hasImages = 0; for (let i = 0; i < Math.min(children.length, 5); i++) { if (children[i].querySelector('img')) { hasImages++; } } if (hasImages >= 2) { // At least 2 children with images return element; } } } } return null; } // Function to find all card items within the grid function findCardItems(container) { if (!container) return []; const children = Array.from(container.children); return children.filter(child => { // Check if this looks like a card container const hasCardClasses = child.classList.contains('relative') || child.querySelector('.AspectRatioImageCard_content__IGj_A'); // Check if it has a valid structure (either loaded content or loading placeholder) const hasContent = child.querySelector('.AspectRatioImageCard_content__IGj_A'); // Include if it has the card structure, regardless of whether content is loaded return hasCardClasses && hasContent; }); } function updateItemList() { if (!gridContainer) { gridContainer = findGridContainer(); } if (gridContainer) { items = findCardItems(gridContainer); if (currentItemIndex >= items.length && items.length > 0) { currentItemIndex = items.length - 1; } else if (items.length === 0) { currentItemIndex = 0; } } } function scrollToItem(index) { updateItemList(); if (index >= 0 && index < items.length) { currentItemIndex = index; const targetElement = items[currentItemIndex]; if (targetElement) { targetElement.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' }); } } } function handleKeyDown(event) { updateItemList(); if (items.length === 0) return; if (event.key === 'ArrowDown') { event.preventDefault(); if (currentItemIndex < items.length - 1) { scrollToItem(currentItemIndex + 1); } else { scrollToItem(items.length - 1); } } else if (event.key === 'ArrowUp') { event.preventDefault(); if (currentItemIndex > 0) { scrollToItem(currentItemIndex - 1); } else { scrollToItem(0); } } } function applyStyles() { // Calculate initial height accounting for headers const initialHeight = calculateThumbnailHeight(); document.documentElement.style.setProperty('--civitai-thumbnail-height', `${initialHeight}px`); GM_addStyle(` html, body { scroll-behavior: auto !important; } body { overflow-x: hidden !important; } /* Dynamic grid container styling - will be applied to detected grid */ .civitai-fullscreen-grid { display: flex !important; flex-direction: column !important; align-items: center !important; width: 100% !important; gap: 0px !important; grid-template-columns: none !important; } /* Dynamic card styling - will be applied to detected cards */ .civitai-fullscreen-card { width: 100vw !important; max-width: 100% !important; height: var(--civitai-thumbnail-height) !important; min-height: var(--civitai-thumbnail-height) !important; display: flex !important; flex-direction: column !important; justify-content: flex-start !important; align-items: stretch !important; padding: 0px !important; box-sizing: border-box !important; background-color: #1A1B1E !important; position: relative !important; margin: 0 !important; aspect-ratio: auto !important; transition: height 0.3s ease !important; } /* Style images within cards */ .civitai-fullscreen-card img { width: 100% !important; height: 100% !important; object-fit: contain !important; display: block !important; border-radius: 4px !important; } /* Style links within cards */ .civitai-fullscreen-card a { display: flex !important; flex-grow: 1 !important; height: 100% !important; justify-content: center !important; align-items: center !important; } `); } // Function to observe header visibility changes function observeHeaderChanges() { // Create a ResizeObserver to detect viewport changes const resizeObserver = new ResizeObserver(() => { updateThumbnailHeights(); }); resizeObserver.observe(document.body); // Also listen for scroll events to detect header hide/show let scrollTimeout; window.addEventListener('scroll', () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { updateThumbnailHeights(); }, 100); }); // Listen for window resize window.addEventListener('resize', () => { updateThumbnailHeights(); }); } function initializeFullscreen() { if (isInitialized) return; updateItemList(); if (gridContainer && items.length > 0) { // Apply classes to the grid container gridContainer.classList.add('civitai-fullscreen-grid'); // Apply classes to all card items items.forEach(item => { item.classList.add('civitai-fullscreen-card'); }); // Set up header observation observeHeaderChanges(); isInitialized = true; console.log(`Civitai script initialized with ${items.length} items`); } } // Function to periodically check for new content function checkForUpdates() { const newGridContainer = findGridContainer(); if (newGridContainer && newGridContainer !== gridContainer) { gridContainer = newGridContainer; isInitialized = false; initializeFullscreen(); } else if (gridContainer) { const newItems = findCardItems(gridContainer); if (newItems.length !== items.length) { // New items detected, reapply styles newItems.forEach(item => { if (!item.classList.contains('civitai-fullscreen-card')) { item.classList.add('civitai-fullscreen-card'); } }); items = newItems; // Update heights for new items updateThumbnailHeights(); } } } // Initialize the script function init() { applyStyles(); // Wait for the main content area to load waitForElement('#main') .then(() => { // Wait a bit more for dynamic content setTimeout(() => { initializeFullscreen(); // Set up periodic checks for new content setInterval(checkForUpdates, 2000); // Add keyboard event listener document.addEventListener('keydown', handleKeyDown); }, 2000); }) .catch(error => { console.error('Civitai script: Failed to find main content area', error); }); } // Start initialization after a delay to ensure page is ready setTimeout(init, 1000); })();