Greasy Fork is available in English.
双击图片下载或点击下载图标
// ==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
// ==/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 = '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 3a1 1 0 0 1 1 1v9.59l2.3-2.3a1 1 0 1 1 1.4 1.42l-4 3.98a1 1 0 0 1-1.4 0l-4-3.98a1 1 0 1 1 1.4-1.42l2.3 2.3V4a1 1 0 0 1 1-1Zm-6 15a1 1 0 0 1 1 1v1h10v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Z"/></svg>';
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();
})();