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.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);
})();