Greasy Fork

Greasy Fork is available in English.

花火御用表情包面板 一键爆炸

可自定义添加/删除表情,点击表情包直接插入并发送,带磨砂质感浮动面板与开关 + 发送提示

当前为 2025-10-08 提交的版本,查看 最新版本

// ==UserScript==
// @name         花火御用表情包面板 一键爆炸
// @namespace    https://deepflood.com/
// @version      0.1
// @description  可自定义添加/删除表情,点击表情包直接插入并发送,带磨砂质感浮动面板与开关 + 发送提示
// @author       Sparkle
// @license      MIT
// @match        *://www.deepflood.com/*
// @match        *://www.nodeseek.com/*
// @grant        none
// @icon         https://img.meituan.net/video/1f498ca05808be0e7a8a837d4e51e995233496.png
// ==/UserScript==rript==
// @name         花火御用表情包面板 一键爆炸
// @namespace    https://deepflood.com/
// @version      0.1
// @description  可自定义添加/删除表情,点击表情包直接插入并发送,带磨砂质感浮动面板与开关 + 发送提示
// @author       Sparkle
// @match        *://www.deepflood.com/*
// @match        *://www.nodeseek.com/*
// @grant        none
// @icon         https://img.meituan.net/video/1f498ca05808be0e7a8a837d4e51e995233496.png
// ==/UserScript==

