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