// ==UserScript== // @name AI Image Description Generator Gimini // @namespace http://tampermonkey.net/ // @version 1.0 // @description 使用AI生成网页图片描述 // @author AlphaCat // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 全局变量 let isSelectionMode = false; // 定义默认提示词 const DEFAULT_PROMPT = "Describe the image from the perspective of someone with only a kindergarten education. If it is a realistic photo, analyze the elements of aperture, focal length, and shutter separately from the perspective of a professional photographer and describe the subject matter of the photo. Answer in Chinese."; // 在全局变量部分添加 const DEFAULT_API_KEY = 'AIzaSyCgR85KjcCe3PgmR23ONaFlULii8wqdLvo'; const DEFAULT_API_ENDPOINT = 'https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash-exp:generateContent'; const DEFAULT_MODEL = 'gemini-2.0-flash-exp'; // 添加支持的图片格式 const SUPPORTED_MIME_TYPES = [ 'image/png', 'image/jpeg', 'image/webp', 'image/heic', 'image/heif' ]; const MAX_FILE_SIZE = 7 * 1024 * 1024; // 7MB const TARGET_FILE_SIZE = 1 * 1024 * 1024; // 1MB // 添加日志函数 function log(message, data = null) { const timestamp = new Date().toISOString(); if (data) { console.log(`[Gemini] ${timestamp} ${message}:`, data); } else { console.log(`[Gemini] ${timestamp} ${message}`); } } // 修改图片压缩函数 async function compressImage(base64Image, mimeType) { log('开始压缩图片', { mimeType }); return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { let quality = 0.9; let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); let width = img.width; let height = img.height; log('原始图片尺寸', { width, height }); const MAX_DIMENSION = 2048; if (width > MAX_DIMENSION || height > MAX_DIMENSION) { const ratio = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height); width *= ratio; height *= ratio; log('调整后的图片尺寸', { width, height }); } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); const compress = () => { const base64 = canvas.toDataURL(mimeType, quality); const size = Math.ceil((base64.length * 3) / 4); log('当前压缩质量和大小', { quality, size: `${(size/1024/1024).toFixed(2)}MB` }); if (size > TARGET_FILE_SIZE && quality > 0.1) { quality -= 0.1; compress(); } else { log('压缩完成', { finalQuality: quality, finalSize: `${(size/1024/1024).toFixed(2)}MB` }); resolve(base64.split(',')[1]); } }; compress(); }; img.onerror = (error) => { log('图片加载失败', error); reject(error); }; img.src = `data:${mimeType};base64,${base64Image}`; }); } // 修改图片上传函数 async function uploadImageToGemini(base64Image, mimeType) { try { log('开始上传图片', { mimeType }); if (!SUPPORTED_MIME_TYPES.includes(mimeType)) { throw new Error('不支持的图片格式,仅支持 PNG、JPEG、WEBP、HEIC、HEIF 格式'); } const originalSize = Math.ceil((base64Image.length * 3) / 4); log('原始文件大小', `${(originalSize/1024/1024).toFixed(2)}MB`); let finalBase64 = base64Image; if (originalSize > MAX_FILE_SIZE) { log('图片超过大小限制,开始压缩'); finalBase64 = await compressImage(base64Image, mimeType); } // 转换为二进制数据 const binaryData = atob(finalBase64); const bytes = new Uint8Array(binaryData.length); for (let i = 0; i < binaryData.length; i++) { bytes[i] = binaryData.charCodeAt(i); } const blob = new Blob([bytes], { type: mimeType }); log('准备上传的文件大小', `${(blob.size/1024/1024).toFixed(2)}MB`); // 第一步:发起 resumable 上传请求 log('发起 resumable 上传请求'); const initResponse = await fetch(`https://generativelanguage.googleapis.com/upload/v1beta/files?key=${DEFAULT_API_KEY}`, { method: 'POST', headers: { 'X-Goog-Upload-Protocol': 'resumable', 'X-Goog-Upload-Command': 'start', 'X-Goog-Upload-Header-Content-Length': blob.size.toString(), 'X-Goog-Upload-Header-Content-Type': mimeType, 'Content-Type': 'application/json' }, body: JSON.stringify({ file: { display_name: `image_${Date.now()}.${mimeType.split('/')[1]}` } }) }); // 从响应头中获取上传 URL const uploadUrl = initResponse.headers.get('x-goog-upload-url'); if (!uploadUrl) { throw new Error('未能获取上传 URL'); } log('获取到上传 URL', uploadUrl); // 第二步:上传实际的图片数据 log('开始上传图片数据'); const uploadResponse = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Length': blob.size.toString(), 'X-Goog-Upload-Offset': '0', 'X-Goog-Upload-Command': 'upload, finalize' }, body: blob }); const data = await uploadResponse.json(); log('上传响应数据', data); if (data.file && data.file.uri) { log('文件上传成功', { fileUri: data.file.uri }); return data.file.uri; } else { log('上传响应数据异常', data); throw new Error(`文件上传失败: ${JSON.stringify(data)}`); } } catch (error) { log('上传图片失败', error); throw error; } } // 修改 imageToBase64 函数,添加跨域处理 async function imageToBase64(imgElement) { return new Promise((resolve, reject) => { try { // 创建新的图片对象来处理跨域 const img = new Image(); img.crossOrigin = 'anonymous'; // 关键是添加这个属性 img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); try { ctx.drawImage(img, 0, 0); // 获取图片的实际 MIME 类型 let mimeType = 'image/jpeg'; // 默认值 const src = imgElement.src; if (src.startsWith('data:')) { mimeType = src.split(';')[0].split(':')[1]; } else { const extension = src.split('.').pop().toLowerCase(); const mimeMap = { 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'webp': 'image/webp', 'heic': 'image/heic', 'heif': 'image/heif' }; mimeType = mimeMap[extension] || 'image/jpeg'; } // 返回 base64 数据和 MIME 类型 const base64 = canvas.toDataURL(mimeType).split(',')[1]; resolve({ base64, mimeType }); } catch (e) { // 如果跨域请求失败,尝试直接通过 fetch 获取图片 log('Canvas 绘制失败,尝试直接获取图片', e); fetchImageAsBase64(imgElement.src).then(resolve).catch(reject); } }; img.onerror = () => { // 如果加载失败,尝试直接通过 fetch 获取图片 log('图片加载失败,尝试直接获取图片'); fetchImageAsBase64(imgElement.src).then(resolve).catch(reject); }; // 设置图片源 img.src = imgElement.src; // 如果图片已经被缓存,可能不会触发 onload if (img.complete) { img.onload(); } } catch (error) { reject(error); } }); } // 添加通过 fetch 获取图片的函数 async function fetchImageAsBase64(url) { try { log('开始通过 fetch 获取图片', url); const response = await fetch(url); const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const base64 = reader.result.split(',')[1]; resolve({ base64, mimeType: blob.type }); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } catch (error) { log('Fetch 获取图片失败', error); throw new Error('无法获取图片数据'); } } // 修改生成描述的函数 async function generateImageDescription(imageBase64, prompt, mimeType) { try { log('开始生成图片描述'); log('使用的提示词', prompt); const fileUri = await uploadImageToGemini(imageBase64, mimeType); log('开始调用生成接口'); // 完全按照 demo-gemini.sh 的请求格式修改 const requestBody = { contents: [{ parts: [ { text: prompt || DEFAULT_PROMPT }, { file_data: { // 注意这里是 file_data 而不是 fileData mime_type: mimeType, // 使用下划线格式 file_uri: fileUri // 使用下划线格式 } } ] }] }; log('请求参数', requestBody); // 修改请求 URL,使用 v1beta 版本的 API const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${DEFAULT_API_KEY}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); const data = await response.json(); log('生成接口响应', data); // 解析响应数据 if (data.candidates && data.candidates[0] && data.candidates[0].content) { const text = data.candidates[0].content.parts[0].text; log('成功生成描述'); return text; } else { throw new Error(data.error?.message || '无法获取图片描述'); } } catch (error) { log('生成描述失败', error); throw error; } } // 修改 API 检测功能 async function checkApiKey(apiKey) { try { log('开始验证 API Key'); const response = await fetch(`https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash-exp`, { headers: { 'x-goog-api-key': apiKey } }); const data = await response.json(); log('API 验证响应', data); if (data.name && data.name.includes('gemini-2.0-flash-exp')) { log('API Key 验证成功'); return [data]; } throw new Error('无效的 API Key 或模型不可用'); } catch (error) { log('API 验证失败', error); throw new Error(`API 验证失败: ${error.message}`); } } // 添加样式 GM_addStyle(` .ai-config-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 10000; min-width: 500px; height: auto; } .ai-config-modal h3 { margin: 0 0 15px 0; font-size: 14px; font-weight: bold; color: #333; } .ai-config-modal label { display: inline-block; font-size: 12px; font-weight: bold; color: #333; margin: 0; line-height: normal; height: auto; } .ai-config-modal .input-wrapper { position: relative; display: flex; align-items: center; } .ai-config-modal input { display: block; width: 100%; padding: 2px 24px 2px 2px; margin: 2px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; line-height: normal; height: auto; box-sizing: border-box; } .ai-config-modal .input-icon { position: absolute; right: 4px; width: 16px; height: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #666; font-size: 12px; user-select: none; } .ai-config-modal .clear-icon { right: 24px; } .ai-config-modal .toggle-password { right: 4px; } .ai-config-modal .input-icon:hover { color: #333; } .ai-config-modal .input-group { margin-bottom: 12px; height: auto; display: flex; flex-direction: column; } .ai-config-modal .button-row { display: flex; gap: 10px; align-items: center; margin-top: 5px; } .ai-config-modal .check-button { padding: 4px 8px; border: none; border-radius: 4px; background: #007bff; color: white; cursor: pointer; font-size: 12px; } .ai-config-modal .check-button:hover { background: #0056b3; } .ai-config-modal .check-button:disabled { background: #cccccc; cursor: not-allowed; } .ai-config-modal select { width: 100%; padding: 4px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; margin-top: 2px; } .ai-config-modal .status-text { font-size: 12px; margin-left: 10px; } .ai-config-modal .status-success { color: #28a745; } .ai-config-modal .status-error { color: #dc3545; } .ai-config-modal button { margin: 10px 5px; padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } .ai-config-modal button#ai-save-config { background: #4CAF50; color: white; } .ai-config-modal button#ai-cancel-config { background: #dc3545; color: white; } .ai-config-modal button:hover { opacity: 0.9; } .ai-floating-btn { position: fixed; width: 32px; height: 32px; background: #4CAF50; color: white; border-radius: 50%; cursor: move; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; user-select: none; transition: background-color 0.3s; } .ai-floating-btn:hover { background: #45a049; } .ai-floating-btn svg { width: 20px; height: 20px; fill: white; } .ai-menu { position: absolute; background: white; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 8px; z-index: 10000; display: flex; gap: 8px; } .ai-menu-item { width: 32px; height: 32px; padding: 6px; cursor: pointer; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background-color 0.3s; } .ai-menu-item:hover { background: #f5f5f5; } .ai-menu-item svg { width: 20px; height: 20px; fill: #666; } .ai-menu-item:hover svg { fill: #4CAF50; } .ai-image-options { display: flex; flex-direction: column; gap: 10px; margin: 15px 0; } .ai-image-options button { padding: 8px 15px; border: none; border-radius: 4px; background: #4CAF50; color: white; cursor: pointer; transition: background-color 0.3s; font-size: 14px; } .ai-image-options button:hover { background: #45a049; } #ai-cancel { background: #dc3545; color: white; } #ai-cancel:hover { opacity: 0.9; } .ai-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; background: rgba(0, 0, 0, 0.8); color: white; border-radius: 4px; font-size: 14px; z-index: 10000; animation: fadeInOut 3s ease; pointer-events: none; white-space: pre-line; text-align: center; max-width: 80%; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } @keyframes fadeInOut { 0% { opacity: 0; transform: translate(-50%, 10px); } 10% { opacity: 1; transform: translate(-50%, 0); } 90% { opacity: 1; transform: translate(-50%, 0); } 100% { opacity: 0; transform: translate(-50%, -10px); } } .ai-config-modal .button-group { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .ai-config-modal .button-group button { padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } .ai-config-modal .save-button { background: #007bff; color: white; } .ai-config-modal .save-button:hover { background: #0056b3; } .ai-config-modal .save-button:disabled { background: #cccccc; cursor: not-allowed; } .ai-config-modal .cancel-button { background: #f8f9fa; color: #333; } .ai-config-modal .cancel-button:hover { background: #e2e6ea; } .ai-selecting-image { cursor: crosshair !important; } .ai-selecting-image * { cursor: crosshair !important; } .ai-image-description { position: fixed; background: rgba(0, 0, 0, 0.8); color: white; padding: 8px 12px; border-radius: 4px; font-size: 14px; line-height: 1.4; max-width: 300px; text-align: center; word-wrap: break-word; z-index: 10000; pointer-events: none; animation: fadeIn 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999; } .ai-result-modal { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); position: relative; min-width: 300px; max-width: 1000px; max-height: 540px; overflow-y: auto; width: 90%; } .ai-result-modal h3 { margin: 0 0 10px 0; font-size: 14px; color: #333; } .ai-result-modal .description-code { background: #1e1e1e; color: #ffffff; padding: 12px; border-radius: 4px; margin: 5px 0; cursor: pointer; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; border: 1px solid #333; position: relative; max-height: 500px; overflow-y: auto; font-size: 12px; line-height: 1.4; } .ai-result-modal .description-code * { color: #ffffff !important; } .ai-result-modal .description-code code { color: #ffffff; display: block; width: 100%; } .ai-result-modal .description-code:hover { background: #2d2d2d; color: #ffffff; } .ai-result-modal .copy-hint { font-size: 11px; color: #666; text-align: center; margin: 2px 0; } .ai-result-modal .close-button { position: absolute; top: 8px; right: 8px; background: none; border: none; font-size: 18px; cursor: pointer; color: #666; padding: 2px 6px; line-height: 1; } .ai-result-modal .close-button:hover { color: #333; } .ai-selection-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; cursor: crosshair; pointer-events: none; } .ai-selecting-image img { position: relative; z-index: 9999; cursor: pointer !important; transition: outline 0.2s ease; } .ai-selecting-image img:hover { outline: 2px solid white; outline-offset: 2px; } /* 移动端样式优化 */ @media (max-width: 768px) { .ai-floating-btn { width: 40px; height: 40px; touch-action: none; } .ai-floating-btn svg { width: 24px; height: 24px; } .ai-config-modal { width: 90%; min-width: auto; max-width: 400px; padding: 15px; margin: 10px; box-sizing: border-box; } .ai-config-modal .button-group { margin-top: 15px; flex-direction: row; justify-content: space-between; gap: 10px; } .ai-config-modal .button-group button { flex: 1; min-height: 44px; font-size: 16px; padding: 10px; margin: 0; } .ai-result-modal { width: 95%; min-width: auto; max-width: 90%; margin: 10px; padding: 15px; } .ai-modal-overlay { padding: 10px; box-sizing: border-box; } .ai-config-modal button, .ai-config-modal .input-icon, .ai-config-modal select, .ai-config-modal input { min-height: 44px; padding: 10px; font-size: 16px; } .ai-config-modal textarea { min-height: 100px; font-size: 16px; padding: 10px; } .ai-config-modal .input-icon { width: 44px; height: 44px; font-size: 20px; } .ai-config-modal { max-height: 90vh; overflow-y: auto; -webkit-overflow-scrolling: touch; } } `); // 显示toast提示 function showToast(message, duration = 3000) { const toast = document.createElement('div'); toast.className = 'ai-toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, duration); } // 进入图片选择模式 function enterImageSelectionMode() { if(isSelectionMode) return; isSelectionMode = true; const floatingBtn = document.querySelector('.ai-floating-btn'); if(floatingBtn) { floatingBtn.style.display = 'none'; } const overlay = document.createElement('div'); overlay.className = 'ai-selection-overlay'; document.body.appendChild(overlay); document.body.classList.add('ai-selecting-image'); const clickHandler = async function(e) { if (!isSelectionMode) return; if (e.target.tagName === 'IMG') { e.preventDefault(); e.stopPropagation(); try { showToast('正在生成图片描述...'); // 获取图片的 base64 编码和 MIME 类型 const { base64, mimeType } = await imageToBase64(e.target); // 获取用户设置的提示词 const customPrompt = GM_getValue('customPrompt', DEFAULT_PROMPT); // 调用 Gemini API 获取描述 const description = await generateImageDescription(base64, customPrompt, mimeType); // 创建结果展示模态框 const modalOverlay = document.createElement('div'); modalOverlay.className = 'ai-modal-overlay'; modalOverlay.innerHTML = `

