您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances Google Gemini with a toolbar for snippets and canvas content download.
当前为
- // ==UserScript==
- // @name Google Gemini Mod (Toolbar & Download)
- // @namespace http://tampermonkey.net/
- // @version 0.0.5
- // @description Enhances Google Gemini with a toolbar for snippets and canvas content download.
- // @description[de] Verbessert Google Gemini mit einer Symbolleiste für Snippets und dem Herunterladen von Canvas-Inhalten.
- // @author Adromir
- // @match https://gemini.google.com/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
- // @license MIT
- // @licenseURL https://opensource.org/licenses/MIT
- // @homepageURL https://github.com/adromir/scripts/tree/main/userscripts/gemini-snippets
- // @supportURL https://github.com/adromir/scripts/issues
- // @grant GM_addStyle
- // @grant GM_setClipboard
- // @grant GM_getClipboard
- // ==/UserScript==
- (function() {
- 'use strict';
- // ===================================================================================
- // I. CONFIGURATION SECTION
- // ===================================================================================
- // --- Customizable Labels for Toolbar Buttons ---
- const PASTE_BUTTON_LABEL = "📋 Paste";
- const DOWNLOAD_BUTTON_LABEL = "💾 Download Canvas as File";
- // --- CSS Selectors for DOM Elements ---
- // Selector to find the h2 title element of an active canvas.
- const GEMINI_CANVAS_TITLE_TEXT_SELECTOR = "#app-root > main > side-navigation-v2 > bard-sidenav-container > bard-sidenav-content > div.content-wrapper > div > div.content-container > chat-window > immersive-panel > code-immersive-panel > toolbar > div > div.left-panel > h2.title-text.gds-title-s.ng-star-inserted";
- // Selector for the "Copy to Clipboard" button, relative to the toolbar element.
- const GEMINI_COPY_BUTTON_IN_TOOLBAR_SELECTOR = "div.action-buttons > copy-button.ng-star-inserted > button.copy-button";
- // Selectors for the Gemini input field (for snippet insertion)
- const GEMINI_INPUT_FIELD_SELECTORS = [
- '.ql-editor p',
- '.ql-editor',
- 'div[contenteditable="true"]'
- ];
- // --- Download Feature Configuration ---
- const DEFAULT_DOWNLOAD_EXTENSION = "txt";
- // --- Regular Expressions for Filename Sanitization ---
- // eslint-disable-next-line no-control-regex
- const INVALID_FILENAME_CHARS_REGEX = /[<>:"/\\|?*\x00-\x1F]/g;
- const RESERVED_WINDOWS_NAMES_REGEX = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
- const FILENAME_WITH_EXT_REGEX = /^(.+)\.([a-zA-Z0-9]{1,8})$/;
- const SUBSTRING_FILENAME_REGEX = /([\w\s.,\-()[\\]{}'!~@#$%^&+=]+?\.([a-zA-Z0-9]{1,8}))(?=\s|$|[,.;:!?])/g;
- // ===================================================================================
- // II. TOOLBAR ELEMENT DEFINITIONS
- // ===================================================================================
- const buttonSnippets = [
- { label: "Greeting", text: "Hello Gemini!" },
- { label: "Explain", text: "Could you please explain ... in more detail?" },
- ];
- const dropdownConfigurations = [
- {
- placeholder: "Actions...",
- options: [
- { label: "Summarize", text: "Please summarize the following text:\n" },
- { label: "Ideas", text: "Give me 5 ideas for ..." },
- { label: "Code (JS)", text: "Give me a JavaScript code example for ..." },
- ]
- },
- {
- placeholder: "Translations",
- options: [
- { label: "DE -> EN", text: "Translate the following into English:\n" },
- { label: "EN -> DE", text: "Translate the following into German:\n" },
- { label: "Correct Text", text: "Please correct the grammar and spelling in the following text:\n" }
- ]
- },
- ];
- // ===================================================================================
- // III. SCRIPT LOGIC
- // ===================================================================================
- // --- Embedded CSS for the Toolbar ---
- const embeddedCSS = `
- #gemini-snippet-toolbar-userscript {
- position: fixed !important; top: 0 !important; left: 50% !important;
- transform: translateX(-50%) !important;
- width: auto !important;
- max-width: 80% !important;
- padding: 10px 15px !important;
- z-index: 999999 !important;
- display: flex !important; flex-wrap: wrap !important;
- gap: 8px !important; align-items: center !important; font-family: 'Roboto', 'Arial', sans-serif !important;
- box-sizing: border-box !important; background-color: rgba(40, 42, 44, 0.95) !important;
- border-radius: 0 0 16px 16px !important;
- box-shadow: 0 4px 12px rgba(0,0,0,0.25);
- }
- #gemini-snippet-toolbar-userscript button,
- #gemini-snippet-toolbar-userscript select {
- padding: 4px 10px !important; cursor: pointer !important; background-color: #202122 !important;
- color: #e3e3e3 !important; border-radius: 16px !important; font-size: 13px !important;
- font-family: inherit !important; font-weight: 500 !important; height: 28px !important;
- box-sizing: border-box !important; vertical-align: middle !important;
- transition: background-color 0.2s ease, transform 0.1s ease !important;
- border: none !important; flex-shrink: 0;
- }
- #gemini-snippet-toolbar-userscript select {
- padding-right: 25px !important;
- appearance: none !important;
- background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="%23e3e3e3" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/></svg>') !important;
- background-repeat: no-repeat !important;
- background-position: right 8px center !important;
- background-size: 12px 12px !important;
- }
- #gemini-snippet-toolbar-userscript option {
- background-color: #2a2a2a !important;
- color: #e3e3e3 !important;
- font-weight: normal !important;
- padding: 5px 10px !important;
- }
- #gemini-snippet-toolbar-userscript button:hover,
- #gemini-snippet-toolbar-userscript select:hover {
- background-color: #4a4e51 !important;
- }
- #gemini-snippet-toolbar-userscript button:active {
- background-color: #5f6368 !important;
- transform: scale(0.98) !important;
- }
- .userscript-toolbar-spacer {
- margin-left: auto !important;
- }
- `;
- /**
- * Injects the embedded CSS using GM_addStyle.
- */
- function injectCustomCSS() {
- try {
- GM_addStyle(embeddedCSS);
- console.log("Gemini Mod Userscript: Custom CSS injected successfully.");
- } catch (error) {
- console.error("Gemini Mod Userscript: Failed to inject custom CSS:", error);
- const styleId = 'gemini-mod-userscript-styles';
- if (document.getElementById(styleId)) return;
- const style = document.createElement('style');
- style.id = styleId;
- style.textContent = embeddedCSS;
- document.head.appendChild(style);
- }
- }
- /**
- * Displays a message to the user (console and alert).
- * @param {string} message - The message to display.
- * @param {boolean} isError - True if it's an error message.
- */
- function displayUserscriptMessage(message, isError = true) {
- const prefix = "Gemini Mod Userscript: ";
- if (isError) console.error(prefix + message);
- else console.log(prefix + message);
- alert(prefix + message);
- }
- /**
- * Moves the cursor to the end of the provided element's content.
- * @param {Element} element - The contenteditable element or paragraph within it.
- */
- function moveCursorToEnd(element) {
- try {
- const range = document.createRange();
- const sel = window.getSelection();
- range.selectNodeContents(element);
- range.collapse(false);
- sel.removeAllRanges();
- sel.addRange(range);
- element.focus();
- } catch (e) {
- console.error("Gemini Mod Userscript: Error setting cursor position:", e);
- }
- }
- /**
- * Finds the target Gemini input element.
- * @returns {Element | null} The found input element or null.
- */
- function findTargetInputElement() {
- let targetInputElement = null;
- for (const selector of GEMINI_INPUT_FIELD_SELECTORS) {
- const element = document.querySelector(selector);
- if (element) {
- if (element.classList.contains('ql-editor')) {
- const pInEditor = element.querySelector('p');
- targetInputElement = pInEditor || element;
- } else {
- targetInputElement = element;
- }
- break;
- }
- }
- return targetInputElement;
- }
- /**
- * Inserts text into the Gemini input field, always appending.
- * @param {string} textToInsert - The text snippet to insert.
- */
- function insertSnippetText(textToInsert) {
- let targetInputElement = findTargetInputElement();
- if (!targetInputElement) {
- displayUserscriptMessage("Could not find Gemini input field.");
- return;
- }
- let actualInsertionPoint = targetInputElement;
- if (targetInputElement.classList.contains('ql-editor')) {
- let p = targetInputElement.querySelector('p');
- if (!p) {
- p = document.createElement('p');
- targetInputElement.appendChild(p);
- }
- actualInsertionPoint = p;
- }
- actualInsertionPoint.focus();
- setTimeout(() => {
- moveCursorToEnd(actualInsertionPoint);
- let insertedViaExec = false;
- try {
- insertedViaExec = document.execCommand('insertText', false, textToInsert);
- } catch (e) {
- console.warn("Gemini Mod Userscript: execCommand('insertText') threw an error:", e);
- }
- if (!insertedViaExec) {
- if (actualInsertionPoint.innerHTML === '<br>') actualInsertionPoint.innerHTML = '';
- actualInsertionPoint.textContent += textToInsert;
- moveCursorToEnd(actualInsertionPoint);
- }
- const editorToDispatchOn = document.querySelector('.ql-editor') || targetInputElement;
- if (editorToDispatchOn) {
- editorToDispatchOn.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
- editorToDispatchOn.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
- }
- console.log("Gemini Mod Userscript: Snippet inserted.");
- }, 50);
- }
- /**
- * Handles the paste button click. Reads from clipboard and inserts text.
- */
- async function handlePasteButtonClick() {
- try {
- if (!navigator.clipboard || !navigator.clipboard.readText) {
- displayUserscriptMessage("Clipboard access is not available or not permitted.");
- return;
- }
- const text = await navigator.clipboard.readText();
- if (text) insertSnippetText(text);
- else console.log("Gemini Mod Userscript: Clipboard is empty.");
- } catch (err) {
- console.error('Gemini Mod Userscript: Failed to read clipboard contents: ', err);
- displayUserscriptMessage(err.name === 'NotAllowedError' ? 'Permission to read clipboard was denied.' : 'Failed to paste from clipboard. See console.');
- }
- }
- /**
- * Helper function to ensure filename length does not exceed a maximum.
- * @param {string} filename - The filename to check.
- * @param {number} maxLength - The maximum allowed length.
- * @returns {string} The potentially truncated filename.
- */
- function ensureLength(filename, maxLength = 255) {
- if (filename.length <= maxLength) {
- return filename;
- }
- const dotIndex = filename.lastIndexOf('.');
- if (dotIndex === -1 || dotIndex < filename.length - 10 ) {
- return filename.substring(0, maxLength);
- }
- const base = filename.substring(0, dotIndex);
- const ext = filename.substring(dotIndex);
- const maxBaseLength = maxLength - ext.length;
- if (maxBaseLength <= 0) {
- return filename.substring(0, maxLength);
- }
- return base.substring(0, maxBaseLength) + ext;
- }
- /**
- * Sanitizes a base filename part (no extension).
- * @param {string} baseName - The base name to sanitize.
- * @returns {string} The sanitized base name.
- */
- function sanitizeBasename(baseName) {
- if (typeof baseName !== 'string' || baseName.trim() === "") return "downloaded_document";
- let sanitized = baseName.trim()
- .replace(INVALID_FILENAME_CHARS_REGEX, '_')
- .replace(/\s+/g, '_')
- .replace(/__+/g, '_')
- .replace(/^[_.-]+|[_.-]+$/g, '');
- if (!sanitized || RESERVED_WINDOWS_NAMES_REGEX.test(sanitized)) {
- sanitized = `_${sanitized || "file"}_`;
- sanitized = sanitized.replace(INVALID_FILENAME_CHARS_REGEX, '_').replace(/\s+/g, '_').replace(/__+/g, '_').replace(/^[_.-]+|[_.-]+$/g, '');
- }
- return sanitized || "downloaded_document";
- }
- /**
- * Determines the filename for download based on the canvas title,
- * prioritizing a `basename.ext` structure if found.
- * @param {string} title - The original string (e.g., canvas title).
- * @param {string} defaultExtension - The default extension if no structure is found.
- * @returns {string} A processed filename.
- */
- function determineFilename(title, defaultExtension = "txt") {
- const logPrefix = "Gemini Mod Userscript: determineFilename - ";
- if (!title || typeof title !== 'string' || title.trim() === "") {
- console.log(`${logPrefix}Input title invalid or empty, defaulting to "downloaded_document.${defaultExtension}".`);
- return ensureLength(`downloaded_document.${defaultExtension}`);
- }
- let trimmedTitle = title.trim();
- let baseNamePart = "";
- let extensionPart = "";
- const fullTitleMatch = trimmedTitle.match(FILENAME_WITH_EXT_REGEX);
- if (fullTitleMatch) {
- const potentialBase = fullTitleMatch[1];
- const potentialExt = fullTitleMatch[2].toLowerCase();
- if (!INVALID_FILENAME_CHARS_REGEX.test(potentialBase.replace(/\s/g, '_'))) {
- baseNamePart = potentialBase;
- extensionPart = potentialExt;
- console.log(`${logPrefix}Entire title "${trimmedTitle}" matches basename.ext. Base: "${baseNamePart}", Ext: "${extensionPart}"`);
- }
- }
- if (!extensionPart) {
- let lastMatch = null;
- let currentMatch;
- SUBSTRING_FILENAME_REGEX.lastIndex = 0;
- while ((currentMatch = SUBSTRING_FILENAME_REGEX.exec(trimmedTitle)) !== null) {
- lastMatch = currentMatch;
- }
- if (lastMatch) {
- const substringExtMatch = lastMatch[1].match(FILENAME_WITH_EXT_REGEX);
- if (substringExtMatch) {
- baseNamePart = substringExtMatch[1];
- extensionPart = substringExtMatch[2].toLowerCase();
- console.log(`${logPrefix}Found substring "${lastMatch[1]}" matching basename.ext. Base: "${baseNamePart}", Ext: "${extensionPart}"`);
- }
- }
- }
- if (extensionPart) {
- const sanitizedBase = sanitizeBasename(baseNamePart);
- return ensureLength(`${sanitizedBase}.${extensionPart}`);
- } else {
- console.log(`${logPrefix}No basename.ext pattern found. Sanitizing full title "${trimmedTitle}" with default extension "${defaultExtension}".`);
- const sanitizedTitleBase = sanitizeBasename(trimmedTitle);
- return ensureLength(`${sanitizedTitleBase}.${defaultExtension}`);
- }
- }
- /**
- * Creates and triggers a download for the given text content.
- * @param {string} filename - The desired filename.
- * @param {string} content - The text content to download.
- */
- function triggerDownload(filename, content) {
- try {
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- console.log(`Gemini Mod Userscript: Download triggered for "${filename}".`);
- } catch (error) {
- console.error(`Gemini Mod Userscript: Failed to trigger download for "${filename}":`, error);
- displayUserscriptMessage(`Failed to download: ${error.message}`);
- }
- }
- /**
- * Handles the click of the global canvas download button.
- * Finds the active canvas title, then its toolbar and copy button,
- * then reads from clipboard and initiates download.
- */
- async function handleGlobalCanvasDownload() {
- const titleTextElement = document.querySelector(GEMINI_CANVAS_TITLE_TEXT_SELECTOR);
- if (!titleTextElement) {
- console.warn("Gemini Mod Userscript: No active canvas title found. Selector:", GEMINI_CANVAS_TITLE_TEXT_SELECTOR);
- displayUserscriptMessage("No active canvas found to download.");
- return;
- }
- console.log("Gemini Mod Userscript: Found canvas title element:", titleTextElement);
- const toolbarElement = titleTextElement.closest('toolbar');
- if (!toolbarElement) {
- console.warn("Gemini Mod Userscript: Could not find parent toolbar for the title element. Searched for 'toolbar' tag from title.");
- displayUserscriptMessage("Could not locate the toolbar for the active canvas.");
- return;
- }
- console.log("Gemini Mod Userscript: Found toolbar element relative to title:", toolbarElement);
- const copyButton = toolbarElement.querySelector(GEMINI_COPY_BUTTON_IN_TOOLBAR_SELECTOR);
- if (!copyButton) {
- console.warn("Gemini Mod Userscript: 'Copy to Clipboard' button not found within the identified toolbar. Selector used on toolbar:", GEMINI_COPY_BUTTON_IN_TOOLBAR_SELECTOR);
- displayUserscriptMessage("Could not find the 'Copy to Clipboard' button in the active canvas's toolbar.");
- return;
- }
- console.log("Gemini Mod Userscript: Found 'Copy to Clipboard' button:", copyButton);
- copyButton.click();
- console.log("Gemini Mod Userscript: Programmatically clicked 'Copy to Clipboard' button.");
- setTimeout(async () => {
- try {
- if (!navigator.clipboard || !navigator.clipboard.readText) {
- displayUserscriptMessage("Clipboard access not available.");
- return;
- }
- const clipboardContent = await navigator.clipboard.readText();
- console.log("Gemini Mod Userscript: Successfully read from clipboard.");
- if (!clipboardContent || clipboardContent.trim() === "") {
- displayUserscriptMessage("Clipboard empty after copy. Nothing to download.");
- return;
- }
- const canvasTitle = (titleTextElement.textContent || "Untitled Canvas").trim();
- const filename = determineFilename(canvasTitle);
- triggerDownload(filename, clipboardContent);
- console.log("Gemini Mod Userscript: Global download initiated for canvas title:", canvasTitle, "using clipboard content. Filename:", filename);
- } catch (err) {
- console.error('Gemini Mod Userscript: Error reading from clipboard:', err);
- displayUserscriptMessage(err.name === 'NotAllowedError' ? 'Clipboard permission denied.' : 'Failed to read clipboard.');
- }
- }, 300);
- }
- /**
- * Creates the snippet toolbar and adds it to the page.
- */
- function createToolbar() {
- const toolbarId = 'gemini-snippet-toolbar-userscript';
- if (document.getElementById(toolbarId)) {
- console.log("Gemini Mod Userscript: Toolbar already exists.");
- return;
- }
- console.log("Gemini Mod Userscript: Initializing toolbar...");
- const toolbar = document.createElement('div');
- toolbar.id = toolbarId;
- buttonSnippets.forEach(snippet => {
- const button = document.createElement('button');
- button.textContent = snippet.label;
- button.title = snippet.text;
- button.addEventListener('click', () => insertSnippetText(snippet.text));
- toolbar.appendChild(button);
- });
- dropdownConfigurations.forEach(config => {
- if (config.options && config.options.length > 0) {
- const select = document.createElement('select');
- select.title = config.placeholder || "Select snippet";
- const defaultOption = document.createElement('option');
- defaultOption.textContent = config.placeholder || "Select...";
- defaultOption.value = "";
- defaultOption.disabled = true;
- defaultOption.selected = true;
- select.appendChild(defaultOption);
- config.options.forEach(snippet => {
- const option = document.createElement('option');
- option.textContent = snippet.label;
- option.value = snippet.text;
- select.appendChild(option);
- });
- select.addEventListener('change', (event) => {
- const selectedText = event.target.value;
- if (selectedText) {
- insertSnippetText(selectedText);
- event.target.selectedIndex = 0;
- }
- });
- toolbar.appendChild(select);
- }
- });
- const spacer = document.createElement('div');
- spacer.className = 'userscript-toolbar-spacer';
- toolbar.appendChild(spacer);
- const pasteButton = document.createElement('button');
- pasteButton.textContent = PASTE_BUTTON_LABEL;
- pasteButton.title = "Paste from Clipboard";
- pasteButton.addEventListener('click', handlePasteButtonClick);
- toolbar.appendChild(pasteButton);
- const globalDownloadButton = document.createElement('button');
- globalDownloadButton.textContent = DOWNLOAD_BUTTON_LABEL;
- globalDownloadButton.title = "Download active canvas content (uses canvas's copy button)";
- globalDownloadButton.addEventListener('click', handleGlobalCanvasDownload);
- toolbar.appendChild(globalDownloadButton);
- document.body.insertBefore(toolbar, document.body.firstChild);
- console.log("Gemini Mod Userscript: Toolbar inserted.");
- }
- /**
- * Handles dark mode. For a userscript, this is mostly about adapting to the site's
- * existing dark mode, if necessary for the toolbar.
- */
- function handleDarkModeForUserscript() {
- console.log("Gemini Mod Userscript: Dark mode handling is passive (toolbar is dark by default).");
- }
- // --- Initialization Logic ---
- function init() {
- console.log("Gemini Mod Userscript: Initializing...");
- injectCustomCSS();
- const M_INITIALIZATION_DELAY = 1500;
- setTimeout(() => {
- try {
- createToolbar();
- handleDarkModeForUserscript();
- console.log("Gemini Mod Userscript: Fully initialized.");
- } catch(e) {
- console.error("Gemini Mod Userscript: Error during delayed initialization:", e);
- displayUserscriptMessage("Error initializing toolbar. See console.");
- }
- }, M_INITIALIZATION_DELAY);
- }
- if (document.readyState === 'loading') {
- window.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址