Greasy Fork is available in English.

Gemini Code Block Scroller (v2.0)

Gemini 代码块滚动增强:标题居中、状态跟随、自动历史补全、双击展开。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Gemini Code Block Scroller (v2.0)
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Gemini 代码块滚动增强:标题居中、状态跟随、自动历史补全、双击展开。
// @author       Gemini User
// @match        https://gemini.google.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ================= 配置区域 =================
    const CONFIG = {
        maxHeight: '300px',       // 限制高度
        transitionTime: '0.2s',   // 动画速度
        statusText: {
            generating: '⏳ 生成中...',
            done: '✅ 代码生成完毕'
        }
    };

    // 1. 注入样式
    GM_addStyle(`
        /* 核心滚动样式 */
        .code-block .formatted-code-block-internal-container pre {
            max-height: ${CONFIG.maxHeight} !important;
            overflow-y: auto !important;
            display: block !important;
            cursor: zoom-in !important;
            transition: max-height ${CONFIG.transitionTime} ease-out;
            padding-bottom: 20px !important;
            border-bottom: 2px solid rgba(138, 180, 248, 0.1);
        }
        
        .code-block .formatted-code-block-internal-container pre.gm-expanded {
            max-height: none !important;
            cursor: zoom-out !important;
        }

        /* === 布局核心样式 === */
        
        /* 1. 状态标签样式 */
        .gm-status-badge {
            font-size: 12px;
            padding: 2px 8px;
            border-radius: 12px;
            
            /* 关键布局:把右边的按钮组狠狠推到最右边 */
            margin-right: auto !important; 
            
            /* 和左边的语言标签保持距离 */
            margin-left: 10px !important; 
            
            font-family: "Google Sans", Roboto, sans-serif;
            font-weight: 500;
            z-index: 999;
            display: inline-flex;
            align-items: center;
            white-space: nowrap;
            height: 24px;
        }
        
        /* 2. 语言标签样式 (JS动态添加此类名) */
        /* 作用:把左边的红绿灯/空白狠狠推到最左边 */
        .gm-lang-label-centered {
            margin-left: auto !important;
        }

        .gm-status-badge.generating {
            background-color: rgba(253, 214, 99, 0.15);
            color: #fdd663;
            border: 1px solid rgba(253, 214, 99, 0.3);
            animation: pulse 1.5s infinite;
        }
        
        .gm-status-badge.done {
            background-color: rgba(129, 201, 149, 0.15);
            color: #81c995;
            border: 1px solid rgba(129, 201, 149, 0.3);
        }

        @keyframes pulse {
            0% { opacity: 0.6; }
            50% { opacity: 1; }
            100% { opacity: 0.6; }
        }
    `);

    // 2. 双击交互
    document.addEventListener('dblclick', function(e) {
        const preBlock = e.target.closest('.formatted-code-block-internal-container pre');
        if (preBlock) {
            e.preventDefault();
            window.getSelection()?.removeAllRanges();
            preBlock.classList.toggle('gm-expanded');
        }
    });

    // 3. 智能检测与布局修正
    setInterval(() => {
        fixAllBlocks();
    }, 500);

    function checkIsGenerating() {
        const buttons = Array.from(document.querySelectorAll('button[aria-label]'));
        const stopBtn = buttons.find(btn => {
            const label = btn.getAttribute('aria-label');
            const hasStop = label.includes('Stop') || label.includes('停止');
            const isMedia = label.includes('朗读') || label.includes('收听') || label.includes('播放') || label.includes('Read') || label.includes('Listen');
            return hasStop && !isMedia;
        });
        return !!stopBtn;
    }

    function fixAllBlocks() {
        const allBlocks = document.querySelectorAll('.code-block');
        if (allBlocks.length === 0) return;

        const isGenerating = checkIsGenerating();

        allBlocks.forEach((block, index) => {
            const isLast = index === allBlocks.length - 1;
            const status = (isGenerating && isLast) ? 'generating' : 'done';
            
            ensureBadgeAndLayout(block, status);
        });
    }

    function ensureBadgeAndLayout(block, statusType) {
        let header = block.querySelector('.code-block-decoration');
        if (!header) header = block.querySelector('div:first-child');
        if (!header) return;

        // === 步骤1:处理语言标签居中 ===
        // 查找语言标签 (通常是 span)
        const langSpan = header.querySelector('span');
        if (langSpan && !langSpan.classList.contains('gm-lang-label-centered')) {
            langSpan.classList.add('gm-lang-label-centered');
        }

        // === 步骤2:处理状态徽章 ===
        let badge = header.querySelector('.gm-status-badge');
        const targetText = statusType === 'generating' ? CONFIG.statusText.generating : CONFIG.statusText.done;

        if (!badge) {
            badge = document.createElement('span');
            badge.className = 'gm-status-badge ' + statusType;
            badge.textContent = targetText;
            
            // 插入逻辑:紧跟在语言标签后面
            if (langSpan) {
                // 插在 span 后面,这样它们就形成了一个中间的组
                // header.insertBefore(badge, langSpan.nextSibling);
                // 为了保险,如果 span 后面有东西,插在它前面;如果没东西,appendChild
                if (langSpan.nextSibling) {
                    header.insertBefore(badge, langSpan.nextSibling);
                } else {
                    header.appendChild(badge);
                }
            } else {
                // 找不到 span?那只能插在最前面凑合一下
                header.appendChild(badge);
            }
        } 
        else if (badge.textContent !== targetText || !badge.classList.contains(statusType)) {
            badge.className = 'gm-status-badge ' + statusType;
            badge.textContent = targetText;
        }
    }

    console.log('Gemini Scroller v2.4: 居中布局版已启动');
})();