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

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

Verze ze dne 10. 06. 2025. Zobrazit nejnovější verzi.

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

})();