Greasy Fork is available in English.

Gemini Code Download Button

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Gemini Code Download Button
// @name:zh-CN   Gemini 代码一键下载按钮
// @namespace    http://tampermonkey.net/
// @version      1.0
// @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 copyIcons = document.querySelectorAll('mat-icon[fonticon="content_copy"]');

        copyIcons.forEach(icon => {
            // 跳过用户提问区
            if (icon.closest('user-query')) return;

            let copyBtn = icon.closest('button');
            if (!copyBtn) return;

            let actionContainer = copyBtn.parentElement;
            if (actionContainer.querySelector('.gemini-code-download-btn')) return;

            let blockContainer = copyBtn.closest('code-block') ||
                                 copyBtn.closest('.code-block-decoration') ||
                                 copyBtn.closest('div:has(pre)');

            let pre = (blockContainer && blockContainer.querySelector('pre')) ||
                      actionContainer.parentElement.querySelector('pre') ||
                      actionContainer.closest('div:has(>pre)')?.querySelector('pre');

            if (!pre) return;

            // 原生 DOM 创建下载按钮
            let downloadBtn = document.createElement('button');
            downloadBtn.className = copyBtn.className + ' gemini-code-download-btn';
            downloadBtn.style.cssText = copyBtn.style.cssText;
            downloadBtn.style.marginRight = '4px';
            downloadBtn.setAttribute('aria-label', '下载代码');
            downloadBtn.title = '下载代码';

            let iconNode = document.createElement('mat-icon');
            iconNode.className = icon.className;
            iconNode.setAttribute('fonticon', 'download');
            iconNode.innerText = 'download';

            downloadBtn.appendChild(iconNode);

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

                let codeText = pre.innerText;

                let langNode = actionContainer.parentElement.querySelector('.language-name') ||
                               blockContainer?.querySelector('span');

                let rawLang = "";
                let safeLangStr = "Unknown";
                let ext = "txt"; // 默认后缀为 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);

                iconNode.setAttribute('fonticon', 'check');
                iconNode.innerText = 'check';
                setTimeout(() => {
                    iconNode.setAttribute('fonticon', 'download');
                    iconNode.innerText = 'download';
                }, 2000);
            };

            actionContainer.insertBefore(downloadBtn, copyBtn);
        });
    }

    const observer = new MutationObserver(() => {
        addDownloadButtons();
    });

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

})();