您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Makes Chub.ai card titles into links (copying existing link), styles them, and adds pagination.
// ==UserScript== // @name Improved Chub.ai (Card Title Links, Styling, and Pagination) // @namespace http://tampermonkey.net/ // @version 2.0 // @description Makes Chub.ai card titles into links (copying existing link), styles them, and adds pagination. // @author Marcal91 // @match https://chub.ai/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Core Functions to Enhance Page Elements --- function createTitleLink(cardElement) { const titleContainer = cardElement.querySelector('.ant-card-head-title .ant-row > span'); if (!titleContainer || titleContainer.querySelector('a.chub-card-title-link')) { return; } const titleTextSpan = titleContainer.querySelector('span'); const cardLink = cardElement.closest('a'); if (!titleTextSpan || !cardLink) return; const cardURL = cardLink.href; const linkElement = document.createElement('a'); linkElement.href = cardURL; linkElement.textContent = titleTextSpan.textContent.trim(); linkElement.target = '_blank'; linkElement.rel = 'noopener noreferrer'; linkElement.classList.add('chub-card-title-link'); titleContainer.replaceChild(linkElement, titleTextSpan); } function convertAllTagsOnPage() { const tagWrappers = document.querySelectorAll('span.cursor-pointer:has(> .ant-tag)'); tagWrappers.forEach(wrapper => { if (wrapper.parentElement.tagName.toLowerCase() === 'a') return; const tagTextSpan = wrapper.querySelector('.ant-tag > span:first-child'); if (!tagTextSpan || !tagTextSpan.textContent) return; const tagName = tagTextSpan.textContent.trim(); const tagURL = `https://chub.ai/characters?tags=${encodeURIComponent(tagName)}`; const linkElement = document.createElement('a'); linkElement.href = tagURL; linkElement.rel = 'noopener noreferrer'; wrapper.parentNode.insertBefore(linkElement, wrapper); linkElement.appendChild(wrapper); }); } function addPagination() { const buttonContainer = document.querySelector('.flex.justify-between.mt-4'); if (!buttonContainer) return; // **STABILITY FIX**: If our pagination already exists, don't rebuild it. if (buttonContainer.querySelector('.pagination-container')) return; const currentURL = new URL(window.location.href); let currentPage = parseInt(currentURL.searchParams.get('page') || '1', 10); const paginationContainer = document.createElement('div'); paginationContainer.classList.add('pagination-container'); function createPageButton(pageNumber, text) { const pageButton = document.createElement('button'); pageButton.type = 'button'; pageButton.textContent = text || pageNumber.toString(); pageButton.classList.add('ant-btn', 'css-s6hibu', 'ant-btn-default', 'pagination-link'); if (!text && pageNumber === currentPage) { pageButton.classList.add('current-page'); } pageButton.addEventListener('click', (event) => { event.preventDefault(); const url = new URL(window.location.href); url.searchParams.set('page', pageNumber.toString()); window.location.href = url.toString(); }); return pageButton; } paginationContainer.appendChild(createPageButton(1, 'First')); let startPage = Math.max(1, currentPage - 2); let endPage = startPage + 4; const assumedTotalPages = 1000; if (endPage > assumedTotalPages) { endPage = assumedTotalPages; startPage = Math.max(1, endPage - 4); } startPage = Math.max(1, startPage); for (let i = startPage; i <= endPage; i++) { paginationContainer.appendChild(createPageButton(i)); } const goToContainer = document.createElement('div'); goToContainer.classList.add('go-to-container'); const pageInput = document.createElement('input'); pageInput.type = 'number'; pageInput.placeholder = 'Go to...'; pageInput.min = '1'; pageInput.classList.add('pagination-input'); const goButton = document.createElement('button'); goButton.type = 'button'; goButton.textContent = 'Go'; goButton.classList.add('ant-btn', 'css-s6hibu', 'ant-btn-default', 'pagination-link'); const navigateToPage = () => { const pageNum = pageInput.value; if (pageNum && pageNum >= 1) { const url = new URL(window.location.href); url.searchParams.set('page', pageNum); window.location.href = url.toString(); } }; goButton.addEventListener('click', navigateToPage); pageInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); navigateToPage(); } }); goToContainer.appendChild(pageInput); goToContainer.appendChild(goButton); paginationContainer.appendChild(goToContainer); const nextButton = buttonContainer.querySelector('button:last-child'); buttonContainer.insertBefore(paginationContainer, nextButton); } // --- Main Execution Logic --- function processAllEnhancements() { document.querySelectorAll('.ant-card.char-card-class').forEach(createTitleLink); convertAllTagsOnPage(); addPagination(); } let debounceTimer; const observer = new MutationObserver((mutations) => { // We only care if nodes are added or removed. const hasRelevantChanges = mutations.some(m => m.addedNodes.length > 0 || m.removedNodes.length > 0); if (hasRelevantChanges) { clearTimeout(debounceTimer); debounceTimer = setTimeout(processAllEnhancements, 150); // Shorter delay now } }); observer.observe(document.body, { childList: true, subtree: true }); let lastUrl = window.location.href; setInterval(() => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; // The main observer will handle DOM changes, but we specifically // need to re-check pagination if the URL changes without a major DOM update. // This is a backup check. setTimeout(addPagination, 150); } }, 500); // Check URL every 500ms // --- Initial Setup --- // Use a small delay on initial load to let the site's own scripts finish first. setTimeout(processAllEnhancements, 500); // --- CSS Styles --- const styleElement = document.createElement('style'); styleElement.textContent = ` .chub-card-title-link { color: white; } .chub-card-title-link:visited { color: yellow; } .pagination-container { display: flex; align-items: center; gap: 0.5rem; } .pagination-container .pagination-link { background-color: #141414 !important; border: 1px solid #424242 !important; color: rgba(242,228,214,0.85) !important; min-width: 32px; height: 32px; padding: 0px 15px !important; line-height: 1.5 !important; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px !important; } .pagination-container .pagination-link:not(.current-page):hover { background-color: #1f1f1f !important; border-color: #5852a5 !important; color: #5852a5 !important; } .pagination-container .pagination-link.current-page { background-color: #2e2b74 !important; border-color: #5852a5 !important; color: white !important; font-weight: bold; } .go-to-container { display: flex; gap: 0.25rem; } .pagination-input { width: 80px; height: 32px; padding: 4px 11px; background: #141414; border: 1px solid #424242; border-radius: 6px; color: rgba(242,228,214,0.85); font-size: 14px; } .pagination-input:focus { border-color: #5852a5; outline: none; } .pagination-input::-webkit-outer-spin-button, .pagination-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .pagination-input { -moz-appearance: textfield; } div.custom-scroll:hover::-webkit-scrollbar { height: 8px !important; } div.custom-scroll:hover::-webkit-scrollbar-thumb { background-color: #888 !important; border-radius: 4px !important; } div.custom-scroll:hover::-webkit-scrollbar-track { background-color: rgba(50, 50, 50, 0.2) !important; border-radius: 4px !important; } `; document.head.appendChild(styleElement); })();