Greasy Fork

Greasy Fork is available in English.

Emoji 含义选中提示

在网页中选中 Emoji 时,强制以 PNG 格式图片显示其高清图标及名称和分类,通过 GM_xmlhttpRequest 绕过 CSP 限制。

当前为 2025-12-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Emoji Tooltip
// @name:zh-CN  Emoji 含义选中提示
// @namespace   http://tampermonkey.net/
// @version     1.12
// @description:zh-CN 在网页中选中 Emoji 时,强制以 PNG 格式图片显示其高清图标及名称和分类,通过 GM_xmlhttpRequest 绕过 CSP 限制。
// @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, bypassing CSP using GM_xmlhttpRequest.
// @author      Kaesinol
// @match       *://*/*
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @connect     cdn.jsdelivr.net
// @connect     www.emojiall.com
// @connect     *emojiall.com
// @run-at      document-start
// @license     MIT
// ==/UserScript==

(function () {
    'use strict';

    // ====================
    // ⚙️ Configuration
    // ====================
    const CONFIG = {
        BASE_URL: 'https://cdn.jsdelivr.net/npm/[email protected]',
        PNG_BASE_URL: 'https://www.emojiall.com/images/60/google',
        CACHE_KEY: 'emoji_tooltip_data_v5',
        CACHE_VERSION: '1.12', // 版本更新
        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
    // ====================

    /** 辅助函数:将 ArrayBuffer 转换为 Base64 字符串 */
    function arrayBufferToBase64(buffer) {
        let binary = '';
        const bytes = new Uint8Array(buffer);
        const len = bytes.byteLength;
        for (let i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }

    /** 初始化 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 {
            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;
        // 使用 clientWidth/Height 获取实际渲染尺寸
        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 = `
            <div style="display: flex; align-items: center; gap: 12px">
                <div style="width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-size: 16px;">
                    ⏳
                </div>
                <div>
                    <div style="font-weight: 600; font-size: 14px; color: #f9e67d;">
                        ✨ Loading Emoji Data...
                    </div>
                    <div style="color: #bbb; font-size: 12px;">
                        (Please wait a moment)
                    </div>
                </div>
            </div>
        `;
        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 内容并显示
     * 关键:使用 GM_xmlhttpRequest 异步加载图片并转换为 data:URI 绕过 CSP
     */
    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`;

        // 1. 先显示一个带有加载提示的 Tooltip
        let initialContent = `
            <div style="display: flex; align-items: center; gap: 12px">
                <div style="width: 32px; height: 32px; border: 2px solid #555; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 16px;">
                    🖼️
                </div>
                <div>
                    <div style="font-weight: 600; line-height: 1.3; font-size: 14px;">${name}</div>
                    <div style="color: #f9e67d; font-size: 12px; margin-top: 3px;">
                        Loading image via GM_xhr...
                    </div>
                </div>
            </div>`;
        showTooltip(initialContent, x, y);
        lastMousePosition = { x, y };

        // 2. 使用 GM_xmlhttpRequest 异步加载图片
        GM_xmlhttpRequest({
            method: 'GET',
            url: pngUrl,
            responseType: 'arraybuffer', // 关键:以二进制数组形式获取数据
            onload: function (response) {
                if (response.status === 200) {
                    // 将 ArrayBuffer 转换为 Base64 字符串
                    const base64String = arrayBufferToBase64(response.response);
                    const dataUri = `data:image/png;base64,${base64String}`;

                    // 3. 图片加载成功后,检查 Tooltip 是否仍然可见且位置相同
                    // 如果用户鼠标已经移开,则不再更新 Tooltip
                    if (isTooltipVisible && lastMousePosition.x === x && lastMousePosition.y === y) {
                        const iconHtml = `<img src="${dataUri}" alt="${emojiChar}" style="width: 32px; height: 32px; vertical-align: middle; object-fit: contain;">`;

                        const finalContent = `
                            <div style="display: flex; align-items: center; gap: 12px">
                                ${iconHtml}
                                <div>
                                    <div style="font-weight: 600; line-height: 1.3; font-size: 14px;">${name}</div>
                                    <div style="color: #bbb; font-size: 12px; margin-top: 3px;">
                                        Group: ${emojiData.group}
                                    </div>
                                </div>
                            </div>
                        `;
                        // 重新显示最终内容
                        showTooltip(finalContent, x, y);
                    }
                } else {
                    console.error(`[Emoji Tooltip] Failed to load image from ${pngUrl}. Status: ${response.status}`);
                    // 如果加载失败,显示文本回退
                    const textFallbackContent = `
                        <div style="display: flex; align-items: center; gap: 12px">
                            <span style="font-size: 2em; line-height: 1; display: inline-block;">${emojiChar}</span>
                            <div>
                                <div style="font-weight: 600; line-height: 1.3; font-size: 14px;">${name}</div>
                                <div style="color: #ff6b6b; font-size: 12px; margin-top: 3px;">
                                    ERROR: Image Blocked/Failed
                                </div>
                            </div>
                        </div>`;
                    showTooltip(textFallbackContent, x, y);
                }
            },
            onerror: function (error) {
                console.error(`[Emoji Tooltip] Network error during image fetch:`, error);
                // 同样进行错误回退
                const textFallbackContent = `
                        <div style="display: flex; align-items: center; gap: 12px">
                            <span style="font-size: 2em; line-height: 1; display: inline-block;">${emojiChar}</span>
                            <div>
                                <div style="font-weight: 600; line-height: 1.3; font-size: 14px;">${name}</div>
                                <div style="color: #ff6b6b; font-size: 12px; margin-top: 3px;">
                                    ERROR: Network Failed
                                </div>
                            </div>
                        </div>`;
                    showTooltip(textFallbackContent, 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
    // ====================

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

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

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

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