logo NodeSeekbeta

【论坛优化】分享一个自制的 "花火" 表情包论坛扩展 也可自定义添加表情包 v0.3.4 油猴脚本

今天晚上摸鱼把一些网上的花火表情包搜集了一下,转化了一下表情包的尺寸大小,图床是用的是论坛官方图床,点击即可插入表情包,给论坛带来更多欢愉

增加了预览功能,修复了部分脚本错误

image|

使用演示 GIF

M04vdGZ7nZ21KrmRvavYPHYnjpeeSiHC.gif

表情包示例

emote emote emote emote emote

代码 复制全部内容,在油猴插件里新建脚本粘贴即可~

// ==UserScript==
// @name         花火御用表情包面板 一键爆炸 + 悬浮/长按预览(修复)
// @namespace    https://deepflood.com/
// @version      0.3.4
// @description  自定义表情;点击即发并复制;鼠标悬浮/触摸长按预览;修复预览偶发不出现与前置空格错位;移除重复声明导致的启动失败
// @author       Sparkle
// @match        *://www.deepflood.com/*
// @match        *://www.nodeseek.com/*
// @grant        none
// @icon         https://cdn.nodeimage.com/i/IEyH4NN7T0gzIj2GNDLEkMNfxi5e7S5R.png
// ==/UserScript==

