Greasy Fork is available in English.
"指尖轻触,万象凝于一瞥。A tap, a glimpse — the world in focus."
// ==UserScript==
// @name Yipeek
// @name:zh-CN 一瞥
// @namespace https://github.com/Chumor/Yipeek
// @version 1.2.0
// @description "指尖轻触,万象凝于一瞥。A tap, a glimpse — the world in focus."
// @author Chumor
// @match *://*/*
// @grant none
// @run-at document-end
// @homepage https://github.com/Chumor/Yipeek
// @supportURL https://github.com/Chumor/Yipeek/issues
// ==/UserScript==
(function() {
'use strict';
const DEBUG_MODE = false;
const VERSION = typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown';
let isPreviewMode = false;
let previewContainer = null;
let previewImage = null;
let imageList = [];
let currentIndex = 0;
let lastTap = 0;
let currentScale = 1;
let currentX = 0;
let currentY = 0;
let isDragging = false;
let startDragX = 0;
let startDragY = 0;
let lastTouchDistance = 0;
let imageInfoElement = null;
let zoomIndicator = null;
let containerWidth = 0;
let containerHeight = 0;
let originalImageWidth = 0;
let originalImageHeight = 0;
let imageNaturalWidth = 0;
let imageNaturalHeight = 0;
let bodyOverflow = '';
let bodyPointerEvents = '';
// 创建预览容器
function createPreviewContainer() {
if (previewContainer) return;
previewContainer = document.createElement('div');
previewContainer.id = 'image-preview-container';
previewContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
z-index: 999999;
display: none;
justify-content: center;
align-items: center;
overflow: hidden;
touch-action: none;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
pointer-events: auto;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
`;
// 关闭按钮容器
const closeButtonContainer = document.createElement('div');
closeButtonContainer.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
z-index: 2147483647;
pointer-events: auto;
transform: translateZ(0);
`;
// 关闭按钮
const closeButton = document.createElement('div');
closeButton.innerHTML = '×';
closeButton.style.cssText = `
position: relative;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(30, 30, 30, 0.8);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
cursor: pointer;
opacity: 0.9;
transition: all 0.2s ease;
`;
// 触摸反馈
closeButton.addEventListener('touchstart', function() {
this.style.opacity = '1';
});
closeButton.addEventListener('touchend', function() {
this.style.opacity = '0.9';
});
// 关闭按钮点击
closeButton.addEventListener('touchstart', function(e) {
e.stopPropagation();
e.preventDefault();
closePreview();
});
closeButton.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
closePreview();
});
closeButtonContainer.appendChild(closeButton);
previewContainer.appendChild(closeButtonContainer);
// 图片容器
const imageContainer = document.createElement('div');
imageContainer.style.cssText = `
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: auto;
`;
// 图片信息
imageInfoElement = document.createElement('div');
imageInfoElement.id = 'yipeek-image-info';
imageInfoElement.style.cssText = `
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
color: white;
background: rgba(0, 0, 0, 0.6);
padding: 6px 12px;
border-radius: 16px;
font-size: 13px;
z-index: 1000;
text-align: center;
opacity: 0.9;
pointer-events: none;
max-width: 90%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: opacity 0.3s ease;
`;
// 缩放指示器
zoomIndicator = document.createElement('div');
zoomIndicator.style.cssText = `
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
background: rgba(0, 0, 0, 0.6);
padding: 6px 12px;
border-radius: 16px;
font-size: 13px;
z-index: 1000;
text-align: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
`;
// 加载指示器
const loading = document.createElement('div');
loading.id = 'image-preview-loading';
loading.textContent = '加载中...';
loading.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
padding: 10px 20px;
background: rgba(0,0,0,0.5);
border-radius: 12px;
z-index: 1000;
pointer-events: none;
`;
previewImage = document.createElement('img');
previewImage.style.cssText = `
max-width: 95%;
max-height: 90%;
object-fit: contain;
user-select: none;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: transform 0.2s ease;
pointer-events: auto;
`;
imageContainer.appendChild(loading);
imageContainer.appendChild(previewImage);
previewContainer.appendChild(imageContainer);
previewContainer.appendChild(imageInfoElement);
previewContainer.appendChild(zoomIndicator);
document.body.appendChild(previewContainer);
// 点击背景关闭预览
previewContainer.addEventListener('click', function(e) {
if (e.target === previewContainer) {
closePreview();
}
});
// 阻止滚动
previewContainer.addEventListener('touchmove', function(e) {
if (isDragging) {
e.preventDefault();
}
}, {
passive: false
});
// 更新容器尺寸
updateContainerSize();
// 监听窗口大小变化
window.addEventListener('resize', updateContainerSize);
}
// 更新容器尺寸
function updateContainerSize() {
if (!previewContainer) return;
// 图片信息区域预留空间
const infoHeight = 40;
containerWidth = window.innerWidth * 0.95;
containerHeight = window.innerHeight * 0.85;
// 屏幕适配
if (containerWidth > window.innerWidth) containerWidth = window.innerWidth;
if (containerHeight > window.innerHeight) containerHeight = window.innerHeight;
}
// GtHub 适配
function normalizeImageUrl(url) {
if (!url) return url;
if (url.includes('github.com') && url.includes('/blob/')) {
return url
.replace('github.com', 'raw.githubusercontent.com')
.replace('/blob/', '/');
}
return url;
}
// 重置变换
function resetTransform() {
currentScale = 1;
currentX = 0;
currentY = 0;
if (previewImage) {
previewImage.style.transform = `translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px)) scale(${currentScale})`;
}
zoomIndicator.style.opacity = '0';
}
// 应用变换
function applyTransform() {
if (previewImage) {
previewImage.style.transform = `translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px)) scale(${currentScale})`;
}
// 显示缩放比例
if (currentScale !== 1) {
zoomIndicator.textContent = `${Math.round(currentScale * 100)}%`;
zoomIndicator.style.opacity = '0.9';
} else {
zoomIndicator.style.opacity = '0';
}
// 更新边界
updateBoundary();
}
// 更新边界
function updateBoundary() {
if (!previewImage) return;
const imgRect = previewImage.getBoundingClientRect();
const imgWidth = imgRect.width * currentScale;
const imgHeight = imgRect.height * currentScale;
// 计算最大可拖动范围
const maxX = Math.max(0, (imgWidth - containerWidth) / 2);
const maxY = Math.max(0, (imgHeight - containerHeight) / 2);
// 限制拖动范围
currentX = Math.max(-maxX, Math.min(maxX, currentX));
currentY = Math.max(-maxY, Math.min(maxY, currentY));
if (previewImage) {
previewImage.style.transform = `translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px)) scale(${currentScale})`;
}
}
// 获取图片标题
function getImageTitle(img) {
if (!img) return '图片';
let title = img.alt || img.title || '';
// 从src提取文件名
if (!title && img.src) {
const filename = img.src.split('/').pop() || '';
title = filename.replace(/\.[^/.]+$/, '');
// 移除参数
title = title.split('?')[0];
title = title.split('&')[0];
// 简化常见文件名
title = title.replace(/(^[\d-]+_|[\d-]+$)/g, '').trim() || '图片';
}
// 限制长度
if (title.length > 25) {
title = title.substring(0, 22) + '...';
}
return title;
}
// 更新图片信息
function updateImageInfo() {
if (!imageInfoElement) {
imageInfoElement = document.getElementById('yipeek-image-info');
if (!imageInfoElement) return;
}
if (currentIndex < 0 || currentIndex >= imageList.length) {
imageInfoElement.textContent = '';
imageInfoElement.style.opacity = '0';
return;
}
try {
const img = imageList[currentIndex];
const title = getImageTitle(img);
const currentNum = currentIndex + 1;
const totalNum = imageList.length;
imageInfoElement.textContent = `${title} ${currentNum}/${totalNum}`;
imageInfoElement.style.opacity = '0.9';
} catch (error) {
console.error('Error updating image info:', error);
if (imageInfoElement) {
imageInfoElement.textContent = `${currentIndex + 1}/${imageList.length}`;
imageInfoElement.style.opacity = '0.9';
}
}
}
// 预览图片
function previewImageFn(imgElement) {
if (!imgElement || !imgElement.src || isPreviewMode) return;
isPreviewMode = true;
// 确保预览容器已创建
createPreviewContainer();
// 保存body的原始状态
bodyOverflow = document.body.style.overflow || '';
bodyPointerEvents = document.body.style.pointerEvents || '';
// 阻止body滚动和交互
document.body.style.overflow = 'hidden';
document.body.style.pointerEvents = 'none';
// 收集图片
initImageHandlers();
// 更新当前索引
currentIndex = imageList.indexOf(imgElement);
if (currentIndex === -1) {
currentIndex = 0;
imageList = [imgElement];
}
// 更新图片信息
updateImageInfo();
// 显示加载中
const loading = document.getElementById('image-preview-loading');
if (loading) loading.style.display = 'block';
// 重置状态
resetTransform();
// 清除旧图片源
previewImage.src = '';
// 加载图片
previewImage.onload = function() {
// 保存原始尺寸
originalImageWidth = previewImage.naturalWidth;
originalImageHeight = previewImage.naturalHeight;
imageNaturalWidth = originalImageWidth;
imageNaturalHeight = originalImageHeight;
// 隐藏加载中
if (loading) loading.style.display = 'none';
// 设置初始尺寸
setOptimalInitialSize();
// 初始化手势
initGestures();
};
previewImage.onerror = function(e) {
console.error('Image load error:', e);
if (loading) {
loading.textContent = '加载失败';
loading.style.backgroundColor = 'rgba(200,0,0,0.7)';
}
};
// 加载新图片
const rawSrc = normalizeImageUrl(imgElement.src);
previewImage.src = rawSrc;
// 显示预览
if (previewContainer) {
previewContainer.style.display = 'flex';
}
}
// 初始尺寸配置
function setOptimalInitialSize() {
if (!previewImage || imageNaturalWidth <= 0 || imageNaturalHeight <= 0) return;
// 计算图片的宽高比
const imageRatio = imageNaturalWidth / imageNaturalHeight;
const containerRatio = containerWidth / containerHeight;
let targetScale = 1;
// 计算最佳缩放比例
if (imageRatio > containerRatio) {
// 横向图片按宽度缩放,考虑信息区。
targetScale = containerWidth / imageNaturalWidth;
// 高度过小时适当放大
const heightRatio = (containerHeight * 0.9) / imageNaturalHeight;
if (heightRatio > targetScale) {
targetScale = heightRatio;
}
} else {
// 纵向图片,以高度为基准
targetScale = containerHeight / imageNaturalHeight;
}
// 限制缩放范围
const maxScale = 1.0; // 初始不放大
const minScale = 0.6; // 最小缩放比例
targetScale = Math.min(targetScale, maxScale);
targetScale = Math.max(targetScale, minScale);
// 应用缩放
currentScale = targetScale;
currentX = 0;
currentY = 0;
// 应用变换
applyTransform();
}
// 初始化手势
function initGestures() {
if (!previewImage) return;
// 双击放大/缩小
previewImage.addEventListener('click', function(e) {
e.stopPropagation();
const now = Date.now();
const DOUBLE_TAP_DELAY = 300;
if (now - lastTap < DOUBLE_TAP_DELAY) {
// 双击重置
if (currentScale !== 1) {
resetTransform();
} else {
// 双击放大到2倍
currentScale = 2;
applyTransform();
}
// 更新信息
updateImageInfo();
} else {
// 单击:放大时重置
if (currentScale !== 1) {
resetTransform();
// 更新信息
updateImageInfo();
}
}
lastTap = now;
});
// 拖动和捏合缩放
previewImage.addEventListener('touchstart', function(e) {
if (e.touches.length === 1 && currentScale > 1) {
isDragging = true;
startDragX = e.touches[0].clientX - currentX;
startDragY = e.touches[0].clientY - currentY;
e.stopPropagation();
} else if (e.touches.length === 2) {
// 记录初始距离
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastTouchDistance = Math.sqrt(dx * dx + dy * dy);
e.stopPropagation();
}
}, {
passive: false
});
previewImage.addEventListener('touchmove', function(e) {
if (isDragging && e.touches.length === 1) {
currentX = e.touches[0].clientX - startDragX;
currentY = e.touches[0].clientY - startDragY;
applyTransform();
e.preventDefault();
e.stopPropagation();
} else if (e.touches.length === 2) {
// 捏合缩放
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (lastTouchDistance > 0) {
const scaleChange = distance / lastTouchDistance;
currentScale *= scaleChange;
// 限制缩放范围
currentScale = Math.max(0.5, Math.min(currentScale, 4));
applyTransform();
e.preventDefault();
}
lastTouchDistance = distance;
e.stopPropagation();
}
}, {
passive: false
});
previewImage.addEventListener('touchend', function(e) {
if (isDragging) {
isDragging = false;
}
if (e.touches.length < 2) {
lastTouchDistance = 0;
}
updateBoundary();
}, {
passive: true
});
previewImage.addEventListener('touchcancel', function() {
isDragging = false;
lastTouchDistance = 0;
updateBoundary();
});
}
// 关闭预览
function closePreview() {
if (previewContainer) {
previewContainer.style.display = 'none';
}
// 恢复body的原始状态
document.body.style.overflow = bodyOverflow;
document.body.style.pointerEvents = bodyPointerEvents;
isPreviewMode = false;
currentScale = 1;
currentX = 0;
currentY = 0;
isDragging = false;
originalImageWidth = 0;
originalImageHeight = 0;
imageNaturalWidth = 0;
imageNaturalHeight = 0;
}
// 为所有图片添加点击事件
function initImageHandlers() {
// 收集所有可点击的图片
const allImages = document.querySelectorAll('img');
// 过滤可预览图片
imageList = Array.from(allImages).filter(img => {
const rect = img.getBoundingClientRect();
if (rect.width <= 48 && rect.height <= 48) return false;
const parent = img.parentElement;
if (parent) {
const cls = parent.className || '';
// 无条件忽略
if (/logo|kmlogo/i.test(cls)) return false;
// 规则忽略
const isOtherUI = /to-|goto-|go-|jump-|nav|menu|btn|icon|header|footer|aside|navbar|avatar|ad|banner|sponsor|watermark|placeholder|skeleton/i.test(cls);
if (isOtherUI && !img.alt && !img.title) return false;
}
const parentLink = img.closest('a');
if (
(parentLink && parentLink.hasAttribute('data-preview-ignore')) ||
img.closest('[data-yipeek-ignore]')
) return false;
return true;
});
imageList.forEach(img => {
if (img.dataset.yipeekBound) return;
img.addEventListener('click', function(e) {
// 预览功能兜底
e.preventDefault();
e.stopPropagation();
const parentLink = img.closest('a');
if (parentLink) {
parentLink.addEventListener('click', function(e2) {
e2.preventDefault();
e2.stopPropagation();
}, {
once: true,
passive: false
});
}
if (!isPreviewMode) {
previewImageFn(img);
}
}, {
passive: false
});
img.dataset.yipeekBound = 'true';
});
}
// 监听DOM变化
const observer = new MutationObserver(() => {
if (!isPreviewMode) {
initImageHandlers();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 初始化
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initImageHandlers);
} else {
initImageHandlers();
}
// 首次预览时创建预览容器
createPreviewContainer();
previewContainer.style.display = 'none';
}
// 启动
init();
console.log(`Yipeek 一瞥 v${VERSION} - 指尖轻触,万象凝于一瞥`);
})();