Improved Chub.ai (Card Title Links, Styling, and Pagination)

Makes Chub.ai card titles into links (copying existing link), styles them, and adds pagination.

目前為 2025-06-08 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();