// ==UserScript== // @name Yipeek // @name:zh-CN 一瞥 // @namespace https://github.com/Chumor/Yipeek // @version 1.4.0 // @description "指尖轻触,万象凝于一瞥。A tap, a glimpse — the world in focus." // @author Chumor // @match *://*/* // @grant GM_getResourceText // @run-at document-end // @resource filteringRules https://raw.githubusercontent.com/Chumor/Yipeek/dev/filtering-rules.json // @icon https://cdn.jsdelivr.net/gh/Chumor/Yipeek@dev/assets/google-photos-128.png // @homepage https://github.com/Chumor/Yipeek // @supportURL https://github.com/Chumor/Yipeek/issues // ==/UserScript== (function() { 'use strict'; // 调试与版本信息 const DEBUG_MODE = false; // 是否开启调试模式(true = 开启) const VERSION = typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown'; // 加载外部 JSON 过滤规则 let filteringRules = []; try { const rulesText = GM_getResourceText('filteringRules'); // 读取外部 JSON filteringRules = JSON.parse(rulesText || '{}'); // DEBUG 模式下打印规则对象 if (DEBUG_MODE) console.log('[Yipeek] 过滤规则对象 →', filteringRules); // 输出规则信息 const metaVersion = filteringRules._meta?.version || 'unknown'; const parentClassesCount = filteringRules.ignoreParentClasses?.length || 0; const parentClassesRegexCount = filteringRules.ignoreParentClassesRegex?.length || 0; const linkPatternsCount = filteringRules.ignoreLinkPatterns?.length || 0; const dataAttrCount = filteringRules.ignoreDataAttributes?.length || 0; console.log( `[Yipeek] 过滤规则加载成功 · v${metaVersion} -> ` + `ignoreParentClasses: ${parentClassesCount}, ` + `ignoreParentClassesRegex: ${parentClassesRegexCount}, ` + `ignoreLinkPatterns: ${linkPatternsCount}, ` + `ignoreDataAttributes: ${dataAttrCount}, ` + `ignoreOnClick: ${filteringRules.ignoreOnClick ? 1 : 0}` ); } catch (e) { console.warn('过滤规则加载失败', e); } // 过滤规则应用函数 function applyFilteringRules(img) { if (!img || !filteringRules) return; // 忽略小图片 if (img.naturalWidth < filteringRules.minWidth || img.naturalHeight < filteringRules.minHeight) { img.style.display = 'none'; return; } // 忽略父元素 class let parent = img.parentElement; while (parent) { if (filteringRules.ignoreParentClasses?.some(cls => parent.classList.contains(cls))) { return; } if (filteringRules.ignoreParentClassesRegex?.some(rx => new RegExp(rx).test(parent.className))) { return; } parent = parent.parentElement; } // 忽略链接属性 const link = img.closest('a'); if (link) { if (filteringRules.ignoreLinkPatterns?.some(p => new RegExp(p).test(link.href))) return; if (filteringRules.ignoreLinkAttribute && link.hasAttribute(filteringRules.ignoreLinkAttribute)) return; } // 忽略 data 属性 if (filteringRules.ignoreDataAttributes?.some(attr => img.hasAttribute(attr))) return; // 忽略 onclick if (filteringRules.ignoreOnClick && (img.onclick || img.parentElement?.onclick)) return; } 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 lastX = 0; let lastY = 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 imageNaturalWidth = 0; let imageNaturalHeight = 0; let bodyOverflow = ''; let bodyPointerEvents = ''; let gesturesInited = false; let lastFocusX = 0; let lastFocusY = 0; let rafPending = false; 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; transition: opacity 0.32s cubic-bezier(0.22,0.61,0.36,1), transform 0.32s cubic-bezier(0.22,0.61,0.36,1); `; previewImage = document.createElement('img'); previewImage.style.cssText = ` max-width: 95%; max-height: 90%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.96); transition: transform 0.32s cubic-bezier(0.22,0.61,0.36,1), opacity 0.32s ease; user-select: none; pointer-events: auto; opacity: 0; will-change: transform; `; 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 closeBtn = document.createElement('div'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` position: fixed; top: 16px; right: 16px; width: 36px; height: 36px; 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; z-index: 2147483647; pointer-events: auto; transform: translateZ(0); transition: all 0.2s ease; opacity: 0.9; `; closeBtn.addEventListener('click', e => { e.stopPropagation(); closePreview(); }); closeBtn.addEventListener('touchstart', e => { e.stopPropagation(); closePreview(); }); previewContainer.appendChild(previewImage); previewContainer.appendChild(imageInfoElement); previewContainer.appendChild(zoomIndicator); previewContainer.appendChild(closeBtn); document.body.appendChild(previewContainer); previewContainer.addEventListener('click', e => { if (e.target === previewContainer) closePreview(); }); previewContainer.addEventListener('touchmove', e => { if (isDragging) e.preventDefault(); }, { passive: false }); updateContainerSize(); window.addEventListener('resize', updateContainerSize); } function updateContainerSize() { containerWidth = Math.max(1, Math.round(window.innerWidth * 0.95)); containerHeight = Math.max(1, Math.round(window.innerHeight * 0.85)); } 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; lastFocusX = containerWidth / 2; lastFocusY = containerHeight / 2; applyTransform(true); zoomIndicator.style.opacity = '0'; } // 应用预览图片的平移与缩放,并更新缩放指示器和边界 function applyTransform(immediate) { if (!previewImage) return; const imgWidth = imageNaturalWidth * currentScale; const imgHeight = imageNaturalHeight * 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)); const tx = `calc(-50% + ${currentX}px)`; const ty = `calc(-50% + ${currentY}px)`; const t = `translate(${tx}, ${ty}) scale(${currentScale})`; if (immediate || isDragging) { previewImage.style.transition = 'transform 0s'; previewImage.style.transform = t; } else { previewImage.style.transition = 'transform 0.32s cubic-bezier(0.22,0.61,0.36,1)'; previewImage.style.transform = t; } if (currentScale !== 1) { zoomIndicator.textContent = `${Math.round(currentScale * 100)}%`; zoomIndicator.style.opacity = '0.9'; } else { zoomIndicator.style.opacity = '0'; } } function updateBoundary() { if (!previewImage) return; const imgWidth = imageNaturalWidth * currentScale; const imgHeight = imageNaturalHeight * currentScale; const maxX = (imgWidth - containerWidth) / 2; const maxY = (imgHeight - containerHeight) / 2; currentX = Math.max(-maxX, Math.min(maxX, currentX)); currentY = Math.max(-maxY, Math.min(maxY, currentY)); } function getImageTitle(img) { if (!img) return '图片'; let title = img.alt || img.title || ''; if (!title && img.src) { const filename = img.src.split('/').pop() || ''; title = filename.replace(/\.[^/.]+$/, '').split('?')[0].split('&')[0].replace(/(^[\d-]+_|[\d-]+$)/g, '').trim() || '图片'; } if (title.length > 25) title = title.substring(0, 22) + '...'; return title; } function updateImageInfo() { if (!imageInfoElement || !imageList[currentIndex]) return; try { const img = imageList[currentIndex]; const title = getImageTitle(img); imageInfoElement.textContent = `${title} ${currentIndex+1}/${imageList.length}`; } catch (e) { imageInfoElement.textContent = `${currentIndex+1}/${imageList.length}`; } imageInfoElement.style.opacity = '0.9'; } 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; } targetScale = Math.min(targetScale, 1.0); targetScale = Math.max(targetScale, 0.6); currentScale = targetScale; currentX = 0; currentY = 0; lastFocusX = containerWidth / 2; lastFocusY = containerHeight / 2; applyTransform(true); } // 初始化触摸手势与点击手势 function initGestures() { // 手势初始化:如果 previewImage 尚未创建,延迟执行 if (!previewImage) { console.warn('previewImage not ready, delaying gesture init'); setTimeout(initGestures, 50); return; } // 如果手势已初始化,则跳过 if (gesturesInited) return; gesturesInited = true; // 点双击手势处理 previewImage.addEventListener('click', e => { e.stopPropagation(); const now = Date.now(); const DOUBLE_TAP_DELAY = 300; lastFocusX = e.clientX; lastFocusY = e.clientY; if (now - lastTap < DOUBLE_TAP_DELAY) { // 双击 if (currentScale !== 1) { currentScale = 1; currentX = 0; currentY = 0; } else { currentScale = 2; } applyTransform(true); updateImageInfo(); } else { // 单击 if (currentScale !== 1) { resetTransform(); updateImageInfo(); } } lastTap = now; }, { passive: false }); // 单双指 touchstart 手势 previewImage.addEventListener('touchstart', e => { if (!previewImage) return; if (e.touches.length === 1) { isDragging = true; startDragX = e.touches[0].clientX - currentX; startDragY = e.touches[0].clientY - currentY; lastFocusX = e.touches[0].clientX; lastFocusY = e.touches[0].clientY; previewImage.style.transition = 'transform 0s'; } 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); lastFocusX = (e.touches[0].clientX + e.touches[1].clientX) / 2; lastFocusY = (e.touches[0].clientY + e.touches[1].clientY) / 2; previewImage.style.transition = 'transform 0s'; } }, { passive: false }); // touchmove 手势处理 previewImage.addEventListener('touchmove', e => { if (!previewImage) return; if (e.touches.length === 1 && isDragging) { lastX = currentX; lastY = currentY; // 阻尼拖动(边缘缓冲) const dx = e.touches[0].clientX - startDragX; const dy = e.touches[0].clientY - startDragY; currentX = dx; currentY = dy; lastFocusX = e.touches[0].clientX; lastFocusY = e.touches[0].clientY; applyTransform(true); 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 = Math.max(0.5, Math.min(currentScale * scaleChange, 4.5)); lastFocusX = (e.touches[0].clientX + e.touches[1].clientX) / 2; lastFocusY = (e.touches[0].clientY + e.touches[1].clientY) / 2; applyTransform(true); e.preventDefault(); } lastTouchDistance = distance; e.stopPropagation(); } else if (e.touches.length > 2) { // 多指触控异常处理,重置拖动和缩放状态 isDragging = false; lastTouchDistance = 0; previewImage.style.transition = 'transform 0.32s cubic-bezier(0.22,0.61,0.36,1)'; e.preventDefault(); e.stopPropagation(); } }, { passive: false }); // touchend(阻尼惯性反馈) previewImage.addEventListener('touchend', e => { isDragging = false; if (e.touches && e.touches.length < 2) lastTouchDistance = 0; // 阻尼惯性速度 let vx = (currentX - lastX) * 0.3; let vy = (currentY - lastY) * 0.3; const friction = 0.9; // 惯性阻尼 const bounceFactor = 0.8; // 回弹阈值 function animateInertia() { vx *= friction; vy *= friction; currentX += vx; currentY += vy; // 越界时回弹 const imgWidth = imageNaturalWidth * currentScale; const imgHeight = imageNaturalHeight * currentScale; const maxX = Math.max(0, (imgWidth - containerWidth) / 2); const maxY = Math.max(0, (imgHeight - containerHeight) / 2); if (currentX > maxX || currentX < -maxX) vx *= -bounceFactor; if (currentY > maxY || currentY < -maxY) vy *= -bounceFactor; updateBoundary(); applyTransform(false); if (Math.abs(vx) > 0.8 || Math.abs(vy) > 0.8) { requestAnimationFrame(animateInertia); } else { previewImage.style.transition = 'transform 0.32s cubic-bezier(0.22,0.61,0.36,1)'; } } requestAnimationFrame(animateInertia); // 恢复平滑过渡 previewImage.style.transition = 'transform 0.32s cubic-bezier(0.22,0.61,0.36,1)'; }, { passive: true }); // touchcancel previewImage.addEventListener('touchcancel', () => { isDragging = false; lastTouchDistance = 0; previewImage.style.transition = 'transform 0.32s cubic-bezier(0.22,0.61,0.36,1)'; updateBoundary(); }); } function previewImageFn(imgElement) { if (!imgElement || !imgElement.src || isPreviewMode) return; createPreviewContainer(); isPreviewMode = true; bodyOverflow = document.body.style.overflow || ''; bodyPointerEvents = document.body.style.pointerEvents || ''; document.body.style.overflow = 'hidden'; document.body.style.pointerEvents = 'none'; initImageHandlers(); currentIndex = imageList.indexOf(imgElement); if (currentIndex < 0) { imageList = [imgElement]; currentIndex = 0; } updateImageInfo(); resetTransform(); 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;'; previewContainer.appendChild(loading); previewImage.onload = () => { imageNaturalWidth = previewImage.naturalWidth; imageNaturalHeight = previewImage.naturalHeight; loading.remove(); setOptimalInitialSize(); initGestures(); requestAnimationFrame(() => { previewImage.style.opacity = '1'; previewImage.style.transform = `translate(-50%, -50%) scale(${currentScale})`; previewContainer.style.opacity = '1'; previewContainer.style.transform = 'scale(1)'; }); }; previewImage.onerror = (e) => { loading.textContent = '加载失败'; loading.style.backgroundColor = 'rgba(200,0,0,0.7)'; }; const rawSrc = normalizeImageUrl(imgElement.src); previewImage.src = rawSrc; previewContainer.style.display = 'flex'; previewContainer.style.opacity = '0'; previewContainer.style.transform = 'scale(0.96)'; previewImage.style.opacity = '0'; previewImage.style.transform = 'translate(-50%,-50%) scale(0.96)'; } function initImageHandlers() { const allImages = document.querySelectorAll('img'); imageList = Array.from(allImages); imageList.forEach(img => { if (img.dataset.yipeekBound) return; img.dataset.yipeekBound = 'true'; // 如果图片匹配忽略规则,则不绑定预览 if (shouldIgnoreImage(img)) return; img.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); if (!isPreviewMode) previewImageFn(img); }, { passive: false }); }); } function shouldIgnoreImage(img) { if (!img || !filteringRules) return false; // 忽略小图片 if (img.naturalWidth < filteringRules.minWidth || img.naturalHeight < filteringRules.minHeight) { return true; } // 忽略父元素 class let parent = img.parentElement; while (parent) { if (filteringRules.ignoreParentClasses?.some(cls => parent.classList.contains(cls))) return true; if (filteringRules.ignoreParentClassesRegex?.some(rx => new RegExp(rx).test(parent.className))) return true; parent = parent.parentElement; } // 忽略链接属性 const link = img.closest('a'); if (link) { if (filteringRules.ignoreLinkPatterns?.some(p => new RegExp(p).test(link.href))) return true; if (filteringRules.ignoreLinkAttribute && link.hasAttribute(filteringRules.ignoreLinkAttribute)) return true; } // 忽略 data 属性 if (filteringRules.ignoreDataAttributes?.some(attr => img.hasAttribute(attr))) return true; // 忽略 onclick if (filteringRules.ignoreOnClick && (img.onclick || img.parentElement?.onclick)) return true; return false; } const observer = new MutationObserver(() => { if (!isPreviewMode) initImageHandlers(); }); observer.observe(document.body, { childList: true, subtree: true }); // 关闭图片预览并重置状态 function closePreview() { if (!previewContainer) return; previewContainer.style.opacity = '0'; previewContainer.style.transform = 'scale(0.96)'; if (previewImage) { previewImage.style.opacity = '0'; previewImage.style.transform = 'translate(-50%,-50%) scale(0.96)'; } setTimeout(() => { previewContainer.style.display = 'none'; document.body.style.overflow = bodyOverflow; document.body.style.pointerEvents = bodyPointerEvents; isPreviewMode = false; gesturesInited = false; currentScale = 1; currentX = 0; currentY = 0; isDragging = false; imageNaturalWidth = 0; imageNaturalHeight = 0; }, 320); } function init() { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initImageHandlers); else initImageHandlers(); createPreviewContainer(); previewContainer.style.display = 'none'; } init(); console.log(`[Yipeek] 一瞥 v${VERSION} - 指尖轻触,万象凝于一瞥`); })();