// ==UserScript== // @name Emoji Tooltip // @name:zh-CN Emoji 含义选中提示 // @namespace http://tampermonkey.net/ // @version 1.11 // @description:zh-CN 在网页中选中 Emoji 时,强制以 PNG 格式图片显示其高清图标及名称和分类,支持所有带肤色变体的 Emoji。 // @description When an emoji is selected on a webpage, it forces the display of its high-resolution icon, name, and category as a PNG image, supporting all emojis with skin tone variations. // @author Kaesinol // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @connect cdn.jsdelivr.net // @connect www.emojiall.com // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // ==================== // ⚙️ Configuration // ==================== const CONFIG = { BASE_URL: 'https://cdn.jsdelivr.net/npm/emojibase-data@17.0.0', PNG_BASE_URL: 'https://www.emojiall.com/images/60/google', CACHE_KEY: 'emoji_tooltip_data_v5', CACHE_VERSION: '1.11', AUTO_HIDE_DELAY: 10000, MOUSE_MOVE_THRESHOLD: 300, GROUP_MAP: { 0: 'Smileys & Emotion', 1: 'People & Body', 2: 'Component', 3: 'Animals & Nature', 4: 'Food & Drink', 5: 'Travel & Places', 6: 'Activities', 7: 'Objects', 8: 'Symbols', 9: 'Flags' } }; // ==================== // 📦 State Variables // ==================== let emojiMap = new Map(); let tooltipElement; let hideTimer, autoHideTimer; let isTooltipVisible = false; let lastMousePosition = { x: 0, y: 0 }; // ==================== // 🎨 Tooltip UI Logic // ==================== /** 初始化 Tooltip 元素并注入到 DOM */ function initTooltipElement() { tooltipElement = document.createElement('div'); tooltipElement.style.cssText = ` position: fixed; background: #2b2b2b; color: #fff; padding: 10px 14px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.4); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.4; z-index: 2147483647; max-width: 320px; opacity: 0; transition: opacity 0.2s, transform 0.2s; display: none; transform: translateX(10px) translateY(5px); pointer-events: none; border: 1px solid #444; `; if (document.body) { document.body.appendChild(tooltipElement); } else { // document-start 运行,可能 body 还没准备好,使用 MutationObserver new MutationObserver((mutations, observer) => { if (document.body) { document.body.appendChild(tooltipElement); observer.disconnect(); } }).observe(document.documentElement, { childList: true, subtree: true }); } } /** 显示 Tooltip */ function showTooltip(content, x, y) { clearTimeout(hideTimer); clearTimeout(autoHideTimer); tooltipElement.innerHTML = content; tooltipElement.style.display = 'block'; tooltipElement.style.opacity = '0'; tooltipElement.style.transform = 'translateX(10px) translateY(5px)'; void tooltipElement.offsetWidth; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const tooltipWidth = tooltipElement.clientWidth || 200; const tooltipHeight = tooltipElement.clientHeight || 80; let left = x + 15; let top = y + 15; if (left + tooltipWidth > viewportWidth - 10) left = x - tooltipWidth - 15; if (top + tooltipHeight > viewportHeight - 10) top = y - tooltipHeight - 15; tooltipElement.style.left = `${left}px`; tooltipElement.style.top = `${top}px`; requestAnimationFrame(() => { tooltipElement.style.opacity = '1'; tooltipElement.style.transform = 'translateX(0) translateY(0)'; }); isTooltipVisible = true; autoHideTimer = setTimeout(hideTooltip, CONFIG.AUTO_HIDE_DELAY); } /** 隐藏 Tooltip */ function hideTooltip() { if (!isTooltipVisible) return; clearTimeout(hideTimer); clearTimeout(autoHideTimer); tooltipElement.style.opacity = '0'; tooltipElement.style.transform = 'translateX(10px) translateY(5px)'; hideTimer = setTimeout(() => { tooltipElement.style.display = 'none'; isTooltipVisible = false; }, 200); } /** 显示数据加载中的提示Tooltip */ function showLoadingTooltip(x, y) { const content = `
✨ Loading Emoji Data...
(Please wait a moment)
`; showTooltip(content, x, y); lastMousePosition = { x, y }; } // ==================== // 🧠 Event Handling // ==================== /** 鼠标松开(选中)事件处理 */ function handleSelection(e) { let selection; try { selection = window.getSelection().toString().trim(); } catch (error) { hideTooltip(); return; } if (!selection || selection.length < 1 || selection.length > 15) { if (isTooltipVisible) hideTimer = setTimeout(hideTooltip, 2000); return; } const emojiData = emojiMap.get(selection); if (emojiData) { // 1. 数据已加载且匹配:显示 Emoji 详情 showEmojiTooltip(emojiData, selection, e.clientX, e.clientY); } else if (emojiMap.size === 0) { // 2. 数据未加载(Map为空):显示加载提示 showLoadingTooltip(e.clientX, e.clientY); } else { // 3. 数据已加载但未匹配:隐藏 Tooltip if (isTooltipVisible) hideTooltip(); } } /** 构建 Tooltip 内容并显示 */ function showEmojiTooltip(emojiData, emojiChar, x, y) { const name = emojiData.name.charAt(0).toUpperCase() + emojiData.name.slice(1); const pngUrl = `${CONFIG.PNG_BASE_URL}/${emojiData.hexcode.toLowerCase()}.png`; const iconHtml = `${emojiChar}`; const content = `
${iconHtml}
${name}
Group: ${emojiData.group}
`; showTooltip(content, x, y); lastMousePosition = { x, y }; } /** 鼠标大幅度移动事件处理 (用于自动隐藏) */ function handleMouseMove(e) { if (!isTooltipVisible) return; const dx = Math.abs(e.clientX - lastMousePosition.x); const dy = Math.abs(e.clientY - lastMousePosition.y); if (dx > CONFIG.MOUSE_MOVE_THRESHOLD || dy > CONFIG.MOUSE_MOVE_THRESHOLD) { hideTooltip(); lastMousePosition = { x: e.clientX, y: e.clientY }; } } // ==================== // 💾 Data & Cache Logic // ==================== /** * 处理原始数据并保存到 emojiMap 和 GM_setValue。 */ function processAndCacheData(data, langCode, origin) { try { emojiMap.clear(); data.forEach(item => { if (item.emoji && item.label && item.hexcode) { emojiMap.set(item.emoji, { name: item.label, group: CONFIG.GROUP_MAP[item.group] || 'Other', hexcode: item.hexcode }); } if (Array.isArray(item.skins)) { item.skins.forEach(skin => { if (skin.emoji && skin.label && skin.hexcode) { emojiMap.set(skin.emoji, { name: skin.label, group: CONFIG.GROUP_MAP[skin.group || item.group] || 'Other', hexcode: skin.hexcode }); } }); } }); if (origin === 'network') { GM_setValue(CONFIG.CACHE_KEY, { version: CONFIG.CACHE_VERSION, lang: langCode, timestamp: Date.now(), data: data }); } console.log(`[Emoji Tooltip] ${emojiMap.size} emojis loaded (${langCode}) from ${origin}.`); } catch (e) { console.error('[Emoji Tooltip] Failed to process data', e); } } /** 从网络获取 Emoji 数据 */ function fetchEmojiData(langCode, isFallback = false) { const url = `${CONFIG.BASE_URL}/${langCode}/data.json`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (response) { if (response.status === 200) { processAndCacheData(JSON.parse(response.responseText), langCode, 'network'); } else if (!isFallback && langCode !== 'en') { console.warn(`[Emoji Tooltip] Failed to load ${langCode}. Falling back to English (en).`); fetchEmojiData('en', true); } }, onerror: function (error) { console.error(`[Emoji Tooltip] Network error loading ${langCode}:`, error); if (!isFallback && langCode !== 'en') { fetchEmojiData('en', true); } } }); } /** 加载或获取 Emoji 数据 */ function loadEmojiData() { const browserLang = (navigator.language || 'en').split('-')[0]; const cached = GM_getValue(CONFIG.CACHE_KEY, null); if (cached && cached.version === CONFIG.CACHE_VERSION) { if (cached.lang === browserLang || cached.lang === 'en') { try { processAndCacheData(cached.data, cached.lang, 'cache'); return; } catch (e) { console.error('[Emoji Tooltip] Cached data corrupted, forcing network reload.'); GM_setValue(CONFIG.CACHE_KEY, null); } } } fetchEmojiData(browserLang); } // ==================== // 启动程序 // ==================== function init() { initTooltipElement(); // 确保 UI 元素立即创建 loadEmojiData(); // 开始异步加载数据 // 绑定事件,Tooltip 已经就绪,可以接收用户的选中操作 document.addEventListener('mouseup', handleSelection, { passive: true }); document.addEventListener('mousemove', handleMouseMove, { passive: true }); window.addEventListener('scroll', hideTooltip, { passive: true }); window.addEventListener('blur', hideTooltip); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isTooltipVisible) { hideTooltip(); } }); } init(); })();