Greasy Fork

来自缓存

Greasy Fork is available in English.

Twitter/X 微博式看图工具 - 添加缩放、拖拽和旋转功能

为 Twitter/X 图片添加缩放、拖拽和旋转功能。非常适合在桌面端阅读看不清的大段文字图片或长截图。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitter/X Image Viewer Enhanced - Adds zoom, drag, and rotation capabilities to Twitter/X images.
// @name:zh-CN   Twitter/X 微博式看图工具 - 添加缩放、拖拽和旋转功能
// @namespace    http://greasyfork.icu/en/users/1551895-piliplan
// @version      1.3.1
// @description  Adds zoom, drag, and rotation capabilities to Twitter/X images. Perfect for reading large text images or vertical screenshots that are hard to see on the desktop.
// @description:zh-CN 为 Twitter/X 图片添加缩放、拖拽和旋转功能。非常适合在桌面端阅读看不清的大段文字图片或长截图。
// @author       PILIPLAN
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Core State
    let state = {
        scale: 1,
        rotate: 0,
        x: 0,
        y: 0,
        isDragging: false,
        startX: 0,
        startY: 0
    };

    let lastToolbarX = 0;
    let lastToolbarY = 0;
    let activeElement = null;
    let toolbar = null;

    // --- Helper: Check if mouse is over a scrollable area (Sidebar) ---
    function isInsideScrollable(element) {
        let el = element;
        // Traverse up the DOM tree
        while (el && el !== document.body) {
            const style = window.getComputedStyle(el);
            // Check if element has scrollbars
            const isScrollableY = style.overflowY === 'auto' || style.overflowY === 'scroll';
            // Check if it actually has content to scroll
            const canScroll = el.scrollHeight > el.clientHeight;

            if (isScrollableY && canScroll) {
                return true; // Found a scrollable parent (the sidebar)
            }
            el = el.parentElement;
        }
        return false;
    }

    // --- Core 1: Find Image in Layers ---
    function findCenterElement() {
        const layersContainer = document.querySelector('#layers');
        if (!layersContainer) return null;

        const candidates = layersContainer.querySelectorAll('img, div[style*="background-image"]');
        let bestCandidate = null;
        let minDistance = Infinity;

        const anchor = getAnchorPoint();
        const centerX = anchor ? anchor.x : window.innerWidth / 2;
        const centerY = window.innerHeight / 2;

        candidates.forEach(el => {
            const rect = el.getBoundingClientRect();
            if (rect.width < 50 || rect.height < 50) return;
            if (window.getComputedStyle(el).opacity === '0') return;

            const dist = Math.sqrt(
                Math.pow((rect.left + rect.width / 2) - centerX, 2) +
                Math.pow((rect.top + rect.height / 2) - centerY, 2)
            );

            if (dist < minDistance) {
                minDistance = dist;
                bestCandidate = el;
            }
        });

        return (minDistance < 600) ? bestCandidate : null;
    }

    // --- Core 2: Get Anchor Point ---
    function getAnchorPoint() {
        const likeBtn = document.querySelector('#layers [data-testid="like"], #layers [data-testid="unlike"]');
        if (likeBtn) {
            const group = likeBtn.closest('[role="group"]');
            if (group) {
                const rect = group.getBoundingClientRect();
                return { x: rect.left + rect.width / 2, y: rect.top };
            }
        }
        return null;
    }

    // --- Core 3: Update Toolbar Position ---
    function updateToolbarPosition() {
        if (!toolbar) return;

        const anchor = getAnchorPoint();
        let targetX, targetY;

        if (anchor) {
            targetX = anchor.x;
            targetY = window.innerHeight - anchor.y + 10;
        } else {
            targetX = window.innerWidth / 2;
            targetY = 40;
        }

        // Dirty Check Optimization
        if (Math.abs(targetX - lastToolbarX) < 0.5 && Math.abs(targetY - lastToolbarY) < 0.5) {
            return;
        }

        if (anchor) {
            toolbar.style.left = targetX + 'px';
            toolbar.style.bottom = targetY + 'px';
            if(toolbar.style.transform !== 'translateX(-50%)') toolbar.style.transform = 'translateX(-50%)';
        } else {
            toolbar.style.left = '50%';
            toolbar.style.bottom = '40px';
        }

        lastToolbarX = targetX;
        lastToolbarY = targetY;
    }

    // --- Fix Clipping ---
    function unclipParents(element) {
        if (!element) return;
        let parent = element.parentElement;
        for (let i = 0; i < 8; i++) {
            if (parent && parent.style) {
                const style = window.getComputedStyle(parent);
                if (style.overflow !== 'visible') parent.style.setProperty('overflow', 'visible', 'important');
                if (style.maskType || style.webkitMask) {
                     parent.style.mask = 'none';
                     parent.style.webkitMask = 'none';
                }
            }
            parent = parent ? parent.parentElement : null;
        }
    }

    // --- Transform Logic ---
    function updateTransform() {
        if (!activeElement) activeElement = findCenterElement();
        if (!activeElement) return;

        unclipParents(activeElement);

        activeElement.style.transform = `
            translate(${state.x}px, ${state.y}px)
            rotate(${state.rotate}deg)
            scale(${state.scale})
        `;

        activeElement.style.transition = state.isDragging ? 'none' : 'transform 0.1s linear';
        activeElement.style.cursor = state.isDragging ? 'grabbing' : 'grab';
        activeElement.style.zIndex = '9999';

        if (activeElement.tagName === 'DIV') activeElement.style.backgroundSize = 'contain';
    }

    // --- Actions ---
    function zoom(delta) {
        state.scale += delta;
        if (state.scale < 0.1) state.scale = 0.1;
        initDragEvents();
        updateTransform();
    }

    function rotate(deg) {
        state.rotate += deg;
        updateTransform();
    }

    function reset() {
        state = { scale: 1, rotate: 0, x: 0, y: 0, isDragging: false, startX: 0, startY: 0 };
        if (activeElement) {
            activeElement.style.transform = '';
            activeElement.style.cursor = 'grab';
            activeElement.style.zIndex = '';
        }
    }

    // --- 🟢 Global Wheel Interception (Smart Mode) ---
    window.addEventListener('wheel', (e) => {
        // 1. Only work in photo mode
        if (!location.href.includes('/photo/')) return;

        // 2. 🟢 CRITICAL FIX: Check if mouse is over sidebar
        // If the user is hovering over a scrollable container (comments),
        // DO NOT INTERCEPT. Let native scroll happen.
        if (isInsideScrollable(e.target)) return;

        // 3. Find target image
        const target = findCenterElement();
        if (!target) return;

        activeElement = target;

        // 4. Intercept zoom
        e.preventDefault();
        e.stopImmediatePropagation();

        const delta = e.deltaY > 0 ? -0.1 : 0.1;
        zoom(delta);

    }, { passive: false });

    // --- Drag Logic ---
    function initDragEvents() {
        if (!activeElement) activeElement = findCenterElement();
        if (!activeElement || activeElement.dataset.dragBound) return;

        activeElement.dataset.dragBound = 'true';
        activeElement.style.cursor = 'grab';

        activeElement.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return;
            e.preventDefault();
            e.stopPropagation();
            state.isDragging = true;
            state.startX = e.clientX - state.x;
            state.startY = e.clientY - state.y;
            activeElement.style.cursor = 'grabbing';
        });
    }

    window.addEventListener('mousemove', (e) => {
        if (!state.isDragging) return;
        e.preventDefault();
        state.x = e.clientX - state.startX;
        state.y = e.clientY - state.startY;
        updateTransform();
    });

    window.addEventListener('mouseup', () => {
        state.isDragging = false;
        if (activeElement) activeElement.style.cursor = 'grab';
    });

    // --- UI Toolbar ---
    function createToolbar() {
        if (document.getElementById('x-fusion-toolbar')) return;

        toolbar = document.createElement('div');
        toolbar.id = 'x-fusion-toolbar';

        // High Transparency & Blur
        toolbar.style.cssText = `
            position: fixed;
            transform: translateX(-50%);
            z-index: 2147483647;
            background: rgba(0, 0, 0, 0.4);
            padding: 8px 25px;
            border-radius: 50px;
            display: flex;
            gap: 25px;
            backdrop-filter: blur(8px);
            border: 1px solid rgba(255,255,255,0.15);
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            opacity: 0; pointer-events: none; transition: opacity 0.2s;
        `;

        toolbar.addEventListener('mousedown', e => e.stopPropagation());

        const btns = [
            { t: '⟲', f: () => rotate(-90), title: 'Rotate Left' },
            { t: '-', f: () => zoom(-0.25), title: 'Zoom Out' },
            { t: 'RESET', f: reset, s: 'font-size:12px; font-weight:700; letter-spacing:1px; opacity:0.9;', title: 'Reset All' },
            { t: '+', f: () => zoom(0.25), title: 'Zoom In' },
            { t: '⟳', f: () => rotate(90), title: 'Rotate Right' }
        ];

        btns.forEach(b => {
            const s = document.createElement('span');
            s.innerHTML = b.t;
            s.style.cssText = `
                color: rgba(255, 255, 255, 0.9);
                cursor: pointer; font-size: 22px; width: 30px;
                display: flex; justify-content: center; align-items: center;
                user-select: none; transition: transform 0.1s;
                text-shadow: 0 1px 2px rgba(0,0,0,0.5);
                ${b.s||''}
            `;
            s.onmouseover = () => { s.style.color = '#fff'; s.style.transform = 'scale(1.1)'; };
            s.onmouseout = () => { s.style.color = 'rgba(255, 255, 255, 0.9)'; s.style.transform = 'scale(1)'; };
            s.onclick = (e) => { e.stopPropagation(); b.f(); };
            toolbar.appendChild(s);
        });

        document.body.appendChild(toolbar);
    }

    // --- Main Loop ---
    function loop() {
        const isPhotoView = location.href.includes('/photo/');
        if (!toolbar) createToolbar();

        if (isPhotoView) {
            toolbar.style.opacity = '1';
            toolbar.style.pointerEvents = 'auto';
            updateToolbarPosition();

            if (!activeElement) {
                 activeElement = findCenterElement();
                 if (activeElement) initDragEvents();
            }
        } else {
            toolbar.style.opacity = '0';
            toolbar.style.pointerEvents = 'none';
            if (state.scale !== 1 || state.rotate !== 0) reset();
            activeElement = null;
        }

        requestAnimationFrame(loop);
    }

    requestAnimationFrame(loop);

    window.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') reset();
    });

})();