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.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();