Chub.ai Universal Enhancer (Links, Styles, Pagination, Tooltips)

Makes card titles/tags proper links, styles them, adds pagination, fixes description cutoff, and adds floating title tooltips.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Chub.ai Universal Enhancer (Links, Styles, Pagination, Tooltips)
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Makes card titles/tags proper links, styles them, adds pagination, fixes description cutoff, and adds floating title tooltips.
// @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; // Already processed or no container
        }

        const titleTextSpan = titleContainer.querySelector('span');
        const cardLink = cardElement.closest('a');
        if (!titleTextSpan || !cardLink) return;

        const fullTitleText = titleTextSpan.textContent.trim();
        const cardURL = cardLink.href;

        const linkElement = document.createElement('a');
        linkElement.href = cardURL;
        linkElement.textContent = fullTitleText;
        linkElement.target = '_blank';
        linkElement.rel = 'noopener noreferrer';
        linkElement.classList.add('chub-card-title-link');

        // Store the full title in a data attribute for our tooltip to read
        linkElement.dataset.fullTitle = fullTitleText;

        titleTextSpan.parentNode.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 || 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);
    }

    // --- Custom Tooltip Logic ---
    function setupGlobalTooltip() {
        // Create the tooltip element
        const tooltip = document.createElement('div');
        tooltip.id = 'chub-custom-tooltip';
        tooltip.style.display = 'none';
        document.body.appendChild(tooltip);

        // Event delegation: Listen for mouse events on the document
        document.addEventListener('mouseover', (e) => {
            // Check if the hovered element (or its parent) is our title link
            const target = e.target.closest('.chub-card-title-link');
            if (target && target.dataset.fullTitle) {
                tooltip.textContent = target.dataset.fullTitle;
                tooltip.style.display = 'block';
            }
        });

        document.addEventListener('mouseout', (e) => {
            const target = e.target.closest('.chub-card-title-link');
            if (target) {
                tooltip.style.display = 'none';
            }
        });

        document.addEventListener('mousemove', (e) => {
            // If tooltip is visible, update position
            if (tooltip.style.display === 'block') {
                // Position slightly below and to the right of cursor
                const offset = 15;
                let top = e.clientY + offset;
                let left = e.clientX + offset;

                // Boundary checks to keep it on screen
                if (left + tooltip.offsetWidth > window.innerWidth) {
                    left = window.innerWidth - tooltip.offsetWidth - offset;
                }
                if (top + tooltip.offsetHeight > window.innerHeight) {
                    top = e.clientY - tooltip.offsetHeight - offset;
                }

                tooltip.style.top = `${top}px`;
                tooltip.style.left = `${left}px`;
            }
        });
    }

    // --- Main Execution Logic ---

    function processAllEnhancements() {
        document.querySelectorAll('.ant-card.char-card-class').forEach(createTitleLink);
        convertAllTagsOnPage();
        addPagination();
    }

    let debounceTimer;
    const observer = new MutationObserver(() => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(processAllEnhancements, 150);
    });
    observer.observe(document.body, { childList: true, subtree: true });

    let lastUrl = window.location.href;
    setInterval(() => {
        if (window.location.href !== lastUrl) {
            lastUrl = window.location.href;
            setTimeout(addPagination, 150);
        }
    }, 500);

    // --- Initial Setup ---
    setTimeout(() => {
        processAllEnhancements();
        setupGlobalTooltip(); // Initialize tooltip once
    }, 500);

    // --- CSS Styles ---
    const styleElement = document.createElement('style');
    styleElement.textContent = `
        /* Visited link styling for card titles */
        .chub-card-title-link { color: white; }
        .chub-card-title-link:visited { color: yellow; }

        /* --- Tooltip Styling --- */
        #chub-custom-tooltip {
            position: fixed;
            background-color: #1f1f1f; /* Dark background */
            color: #e5e0d8;            /* Text color */
            border: 1px solid #5852a5; /* Purple border matching buttons */
            padding: 6px 12px;
            border-radius: 6px;
            font-size: 0.9rem;
            z-index: 10000;            /* Ensure it's on top of everything */
            pointer-events: none;      /* Let clicks pass through */
            box-shadow: 0 4px 6px rgba(0,0,0,0.3);
            max-width: 400px;
            word-wrap: break-word;
        }

        /* --- FIX: Force full description to show on character/lorebook pages --- */
        div.ant-card-meta-description {
            max-height: none !important;
            -webkit-mask-image: none !important;
            mask-image: none !important;
        }
        .show-more-button-container {
            display: none !important;
        }

        /* Pagination container and button styling */
        .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-page feature styling */
        .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; }

        /* Bigger scrollbar for all tag containers */
        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);

})();