(function () {
    'use strict';

    // GitHub仓库配置
    const REPO_BASE_URL = "https://cdn.jsdelivr.net/gh/1143520/doro@main/loop/";
    const REPO_API_URL = "https://api.github.com/repos/1143520/doro/contents/loop";

    // 图片处理配置
    const USE_IMAGE_PROXY = true; // 是否使用图片处理服务来调整尺寸
    const IMAGE_PROXY_URL = "https://wsrv.nl/?url="; // 图片处理服务(用于尺寸调整)
    const TARGET_SIZE = "60"; // 目标尺寸

    // 默认表情列表 - 将在异步加载后填充
    let defaultEmojiList = [];
    let allGifFiles = []; // 存储所有GIF文件名
    let isLoading = true;

    // 从GitHub API获取所有GIF文件列表
    async function fetchAllGifFiles() {
        try {
            console.log("🔄 开始从GitHub获取表情包列表...");
            const response = await fetch(REPO_API_URL);
            const files = await response.json();

            // 筛选出所有.gif文件
            allGifFiles = files
                .filter(file => file.name.endsWith('.gif') && file.type === 'file')
                .map(file => file.name);

            console.log(`✅ 成功加载 ${allGifFiles.length} 个表情包`);

            // 随机选择20个
            defaultEmojiList = getRandomEmojis(20);
            isLoading = false;

            // 渲染表情
            renderEmojis();
        } catch (error) {
            console.error("❌ 获取表情包列表失败:", error);
            // 如果API失败,使用备用列表
            allGifFiles = [
                "1735348712826.gif", "1735348724291.gif", "1735348726658.gif", "1735348736520.gif",
                "1735348738391.gif", "1735348747247.gif", "1735348751230.gif", "1735348761071.gif",
                "1735348763774.gif", "1735348770585.gif", "2314666038.gif", "2314666040.gif",
                "2314666044.gif", "2422329068.gif", "2422329071.gif", "2422329072.gif",
                "2437195856.gif", "2437195898.gif", "2437195910.gif", "2437195912.gif"
            ];
            defaultEmojiList = getRandomEmojis(20);
            isLoading = false;
            renderEmojis();
        }
    }

    // 随机选择表情包
    function getRandomEmojis(count = 20) {
        if (allGifFiles.length === 0) return [];
        const shuffled = [...allGifFiles].sort(() => Math.random() - 0.5);
        const selected = shuffled.slice(0, Math.min(count, allGifFiles.length));
        return selected.map(filename => REPO_BASE_URL + filename);
    }

    // 刷新表情包列表(重新随机选择)
    function refreshEmojis() {
        if (allGifFiles.length === 0) {
            showToast("❌ 表情包列表为空,无法刷新");
            console.error("allGifFiles is empty");
            return;
        }

        console.log(`🔄 刷新前: ${defaultEmojiList.length} 个表情`);
        console.log(`📦 表情池总数: ${allGifFiles.length} 个`);

        defaultEmojiList = getRandomEmojis(20);

        console.log(`✅ 刷新后: ${defaultEmojiList.length} 个表情`);
        console.log(`🎲 随机表情:`, defaultEmojiList.slice(0, 3).map(url => url.split('/').pop()));

        renderEmojis();
        showToast(`🔄 已刷新!(共${allGifFiles.length}个表情池)`);
    }

    // --- 新增功能:全局变量 ---
    const STORAGE_KEY = 'hanabi_custom_emojis';
    let isDeleteMode = false;
    let customEmojiList = [];

    // --- 新增功能:本地存储操作 ---
    function loadCustomEmojis() {
        try {
            const stored = localStorage.getItem(STORAGE_KEY);
            return stored ? JSON.parse(stored) : [];
        } catch (e) {
            console.error("加载自定义表情失败", e);
            return [];
        }
    }

    function saveCustomEmojis(emojis) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(emojis));
        } catch (e) {
            console.error("保存自定义表情失败", e);
        }
    }


    function findInputElement() {
        const selectors = [
            'textarea[name="message"]', 'textarea[placeholder*="输入"]', 'textarea[placeholder*="回复"]', 'textarea[placeholder*="说点什么"]',
            'input[type="text"][name="message"]', 'input[type="text"][placeholder*="输入"]',
            '.editor-input textarea', '.message-input textarea', '.chat-input textarea', '.reply-box textarea', '.comment-box textarea',
            'textarea.form-control', 'textarea', 'input[type="text"]'
        ];
        for (const selector of selectors) {
            const el = document.querySelector(selector);
            if (el && !el.disabled && !el.readOnly && el.offsetWidth > 0 && el.offsetHeight > 0) return el;
        }
        const focused = document.activeElement;
        return (focused && (focused.tagName === 'TEXTAREA' || (focused.tagName === 'INPUT' && focused.type === 'text'))) ? focused : null;
    }

    function insertTextAtCursor(el, text) {
        if (!el) return false;
        el.focus();
        if (document.execCommand) document.execCommand('insertText', false, text);
        else if (el.setRangeText) {
            const s = el.selectionStart || 0, e = el.selectionEnd || 0;
            el.setRangeText(text, s, e, 'end');
        } else {
            const s = el.selectionStart || el.value.length;
            const before = el.value.substring(0, s);
            const after = el.value.substring(el.selectionEnd || el.value.length);
            el.value = before + text + after;
            el.selectionStart = el.selectionEnd = s + text.length;
        }
        el.dispatchEvent(new Event('input', { bubbles: true }));
        return true;
    }

    function showToast(msg) {
        const toast = document.createElement("div");
        toast.textContent = msg;
        Object.assign(toast.style, {
            position: "fixed", bottom: "90px", right: "20px", padding: "10px 20px", borderRadius: "12px",
            background: "rgba(255,255,255,0.3)", backdropFilter: "blur(10px) saturate(180%)", color: "#fff",
            fontWeight: "500", fontSize: "15px", boxShadow: "0 4px 12px rgba(0,0,0,0.2)", zIndex: "100000",
            opacity: "0", transition: "opacity 0.3s ease, transform 0.3s ease", transform: "translateY(10px)"
        });
        document.body.appendChild(toast);
        requestAnimationFrame(() => {
            toast.style.opacity = "1";
            toast.style.transform = "translateY(0)";
        });
        setTimeout(() => {
            toast.style.opacity = "0";
            toast.style.transform = "translateY(10px)";
            setTimeout(() => toast.remove(), 300);
        }, 1500);
    }

    // === 悬浮按钮 ===
    const toggleBtn = document.createElement("img");
    toggleBtn.src = "https://img.meituan.net/video/1f498ca05808be0e7a8a837d4e51e995233496.png";
    Object.assign(toggleBtn.style, {
        position: "fixed", right: "15px", bottom: "15px", width: "60px", height: "60px", borderRadius: "50%",
        cursor: "pointer", zIndex: "99998", background: "rgba(255,255,255,0.4)", backdropFilter: "blur(10px) saturate(180%)",
        border: "1px solid rgba(255,255,255,0.5)", boxShadow: "0 4px 18px rgba(0,0,0,0.25)", transition: "transform 0.25s ease, box-shadow 0.25s ease"
    });
    toggleBtn.addEventListener("mouseenter", () => { toggleBtn.style.transform = "scale(1.1)"; toggleBtn.style.boxShadow = "0 6px 20px rgba(0,0,0,0.35)"; });
    toggleBtn.addEventListener("mouseleave", () => { toggleBtn.style.transform = "scale(1)"; toggleBtn.style.boxShadow = "0 4px 18px rgba(0,0,0,0.25)"; });
    document.body.appendChild(toggleBtn);

    // === 主面板 ===
    const panel = document.createElement("div");
    panel.id = "emoji-panel";
    Object.assign(panel.style, {
        position: "fixed", right: "80px", bottom: "80px", width: "240px", height: "auto", maxHeight: "50vh", display: "flex", flexDirection: "column",
        background: "rgba(255, 255, 255, 0.15)", border: "1px solid rgba(255, 255, 255, 0.4)", borderRadius: "16px",
        backdropFilter: "blur(12px) saturate(180%)", boxShadow: "0 10px 30px rgba(0,0,0,0.25)", zIndex: "99999",
        padding: "10px", color: "#222", display: "none", transition: "opacity 0.3s ease, transform 0.3s ease", transform: "translateY(10px)",
    });

    const style = document.createElement("style");
    style.textContent = `
      #emoji-panel * { box-sizing: border-box; }
      #emoji-panel-grid::-webkit-scrollbar { width: 6px; }
      #emoji-panel-grid::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.4); border-radius: 3px; }
      #emoji-panel-grid::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.6); }
      .emoji-item img:hover { transform: scale(1.08); box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
      /* 新增:删除模式样式 */
      #emoji-panel.delete-mode .emoji-item[data-is-custom="true"] > img { border: 2px dashed #ff4757; opacity: 0.8; cursor: pointer; }
      #emoji-panel.delete-mode .emoji-item[data-is-custom="true"]:hover > img { opacity: 1; box-shadow: 0 0 10px #ff4757; }
      #emoji-panel.delete-mode .emoji-item:not([data-is-custom="true"]) { filter: grayscale(80%); opacity: 0.5; pointer-events: none; }
      .control-button { background: rgba(255,255,255,0.3); border: none; padding: 4px 8px; font-size: 12px; border-radius: 6px; color: white; cursor: pointer; transition: background 0.2s ease; }
      .control-button:hover { background: rgba(255,255,255,0.5); }
    `;
    document.head.appendChild(style);

    const header = document.createElement("div");
    Object.assign(header.style, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "8px", color: "#fff", fontWeight: "600", textShadow: "0 1px 3px rgba(0,0,0,0.4)", cursor: "move", flexShrink: "0" });
    header.innerHTML = `<span>🌸 花火表情包面板</span><span style="cursor:pointer;font-size:16px;">✖</span>`;
    header.querySelector("span:last-child").onclick = () => { panel.style.display = "none"; };
    panel.appendChild(header);

    const grid = document.createElement("div");
    grid.id = "emoji-panel-grid";
    Object.assign(grid.style, { display: "flex", flexWrap: "wrap", justifyContent: "flex-start", overflowY: "auto", flexGrow: "1" });
    panel.appendChild(grid);

    // --- 新增功能:控制区 ---
    const controls = document.createElement("div");
    controls.style.marginTop = "8px";
    controls.style.flexShrink = "0";
    const urlInput = document.createElement("input");
    Object.assign(urlInput.style, { width: "100%", padding: "6px", borderRadius: "6px", border: "1px solid rgba(255,255,255,0.4)", background: "rgba(0,0,0,0.1)", color: "white", marginBottom: "6px" });
    urlInput.placeholder = "粘贴图片链接...";
    const buttonContainer = document.createElement("div");
    Object.assign(buttonContainer.style, { display: "flex", justifyContent: "space-between", gap: "4px" });
    const addButton = document.createElement("button");
    addButton.textContent = "✓ 添加";
    addButton.className = "control-button";
    const deleteModeButton = document.createElement("button");
    deleteModeButton.textContent = "🗑️ 删除";
    deleteModeButton.className = "control-button";
    const refreshButton = document.createElement("button");
    refreshButton.textContent = "🔄 换一批";
    refreshButton.className = "control-button";
    refreshButton.title = "随机更换20个表情包";
    buttonContainer.append(addButton, deleteModeButton, refreshButton);
    controls.append(urlInput, buttonContainer);
    panel.appendChild(controls);

    document.body.appendChild(panel);

    // --- 核心功能重构:渲染所有表情 ---
    function renderEmojis() {
        grid.innerHTML = ''; // 清空
        const createEmojiItem = (url, isCustom) => {
            const item = document.createElement("div");
            item.className = "emoji-item";
            if (isCustom) item.dataset.isCustom = "true";

            const img = document.createElement("img");
            img.src = url;
            img.loading = "lazy";
            Object.assign(img.style, { width: "60px", height: "60px", borderRadius: "10px", margin: "4px", objectFit: "cover", cursor: "pointer", transition: "transform 0.2s ease, box-shadow 0.2s ease" });

            img.onclick = () => {
                // 删除模式逻辑
                if (isDeleteMode && isCustom) {
                    if (confirm("确定要删除这个自定义表情吗?")) {
                        customEmojiList = customEmojiList.filter(e => e !== url);
                        saveCustomEmojis(customEmojiList);
                        renderEmojis();
                        showToast("🗑️ 表情已删除!");
                    }
                    return;
                }
                // 发送模式逻辑
                let finalUrl = url;

                // 如果启用图片代理服务,使用wsrv.nl来调整图片尺寸
                if (USE_IMAGE_PROXY) {
                    // wsrv.nl 参数: w=宽度, h=高度, fit=contain, n=-1(保持所有GIF帧)
                    finalUrl = `${IMAGE_PROXY_URL}${encodeURIComponent(url)}&w=${TARGET_SIZE}&h=${TARGET_SIZE}&fit=contain&n=-1`;
                }

                const markdown = ` ![emote](${finalUrl}) `;
                const input = findInputElement();
                if (input && insertTextAtCursor(input, markdown)) {
                    setTimeout(() => {
                        const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true });
                        input.dispatchEvent(enterEvent);
                    }, 50);
                    showToast("✨ 表情包已发送!");
                }
            };
            item.appendChild(img);
            grid.appendChild(item);
        };

        defaultEmojiList.forEach(url => createEmojiItem(url, false));
        customEmojiList.forEach(url => createEmojiItem(url, true));
    }


    // --- 新增功能:按钮事件监听 ---
    addButton.onclick = () => {
        const url = urlInput.value.trim();
        if (!url || !url.startsWith('http')) {
            showToast("❌ 请输入有效的图片链接!");
            return;
        }
        if (customEmojiList.includes(url)) {
            showToast("😅 这个表情已经添加过啦!");
            return;
        }
        customEmojiList.push(url);
        saveCustomEmojis(customEmojiList);
        renderEmojis();
        urlInput.value = '';
        showToast("✅ 自定义表情已添加!");
        grid.scrollTop = grid.scrollHeight; // 滚动到底部
    };

    deleteModeButton.onclick = () => {
        isDeleteMode = !isDeleteMode;
        panel.classList.toggle('delete-mode', isDeleteMode);
        deleteModeButton.textContent = isDeleteMode ? "✓ 完成" : "🗑️ 删除";
        deleteModeButton.style.background = isDeleteMode ? "rgba(255, 71, 87, 0.5)" : "rgba(255,255,255,0.3)";
    };

    refreshButton.onclick = () => {
        console.log("🔄 点击刷新按钮");
        console.log(`📊 当前状态: allGifFiles.length = ${allGifFiles.length}, isLoading = ${isLoading}`);

        if (allGifFiles.length > 0) {
            refreshEmojis();
        } else {
            showToast("⏳ 表情包列表加载中...");
            console.warn("⚠️ allGifFiles 为空,可能API加载失败");
        }
    };

    toggleBtn.onclick = () => {
        const show = panel.style.display === "none" || !panel.style.display;
        panel.style.display = show ? "flex" : "none";
        panel.style.opacity = show ? "1" : "0";
        panel.style.transform = show ? "translateY(0)" : "translateY(10px)";
        // 退出时,自动关闭删除模式
        if (!show && isDeleteMode) {
            isDeleteMode = false;
            panel.classList.remove('delete-mode');
            deleteModeButton.textContent = "🗑️ 删除";
            deleteModeButton.style.background = "rgba(255,255,255,0.3)";
        }
    };

    // --- 初始化 ---
    customEmojiList = loadCustomEmojis();

    // 显示加载提示
    grid.innerHTML = '<div style="width:100%;text-align:center;color:#fff;padding:20px;">⏳ 正在加载表情包...</div>';

    // 异步加载GitHub表情包列表
    fetchAllGifFiles();

    console.log("🌸 花火表情包面板 已加载");
})();