Sleazy Fork is available in English.

让我康康你的xp!

分析ehentai收藏五页的漫画标签,每个分类生成一个饼图让你可以知道自己的XP(注意,只在extend模式下有效)

// ==UserScript==
// @name         让我康康你的xp!
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  分析ehentai收藏五页的漫画标签,每个分类生成一个饼图让你可以知道自己的XP(注意,只在extend模式下有效)
// @author      cheerchen37
// @license     MIT
// @match        https://e-hentai.org/favorites.php*
// @grant        GM_xmlhttpRequest
// @connect      e-hentai.org
// @require      https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 标签类别
    const TAG_CATEGORIES = ['language', 'parody', 'female', 'male', 'mixed', 'group', 'artist'];

    // 创建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: 80vw;
            max-width: 1200px;
            height: 80vh;
            max-height: 800px;
            background-color: white;
            padding: 15px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0,0,0,0.2);
            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 #eee;
        `;

        // 创建图表区域
        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: ${index === 0 ? '#4a4a4a' : '#eee'};
                color: ${index === 0 ? 'white' : 'black'};
                transition: all 0.3s ease;
            `;

            button.onclick = () => switchTab(category);
            tabButtons.appendChild(button);
        });

        // 创建图表canvas
        const canvas = document.createElement('canvas');
        canvas.id = 'tagsChart';
        canvas.style.cssText = `
            width: 100%;
            height: 100%;
        `;

        // 组装UI
        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: #666;
            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' && !window.myChart) {
                init();
            }
        };

        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 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();
            // 合并第一个标签集
            if (tags1[category]) {
                for (const [tag, count] of tags1[category]) {
                    merged[category].set(tag, count);
                }
            }
            // 合并第二个标签集
            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;
    }

    // 获取页面数据
    async 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 currentPage = window.location.href;
        let allTags = {};

        // 初始化加载动画
        const chartArea = document.querySelector('#chartContainer div:nth-child(2)');
        const loadingSpinner = createLoadingSpinner();
        chartArea.appendChild(loadingSpinner);

        try {
            // 获取当前页面的标签
            allTags = parseTagsFromHtml(document.documentElement.outerHTML);
            updateLoadingProgress(1);

            // 获取后续4页的标签
            for (let i = 0; i < 4; i++) {
                const nextLink = document.querySelector('#dnext');
                if (!nextLink) break;

                const nextUrl = nextLink.href;
                const html = await fetchPage(nextUrl);
                const pageTags = parseTagsFromHtml(html);
                allTags = mergeCategoryTags(allTags, pageTags);
                updateLoadingProgress(i + 2);
            }
        } catch (error) {
            console.error('Error fetching pages:', error);
        } finally {
            loadingSpinner.remove();
        }

        return allTags;
    }

    // 创建饼图
    function createPieChart(tagsData, category) {
        const ctx = document.getElementById('tagsChart').getContext('2d');

        // 直接访问对应分类的Map
        const categoryTags = tagsData[category];

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

        const labels = sortedTags.map(([tag]) => tag);
        const data = sortedTags.map(([, count]) => count);
        const colors = generateColors(labels.length);
        const total = data.reduce((acc, curr) => acc + curr, 0);

        if (window.myChart) {
            window.myChart.destroy();
        }

        window.myChart = new Chart(ctx, {
            type: 'pie',
            data: {
                labels: labels,
                datasets: [{
                    data: data,
                    backgroundColor: colors,
                    borderWidth: 1,
                    borderColor: '#fff'
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    title: {
                        display: true,
                        text: `${category.charAt(0).toUpperCase() + category.slice(1)} Tags Distribution`,
                        font: {
                            size: 16
                        }
                    },
                    legend: {
                        position: 'right',
                        onClick: function(e, legendItem, legend) {
                            const index = legendItem.index;
                            const ci = legend.chart;
                            const meta = ci.getDatasetMeta(0);
                            meta.data[index].hidden = !meta.data[index].hidden;
                            ci.update();
                        },
                        labels: {
                            font: {
                                size: 11
                            },
                            generateLabels: function(chart) {
                                const data = chart.data;
                                if (data.labels.length && data.datasets.length) {
                                    return data.labels.map((label, i) => {
                                        const meta = chart.getDatasetMeta(0);
                                        const value = data.datasets[0].data[i];
                                        const percentage = ((value / total) * 100).toFixed(1);
                                        return {
                                            text: `${label} (${value}, ${percentage}%)`,
                                            fillStyle: data.datasets[0].backgroundColor[i],
                                            hidden: meta.data[i].hidden,
                                            index: i
                                        };
                                    });
                                }
                                return [];
                            }
                        }
                    },
                    tooltip: {
                        callbacks: {
                            label: function(context) {
                                const value = context.raw;
                                const percentage = ((value / total) * 100).toFixed(1);
                                return `${context.label}: ${value} (${percentage}%)`;
                            }
                        }
                    }
                }
            }
        });
    }

    // 切换标签页
    function switchTab(category) {
        // 更新按钮样式
        const buttons = document.querySelectorAll('#chartContainer button[data-category]');
        buttons.forEach(button => {
            if (button.dataset.category === category) {
                button.style.backgroundColor = '#4a4a4a';
                button.style.color = 'white';
            } else {
                button.style.backgroundColor = '#eee';
                button.style.color = 'black';
            }
        });

        // 更新图表
        if (window.tagData) {
            createPieChart(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;">Loading data from pages (0/5)...</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 pages (${loadedPages}/5)...`;
        }
    }

    // 生成颜色
    function generateColors(count) {
        const colors = [];
        const baseHues = [0, 60, 120, 180, 240, 300];

        for (let i = 0; i < count; i++) {
            const baseHue = baseHues[i % baseHues.length];
            const hueOffset = (Math.floor(i / baseHues.length) * 20) % 60;
            const hue = (baseHue + hueOffset) % 360;
            const saturation = 70 + Math.random() * 20;
            const lightness = 50 + Math.random() * 20;
            colors.push(`hsla(${hue}, ${saturation}%, ${lightness}%,0.8)`);
        }
        return colors;
    }

    // 初始化
    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);
                createPieChart(window.tagData, firstValidCategory || TAG_CATEGORIES[0]);
            } else {
                console.log('No tag data found');
            }
        }
    }

    // 创建UI
    createUI();
})();