您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Image magnifier for Twitter/X. Hold hotkey Ctrl+Alt to activate, press any key to disable. Hover over images to view them with magnifying glass. Features adjustable size, zoom level, and scroll wheel zoom control.
// ==UserScript== // @name Twitter Image Magnifying Glass // @namespace http://tampermonkey.net/ // @version 2 // @description Image magnifier for Twitter/X. Hold hotkey Ctrl+Alt to activate, press any key to disable. Hover over images to view them with magnifying glass. Features adjustable size, zoom level, and scroll wheel zoom control. // @author You // @match https://twitter.com/* // @match https://x.com/* // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48Y2lyY2xlIGN4PSIxMSIgY3k9IjExIiByPSI4Ii8+PHBhdGggZD0ibTIxIDIxLTQuMzUtNC4zNSIvPjwvc3ZnPg== // @license MIT // @run-at document-end // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // State variables let magnifier = null; let currentImage = null; let magnifierActive = false; // Load saved settings let savedHotkey = GM_getValue('magnifier_hotkey', 'ctrl+alt'); let savedSize = GM_getValue('magnifier_size', 200); let savedZoom = GM_getValue('magnifier_zoom', 3); // Utility functions const createSliderModal = (title, icon, currentValue, unit, min, max, step, tickStep, majorTickStep) => { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 999999; display: flex; align-items: center; justify-content: center; font-family: Arial, sans-serif; `; overlay.innerHTML = ` <div style="background: white; padding: 30px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); max-width: 400px; width: 90%;"> <h3 style="margin: 0 0 20px 0; text-align: center; color: #333;">${icon} ${title}</h3> <div style="margin-bottom: 20px;"> <label style="display: block; margin-bottom: 10px; color: #555; font-weight: bold;"> ${title.split(' ')[1]}: <span id="valueDisplay">${currentValue}</span>${unit} </label> <div style="position: relative;"> <input type="range" id="slider" min="${min}" max="${max}" step="${step}" value="${currentValue}" style="width: 100%; height: 8px; border-radius: 5px; background: #ddd; outline: none;"> <div id="ticks" style="position: relative; height: 20px; margin-top: 5px;"></div> </div> </div> <div style="margin-bottom: 20px; font-size: 12px; color: #666; text-align: center;"> Click tick marks for quick ${title.toLowerCase()} • ${min}${unit} to ${max}${unit} </div> <div style="display: flex; gap: 10px; justify-content: center;"> <button id="save" style="background: #1da1f2; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 14px;">Save</button> <button id="cancel" style="background: #ccc; color: #333; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 14px;">Cancel</button> </div> </div> `; document.body.appendChild(overlay); const slider = overlay.querySelector('#slider'); const valueDisplay = overlay.querySelector('#valueDisplay'); const ticksContainer = overlay.querySelector('#ticks'); // Create ticks for (let i = min; i <= max; i += tickStep) { const position = ((i - min) / (max - min)) * 100; const isMajor = majorTickStep && i % majorTickStep === 0; const isWhole = tickStep >= 1 ? true : i % 1 === 0; const tick = document.createElement('div'); tick.style.cssText = ` position: absolute; left: ${position}%; top: 0; width: 2px; height: ${isMajor ? '12px' : isWhole ? '10px' : '6px'}; background: ${isMajor ? '#666' : isWhole ? '#666' : '#bbb'}; cursor: pointer; transform: translateX(-50%); `; tick.addEventListener('click', () => { slider.value = i; valueDisplay.textContent = i; }); ticksContainer.appendChild(tick); // Add labels for major ticks if (isMajor && (i === min || i % majorTickStep === 0)) { const label = document.createElement('div'); label.style.cssText = ` position: absolute; left: ${position}%; top: 14px; font-size: 10px; color: #666; transform: translateX(-50%); cursor: pointer; `; label.textContent = i + unit; label.addEventListener('click', () => { slider.value = i; valueDisplay.textContent = i; }); ticksContainer.appendChild(label); } } slider.addEventListener('input', () => valueDisplay.textContent = slider.value); overlay.addEventListener('click', (e) => { if (e.target === overlay) document.body.removeChild(overlay); }); return { overlay, slider, saveBtn: overlay.querySelector('#save'), cancelBtn: overlay.querySelector('#cancel') }; }; // Menu commands GM_registerMenuCommand('Configure Hotkey', () => { const validKeys = ['alt', 'ctrl', 'shift', 'ctrl+alt', 'ctrl+shift', 'alt+shift']; let input, error = false; do { input = prompt(`Enter magnifier activation hotkey:${error ? '\n\n❌ INVALID INPUT! Please use one of the options below.' : ''}\n\n📋 Valid options:\n• ${validKeys.join('\n• ')}\n\nCurrent hotkey: ${savedHotkey}`, savedHotkey); if (input === null) return; input = input.trim().toLowerCase(); error = !validKeys.includes(input); } while (error); savedHotkey = input; GM_setValue('magnifier_hotkey', savedHotkey); alert(`✅ Hotkey set to: ${savedHotkey.toUpperCase().replace('+', ' + ')}\n\nPlease refresh the page for changes to take effect.`); }); GM_registerMenuCommand('Configure Size', () => { const { overlay, slider, saveBtn, cancelBtn } = createSliderModal('Magnifier Size', '🔍', savedSize, 'px', 50, 2000, 1, 50, 200); saveBtn.addEventListener('click', () => { savedSize = parseInt(slider.value); GM_setValue('magnifier_size', savedSize); document.body.removeChild(overlay); alert(`✅ Magnifier size set to: ${savedSize}px\n\nPlease refresh the page for changes to take effect.`); }); cancelBtn.addEventListener('click', () => document.body.removeChild(overlay)); }); GM_registerMenuCommand('Configure Zoom', () => { const { overlay, slider, saveBtn, cancelBtn } = createSliderModal('Zoom Level', '🔎', savedZoom, 'x', 1, 20, 0.5, 0.5, 2); saveBtn.addEventListener('click', () => { savedZoom = parseFloat(slider.value); GM_setValue('magnifier_zoom', savedZoom); document.body.removeChild(overlay); alert(`✅ Zoom level set to: ${savedZoom}x\n\nPlease refresh the page for changes to take effect.`); }); cancelBtn.addEventListener('click', () => document.body.removeChild(overlay)); }); // Parse hotkey settings const keySettings = (() => { const keyMap = { 'alt': { alt: true, ctrl: false, shift: false }, 'ctrl': { alt: false, ctrl: true, shift: false }, 'shift': { alt: false, ctrl: false, shift: true }, 'ctrl+alt': { alt: true, ctrl: true, shift: false }, 'ctrl+shift': { alt: false, ctrl: true, shift: true }, 'alt+shift': { alt: true, ctrl: false, shift: true } }; return keyMap[savedHotkey] || { alt: true, ctrl: true, shift: false }; })(); // Core functions const createMagnifier = () => { const mag = document.createElement('div'); mag.id = 'image-magnifier'; mag.style.cssText = ` position: fixed; width: ${savedSize}px; height: ${savedSize}px; border: 3px solid #000; border-radius: 50%; background: #fff; background-repeat: no-repeat; box-shadow: 0 0 20px rgba(0,0,0,0.5); pointer-events: none; z-index: 10000; display: none; transition: opacity 0.1s ease; `; document.body.appendChild(mag); return mag; }; const updateMagnifier = (e, img) => { if (!magnifier || !img) return; const rect = img.getBoundingClientRect(); const x = e.clientX - rect.left, y = e.clientY - rect.top; const magSize = savedSize / 2; // Use natural dimensions for proper aspect ratio const bgWidth = (img.naturalWidth || rect.width) * savedZoom; const bgHeight = (img.naturalHeight || rect.height) * savedZoom; // Calculate scale factors to map mouse position correctly const scaleX = bgWidth / rect.width; const scaleY = bgHeight / rect.height; magnifier.style.backgroundImage = `url('${img.src}')`; magnifier.style.backgroundSize = `${bgWidth}px ${bgHeight}px`; magnifier.style.backgroundPosition = `${-((x * scaleX) - magSize)}px ${-((y * scaleY) - magSize)}px`; magnifier.style.left = `${e.clientX - magSize}px`; magnifier.style.top = `${e.clientY - magSize}px`; magnifier.style.display = 'block'; }; const hideMagnifier = () => { if (magnifier) { magnifier.style.display = 'none'; } }; const isImage = (el) => { if (!el) return false; if (el.tagName === 'IMG') return true; if (el.style?.backgroundImage && el.style.backgroundImage !== 'none') return true; if (el.getAttribute('data-testid') === 'tweetPhoto') return true; if (el.classList.contains('css-9pa8cd')) return true; if (el.querySelector('img')) return true; return false; }; const isActivationKeyPressed = (e) => e.altKey === keySettings.alt && e.ctrlKey === keySettings.ctrl && e.shiftKey === keySettings.shift; // Event listeners document.addEventListener('keydown', (e) => { if (isActivationKeyPressed(e) && !magnifierActive) { magnifierActive = true; if (!magnifier) magnifier = createMagnifier(); } else if (magnifierActive && !isActivationKeyPressed(e)) { magnifierActive = false; currentImage = null; hideMagnifier(); } }, { passive: true }); document.addEventListener('click', (e) => { if (magnifierActive) { magnifierActive = false; currentImage = null; hideMagnifier(); } }, { passive: true }); document.addEventListener('mousemove', (e) => { if (!magnifierActive) return; const target = e.target; if (isImage(target)) { currentImage = target; updateMagnifier(e, target); } else { currentImage = null; hideMagnifier(); } }); document.addEventListener('mouseleave', () => { if (magnifierActive) { currentImage = null; hideMagnifier(); } }, { passive: true }); document.addEventListener('wheel', (e) => { if (!magnifierActive || !currentImage) return; e.preventDefault(); e.stopPropagation(); savedZoom = Math.max(0.1, Math.min(20, savedZoom + (e.deltaY > 0 ? -0.1 : 0.1))); GM_setValue('magnifier_zoom', savedZoom); updateMagnifier(e, currentImage); // Show zoom indicator let indicator = document.getElementById('zoomIndicator'); if (!indicator) { indicator = document.createElement('div'); indicator.id = 'zoomIndicator'; indicator.style.cssText = ` position: fixed; top: 20px; right: 20px; background: rgba(0,0,0,0.8); color: white; padding: 8px 12px; border-radius: 4px; font-family: Arial, sans-serif; font-size: 14px; z-index: 10001; pointer-events: none; will-change: opacity; `; document.body.appendChild(indicator); } indicator.textContent = `Zoom: ${savedZoom.toFixed(1)}x`; indicator.style.display = 'block'; clearTimeout(window.zoomIndicatorTimeout); window.zoomIndicatorTimeout = setTimeout(() => indicator.style.display = 'none', 1000); }, { passive: false }); // Load message console.log(`🔍 Twitter Image Magnifier loaded! Press ${savedHotkey.toUpperCase().replace('+', ' + ')} to activate.`); console.log(`💡 Works on Twitter/X images. Right-click Tampermonkey icon → Configure settings.`); console.log(`🎯 Scroll wheel adjusts zoom when magnifier is active.`); })();