(function () {
  'use strict';

  // === 默认表情 ===
  const defaultEmojiList = [
    "https://cdn.nodeimage.com/i/kiTPNRkkUMXrFbcqiQjInY997EXX8ugQ.png",
    "https://cdn.nodeimage.com/i/yNwUePEGVy7u15Dav0dsnKe44c3YJvAK.gif",
    "https://cdn.nodeimage.com/i/YtrI0qXlSjH1vjIVEPCIFrra7p93S9kn.webp",
    "https://cdn.nodeimage.com/i/BAyT6QNxu41dsi2jLwmhfE2qaRiIxyY6.webp",
    "https://cdn.nodeimage.com/i/p4NKizdYxK7YtdBjHmuYqaogU4QBYaUd.webp",
    "https://cdn.nodeimage.com/i/vTcG6KHwda3ZzsWCYHxPIXI36cPvFgaR.avif",
    "https://cdn.nodeimage.com/i/MdE5TIiLs1sogvre8Rc1TunNkROvwXbR.png",
    "https://cdn.nodeimage.com/i/DLckJTHxmoTysBWShRGhXcmY3bmB56TG.png",
    "https://cdn.nodeimage.com/i/rM7RVynnxChinGGZXXc1kzcJNaPsxx09.webp",
    "https://cdn.nodeimage.com/i/nJj1NJMN3FrvCjiYxLjDGnnMQrOuodX0.webp",
    "https://cdn.nodeimage.com/i/dI2NfaGlg4LMHsJJYbj6Deg7AvSrvfPW.webp",
    "https://cdn.nodeimage.com/i/v8bbgwPmDhn8CJfRLoJfTExECZXY9cAJ.avif",
    "https://cdn.nodeimage.com/i/s6yvTaNndMqSW2237VHvVaGbGUrtR482.avif",
    "https://cdn.nodeimage.com/i/IqtoK3LNY6UH8F49SMH6PnrdHbJDxbL5.avif",
    "https://cdn.nodeimage.com/i/EcfVbQVEhIQuhhZfz3GfmUbXbOugaTKs.avif",
    "https://cdn.nodeimage.com/i/jjmCWuOeedjWFyUMI1tM9KDLpK0YzMuU.avif",
    "https://cdn.nodeimage.com/i/d2Ybd1xx8ejNkfu7Rvv6YXHEEcj4dDuE.avif",
    "https://cdn.nodeimage.com/i/a53uo9CgZlL78BqIResfFmVJSk6YSVpi.webp",
    "https://cdn.nodeimage.com/i/2Jmuo2O6rd2BAwUv5IPnzrqOndF4GaXH.avif",
    "https://cdn.nodeimage.com/i/aa5u2pyhmLgxiqNye1Bm600GdFgQwnoH.avif",
    "https://cdn.nodeimage.com/i/sg1QIipECwsa0mVAXhGtDEUtrXz32I9j.avif",
    "https://cdn.nodeimage.com/i/Z24cydZ2nUL5jYmIxYbx8PTCgHUYWHVk.avif",
    "https://cdn.nodeimage.com/i/ixMkhZuRNt39ObpRpY5XTyNdyEpVenGh.avif",
    "https://cdn.nodeimage.com/i/qJbzpaPkUflHQxf0aUszKZrEgQzta5jd.webp",
    "https://cdn.nodeimage.com/i/Bjm9HjOzzaXTqNUWYx1yZexGs0lIN9so.webp",
    "https://cdn.nodeimage.com/i/3TyPkkNXQ2iWsT2cuUBb5lqNEXkhQwMu.gif",
    "https://cdn.nodeimage.com/i/Rs1eZBpod5QsSoV1kIv1husXtopA6AcI.png",
    "https://cdn.nodeimage.com/i/Gy9MjsQyA7tW6kkRBux6Vurjw8tIuGAX.png",
    "https://cdn.nodeimage.com/i/4dQT9ylVI51vzTvwdWAzZalZCOXS3RYc.png",
    "https://cdn.nodeimage.com/i/5Z2V8HGqrFYzTdJDdoNAvIX4WQIQnBZz.png",
    "https://cdn.nodeimage.com/i/yIdwRnUajcvuVOlHaO5647PdMcRua2yS.png",
    "https://cdn.nodeimage.com/i/IEyH4NN7T0gzIj2GNDLEkMNfxi5e7S5R.png",
    "https://cdn.nodeimage.com/i/Q05BMry7UbL1QzNAAr0b4CO1jwB7po5v.png",
    "https://cdn.nodeimage.com/i/jCc6NDHOEP8AmpWifMkopF0w1vcNJVMC.png",
    "https://cdn.nodeimage.com/i/cLmlEhOL5qv1r7UNW6MgRuYePyupd35k.png",
    "https://cdn.nodeimage.com/i/5qP7UR9sOgTBVH63ZXf5pWPiMq18iTyK.png",
    "https://cdn.nodeimage.com/i/43KwtgzDgaU2C91WXwp4ItBKHjsevHQE.png",
    "https://cdn.nodeimage.com/i/mRtWtxLeHYXdhM4yXmKAvPAZlkTzHfSl.gif",
    "https://cdn.nodeimage.com/i/Jrn7JvYVY7dAhQHMrQ0i2ixfaDDvxGYU.png"
  ];

  // === 全局 ===
  const STORAGE_KEY = 'hanabi_custom_emojis';
  let isDeleteMode = false;
  let customEmojiList = [];
  const PREVIEW_MAX_W = 260;
  const PREVIEW_MAX_H = 260;
  const PREVIEW_OFFSET = 14;

  // 预览状态 & 防抖
  let lastPointerX = 0, lastPointerY = 0;
  let hideTimer = null;

  // === 存储 ===
  function loadCustomEmojis() {
    try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
    catch { return []; }
  }
  function saveCustomEmojis(emojis) {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(emojis)); } catch {}
  }

  // === 输入框 ===
  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 s of selectors) {
      const el = document.querySelector(s);
      if (el && !el.disabled && !el.readOnly && el.offsetWidth > 0 && el.offsetHeight > 0) return el;
    }
    const f = document.activeElement;
    return (f && (f.tagName === 'TEXTAREA' || (f.tagName === 'INPUT' && f.type === 'text'))) ? f : 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;
      el.value = el.value.slice(0, s) + text + el.value.slice(el.selectionEnd || el.value.length);
      el.selectionStart = el.selectionEnd = s + text.length;
    }
    el.dispatchEvent(new Event('input', { bubbles: true }));
    return true;
  }

  // === Toast ===
  function showToast(msg) {
    const t = document.createElement('div');
    t.textContent = msg;
    Object.assign(t.style, {
      position:'fixed', bottom:'90px', right:'20px', padding:'10px 20px', borderRadius:'12px',
      background:'rgba(0,0,0,0.65)', color:'#fff', fontWeight:'500', fontSize:'15px',
      boxShadow:'0 4px 12px rgba(0,0,0,0.2)', zIndex:'100002',
      opacity:'0', transform:'translateY(10px)', transition:'opacity .2s ease, transform .2s ease'
    });
    document.body.appendChild(t);
    requestAnimationFrame(()=>{ t.style.opacity='1'; t.style.transform='translateY(0)'; });
    setTimeout(()=>{ t.style.opacity='0'; t.style.transform='translateY(10px)'; setTimeout(()=>t.remove(),220); },1500);
  }

  // === 复制 ===
  function copyToClipboard(text){
    if(navigator.clipboard?.writeText) return navigator.clipboard.writeText(text).then(()=>true).catch(()=>false);
    try{
      const ta=document.createElement('textarea');
      ta.value=text; ta.style.position='fixed'; ta.style.opacity='0';
      document.body.appendChild(ta); ta.select();
      const ok=document.execCommand('copy'); document.body.removeChild(ta);
      return Promise.resolve(ok);
    }catch{ return Promise.resolve(false); }
  }

  // === 浮动按钮 ===
  const toggleBtn = document.createElement('img');
  toggleBtn.src = "https://cdn.nodeimage.com/i/IEyH4NN7T0gzIj2GNDLEkMNfxi5e7S5R.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 .25s, box-shadow .25s'
  });
  toggleBtn.addEventListener('mouseenter',()=>{ toggleBtn.style.transform='scale(1.1)'; toggleBtn.style.boxShadow='0 6px 20px rgba(0,0,0,.35)'; });
  toggleBtn.addEventListener('mouseleave',()=>{ toggleBtn.style.transform='scale(1)'; toggleBtn.style.boxShadow='0 4px 18px rgba(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', maxHeight:'50vh',
    display:'none', flexDirection:'column', padding:'10px',
    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',
    color:'#222', transition:'opacity .3s ease, transform .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 .2s; }
    .control-button:hover { background: rgba(255,255,255,0.5); }

    #emoji-preview {
      position: fixed; top: 0; left: 0; display: none; padding: 8px; border-radius: 12px;
      background: rgba(0,0,0,0.45); backdrop-filter: blur(10px) saturate(160%);
      box-shadow: 0 10px 30px rgba(0,0,0,0.35); z-index: 100001; pointer-events: none;
      opacity: 0; transform: translate3d(0,0,0) scale(0.98); transition: opacity .12s ease, transform .12s ease;
    }
    #emoji-preview.show { opacity: 1; transform: translate3d(0,0,0) scale(1); }
    #emoji-preview img { display:block; max-width:${PREVIEW_MAX_W}px; max-height:${PREVIEW_MAX_H}px; border-radius:8px; object-fit:contain; }
  `;
  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);

  // 控制区(创建即绑定事件,不再在后面 querySelector/重复声明)
  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'});
  const addButton = document.createElement('button'); addButton.textContent='✓ 添加表情'; addButton.className='control-button';
  const deleteModeButton = document.createElement('button'); deleteModeButton.textContent='🗑️ 删除模式'; deleteModeButton.className='control-button';
  buttonContainer.append(addButton, deleteModeButton);
  controls.append(urlInput, buttonContainer);
  panel.appendChild(controls);
  document.body.appendChild(panel);

  // === 预览元素 ===
  const preview = document.createElement('div'); preview.id='emoji-preview';
  const previewImg = document.createElement('img');
  preview.appendChild(previewImg); document.body.appendChild(preview);

  function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
  function positionPreview(x, y){
    preview.style.left = `${x + PREVIEW_OFFSET}px`;
    preview.style.top  = `${y + PREVIEW_OFFSET}px`;
    const rect = preview.getBoundingClientRect();
    const nx = clamp(rect.left, 8, window.innerWidth - rect.width - 8);
    const ny = clamp(rect.top , 8, window.innerHeight - rect.height - 8);
    preview.style.left = `${nx}px`; preview.style.top = `${ny}px`;
  }
  function showPreview(src, x, y){
    if (!src) return;
    if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
    lastPointerX = x; lastPointerY = y;
    previewImg.onload = ()=>{ positionPreview(lastPointerX, lastPointerY); };
    previewImg.onerror = ()=>{ showToast('图片预览加载失败'); };
    previewImg.decoding = 'async';
    previewImg.src = src;
    preview.style.display = 'block';
    requestAnimationFrame(()=>{ positionPreview(x, y); preview.classList.add('show'); });
  }
  function movePreview(x, y){
    if (preview.style.display !== 'block') return;
    lastPointerX = x; lastPointerY = y;
    positionPreview(x, y);
  }
  function hidePreview(){
    if (hideTimer) clearTimeout(hideTimer);
    hideTimer = setTimeout(()=>{
      preview.classList.remove('show');
      setTimeout(()=>{ preview.style.display='none'; }, 120);
    }, 120);
  }

  // === 渲染 ===
  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:'64px',height:'64px',borderRadius:'10px',margin:'4px',objectFit:'cover',cursor:'pointer',transition:'transform .2s, box-shadow .2s'});

      // 点击发送(去掉多余空格)
      let suppressNextClick = false;
      img.onclick = ()=>{
        if (suppressNextClick) { suppressNextClick = false; return; }
        if (isDeleteMode && isCustom){
          if (confirm('确定要删除这个自定义表情吗?')){
            customEmojiList = customEmojiList.filter(e=>e!==url);
            saveCustomEmojis(customEmojiList);
            renderEmojis(); showToast('🗑️ 表情已删除!');
          }
          return;
        }
        const markdown = `![emote](${url})`; // 不带前后空格,避免错位
        copyToClipboard(markdown);
        const input = findInputElement();
        if (input && insertTextAtCursor(input, markdown)){
          setTimeout(()=>{
            const ev = new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true,cancelable:true});
            input.dispatchEvent(ev);
          },50);
          showToast('✨ 表情包已发送并复制!');
        }
      };

      // 悬浮预览(鼠标)
      img.addEventListener('mouseenter', e=>{ showPreview(url, e.clientX, e.clientY); });
      img.addEventListener('mousemove',  e=>{ movePreview(e.clientX, e.clientY); });
      img.addEventListener('mouseleave', hidePreview);

      // 长按预览(触摸)
      let pressTimer=null, longPress=false, startX=0, startY=0;
      const LONG_MS=450, MOVE_CANCEL=10;
      img.addEventListener('contextmenu', e=>e.preventDefault());
      img.addEventListener('touchstart', e=>{
        if (e.touches.length!==1) return;
        const t = e.touches[0];
        startX=t.clientX; startY=t.clientY; longPress=false; suppressNextClick=false;
        pressTimer=setTimeout(()=>{ longPress=true; suppressNextClick=true; showPreview(url, startX, startY); }, LONG_MS);
      }, {passive:true});
      img.addEventListener('touchmove', e=>{
        if (e.touches.length!==1) return;
        const t=e.touches[0], dx=t.clientX-startX, dy=t.clientY-startY;
        if (!longPress){
          if (Math.hypot(dx,dy)>MOVE_CANCEL){ clearTimeout(pressTimer); pressTimer=null; }
        } else {
          movePreview(t.clientX, t.clientY);
        }
      }, {passive:true});
      function endTouch(){ clearTimeout(pressTimer); pressTimer=null; if (longPress) hidePreview(); }
      img.addEventListener('touchend', endTouch, {passive:true});
      img.addEventListener('touchcancel', endTouch, {passive:true});

      item.appendChild(img); grid.appendChild(item);
    };
    defaultEmojiList.forEach(u=>createEmojiItem(u,false));
    customEmojiList.forEach(u=>createEmojiItem(u,true));
  }

  // === 控件事件:直接使用创建时的 addButton / deleteModeButton ===
  addButton.onclick = ()=>{
    const url = urlInput.value.trim();
    if (!/^https?:\/\//i.test(url)) return showToast('❌ 请输入有效的图片链接!');
    if (customEmojiList.includes(url)) return showToast('😅 这个表情已经添加过啦!');
    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)';
  };
  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)';
    }
    if (!show) { if (hideTimer) { clearTimeout(hideTimer); hideTimer=null; } preview.style.display='none'; preview.classList.remove('show'); }
  };

  // === 初始化 ===
  customEmojiList = loadCustomEmojis();
  renderEmojis();
  console.log('🌸 花火表情包面板 已加载(修复预览 & 去除多余空格 & 无重复声明)');
})();

12345
12345

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有43595位seeker

🎉欢迎新用户🎉