成人交流 · text · 0 分 · 评论 0

做了一个番号弹出的小脚本

本帖最後由 微微一硬 於 2026-5-6 22:51 編輯

油猴脚本,效果很简单,网页上的jav 番号,鼠标划过去弹出 2 个按钮,一个是快捷复制番号,一个是跳转到 javbus.com 的作品页面(在用 javbus 镜像站的可以自行添加到脚本里)



// ==UserScript==
// @name         JAV hover tools
// @namespace    https://www.javbus.com/
// @version      1.0.0
// @description  网页上出现的番号,鼠标划过浮出复制按钮+跳转 JavBus作品页
// @author       Cod
// @match        *://*/*
// @grant        GM_setClipboard
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const JAVBUS_ORIGIN = 'https://www.javbus.com';
  const MARK_CLASS = 'javbus-number-hover-mark';
  const SKIP_SELECTOR = [
    'script',
    'style',
    'textarea',
    'input',
    'select',
    'option',
    'code',
    'pre',
    '[contenteditable="true"]',
    `.${MARK_CLASS}`,
  ].join(',');

  const numberPattern = /\b(?:FC2(?:[-_\s]*PPV)?[-_\s]*\d{5,8}|[A-Z]{2,8}(?:[-_]\d{2,6}|\d{3,5}))\b/gi;
  const noSeparatorPrefixes = new Set([
    'ABP', 'ABS', 'ADN', 'ADZ', 'AUKG', 'AVOP', 'BDA', 'BF', 'CAWD', 'CJOD',
    'DANDY', 'DASD', 'DVAJ', 'EBOD', 'EKDV', 'FSDSS', 'FSDSSS', 'GENM', 'GVG',
    'HND', 'IPX', 'IPZ', 'JUL', 'JUQ', 'JUX', 'KAWD', 'MIAA', 'MIDE', 'MIGD',
    'MIMK', 'MIRD', 'MKMP', 'MMND', 'MOGI', 'NACR', 'NHDTA', 'NSPS', 'PRED',
    'RBD', 'SDDE', 'SDJS', 'SDMU', 'SDNM', 'SDAB', 'SSIS', 'SSNI', 'START',
    'STARS', 'SW', 'TEK', 'VEC', 'WANZ', 'WAAA', 'XVSR',
  ]);
  const processedNodes = new WeakSet();
  let activeMark = null;
  let hideTimer = 0;

  const style = document.createElement('style');
  style.textContent = `
    .${MARK_CLASS} {
      position: relative;
      border-bottom: 1px dotted rgba(32, 92, 255, 0.85);
      cursor: pointer;
    }

    #javbus-number-hover-toolbar {
      position: fixed;
      z-index: 2147483647;
      display: none;
      align-items: center;
      gap: 6px;
      padding: 5px;
      border: 1px solid rgba(0, 0, 0, 0.12);
      border-radius: 7px;
      background: rgba(255, 255, 255, 0.98);
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
      color: #1d2433;
      font: 12px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      backdrop-filter: blur(8px);
    }

    #javbus-number-hover-toolbar button {
      min-width: 34px;
      height: 26px;
      padding: 0 9px;
      border: 0;
      border-radius: 5px;
      background: #1f6feb;
      color: #fff;
      font: inherit;
      font-weight: 600;
      cursor: pointer;
    }

    #javbus-number-hover-toolbar button:hover {
      background: #1759c7;
    }

    #javbus-number-hover-toolbar button[data-action="open"] {
      background: #1f883d;
    }

    #javbus-number-hover-toolbar button[data-action="open"]:hover {
      background: #187033;
    }
  `;
  document.documentElement.appendChild(style);

  const toolbar = document.createElement('div');
  toolbar.id = 'javbus-number-hover-toolbar';
  toolbar.innerHTML = `
    <button type="button" data-action="copy" title="复制番号">copy</button>
    <button type="button" data-action="open" title="打开 JavBus 页面">JavBus</button>
  `;
  document.documentElement.appendChild(toolbar);

  function normalizeNumber(raw) {
    let text = raw.toUpperCase().replace(/[_\s]+/g, '-').replace(/-+/g, '-');

    if (/^FC2-?PPV-?\d+$/.test(text)) {
      return text.replace(/^FC2-?PPV-?/, 'FC2-PPV-');
    }

    if (/^FC2-?\d+$/.test(text)) {
      return text.replace(/^FC2-?/, 'FC2-');
    }

    if (/^[A-Z]{2,8}\d{2,6}$/.test(text)) {
      text = text.replace(/^([A-Z]{2,8})(\d{2,6})$/, '$1-$2');
    }

    return text;
  }

  function isLikelyJavNumber(raw) {
    const text = raw.toUpperCase().trim();

    if (/^FC2(?:[-_\s]*PPV)?[-_\s]*\d{5,8}$/.test(text)) {
      return true;
    }

    if (/^[A-Z]{2,8}[-_]\d{2,6}$/.test(text)) {
      return true;
    }

    const noSeparatorMatch = text.match(/^([A-Z]{2,8})(\d{3,5})$/);
    if (!noSeparatorMatch) {
      return false;
    }

    return noSeparatorPrefixes.has(noSeparatorMatch[1]);
  }

  function isSkippable(node) {
    const parent = node.parentElement;
    if (!parent || parent.closest(SKIP_SELECTOR)) return true;
    if (!node.nodeValue || !numberPattern.test(node.nodeValue)) return true;
    numberPattern.lastIndex = 0;
    return false;
  }

  function markTextNode(textNode) {
    if (processedNodes.has(textNode) || isSkippable(textNode)) return;
    processedNodes.add(textNode);

    const text = textNode.nodeValue;
    const fragment = document.createDocumentFragment();
    let lastIndex = 0;
    let match;

    numberPattern.lastIndex = 0;
    while ((match = numberPattern.exec(text)) !== null) {
      const rawNumber = match[0];
      if (!isLikelyJavNumber(rawNumber)) continue;

      const start = match.index;
      const end = start + rawNumber.length;

      if (start > lastIndex) {
        fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
      }

      const mark = document.createElement('span');
      mark.className = MARK_CLASS;
      mark.dataset.javNumber = normalizeNumber(rawNumber);
      mark.textContent = rawNumber;
      fragment.appendChild(mark);
      lastIndex = end;
    }

    if (lastIndex < text.length) {
      fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
    }

    if (fragment.childNodes.length > 1 || lastIndex > 0) {
      textNode.parentNode.replaceChild(fragment, textNode);
    }
  }

  function scan(root = document.body) {
    if (!root) return;

    if (root.nodeType === Node.TEXT_NODE) {
      markTextNode(root);
      return;
    }

    if (root.nodeType !== Node.ELEMENT_NODE || root.closest?.(SKIP_SELECTOR)) return;

    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        return isSkippable(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
      },
    });

    const textNodes = [];
    let node;
    while ((node = walker.nextNode())) {
      textNodes.push(node);
    }

    textNodes.forEach(markTextNode);
  }

  function showToolbar(mark) {
    activeMark = mark;
    clearTimeout(hideTimer);

    const rect = mark.getBoundingClientRect();
    toolbar.style.display = 'flex';
    const toolbarRect = toolbar.getBoundingClientRect();

    const top = Math.max(8, rect.top - toolbarRect.height - 7);
    const left = Math.min(
      window.innerWidth - toolbarRect.width - 8,
      Math.max(8, rect.left + rect.width / 2 - toolbarRect.width / 2),
    );

    toolbar.style.top = `${top}px`;
    toolbar.style.left = `${left}px`;
  }

  function scheduleHide() {
    clearTimeout(hideTimer);
    hideTimer = window.setTimeout(() => {
      toolbar.style.display = 'none';
      activeMark = null;
    }, 220);
  }

  async function copyText(text) {
    if (typeof GM_setClipboard === 'function') {
      GM_setClipboard(text, 'text');
      return;
    }

    await navigator.clipboard.writeText(text);
  }

  document.addEventListener('mouseover', (event) => {
    const mark = event.target.closest?.(`.${MARK_CLASS}`);
    if (mark) showToolbar(mark);
  });

  document.addEventListener('mouseout', (event) => {
    const mark = event.target.closest?.(`.${MARK_CLASS}`);
    if (mark && !toolbar.matches(':hover')) scheduleHide();
  });

  toolbar.addEventListener('mouseenter', () => clearTimeout(hideTimer));
  toolbar.addEventListener('mouseleave', scheduleHide);

  toolbar.addEventListener('click', async (event) => {
    const button = event.target.closest('button');
    if (!button || !activeMark) return;

    const number = activeMark.dataset.javNumber;
    if (button.dataset.action === 'copy') {
      await copyText(number);
      button.textContent = '✅';
      window.setTimeout(() => {
        button.textContent = 'copy';
      }, 900);
      return;
    }

    window.open(`${JAVBUS_ORIGIN}/${encodeURIComponent(number)}`, '_blank', 'noopener');
  });

  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        scan(node);
      }
    }
  });

  scan();
  observer.observe(document.documentElement, { childList: true, subtree: true });
})();複製代碼



评论 (0)

登录 后参与讨论。

还没有评论,来抢沙发吧。