E-Hentai Tag Cloud Generator

Analyzes all manga tags in favorites and generates a tag cloud (only works in extend mode)

// ==UserScript==
// @name         E-Hentai Tag Cloud Generator
// @name:en      E-Hentai Tag Cloud Generator
// @name:ja      E-Hentai タグクラウドジェネレーター
// @name:zh-CN   E-Hentai 生成标签云
// @namespace    https://github.com/CheerChen
// @version      3.1
// @description  Analyzes all manga tags in favorites and generates a tag cloud (only works in extend mode)
// @description:en Analyzes all manga tags in favorites and generates a tag cloud (only works in extend mode)
// @description:ja お気に入り内の全ページの漫画タグを分析してタグクラウドを生成します(extendモードでのみ有効)
// @description:zh-CN 分析收藏夹内所有页面的漫画标签,生成标签云(注意,只在extend模式下有效)
// @author       cheerchen37
// @license      MIT
// @match        https://e-hentai.org/favorites.php*
// @icon         https://www.google.com/s2/favicons?domain=e-hentai.org
// @grant        GM_xmlhttpRequest
// @connect      e-hentai.org
// @homepage     https://github.com/CheerChen/userscripts
// @supportURL   https://github.com/CheerChen/userscripts/issues
// @require      https://cdn.jsdelivr.net/npm/[email protected]/src/wordcloud2.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 标签类别
    const TAG_CATEGORIES = ['language', 'parody', 'female', 'male', 'mixed', 'group', 'artist', 'cosplayer', 'character', 'reclass', 'other'];
    
    // 黑暗模式状态
    let isDarkMode = true;

    // 创建UI
    function createUI() {
        const toggleButton = document.createElement('div');
        toggleButton.id = 'toggleChartButton';
        toggleButton.innerHTML = '📊';
        toggleButton.style.cssText = `
            position: fixed;
            right: 20px;
            top: 20px;
            width: 40px;
            height: 40px;
            background-color: #4a4a4a;
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 20px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 10000;
            transition: transform 0.3s ease;
        `;

        const container = document.createElement('div');
        container.id = 'chartContainer';
        container.style.cssText = `
            position: fixed;
            right: 20px;
            top: 70px;
            width: 70vw;
            max-width: 900px;
            height: 70vh;
            max-height: 600px;
            background-color: #1a1a1a;
            color: #ffffff;
            padding: 15px;
            border-radius: 10px;
            box-shadow: 0 0 20px rgba(0,0,0,0.5);
            z-index: 9999;
            display: none;
            transition: opacity 0.3s ease;
            overflow: hidden;
        `;

        const tabsContainer = document.createElement('div');
        tabsContainer.style.cssText = `
            display: flex;
            flex-direction: column;
            height: 100%;
        `;

        const tabButtons = document.createElement('div');
        tabButtons.style.cssText = `
            display: flex;
            gap: 10px;
            padding: 10px;
            border-bottom: 1px solid #333;
            flex-wrap: wrap; /* 允许换行 */
            align-items: center;
        `;

        // 添加黑暗模式切换按钮
        const darkModeToggle = document.createElement('button');
        darkModeToggle.id = 'darkModeToggle';
        darkModeToggle.innerHTML = '🌙';
        darkModeToggle.title = 'Toggle Dark Mode';
        darkModeToggle.style.cssText = `
            padding: 8px;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            background-color: #333;
            color: #fff;
            font-size: 16px;
            width: 36px;
            height: 36px;
            margin-left: auto;
            transition: all 0.3s ease;
        `;
        darkModeToggle.onclick = toggleDarkMode;

        const chartArea = document.createElement('div');
        chartArea.style.cssText = `
            flex: 1;
            position: relative;
            overflow: hidden;
        `;

        TAG_CATEGORIES.forEach((category, index) => {
            const button = document.createElement('button');
            button.textContent = category.charAt(0).toUpperCase() + category.slice(1);
            button.dataset.category = category;
            button.style.cssText = `
                padding: 8px 16px;
                border: none;
                border-radius: 5px;
                cursor: pointer;
                background-color: #333;
                color: #fff;
                transition: all 0.3s ease;
            `;
            button.onclick = () => switchTab(category);
            tabButtons.appendChild(button);
        });

        // 添加黑暗模式切换按钮到按钮容器
        tabButtons.appendChild(darkModeToggle);

        const canvas = document.createElement('div');
        canvas.id = 'tagsChart';
        canvas.style.cssText = `
            width: 100%;
            height: 100%;
            background-color: #2a2a2a;
            border-radius: 5px;
        `;

        chartArea.appendChild(canvas);
        tabsContainer.appendChild(tabButtons);
        tabsContainer.appendChild(chartArea);
        container.appendChild(tabsContainer);

        const closeButton = document.createElement('button');
        closeButton.innerHTML = '✕';
        closeButton.style.cssText = `
            position: absolute;
            right: 10px;
            top: 10px;
            background: none;
            border: none;
            font-size: 20px;
            cursor: pointer;
            color: #fff;
            z-index: 1;
        `;

        container.appendChild(closeButton);
        document.body.appendChild(toggleButton);
        document.body.appendChild(container);

        toggleButton.onclick = () => {
            container.style.display = container.style.display === 'none' ? 'block' : 'none';
            if (container.style.display === 'block') {
                if (!window.tagData) {
                    // 首次打开,需要加载数据
                    init();
                } else {
                    // 已有缓存数据,直接显示
                    const firstValidCategory = TAG_CATEGORIES.find(category =>
                        window.tagData[category] && window.tagData[category].size > 0);
                    if (firstValidCategory) {
                        switchTab(firstValidCategory);
                    }
                }
            }
        };

        closeButton.onclick = () => {
            container.style.display = 'none';
        };

        container.onclick = (e) => e.stopPropagation();

        document.addEventListener('click', (e) => {
            if (!container.contains(e.target) && e.target !== toggleButton) {
                container.style.display = 'none';
            }
        });

        return canvas;
    }

    // 切换黑暗模式
    function toggleDarkMode() {
        isDarkMode = !isDarkMode;
        const container = document.getElementById('chartContainer');
        const canvas = document.getElementById('tagsChart');
        const tabButtons = document.querySelectorAll('#chartContainer button[data-category]');
        const darkModeToggle = document.getElementById('darkModeToggle');
        const closeButton = container.querySelector('button:not([data-category]):not(#darkModeToggle)');
        const tabsContainer = container.querySelector('div:first-child > div:first-child');

        if (isDarkMode) {
            // 切换到黑暗模式
            container.style.backgroundColor = '#1a1a1a';
            container.style.color = '#ffffff';
            container.style.boxShadow = '0 0 20px rgba(0,0,0,0.5)';
            canvas.style.backgroundColor = '#2a2a2a';
            closeButton.style.color = '#fff';
            tabsContainer.style.borderBottomColor = '#333';
            darkModeToggle.innerHTML = '🌙';
            darkModeToggle.style.backgroundColor = '#333';
            darkModeToggle.style.color = '#fff';
            
            tabButtons.forEach(button => {
                if (button.style.backgroundColor === 'rgb(74, 74, 74)') {
                    // 选中状态
                    button.style.backgroundColor = '#4a4a4a';
                    button.style.color = '#fff';
                } else {
                    // 未选中状态
                    button.style.backgroundColor = '#333';
                    button.style.color = '#fff';
                }
            });
        } else {
            // 切换到亮色模式
            container.style.backgroundColor = '#ffffff';
            container.style.color = '#000000';
            container.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
            canvas.style.backgroundColor = '#f9f9f9';
            closeButton.style.color = '#666';
            tabsContainer.style.borderBottomColor = '#eee';
            darkModeToggle.innerHTML = '☀️';
            darkModeToggle.style.backgroundColor = '#eee';
            darkModeToggle.style.color = '#000';
            
            tabButtons.forEach(button => {
                if (button.style.backgroundColor === 'rgb(74, 74, 74)') {
                    // 选中状态
                    button.style.backgroundColor = '#4a4a4a';
                    button.style.color = '#fff';
                } else {
                    // 未选中状态
                    button.style.backgroundColor = '#eee';
                    button.style.color = '#000';
                }
            });
        }

        // 如果有标签云数据,重新渲染以适应新的背景色
        if (window.tagData && window.currentCategory) {
            createWordCloud(window.tagData, window.currentCategory);
        }
    }

    // 解析页面中的标签
    function parseTagsFromHtml(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const tagCategories = {};

        TAG_CATEGORIES.forEach(category => {
            tagCategories[category] = new Map();
        });

        const tables = doc.querySelectorAll('tr td.tc');
        tables.forEach(categoryCell => {
            const category = categoryCell.textContent.replace(':', '');
            if (TAG_CATEGORIES.includes(category)) {
                const tagElements = categoryCell.parentElement.querySelectorAll('.gt, .gtl');
                tagElements.forEach(tag => {
                    const tagTitle = tag.getAttribute('title');
                    if (tagTitle) {
                        const tagName = tagTitle.split(':')[1];
                        if (tagName) {
                            const count = tagCategories[category].get(tagName) || 0;
                            tagCategories[category].set(tagName, count + 1);
                        }
                    }
                });
            }
        });

        return tagCategories;
    }

    // 合并标签数据
    function mergeCategoryTags(tags1, tags2) {
        const merged = {};
        TAG_CATEGORIES.forEach(category => {
            merged[category] = new Map(tags1[category] || []);
            if (tags2[category]) {
                for (const [tag, count] of tags2[category]) {
                    const currentCount = merged[category].get(tag) || 0;
                    merged[category].set(tag, currentCount + count);
                }
            }
        });
        return merged;
    }

    // 获取页面数据
    function fetchPage(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(response.responseText);
                    } else {
                        reject(new Error(`Failed to fetch page: ${response.status}`));
                    }
                },
                onerror: reject
            });
        });
    }

    // 收集所有标签
    async function collectTags() {
        let allTags = {};
        const chartArea = document.querySelector('#chartContainer div:nth-child(2)');
        const loadingSpinner = createLoadingSpinner();
        chartArea.appendChild(loadingSpinner);

        try {
            let currentPageHtml = document.documentElement.outerHTML;
            let pageCount = 1;

            while (true) {
                updateLoadingProgress(pageCount);

                // 从当前页面的HTML中解析标签
                const pageTags = parseTagsFromHtml(currentPageHtml);
                allTags = mergeCategoryTags(allTags, pageTags);

                // 从当前HTML中查找下一页的链接
                const parser = new DOMParser();
                const doc = parser.parseFromString(currentPageHtml, 'text/html');
                const nextLink = doc.querySelector('#dnext');

                if (nextLink && nextLink.href) {
                    // 如果存在下一页,则获取其内容
                    currentPageHtml = await fetchPage(nextLink.href);
                    pageCount++;
                } else {
                    // 没有更多页面,跳出循环
                    break;
                }
            }
        } catch (error) {
            console.error('Error fetching pages:', error);
            const spinner = document.getElementById('loadingSpinner');
            if(spinner) spinner.querySelector('p').textContent = `Error: ${error.message}`;
        } finally {
            // 延迟移除加载动画,以防用户看不到错误信息
            setTimeout(() => loadingSpinner.remove(), 2000);
        }

        return allTags;
    }

    // 创建标签云
    function createWordCloud(tagsData, category) {
        const container = document.getElementById('tagsChart');
        const categoryTags = tagsData[category];

        // 清空容器
        container.innerHTML = '';

        if (!categoryTags || categoryTags.size === 0) {
            const noDataColor = isDarkMode ? '#999' : '#666';
            container.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: ${noDataColor}; font-size: 18px;">No ${category} tags found</div>`;
            return;
        }

        // 将Map转换为数组,排序并截取前50个
        const sortedTags = Array.from(categoryTags)
            .sort((a, b) => b[1] - a[1])
            .slice(0, 50);

        // 准备词云数据
        const wordList = sortedTags.map(([tag, count]) => {
            // 根据标签出现次数计算字体大小
            const maxCount = sortedTags[0][1];
            const minCount = sortedTags[sortedTags.length - 1][1];
            const weight = ((count - minCount) / (maxCount - minCount)) * 40 + 12; // 字体大小在12-52之间
            return [tag, weight];
        });

        // 创建词云容器
        const cloudContainer = document.createElement('div');
        cloudContainer.style.cssText = `
            width: 100%;
            height: 100%;
            position: relative;
        `;
        container.appendChild(cloudContainer);

        // 生成标签云
        WordCloud(cloudContainer, {
            list: wordList,
            gridSize: 8,
            weightFactor: 1,
            fontFamily: 'Arial, sans-serif',
            color: function() {
                // 生成随机颜色
                const colors = [
                    '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
                    '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
                    '#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D2B4DE'
                ];
                return colors[Math.floor(Math.random() * colors.length)];
            },
            backgroundColor: 'transparent',
            rotateRatio: 0.3,
            rotationSteps: 2,
            minSize: 12,
            drawOutOfBound: false,
            shrinkToFit: true,
            hover: function(item) {
                if (item) {
                    // 显示标签信息
                    const tooltip = document.getElementById('wordcloud-tooltip') || createTooltip();
                    const tagName = item[0];
                    const tagCount = sortedTags.find(([tag]) => tag === tagName)?.[1] || 0;
                    tooltip.innerHTML = `${tagName}: ${tagCount} times`;
                    tooltip.style.display = 'block';
                } else {
                    const tooltip = document.getElementById('wordcloud-tooltip');
                    if (tooltip) tooltip.style.display = 'none';
                }
            },
            click: function(item) {
                if (item) {
                    // 点击标签时的操作,例如搜索该标签
                    const tagName = item[0];
                    console.log(`Clicked on tag: ${tagName}`);
                    // 可以在这里添加搜索功能
                }
            }
        });
    }

    // 创建提示框
    function createTooltip() {
        const tooltip = document.createElement('div');
        tooltip.id = 'wordcloud-tooltip';
        tooltip.style.cssText = `
            position: absolute;
            background-color: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 5px 10px;
            border-radius: 3px;
            font-size: 12px;
            pointer-events: none;
            z-index: 10001;
            display: none;
        `;
        document.body.appendChild(tooltip);

        // 跟随鼠标移动
        document.addEventListener('mousemove', (e) => {
            tooltip.style.left = (e.pageX + 10) + 'px';
            tooltip.style.top = (e.pageY - 30) + 'px';
        });

        return tooltip;
    }


    // 切换标签页
    function switchTab(category) {
        window.currentCategory = category; // 保存当前类别
        const buttons = document.querySelectorAll('#chartContainer button[data-category]');
        buttons.forEach(button => {
            const isSelected = button.dataset.category === category;
            if (isSelected) {
                button.style.backgroundColor = '#4a4a4a';
                button.style.color = '#fff';
            } else {
                button.style.backgroundColor = isDarkMode ? '#333' : '#eee';
                button.style.color = isDarkMode ? '#fff' : '#000';
            }
        });

        if (window.tagData) {
            createWordCloud(window.tagData, category);
        }
    }

    // 创建加载动画
    function createLoadingSpinner() {
        const spinner = document.createElement('div');
        spinner.id = 'loadingSpinner';
        spinner.style.cssText = `
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            text-align: center;
        `;
        spinner.innerHTML = `
            <div style="
                width: 50px;
                height: 50px;
                border: 5px solid #f3f3f3;
                border-top: 5px solid #3498db;
                border-radius: 50%;
                animation: spin 1s linear infinite;
                margin: 0 auto;
            "></div>
            <p style="margin-top: 10px;">Initializing...</p>
        `;

        if (!document.getElementById('spinnerStyle')) {
            const style = document.createElement('style');
            style.id = 'spinnerStyle';
            style.textContent = `
                @keyframes spin {
                    0% { transform: rotate(0deg); }
                    100% { transform: rotate(360deg); }
                }
            `;
            document.head.appendChild(style);
        }

        return spinner;
    }

    // 更新加载进度
    function updateLoadingProgress(loadedPages) {
        const spinner = document.getElementById('loadingSpinner');
        if (spinner) {
            spinner.querySelector('p').textContent = `Loading data from page ${loadedPages}...`;
        }
    }

    // 初始化
    async function init() {
        if (!window.tagData) {
            window.tagData = await collectTags();

            const hasData = TAG_CATEGORIES.some(category =>
                window.tagData[category] && window.tagData[category].size > 0);

            if (hasData) {
                const firstValidCategory = TAG_CATEGORIES.find(category =>
                    window.tagData[category] && window.tagData[category].size > 0);
                switchTab(firstValidCategory || TAG_CATEGORIES[0]);
            } else {
                console.log('No tag data found. Make sure you are in "Extended" favorites view.');
                const chartArea = document.querySelector('#chartContainer div:nth-child(2)');
                chartArea.innerHTML = `<p style="text-align:center; margin-top: 20px;">No tag data found. Please ensure you are in "Extended" favorites view.</p>`;
            }
        }
    }

    createUI();
})();