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