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 1.8
// @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';
function createTitleLink(cardElement) {
// 1. Find the title element (innermost span holding the text).
const titleSpan = cardElement.querySelector('.ant-card-head-title .ant-row span span');
if (!titleSpan) {
// If the specific inner span is not found, it might have already been replaced by our link.
// Check if the link already exists within the title container.
const titleContainer = cardElement.querySelector('.ant-card-head-title .ant-row > span');
if (titleContainer && titleContainer.querySelector('a.chub-card-title-link')) {
return; // Already processed
}
if(!titleSpan) return; // If still not found, bail.
}
// 2. Get the existing card link (the parent <a> of the whole card).
const cardLink = cardElement.closest('a');
if (!cardLink) {
console.error("Chub.ai: Could not find parent card link for title:", cardElement);
return;
}
// 3. Get the URL from the existing card link.
const cardURL = cardLink.href;
// 4. Create the new <a> element for the title.
const linkElement = document.createElement('a');
linkElement.href = cardURL;
linkElement.textContent = titleSpan.textContent.trim();
linkElement.target = '_blank';
linkElement.rel = 'noopener noreferrer';
linkElement.classList.add('chub-card-title-link');
// 5. Replace the title span with the link.
if (titleSpan.parentNode) {
titleSpan.parentNode.replaceChild(linkElement, titleSpan);
}
}
function addPagination() {
const buttonContainer = document.querySelector('.flex.justify-between.mt-4');
if (!buttonContainer) 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) { // Only style numeric current page
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;
}
const firstPageButton = createPageButton(1, 'First');
paginationContainer.appendChild(firstPageButton);
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++) {
const pageButton = createPageButton(i);
paginationContainer.appendChild(pageButton);
}
const randomButton = document.createElement('button');
randomButton.type = 'button';
randomButton.textContent = 'Random';
randomButton.classList.add('ant-btn', 'css-s6hibu', 'ant-btn-default', 'pagination-link');
randomButton.addEventListener('click', (event) => {
event.preventDefault();
const randomPage = Math.floor(Math.random() * assumedTotalPages) + 1;
const url = new URL(window.location.href);
url.searchParams.set('page', randomPage.toString());
window.location.href = url.toString();
});
paginationContainer.appendChild(randomButton);
const existingPagination = buttonContainer.querySelector('.pagination-container');
if (existingPagination) {
buttonContainer.replaceChild(paginationContainer, existingPagination);
} else {
buttonContainer.appendChild(paginationContainer);
}
}
const observer = new MutationObserver((mutations) => {
let cardsChanged = false;
let buttonsChanged = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('.ant-card.char-card-class') || node.querySelector('.ant-card.char-card-class')) {
cardsChanged = true;
}
if (node.matches('.flex.justify-between.mt-4') || node.querySelector('.flex.justify-between.mt-4')) {
buttonsChanged = true;
}
}
});
});
if (cardsChanged) {
document.querySelectorAll('.ant-card.char-card-class').forEach(createTitleLink);
}
if (buttonsChanged) {
addPagination();
}
});
observer.observe(document.body, { childList: true, subtree: true });
let lastUrl = window.location.href;
const urlObserver = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
lastUrl = window.location.href;
setTimeout(addPagination, 100);
}
});
urlObserver.observe(document, {childList: true, subtree: true});
document.querySelectorAll('.ant-card.char-card-class').forEach(createTitleLink);
addPagination();
const styleElement = document.createElement('style');
styleElement.textContent = `
.chub-card-title-link {
color: white; /* Unvisited */
}
.chub-card-title-link:visited {
color: yellow; /* Visited */
}
.pagination-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination-container .pagination-link { /* Applied to all buttons in pagination */
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;
}
/* --- Styles for bigger tag scrollbar --- */
div.custom-scroll:hover::-webkit-scrollbar {
height: 8px !important; /* Increase height from site's 2px to 8px */
/* The site CSS already positions it with 'position: absolute; bottom: 0;' which is fine. */
}
div.custom-scroll:hover::-webkit-scrollbar-thumb {
background-color: #888 !important; /* A more visible scrollbar thumb color */
border-radius: 4px !important; /* Rounded corners for the thumb */
}
/* Optional: Add a subtle track background for the scrollbar area when hovering the tag container */
div.custom-scroll:hover::-webkit-scrollbar-track {
background-color: rgba(50, 50, 50, 0.2) !important; /* Light grey track background */
border-radius: 4px !important;
}
`;
document.head.appendChild(styleElement);
})();