图片描述结果

${description}
点击上方文本可复制
`; document.body.appendChild(modalOverlay); // 添加复制功能 const codeBlock = modalOverlay.querySelector('.description-code'); codeBlock.addEventListener('click', () => { navigator.clipboard.writeText(description); showToast('已复制到剪贴板'); }); // 添加关闭功能 const closeButton = modalOverlay.querySelector('.close-button'); closeButton.addEventListener('click', () => modalOverlay.remove()); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { modalOverlay.remove(); } }); } catch (error) { showToast(`生成描述失败: ${error.message}`); } exitImageSelectionMode(); } }; document.addEventListener('click', clickHandler, true); const escHandler = (e) => { if (e.key === 'Escape') { exitImageSelectionMode(); } }; document.addEventListener('keydown', escHandler); window._imageSelectionHandlers = { click: clickHandler, keydown: escHandler }; } // 退出图片选择模式 function exitImageSelectionMode() { isSelectionMode = false; const floatingBtn = document.querySelector('.ai-floating-btn'); if(floatingBtn) { floatingBtn.style.display = 'flex'; } const overlay = document.querySelector('.ai-selection-overlay'); if (overlay) { overlay.remove(); } document.body.classList.remove('ai-selecting-image'); if (window._imageSelectionHandlers) { document.removeEventListener('click', window._imageSelectionHandlers.click, true); document.removeEventListener('keydown', window._imageSelectionHandlers.keydown); window._imageSelectionHandlers = null; } } // 修改配置界面创建函数 function createConfigUI() { const existingModal = document.querySelector('.ai-modal-overlay'); if (existingModal) { existingModal.remove(); } const overlay = document.createElement('div'); overlay.className = 'ai-modal-overlay'; const modal = document.createElement('div'); modal.className = 'ai-config-modal'; modal.innerHTML = `

