Image Capture

Capture images loaded to a queue for manual download management

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Image Capture
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Capture images loaded to a queue for manual download management
// @author       Van
// @match        *://*/*
// @grant        GM_download
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // Global debug flag - set to true to enable logging
  const DEBUG_ENABLED = false;

  // Unified logging function
  function log(...args) {
    if (DEBUG_ENABLED) {
      console.log('[ImageCapture]', ...args);
    }
  }

  function logError(...args) {
    if (DEBUG_ENABLED) {
      console.error('[ImageCapture]', ...args);
    }
  }

  // Check if current page is in exclude list
  const excludedUrls = GM_getValue('excludedUrls', []);
  const currentUrl = window.location.hostname;

  if (excludedUrls.includes(currentUrl)) {
    log('This page is excluded');
    return;
  }

  // Image queue
  let imageQueue = [];
  let downloadCounter = 0;
  // Track images being checked to prevent recursion (per-image tracking)
  const imagesBeingChecked = new WeakSet();

  // Calculate iframe depth level
  let iframeLevel = 0;
  let currentWindow = window;
  try {
    while (currentWindow !== currentWindow.parent) {
      iframeLevel++;
      currentWindow = currentWindow.parent;
      if (iframeLevel > 10) break; // Safety limit
    }
  } catch (e) {
    // Cross-origin, can't determine exact level
    iframeLevel = window.self !== window.top ? 1 : 0;
  }

  // Create main container
  const container = document.createElement('div');
  container.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        z-index: 999999;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        padding: 15px;
        border-radius: 16px;
        box-shadow: 0 8px 32px rgba(0,0,0,0.3);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        font-size: 14px;
        backdrop-filter: blur(10px);
        transition: all 0.3s ease;
        cursor: pointer;
    `;

  // Collapsed state
  let isExpanded = false;

  // Collapsed view
  const collapsedView = document.createElement('div');
  collapsedView.style.cssText = `
        display: flex;
        align-items: center;
        gap: 8px;
        color: white;
        font-weight: 600;
    `;
  collapsedView.innerHTML = `🖼️ Image Capture <span style="
    background: rgba(255,255,255,0.2);
    padding: 2px 8px;
    border-radius: 12px;
    font-size: 11px;
    margin-left: 4px;
  ">L${iframeLevel}</span>`;

  // Expanded content wrapper
  const expandedContent = document.createElement('div');
  expandedContent.style.cssText = `
        display: none;
        width: 400px;
        max-height: calc(100vh - 40px);
        flex-direction: column;
        overflow: hidden;
    `;

  // Header
  const header = document.createElement('div');
  header.style.cssText = `
        display: flex;
        align-items: center;
        gap: 12px;
        margin-bottom: 15px;
        padding-bottom: 15px;
        border-bottom: 1px solid rgba(255,255,255,0.2);
    `;

  const title = document.createElement('div');
  title.innerHTML = `🖼️ Image Queue <span style="
    background: rgba(255,255,255,0.2);
    padding: 3px 10px;
    border-radius: 12px;
    font-size: 12px;
    margin-left: 8px;
    font-weight: 500;
  ">Level ${iframeLevel}</span>`;
  title.style.cssText = `
        flex: 1;
        font-weight: 600;
        color: white;
        font-size: 16px;
        letter-spacing: 0.3px;
    `;

  // Toggle capture button
  const toggleBtn = document.createElement('button');
  toggleBtn.innerHTML = '▶️';
  toggleBtn.dataset.active = 'false';
  toggleBtn.title = '开始捕获';
  toggleBtn.style.cssText = `
        background: rgba(76, 175, 80, 0.8);
        border: 1px solid rgba(255,255,255,0.3);
        border-radius: 8px;
        cursor: pointer;
        padding: 6px 12px;
        font-size: 18px;
        color: white;
        transition: all 0.3s ease;
    `;

  toggleBtn.addEventListener('click', function () {
    const isActive = this.dataset.active === 'true';
    this.dataset.active = !isActive ? 'true' : 'false';
    this.innerHTML = !isActive ? '⏸️' : '▶️';
    this.title = !isActive ? '暂停捕获' : '开始捕获';
    this.style.background = !isActive ? 'rgba(255, 152, 0, 0.8)' : 'rgba(76, 175, 80, 0.8)';
    log('Capture:', !isActive ? 'enabled' : 'disabled');
  });

  // Enhancement button
  const enhanceBtn = document.createElement('button');
  enhanceBtn.innerHTML = '⚡';
  enhanceBtn.dataset.active = 'false';
  enhanceBtn.title = '开启增强模式';
  enhanceBtn.style.cssText = `
        background: rgba(156, 39, 176, 0.8);
        border: 1px solid rgba(255,255,255,0.3);
        border-radius: 8px;
        cursor: pointer;
        padding: 6px 12px;
        font-size: 18px;
        color: white;
        transition: all 0.3s ease;
        margin-left: 5px;
    `;

  enhanceBtn.addEventListener('click', function () {
    const isActive = this.dataset.active === 'true';
    this.dataset.active = !isActive ? 'true' : 'false';
    this.innerHTML = !isActive ? '✨' : '⚡';
    this.title = !isActive ? '关闭增强模式' : '开启增强模式';
    this.style.background = !isActive ? 'rgba(255, 193, 7, 0.8)' : 'rgba(156, 39, 176, 0.8)';
    log('Enhance mode:', !isActive ? 'enabled' : 'disabled');
  });

  // Settings button
  const settingsBtn = document.createElement('button');
  settingsBtn.innerHTML = '⚙️';
  settingsBtn.style.cssText = `
        background: rgba(255,255,255,0.2);
        border: 1px solid rgba(255,255,255,0.3);
        border-radius: 8px;
        cursor: pointer;
        padding: 6px 12px;
        font-size: 18px;
        color: white;
        transition: all 0.3s ease;
    `;

  // Hide button
  const hideBtn = document.createElement('button');
  hideBtn.innerHTML = '🚫';
  hideBtn.title = '在此网站隐藏';
  hideBtn.style.cssText = `
        background: rgba(244, 67, 54, 0.8);
        border: 1px solid rgba(255,255,255,0.3);
        border-radius: 8px;
        cursor: pointer;
        padding: 6px 12px;
        font-size: 18px;
        color: white;
        transition: all 0.3s ease;
    `;

  hideBtn.addEventListener('click', function () {
    if (confirm(`确定要在 ${currentUrl} 上隐藏此脚本吗?`)) {
      const excludedUrls = GM_getValue('excludedUrls', []);
      if (!excludedUrls.includes(currentUrl)) {
        excludedUrls.push(currentUrl);
        GM_setValue('excludedUrls', excludedUrls);
        container.remove();
      }
    }
  });

  header.appendChild(title);
  header.appendChild(toggleBtn);
  header.appendChild(enhanceBtn);
  header.appendChild(settingsBtn);
  header.appendChild(hideBtn);

  // Settings panel
  const settingsPanel = document.createElement('div');
  settingsPanel.style.cssText = `
        display: none;
        margin-bottom: 15px;
        padding: 15px;
        background: rgba(0,0,0,0.2);
        border-radius: 12px;
        max-height: 400px;
        overflow-y: auto;
    `;

  // File prefix setting
  const prefixLabel = document.createElement('label');
  prefixLabel.textContent = '📁 文件前缀';
  prefixLabel.style.cssText = `
        display: block;
        margin-bottom: 8px;
        font-size: 13px;
        color: white;
        font-weight: 500;
    `;

  const prefixInput = document.createElement('input');
  prefixInput.type = 'text';
  prefixInput.placeholder = '例如: myimage_ 或 downloads/images/';
  prefixInput.value = GM_getValue('savePath', '');
  prefixInput.style.cssText = `
        width: 100%;
        padding: 10px 12px;
        border: 1px solid rgba(255,255,255,0.3);
        border-radius: 8px;
        font-size: 13px;
        box-sizing: border-box;
        background: rgba(255,255,255,0.15);
        color: white;
        margin-bottom: 10px;
    `;

  // Min width
  const minWidthLabel = document.createElement('label');
  minWidthLabel.textContent = '↔️ 最小宽度 (px)';
  minWidthLabel.style.cssText = prefixLabel.style.cssText;

  const minWidthInput = document.createElement('input');
  minWidthInput.type = 'number';
  minWidthInput.placeholder = '留空表示不限制';
  minWidthInput.value = GM_getValue('minWidth', '');
  minWidthInput.style.cssText = prefixInput.style.cssText;

  // Min height
  const minHeightLabel = document.createElement('label');
  minHeightLabel.textContent = '↕️ 最小高度 (px)';
  minHeightLabel.style.cssText = prefixLabel.style.cssText;

  const minHeightInput = document.createElement('input');
  minHeightInput.type = 'number';
  minHeightInput.placeholder = '留空表示不限制';
  minHeightInput.value = GM_getValue('minHeight', '');
  minHeightInput.style.cssText = prefixInput.style.cssText;

  // Excluded sites section
  const excludedLabel = document.createElement('label');
  excludedLabel.textContent = '🚫 已隐藏的网站';
  excludedLabel.style.cssText = prefixLabel.style.cssText;

  const excludedListContainer = document.createElement('div');
  excludedListContainer.style.cssText = `
        background: rgba(0,0,0,0.3);
        border-radius: 8px;
        padding: 10px;
        margin-bottom: 10px;
        max-height: 150px;
        overflow-y: auto;
    `;

  function updateExcludedList() {
    const excludedUrls = GM_getValue('excludedUrls', []);
    if (excludedUrls.length === 0) {
      excludedListContainer.innerHTML = '<div style="color: rgba(255,255,255,0.5); text-align: center; padding: 10px; font-size: 12px;">暂无隐藏的网站</div>';
    } else {
      excludedListContainer.innerHTML = excludedUrls.map(url => `
        <div style="
          display: flex;
          align-items: center;
          justify-content: space-between;
          padding: 6px 8px;
          margin-bottom: 6px;
          background: rgba(255,255,255,0.1);
          border-radius: 6px;
        ">
          <span style="color: white; font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${url}</span>
          <button class="remove-excluded-btn" data-url="${url}" style="
            padding: 4px 8px;
            background: rgba(244, 67, 54, 0.8);
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            margin-left: 8px;
          ">移除</button>
        </div>
      `).join('');

      // Add event listeners to remove buttons
      excludedListContainer.querySelectorAll('.remove-excluded-btn').forEach(btn => {
        btn.addEventListener('click', function (e) {
          e.stopPropagation();
          const urlToRemove = this.dataset.url;
          if (confirm(`确定要移除 ${urlToRemove} 吗?`)) {
            const excludedUrls = GM_getValue('excludedUrls', []);
            const newExcludedUrls = excludedUrls.filter(url => url !== urlToRemove);
            GM_setValue('excludedUrls', newExcludedUrls);
            updateExcludedList();
            alert('已移除!刷新页面后生效。');
          }
        });
      });
    }
  }

  const saveSettingsBtn = document.createElement('button');
  saveSettingsBtn.innerHTML = '💾 保存设置';
  saveSettingsBtn.style.cssText = `
        width: 100%;
        padding: 10px;
        background: rgba(76, 175, 80, 0.9);
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 13px;
        font-weight: 600;
    `;

  saveSettingsBtn.addEventListener('click', function () {
    GM_setValue('savePath', prefixInput.value.trim());
    GM_setValue('minWidth', minWidthInput.value.trim());
    GM_setValue('minHeight', minHeightInput.value.trim());
    alert('设置已保存!');
    settingsPanel.style.display = 'none';
  });

  settingsPanel.appendChild(prefixLabel);
  settingsPanel.appendChild(prefixInput);
  settingsPanel.appendChild(minWidthLabel);
  settingsPanel.appendChild(minWidthInput);
  settingsPanel.appendChild(minHeightLabel);
  settingsPanel.appendChild(minHeightInput);
  settingsPanel.appendChild(excludedLabel);
  settingsPanel.appendChild(excludedListContainer);
  settingsPanel.appendChild(saveSettingsBtn);

  // Update excluded list when settings panel is opened
  settingsBtn.addEventListener('click', function () {
    const willShow = settingsPanel.style.display === 'none';
    if (willShow) {
      updateExcludedList();
    }
  });

  settingsBtn.addEventListener('click', function () {
    settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
  });

  // Queue controls
  const queueControls = document.createElement('div');
  queueControls.style.cssText = `
        display: flex;
        gap: 8px;
        margin-bottom: 12px;
    `;

  const queueCount = document.createElement('div');
  queueCount.textContent = '队列: 0';
  queueCount.style.cssText = `
        flex: 1;
        color: white;
        font-weight: 500;
        display: flex;
        align-items: center;
        font-size: 13px;
    `;

  const downloadAllBtn = document.createElement('button');
  downloadAllBtn.innerHTML = '⬇️ 全部下载';
  downloadAllBtn.style.cssText = `
        padding: 8px 12px;
        background: rgba(33, 150, 243, 0.9);
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 12px;
        font-weight: 600;
        transition: all 0.3s ease;
    `;

  downloadAllBtn.addEventListener('click', function () {
    if (imageQueue.length === 0) {
      alert('队列为空!');
      return;
    }
    if (confirm(`确定要下载全部 ${imageQueue.length} 张图片吗?`)) {
      imageQueue.forEach(item => downloadImage(item));
      imageQueue = [];
      updateQueueDisplay();
    }
  });

  const clearAllBtn = document.createElement('button');
  clearAllBtn.innerHTML = '🗑️ 清空';
  clearAllBtn.style.cssText = `
        padding: 8px 12px;
        background: rgba(244, 67, 54, 0.9);
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 12px;
        font-weight: 600;
        transition: all 0.3s ease;
    `;

  clearAllBtn.addEventListener('click', function () {
    if (imageQueue.length === 0) return;
    if (confirm(`确定要清空全部 ${imageQueue.length} 张图片吗?`)) {
      imageQueue = [];
      updateQueueDisplay();
    }
  });

  queueControls.appendChild(queueCount);
  queueControls.appendChild(downloadAllBtn);
  queueControls.appendChild(clearAllBtn);

  // Queue list container with flex
  const queueContainer = document.createElement('div');
  queueContainer.style.cssText = `
        flex: 1;
        display: flex;
        flex-direction: column;
        min-height: 0;
        overflow: hidden;
    `;

  const queueList = document.createElement('div');
  queueList.style.cssText = `
        flex: 1;
        overflow-y: auto;
        background: rgba(0,0,0,0.2);
        border-radius: 12px;
        padding: 10px;
        min-height: 150px;
    `;

  queueContainer.appendChild(queueList);

  // Assemble UI
  expandedContent.appendChild(header);
  expandedContent.appendChild(settingsPanel);
  expandedContent.appendChild(queueControls);
  expandedContent.appendChild(queueContainer);

  container.appendChild(collapsedView);
  container.appendChild(expandedContent);
  document.body.appendChild(container);

  log(`Image Queue Manager initialized at Level ${iframeLevel}`);

  // Toggle expand/collapse
  container.addEventListener('click', function (e) {
    // Don't toggle if clicking on buttons or inputs inside expanded content
    if (isExpanded && (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.closest('button') || e.target.closest('input'))) {
      return;
    }

    isExpanded = !isExpanded;

    if (isExpanded) {
      collapsedView.style.display = 'none';
      expandedContent.style.display = 'flex';
      container.style.cursor = 'default';
      container.style.padding = '20px';
    } else {
      collapsedView.style.display = 'flex';
      expandedContent.style.display = 'none';
      container.style.cursor = 'pointer';
      container.style.padding = '15px';
    }
  });

  // Functions
  function getImageDimensions(dataUrl, callback) {
    const img = new Image();
    imagesBeingChecked.add(img); // Track this specific image
    img.onload = function () {
      imagesBeingChecked.delete(img); // Remove from tracking
      callback(this.width, this.height);
    };
    img.onerror = function () {
      imagesBeingChecked.delete(img); // Remove from tracking
      callback(0, 0);
    };
    img.src = dataUrl;
  }

  function meetsMinimumSize(width, height) {
    const minWidth = parseInt(GM_getValue('minWidth', '')) || 0;
    const minHeight = parseInt(GM_getValue('minHeight', '')) || 0;
    return (minWidth === 0 || width >= minWidth) && (minHeight === 0 || height >= minHeight);
  }

  function addToQueue(dataUrl) {
    log('[addToQueue] Called, active:', toggleBtn.dataset.active, 'URL:', dataUrl.substring(0, 80));

    if (toggleBtn.dataset.active !== 'true') {
      log('[addToQueue] Skipped - capture not active');
      return;
    }

    // Check if already in queue - use full URL for exact matching
    if (imageQueue.some(item => item.dataUrl === dataUrl)) {
      log('[addToQueue] Skipped - already in queue');
      return;
    }

    log('[addToQueue] Getting dimensions...');
    getImageDimensions(dataUrl, function (width, height) {
      log('[addToQueue] Dimensions:', width, 'x', height);

      // Filter out images with 0 dimensions (failed to load or invalid)
      if (width === 0 || height === 0) {
        log('[addToQueue] Skipped - invalid dimensions');
        return;
      }

      if (!meetsMinimumSize(width, height)) {
        log('[addToQueue] Skipped - below minimum size');
        return;
      }

      // Double-check before adding (in case of race condition)
      if (imageQueue.some(item => item.dataUrl === dataUrl)) {
        log('[addToQueue] Skipped - race condition detected');
        return;
      }

      const id = Date.now() + Math.random();
      const imageType = dataUrl.startsWith('data:image/') ? 'data:image' : 'url';
      imageQueue.push({ id, dataUrl, width, height, timestamp: Date.now(), type: imageType });
      log('[addToQueue] ✓ Added to queue:', width, 'x', height, imageType);
      updateQueueDisplay();
    });
  }

  function downloadImage(item) {
    let format = 'png';

    // Determine format based on URL type
    if (item.dataUrl.startsWith('data:image/')) {
      // For data:image URLs
      const matches = item.dataUrl.match(/^data:image\/(\w+);base64,/);
      format = matches ? matches[1] : 'png';
    } else {
      // For regular URLs, extract extension from URL
      const urlMatch = item.dataUrl.match(/\.([a-z0-9]+)(\?|$)/i);
      if (urlMatch) {
        format = urlMatch[1].toLowerCase();
      } else {
        // Try to get from content-type if available
        format = 'jpg'; // Default for URL images
      }
    }

    const customPath = GM_getValue('savePath', '').trim();
    const baseFilename = `image_${item.timestamp}_${item.width}x${item.height}_${downloadCounter++}.${format}`;
    const filename = customPath ? customPath + baseFilename : baseFilename;

    GM_download({
      url: item.dataUrl,
      name: filename,
      onload: function () {
        log(`✓ Downloaded: ${filename}`);
      },
      onerror: function (error) {
        logError(`✗ Download failed:`, error);
      }
    });
  }

  // Image preview modal
  function showImagePreview(item) {
    // Create modal overlay
    const modal = document.createElement('div');
    modal.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      background: rgba(0, 0, 0, 0.95);
      z-index: 9999999;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 20px;
      box-sizing: border-box;
      animation: fadeIn 0.2s ease;
    `;

    // Add fade-in animation
    const style = document.createElement('style');
    style.textContent = `
      @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
      }
    `;
    document.head.appendChild(style);

    // Image info bar
    const infoBar = document.createElement('div');
    infoBar.style.cssText = `
      position: absolute;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(255, 255, 255, 0.1);
      backdrop-filter: blur(10px);
      padding: 12px 24px;
      border-radius: 12px;
      color: white;
      font-size: 14px;
      font-weight: 500;
      display: flex;
      gap: 20px;
      align-items: center;
    `;

    const typeColor = item.type === 'data:image' ? '#4CAF50' : '#2196F3';
    const typeLabel = item.type === 'data:image' ? 'Data' : 'URL';

    infoBar.innerHTML = `
      <span>📐 ${item.width} × ${item.height}</span>
      <span style="background: ${typeColor}; padding: 4px 10px; border-radius: 6px; font-size: 12px;">${typeLabel}</span>
      <span>🕐 ${new Date(item.timestamp).toLocaleString()}</span>
    `;

    // Image container
    const imgContainer = document.createElement('div');
    imgContainer.style.cssText = `
      max-width: 90vw;
      max-height: 80vh;
      display: flex;
      align-items: center;
      justify-content: center;
    `;

    const img = document.createElement('img');
    img.src = item.dataUrl;
    img.style.cssText = `
      max-width: 100%;
      max-height: 80vh;
      object-fit: contain;
      border-radius: 8px;
      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    `;

    imgContainer.appendChild(img);

    // Close button
    const closeBtn = document.createElement('button');
    closeBtn.innerHTML = '✕';
    closeBtn.style.cssText = `
      position: absolute;
      top: 20px;
      right: 20px;
      width: 50px;
      height: 50px;
      background: rgba(244, 67, 54, 0.9);
      color: white;
      border: none;
      border-radius: 50%;
      font-size: 24px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.3s ease;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    `;

    closeBtn.addEventListener('mouseenter', function () {
      this.style.transform = 'scale(1.1) rotate(90deg)';
      this.style.background = 'rgba(244, 67, 54, 1)';
    });

    closeBtn.addEventListener('mouseleave', function () {
      this.style.transform = 'scale(1) rotate(0deg)';
      this.style.background = 'rgba(244, 67, 54, 0.9)';
    });

    // Action buttons
    const actionBar = document.createElement('div');
    actionBar.style.cssText = `
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      gap: 12px;
    `;

    const downloadBtn = document.createElement('button');
    downloadBtn.innerHTML = '⬇️ 下载';
    downloadBtn.style.cssText = `
      padding: 12px 24px;
      background: rgba(33, 150, 243, 0.9);
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s ease;
    `;

    downloadBtn.addEventListener('mouseenter', function () {
      this.style.background = 'rgba(33, 150, 243, 1)';
      this.style.transform = 'translateY(-2px)';
    });

    downloadBtn.addEventListener('mouseleave', function () {
      this.style.background = 'rgba(33, 150, 243, 0.9)';
      this.style.transform = 'translateY(0)';
    });

    downloadBtn.addEventListener('click', function (e) {
      e.stopPropagation();
      downloadImage(item);
      modal.remove();
    });

    actionBar.appendChild(downloadBtn);

    // Close modal on click outside or ESC key
    const closeModal = () => {
      modal.style.animation = 'fadeOut 0.2s ease';
      setTimeout(() => modal.remove(), 200);
    };

    modal.addEventListener('click', function (e) {
      if (e.target === modal) {
        closeModal();
      }
    });

    closeBtn.addEventListener('click', closeModal);

    document.addEventListener('keydown', function escHandler(e) {
      if (e.key === 'Escape') {
        closeModal();
        document.removeEventListener('keydown', escHandler);
      }
    });

    // Assemble modal
    modal.appendChild(infoBar);
    modal.appendChild(imgContainer);
    modal.appendChild(closeBtn);
    modal.appendChild(actionBar);
    document.body.appendChild(modal);

    // Add fade-out animation
    style.textContent += `
      @keyframes fadeOut {
        from { opacity: 1; }
        to { opacity: 0; }
      }
    `;
  }

  function updateQueueDisplay() {
    queueCount.textContent = `队列: ${imageQueue.length}`;

    if (imageQueue.length === 0) {
      queueList.innerHTML = '<div style="color: rgba(255,255,255,0.6); text-align: center; padding: 40px 20px; font-size: 13px;">暂无图片<br>点击 ▶️ 开始捕获</div>';
      return;
    }

    queueList.innerHTML = imageQueue.map(item => {
      const typeColor = item.type === 'data:image' ? '#4CAF50' : '#2196F3';
      const typeLabel = item.type === 'data:image' ? 'Data' : 'URL';

      return `
      <div class="queue-item" data-id="${item.id}" style="
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 8px;
        margin-bottom: 8px;
        background: rgba(255,255,255,0.1);
        border-radius: 8px;
        transition: all 0.3s ease;
      ">
        <img class="preview-img" src="${item.dataUrl}" style="
          width: 50px;
          height: 50px;
          object-fit: cover;
          border-radius: 6px;
          border: 2px solid rgba(255,255,255,0.3);
          cursor: pointer;
          transition: all 0.3s ease;
        " title="点击查看大图">
        <div style="flex: 1; min-width: 0;">
          <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 2px;">
            <span style="color: white; font-size: 12px; font-weight: 500;">${item.width} × ${item.height}</span>
            <span style="background: ${typeColor}; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 600;">${typeLabel}</span>
          </div>
          <div style="color: rgba(255,255,255,0.7); font-size: 11px;">${new Date(item.timestamp).toLocaleTimeString()}</div>
        </div>
        <button class="download-btn" style="
          padding: 6px 10px;
          background: rgba(33, 150, 243, 0.9);
          color: white;
          border: none;
          border-radius: 6px;
          cursor: pointer;
          font-size: 11px;
          white-space: nowrap;
        ">⬇️ 下载</button>
        <button class="delete-btn" style="
          padding: 6px 10px;
          background: rgba(244, 67, 54, 0.9);
          color: white;
          border: none;
          border-radius: 6px;
          cursor: pointer;
          font-size: 11px;
        ">🗑️</button>
      </div>
    `;
    }).join('');

    // Add hover effect to preview images
    queueList.querySelectorAll('.preview-img').forEach(img => {
      img.addEventListener('mouseenter', function () {
        this.style.transform = 'scale(1.1)';
        this.style.borderColor = 'rgba(255,255,255,0.6)';
      });
      img.addEventListener('mouseleave', function () {
        this.style.transform = 'scale(1)';
        this.style.borderColor = 'rgba(255,255,255,0.3)';
      });
    });

    // Add event listeners for preview
    queueList.querySelectorAll('.preview-img').forEach((img, index) => {
      img.addEventListener('click', function (e) {
        e.stopPropagation();
        showImagePreview(imageQueue[index]);
      });
    });

    // Add event listeners for download
    queueList.querySelectorAll('.download-btn').forEach((btn, index) => {
      btn.addEventListener('click', function (e) {
        e.stopPropagation();
        const item = imageQueue[index];
        downloadImage(item);
        imageQueue.splice(index, 1);
        updateQueueDisplay();
      });
    });

    // Add event listeners for delete
    queueList.querySelectorAll('.delete-btn').forEach((btn, index) => {
      btn.addEventListener('click', function (e) {
        e.stopPropagation();
        imageQueue.splice(index, 1);
        updateQueueDisplay();
      });
    });
  }

  updateQueueDisplay();

  // Intercept data:image creation
  const OriginalImage = window.Image;
  const originalSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');

  window.Image = function () {
    const img = new OriginalImage();

    // Create a flag to prevent infinite recursion
    let isSettingSrc = false;

    Object.defineProperty(img, 'src', {
      get: function () {
        return originalSrcDescriptor.get.call(this);
      },
      set: function (value) {
        if (!isSettingSrc && !imagesBeingChecked.has(this) && value && typeof value === 'string') {
          // Capture both data:image and regular URLs
          // Match explicit extensions OR URLs with image indicators (f=JPEG, fmt=auto, etc.)
          log('image value', value)
          const isEnhancedMode = enhanceBtn.dataset.active === 'true';
          if (value.startsWith('data:image/') ||
            (value.startsWith('http') && (
              (isEnhancedMode ? /(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(value) : /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(value)) ||
              /[?&](f|fmt|format)=(jpe?g|png|gif|webp|svg|bmp|ico)/i.test(value)))) {
            addToQueue(value);
          }
        }
        isSettingSrc = true;
        originalSrcDescriptor.set.call(this, value);
        isSettingSrc = false;
      },
      configurable: true
    });
    return img;
  };

  // Also intercept the prototype directly for existing images
  const originalPrototypeSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');

  // Track which images we've already processed to prevent infinite loops
  const processedImages = new WeakSet();

  Object.defineProperty(HTMLImageElement.prototype, 'src', {
    get: function () {
      return originalPrototypeSrcDescriptor.get.call(this);
    },
    set: function (value) {
      log('[HTMLImageElement.prototype.src] Set called:', value ? value.substring(0, 80) : 'null');
      log('[HTMLImageElement.prototype.src] Flags - beingChecked:', imagesBeingChecked.has(this), 'processed:', processedImages.has(this));

      // Prevent infinite loops by tracking this specific element
      if (!imagesBeingChecked.has(this) && !processedImages.has(this) && value && typeof value === 'string') {
        // Match explicit extensions OR URLs with image indicators (f=JPEG, fmt=auto, etc.)
        const isEnhancedMode = enhanceBtn.dataset.active === 'true';
        if (value.startsWith('data:image/') ||
          (value.startsWith('http') && (
            (isEnhancedMode ? /(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(value) : /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(value)) ||
            /[?&](f|fmt|format)=(jpe?g|png|gif|webp|svg|bmp|ico)/i.test(value)))) {
          log('[HTMLImageElement.prototype.src] ✓ Match found, adding to queue');
          processedImages.add(this);
          addToQueue(value);
        } else {
          log('[HTMLImageElement.prototype.src] ✗ No match - starts with:', value.substring(0, 20));
        }
      } else {
        log('[HTMLImageElement.prototype.src] Skipped due to flags or already processed');
      }
      originalPrototypeSrcDescriptor.set.call(this, value);
    },
    configurable: true
  });

  const originalSetAttribute = Element.prototype.setAttribute;
  Element.prototype.setAttribute = function (name, value) {
    const result = originalSetAttribute.call(this, name, value);
    if (!imagesBeingChecked.has(this) && this.tagName === 'IMG' && name === 'src' && value) {
      log('[Element.prototype.setAttribute] called, the tagName is IMG', value);
      // Capture both data:image and regular image URLs
      // Match explicit extensions OR URLs with image indicators (f=JPEG, fmt=auto, etc.)
      const isEnhancedMode = enhanceBtn.dataset.active === 'true';
      if (value.startsWith('data:image/') ||
        (value.startsWith('http') && (
          (isEnhancedMode ? /(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(value) : /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(value)) ||
          /[?&](f|fmt|format)=(jpe?g|png|gif|webp|svg|bmp|ico)/i.test(value)))) {
        addToQueue(value);
      }
    }
    return result;
  };

  // Monitor XHR
  const originalXHROpen = XMLHttpRequest.prototype.open;
  const originalXHRSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method, url) {
    this._requestUrl = url;
    return originalXHROpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function () {
    this.addEventListener('load', function () {
      if (toggleBtn.dataset.active !== 'true') return;

      try {
        // Check if this is an image request by URL
        const url = this._requestUrl;
        if (url && typeof url === 'string') {
          // Check if URL is an image - match explicit extensions OR image indicators
          const isEnhancedMode = enhanceBtn.dataset.active === 'true';
          if ((isEnhancedMode ? /(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(url) : /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(url)) ||
            /[?&](f|fmt|format)=(jpe?g|png|gif|webp|svg|bmp|ico)/i.test(url) ||
            (this.responseType === 'blob' && this.response && this.response.type && this.response.type.startsWith('image/'))) {
            // For image URLs, add directly
            log('[XMLHttpRequest.prototype.send.load] called, the request url is image type');
            if (url.startsWith('http')) {
              addToQueue(url);
            }
          }
        }

        // Also check response text for data:image
        const responseText = this.responseText;
        if (responseText && responseText.includes('data:image/')) {
          log('[XMLHttpRequest.prototype.send.load] called, the response is image type');
          const dataImageRegex = /data:image\/[\w+]+;base64,[A-Za-z0-9+/=]+/g;
          const matches = responseText.match(dataImageRegex);
          if (matches) {
            matches.forEach(dataUrl => addToQueue(dataUrl));
          }
        }
      } catch (error) { }
    });
    return originalXHRSend.apply(this, arguments);
  };

  // Monitor fetch
  const originalFetch = window.fetch;
  window.fetch = function (resource, init) {
    const url = typeof resource === 'string' ? resource : resource.url;

    return originalFetch.apply(this, arguments).then(function (response) {
      if (toggleBtn.dataset.active !== 'true') return response;

      // Check if this is an image request by URL - match explicit extensions OR image indicators
      if (url && typeof url === 'string') {
        const isEnhancedMode = enhanceBtn.dataset.active === 'true';
        if ((isEnhancedMode ? /(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(url) : /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?|$)/i.test(url)) ||
          /[?&](f|fmt|format)=(jpe?g|png|gif|webp|svg|bmp|ico)/i.test(url) ||
          (response.headers.get('content-type') && response.headers.get('content-type').startsWith('image/'))) {
          log('[window.fetch] called, the request url is image type');
          // For image URLs, add directly
          if (url.startsWith('http')) {
            addToQueue(url);
          }
        }
      }

      // Also check response text for data:image
      const clonedResponse = response.clone();
      clonedResponse.text().then(function (text) {
        if (text && text.includes('data:image/')) {
          log('[window.fetch] called, the response is image type');
          const dataImageRegex = /data:image\/[\w+]+;base64,[A-Za-z0-9+/=]+/g;
          const matches = text.match(dataImageRegex);
          if (matches) {
            matches.forEach(dataUrl => addToQueue(dataUrl));
          }
        }
      }).catch(function () { });

      return response;
    });
  };

  // Monitor canvas
  const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
  HTMLCanvasElement.prototype.toDataURL = function () {
    const result = originalToDataURL.apply(this, arguments);
    if (result && result.startsWith('data:image/')) {
      log('[HTMLCanvasElement.prototype.toDataURL] called, get canvas picture');
      addToQueue(result);
    }
    return result;
  };

  log('Image Capture initialized');
})();