1. 安装油猴插件
2. 打开油猴面板,添加新脚本
功能介绍
1.点击图片可以查看大图
2.图片是按钮的,长按图片即可
// ==UserScript==
// @name Smart Image Viewer Pro
// @namespace https://tampermonkey.net/
// @version 1.2.0
// @description Lzh10 图片查看器:点击查看、长按查看、缩放、旋转、拖拽、画廊、下载与全屏
// @author Lzh10
// @match *://*/*
// @run-at document-start
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(() => {
'use strict';
/*
* 配置
*/
const CONFIG = {
minWidth: 100,
minHeight: 100,
longPressTime: 550,
moveThreshold: 8,
maxScale: 20,
minScale: 0.05,
wheelZoomSpeed: 0.0015,
enableNormalClick: true,
enableLongPress: true,
showToolbar: true,
enableDownload: true,
enableCopy: true,
enableGallery: true,
ignoreSmallImages: true,
ignoredParents: [
'svg',
'canvas',
'video',
'iframe'
]
};
/*
* 状态
*/
let viewer = null;
let viewerImage = null;
let toolbar = null;
let info = null;
let currentImages = [];
let currentIndex = -1;
let scale = 1;
let rotation = 0;
let translateX = 0;
let translateY = 0;
let dragging = false;
let dragStartX = 0;
let dragStartY = 0;
let startTranslateX = 0;
let startTranslateY = 0;
let pointerDownX = 0;
let pointerDownY = 0;
let longPressTimer = null;
let longPressTriggered = false;
let currentSourceImage = null;
let isOpening = false;
/*
* 工具函数
*/
function isElementVisible(el) {
if (!el || !(el instanceof Element)) {
return false;
}
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
return (
rect.width > 0 &&
rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
parseFloat(style.opacity || '1') > 0
);
}
function isLargeEnough(img) {
if (!CONFIG.ignoreSmallImages) {
return true;
}
const rect = img.getBoundingClientRect();
const width = Math.max(
img.naturalWidth || 0,
rect.width
);
const height = Math.max(
img.naturalHeight || 0,
rect.height
);
return (
width >= CONFIG.minWidth &&
height >= CONFIG.minHeight
);
}
function getImageUrl(img) {
if (!img) {
return null;
}
return (
img.currentSrc ||
img.src ||
img.getAttribute('data-src') ||
img.getAttribute('data-original') ||
img.getAttribute('data-lazy-src') ||
img.getAttribute('data-url') ||
null
);
}
function isImageUrl(url) {
if (!url) {
return false;
}
return (
/^data:image\//i.test(url) ||
/\.(jpg|jpeg|png|gif|webp|bmp|svg|avif|ico)(?:[?#].*)?$/i.test(url)
);
}
function looksLikeExistingViewer(img) {
if (!img) {
return true;
}
const selectors = [
'[data-lightbox]',
'[data-gallery]',
'[data-fancybox]',
'[data-glightbox]',
'[data-pswp]',
'[data-photoswipe]',
'[data-viewer]',
'[data-image-viewer]',
'[data-zoom]',
'[data-zoom-image]',
'.lightbox',
'.light-box',
'.fancybox',
'.fancybox-container',
'.fancybox-image',
'.pswp',
'.photoswipe',
'.photo-swipe',
'.glightbox',
'.viewer',
'.viewer-container',
'.image-viewer',
'.imageViewer',
'.gallery-viewer',
'.lightGallery',
'.lg-container',
'.zoomable',
'.zoom-image',
'.magnify',
'.magnifier'
];
try {
for (const selector of selectors) {
if (
img.matches(selector) ||
img.closest(selector)
) {
return true;
}
}
} catch (_) {}
let node = img;
for (
let i = 0;
i < 5 && node;
i++,
node = node.parentElement
) {
const onclick = node.getAttribute?.('onclick');
if (onclick) {
const text = onclick.toLowerCase();
if (
text.includes('lightbox') ||
text.includes('fancybox') ||
text.includes('photoswipe') ||
text.includes('gallery') ||
text.includes('zoom') ||
text.includes('viewer') ||
text.includes('magnif')
) {
return true;
}
}
}
const link = img.closest('a');
if (link) {
const href = link.href;
if (
href &&
(
isImageUrl(href) ||
href === getImageUrl(img)
)
) {
return true;
}
}
return false;
}
function getClickableParent(img) {
if (!img) {
return null;
}
let node = img.parentElement;
for (
let i = 0;
i < 6 && node;
i++,
node = node.parentElement
) {
if (
node.tagName === 'A' ||
node.tagName === 'BUTTON' ||
node.getAttribute('role') === 'button' ||
node.getAttribute('role') === 'link' ||
node.hasAttribute('onclick') ||
node.hasAttribute('data-href') ||
node.hasAttribute('data-url') ||
node.tabIndex >= 0
) {
return node;
}
}
return null;
}
function shouldIgnore(img) {
if (!(img instanceof HTMLImageElement)) {
return true;
}
if (!isElementVisible(img)) {
return true;
}
if (!isLargeEnough(img)) {
return true;
}
const url = getImageUrl(img);
if (!url) {
return true;
}
for (const parentSelector of CONFIG.ignoredParents) {
if (img.closest(parentSelector)) {
return true;
}
}
const role = img.getAttribute('role');
if (role === 'presentation') {
return true;
}
if (img.getAttribute('aria-hidden') === 'true') {
return true;
}
const cursor = getComputedStyle(img).cursor;
if (cursor === 'pointer') {
if (looksLikeExistingViewer(img)) {
return true;
}
}
if (looksLikeExistingViewer(img)) {
return true;
}
return false;
}
function collectImages() {
if (!CONFIG.enableGallery) {
return currentSourceImage
? [currentSourceImage]
: [];
}
const imgs = Array.from(document.images).filter(img => {
if (shouldIgnore(img)) {
return false;
}
return !!getImageUrl(img);
});
const result = [];
const seen = new Set();
for (const img of imgs) {
const url = getImageUrl(img);
if (!url || seen.has(url)) {
continue;
}
seen.add(url);
result.push(img);
}
return result;
}
/*
* 创建查看器
*/
function createViewer() {
if (viewer) {
return;
}
viewer = document.createElement('div');
viewer.id = 'smart-image-viewer';
viewer.innerHTML = `
<div class="siv-backdrop"></div>
<div class="siv-stage">
<img
class="siv-image"
draggable="false"
alt=""
>
<div class="siv-loading">
<div class="siv-spinner"></div>
<span>加载中…</span>
</div>
</div>
<div class="siv-topbar">
<div class="siv-title">
<span class="siv-counter"></span>
</div>
<div class="siv-actions">
<button
data-action="download"
title="下载"
aria-label="下载"
>
<svg viewBox="0 0 24 24">
<path d="M12 3v12"></path>
<path d="m7 10 5 5 5-5"></path>
<path d="M5 19h14"></path>
</svg>
</button>
<button
data-action="copy"
title="复制地址"
aria-label="复制地址"
>
<svg viewBox="0 0 24 24">
<rect
x="8"
y="8"
width="11"
height="11"
rx="2"
></rect>
<path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h3"></path>
</svg>
</button>
<button
data-action="fullscreen"
title="全屏"
aria-label="全屏"
>
<svg viewBox="0 0 24 24">
<path d="M8 3H3v5"></path>
<path d="M16 3h5v5"></path>
<path d="M21 16v5h-5"></path>
<path d="M3 16v5h5"></path>
</svg>
</button>
<button
data-action="close"
title="关闭"
aria-label="关闭"
>
<svg viewBox="0 0 24 24">
<path d="M6 6l12 12"></path>
<path d="M18 6 6 18"></path>
</svg>
</button>
</div>
</div>
<button
class="siv-prev"
data-action="prev"
title="上一张"
aria-label="上一张"
>
<svg viewBox="0 0 24 24">
<path d="m15 18-6-6 6-6"></path>
</svg>
</button>
<button
class="siv-next"
data-action="next"
title="下一张"
aria-label="下一张"
>
<svg viewBox="0 0 24 24">
<path d="m9 18 6-6-6-6"></path>
</svg>
</button>
<div class="siv-toolbar">
<button
data-action="zoom-out"
title="缩小"
aria-label="缩小"
>
<svg viewBox="0 0 24 24">
<path d="M5 12h14"></path>
</svg>
</button>
<button
data-action="fit"
title="适应窗口"
aria-label="适应窗口"
>
<svg viewBox="0 0 24 24">
<path d="M4 8V5a1 1 0 0 1 1-1h3"></path>
<path d="M16 4h3a1 1 0 0 1 1 1v3"></path>
<path d="M20 16v3a1 1 0 0 1-1 1h-3"></path>
<path d="M8 20H5a1 1 0 0 1-1-1v-3"></path>
</svg>
</button>
<button
data-action="actual"
title="实际大小"
aria-label="实际大小"
>
<svg viewBox="0 0 24 24">
<path d="M4 4h6"></path>
<path d="M4 4v6"></path>
<path d="M20 4h-6"></path>
<path d="M20 4v6"></path>
<path d="M4 20h6"></path>
<path d="M4 20v-6"></path>
<path d="M20 20h-6"></path>
<path d="M20 20v-6"></path>
</svg>
</button>
<button
data-action="zoom-in"
title="放大"
aria-label="放大"
>
<svg viewBox="0 0 24 24">
<path d="M12 5v14"></path>
<path d="M5 12h14"></path>
</svg>
</button>
<span class="siv-toolbar-divider"></span>
<button
data-action="rotate-left"
title="向左旋转"
aria-label="向左旋转"
>
<svg viewBox="0 0 24 24">
<path d="M9 4 5 8l4 4"></path>
<path d="M5 8h7a7 7 0 1 1-6.4 10"></path>
</svg>
</button>
<button
data-action="rotate-right"
title="向右旋转"
aria-label="向右旋转"
>
<svg viewBox="0 0 24 24">
<path d="m15 4 4 4-4 4"></path>
<path d="M19 8h-7a7 7 0 1 0 6.4 10"></path>
</svg>
</button>
<button
data-action="reset"
title="重置"
aria-label="重置"
>
<svg viewBox="0 0 24 24">
<path d="M4 12a8 8 0 1 0 2.3-5.7"></path>
<path d="M4 5v5h5"></path>
</svg>
</button>
<span class="siv-zoom">100%</span>
</div>
<div class="siv-info"></div>
`;
document.documentElement.appendChild(viewer);
viewerImage = viewer.querySelector('.siv-image');
toolbar = viewer.querySelector('.siv-toolbar');
info = viewer.querySelector('.siv-info');
viewer.addEventListener(
'click',
handleViewerClick
);
viewerImage.addEventListener(
'pointerdown',
startDrag
);
viewerImage.addEventListener(
'pointermove',
moveDrag
);
viewerImage.addEventListener(
'pointerup',
endDrag
);
viewerImage.addEventListener(
'pointercancel',
endDrag
);
viewer.addEventListener(
'wheel',
handleWheel,
{ passive: false }
);
viewerImage.addEventListener(
'dblclick',
() => {
if (scale > 1.01) {
fitImage();
} else {
actualSize();
}
}
);
viewerImage.addEventListener(
'dragstart',
e => e.preventDefault()
);
viewer.addEventListener(
'transitionend',
e => {
if (
e.target === viewer &&
!viewer.classList.contains('siv-open')
) {
viewer.style.display = 'none';
}
}
);
}
/*
* CSS
*/
function injectCSS() {
if (document.getElementById('siv-style')) {
return;
}
const style = document.createElement('style');
style.id = 'siv-style';
style.textContent = `
#smart-image-viewer {
position: fixed;
inset: 0;
z-index: 2147483647;
display: none;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
user-select: none;
opacity: 0;
transition:
opacity
0.3s
cubic-bezier(.2,.9,.3,1);
will-change: opacity;
}
#smart-image-viewer.siv-open {
display: block;
opacity: 1;
}
#smart-image-viewer.siv-closing {
opacity: 0;
pointer-events: none;
}
/*
* 背景
*/
.siv-backdrop {
position: absolute;
inset: 0;
background:
rgba(0,0,0,.88);
backdrop-filter:
blur(12px)
saturate(1.4);
-webkit-backdrop-filter:
blur(12px)
saturate(1.4);
transition:
background
.3s;
}
/*
* 图片区域
*/
.siv-stage {
position: absolute;
inset: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
touch-action: none;
}
.siv-image {
position: absolute;
max-width: none;
max-height: none;
width: auto;
height: auto;
object-fit: contain;
transform-origin: center center;
will-change: transform;
cursor: grab;
user-select: none;
-webkit-user-drag: none;
transition:
transform
.25s
cubic-bezier(.2,.8,.2,1);
filter:
drop-shadow(
0 8px 30px
rgba(0,0,0,.3)
);
border-radius: 4px;
}
.siv-image.siv-dragging {
cursor: grabbing;
transition: none;
}
.siv-image.siv-loading-image {
opacity: 0;
transform:
scale(.96);
}
.siv-image.siv-loaded {
opacity: 1;
transform:
scale(1);
transition:
opacity .3s,
transform .3s;
}
/*
* 加载
*/
.siv-loading {
position: absolute;
left: 50%;
top: 50%;
transform:
translate(-50%,-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color:
rgba(255,255,255,.7);
font-size: 14px;
letter-spacing: .3px;
pointer-events: none;
opacity: 0;
transition:
opacity .2s;
}
.siv-loading.active {
opacity: 1;
}
.siv-spinner {
width: 40px;
height: 40px;
border-radius: 50%;
border:
3px solid
rgba(255,255,255,.15);
border-top-color:
#fff;
animation:
siv-spin
.8s
cubic-bezier(.6,0,.4,1)
infinite;
}
@keyframes siv-spin {
to {
transform: rotate(360deg);
}
}
/*
* 顶部
*/
.siv-topbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
background:
linear-gradient(
to bottom,
rgba(0,0,0,.6),
transparent
);
pointer-events: none;
}
.siv-topbar > * {
pointer-events: auto;
}
.siv-title {
color: #fff;
font-size: 14px;
font-weight: 400;
text-shadow:
0 1px 6px
rgba(0,0,0,.5);
letter-spacing: .2px;
}
.siv-counter {
display: inline-flex;
align-items: center;
min-height: 26px;
box-sizing: border-box;
padding:
4px 12px;
border-radius: 20px;
background:
rgba(255,255,255,.12);
backdrop-filter:
blur(4px);
-webkit-backdrop-filter:
blur(4px);
}
/*
* SVG 图标统一样式
*/
.siv-actions button svg,
.siv-toolbar button svg,
.siv-prev svg,
.siv-next svg {
display: block;
width: 18px;
height: 18px;
fill: none;
stroke:
currentColor;
stroke-width:
1.8;
stroke-linecap:
round;
stroke-linejoin:
round;
pointer-events: none;
}
.siv-actions button svg {
width: 19px;
height: 19px;
}
/*
* 顶部操作按钮
*/
.siv-actions {
display: flex;
gap: 8px;
}
.siv-actions button,
.siv-toolbar button {
border: 0;
outline: 0;
color: #fff;
background:
rgba(255,255,255,.10);
backdrop-filter:
blur(6px);
-webkit-backdrop-filter:
blur(6px);
border-radius: 12px;
min-width: 40px;
height: 40px;
padding:
0 12px;
cursor: pointer;
transition:
background .2s,
transform .15s,
box-shadow .2s;
box-shadow:
0 2px 12px
rgba(0,0,0,.1);
display: inline-flex;
align-items: center;
justify-content: center;
}
.siv-actions button:hover,
.siv-toolbar button:hover {
background:
rgba(255,255,255,.22);
transform:
scale(1.05);
box-shadow:
0 4px 16px
rgba(0,0,0,.2);
}
.siv-actions button:active,
.siv-toolbar button:active {
transform:
scale(.94);
}
/*
* 上一张 / 下一张
*/
.siv-prev,
.siv-next {
position: absolute;
top: 50%;
transform:
translateY(-50%);
width: 56px;
height: 80px;
border: 0;
outline: 0;
border-radius: 16px;
background:
rgba(255,255,255,.08);
backdrop-filter:
blur(6px);
-webkit-backdrop-filter:
blur(6px);
color: #fff;
cursor: pointer;
opacity: .5;
transition:
opacity .25s,
background .25s,
transform .2s;
display: flex;
align-items: center;
justify-content: center;
}
.siv-prev:hover,
.siv-next:hover {
opacity: 1;
background:
rgba(255,255,255,.18);
transform:
translateY(-50%)
scale(1.04);
}
.siv-prev {
left: 20px;
}
.siv-next {
right: 20px;
}
.siv-prev svg,
.siv-next svg {
width: 30px;
height: 30px;
stroke-width: 1.7;
}
/*
* 底部工具栏
*/
.siv-toolbar {
position: absolute;
left: 50%;
bottom: 28px;
transform:
translateX(-50%);
display: flex;
align-items: center;
gap: 6px;
padding:
8px 12px;
border-radius: 18px;
background:
rgba(20,20,20,.70);
backdrop-filter:
blur(20px)
saturate(1.6);
-webkit-backdrop-filter:
blur(20px)
saturate(1.6);
box-shadow:
0 12px 40px
rgba(0,0,0,.4);
border:
1px solid
rgba(255,255,255,.06);
flex-wrap: nowrap;
max-width:
calc(100vw - 40px);
overflow-x: auto;
-webkit-overflow-scrolling:
touch;
scrollbar-width: none;
}
.siv-toolbar::-webkit-scrollbar {
display: none;
}
.siv-toolbar button {
min-width: 36px;
height: 36px;
padding: 0 8px;
border-radius: 10px;
background:
rgba(255,255,255,.06);
backdrop-filter: none;
box-shadow: none;
}
.siv-toolbar button:hover {
background:
rgba(255,255,255,.18);
}
.siv-toolbar-divider {
width: 1px;
height: 20px;
margin:
0 3px;
background:
rgba(255,255,255,.12);
flex:
0 0 auto;
}
.siv-zoom {
min-width: 52px;
text-align: center;
color:
rgba(255,255,255,.8);
font-size: 13px;
font-variant-numeric:
tabular-nums;
letter-spacing: .2px;
}
/*
* 图片信息
*/
.siv-info {
position: absolute;
left: 50%;
bottom: 96px;
transform:
translateX(-50%);
color:
rgba(255,255,255,.65);
font-size: 13px;
pointer-events: none;
text-shadow:
0 2px 8px
rgba(0,0,0,.5);
background:
rgba(0,0,0,.25);
padding:
4px 16px;
border-radius: 30px;
backdrop-filter:
blur(4px);
-webkit-backdrop-filter:
blur(4px);
white-space: nowrap;
max-width: 80vw;
overflow: hidden;
text-overflow: ellipsis;
}
/*
* Toast
*/
#siv-toast {
position: fixed;
left: 50%;
top: 50%;
transform:
translate(-50%,-50%);
z-index:
2147483647;
padding:
14px 28px;
border-radius: 16px;
background:
rgba(0,0,0,.78);
backdrop-filter:
blur(10px);
-webkit-backdrop-filter:
blur(10px);
color: #fff;
font-size: 15px;
font-weight: 500;
pointer-events: none;
opacity: 0;
transition:
opacity .25s ease;
box-shadow:
0 12px 40px
rgba(0,0,0,.5);
border:
1px solid
rgba(255,255,255,.06);
}
#siv-toast.visible {
opacity: 1;
}
/*
* 移动端
*/
@media (max-width: 600px) {
.siv-topbar {
height: 56px;
padding: 0 12px;
}
.siv-topbar button {
min-width: 36px;
height: 36px;
}
.siv-prev,
.siv-next {
width: 44px;
height: 60px;
}
.siv-prev {
left: 8px;
}
.siv-next {
right: 8px;
}
.siv-toolbar {
bottom: 16px;
padding:
6px 10px;
gap: 4px;
}
.siv-toolbar button {
min-width: 32px;
height: 32px;
}
.siv-toolbar button svg {
width: 16px;
height: 16px;
}
.siv-info {
bottom: 72px;
font-size: 11px;
padding:
2px 12px;
}
}
`;
document.documentElement.appendChild(style);
}
/*
* 图片变换
*/
function updateTransform() {
if (!viewerImage) {
return;
}
viewerImage.style.transform = `
translate3d(
${translateX}px,
${translateY}px,
0
)
rotate(${rotation}deg)
scale(${scale})
`;
const zoomText =
viewer?.querySelector('.siv-zoom');
if (zoomText) {
zoomText.textContent =
Math.round(scale * 100) + '%';
}
}
function calculateFitScale() {
if (
!viewerImage?.naturalWidth ||
!viewerImage?.naturalHeight
) {
return 1;
}
const stage =
viewer
.querySelector('.siv-stage')
.getBoundingClientRect();
const padding = 50;
const availableWidth =
Math.max(
100,
stage.width - padding * 2
);
const availableHeight =
Math.max(
100,
stage.height - padding * 2
);
const sx =
availableWidth /
viewerImage.naturalWidth;
const sy =
availableHeight /
viewerImage.naturalHeight;
return Math.min(sx, sy, 1);
}
function fitImage() {
scale = calculateFitScale();
rotation = 0;
translateX = 0;
translateY = 0;
updateTransform();
}
function actualSize() {
scale = 1;
rotation = 0;
translateX = 0;
translateY = 0;
updateTransform();
}
/*
* 打开 / 关闭
*/
function openImage(img) {
if (!img || isOpening) {
return;
}
createViewer();
currentSourceImage = img;
currentImages =
collectImages();
currentIndex =
currentImages.indexOf(img);
if (currentIndex < 0) {
currentImages = [img];
currentIndex = 0;
}
viewer.classList.remove(
'siv-closing'
);
viewer.style.display = 'block';
void viewer.offsetWidth;
viewer.classList.add('siv-open');
isOpening = true;
document.documentElement.style.overflow =
'hidden';
document.body?.classList.add(
'siv-image-viewer-open'
);
loadCurrentImage();
const onOpenEnd = () => {
viewer.removeEventListener(
'transitionend',
onOpenEnd
);
isOpening = false;
};
viewer.addEventListener(
'transitionend',
onOpenEnd
);
if (
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
}
function closeViewer() {
if (
!viewer ||
!viewer.classList.contains('siv-open')
) {
return;
}
viewer.classList.add(
'siv-closing'
);
viewer.classList.remove(
'siv-open'
);
document.documentElement.style.overflow =
'';
document.body?.classList.remove(
'siv-image-viewer-open'
);
currentSourceImage = null;
const loading =
viewer.querySelector('.siv-loading');
if (loading) {
loading.classList.remove(
'active'
);
}
viewerImage.style.transition =
'none';
}
/*
* 加载图片
*/
function loadCurrentImage() {
const img =
currentImages[currentIndex];
if (!img) {
return;
}
const url =
getImageUrl(img);
if (!url) {
return;
}
const loading =
viewer.querySelector('.siv-loading');
loading.classList.add('active');
viewerImage.classList.add(
'siv-loading-image'
);
viewerImage.classList.remove(
'siv-loaded'
);
viewerImage.style.visibility =
'hidden';
scale = 1;
rotation = 0;
translateX = 0;
translateY = 0;
viewerImage.onload = () => {
loading.classList.remove(
'active'
);
viewerImage.style.visibility =
'visible';
viewerImage.classList.remove(
'siv-loading-image'
);
viewerImage.classList.add(
'siv-loaded'
);
fitImage();
updateInfo();
setTimeout(() => {
viewerImage.classList.remove(
'siv-loaded'
);
}, 400);
};
viewerImage.onerror = () => {
loading.classList.remove(
'active'
);
viewerImage.style.visibility =
'visible';
viewerImage.classList.remove(
'siv-loading-image'
);
info.textContent =
'图片加载失败';
};
viewerImage.src = url;
updateInfo();
}
/*
* 信息
*/
function updateInfo() {
const count =
currentImages.length;
const img =
currentImages[currentIndex];
const url =
getImageUrl(img);
const filename = (() => {
try {
return decodeURIComponent(
new URL(
url,
location.href
)
.pathname
.split('/')
.pop()
);
} catch (_) {
return '';
}
})();
const counter =
viewer.querySelector(
'.siv-counter'
);
if (counter) {
counter.textContent =
count > 1
? `${currentIndex + 1} / ${count}`
: '';
}
if (info) {
const dimensions =
viewerImage.naturalWidth &&
viewerImage.naturalHeight
? `${viewerImage.naturalWidth} × ${viewerImage.naturalHeight}`
: '';
info.textContent =
`${filename || '图片'}${
dimensions
? ' · ' + dimensions
: ''
}`;
}
const prev =
viewer.querySelector(
'.siv-prev'
);
const next =
viewer.querySelector(
'.siv-next'
);
if (prev) {
prev.style.display =
count > 1
? ''
: 'none';
}
if (next) {
next.style.display =
count > 1
? ''
: 'none';
}
}
/*
* 图片导航
*/
function previousImage() {
if (currentImages.length <= 1) {
return;
}
currentIndex =
(
currentIndex -
1 +
currentImages.length
) %
currentImages.length;
loadCurrentImage();
}
function nextImage() {
if (currentImages.length <= 1) {
return;
}
currentIndex =
(
currentIndex +
1
) %
currentImages.length;
loadCurrentImage();
}
/*
* 缩放
*/
function zoomAt(
factor,
centerX,
centerY
) {
const oldScale =
scale;
const newScale =
Math.max(
CONFIG.minScale,
Math.min(
CONFIG.maxScale,
scale * factor
)
);
if (newScale === oldScale) {
return;
}
const stage =
viewer
.querySelector('.siv-stage')
.getBoundingClientRect();
const x =
centerX -
(
stage.left +
stage.width / 2
);
const y =
centerY -
(
stage.top +
stage.height / 2
);
const ratio =
newScale /
oldScale;
translateX =
x -
(x - translateX) *
ratio;
translateY =
y -
(y - translateY) *
ratio;
scale =
newScale;
updateTransform();
}
function zoomIn() {
zoomAt(
1.2,
window.innerWidth / 2,
window.innerHeight / 2
);
}
function zoomOut() {
zoomAt(
1 / 1.2,
window.innerWidth / 2,
window.innerHeight / 2
);
}
function handleWheel(e) {
if (
!viewer?.classList.contains(
'siv-open'
)
) {
return;
}
e.preventDefault();
const factor =
Math.exp(
-e.deltaY *
CONFIG.wheelZoomSpeed
);
zoomAt(
factor,
e.clientX,
e.clientY
);
}
/*
* 拖拽
*/
function startDrag(e) {
if (e.button !== 0) {
return;
}
dragging = true;
viewerImage.classList.add(
'siv-dragging'
);
dragStartX =
e.clientX;
dragStartY =
e.clientY;
startTranslateX =
translateX;
startTranslateY =
translateY;
viewerImage.setPointerCapture?.(
e.pointerId
);
}
function moveDrag(e) {
if (!dragging) {
return;
}
translateX =
startTranslateX +
(
e.clientX -
dragStartX
);
translateY =
startTranslateY +
(
e.clientY -
dragStartY
);
updateTransform();
}
function endDrag(e) {
dragging = false;
viewerImage.classList.remove(
'siv-dragging'
);
try {
viewerImage.releasePointerCapture?.(
e.pointerId
);
} catch (_) {}
}
/*
* 查看器点击
*/
function handleViewerClick(e) {
const action =
e.target
.closest?.(
'[data-action]'
)
?.dataset.action;
if (!action) {
if (
e.target.classList.contains(
'siv-backdrop'
)
) {
closeViewer();
}
return;
}
switch (action) {
case 'close':
closeViewer();
break;
case 'prev':
previousImage();
break;
case 'next':
nextImage();
break;
case 'zoom-in':
zoomIn();
break;
case 'zoom-out':
zoomOut();
break;
case 'fit':
fitImage();
break;
case 'actual':
actualSize();
break;
case 'reset':
fitImage();
break;
case 'rotate-left':
rotation -= 90;
updateTransform();
break;
case 'rotate-right':
rotation += 90;
updateTransform();
break;
case 'fullscreen':
toggleFullscreen();
break;
case 'download':
if (CONFIG.enableDownload) {
downloadCurrent();
}
break;
case 'copy':
if (CONFIG.enableCopy) {
copyCurrentUrl();
}
break;
}
}
/*
* 全屏
*/
async function toggleFullscreen() {
try {
if (!document.fullscreenElement) {
await viewer.requestFullscreen();
} else {
await document.exitFullscreen();
}
} catch (_) {}
}
/*
* 下载
*/
function downloadCurrent() {
const url =
getImageUrl(
currentImages[currentIndex]
);
if (!url) {
return;
}
const filename = (() => {
try {
return (
decodeURIComponent(
new URL(
url,
location.href
)
.pathname
.split('/')
.pop()
) ||
'image'
);
} catch (_) {
return 'image';
}
})();
if (
typeof GM_download ===
'function'
) {
try {
GM_download({
url,
name: filename,
saveAs: false
});
return;
} catch (_) {}
}
const a =
document.createElement('a');
a.href = url;
a.download = filename;
a.target = '_blank';
document.body?.appendChild(a);
a.click();
a.remove();
}
/*
* 复制地址
*/
async function copyCurrentUrl() {
const url =
getImageUrl(
currentImages[currentIndex]
);
if (!url) {
return;
}
try {
await navigator.clipboard.writeText(
url
);
showToast(
'图片地址已复制'
);
} catch (_) {
const textarea =
document.createElement(
'textarea'
);
textarea.value = url;
textarea.style.position =
'fixed';
textarea.style.opacity =
'0';
document.body?.appendChild(
textarea
);
textarea.select();
try {
document.execCommand(
'copy'
);
} catch (_) {}
textarea.remove();
showToast(
'图片地址已复制'
);
}
}
/*
* Toast
*/
function showToast(text) {
let toast =
document.getElementById(
'siv-toast'
);
if (!toast) {
toast =
document.createElement(
'div'
);
toast.id =
'siv-toast';
(
document.body ||
document.documentElement
).appendChild(toast);
}
toast.textContent = text;
toast.classList.add(
'visible'
);
clearTimeout(
toast._timer
);
toast._timer =
setTimeout(() => {
toast.classList.remove(
'visible'
);
}, 1200);
}
/*
* 普通图片点击
*/
function handleNormalImageClick(e) {
if (!CONFIG.enableNormalClick) {
return;
}
if (
viewer?.classList.contains(
'siv-open'
)
) {
return;
}
const img =
e.target.closest?.('img');
if (!img) {
return;
}
if (shouldIgnore(img)) {
return;
}
const clickable =
getClickableParent(img);
if (clickable) {
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
openImage(img);
}
/*
* 长按
*/
function handlePointerDown(e) {
if (!CONFIG.enableLongPress) {
return;
}
if (e.button !== 0) {
return;
}
const img =
e.target.closest?.('img');
if (!img) {
return;
}
if (shouldIgnore(img)) {
return;
}
const clickable =
getClickableParent(img);
if (!clickable) {
return;
}
if (looksLikeExistingViewer(img)) {
return;
}
pointerDownX =
e.clientX;
pointerDownY =
e.clientY;
longPressTriggered =
false;
clearTimeout(
longPressTimer
);
longPressTimer =
setTimeout(() => {
longPressTriggered =
true;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
openImage(img);
}, CONFIG.longPressTime);
}
function handlePointerMove(e) {
if (!longPressTimer) {
return;
}
const dx =
e.clientX -
pointerDownX;
const dy =
e.clientY -
pointerDownY;
if (
Math.sqrt(
dx * dx +
dy * dy
) >
CONFIG.moveThreshold
) {
clearTimeout(
longPressTimer
);
longPressTimer =
null;
}
}
function handlePointerUp() {
if (longPressTimer) {
clearTimeout(
longPressTimer
);
longPressTimer =
null;
}
}
function handleClickAfterLongPress(e) {
if (!longPressTriggered) {
return;
}
longPressTriggered =
false;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
/*
* 键盘
*/
function handleKeyboard(e) {
if (
!viewer?.classList.contains(
'siv-open'
)
) {
return;
}
switch (e.key) {
case 'Escape':
e.preventDefault();
closeViewer();
break;
case 'ArrowLeft':
e.preventDefault();
previousImage();
break;
case 'ArrowRight':
e.preventDefault();
nextImage();
break;
case '+':
case '=':
e.preventDefault();
zoomIn();
break;
case '-':
case '_':
e.preventDefault();
zoomOut();
break;
case '0':
e.preventDefault();
fitImage();
break;
case '1':
e.preventDefault();
actualSize();
break;
case 'r':
case 'R':
e.preventDefault();
rotation += 90;
updateTransform();
break;
case 'f':
case 'F':
e.preventDefault();
toggleFullscreen();
break;
}
}
/*
* 窗口变化
*/
window.addEventListener(
'resize',
() => {
if (
viewer?.classList.contains(
'siv-open'
)
) {
fitImage();
}
}
);
/*
* 初始化
*/
function init() {
injectCSS();
createViewer();
document.addEventListener(
'click',
handleNormalImageClick,
true
);
document.addEventListener(
'pointerdown',
handlePointerDown,
true
);
document.addEventListener(
'pointermove',
handlePointerMove,
true
);
document.addEventListener(
'pointerup',
handlePointerUp,
true
);
document.addEventListener(
'pointercancel',
handlePointerUp,
true
);
document.addEventListener(
'click',
handleClickAfterLongPress,
true
);
document.addEventListener(
'keydown',
handleKeyboard,
true
);
window.addEventListener(
'popstate',
closeViewer
);
/*
* document-start 时 body 可能还不存在,
* 所以 Toast 放到 html 上即可。
*/
const toast =
document.createElement('div');
toast.id =
'siv-toast';
(
document.body ||
document.documentElement
).appendChild(toast);
}
if (
document.readyState ===
'loading'
) {
if (document.documentElement) {
init();
} else {
document.addEventListener(
'DOMContentLoaded',
init,
{ once: true }
);
}
} else {
init();
}
})();