AI图像描述配置

`; overlay.appendChild(modal); document.body.appendChild(overlay); // 只保留提示词的重置功能 const clearButtons = modal.querySelectorAll('.clear-icon'); clearButtons.forEach(button => { button.addEventListener('click', function(e) { const input = this.parentElement.querySelector('textarea'); if (input && input.id === 'ai-prompt') { input.value = DEFAULT_PROMPT; input.focus(); } }); }); // 修改保存按钮事件 const saveButton = modal.querySelector('#ai-save-config'); if (saveButton) { saveButton.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); showToast('配置已保存'); overlay.remove(); }); } // 取消配置 const cancelButton = modal.querySelector('#ai-cancel-config'); if (cancelButton) { cancelButton.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); overlay.remove(); }); } // 点击遮罩层关闭 overlay.addEventListener('click', function(e) { if (e.target === overlay) { overlay.remove(); } }); // 阻止模态框内的点击事件冒泡 modal.addEventListener('click', function(e) { e.stopPropagation(); }); } // 创建悬浮按钮 function createFloatingButton() { const btn = document.createElement('div'); btn.className = 'ai-floating-btn'; btn.innerHTML = ` `; const savedPos = JSON.parse(GM_getValue('btnPosition', '{"x": 20, "y": 20}')); btn.style.left = (savedPos.x || 20) + 'px'; btn.style.top = (savedPos.y || 20) + 'px'; btn.style.right = 'auto'; btn.style.bottom = 'auto'; let isDragging = false; let hasMoved = false; let startX, startY; let initialLeft, initialTop; let longPressTimer; let touchStartTime; // 触屏事件处理 btn.addEventListener('touchstart', function(e) { e.preventDefault(); touchStartTime = Date.now(); longPressTimer = setTimeout(() => { exitImageSelectionMode(); createConfigUI(); }, 500); const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; const rect = btn.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; }); btn.addEventListener('touchmove', function(e) { e.preventDefault(); clearTimeout(longPressTimer); const touch = e.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { hasMoved = true; } const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX)); const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY)); btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px'; }); btn.addEventListener('touchend', function(e) { e.preventDefault(); clearTimeout(longPressTimer); const touchDuration = Date.now() - touchStartTime; if (!hasMoved && touchDuration < 500) { enterImageSelectionMode(); } if (hasMoved) { const rect = btn.getBoundingClientRect(); GM_setValue('btnPosition', JSON.stringify({ x: rect.left, y: rect.top })); } hasMoved = false; }); // 鼠标事件处理 btn.addEventListener('click', function(e) { if (e.button === 0 && !hasMoved) { enterImageSelectionMode(); e.stopPropagation(); } hasMoved = false; }); btn.addEventListener('contextmenu', function(e) { e.preventDefault(); exitImageSelectionMode(); createConfigUI(); }); // 拖拽相关事件 function dragStart(e) { if (e.target === btn || btn.contains(e.target)) { isDragging = true; hasMoved = false; const rect = btn.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; initialLeft = rect.left; initialTop = rect.top; e.preventDefault(); } } function drag(e) { if (isDragging) { e.preventDefault(); const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { hasMoved = true; } const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX)); const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY)); btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px'; } } function dragEnd(e) { if (isDragging) { isDragging = false; const rect = btn.getBoundingClientRect(); GM_setValue('btnPosition', JSON.stringify({ x: rect.left, y: rect.top })); } } btn.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); document.body.appendChild(btn); return btn; } // 初始化 function initialize() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { createFloatingButton(); }); } else { createFloatingButton(); } } // 启动脚本 initialize(); })();