Greasy Fork

Greasy Fork is available in English.

Twitter/X Layout Modifier

Remove right sidebar, expand middle column, and inject custom vertical image stack with sizing fixes

当前为 2025-12-31 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Twitter/X Layout Modifier
// @namespace    http://tampermonkey.net/
// @license MIT
// @version      1.4
// @description  Remove right sidebar, expand middle column, and inject custom vertical image stack with sizing fixes
// @author       maye9999
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // 1. CSS for Structural Layout Changes (Sidebar & Column Width)
    const style = document.createElement('style');
    style.innerHTML = `
        /* Remove the right sidebar */
        [data-testid="sidebarColumn"] {
            display: none !important;
        }

        /* Expand the middle column (primaryColumn) */
        [data-testid="primaryColumn"] {
            max-width: 100% !important;
            width: 100% !important;
            flex-basis: auto !important;
        }

        /* Ensure parent containers allow expansion */
        div:has(> [data-testid="primaryColumn"]) {
             max-width: 100% !important;
             width: 100% !important;
        }

        .r-1ye8kvj {
            max-width: 100% !important;
        }

        /* Custom class for our injected container */
        .ag-custom-media-stack {
            display: flex;
            flex-direction: column;
            width: 100%;
            margin-top: 10px;
            align-items: center; /* Center images if they are narrower than width */
        }

        /* Image styling */
        .ag-custom-media-stack img {
            width: auto;           /* Let width adjust naturally */
            max-width: 100%;       /* Constraint: Don't overflow the column */
            max-height: 85vh;      /* Constraint: Don't exceed 85% of screen height */
            height: auto;          /* Maintain aspect ratio */
            border-radius: 12px;
            display: block;
            border: 1px solid rgba(255,255,255,0.1);
            cursor: pointer;
            object-fit: contain;   /* Ensure entire image is visible within box */
        }

        /* Separator Styling */
        .ag-media-separator {
            width: 100%;
            height: 1px;
            background-color: rgb(47, 51, 54); /* Common Twitter border color */
            margin: 20px 0;
            opacity: 0.6;
        }

        /* Lightbox-like effect on click (optional simple zoom) */
        .ag-custom-media-stack img:active {
            transform: scale(1.02);
            transition: transform 0.1s;
        }
    `;
    document.head.appendChild(style);


    // 2. JavaScript for Custom Layout Injection
    function processTweets() {
        // Find all tweets or photo-containing elements
        const photos = document.querySelectorAll('[data-testid="tweetPhoto"]');

        // Map to store unique root containers we've found in this pass
        const rootsToProcess = new Map(); // Map<Element, ImageUrl[]>

        photos.forEach(photo => {
            if (photo.hasAttribute('data-ag-processed')) return;

            // Extract URL
            const img = photo.querySelector('img');
            if (!img) return;

            let src = img.src;
            if (src.includes('&name=')) {
                src = src.replace(/&name=[a-z0-9]+/, '&name=large');
            }

            // Find root container logic
            const tweet = photo.closest('[data-testid="tweet"]');
            if (tweet) {
                // Inside this tweet, find the container that holds ALL images.
                const allImagesInTweet = Array.from(tweet.querySelectorAll('[data-testid="tweetPhoto"]'));
                if (allImagesInTweet.length > 0) {
                    // Find common ancestor
                    let ancestor = allImagesInTweet[0];
                    if (allImagesInTweet.length > 1) {
                        // Simple common ancestor algorithm
                        const parents = new Set();
                        let p = ancestor;
                        while (p && p !== tweet) {
                            parents.add(p);
                            p = p.parentElement;
                        }

                        // Check second image parents against set
                        let p2 = allImagesInTweet[1].parentElement;
                        while (p2 && p2 !== tweet) {
                            if (parents.has(p2)) {
                                ancestor = p2;
                                break;
                            }
                            p2 = p2.parentElement;
                        }
                    }

                    // Climb up from ancestor until the parent contains tweetText (sibling logic)
                    let contentRoot = ancestor;
                    while (contentRoot.parentElement && contentRoot.parentElement !== tweet) {
                        if (contentRoot.parentElement.querySelector('[data-testid="tweetText"]')) {
                            break;
                        }
                        contentRoot = contentRoot.parentElement;
                    }

                    if (!rootsToProcess.has(contentRoot)) {
                        rootsToProcess.set(contentRoot, []);
                    }

                    // Check if we already have this URL for this root
                    const list = rootsToProcess.get(contentRoot);
                    if (!list.includes(src)) {
                        list.push(src);
                    }

                    // Mark photo as pending processing
                    photo.setAttribute('data-ag-processed', 'true');
                }
            }
        });

        // Now process the roots
        rootsToProcess.forEach((urls, root) => {
            // 1. Tag root as processed
            if (root.getAttribute('data-ag-replaced')) return;

            // 2. Hide Root
            root.style.display = "none";
            root.setAttribute('data-ag-replaced', 'true');

            // 3. Inject Custom Stack
            const stack = document.createElement('div');
            stack.className = 'ag-custom-media-stack';

            urls.forEach((url, index) => {
                // ADD SEPARATOR between images (not before the first one)
                if (index > 0) {
                    const sep = document.createElement('div');
                    sep.className = 'ag-media-separator';
                    stack.appendChild(sep);
                }

                const img = document.createElement('img');
                img.src = url;
                img.onclick = (e) => {
                    e.stopPropagation();
                    window.open(url, '_blank');
                };
                stack.appendChild(img);
            });

            // Insert after the hidden root
            root.insertAdjacentElement('afterend', stack);
        });
    }

    // 3. Observe the DOM for new tweets
    const observer = new MutationObserver((mutations) => {
        processTweets();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Initial run
    setTimeout(processTweets, 500);
    setInterval(processTweets, 2000);

})();