Adds a download button to Gemini code blocks. Automatically identifies the language and generates the corresponding file extension.
// ==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);
})();