Gemini Web UI Optimizer

Optimize long text fluency and add local code folding. Note: The global folding feature is currently in Beta.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gemini Web UI Optimizer
// @name:zh-CN   Gemini网页版UI优化器
// @namespace    https://github.com/JayConstruct/gemini-web-optimizer
// @version      1.0.1
// @description  Optimize long text fluency and add local code folding. Note: The global folding feature is currently in Beta.
// @description:zh-CN 优化长文本流畅度,添加局部代码折叠功能。注:全局折叠功能目前为测试版(Beta)。
// @author       JayConstruct
// @license      MIT
// @match        https://gemini.google.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. 样式注入:原生 UI 像素级匹配与细节优化
    // ==========================================
    const style = document.createElement('style');
    style.textContent = `
        * { overflow-anchor: none !important; }
        user-query, model-response { 
            content-visibility: auto !important; 
            contain: content !important; 
            transform: translateZ(0); 
            will-change: transform; 
        }

        /* 强制操作区按钮垂直居中对齐 */
        code-block .buttons {
            display: flex !important;
            align-items: center !important;
        }

        /* 🌟 局部与全局按钮的基础样式共用 */
        .custom-fold-btn, .custom-global-fold-btn { 
            background: transparent; 
            border: none; 
            color: var(--mat-sys-on-surface-variant, inherit);
            width: 40px; 
            height: 40px; 
            border-radius: 50%; 
            cursor: pointer; 
            margin: 0 2px; 
            display: inline-flex; 
            align-items: center; 
            justify-content: center;
            outline: none;
            position: relative;
            transition: background-color 200ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        
        .custom-fold-btn:hover, .custom-global-fold-btn:hover { 
            background-color: rgba(154, 160, 166, 0.15); 
        }
        .custom-fold-btn:active, .custom-global-fold-btn:active { 
            background-color: rgba(154, 160, 166, 0.3); 
        }

        /* SVG 图标控制 */
        .custom-fold-btn svg, .custom-global-fold-btn svg {
            width: 24px;
            height: 24px;
            fill: currentColor;
            transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        
        /* 局部按钮的 180度旋转 */
        .custom-fold-btn.is-rotated svg {
            transform: rotate(-180deg);
        }

        /* 头部背景框底部圆角平滑过渡 */
        code-block > div > div:first-child {
            transition: border-radius 250ms cubic-bezier(0.4, 0, 0.2, 1) !important;
        }
        .code-folded > div > div:first-child {
            border-bottom-left-radius: 16px !important;
            border-bottom-right-radius: 16px !important;
        }

        /* 瞬间折叠以修复滚动锚定 Bug */
        code-block > div > div:nth-of-type(2) {
            opacity: 1;
            transition: opacity 0.15s ease-out; 
        }
        .code-folded > div > div:nth-of-type(2) {
            display: none !important; 
        }
    `;
    document.head.appendChild(style);

    // ==========================================
    // 2. 原生图标构建器
    // ==========================================
    // 局部折叠图标 (expand_more)
    const createNativeIcon = () => {
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 24 24');
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', "M12 15.4l-6-6L7.4 8l4.6 4.6L16.6 8 18 9.4z");
        svg.appendChild(path);
        return svg;
    };

    // ==========================================
    // 3. 🎯 V9.8 核心:全局折叠控制器
    // ==========================================
    let isAllFolded = false; // 全局状态
    
    // Material 语义路径
    const PATH_COLLAPSE_ALL = "M7.41 18.59L8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"; // unfold_less
    const PATH_EXPAND_ALL = "M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"; // unfold_more

    const injectGlobalButton = () => {
        if (document.querySelector('.custom-global-fold-btn')) return;

        // 查找所有 share 按钮,取第一个(通常是顶部 Header 里的那个全局 Share 按钮)
        const shareIcons = document.querySelectorAll('mat-icon[data-mat-icon-name="share"]');
        if (shareIcons.length === 0) return;

        const topShareBtn = shareIcons[0].closest('button');
        if (!topShareBtn || !topShareBtn.parentNode) return;

        // 创建全局按钮
        const globalBtn = document.createElement('button');
        globalBtn.className = 'custom-global-fold-btn';
        globalBtn.title = '折叠所有代码块';

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 24 24');
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        // 初始状态为全部展开,因此显示“准备折叠”的图标
        path.setAttribute('d', PATH_COLLAPSE_ALL);
        svg.appendChild(path);
        globalBtn.appendChild(svg);

        // 全局点击事件
        globalBtn.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();

            isAllFolded = !isAllFolded;

            // 1. 更新全局按钮自身的状态
            if (isAllFolded) {
                path.setAttribute('d', PATH_EXPAND_ALL);
                globalBtn.title = '展开所有代码块';
            } else {
                path.setAttribute('d', PATH_COLLAPSE_ALL);
                globalBtn.title = '折叠所有代码块';
            }

            // 2. 同步并覆盖所有局部代码块的状态
            document.querySelectorAll('code-block').forEach(block => {
                const localBtn = block.querySelector('.custom-fold-btn');
                if (isAllFolded) {
                    block.classList.add('code-folded');
                    if (localBtn) {
                        localBtn.classList.add('is-rotated');
                        localBtn.title = '展开代码';
                    }
                } else {
                    block.classList.remove('code-folded');
                    if (localBtn) {
                        localBtn.classList.remove('is-rotated');
                        localBtn.title = '收起代码';
                    }
                }
            });
        };

        // 将按钮插入到 Share 按钮的左边
        topShareBtn.parentNode.insertBefore(globalBtn, topShareBtn);
    };

    // ==========================================
    // 4. 局部按钮注入与精准滚动引擎
    // ==========================================
    const injectFoldButton = (codeBlock) => {
        if (codeBlock.querySelector('.custom-fold-btn')) return;
        const topBar = codeBlock.querySelector('div > div:first-child');
        if (!topBar) return;
        const copyBtn = topBar.querySelector('button');
        if (!copyBtn) return; 

        const foldBtn = document.createElement('button');
        foldBtn.className = 'custom-fold-btn';
        foldBtn.title = '收起代码'; 
        foldBtn.appendChild(createNativeIcon());
        
        foldBtn.onclick = (e) => {
            e.preventDefault(); e.stopPropagation(); 
            
            const rectBefore = foldBtn.getBoundingClientRect();
            const isFolded = codeBlock.classList.toggle('code-folded');
            foldBtn.classList.toggle('is-rotated');
            foldBtn.title = isFolded ? '展开代码' : '收起代码';

            requestAnimationFrame(() => {
                const rectAfter = foldBtn.getBoundingClientRect();
                const delta = rectAfter.top - rectBefore.top;

                if (Math.abs(delta) > 0) {
                    let scroller = codeBlock;
                    while (scroller && scroller !== document.body && scroller !== document.documentElement) {
                        const s = window.getComputedStyle(scroller);
                        if ((s.overflowY === 'auto' || s.overflowY === 'scroll' || s.overflowY === 'overlay') && scroller.scrollHeight > scroller.clientHeight) break;
                        scroller = scroller.parentNode;
                    }
                    if (scroller && scroller !== document.body && scroller !== document.documentElement) {
                        scroller.scrollTop += delta;
                    } else {
                        window.scrollBy(0, delta);
                    }
                }
            });
        };
        copyBtn.parentNode.insertBefore(foldBtn, copyBtn);
    };

    // ==========================================
    // 5. Fiber 调度与增量监听
    // ==========================================
    const initHighlightScheduler = () => {
        if (!window.hljs || window.hljs._isFiberOptimized) return;
        const originalHighlight = window.hljs.highlightBlock;
        let pendingBlocks = new Set();
        let streamTimeout = null;
        let isStreaming = false;

        const processPendingBlocks = (idleDeadline) => {
            while (pendingBlocks.size > 0 && (idleDeadline.timeRemaining() > 5 || idleDeadline.didTimeout)) {
                const block = pendingBlocks.values().next().value;
                pendingBlocks.delete(block);
                try { originalHighlight.call(window.hljs, block); } catch (e) {}
            }
            if (pendingBlocks.size > 0) requestIdleCallback(processPendingBlocks, { timeout: 2000 });
        };

        window.hljs.highlightBlock = function(block) {
            pendingBlocks.add(block);
            clearTimeout(streamTimeout);
            if (!isStreaming) block.classList.remove('hljs'); 
            isStreaming = true;
            streamTimeout = setTimeout(() => {
                isStreaming = false;
                if (window.requestIdleCallback) requestIdleCallback(processPendingBlocks, { timeout: 2000 });
            }, 1000); 
        };
        window.hljs._isFiberOptimized = true;
    };

    const domObserver = new MutationObserver((mutations) => {
        if (!window.hljs || !window.hljs._isFiberOptimized) initHighlightScheduler();
        
        // 尝试在动态 DOM 变化时重新挂载全局按钮(防止被 Angular 刷新掉)
        injectGlobalButton();

        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'CODE-BLOCK') injectFoldButton(node);
                    else if (node.querySelectorAll) {
                        const blocks = node.querySelectorAll('code-block');
                        if (blocks.length > 0) blocks.forEach(injectFoldButton);
                    }
                }
            }
        }
    });

    setTimeout(() => {
        document.querySelectorAll('code-block').forEach(injectFoldButton);
        const chatContainer = document.querySelector('chat-container') || document.body;
        domObserver.observe(chatContainer, { childList: true, subtree: true });
        initHighlightScheduler(); 
        injectGlobalButton(); // 首次注入全局按钮
        console.log("🚀 Gemini Ultimate Tool v9.8:全局一键折叠控制引擎已上线。");
    }, 3000);
})();