// ==UserScript== // @name Global Image Quick Saver PC // @namespace https://docs.scriptcat.org/ // @version 0.0.1 // @description Alt/Option + 左键快速保存图片,全站可用,强拦截网站跳转,支持跨域图片下载 // @author AmzBasara // @match *://*/* // @grant GM_download // @noframes // @run-at document-start // ==/UserScript== (function () { 'use strict'; const SITE_NAME_MAP = { chatgpt: 'ChatGPT' }; const IMAGE_EXT_REG = /\.(jpg|jpeg|png|webp|gif|bmp|avif|svg)(\?|#|$)/i; let suppressUntil = 0; let lastDownloadUrl = ''; function isAltLeftClick(event) { return event.button === 0 && event.altKey; } function shouldSuppress() { return Date.now() < suppressUntil; } function suppressEvent(event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } function findImageElement(target) { if (!target) return null; if (target.tagName === 'IMG') { return target; } return target.closest?.('img') || null; } function getImageUrl(img) { if (!img) return ''; return ( img.currentSrc || img.src || img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original') || img.getAttribute('data-lazy-src') || img.getAttribute('data-url') || '' ); } function sanitizeFilename(name) { return String(name || '') .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .trim() .slice(0, 150); } function getExtFromMime(mimeType) { const map = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif', 'image/svg+xml': 'svg', 'image/bmp': 'bmp', 'image/avif': 'avif' }; return map[String(mimeType || '').toLowerCase()] || ''; } function getExtFromUrl(url) { try { const u = new URL(url, location.href); // 正常 URL 路径里带后缀 let match = u.pathname.match(/\.(jpg|jpeg|png|webp|gif|bmp|avif|svg)$/i); if (match) { return match[1].toLowerCase() === 'jpeg' ? 'jpg' : match[1].toLowerCase(); } // 兼容这种代理图片: // https://img03.sogoucdn.com/.../?appid=122&url=https://xxx.jpg const innerUrl = u.searchParams.get('url'); if (innerUrl) { match = innerUrl.match(/\.(jpg|jpeg|png|webp|gif|bmp|avif|svg)(\?|#|$)/i); if (match) { return match[1].toLowerCase() === 'jpeg' ? 'jpg' : match[1].toLowerCase(); } } } catch (_) {} return ''; } function getSiteName() { const hostname = location.hostname || 'image'; if (hostname === 'localhost') { return 'Localhost'; } if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { return sanitizeFilename(hostname); } const host = hostname.replace(/^www\./i, ''); const parts = host.split('.').filter(Boolean); let core = parts[0] || 'image'; if (parts.length >= 2) { core = parts[parts.length - 2]; const last = parts[parts.length - 1]; const secondLast = parts[parts.length - 2]; if ( parts.length >= 3 && ['uk', 'jp', 'cn', 'au', 'hk'].includes(last) && ['co', 'com', 'net', 'org', 'gov', 'edu'].includes(secondLast) ) { core = parts[parts.length - 3]; } } const lowerCore = core.toLowerCase(); if (SITE_NAME_MAP[lowerCore]) { return sanitizeFilename(SITE_NAME_MAP[lowerCore]); } const pretty = core .split(/[-_]+/) .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); return sanitizeFilename(pretty || 'Image'); } function getDateTimeString() { const d = new Date(); const yyyy = d.getFullYear(); const MM = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); // Windows 安全格式,不能用冒号 return `${yyyy}-${MM}-${dd} ${hh}-${mm}-${ss}`; } function getFilename(url, mimeType = '') { const siteName = getSiteName(); const dateTime = getDateTimeString(); const ext = getExtFromMime(mimeType) || getExtFromUrl(url) || 'jpg'; return sanitizeFilename(`${siteName}-${dateTime}.${ext}`); } function triggerDownloadByA(href, filename) { const a = document.createElement('a'); a.href = href; a.download = filename; a.rel = 'noopener noreferrer'; a.style.display = 'none'; document.documentElement.appendChild(a); a.click(); a.remove(); } function downloadByGM(url, filename) { return new Promise((resolve, reject) => { if (typeof GM_download !== 'function') { reject(new Error('GM_download 不可用')); return; } GM_download({ url, name: filename, saveAs: false, onload: resolve, onerror: reject, ontimeout: reject }); }); } async function downloadByFetch(url) { const response = await fetch(url, { credentials: 'omit', cache: 'no-store' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const blob = await response.blob(); const objectUrl = URL.createObjectURL(blob); const filename = getFilename(url, blob.type); triggerDownloadByA(objectUrl, filename); setTimeout(() => { URL.revokeObjectURL(objectUrl); }, 3000); } async function saveImageUrl(url) { if (!url) { console.warn('[Image Quick Saver] 没有找到图片地址'); return; } const absoluteUrl = new URL(url, location.href).href; const filename = getFilename(absoluteUrl); console.log('[Image Quick Saver] 尝试下载:', absoluteUrl); // data:image 直接用 a 下载 if (absoluteUrl.startsWith('data:image/')) { triggerDownloadByA(absoluteUrl, filename); return; } // blob: 只能 fetch 当前页面里的 blob if (absoluteUrl.startsWith('blob:')) { try { await downloadByFetch(absoluteUrl); return; } catch (err) { console.warn('[Image Quick Saver] blob fetch 失败:', err); triggerDownloadByA(absoluteUrl, filename); return; } } // 普通 http/https 图片,优先 GM_download,跨域更稳 try { await downloadByGM(absoluteUrl, filename); return; } catch (err) { console.warn('[Image Quick Saver] GM_download 失败,尝试 fetch:', err); } try { await downloadByFetch(absoluteUrl); return; } catch (err) { console.warn('[Image Quick Saver] fetch 失败,尝试 a[download]:', err); } triggerDownloadByA(absoluteUrl, filename); } function handleAltImageEvent(event) { if (!isAltLeftClick(event)) return; const img = findImageElement(event.target); if (!img) return; const url = getImageUrl(img); if (!url) return; suppressUntil = Date.now() + 1500; lastDownloadUrl = url; suppressEvent(event); saveImageUrl(url); } /** * 第一组:真正触发下载。 * pointerdown 比 mousedown 更早,很多现代站点会优先监听 pointer 事件。 */ window.addEventListener('pointerdown', handleAltImageEvent, true); document.addEventListener('pointerdown', handleAltImageEvent, true); window.addEventListener('mousedown', handleAltImageEvent, true); document.addEventListener('mousedown', handleAltImageEvent, true); /** * 第二组:只负责阻止网站后续事件。 * 防止网站在 pointerup / mouseup / click 阶段打开 blob 页面。 */ [ 'pointerup', 'mouseup', 'click', 'auxclick', 'dblclick' ].forEach(eventName => { window.addEventListener( eventName, function (event) { if (!shouldSuppress()) return; const img = findImageElement(event.target); if (!img) return; suppressEvent(event); }, true ); document.addEventListener( eventName, function (event) { if (!shouldSuppress()) return; const img = findImageElement(event.target); if (!img) return; suppressEvent(event); }, true ); }); console.log('[Image Quick Saver] 已启用:Alt/Option + 左键保存图片,强拦截跳转版'); })();