// ==UserScript== // @name 图片下载工具 // @namespace https://tampermonkey.net/ // @version 1.0.2 // @description 双击图片下载或点击下载图标 // @match *://*/* // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/572743/%E5%9B%BE%E7%89%87%E4%B8%8B%E8%BD%BD%E5%B7%A5%E5%85%B7.user.js // @updateURL https://update.greasyfork.icu/scripts/572743/%E5%9B%BE%E7%89%87%E4%B8%8B%E8%BD%BD%E5%B7%A5%E5%85%B7.meta.js // ==/UserScript== (function () { 'use strict'; const ENABLE_KEY = 'imageDownloaderEnabled'; const MIN_WIDTH_KEY = 'imageDownloaderMinWidth'; const MIN_HEIGHT_KEY = 'imageDownloaderMinHeight'; const MIN_SIZE_DEFAULT = 200; const ICON_SIZE = 24; const ICON_GAP = 6; let isEnabled = GM_getValue(ENABLE_KEY, true); let minWidth = normalizeMinSizeValue(GM_getValue(MIN_WIDTH_KEY, MIN_SIZE_DEFAULT)); let minHeight = normalizeMinSizeValue(GM_getValue(MIN_HEIGHT_KEY, MIN_SIZE_DEFAULT)); let currentImage = null; let currentHost = null; let leaveTimer = null; const icon = document.createElement('button'); icon.type = 'button'; icon.innerHTML = ''; icon.style.position = 'absolute'; icon.style.width = `${ICON_SIZE}px`; icon.style.height = `${ICON_SIZE}px`; icon.style.display = 'flex'; icon.style.alignItems = 'center'; icon.style.justifyContent = 'center'; icon.style.padding = '0'; icon.style.border = '0'; icon.style.borderRadius = '999px'; icon.style.background = 'rgba(0, 0, 0, 0.7)'; icon.style.color = '#fff'; icon.style.cursor = 'pointer'; icon.style.zIndex = '2147483647'; icon.style.visibility = 'hidden'; icon.style.boxShadow = '0 2px 8px rgba(0,0,0,0.35)'; function registerMenu() { const label = isEnabled ? '图片下载:已开启(点击关闭)' : '图片下载:已关闭(点击开启)'; GM_registerMenuCommand(label, () => { isEnabled = !isEnabled; GM_setValue(ENABLE_KEY, isEnabled); if (!isEnabled) { hideIcon(); } location.reload(); }); GM_registerMenuCommand(`最小尺寸:${minWidth} x ${minHeight}(点击设置)`, () => { const widthInput = window.prompt('请输入最小宽度(像素)', String(minWidth)); if (widthInput === null) { return; } const heightInput = window.prompt('请输入最小高度(像素)', String(minHeight)); if (heightInput === null) { return; } const nextWidth = parseInt(widthInput, 10); const nextHeight = parseInt(heightInput, 10); if (!Number.isFinite(nextWidth) || !Number.isFinite(nextHeight) || nextWidth <= 0 || nextHeight <= 0) { window.alert('请输入大于 0 的整数宽高'); return; } minWidth = nextWidth; minHeight = nextHeight; GM_setValue(MIN_WIDTH_KEY, minWidth); GM_setValue(MIN_HEIGHT_KEY, minHeight); hideIcon(); location.reload(); }); } function normalizeMinSizeValue(value) { const parsed = parseInt(String(value), 10); if (!Number.isFinite(parsed) || parsed <= 0) { return MIN_SIZE_DEFAULT; } return parsed; } function getImageUrl(img) { if (!img) { return ''; } if (img.currentSrc) { return img.currentSrc; } return img.src || ''; } function getFilenameFromUrl(url) { try { const parsed = new URL(url, location.href); const raw = parsed.pathname.split('/').pop() || ''; const safe = raw.replace(/[\\/:*?"<>|]/g, '_'); if (safe) { return safe; } } catch (error) { const fallback = String(url || '').split('/').pop()?.split('?')[0] || ''; const safe = fallback.replace(/[\\/:*?"<>|]/g, '_'); if (safe) { return safe; } } return `image_${Date.now()}.jpg`; } function downloadImage(url) { if (!isEnabled || !url) { return; } if (url.startsWith('data:')) { return; } GM_download({ url, name: getFilenameFromUrl(url), saveAs: false }); } function ensureHostPosition(host) { const computed = window.getComputedStyle(host).position; if (computed === 'static') { host.dataset.tmImageDownloaderRelative = '1'; host.style.position = 'relative'; } } function isImageSizeValid(img) { if (!(img instanceof HTMLImageElement)) { return false; } const width = img.naturalWidth || img.clientWidth || img.width || 0; const height = img.naturalHeight || img.clientHeight || img.height || 0; return width >= minWidth && height >= minHeight; } function attachIconToImage(img) { if (!img || !isEnabled) { return; } if (!isImageSizeValid(img)) { hideIcon(); return; } const host = img.parentElement; if (!host) { return; } ensureHostPosition(host); currentImage = img; currentHost = host; if (!host.contains(icon)) { host.appendChild(icon); } const left = img.offsetLeft + img.clientWidth - ICON_SIZE - ICON_GAP; const top = img.offsetTop + ICON_GAP; icon.style.left = `${Math.max(ICON_GAP, left)}px`; icon.style.top = `${Math.max(ICON_GAP, top)}px`; icon.style.visibility = 'visible'; } function hideIcon() { icon.style.visibility = 'hidden'; if (icon.parentElement) { icon.parentElement.removeChild(icon); } currentImage = null; currentHost = null; } function bindImage(img) { if (!img || img.dataset.tmImageDownloaderBound === '1') { return; } img.dataset.tmImageDownloaderBound = '1'; img.addEventListener('mouseenter', () => { clearTimeout(leaveTimer); attachIconToImage(img); }); img.addEventListener('mouseleave', () => { leaveTimer = window.setTimeout(() => { if (!icon.matches(':hover')) { hideIcon(); } }, 100); }); } function bindAllImages(root) { const scope = root && root.querySelectorAll ? root : document; const images = scope.querySelectorAll('img'); images.forEach(bindImage); } function observeImages() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { mutation.addedNodes.forEach((node) => { if (!(node instanceof Element)) { return; } if (node.tagName && node.tagName.toLowerCase() === 'img') { bindImage(node); return; } bindAllImages(node); }); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } icon.addEventListener('mouseenter', () => { clearTimeout(leaveTimer); }); icon.addEventListener('mouseleave', () => { leaveTimer = window.setTimeout(() => { hideIcon(); }, 100); }); icon.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const url = getImageUrl(currentImage); downloadImage(url); }); document.addEventListener('dblclick', (event) => { if (!isEnabled) { return; } const target = event.target; if (!(target instanceof HTMLImageElement)) { return; } if (!isImageSizeValid(target)) { return; } const url = getImageUrl(target); downloadImage(url); }, true); registerMenu(); document.documentElement.appendChild(icon); bindAllImages(document); observeImages(); })();