您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在图片对象上面:特别优化了Chrome浏览器下的点击拦截,不会点击后图片放大。1、按ctrl单击图片自动下载(默认转换为JPG格式);2、按alt 单击图片会弹窗选择命名方式(无需刷新网页即可生效),文件名可以根据当前日期时间time、网页域名domain、网页标题title,或用户自定义进行命名。
// ==UserScript== // @name ctrl+单击图片自动下载超级增强版 // @namespace https://www.uiwow.com/ // @version 1.1 // @description 在图片对象上面:特别优化了Chrome浏览器下的点击拦截,不会点击后图片放大。1、按ctrl单击图片自动下载(默认转换为JPG格式);2、按alt 单击图片会弹窗选择命名方式(无需刷新网页即可生效),文件名可以根据当前日期时间time、网页域名domain、网页标题title,或用户自定义进行命名。 // @author Techwb.cn // @match *://*/* // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; var clickedImage = null; var nameOption = GM_getValue('nameOption', 'time'); //默认命名方式time var customName = GM_getValue('customName', ''); var isDownloading = false; // 检测Chrome浏览器 var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); // 关键改进:使用多个事件阶段和类型进行拦截 document.addEventListener('mousedown', handleMouseDown, true); document.addEventListener('click', handleClick, true); document.addEventListener('mouseup', handleMouseUp, true); // 拦截右键菜单,防止图片被打开 if (isChrome) { document.addEventListener('contextmenu', function(e) { if (isDownloading && e.target.tagName === 'IMG') { e.preventDefault(); e.stopImmediatePropagation(); } }, true); } function handleMouseDown(event) { var target = event.target; if (isImageElement(target) && event.button === 0) { clickedImage = target; if (event.ctrlKey) { // 标记正在下载 isDownloading = true; // 彻底阻止事件传播 event.stopImmediatePropagation(); event.preventDefault(); // 特殊处理Chrome浏览器 if (isChrome) { // 临时修改图片属性,防止浏览器打开 var originalSrc = clickedImage.src; clickedImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='; // 延迟执行下载,确保属性修改生效 setTimeout(function() { handleDownload(originalSrc); // 恢复图片源(可选) setTimeout(function() { clickedImage.src = originalSrc; isDownloading = false; }, 100); }, 0); } else { handleDownload(); } } else if (event.altKey) { event.preventDefault(); handleNaming(); } } } function handleClick(event) { // 确保Ctrl+点击事件不会触发其他点击处理 if (isDownloading && event.button === 0) { event.stopImmediatePropagation(); event.preventDefault(); } } function handleMouseUp(event) { // 重置下载状态 if (isDownloading && event.button === 0) { setTimeout(function() { isDownloading = false; }, 500); } } // 增强版:判断元素是否是图片或图片容器 function isImageElement(element) { if (element.tagName === 'IMG') return true; // 检查是否是常见的图片查看器容器 if (element.classList.contains('fancybox-image') || element.classList.contains('magnifier-image') || element.classList.contains('zoom-img') || element.classList.contains('photo-gallery-image')) { return true; } // 检查内部是否包含图片 var img = element.querySelector('img'); if (img && element.getBoundingClientRect().width === img.getBoundingClientRect().width) { return true; } return false; } function handleDownload(forcedSrc) { if (!clickedImage) return; // 使用强制提供的src(如果有) var imgSrc = forcedSrc || getRealImageUrl(clickedImage); if (!imgSrc) { console.log('无法获取图片URL'); return; } // 尝试使用GM_download,失败则回退到原生方法 try { // 先获取原始文件名 var fileName = getFileName(imgSrc); // 如果是GIF,进一步检查是否为动画 if (fileName.toLowerCase().endsWith('.gif')) { checkIfAnimatedGif(imgSrc).then(isAnimated => { // 如果不是动画且用户偏好其他格式,可以考虑转换 // 这里保持原有行为,只确保正确的扩展名 downloadWithName(imgSrc, fileName); }).catch(err => { console.log('GIF动画检测失败,使用默认文件名:', err); downloadWithName(imgSrc, fileName); }); } else { // 非GIF图片直接下载 downloadWithName(imgSrc, fileName); } } catch (e) { console.log('下载出错,回退到原生方法:', e); fallbackDownload(imgSrc, getFileName(imgSrc)); } } function downloadWithName(imgSrc, fileName) { if (typeof GM_download !== 'undefined') { GM_download({ url: imgSrc, name: fileName, onerror: function(err) { console.log('GM_download失败,回退到原生方法:', err); fallbackDownload(imgSrc, fileName); } }); } else { fallbackDownload(imgSrc, fileName); } } // 原生下载方法,增强了对不同URL类型的处理 function fallbackDownload(imgSrc, fileName) { // 处理dataURL if (imgSrc.startsWith('data:')) { var a = document.createElement('a'); a.href = imgSrc; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); return; } // 处理普通URL fetch(imgSrc, { mode: 'cors' }) .then(function(response) { if (!response.ok) throw new Error('网络响应错误: ' + response.status); return response.blob(); }) .then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }) .catch(function(error) { console.log('下载失败:', error); alert('下载失败: ' + error.message); }); } // 增强版:获取真实图片URL,处理各种情况 function getRealImageUrl(imgElement) { // 优先使用data-src等可能的真实图片源 var imgSrc = imgElement.dataset.src || imgElement.dataset.original || imgElement.dataset.lazySrc || imgElement.dataset.image || imgElement.src; // 处理图片代理URL if (imgSrc.includes('proxy?url=')) { imgSrc = decodeURIComponent(imgSrc.split('proxy?url=')[1]); } // 确保URL有效 if (!imgSrc || imgSrc.startsWith('data:')) { return imgSrc; } // 添加协议头(如果缺失) if (imgSrc.startsWith('//')) { imgSrc = window.location.protocol + imgSrc; } return imgSrc; } // 关键改进:智能识别图片格式,保持GIF不变 function getFileName(imgUrl) { var fileName = ''; if (nameOption === 'time') { fileName = '图片_' + getDate(); } else if (nameOption === 'domain') { fileName = getDomain(); } else if (nameOption === 'title') { fileName = getTitle(); } else { fileName = customName; } // 从URL中提取文件扩展名 var ext = getFileExtension(imgUrl); // 过滤非法文件名字符 fileName = fileName.replace(/[\\/:*?"<>|]/g, '_'); // 添加扩展名(如果有) return ext ? fileName + '.' + ext : fileName + '.jpg'; } // 关键改进:获取并保留原始图片格式 function getFileExtension(url) { if (!url) return ''; // 尝试从URL中提取扩展名 var ext = url.split('?')[0].split('.').pop().toLowerCase(); // 检查是否是有效的图片扩展名 var validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; if (validExtensions.includes(ext)) { return ext; } // 如果无法确定,尝试从Content-Type判断(针对dataURL) if (url.startsWith('data:')) { var matches = url.match(/data:image\/([^;]+);/); if (matches && matches[1]) { return matches[1].toLowerCase(); } } // 默认返回jpg(保持原有行为) return ''; } // 新增:检测GIF是否为动画 function checkIfAnimatedGif(url) { return new Promise((resolve, reject) => { // 对于dataURL直接处理 if (url.startsWith('data:')) { checkGifData(url).then(resolve).catch(reject); return; } // 对于普通URL,先获取二进制数据 fetch(url) .then(response => response.arrayBuffer()) .then(buffer => { checkGifBuffer(buffer).then(resolve).catch(reject); }) .catch(reject); }); } // 检测GIF数据是否包含多个帧 function checkGifBuffer(buffer) { return new Promise((resolve, reject) => { try { // 检查GIF签名 (GIF87a 或 GIF89a) const signature = new TextDecoder().decode(buffer.slice(0, 6)); if (signature !== 'GIF87a' && signature !== 'GIF89a') { resolve(false); return; } // 查找动画帧标记 (0x2C) const view = new DataView(buffer); let offset = 13; // 跳过头部 // 查找图像分隔符 (0x2C) let frameCount = 0; while (offset < view.byteLength) { const code = view.getUint8(offset); if (code === 0x2C) { // 图像分隔符 frameCount++; if (frameCount > 1) { resolve(true); // 发现多个帧,是动画 return; } // 跳过图像数据 offset += 9; // 跳到图像宽度字段 offset += 2; // 跳过宽度 offset += 2; // 跳过高度 offset += 1; // 跳过标志位 // 跳过局部颜色表 const flags = view.getUint8(offset - 1); if (flags & 0x80) { // 有局部颜色表 const colorTableSize = (flags & 0x07) + 1; offset += 3 * Math.pow(2, colorTableSize); } // 跳过图像数据 while (offset < view.byteLength) { const blockSize = view.getUint8(offset); if (blockSize === 0) break; offset += blockSize + 1; } offset++; // 跳过块终止符 } else if (code === 0x21) { // 扩展块 offset += 1; // 跳过扩展标签 // 跳过扩展块 while (offset < view.byteLength) { const blockSize = view.getUint8(offset); if (blockSize === 0) break; offset += blockSize + 1; } offset++; // 跳过块终止符 } else { break; // 不是有效的GIF格式 } } resolve(frameCount > 1); // 如果只有一帧,不是动画 } catch (error) { console.error('GIF解析错误:', error); resolve(false); // 解析失败,保守地认为不是动画 } }); } // 处理dataURL格式的GIF function checkGifData(dataUrl) { return new Promise((resolve, reject) => { try { // 提取base64部分 const base64 = dataUrl.split(',')[1]; if (!base64) { resolve(false); return; } // 转换为ArrayBuffer const binary = atob(base64); const buffer = new ArrayBuffer(binary.length); const view = new Uint8Array(buffer); for (let i = 0; i < binary.length; i++) { view[i] = binary.charCodeAt(i); } // 检查GIF数据 checkGifBuffer(buffer).then(resolve).catch(reject); } catch (error) { console.error('GIF dataURL解析错误:', error); resolve(false); } }); } // 以下是原脚本的其他功能,保持不变... function handleNaming() { var dateTime = getDate(); var domainName = getDomain(); var titleName = getTitle(); var defaultName = dateTime + '' + titleName + '' + domainName + '.jpg'; var optionsHtml = '<option value="time" ' + (nameOption === 'time' ? 'selected' : '') + '>时间命名方式</option>' + '<option value="domain" ' + (nameOption === 'domain' ? 'selected' : '') + '>域名命名方式</option>' + '<option value="title" ' + (nameOption === 'title' ? 'selected' : '') + '>标题命名方式</option>' + '<option value="custom" ' + (nameOption === 'custom' ? 'selected' : '') + '>自定义命名方式</option>'; var html = '<div><label for="nameOption">命名方式:</label><select id="nameOption">' + optionsHtml + '</select></div>'; var customNameHtml = '<div style="display:none;">' + // 初始隐藏 '<label for="customName">请输入文件名:</label>' + '<input type="text" id="customName" value="' + customName + '">' + '</div>'; var fileNameHtml = '<div><label for="fileName">文件名:</label><input type="text" id="fileName" value="' + defaultName + '" readonly></div>'; var dialog = document.createElement('div'); dialog.innerHTML = html + customNameHtml + fileNameHtml; dialog.style.position = 'fixed'; dialog.style.top = '50%'; dialog.style.left = '50%'; dialog.style.transform = 'translate(-50%, -50%)'; dialog.style.backgroundColor = '#fff'; dialog.style.padding = '20px'; dialog.style.borderRadius = '5px'; dialog.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)'; dialog.style.zIndex = '9999'; document.body.appendChild(dialog); var select = dialog.querySelector('#nameOption'); var customNameInput = dialog.querySelector('#customName'); var fileNameInput = dialog.querySelector('#fileName'); var closeButton = document.createElement('button'); closeButton.innerText = '关闭'; closeButton.style.position = 'absolute'; closeButton.style.top = '2px'; closeButton.style.right = '2px'; closeButton.style.padding = '3px 5px'; closeButton.style.backgroundColor = '#e74c3c'; closeButton.style.border = 'none'; closeButton.style.color = '#fff'; closeButton.style.borderRadius = '3px'; closeButton.style.cursor = 'pointer'; closeButton.addEventListener('click', function() { document.body.removeChild(dialog); }); dialog.appendChild(closeButton); select.addEventListener('change', function() { nameOption = select.value; updateFileName(); GM_setValue('nameOption', nameOption); if (nameOption === 'custom') { customNameInput.parentNode.style.display = 'block'; // 显示 } else { customNameInput.parentNode.style.display = 'none'; // 隐藏 GM_setValue('customName', ''); } }); customNameInput.addEventListener('input', function() { customName = customNameInput.value; updateFileName(); GM_setValue('customName', customName); }); function updateFileName() { var fileName = ''; if (nameOption === 'time') { fileName = '图片_' + getDate(); } else if (nameOption === 'domain') { fileName = getDomain(); } else if (nameOption === 'title') { fileName = getTitle(); } else { fileName = customName; } // 这里默认显示为jpg,实际下载时会根据图片类型自动调整 fileNameInput.value = fileName + '.jpg'; } updateFileName(); } function getDate() { var now = new Date(); return now.getFullYear() + '-' + (now.getMonth() + 1).toString().padStart(2, '0') + '-' + now.getDate().toString().padStart(2, '0') + '_' + now.getHours().toString().padStart(2, '0') + now.getMinutes().toString().padStart(2, '0') + now.getSeconds().toString().padStart(2, '0'); } function getDomain() { var url = window.location.href; var domain = url.split('/')[2]; domain = domain.replace(/www\./, ''); return domain; } function getTitle() { var title = document.title; return title.substring(0, 10).replace(/[\\/:*?"<>|]/g, '_'); } })();