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

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

Versão de: 08/06/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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

})();