Quick Dictionary Lookup

Highlight a word/phrase and click the popup to see its definition, with fallback and retry.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Quick Dictionary Lookup
// @namespace    https://example.com/
// @version      1.5
// @description  Highlight a word/phrase and click the popup to see its definition, with fallback and retry.
// @author       ChatGPT
// @match        *://*/*
// @license      MIT
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  let badge = null;
  let tooltip = null;
  let lastSelection = '';

  // Styles
  GM_addStyle(`
    .dict-badge {
      position: absolute;
      background: #1f5aff;
      color: white;
      padding: 4px 8px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
      z-index: 999999;
      box-shadow: 0 2px 8px rgba(0,0,0,0.3);
      user-select: none;
    }
    .dict-tooltip {
      position: absolute;
      background: white;
      color: #222;
      padding: 12px;
      border: 1px solid #ccc;
      border-radius: 6px;
      max-width: 320px;
      font-size: 13px;
      z-index: 999999;
      box-shadow: 0 4px 16px rgba(0,0,0,0.2);
      line-height: 1.3;
      font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
    }
    .dict-term {
      font-weight: bold;
      margin-bottom: 6px;
      display: block;
    }
    .dict-error {
      color: #b33;
      font-style: italic;
    }
    .dict-close {
      position: absolute;
      top: 4px;
      right: 6px;
      cursor: pointer;
      font-weight: bold;
    }
    .dict-link {
      margin-top: 6px;
      display: inline-block;
      font-size: 12px;
    }
  `);

  // Utility: clean up term
  function normalizeTerm(raw) {
    if (!raw) return '';
    let t = raw.trim().replace(/\s+/g, ' ');
    // strip surrounding punctuation except internal (like "word," -> "word")
    t = t.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, '');
    return t.toLowerCase();
  }

  // Remove existing badge
  function removeBadge() {
    if (badge) {
      badge.remove();
      badge = null;
    }
  }

  // Remove tooltip
  function removeTooltip() {
    if (tooltip) {
      tooltip.remove();
      tooltip = null;
    }
  }

  // Fetch definition from dictionaryapi.dev with structured result
  async function fetchDefinition(term) {
    try {
      const resp = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(term)}`, {
        cache: 'no-store',
      });
      const text = await resp.text();
      let data;
      try {
        data = JSON.parse(text);
      } catch (e) {
        console.warn('Dictionary API: failed to parse JSON for term:', term, 'raw response:', text);
        throw new Error('Invalid JSON from dictionary API');
      }
      console.debug('Dictionary API response for', term, data);
      if (!resp.ok || !Array.isArray(data) || !data[0].meanings) {
        return { found: false, raw: data };
      }
      return { found: true, data };
    } catch (err) {
      console.warn('Dictionary API fetch error for term:', term, err);
      return { found: false, error: err };
    }
  }

  // Create badge near selection
  function createBadge(x, y, rawText) {
    removeBadge();
    removeTooltip();
    badge = document.createElement('div');
    badge.className = 'dict-badge';
    badge.textContent = '📘 Dictionary';
    document.body.appendChild(badge);
    // adjust if offscreen
    const offsetX = 4;
    const offsetY = 4;
    badge.style.left = `${Math.min(x + offsetX, window.scrollX + window.innerWidth - 120)}px`;
    badge.style.top = `${Math.max(y + offsetY, window.scrollY + 4)}px`;

    badge.addEventListener('click', () => {
      showDefinition(rawText, parseInt(badge.style.left, 10), parseInt(badge.style.top, 10) + badge.offsetHeight + 4);
    });
  }

  // Show tooltip with definition, fallback, and retry
  function showDefinition(rawTerm, x, y) {
    const term = normalizeTerm(rawTerm) || rawTerm;
    removeTooltip();
    tooltip = document.createElement('div');
    tooltip.className = 'dict-tooltip';
    tooltip.style.left = `${x}px`;
    tooltip.style.top = `${y}px`;

    // Close button
    const closeBtn = document.createElement('span');
    closeBtn.className = 'dict-close';
    closeBtn.textContent = '×';
    closeBtn.title = 'Close';
    closeBtn.onclick = () => removeTooltip();
    tooltip.appendChild(closeBtn);

    // Title
    const title = document.createElement('div');
    title.className = 'dict-term';
    title.textContent = term;
    tooltip.appendChild(title);

    // Content placeholder
    const content = document.createElement('div');
    content.textContent = 'Loading...';
    tooltip.appendChild(content);

    document.body.appendChild(tooltip);

    // Fetch definition
    fetchDefinition(term).then((result) => {
      content.innerHTML = '';
      if (result.found) {
        const meanings = result.data[0].meanings.slice(0, 2);
        meanings.forEach((meaning) => {
          const part = document.createElement('div');
          part.style.marginBottom = '6px';
          const partTitle = document.createElement('div');
          partTitle.textContent = meaning.partOfSpeech;
          partTitle.style.fontStyle = 'italic';
          part.appendChild(partTitle);
          meaning.definitions.slice(0, 2).forEach((def) => {
            const d = document.createElement('div');
            d.textContent = `• ${def.definition}`;
            part.appendChild(d);
          });
          content.appendChild(part);
        });
      } else {
        let msg = 'Definition not found.';
        if (result.raw && result.raw.title) {
          msg += ` (${result.raw.title})`;
        } else if (result.error) {
          msg += ' (API error)';
        }
        const errorDiv = document.createElement('div');
        errorDiv.className = 'dict-error';
        errorDiv.innerHTML = `${msg} <a href="https://www.google.com/search?q=define+${encodeURIComponent(term)}" target="_blank" rel="noreferrer">Search web</a>`;
        content.appendChild(errorDiv);
      }

      // Retry link
      const retry = document.createElement('div');
      retry.className = 'dict-link';
      retry.innerHTML = `<a href="#" title="Try fetching again">Retry</a>`;
      retry.querySelector('a').addEventListener('click', (e) => {
        e.preventDefault();
        content.textContent = 'Retrying...';
        showDefinition(rawTerm, x, y); // re-open (simpler than partial update)
      });
      content.appendChild(retry);

      // Full search link
      const more = document.createElement('div');
      more.className = 'dict-link';
      more.innerHTML = `<a href="https://www.google.com/search?q=define+${encodeURIComponent(term)}" target="_blank" rel="noreferrer">Full search</a>`;
      content.appendChild(more);
    });
  }

  // Event: selection finished
  document.addEventListener('mouseup', (e) => {
    setTimeout(() => {
      const selObj = window.getSelection();
      const selection = selObj.toString().trim();
      if (selection && selection !== lastSelection) {
        lastSelection = selection;
        let x = e.pageX;
        let y = e.pageY;
        try {
          const range = selObj.getRangeAt(0);
          const rect = range.getBoundingClientRect();
          x = rect.right + window.scrollX;
          y = rect.top + window.scrollY;
        } catch (err) {
          // ignore, fallback to mouse coords
        }
        createBadge(x, y, selection);
      } else if (!selection) {
        removeBadge();
        removeTooltip();
      }
    }, 10);
  });

  // Close tooltip when clicking outside
  document.addEventListener('mousedown', (e) => {
    if (tooltip && !tooltip.contains(e.target) && badge && !badge.contains(e.target)) {
      removeTooltip();
    }
  });

  // Escape closes everything
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      removeBadge();
      removeTooltip();
    }
  });
})();// ==UserScript==
// @name         Quick Dictionary Lookup (Debug & Robust)
// @namespace    https://example.com/
// @version      0.4
// @description  Highlight a word/phrase and click the popup to see its definition, with detailed error reporting, retry, and fallback.
// @author       ChatGPT
// @match        *://*/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  let badge = null;
  let tooltip = null;
  let lastSelection = '';

  GM_addStyle(`
    .dict-badge {
      position: absolute;
      background: #1f5aff;
      color: white;
      padding: 4px 8px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
      z-index: 999999;
      box-shadow: 0 2px 8px rgba(0,0,0,0.3);
      user-select: none;
    }
    .dict-tooltip {
      position: absolute;
      background: white;
      color: #222;
      padding: 12px;
      border: 1px solid #ccc;
      border-radius: 6px;
      max-width: 360px;
      font-size: 13px;
      z-index: 999999;
      box-shadow: 0 4px 16px rgba(0,0,0,0.2);
      line-height: 1.3;
      font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
    }
    .dict-term {
      font-weight: bold;
      margin-bottom: 6px;
      display: block;
    }
    .dict-error {
      color: #b33;
      font-style: italic;
    }
    .dict-close {
      position: absolute;
      top: 4px;
      right: 6px;
      cursor: pointer;
      font-weight: bold;
    }
    .dict-link {
      margin-top: 6px;
      display: inline-block;
      font-size: 12px;
    }
    .dict-meta {
      margin-top: 4px;
      font-size: 11px;
      color: #555;
    }
  `);

  function normalizeTerm(raw) {
    if (!raw) return '';
    let t = raw.trim().replace(/\s+/g, ' ');
    t = t.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, '');
    return t.toLowerCase();
  }

  function removeBadge() {
    if (badge) {
      badge.remove();
      badge = null;
    }
  }

  function removeTooltip() {
    if (tooltip) {
      tooltip.remove();
      tooltip = null;
    }
  }

  async function fetchDefinition(term) {
    try {
      const resp = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(term)}`, {
        cache: 'no-store',
      });
      const text = await resp.text();
      let data;
      try {
        data = JSON.parse(text);
      } catch (e) {
        console.warn('Dictionary API: JSON parse failure for term:', term, 'raw:', text);
        return { found: false, error: new Error('Invalid JSON'), rawText: text, status: resp.status };
      }
      console.debug('Dictionary API response for', term, data);
      if (!resp.ok || !Array.isArray(data) || !data[0].meanings) {
        return { found: false, raw: data, status: resp.status };
      }
      return { found: true, data, status: resp.status };
    } catch (err) {
      console.warn('Dictionary API fetch error for term:', term, err);
      return { found: false, error: err };
    }
  }

  function createBadge(x, y, rawText) {
    removeBadge();
    removeTooltip();
    badge = document.createElement('div');
    badge.className = 'dict-badge';
    badge.textContent = '📘 Dictionary';
    document.body.appendChild(badge);
    const offsetX = 4;
    const offsetY = 4;
    badge.style.left = `${Math.min(x + offsetX, window.scrollX + window.innerWidth - 140)}px`;
    badge.style.top = `${Math.max(y + offsetY, window.scrollY + 4)}px`;

    badge.addEventListener('click', () => {
      showDefinition(rawText, parseInt(badge.style.left, 10), parseInt(badge.style.top, 10) + badge.offsetHeight + 4);
    });
  }

  function showDefinition(rawTerm, x, y) {
    const term = normalizeTerm(rawTerm) || rawTerm;
    removeTooltip();
    tooltip = document.createElement('div');
    tooltip.className = 'dict-tooltip';
    tooltip.style.left = `${x}px`;
    tooltip.style.top = `${y}px`;

    const closeBtn = document.createElement('span');
    closeBtn.className = 'dict-close';
    closeBtn.textContent = '×';
    closeBtn.title = 'Close';
    closeBtn.onclick = () => removeTooltip();
    tooltip.appendChild(closeBtn);

    const title = document.createElement('div');
    title.className = 'dict-term';
    title.textContent = term;
    tooltip.appendChild(title);

    const content = document.createElement('div');
    content.textContent = 'Loading...';
    tooltip.appendChild(content);

    document.body.appendChild(tooltip);

    fetchDefinition(term).then((result) => {
      content.innerHTML = '';

      if (result.found) {
        const meanings = result.data[0].meanings.slice(0, 2);
        meanings.forEach((meaning) => {
          const part = document.createElement('div');
          part.style.marginBottom = '6px';
          const partTitle = document.createElement('div');
          partTitle.textContent = meaning.partOfSpeech;
          partTitle.style.fontStyle = 'italic';
          part.appendChild(partTitle);
          meaning.definitions.slice(0, 2).forEach((def) => {
            const d = document.createElement('div');
            d.textContent = `• ${def.definition}`;
            part.appendChild(d);
          });
          content.appendChild(part);
        });
      } else {
        let msg = 'Definition not found.';
        if (result.status) msg += ` (HTTP ${result.status})`;
        if (result.raw && result.raw.title) {
          msg += ` — ${result.raw.title}`;
        } else if (result.error) {
          msg += ` — ${result.error.message || 'Network/other error'}`;
        }
        const errorDiv = document.createElement('div');
        errorDiv.className = 'dict-error';
        errorDiv.innerHTML = `${msg} <br><a href="https://www.google.com/search?q=define+${encodeURIComponent(term)}" target="_blank" rel="noreferrer">Search web</a>`;
        content.appendChild(errorDiv);
      }

      const meta = document.createElement('div');
      meta.className = 'dict-meta';
      const parts = [];
      if (result.status !== undefined) parts.push(`Status: ${result.status}`);
      if (result.error) parts.push(`Error: ${result.error.message}`);
      if (parts.length) meta.textContent = parts.join(' | ');
      if (parts.length) content.appendChild(meta);

      const retry = document.createElement('div');
      retry.className = 'dict-link';
      retry.innerHTML = `<a href="#" title="Retry lookup">Retry</a>`;
      retry.querySelector('a').addEventListener('click', (e) => {
        e.preventDefault();
        content.textContent = 'Retrying...';
        showDefinition(rawTerm, x, y);
      });
      content.appendChild(retry);

      const more = document.createElement('div');
      more.className = 'dict-link';
      more.innerHTML = `<a href="https://www.google.com/search?q=define+${encodeURIComponent(term)}" target="_blank" rel="noreferrer">Full search</a>`;
      content.appendChild(more);
    });
  }

  document.addEventListener('mouseup', (e) => {
    setTimeout(() => {
      const selObj = window.getSelection();
      const selection = selObj.toString().trim();
      if (selection && selection !== lastSelection) {
        lastSelection = selection;
        let x = e.pageX;
        let y = e.pageY;
        try {
          const range = selObj.getRangeAt(0);
          const rect = range.getBoundingClientRect();
          x = rect.right + window.scrollX;
          y = rect.top + window.scrollY;
        } catch (_) {}
        createBadge(x, y, selection);
      } else if (!selection) {
        removeBadge();
        removeTooltip();
      }
    }, 10);
  });

  document.addEventListener('mousedown', (e) => {
    if (tooltip && !tooltip.contains(e.target) && badge && !badge.contains(e.target)) {
      removeTooltip();
    }
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      removeBadge();
      removeTooltip();
    }
  });
})();