您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-preview Google Sheets image tooltips with zoom-to-fit and crosshairs. ESC or click to close. Clean native cursor only. Only triggers on Google Drive file links.
// ==UserScript== // @name Google Sheets Image Zoom // @namespace https://github.com/1LineAtaTime/TamperMonkey-Scripts // @version 3.3 // @description Auto-preview Google Sheets image tooltips with zoom-to-fit and crosshairs. ESC or click to close. Clean native cursor only. Only triggers on Google Drive file links. // @author 1LineAtaTime // @match https://docs.google.com/spreadsheets/* // @grant none // @license GPL-3.0 // ==/UserScript== (function () { 'use strict'; const DEBUG = false; const FILL_PERCENTAGE = 1.0; const SHOW_CROSSHAIRS = true; const DRIVE_FILE_REGEX = /^https?:\/\/drive\.google\.com\/(?:file\/d\/[^/]+\/view|uc\?id=[^&]+)/i; const BUBBLE_WRAPPER_SELECTOR = '.waffle-multilink-tooltip'; const IMAGE_SELECTOR = '.link-bubble-drive-thumbnail-image'; const LINK_SELECTOR = '#docs-linkbubble-link-text'; const IMAGE_ID = 'gs_bubble_preview_img'; const VLINE_ID = 'gs_crosshair_vline'; const HLINE_ID = 'gs_crosshair_hline'; const ZINDEX = 999999; let currentHref = ''; let observer = null; let mainObserver = null; let observerInterval; function log(...args) { if (DEBUG) console.log('[GS-Preview]', ...args); } function hideOverlay() { [IMAGE_ID, VLINE_ID, HLINE_ID].forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); document.removeEventListener('mousemove', globalMouseMoveHandler); log('hide image & crosshairs'); } function buildOverlay(imgSrc) { log('Request to show image:', imgSrc); hideOverlay(); const img = document.createElement('img'); img.id = IMAGE_ID; img.src = imgSrc; img.style.cssText = ` position: fixed; inset: 0; margin: auto; width: auto; height: auto; object-fit: contain; z-index: ${ZINDEX}; pointer-events: auto; background: transparent; cursor: default; box-shadow: 0 0 25px rgba(0,0,0,0.6); transition: opacity 0.2s ease; opacity: 0; `; img.addEventListener('click', hideOverlay); img.onload = () => { const vw = window.innerWidth; const vh = window.innerHeight; const iw = img.naturalWidth; const ih = img.naturalHeight; log('Original image size:', iw + 'x' + ih); log('Viewport:', vw + 'x' + vh); const aspectRatio = iw / ih; const maxW = vw * FILL_PERCENTAGE; const maxH = vh * FILL_PERCENTAGE; let finalW, finalH; if (maxW / maxH < aspectRatio) { finalW = maxW; finalH = finalW / aspectRatio; } else { finalH = maxH; finalW = finalH * aspectRatio; } log('Scaled image size:', Math.round(finalW) + 'x' + Math.round(finalH)); img.style.width = `${finalW}px`; img.style.height = `${finalH}px`; img.style.opacity = '1'; if (SHOW_CROSSHAIRS) setupCrosshairs(); }; document.body.appendChild(img); } function setupCrosshairs() { let vLine = document.getElementById(VLINE_ID); let hLine = document.getElementById(HLINE_ID); if (!vLine) { vLine = document.createElement('div'); vLine.id = VLINE_ID; vLine.style.cssText = ` position: fixed; width: 1px; background: red; height: 0; left: 0; top: 0; z-index: ${ZINDEX + 1}; pointer-events: none; `; document.body.appendChild(vLine); } if (!hLine) { hLine = document.createElement('div'); hLine.id = HLINE_ID; hLine.style.cssText = ` position: fixed; height: 1px; background: red; width: 0; left: 0; top: 0; z-index: ${ZINDEX + 1}; pointer-events: none; `; document.body.appendChild(hLine); } document.addEventListener('mousemove', globalMouseMoveHandler); } function globalMouseMoveHandler(e) { const img = document.getElementById(IMAGE_ID); const vLine = document.getElementById(VLINE_ID); const hLine = document.getElementById(HLINE_ID); if (!img || !vLine || !hLine) return; const rect = img.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { vLine.style.display = 'none'; hLine.style.display = 'none'; return; } vLine.style.display = 'block'; hLine.style.display = 'block'; vLine.style.left = `${x}px`; vLine.style.top = `${rect.top}px`; vLine.style.height = `${rect.height}px`; hLine.style.left = `${rect.left}px`; hLine.style.top = `${y}px`; hLine.style.width = `${rect.width}px`; } function getBubbleLinkHref() { const linkEl = document.querySelector(LINK_SELECTOR); return linkEl ? linkEl.href : null; } function getImageSrcFromBubble() { const imgEl = document.querySelector(IMAGE_SELECTOR); return imgEl ? imgEl.src : null; } function isBubbleVisible() { const wrapper = document.querySelector(BUBBLE_WRAPPER_SELECTOR); return wrapper && wrapper.offsetParent !== null && wrapper.style.display !== 'none'; } function observeBubbleWrapperWhenAvailable() { if (mainObserver) mainObserver.disconnect(); mainObserver = new MutationObserver(() => { const wrapper = document.querySelector(BUBBLE_WRAPPER_SELECTOR); if (wrapper) { log('Bubble wrapper found — starting inner observer'); startBubbleObserver(wrapper); mainObserver.disconnect(); } }); mainObserver.observe(document.body, { childList: true, subtree: true }); log('Waiting for bubble wrapper to appear...'); } function startBubbleObserver(wrapper) { if (observer) observer.disconnect(); observer = new MutationObserver(() => { if (!isBubbleVisible()) { hideOverlay(); currentHref = ''; return; } const newHref = getBubbleLinkHref(); if (!newHref || newHref === currentHref) return; if (!DRIVE_FILE_REGEX.test(newHref)) { log('Skipping non-Google Drive file link:', newHref); return; } const imgSrc = getImageSrcFromBubble(); if (imgSrc) { currentHref = newHref; buildOverlay(imgSrc); } }); observer.observe(wrapper, { attributes: true, childList: true, subtree: true }); log('Started observing for Sheets bubbles'); } function ensureObserverAlive() { if (!observer || observer.takeRecords().length === 0) { log('Observer heartbeat: refreshing...'); observeBubbleWrapperWhenAvailable(); } } document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideOverlay(); }); window.addEventListener('load', () => { log('Page loaded – watching for bubble wrapper dynamically'); observeBubbleWrapperWhenAvailable(); observerInterval = setInterval(ensureObserverAlive, 3000); }); })();