Image Capture

Capture images loaded to a queue for manual download management

2025-11-05 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Image Capture
// @namespace    http://tampermonkey.net/
// @version      3.0
// @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';

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

  if (excludedUrls.includes(currentUrl)) {
    console.log('Image Capture: 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 Queue <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)';
    console.log('Capture:', !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(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);

  console.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) {
    console.log('[addToQueue] Called, active:', toggleBtn.dataset.active, 'URL:', dataUrl.substring(0, 80));

    if (toggleBtn.dataset.active !== 'true') {
      console.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)) {
      console.log('[addToQueue] Skipped - already in queue');
      return;
    }

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

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

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

      // Double-check before adding (in case of race condition)
      if (imageQueue.some(item => item.dataUrl === dataUrl)) {
        console.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 });
      console.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 () {
        console.log(`✓ Downloaded: ${filename}`);
      },
      onerror: function (error) {
        console.error(`✗ 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.)
          if (value.startsWith('data:image/') ||
            (value.startsWith('http') && (/\.(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) {
      console.log('[HTMLImageElement.prototype.src] Set called:', value ? value.substring(0, 80) : 'null');
      console.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.)
        if (value.startsWith('data:image/') ||
          (value.startsWith('http') && (/\.(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)))) {
          console.log('[HTMLImageElement.prototype.src] ✓ Match found, adding to queue');
          processedImages.add(this);
          addToQueue(value);
        } else {
          console.log('[HTMLImageElement.prototype.src] ✗ No match - starts with:', value.substring(0, 20));
        }
      } else {
        console.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) {
      // Capture both data:image and regular image URLs
      // Match explicit extensions OR URLs with image indicators (f=JPEG, fmt=auto, etc.)
      if (value.startsWith('data:image/') ||
        (value.startsWith('http') && (/\.(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
          if (/\.(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
            if (url.startsWith('http')) {
              addToQueue(url);
            }
          }
        }

        // Also check response text for data:image
        const responseText = this.responseText;
        if (responseText && responseText.includes('data:image/')) {
          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') {
        if (/\.(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/'))) {
          // 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/')) {
          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/')) {
      addToQueue(result);
    }
    return result;
  };

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