Text Explainer

Explain selected text using LLM

目前為 2025-03-05 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Text Explainer
// @namespace    http://tampermonkey.net/
// @version      0.2.3
// @description  Explain selected text using LLM
// @author       RoCry
// @icon         data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAxOTIgMTkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiPjxjaXJjbGUgY3g9IjExNiIgY3k9Ijc2IiByPSI1NCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEyIi8+PHBhdGggc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMTIiIGQ9Ik04Ni41IDEyMS41IDQxIDE2N2MtNC40MTggNC40MTgtMTEuNTgyIDQuNDE4LTE2IDB2MGMtNC40MTgtNC40MTgtNC40MTgtMTEuNTgyIDAtMTZsNDQuNS00NC41TTkyIDYybDEyIDMyIDEyLTMyIDEyIDMyIDEyLTMyIi8+PC9zdmc+
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      generativelanguage.googleapis.com
// @connect      *
// @run-at       document-end
// @inject-into  content
// @require      https://update.gf.qytechs.cn/scripts/528704/1547031/SmolLLM.js
// @require      https://update.gf.qytechs.cn/scripts/528703/1546610/SimpleBalancer.js
// @require      https://update.gf.qytechs.cn/scripts/528763/1547460/Text%20Explainer%20Settings.js
// @require      https://update.gf.qytechs.cn/scripts/528822/1547501/Selection%20Context.js
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // Initialize settings manager with extended default config
  const settingsManager = new TextExplainerSettings({
    model: "gemini-2.0-flash",
    apiKey: null,
    baseUrl: "https://generativelanguage.googleapis.com",
    provider: "gemini",
    language: "Chinese", // Default language
    shortcut: {
      key: "d",
      ctrlKey: false,
      altKey: true,
      shiftKey: false,
      metaKey: false
    },
    floatingButton: {
      enabled: true,
      size: "medium",
      position: "bottom-right"
    }
  });

  // Get current configuration
  let config = settingsManager.getAll();

  // Initialize SmolLLM
  let llm;
  try {
    llm = new SmolLLM();
  } catch (error) {
    console.error('Failed to initialize SmolLLM:', error);
    llm = null;
  }

  // Check if device is touch-enabled
  const isTouchDevice = () => {
    return ('ontouchstart' in window) ||
      (navigator.maxTouchPoints > 0) ||
      (navigator.msMaxTouchPoints > 0);
  };

  const isIOS = () => {
    return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
  };

  // Create and manage floating button
  let floatingButton = null;

  function createFloatingButton() {
    if (floatingButton) return;

    floatingButton = document.createElement('div');
    floatingButton.id = 'explainer-floating-button';

    // Determine size based on settings
    let buttonSize;
    switch (config.floatingButton.size) {
      case 'small': buttonSize = '40px'; break;
      case 'large': buttonSize = '60px'; break;
      default: buttonSize = '50px'; // medium
    }

    // Position based on settings
    let positionCSS;
    switch (config.floatingButton.position) {
      case 'top-left': positionCSS = 'top: 20px; left: 20px;'; break;
      case 'top-right': positionCSS = 'top: 20px; right: 20px;'; break;
      case 'bottom-left': positionCSS = 'bottom: 20px; left: 20px;'; break;
      default: positionCSS = 'bottom: 20px; right: 20px;'; // bottom-right
    }

    floatingButton.style.cssText = `
      width: ${buttonSize};
      height: ${buttonSize};
      border-radius: 50%;
      background-color: rgba(33, 150, 243, 0.8);
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      position: fixed;
      ${positionCSS}
      z-index: 9999;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
      cursor: pointer;
      font-weight: bold;
      font-size: ${parseInt(buttonSize) * 0.4}px;
      opacity: 0;
      transition: opacity 0.3s ease, transform 0.2s ease;
      pointer-events: none;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
    `;

    // Add icon or text
    floatingButton.innerHTML = '✓';

    // Add to DOM
    document.body.appendChild(floatingButton);

    // Add click event
    floatingButton.addEventListener('click', (e) => {
      e.preventDefault();
      processSelectedText();
    });

    // Prevent text selection on button
    floatingButton.addEventListener('mousedown', (e) => {
      e.preventDefault();
    });
  }

  function showFloatingButton() {
    if (!floatingButton || !config.floatingButton.enabled) return;

    // Make visible and enable pointer events
    floatingButton.style.opacity = '1';
    floatingButton.style.pointerEvents = 'auto';

    // Add active effect for touch
    floatingButton.addEventListener('touchstart', () => {
      floatingButton.style.transform = 'scale(0.95)';
    });

    floatingButton.addEventListener('touchend', () => {
      floatingButton.style.transform = 'scale(1)';
    });
  }

  function hideFloatingButton() {
    if (!floatingButton) return;
    floatingButton.style.opacity = '0';
    floatingButton.style.pointerEvents = 'none';
  }

  // Add minimal styles for UI components
  GM_addStyle(`
    #explainer-popup {
        position: fixed;
        width: 450px;
        max-width: 90vw;
        max-height: 80vh;
        background: rgba(255, 255, 255, 0.3);
        backdrop-filter: blur(15px);
        -webkit-backdrop-filter: blur(15px);
        border-radius: 8px;
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        z-index: 10000;
        overflow: auto;
        padding: 16px;
        transition: top 0.3s, left 0.3s, bottom 0.3s, right 0.3s;
    }
    @keyframes slideInFromTop {
        from { transform: translateY(-20px); opacity: 0; }
        to { transform: translateY(0); opacity: 1; }
    }
    @keyframes slideInFromBottom {
        from { transform: translateY(20px); opacity: 0; }
        to { transform: translateY(0); opacity: 1; }
    }
    @keyframes slideInFromLeft {
        from { transform: translateX(-20px); opacity: 0; }
        to { transform: translateX(0); opacity: 1; }
    }
    @keyframes slideInFromRight {
        from { transform: translateX(20px); opacity: 0; }
        to { transform: translateX(0); opacity: 1; }
    }
    @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
    }
    #explainer-loading {
        text-align: center;
        padding: 20px 0;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    #explainer-loading:after {
        content: "";
        width: 24px;
        height: 24px;
        border: 3px solid #ddd;
        border-top: 3px solid #2196F3;
        border-radius: 50%;
        animation: spin 1s linear infinite;
        display: inline-block;
    }
    @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
    }
    #explainer-error {
        color: #d32f2f;
        padding: 8px;
        border-radius: 4px;
        margin-bottom: 10px;
        font-size: 14px;
        display: none;
    }
    /* Dark mode support - minimal */
    @media (prefers-color-scheme: dark) {
        #explainer-popup {
            background: rgba(35, 35, 40, 0.7);
            color: #e0e0e0;
        }
        #explainer-error {
            background-color: rgba(100, 25, 25, 0.4);
            color: #ff8a8a;
        }
        #explainer-floating-button {
            background-color: rgba(33, 150, 243, 0.9);
        }
    }
  `);

  // Function to close the popup
  function closePopup() {
    const popup = document.getElementById('explainer-popup');
    if (popup) {
      popup.remove();
    }
  }

  // Calculate optimal popup position based on selection
  function calculatePopupPosition() {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return null;

    // Get selection position
    const range = selection.getRangeAt(0);
    const selectionRect = range.getBoundingClientRect();

    // Get document dimensions
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;

    // Estimate popup dimensions (will be adjusted once created)
    const popupWidth = 450;
    const popupHeight = Math.min(500, viewportHeight * 0.8);

    // Calculate optimal position
    let position = {};

    // Default margin from selection
    const margin = 20;

    // Try to position below the selection
    if (selectionRect.bottom + margin + popupHeight <= viewportHeight) {
      position.top = selectionRect.bottom + margin;
      position.left = Math.min(
        Math.max(10, selectionRect.left + (selectionRect.width / 2) - (popupWidth / 2)),
        viewportWidth - popupWidth - 10
      );
      position.placement = 'below';
    }
    // Try to position above the selection
    else if (selectionRect.top - margin - popupHeight >= 0) {
      position.bottom = viewportHeight - selectionRect.top + margin;
      position.left = Math.min(
        Math.max(10, selectionRect.left + (selectionRect.width / 2) - (popupWidth / 2)),
        viewportWidth - popupWidth - 10
      );
      position.placement = 'above';
    }
    // Try to position to the right
    else if (selectionRect.right + margin + popupWidth <= viewportWidth) {
      position.top = Math.max(10, Math.min(
        selectionRect.top,
        viewportHeight - popupHeight - 10
      ));
      position.left = selectionRect.right + margin;
      position.placement = 'right';
    }
    // Try to position to the left
    else if (selectionRect.left - margin - popupWidth >= 0) {
      position.top = Math.max(10, Math.min(
        selectionRect.top,
        viewportHeight - popupHeight - 10
      ));
      position.right = viewportWidth - selectionRect.left + margin;
      position.placement = 'left';
    }
    // Fallback to centered position if no good placement found
    else {
      position.top = Math.max(10, Math.min(
        selectionRect.top + selectionRect.height + margin,
        viewportHeight / 2 - popupHeight / 2
      ));
      position.left = Math.max(10, Math.min(
        selectionRect.left + selectionRect.width / 2 - popupWidth / 2,
        viewportWidth - popupWidth - 10
      ));
      position.placement = 'center';
    }

    return position;
  }

  // Create popup
  function createPopup() {
    // Remove existing popup if any
    closePopup();

    const popup = document.createElement('div');
    popup.id = 'explainer-popup';

    popup.innerHTML = `
      <div id="explainer-error"></div>
      <div id="explainer-loading"></div>
      <div id="explainer-content"></div>
    `;

    document.body.appendChild(popup);

    // Get optimal position for the popup
    const position = calculatePopupPosition();

    // Apply dynamic positioning
    if (position) {
      popup.style.transform = 'none'; // Remove the default transform

      // Apply positions based on the calculated values
      if (position.top !== undefined) popup.style.top = `${position.top}px`;
      if (position.bottom !== undefined) popup.style.bottom = `${position.bottom}px`;
      if (position.left !== undefined) popup.style.left = `${position.left}px`;
      if (position.right !== undefined) popup.style.right = `${position.right}px`;

      // Add animation based on placement
      switch (position.placement) {
        case 'above':
          popup.style.animation = 'slideInFromTop 0.3s ease';
          break;
        case 'below':
          popup.style.animation = 'slideInFromBottom 0.3s ease';
          break;
        case 'left':
          popup.style.animation = 'slideInFromLeft 0.3s ease';
          break;
        case 'right':
          popup.style.animation = 'slideInFromRight 0.3s ease';
          break;
        default:
          popup.style.animation = 'fadeIn 0.3s ease';
      }
    } else {
      // Fallback to center positioning if we couldn't determine a good position
      popup.style.top = '50%';
      popup.style.left = '50%';
      popup.style.transform = 'translate(-50%, -50%)';
    }

    // Add event listener for Escape key
    document.addEventListener('keydown', handleEscKey);

    // Add event listener for clicking outside popup
    document.addEventListener('click', handleOutsideClick);

    // Update popup position on resize or scroll
    window.addEventListener('resize', updatePopupPosition);
    window.addEventListener('scroll', updatePopupPosition);

    return popup;
  }

  // Update popup position on resize or scroll
  function updatePopupPosition() {
    const popup = document.getElementById('explainer-popup');
    if (!popup) return;

    const position = calculatePopupPosition();
    if (position) {
      // Remove old positioning
      popup.style.transform = 'none';
      popup.style.top = '';
      popup.style.bottom = '';
      popup.style.left = '';
      popup.style.right = '';

      // Apply new positions
      if (position.top !== undefined) popup.style.top = `${position.top}px`;
      if (position.bottom !== undefined) popup.style.bottom = `${position.bottom}px`;
      if (position.left !== undefined) popup.style.left = `${position.left}px`;
      if (position.right !== undefined) popup.style.right = `${position.right}px`;
    }
  }

  // Handle Escape key to close popup
  function handleEscKey(e) {
    if (e.key === 'Escape') {
      closePopup();
      document.removeEventListener('keydown', handleEscKey);
      document.removeEventListener('click', handleOutsideClick);
      window.removeEventListener('resize', updatePopupPosition);
      window.removeEventListener('scroll', updatePopupPosition);
    }
  }

  // Handle clicks outside popup to close it
  function handleOutsideClick(e) {
    const popup = document.getElementById('explainer-popup');
    if (popup && !popup.contains(e.target)) {
      closePopup();
      document.removeEventListener('keydown', handleEscKey);
      document.removeEventListener('click', handleOutsideClick);
      window.removeEventListener('resize', updatePopupPosition);
      window.removeEventListener('scroll', updatePopupPosition);
    }
  }

  // Function to show an error in the popup
  function showError(message) {
    const errorDiv = document.getElementById('explainer-error');
    if (errorDiv) {
      errorDiv.textContent = message;
      errorDiv.style.display = 'block';
      document.getElementById('explainer-loading').style.display = 'none';
    }
  }

  // Function to call the LLM using SmolLLM
  async function callLLM(prompt, systemPrompt, progressCallback) {
    if (!config.apiKey) {
      throw new Error("Please set up your API key in the settings.");
    }

    if (!llm) {
      throw new Error("SmolLLM library not initialized. Please check console for errors.");
    }

    console.log(`prompt: ${prompt}`);
    console.log(`systemPrompt: ${systemPrompt}`);
    try {
      return await llm.askLLM({
        prompt: prompt,
        systemPrompt: systemPrompt,
        model: config.model,
        apiKey: config.apiKey,
        baseUrl: config.baseUrl,
        providerName: config.provider,
        handler: progressCallback,
        timeout: 60000
      });
    } catch (error) {
      console.error('LLM API error:', error);
      throw error;
    }
  }

  function getPrompt(selectedText, paragraphText, textBefore, textAfter) {
    const wordsCount = selectedText.split(' ').length;
    const systemPrompt = `Respond in ${config.language} using ONLY these HTML tags: <p>, <b>, <i>, <ul>, <ol>, <li>, <a>.
  - Maintain clean formatting without code blocks
  - Prioritize clarity and conciseness
  - Use bullet points when appropriate`;

    // If we have context before/after, include it in the prompt
    const contextPrefix = textBefore || textAfter ?
      `Context before: "${textBefore || 'None'}"
       
       Selected text: "${selectedText}"
       
       Context after: "${textAfter || 'None'}"
       
       Based on the selected text and its surrounding context, ` : '';

    if (selectedText === paragraphText || wordsCount >= 500) {
      return {
        prompt: `${contextPrefix}Create a structured summary in ${config.language}:\n\n"${selectedText}"\n\n
        - Identify key themes and concepts
        - Extract 3-5 main points
        - Use nested <ul> lists for hierarchy
        - Keep bullets concise (under 15 words)`,
        systemPrompt
      };
    }

    // For short text that looks like a sentence, offer translation
    if (wordsCount > 3 && wordsCount < 100) {
      return {
        prompt: `Translate exactly to ${config.language} without commentary:\n\n"${selectedText}"\n\n
        - Preserve technical terms and names
        - Maintain original punctuation
        - Match formal/informal tone of source`,
        systemPrompt
      };
    }

    const pinYinExtraPrompt = config.language === "Chinese" ? ' DO NOT add Pinyin for it.' : '';
    const ipaExtraPrompt = config.language === "Chinese" ? '(with IPA if necessary)' : '';
    const asciiChars = selectedText.replace(/[\s\.,\-_'"!?()]/g, '')
      .split('')
      .filter(char => char.charCodeAt(0) <= 127).length;
    const sampleSentenceLanguage = selectedText.length === asciiChars ? "English" : config.language;
    // Explain words prompt
    return {
      prompt: `Provide an explanation for the word: "${selectedText}${ipaExtraPrompt}" in ${config.language} without commentary.${pinYinExtraPrompt}

Use the context from the surrounding paragraph to inform your explanation when relevant:

"${paragraphText}"

# Consider these scenarios:

## Names
If "${selectedText}" is a person's name, company name, or organization name, provide a brief description (e.g., who they are or what they do).
e.g.
Alan Turing was a British mathematician and computer scientist. He is widely considered to be the father of theoretical computer science and artificial intelligence.
His work was crucial to:
	•	Formalizing the concepts of algorithm and computation with the Turing machine.
	•	Breaking the German Enigma code during World War II, significantly contributing to the Allied victory.
	•	Developing the Turing test, a benchmark for artificial intelligence.


## Technical Terms
If "${selectedText}" is a technical term or jargon, give a concise definition and explain the use case or context where it is commonly used. No need example sentences.
e.g. GAN → 生成对抗网络
生成对抗网络(Generative Adversarial Network),是一种深度学习框架,由Ian Goodfellow在2014年提出。GAN包含两个神经网络:生成器(Generator)和判别器(Discriminator),它们相互对抗训练。生成器尝试创建看起来真实的数据,而判别器则尝试区分真实数据和生成的假数据。通过这种"博弈"过程,生成器逐渐学会创建越来越逼真的数据。

## Normal Words
- For any other word, explain its meaning and provide 1-2 example sentences with the word in ${sampleSentenceLanguage}.
e.g. jargon \\ˈdʒɑrɡən\\ → 行话,专业术语,特定领域内使用的专业词汇。在计算机科学和编程领域,指那些对外行人难以理解的专业术语和缩写。
例句: "When explaining code to beginners, try to avoid using too much technical jargon that might confuse them."(向初学者解释代码时,尽量避免使用太多可能让他们困惑的技术行话。)

# Format

- Output the words first, then the explanation, and then the example sentences in ${sampleSentenceLanguage} if necessary.
- No extra explanation
- Remember to using proper html format like <p> <b> <i> <a> <li> <ol> <ul> to improve readability.
`,
      systemPrompt
    };
  }

  // Main function to process selected text
  async function processSelectedText() {
    // Use the utility function instead of the local getSelectedText
    const { selectedText, textBefore, textAfter, paragraphText } = GetSelectionContext();

    if (!selectedText) {
      showError('No text selected');
      return;
    }

    console.log(`Selected text: '${selectedText}', Paragraph text:\n${paragraphText}`);
    // Create popup
    createPopup();
    const contentDiv = document.getElementById('explainer-content');
    const loadingDiv = document.getElementById('explainer-loading');
    const errorDiv = document.getElementById('explainer-error');

    // Reset display
    errorDiv.style.display = 'none';
    loadingDiv.style.display = 'block';

    // Assemble prompt with language preference
    const { prompt, systemPrompt } = getPrompt(selectedText, paragraphText, textBefore, textAfter);

    // Variable to store ongoing response text
    let responseText = '';
    let responseStartTime = Date.now();

    try {
      // Call LLM with progress callback and await the full response
      const fullResponse = await callLLM(prompt, systemPrompt, (textChunk, currentFullText) => {
        // Update response text with new chunk
        responseText = currentFullText || (responseText + textChunk);

        // Hide loading message if this is the first chunk
        if (loadingDiv.style.display !== 'none') {
          loadingDiv.style.display = 'none';
        }

        // Update content with either HTML or markdown
        updateContentDisplay(contentDiv, responseText);
      });

      // If we got a response
      if (fullResponse && fullResponse.length > 0) {
        responseText = fullResponse;
        loadingDiv.style.display = 'none';
        updateContentDisplay(contentDiv, fullResponse);
      }
      // If no response was received at all
      else if (!fullResponse || fullResponse.length === 0) {
        // If we've received chunks but the final response is empty, use the accumulated text
        if (responseText && responseText.length > 0) {
          updateContentDisplay(contentDiv, responseText);
        } else {
          showError("No response received from the model. Please try again.");
        }
      }

      // Hide loading indicator if it's still visible
      if (loadingDiv.style.display !== 'none') {
        loadingDiv.style.display = 'none';
      }
    } catch (error) {
      console.error('Error:', error);
      // Display error in popup
      showError(`Error: ${error.message}`);
    }
  }

  // Main function to handle keyboard shortcuts
  function handleKeyPress(e) {
    // Get shortcut configuration from settings
    const shortcut = config.shortcut || { key: 'd', ctrlKey: false, altKey: true, shiftKey: false, metaKey: false };

    // More robust shortcut detection using both key and code properties
    if (isShortcutMatch(e, shortcut)) {
      e.preventDefault();
      processSelectedText();
    }
  }

  // Helper function for more robust shortcut detection
  function isShortcutMatch(event, shortcutConfig) {
    // Check all modifier keys first
    if (event.ctrlKey !== !!shortcutConfig.ctrlKey ||
      event.altKey !== !!shortcutConfig.altKey ||
      event.shiftKey !== !!shortcutConfig.shiftKey ||
      event.metaKey !== !!shortcutConfig.metaKey) {
      return false;
    }

    const key = shortcutConfig.key.toLowerCase();

    // Method 1: Direct key match (works for most standard keys)
    if (event.key.toLowerCase() === key) {
      return true;
    }

    // Method 2: Key code match (more reliable for letter keys)
    // This handles the physical key position regardless of keyboard layout
    if (key.length === 1 && /^[a-z]$/.test(key) &&
      event.code === `Key${key.toUpperCase()}`) {
      return true;
    }

    // Method 3: Handle known special characters from Option/Alt key combinations
    // These are the most common mappings on macOS when using Option+key
    const macOptionKeyMap = {
      'a': 'å', 'b': '∫', 'c': 'ç', 'd': '∂', 'e': '´', 'f': 'ƒ',
      'g': '©', 'h': '˙', 'i': 'ˆ', 'j': '∆', 'k': '˚', 'l': '¬',
      'm': 'µ', 'n': '˜', 'o': 'ø', 'p': 'π', 'q': 'œ', 'r': '®',
      's': 'ß', 't': '†', 'u': '¨', 'v': '√', 'w': '∑', 'x': '≈',
      'y': '¥', 'z': 'Ω'
    };

    if (shortcutConfig.altKey && macOptionKeyMap[key] === event.key) {
      return true;
    }

    return false;
  }

  // Helper function to update content display
  function updateContentDisplay(contentDiv, text) {
    if (!text) return;

    try {
      if (!text.trim().startsWith('<')) {
        // fallback
        console.log(`Seems like the response is not HTML: ${text}`);
        text = `<p>${text.replace(/\n/g, '<br>')}</p>`;
      }
      contentDiv.innerHTML = text;
    } catch (e) {
      // Fallback if parsing fails
      console.error(`Error parsing content: ${e.message}`);
      contentDiv.innerHTML = `<p>${text.replace(/\n/g, '<br>')}</p>`;
    }
  }

  // Monitor selection changes for floating button
  function handleSelectionChange() {
    const selection = window.getSelection();
    const hasSelection = selection && selection.toString().trim() !== '';

    if (hasSelection && isTouchDevice() && config.floatingButton.enabled) {
      showFloatingButton();
    } else {
      hideFloatingButton();
    }
  }

  // Settings update callback
  function onSettingsChanged(updatedConfig) {
    config = updatedConfig;
    console.log('Settings updated:', config);

    // Recreate floating button if settings changed
    if (floatingButton) {
      floatingButton.remove();
      floatingButton = null;

      if (isTouchDevice() && config.floatingButton.enabled) {
        createFloatingButton();
        handleSelectionChange(); // Check if there's already a selection
      }
    }
  }

  // Initialize the script
  function init() {
    // Register settings menu in Tampermonkey
    GM_registerMenuCommand("Text Explainer Settings", () => {
      settingsManager.openDialog(onSettingsChanged);
    });

    // Add keyboard shortcut listener
    document.addEventListener('keydown', handleKeyPress);

    // For touch devices, create floating button
    if (isTouchDevice() && config.floatingButton.enabled) {
      createFloatingButton();

      // Monitor text selection
      document.addEventListener('selectionchange', handleSelectionChange);

      // Add touchend handler to show button after selection
      document.addEventListener('touchend', () => {
        // Small delay to ensure selection is updated
        setTimeout(handleSelectionChange, 100);
      });
    }

    console.log('Text Explainer script initialized with language: ' + config.language);
    console.log('Touch device detected: ' + isTouchDevice());
  }

  // Run initialization
  init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址