您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a responsive grid gallery layout for the kemono.su thumbnails, using the first attachment image file as the cover
当前为
// ==UserScript== // @name Kemono Grid Gallery Layout // @namespace https://greasyfork.org/users/172087 // @version 0.3 // @description Add a responsive grid gallery layout for the kemono.su thumbnails, using the first attachment image file as the cover // @author Neko_Aria // @icon https://kemono.su/static/favicon.ico // @match https://kemono.su/* // @grant GM_addStyle // @license MIT // ==/UserScript== (function () { "use strict"; // Constants definition const CONSTANTS = { API_BASE_URL: "https://kemono.su/api/v1", IMAGE_BASE_URL: "https://img.kemono.su/thumbnail", SUPPORTED_IMAGE_EXTENSIONS: [ ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ], GRID_MAX_WIDTH: "1600px", GRID_MIN_COLUMN_WIDTH: "250px", GRID_GAP: "16px", }; // Add grid gallery layout styles GM_addStyle(` .card-list__items { display: grid !important; grid-template-columns: repeat(auto-fill, minmax(${CONSTANTS.GRID_MIN_COLUMN_WIDTH}, 1fr)); gap: ${CONSTANTS.GRID_GAP}; padding: ${CONSTANTS.GRID_GAP}; width: 100%; max-width: ${CONSTANTS.GRID_MAX_WIDTH}; margin: 0 auto; grid-auto-rows: auto; } .post-card { width: 100% !important; margin: 0 !important; break-inside: avoid; background: rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; height: auto !important; transition: transform 0.2s ease; } .post-card:hover { transform: translateY(-2px); } .post-card__image-container { position: relative; width: 100%; height: auto !important; } .post-card__image { width: 100%; height: 100%; object-fit: cover; display: block; } .loading-overlay { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 20px; border-radius: 8px; z-index: 9999; display: flex; align-items: center; } .loading-spinner { width: 20px; height: 20px; border: 3px solid #fff; border-radius: 50%; border-top-color: transparent; animation: spin 1s linear infinite; margin-right: 10px; } @keyframes spin { to { transform: rotate(360deg); } } `); // Utility functions const utils = { // Creates and returns a loading overlay element createLoadingOverlay() { const overlay = document.createElement("div"); overlay.className = "loading-overlay"; overlay.innerHTML = ` <div class="loading-spinner"></div> <span>Loading images...</span> `; document.body.appendChild(overlay); return overlay; }, // Extracts service and user ID from the current URL parseUrlParams() { const path = window.location.pathname; const matches = path.match(/^\/([^\/]+)\/user\/(\d+)/); return matches ? { service: matches[1], userId: matches[2], } : null; }, // Checks if a given path ends with a supported image extension isImageFile(path) { return ( path && CONSTANTS.SUPPORTED_IMAGE_EXTENSIONS.some((ext) => path.toLowerCase().endsWith(ext) ) ); }, // Creates a promise that resolves when an image loads or errors createImageLoadPromise(imgElement) { return new Promise((resolve) => { imgElement.onload = resolve; imgElement.onerror = resolve; }); }, }; // Main gallery class class KemonoGallery { constructor() { this.grid = document.querySelector(".card-list__items"); this.postAttachments = null; this.isInitialized = false; this.loadingOverlay = null; this.imageLoadPromises = []; } // Initialize the gallery async init() { if (!this.grid) return; this.grid.style.removeProperty("--card-size"); // Observe additions of .post-card elements const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.classList && node.classList.contains("post-card")) { this.processPostCard(node); } }); }); }); observer.observe(this.grid, { childList: true, subtree: true, }); // Initialize data await this.initializeData(); // Process existing cards const existingCards = this.grid.querySelectorAll(".post-card"); const processPromises = Array.from(existingCards).map((card) => this.processPostCard(card) ); // Wait for all images to load await Promise.all(this.imageLoadPromises); // Remove loading overlay after all images are loaded if (this.loadingOverlay) { this.loadingOverlay.remove(); } } async initializeData() { this.loadingOverlay = utils.createLoadingOverlay(); const urlParams = utils.parseUrlParams(); if (!urlParams) { this.loadingOverlay.remove(); return; } try { const posts = await this.fetchPostsData(urlParams); this.postAttachments = this.createAttachmentsMap(posts); this.isInitialized = true; } catch (error) { console.error("Failed to initialize data:", error); this.loadingOverlay.remove(); } } // Process a single post-card element async processPostCard(card) { if (!this.isInitialized) return; const link = card.querySelector("a.image-link"); if (!link) return; const postId = link.href.split("/").pop(); const attachmentPath = this.postAttachments.get(postId); const imgElement = card.querySelector(".post-card__image"); if (attachmentPath && imgElement && utils.isImageFile(attachmentPath)) { imgElement.src = `${CONSTANTS.IMAGE_BASE_URL}${attachmentPath}`; const loadPromise = utils.createImageLoadPromise(imgElement); this.imageLoadPromises.push(loadPromise); await loadPromise; } } // Fetches posts data from the API async fetchPostsData({ service, userId }) { const response = await fetch( `${CONSTANTS.API_BASE_URL}/${service}/user/${userId}` ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // Creates a map of post IDs to their first attachment path createAttachmentsMap(posts) { return new Map( posts.map((post) => [ post.id, post.attachments?.[0]?.path || post.file?.path, ]) ); } } // Initialize gallery immediately const gallery = new KemonoGallery(); gallery.init(); })();