您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通用网页图片灯箱:旋转、缩放、拖拽、切换、单张/批量下载,让你看图下图不再受限
// ==UserScript== // @name 通用网页图片灯箱(WebImageBox) // @author setube // @namespace https://github.com/setube/webImageBox // @version 1.6.4 // @description 通用网页图片灯箱:旋转、缩放、拖拽、切换、单张/批量下载,让你看图下图不再受限 // @match *://*/* // @require https://registry.npmmirror.com/fflate/0.8.2/files/umd/index.js // @require https://unpkg.com/[email protected]/dist/index.umd.js // @resource iconFontCSS https://at.alicdn.com/t/c/font_5026690_6mvd6y6o6pr.css // @grant GM_getResourceText // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect * // @license Apache-2.0 // ==/UserScript== ;(function () { 'use strict' // 读取资源 const css = GM_getResourceText('iconFontCSS') // 注入到页面 GM_addStyle(css) // 内联 CSS const style = document.createElement('style') style.textContent = ` #myLightboxOverlay { position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85); display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:100; overflow:hidden; } .lb-main-container { position:relative; width:90%; height:70%; overflow:hidden; display:flex; justify-content:center; align-items:center; } .lb-main { max-width:100%; max-height:100%; position:absolute; } .lb-buttons { position:absolute; top:20px; right:20px; display:flex; gap:8px; z-index:100001; } .lb-buttons button { background: rgba(0,0,0,0.5); color:#fff; border:none; padding:6px 10px; cursor:pointer; border-radius:4px; } .lb-nav-button, .lb-buttons button { background: rgba(0,0,0,0.6); color: #fff; border: none; width: 34px; height: 34px; display: flex; justify-content: center; align-items: center; cursor: pointer; border-radius: 50%; transition: background 0.3s, transform 0.2s; } .lb-nav-button:hover, .lb-buttons button:hover { background: rgba(255,255,255,0.2); } button svg { pointer-events: none; } .lb-prev { position: fixed; left:20px; top:50%; transform:translateY(-50%); } .lb-next { position: fixed; right:20px; top:50%; transform:translateY(-50%); } .lb-thumbs { display:flex; gap:5px; margin-top:10px; overflow-x:auto; max-width:90%; } .lb-thumbs::-webkit-scrollbar { display: none; } .lb-thumbs img { height:60px; cursor:pointer; opacity:0.5; transition:0.3s; flex-shrink:0; } .lb-thumbs img.active { opacity:1; border:2px solid #fff; } .qmsg { z-index: 100002; } @media (max-width: 768px) { .lb-nav-button:hover, .lb-buttons button:hover { background: rgba(0,0,0,0.6); } } ` document.head.appendChild(style) // 创建遮罩和主图片容器 const overlay = document.createElement('div') overlay.id = 'myLightboxOverlay' overlay.style.display = 'none' const mainContainer = document.createElement('div') mainContainer.className = 'lb-main-container' const lbImg = document.createElement('img') lbImg.className = 'lb-main' mainContainer.appendChild(lbImg) overlay.appendChild(mainContainer) // 控制按钮(右上角) const controls = document.createElement('div') controls.className = 'lb-buttons' const btnConfig = [ { title: '左旋转', icon: 'undo' }, { title: '右旋转', icon: 'redo' }, { title: '放大', icon: 'fullscreen' }, { title: '缩小', icon: 'fullscreen-exit' }, { title: '下载', icon: 'download' }, { title: '下载所有', icon: 'file-zip' }, { title: 'Github', icon: 'github-fill' }, { title: '关闭', icon: 'close' } ] btnConfig.forEach(cfg => { const btn = document.createElement('button') btn.title = cfg.title btn.className = `iconfont icon-${cfg.icon}` controls.appendChild(btn) }) overlay.appendChild(controls) // 左右切换按钮 const prevBtn = document.createElement('button') prevBtn.className = 'lb-prev lb-nav-button iconfont icon-left' prevBtn.title = '上一张' prevBtn.setAttribute('aria-label', '上一张') const nextBtn = document.createElement('button') nextBtn.className = 'lb-next lb-nav-button iconfont icon-right' nextBtn.title = '下一张' nextBtn.setAttribute('aria-label', '下一张') overlay.appendChild(prevBtn) overlay.appendChild(nextBtn) // 缩略图列表 const thumbBar = document.createElement('div') thumbBar.className = 'lb-thumbs' overlay.appendChild(thumbBar) document.body.appendChild(overlay) // 图片数组 let imgs = [] let currentIndex = 0 let rotation = 0 let scale = 1 const imgStates = new Map() // key: 图片 src, value: { rotation, scale } // 图片切换动画参数 let isAnimating = false const updateTransform = () => { lbImg.style.transform = `rotate(${rotation}deg) scale(${scale})` } // 缩略图居中 const updateThumbs = () => { thumbBar.innerHTML = '' imgs.forEach((img, i) => { const thumb = document.createElement('img') thumb.src = img.src if (i === currentIndex) thumb.classList.add('active') thumb.addEventListener('click', () => showImage(i)) thumbBar.appendChild(thumb) }) // 缩略图滚动条,让当前图片居中 const activeThumb = thumbBar.querySelector('img.active') if (activeThumb) { const offset = activeThumb.offsetLeft + activeThumb.offsetWidth / 2 - thumbBar.clientWidth / 2 thumbBar.scrollTo({ left: offset, behavior: 'smooth' }) } } // 打开灯箱 const showImage = (index, direction = 0) => { if (imgs.length === 0 || isAnimating) return currentIndex = (index + imgs.length) % imgs.length // 获取该图片的状态,如果没有则初始化 const state = imgStates.get(imgs[currentIndex].src) || { rotation: 0, scale: 1 } rotation = state.rotation scale = state.scale isAnimating = true const newSrc = imgs[currentIndex].src if (lbImg.src) { const tempImg = document.createElement('img') tempImg.src = newSrc tempImg.style.position = 'absolute' tempImg.style.maxWidth = '100%' tempImg.style.maxHeight = '100%' tempImg.style.left = direction >= 0 ? '100%' : '-100%' tempImg.style.top = 'auto' mainContainer.appendChild(tempImg) setTimeout(() => { tempImg.style.left = 'auto' lbImg.style.left = direction >= 0 ? '-100%' : '100%' }, 50) setTimeout(() => { lbImg.src = newSrc lbImg.style.left = 'auto' mainContainer.removeChild(tempImg) updateTransform() overlay.style.display = 'flex' updateThumbs() isAnimating = false }, 50) } else { lbImg.src = newSrc overlay.style.display = 'flex' updateThumbs() isAnimating = false } } const isSmallOrAvatar = img => { // 跳过灯箱内部的缩略图和主图 if (img.closest('#myLightboxOverlay')) return // 忽略头像、小图、被广告插件屏蔽的图片 if ( !img.complete || !img.naturalWidth || !img.naturalHeight || !img.width || !img.height || img.width < 100 || img.height < 100 ) return false // 图片元素必须在页面中可见 const rect = img.getBoundingClientRect() if (!rect.width || !rect.height) return false // CSS 隐藏或无尺寸 const style = getComputedStyle(img) if (style.display === 'none' || style.visibility === 'hidden') return false const keywords = [ 'icon', 'ico', 'avatar', 'ava', 'emoji', 'biaoqing', 'logo', 'btn', 'button', 'qrcode', 'advertisement', 'ads', 'promotation' ] const checkString = str => keywords.some(k => (str || '').toLowerCase().includes(k)) // 检查 img 本身 if (checkString(img.src) || checkString(img.className) || checkString(img.id)) return false for (let attr of img.attributes) { if (checkString(attr.value)) return false } // 检查父 a 标签 let parent = img.parentElement while (parent) { if (checkString(parent.href) || checkString(parent.className) || checkString(parent.id)) return false parent = parent.parentElement } return true } // 设置图片,过滤重复(按 URL 或文件名) const setupImages = () => { const pageImgs = Array.from(document.querySelectorAll('img')) const uniqueSrc = new Set() const uniqueName = new Set() imgs = [] pageImgs.forEach(img => { if (!isSmallOrAvatar(img)) return const fileName = img.src.split('/').pop() if (!uniqueSrc.has(img.src) && !uniqueName.has(fileName)) { uniqueSrc.add(img.src) uniqueName.add(fileName) imgs.push(img) // 避免重复绑定 if (!img.dataset.lb) { img.dataset.lb = 'true' img.style.cursor = 'zoom-in' // 绑定点击事件,打开灯箱 img.addEventListener('click', e => { e.preventDefault() e.stopPropagation() const index = imgs.indexOf(img) openLightbox(index) }) } } }) } const openLightbox = index => { if (imgs.length === 0) return currentIndex = index rotation = 0 scale = 1 // 显示 overlay,初始化透明度和缩放 overlay.style.display = 'flex' overlay.style.opacity = '0' lbImg.style.opacity = '0' lbImg.src = imgs[currentIndex].src // 强制浏览器渲染 requestAnimationFrame(() => { overlay.style.transition = 'opacity 0.35s' overlay.style.opacity = '1' lbImg.style.transition = 'transform 0.35s, opacity 0.35s' lbImg.style.opacity = '1' overlay.style.transition = '' lbImg.style.transition = '' }) updateThumbs() lockBodyScroll() } const closeLightbox = () => { // 淡出动画 overlay.style.opacity = '0' lbImg.style.opacity = '0' setTimeout(() => { overlay.style.display = 'none' // 重置样式,确保下一次打开动画生效 lbImg.style.transition = '' lbImg.style.opacity = '0' }, 350) unlockBodyScroll() } const lockBodyScroll = () => { // 保存当前滚动位置 const scrollY = window.scrollY // 阻止页面滚动 document.body.style.overflow = 'hidden' document.body.dataset.scrollY = scrollY // 保存 scrollY 方便解锁 } const unlockBodyScroll = () => { const scrollY = document.body.dataset.scrollY || 0 document.body.style.overflow = 'auto' window.scrollTo(0, scrollY) } const observer = new MutationObserver(setupImages) observer.observe(document.body, { childList: true, subtree: true }) setupImages() // 控制按钮事件 const [rotateL, rotateR, zoomIn, zoomOut, download, downloadAll, github, closeBtn] = controls.querySelectorAll('button') // 左旋转按钮 rotateL.addEventListener('click', () => { rotation -= 90 imgStates.set(lbImg.src, { rotation, scale }) updateTransform() }) // 右旋转按钮 rotateR.addEventListener('click', () => { rotation += 90 imgStates.set(lbImg.src, { rotation, scale }) updateTransform() }) // 放大按钮 zoomIn.addEventListener('click', () => { scale *= 1.2 imgStates.set(lbImg.src, { rotation, scale }) updateTransform() }) // 缩小按钮 zoomOut.addEventListener('click', () => { scale /= 1.2 imgStates.set(lbImg.src, { rotation, scale }) updateTransform() }) // 打开 Github github.addEventListener('click', () => { window.open('https://github.com/setube/webImageBox', '_blank') }) // 关闭按钮 closeBtn.addEventListener('click', closeLightbox) const fetchBlob = url => { const Referer = new URL(url).origin return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, headers: { Referer }, responseType: 'blob', onload: res => resolve(res.response), onerror: err => reject(err) }) }) } // 单张下载 download.addEventListener('click', async () => { try { let blob, filename const src = lbImg.src if (src.startsWith('data:')) { const [header, data] = src.split(',') const mimeMatch = header.match(/:(.*?)(;|$)/) const mime = mimeMatch ? mimeMatch[1] : 'application/octet-stream' if (header.includes('base64')) { // base64 解码 const bstr = atob(data) const n = bstr.length const u8arr = new Uint8Array(n) for (let i = 0; i < n; i++) u8arr[i] = bstr.charCodeAt(i) blob = new Blob([u8arr], { type: mime }) } else { // URI 编码解码(如 SVG) blob = new Blob([decodeURIComponent(data)], { type: mime }) } // 自动扩展名 let ext = 'png' if (mime === 'image/png') ext = 'png' else if (mime === 'image/jpeg') ext = 'jpg' else if (mime === 'image/svg+xml') ext = 'svg' else if (mime.includes('/')) ext = mime.split('/')[1] filename = `image.${ext}` } else if (src.startsWith('blob:')) { blob = await fetchBlob(src) const mime = blob.type || 'image/png' let ext = mime.includes('/') ? mime.split('/')[1] : 'png' filename = `image.${ext}` } else { const cleanUrl = src.split('?')[0] try { blob = await fetchBlob(cleanUrl) const mime = blob.type || 'image/png' let ext = mime.includes('/') ? mime.split('/')[1] : 'png' filename = cleanUrl.split('/').pop() || `image.${ext}` } catch (err) { Qmsg.error('无法下载 URL:' + cleanUrl) console.error('无法下载 URL:', cleanUrl, err) return // 直接退出,不触发下载 } } const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) Qmsg.success('图片下载成功!') } catch (err) { Qmsg.error('下载失败:' + src) console.error('下载失败', err) } }) // 下载相册 downloadAll.addEventListener('click', async () => { if (!window.fflate) { Qmsg.error('fflate未加载,请稍等') return } let dataUrlCount = 1 const zipFiles = {} const total = imgs.length let completed = 0 // 显示加载条 const loadingMsg = Qmsg.loading(`正在下载图片 0/${total} ...`) // 构建下载任务 const tasks = imgs.map(async img => { try { const src = img.src let uint8arr, filename if (src.startsWith('data:')) { const [header, data] = src.split(',') const mimeMatch = header.match(/:(.*?)(;|$)/) const mime = mimeMatch ? mimeMatch[1] : 'application/octet-stream' if (header.includes('base64')) { const bstr = atob(data) const n = bstr.length uint8arr = new Uint8Array(n) for (let i = 0; i < n; i++) uint8arr[i] = bstr.charCodeAt(i) } else { const decoded = decodeURIComponent(data) uint8arr = new Uint8Array(decoded.length) for (let i = 0; i < decoded.length; i++) uint8arr[i] = decoded.charCodeAt(i) } let ext = mime.split('/')[1] || 'bin' if (mime === 'image/svg+xml') ext = 'svg' filename = `image_${dataUrlCount++}.${ext}` } else if (src.startsWith('blob:')) { Qmsg.warn('blob URL 图片无法下载,已跳过') return } else { let cleanUrl = src.split('?')[0].replace(/\/([^\/]+):[^\/]+$/, '/$1') try { const blob = await fetchBlob(cleanUrl) const arrayBuffer = await blob.arrayBuffer() uint8arr = new Uint8Array(arrayBuffer) const mime = blob.type || 'image/png' let ext = mime.split('/')[1] || 'png' if (mime === 'image/svg+xml') ext = 'svg' const baseName = cleanUrl.split('/').pop() filename = baseName.includes('.') ? baseName : `image_${dataUrlCount++}.${ext}` } catch (err) { Qmsg.error('无法下载 URL: ' + cleanUrl) console.warn('无法下载 URL:', cleanUrl, err) return } } zipFiles[filename] = uint8arr } catch (err) { console.warn('下载失败:', img.src, err) } finally { // 更新进度 completed++ loadingMsg.setText(`正在下载图片 ${completed}/${total} ...`) } }) await Promise.all(tasks) loadingMsg.setText(`图片下载完成,正在生成 ZIP...`) try { const zipped = fflate.zipSync(zipFiles) const blob = new Blob([zipped], { type: 'application/zip' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'album.zip' document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) Qmsg.success('批量下载任务完成!') } catch (err) { Qmsg.error('生成 ZIP 失败') console.error(err) } finally { loadingMsg.close() } }) // 左右切换 prevBtn.addEventListener('click', () => showImage(currentIndex - 1, -1)) nextBtn.addEventListener('click', () => showImage(currentIndex + 1, 1)) // 点击背景或 ESC 关闭 overlay.addEventListener('click', e => { if (e.target === overlay) closeLightbox() }) window.addEventListener('keydown', e => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return if (e.key === 'Escape') closeLightbox() else if (e.key === 'ArrowLeft') showImage(currentIndex - 1, -1) else if (e.key === 'ArrowRight') showImage(currentIndex + 1, 1) }) // 滚轮缩放 window.addEventListener( 'wheel', e => { // 只有灯箱打开时才缩放 if (overlay.style.display === 'flex') { e.preventDefault() // 阻止页面滚动 const delta = e.deltaY || e.detail || e.wheelDelta if (delta < 0) { scale *= 1.1 // 放大 } else { scale /= 1.1 // 缩小 } updateTransform() } }, { passive: false } ) // 双击图片放大 / 恢复 lbImg.addEventListener('dblclick', () => { scale = scale === 1 ? 2 : 1 updateTransform() }) let translateX = 0 let translateY = 0 let isDragging = false let dragStartX = 0 let dragStartY = 0 let spacePressed = false // 阻止浏览器默认拖拽图片 lbImg.addEventListener('dragstart', e => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return e.preventDefault() }) // 监听空格键 document.addEventListener('keydown', e => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return if (e.code === 'Space') { e.preventDefault() // 阻止页面滚动 spacePressed = true lbImg.style.cursor = 'grab' } }) // 监听空格键 document.addEventListener('keyup', e => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return if (e.code === 'Space') { spacePressed = false lbImg.style.cursor = '' // 恢复默认 } }) // 鼠标按下 lbImg.addEventListener('mousedown', e => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return if (!spacePressed) return e.preventDefault() // 阻止默认点击/拖拽行为 isDragging = true dragStartX = e.clientX - translateX dragStartY = e.clientY - translateY lbImg.style.cursor = 'grabbing' }) // 鼠标移动 document.addEventListener('mousemove', e => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return if (!isDragging) return translateX = e.clientX - dragStartX translateY = e.clientY - dragStartY lbImg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale}) rotate(${rotation}deg)` }) // 鼠标松开 window.addEventListener('mouseup', () => { // 灯箱没打开就不处理 if (overlay.style.display !== 'flex') return isDragging = false lbImg.style.cursor = spacePressed ? 'grab' : 'default' }) })()