你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
(我已經安裝了使用者樣式管理器,讓我安裝!)
// ==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);
})();