AI 网页图片上传 压缩

拦截网页图片上传,替换为压缩后的图片,体积更小、加载更快;支持拖动、双击隐藏设置按钮;支持自定义快捷键唤出按钮;隐藏状态持久化

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        AI 网页图片上传 压缩
// @namespace   https://github.com/JustDoIt166
// @version     1.2.7
// @description 拦截网页图片上传,替换为压缩后的图片,体积更小、加载更快;支持拖动、双击隐藏设置按钮;支持自定义快捷键唤出按钮;隐藏状态持久化
// @author      JustDoIt166
// @icon  https://raw.githubusercontent.com/JustDoIt166/AI-Upload-Image-Compressor/refs/heads/main/assets/icon.svg
// @match       https://chat.qwen.ai/*
// @match       https://chat.z.ai/*
// @match       https://chatgpt.com/*
// @match       https://gemini.google.com/*
// @match       https://chat.deepseek.com/*
// @match       https://yiyan.baidu.com/*
// @grant       GM_registerMenuCommand
// @license     MIT
// ==/UserScript==

(function () {
    'use strict';

    const SITE_CONFIGS = {
        'chat.qwen.ai': { fileInputSelector: 'input[type="file"]', name: 'Qwen' },
        'chat.z.ai': { fileInputSelector: 'input[type="file"]', name: 'Z.AI' },
        'gemini.google.com': { fileInputSelector: 'input[type="file"]', name: 'Gemini' },
        'chat.deepseek.com': { fileInputSelector: 'input[type="file"]', name: 'DeepSeek' }
    };

    const DEFAULT_SETTINGS = {
        mimeType: 'image/webp',
        quality: 0.85,
        maxWidth: 4096,
        maxHeight: 2160,
        autoCompress: true,
        adaptiveQuality: true,
        enableHotkey: true,
        hotkey: 'Alt+C',
        enableDblClickReveal: true  // 是否允许双击空白区域唤出按钮
    };

    const stats = {
        totalCompressed: 0,
        totalSizeSaved: 0,
        compressionHistory: []
    };

    const ImageCompressor = {
        settings: { ...DEFAULT_SETTINGS },
        isButtonHidden: false,
        worker: null,

        init() {
            this.loadSettings();
            this.loadStats();
            this.setupEventListeners();
            this.createUI();
            this.initWorker();
            this.setupHotkeyListener();
            if (this.settings.enableDblClickReveal) {
                this.setupGlobalRevealOnDblTap(); // 移动端空白双击唤出按钮
                this.setupDesktopRevealOnDblClick(); // 桌面端空白双击唤出按钮
            }
            if (typeof GM_registerMenuCommand !== 'undefined') {
                GM_registerMenuCommand('打开图片压缩设置', () => {
                    this.showSettingsButton();
                    this.toggleSettingsPanel();
                });
            }
            console.log('🛡️ 图片压缩脚本 v1.2.6 已激活');
        },

        loadSettings() {
            const saved = localStorage.getItem('imageCompressSettings');
            if (saved) {
                this.settings = { ...this.settings, ...JSON.parse(saved) };
            }
            // 加载按钮隐藏状态
            this.isButtonHidden = localStorage.getItem('compressButtonHidden') === 'true';
        },

        saveSettings() {
            localStorage.setItem('imageCompressSettings', JSON.stringify(this.settings));
        },

        loadStats() {
            const saved = localStorage.getItem('compressStats');
            if (saved) {
                const savedStats = JSON.parse(saved);
                stats.totalCompressed = savedStats.totalCompressed || 0;
                stats.totalSizeSaved = savedStats.totalSizeSaved || 0;
                stats.compressionHistory = savedStats.compressionHistory || [];
            }
        },

        updateStats(originalSize, compressedSize) {
            stats.totalCompressed++;
            stats.totalSizeSaved += originalSize - compressedSize;
            stats.compressionHistory.push({
                date: new Date(),
                originalSize,
                compressedSize,
                saved: originalSize - compressedSize
            });
            localStorage.setItem('compressStats', JSON.stringify(stats));
        },

        initWorker() {
            const workerCode = `
                self.onmessage = async function(e) {
                    const { file, mimeType, quality, maxWidth, maxHeight } = e.data;
                    try {
                        const imageBitmap = await createImageBitmap(file);
                        let { width, height } = imageBitmap;
                        const originalRatio = width / height;
                        let needsResize = false;
                        if (width > maxWidth) {
                            width = maxWidth;
                            height = width / originalRatio;
                            needsResize = true;
                        }
                        if (height > maxHeight) {
                            height = maxHeight;
                            width = height * originalRatio;
                            needsResize = true;
                        }
                        const canvas = new OffscreenCanvas(
                            needsResize ? Math.round(width) : imageBitmap.width,
                            needsResize ? Math.round(height) : imageBitmap.height
                        );
                        const ctx = canvas.getContext('2d');
                        if (mimeType === 'image/jpeg') {
                            ctx.fillStyle = '#FFFFFF';
                            ctx.fillRect(0, 0, canvas.width, canvas.height);
                        }
                        ctx.drawImage(imageBitmap, 0, 0, canvas.width, canvas.height);
                        imageBitmap.close();
                        const blob = await canvas.convertToBlob({ type: mimeType, quality });
                        self.postMessage({ compressedBlob: blob });
                    } catch (error) {
                        self.postMessage({ error: error.message });
                    }
                };
            `;
            const blob = new Blob([workerCode], { type: 'application/javascript' });
            this.worker = new Worker(URL.createObjectURL(blob));
        },

        compress(file, onProgress) {
            return new Promise((resolve, reject) => {
                if (!this.worker) {
                    reject(new Error('Worker not initialized'));
                    return;
                }
                let quality = this.settings.quality;
                if (this.settings.adaptiveQuality) {
                    quality = this.getAdaptiveQuality(file.size);
                }
                this.worker.onmessage = (e) => {
                    if (e.data.error) {
                        reject(new Error(e.data.error));
                    } else {
                        resolve(e.data.compressedBlob);
                    }
                };
                this.worker.postMessage({
                    file,
                    mimeType: this.settings.mimeType,
                    quality,
                    maxWidth: this.settings.maxWidth,
                    maxHeight: this.settings.maxHeight
                });
            });
        },

        getAdaptiveQuality(fileSize) {
            if (fileSize < 1024 * 1024) return 0.95;
            if (fileSize < 3 * 1024 * 1024) return 0.85;
            if (fileSize < 5 * 1024 * 1024) return 0.75;
            return 0.65;
        },

        async handleMultipleFiles(files) {
            const compressedFiles = [];
            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                if (!file.type.startsWith('image/')) continue;
                this.showToast(`处理图片 ${i + 1}/${files.length}: ${file.name}`, 'info');
                try {
                    const compressedBlob = await this.compress(file);
                    const compressedFile = new File([compressedBlob], file.name, {
                        type: this.settings.mimeType,
                        lastModified: Date.now()
                    });
                    compressedFiles.push(compressedFile);
                    this.updateStats(file.size, compressedFile.size);
                    const savedMB = ((file.size - compressedFile.size) / 1024 / 1024).toFixed(2);
                    this.showToast(`✅ ${file.name} 压缩完成,节省 ${savedMB} MB`, 'success');
                } catch (err) {
                    console.error(`压缩 ${file.name} 失败:`, err);
                    this.showToast(`❌ 压缩 ${file.name} 失败`, 'error');
                }
            }
            return compressedFiles;
        },

        setupEventListeners() {
            document.addEventListener('change', async (e) => {
                if (e._myScriptIsProcessing) return;
                const target = e.target;
                if (!(target?.tagName === 'INPUT' && target.type === 'file' && target.files?.length > 0)) {
                    return;
                }
                const imageFiles = Array.from(target.files).filter(file => file.type.startsWith('image/'));
                if (imageFiles.length === 0) return;
                if (!this.settings.autoCompress) return;
                e.stopImmediatePropagation();
                e.preventDefault();
                try {
                    const compressedFiles = await this.handleMultipleFiles(imageFiles);
                    const dt = new DataTransfer();
                    Array.from(target.files).forEach(file => {
                        if (!file.type.startsWith('image/')) dt.items.add(file);
                    });
                    compressedFiles.forEach(file => dt.items.add(file));
                    target.files = dt.files;
                    const newEvent = new Event('change', { bubbles: true, cancelable: true });
                    newEvent._myScriptIsProcessing = true;
                    target.dispatchEvent(newEvent);
                } catch (err) {
                    console.error('❌ 压缩替换失败:', err);
                    this.showToast('❌ 图片压缩失败,请重试', 'error');
                }
            }, true);
        },

        createUI() {
            if (document.getElementById('compress-settings-btn')) return;

            const settingsBtn = document.createElement('div');
            settingsBtn.id = 'compress-settings-btn';
            settingsBtn.innerHTML = `
                <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
                    <defs>
                        <linearGradient id="paperGradient" x1="0%" y1="0%" x2="0%" y2="100%">
                        <stop offset="0%" stop-color="#FDF8EC"/>
                        <stop offset="100%" stop-color="#EBE3D6"/>
                        </linearGradient>
                        <linearGradient id="darkPaperGradient" x1="0%" y1="0%" x2="0%" y2="100%">
                        <stop offset="0%" stop-color="#D6CFBF"/>
                        <stop offset="100%" stop-color="#C2BAAB"/>
                        </linearGradient>
                    </defs>

                    <g transform="translate(60, 20) rotate(5)">
                        <rect x="0" y="0" width="100" height="120" rx="10" fill="url(#darkPaperGradient)" stroke="#4A4A4A" stroke-width="2"/>
                        <line x1="85" y1="20" x2="85" y2="100" stroke="#4A4A4A" stroke-width="2" stroke-dasharray="2,4"/>
                    </g>

                    <rect x="25" y="40" width="120" height="140" rx="10" fill="url(#paperGradient)" stroke="#4A4A4A" stroke-width="2"/>

                    <rect x="40" y="55" width="90" height="60" rx="5" fill="#C2E6F2" stroke="#4A4A4A" stroke-width="2"/>

                    <path d="M40 115 L65 75 L85 110 L105 70 L130 115 Z" fill="#2DA592" stroke="#4A4A4A" stroke-width="2" stroke-linejoin="round"/>
                    <path d="M55 115 L75 85 L95 115 Z" fill="#2DA592" stroke="#4A4A4A" stroke-width="2" stroke-linejoin="round"/>

                    <circle cx="115" cy="70" r="8" fill="#FDD755" stroke="#4A4A4A" stroke-width="2"/>

                    <rect x="40" y="130" width="70" height="5" rx="2" fill="#4A4A4A"/>
                    <rect x="40" y="140" width="50" height="5" rx="2" fill="#4A4A4A"/>
                    <rect x="40" y="150" width="60" height="5" rx="2" fill="#4A4A4A"/>
            </svg>
            `;
            settingsBtn.title = '图片压缩设置(双击隐藏)';
            settingsBtn.style.cssText = `
                position: fixed;
                top: 50%;
                right: 20px;
                transform: translateY(-50%);
                width: 50px;
                height: 50px;
                background: #FDF8EC;
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: move;
                z-index: 99999;
                box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                transition: transform 0.2s;
                user-select: none;
                border: 1px solid #e0e0e0; /* 增加轻微边框提升辨识度 */
            `;

            // 恢复位置
            const savedPos = JSON.parse(localStorage.getItem('compressBtnPosition') || 'null');
            if (savedPos && typeof savedPos.x === 'number' && typeof savedPos.y === 'number') {
                const x = Math.max(0, Math.min(savedPos.x, window.innerWidth - 50));
                const y = Math.max(0, Math.min(savedPos.y, window.innerHeight - 50));
                settingsBtn.style.left = x + 'px';
                settingsBtn.style.top = y + 'px';
                settingsBtn.style.right = 'auto';
                settingsBtn.style.bottom = 'auto';
                settingsBtn.style.transform = 'none';
            }

            // 根据持久化状态决定是否显示
            if (this.isButtonHidden) {
                settingsBtn.style.display = 'none';
            }

            let isDragging = false;
            let offsetX, offsetY;

            const onMouseDown = (e) => {
                isDragging = true;
                const rect = settingsBtn.getBoundingClientRect();
                offsetX = e.clientX - rect.left;
                offsetY = e.clientY - rect.top;
                settingsBtn.style.cursor = 'grabbing';
                e.preventDefault();
            };

            const onMouseMove = (e) => {
                if (!isDragging) return;
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;
                const maxX = window.innerWidth - settingsBtn.offsetWidth;
                const maxY = window.innerHeight - settingsBtn.offsetHeight;
                const boundedX = Math.max(0, Math.min(x, maxX));
                const boundedY = Math.max(0, Math.min(y, maxY));
                settingsBtn.style.left = `${boundedX}px`;
                settingsBtn.style.top = `${boundedY}px`;
                settingsBtn.style.right = 'auto';
                settingsBtn.style.bottom = 'auto';
                settingsBtn.style.transform = 'none';
            };

            const onMouseUp = () => {
                isDragging = false;
                settingsBtn.style.cursor = 'move';
                const rect = settingsBtn.getBoundingClientRect();
                const x = rect.left + window.scrollX;
                const y = rect.top + window.scrollY;
                localStorage.setItem('compressBtnPosition', JSON.stringify({ x, y }));
            };

            settingsBtn.addEventListener('mousedown', onMouseDown);
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);

            // 双击隐藏(桌面)
            settingsBtn.addEventListener('dblclick', (e) => {
                e.stopPropagation();
                this.hideSettingsButton();
                if ('ontouchstart' in window) {
                    this.showToast('在空白处双击可重新显示按钮', 'info');
                }
            });

            // 移动端双击模拟
            let lastTap = 0;
            settingsBtn.addEventListener('touchstart', (e) => {
                const now = Date.now();
                if (now - lastTap < 350 && now - lastTap > 0) {
                    e.preventDefault();
                    e.stopPropagation();
                    this.hideSettingsButton();
                    if ('ontouchstart' in window) {
                        this.showToast('在空白处双击可重新显示按钮', 'info');
                    }
                    lastTap = 0;
                } else {
                    lastTap = now;
                }
            });

            settingsBtn.addEventListener('click', (e) => {
                if (isDragging) return;
                e.stopPropagation();
                this.toggleSettingsPanel();
            });

            settingsBtn.addEventListener('mouseenter', () => {
                if (!isDragging) settingsBtn.style.transform = 'scale(1.1)';
            });

            settingsBtn.addEventListener('mouseleave', () => {
                if (!isDragging) settingsBtn.style.transform = 'scale(1)';
            });

            if (document.body) {
                document.body.appendChild(settingsBtn);
            } else {
                document.addEventListener('DOMContentLoaded', () => {
                    document.body.appendChild(settingsBtn);
                });
            }

            this.createSettingsPanel();
        },

        hideSettingsButton() {
            const btn = document.getElementById('compress-settings-btn');
            if (btn) {
                btn.style.display = 'none';
                this.isButtonHidden = true;
                localStorage.setItem('compressButtonHidden', 'true');
            }
        },

        showSettingsButton() {
            const btn = document.getElementById('compress-settings-btn');
            if (btn) {
                btn.style.display = 'flex';
                this.isButtonHidden = false;
                localStorage.setItem('compressButtonHidden', 'false');
            }
        },

        createSettingsPanel() {
            if (document.getElementById('compress-settings-panel')) return;

            const panel = document.createElement('div');
            panel.id = 'compress-settings-panel';
            panel.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: white;
                border-radius: 12px;
                box-shadow: 0 8px 32px rgba(0,0,0,0.2);
                z-index: 100000;
                width: 400px;
                max-width: 90vw;
                max-height: 80vh;
                overflow-y: auto;
                display: none;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                padding: 24px;
                box-sizing: border-box;
            `;

            const savedMB = (stats.totalSizeSaved / 1024 / 1024).toFixed(2);

            panel.innerHTML = `
                <h3 style="margin-top: 0; color: #333;">图片压缩设置</h3>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: block; margin-bottom: 8px; color: #555;">
                        压缩质量: <span id="quality-value">${this.settings.quality}</span>
                    </label>
                    <input type="range" id="quality-slider" min="0.1" max="1" step="0.05" value="${this.settings.quality}" style="width: 100%;">
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: block; margin-bottom: 8px; color: #555;">
                        输出格式:
                    </label>
                    <select id="output-format" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                        <option value="image/webp" ${this.settings.mimeType === 'image/webp' ? 'selected' : ''}>WebP(推荐,更小体积)</option>
                        <option value="image/jpeg" ${this.settings.mimeType === 'image/jpeg' ? 'selected' : ''}>JPEG(兼容性好)</option>
                        <option value="image/png" ${this.settings.mimeType === 'image/png' ? 'selected' : ''}>PNG(无损压缩)</option>
                    </select>
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: block; margin-bottom: 8px; color: #555;">
                        最大宽度 (px):
                    </label>
                    <input type="number" id="max-width" value="${this.settings.maxWidth}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: block; margin-bottom: 8px; color: #555;">
                        最大高度 (px):
                    </label>
                    <input type="number" id="max-height" value="${this.settings.maxHeight}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: flex; align-items: center; color: #555;">
                        <input type="checkbox" id="auto-compress" ${this.settings.autoCompress ? 'checked' : ''} style="margin-right: 8px;">
                        自动压缩上传的图片
                    </label>
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: flex; align-items: center; color: #555;">
                        <input type="checkbox" id="adaptive-quality" ${this.settings.adaptiveQuality ? 'checked' : ''} style="margin-right: 8px;">
                        自适应压缩质量
                    </label>
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: flex; align-items: center; color: #555;">
                        <input type="checkbox" id="enable-hotkey" ${this.settings.enableHotkey ? 'checked' : ''} style="margin-right: 8px;">
                        启用快捷键唤出设置按钮
                    </label>
                </div>

                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: block; margin-bottom: 8px; color: #555;">
                        快捷键(示例:Alt+C、Ctrl+Shift+P):
                    </label>
                    <input type="text" id="hotkey-input" value="${this.settings.hotkey}"
                           placeholder="例如:Alt+C"
                           style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                    <p style="font-size: 12px; color: #888; margin-top: 4px;">
                        支持 Ctrl / Shift / Alt / Meta(Mac ⌘)+ 字母/数字/F1~F12
                    </p>
                </div>
                <div class="setting-item" style="margin-bottom: 16px;">
                    <label style="display: flex; align-items: center; color: #555;">
                        <input type="checkbox" id="enable-dblclick-reveal" ${this.settings.enableDblClickReveal ? 'checked' : ''} style="margin-right: 8px;">
                        允许双击空白区域唤出按钮
                    </label>
                </div>
                <div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #eee;">
                    <p style="color: #666; font-size: 14px; margin: 0 0 16px 0;">
                        已压缩 ${stats.totalCompressed} 张图片,节省 ${savedMB} MB 空间
                    </p>
                </div>
                <div style="display: flex; gap: 12px; justify-content: flex-end;">
                    <button id="reset-stats" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">
                        重置统计
                    </button>
                    <button id="save-settings" style="padding: 8px 16px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer;">
                        保存设置
                    </button>
                </div>
                <a href="https://github.com/JustDoIt166/AI-Upload-Image-Compressor" target="_blank"
                 style="display: block; margin-top: 12px; color: #2196f3; text-decoration: none; font-size: 13px; text-align: center; display: flex; align-items: center; justify-content: center; gap: 6px;">
                <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" style="fill: currentColor;">
                  <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.81 3.65-3.93 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.04-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.27-.82 2.15 0 3.12 1.86 3.73 3.64 3.93-.24.21-.45.74-.45 1.48 0 1.07.01 1.93.01 2.2 0 .21-.15.46-.55.38A8.013 8.013 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
                </svg>
                查看 脚本源代码
              </a>
            `;

            document.body.appendChild(panel);

            const qualitySlider = panel.querySelector('#quality-slider');
            const qualityValue = panel.querySelector('#quality-value');
            qualitySlider.addEventListener('input', (e) => {
                qualityValue.textContent = e.target.value;
            });

            panel.querySelector('#save-settings').addEventListener('click', () => {
                this.settings.quality = parseFloat(qualitySlider.value);
                this.settings.mimeType = panel.querySelector('#output-format').value;
                this.settings.maxWidth = parseInt(panel.querySelector('#max-width').value);
                this.settings.maxHeight = parseInt(panel.querySelector('#max-height').value);
                this.settings.autoCompress = panel.querySelector('#auto-compress').checked;
                this.settings.adaptiveQuality = panel.querySelector('#adaptive-quality').checked;
                this.settings.enableHotkey = panel.querySelector('#enable-hotkey').checked;
                this.settings.hotkey = panel.querySelector('#hotkey-input').value.trim() || 'Alt+C';
                this.settings.enableDblClickReveal = panel.querySelector('#enable-dblclick-reveal').checked;

                this.saveSettings();
                this.setupHotkeyListener();
                this.showToast('设置已保存', 'success');
                panel.style.display = 'none';
            });

            panel.querySelector('#reset-stats').addEventListener('click', () => {
                stats.totalCompressed = 0;
                stats.totalSizeSaved = 0;
                stats.compressionHistory = [];
                localStorage.setItem('compressStats', JSON.stringify(stats));
                const statEl = panel.querySelector('p');
                if (statEl) {
                    statEl.textContent = `已压缩 0 张图片,节省 0.00 MB 空间`;
                }
                this.showToast('统计已重置', 'info');
            });

            panel.addEventListener('click', (e) => {
                if (e.target === panel) panel.style.display = 'none';
            });
        },

        toggleSettingsPanel() {
            let panel = document.getElementById('compress-settings-panel');
            if (!panel) {
                this.createSettingsPanel();
                panel = document.getElementById('compress-settings-panel');
            }

            if (panel.style.display === 'block') {
                panel.style.display = 'none';
            } else {
                panel.style.display = 'block';
                const savedMB = (stats.totalSizeSaved / 1024 / 1024).toFixed(2);
                const statEl = panel.querySelector('p');
                if (statEl) {
                    statEl.textContent = `已压缩 ${stats.totalCompressed} 张图片,节省 ${savedMB} MB 空间`;
                }
            }
        },

        parseHotkey(hotkeyStr) {
            const parts = hotkeyStr.toLowerCase().split('+').map(p => p.trim());
            const modifiers = { ctrl: false, shift: false, alt: false, meta: false };
            let key = '';

            for (const part of parts) {
                if (part === 'ctrl') modifiers.ctrl = true;
                else if (part === 'shift') modifiers.shift = true;
                else if (part === 'alt') modifiers.alt = true;
                else if (['meta', 'cmd', 'command'].includes(part)) modifiers.meta = true;
                else key = part;
            }

            return { ...modifiers, key };
        },

        handleHotkeyEvent: function (e) {
            const self = ImageCompressor;
            if (!self.settings.enableHotkey || !self.settings.hotkey) return;

            const config = self.parseHotkey(self.settings.hotkey);
            const keyMatch = e.key.toLowerCase() === config.key;
            const ctrlMatch = e.ctrlKey === config.ctrl;
            const shiftMatch = e.shiftKey === config.shift;
            const altMatch = e.altKey === config.alt;
            const metaMatch = e.metaKey === config.meta;

            if (keyMatch && ctrlMatch && shiftMatch && altMatch && metaMatch) {
                e.preventDefault();
                if (self.isButtonHidden) {
                    self.showSettingsButton();
                    const btn = document.getElementById('compress-settings-btn');
                    if (btn) {
                        btn.style.transform = 'scale(1.15)';
                        setTimeout(() => {
                            if (!self.isButtonHidden) {
                                btn.style.transform = 'scale(1)';
                            }
                        }, 200);
                    }
                }
            }
        },

        setupHotkeyListener() {
            document.removeEventListener('keydown', this.handleHotkeyEvent);
            if (this.settings.enableHotkey) {
                document.addEventListener('keydown', this.handleHotkeyEvent);
            }
        },

        setupGlobalRevealOnDblTap() {
            if (!('ontouchstart' in window)) return; //仅移动端

            let lastTap = 0;
            const self = this;

            const handleTouchStart = (e) => {
                if (!self.isButtonHidden) return;

                const target = e.target;
                const interactiveTags = ['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A', 'VIDEO', 'CANVAS'];
                if (interactiveTags.includes(target.tagName) ||
                    target.closest('button, a, input, textarea, [contenteditable="true"]')) {
                    return;
                }

                const now = Date.now();
                if (now - lastTap < 350 && now - lastTap > 0) {
                    e.preventDefault();
                    e.stopPropagation();
                    self.showSettingsButton();
                    self.showToast('设置按钮已显示', 'info');
                    lastTap = 0;
                } else {
                    lastTap = now;
                }
            };

            document.addEventListener('touchstart', handleTouchStart, { passive: false });
        },
        setupDesktopRevealOnDblClick() {
            if ('ontouchstart' in window) return; // 仅桌面端(非触屏)

            const handleDblClick = (e) => {
                if (!this.isButtonHidden) return;

                const target = e.target;
                // 跳过可交互元素
                const interactiveTags = ['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A', 'VIDEO', 'CANVAS'];
                if (interactiveTags.includes(target.tagName) ||
                    target.closest('button, a, input, textarea, [contenteditable="true"]')) {
                    return;
                }

                e.preventDefault();
                e.stopPropagation();
                this.showSettingsButton();
                this.showToast('设置按钮已显示', 'info');
            };

            document.addEventListener('dblclick', handleDblClick);
        },

        showToast(message, type = 'info') {
            let container = document.getElementById('qwen-compress-toast-container');
            if (!container) {
                container = document.createElement('div');
                container.id = 'qwen-compress-toast-container';
                container.style.cssText = `
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    z-index: 999999;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                    pointer-events: none;
                `;
                document.body.appendChild(container);
            }

            const toast = document.createElement('div');
            const bgColor = type === 'error' ? '#ff4444' : type === 'success' ? '#4caf50' : '#2196f3';
            toast.textContent = message;
            toast.style.cssText = `
                background: ${bgColor};
                color: white;
                padding: 10px 16px;
                margin-bottom: 8px;
                border-radius: 6px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                max-width: 300px;
                word-break: break-word;
                font-size: 14px;
                opacity: 0;
                transform: translateX(100%);
                transition: opacity 0.3s ease, transform 0.3s ease;
            `;

            container.appendChild(toast);

            requestAnimationFrame(() => {
                toast.style.opacity = '1';
                toast.style.transform = 'translateX(0)';
            });

            setTimeout(() => {
                toast.style.opacity = '0';
                toast.style.transform = 'translateX(100%)';
                setTimeout(() => {
                    if (toast.parentNode) toast.parentNode.removeChild(toast);
                    if (container && !container.hasChildNodes()) container.remove();
                }, 300);
            }, 3000);
        }
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            ImageCompressor.init();
        });
    } else {
        ImageCompressor.init();
    }
})();