Greasy Fork

来自缓存

Greasy Fork is available in English.

图片下载工具

双击图片下载或点击下载图标

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();