Gemini Code Download Button

Adds a download button to Gemini code blocks. Automatically identifies the language and generates the corresponding file extension.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Gemini Code Download Button
// @name:zh-CN   Gemini 代码一键下载按钮
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a download button to Gemini code blocks. Automatically identifies the language and generates the corresponding file extension.
// @description:zh-CN 在 Gemini 代码块右上角添加下载按钮,自动识别主流编程语言并生成对应后缀的文件。
// @author       You
// @match        https://gemini.google.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 语言到后缀名的映射表
    const extMap = {
        'python': 'py', 'py': 'py',
        'javascript': 'js', 'js': 'js', 'node': 'js',
        'typescript': 'ts', 'ts': 'ts',
        'html': 'html',
        'css': 'css',
        'java': 'java',
        'c': 'c',
        'c++': 'cpp', 'cpp': 'cpp',
        'c#': 'cs', 'csharp': 'cs',
        'go': 'go', 'golang': 'go',
        'rust': 'rs', 'rs': 'rs',
        'php': 'php',
        'ruby': 'rb', 'rb': 'rb',
        'swift': 'swift',
        'kotlin': 'kt', 'kt': 'kt',
        'sql': 'sql',
        'bash': 'sh', 'shell': 'sh', 'sh': 'sh',
        'json': 'json',
        'xml': 'xml',
        'yaml': 'yaml', 'yml': 'yml',
        'markdown': 'md', 'md': 'md',
        'dart': 'dart',
        'r': 'r',
        'lua': 'lua',
        'perl': 'pl'
    };

    function addDownloadButtons() {
        // 定位到代码块里的每一个复制按钮
        let copyButtons = document.querySelectorAll('code-block gem-icon-button');

        copyButtons.forEach(copyBtn => {
            // 排除用户提问区域
            if (copyBtn.closest('user-query')) return;

            // 获取存放按钮的直接父容器
            let actionContainer = copyBtn.parentElement;
            if (!actionContainer) return;

            // 避免重复添加
            if (actionContainer.querySelector('.gemini-code-download-btn')) return;

            // 锁定整个代码块的顶级容器 code-block
            let blockContainer = copyBtn.closest('code-block');
            if (!blockContainer) return;

            // 在 code-block 范围内寻找存放代码文本的 pre 标签
            let pre = blockContainer.querySelector('pre');
            if (!pre) return;

            // 放弃使用不稳定的官方自定义标签,改用纯标准 HTML 按钮
            let downloadBtn = document.createElement('button');
            downloadBtn.classList.add('gemini-code-download-btn');
            downloadBtn.setAttribute('aria-label', '下载代码');
            downloadBtn.title = '下载代码';

            // 像素级复制官方按钮的视觉样式,确保完美对齐和悬停效果
            downloadBtn.style.cssText = `
                width: 32px !important;
                height: 32px !important;
                padding: 0 !important;
                margin-right: 12px !important;
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                border: none !important;
                border-radius: 50% !important;
                background-color: transparent !important;
                background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="%23c4c7c5"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/></svg>') !important;
                background-repeat: no-repeat !important;
                background-position: center !important;
                cursor: pointer !important;
                transition: background-color 0.2s ease !important;
            `;

            // 模拟官方的悬停背景变亮特效
            downloadBtn.onmouseover = () => {
                downloadBtn.style.backgroundColor = 'rgba(227, 227, 227, 0.08)';
            };
            downloadBtn.onmouseout = () => {
                downloadBtn.style.backgroundColor = 'transparent';
            };

            // 绑定点击下载事件
            downloadBtn.onclick = (e) => {
                e.preventDefault();
                e.stopPropagation();

                let codeText = pre.innerText;

                // 寻找代码块中标记语言的文本
                let langNode = blockContainer.querySelector('.language-name') || 
                               blockContainer.querySelector('span');

                let rawLang = "";
                let safeLangStr = "Unknown";
                let ext = "txt";

                if (langNode && langNode.innerText && langNode.innerText.length < 20) {
                    rawLang = langNode.innerText.trim().toLowerCase();
                    ext = extMap[rawLang] || "txt";
                    safeLangStr = rawLang.replace(/[^a-z0-9_-]/g, '') || "code";
                }

                let date = new Date();
                let timestamp = date.getFullYear().toString() +
                                (date.getMonth() + 1).toString().padStart(2, '0') +
                                date.getDate().toString().padStart(2, '0') + "-" +
                                date.getHours().toString().padStart(2, '0') +
                                date.getMinutes().toString().padStart(2, '0') +
                                date.getSeconds().toString().padStart(2, '0');

                let filename = `Gemini-${safeLangStr}-${timestamp}.${ext}`;

                let blob = new Blob([codeText], { type: 'text/plain;charset=utf-8' });
                let url = URL.createObjectURL(blob);
                let a = document.createElement('a');
                a.href = url;
                a.download = filename;
                a.click();
                URL.revokeObjectURL(url);

                // 点击成功后切换成对勾图标,2秒后还原
                downloadBtn.style.backgroundImage = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="%2334a853"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>')`;
                setTimeout(() => {
                    downloadBtn.style.backgroundImage = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="%23c4c7c5"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/></svg>')`;
                }, 2000);
            };

            // 把下载按钮插入到官方复制按钮的左侧
            actionContainer.insertBefore(downloadBtn, copyBtn);
        });
    }

    // 持续监听页面的动态重绘
    const observer = new MutationObserver(() => {
        addDownloadButtons();
    });

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

})();