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

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();