Gemini Code Download Button

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();