Agrega un botón de 'Copiar' visible en la barra de acciones de cada respuesta de Gemini y en la barra de herramientas del Canvas.
当前为
// ==UserScript==
// @name Gemini - Botón de Copiar Directo
// @namespace http://tampermonkey.net/
// @version 1.9
// @description Agrega un botón de 'Copiar' visible en la barra de acciones de cada respuesta de Gemini y en la barra de herramientas del Canvas.
// @author Gemini
// @match https://gemini.google.com/app*
// @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Inyectamos CSS para los nuevos botones y los estados de los íconos.
GM_addStyle(`
.copiar-script-button {
margin-right: 8px;
}
.copiar-script-button .copied-icon, .copiar-canvas-button .copied-icon {
color: #6dd58c; /* Verde para el ícono de "check" */
font-variation-settings: 'FILL' 1;
}
.copiar-canvas-button {
margin-right: 8px;
}
`);
/**
* Crea y agrega el botón de copiar a un contenedor de respuesta de chat.
* @param {HTMLElement} responseContainer - El elemento <div> que contiene la respuesta del modelo.
*/
function agregarBotonDeCopiaChat(responseContainer) {
const actionsContainer = responseContainer.querySelector('.buttons-container-v2');
const shareButtonWrapper = responseContainer.querySelector('share-button');
if (!actionsContainer || !shareButtonWrapper) return;
const botonCopiar = document.createElement('button');
botonCopiar.setAttribute('mat-icon-button', '');
botonCopiar.setAttribute('mattooltip', 'Copiar contenido');
botonCopiar.setAttribute('aria-label', 'Copiar contenido');
botonCopiar.className = 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-unthemed copiar-script-button';
const icono = document.createElement('mat-icon');
icono.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
icono.textContent = 'content_copy';
botonCopiar.appendChild(icono);
botonCopiar.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const contentElement = responseContainer.querySelector('.markdown');
if (contentElement) {
navigator.clipboard.writeText(contentElement.innerText).then(() => {
icono.textContent = 'check';
icono.classList.add('copied-icon');
setTimeout(() => {
icono.textContent = 'content_copy';
icono.classList.remove('copied-icon');
}, 2000);
}).catch(err => console.error('Error al copiar el texto del chat: ', err));
}
});
actionsContainer.insertBefore(botonCopiar, shareButtonWrapper);
}
/**
* Crea y agrega el botón de copiar a la barra de herramientas del panel de código (Canvas).
* @param {HTMLElement} panelCanvas - El elemento <code-immersive-panel>.
*/
function agregarBotonDeCopiaCanvas(panelCanvas) {
const actionsContainer = panelCanvas.querySelector('toolbar .action-buttons');
const shareButtonTrigger = panelCanvas.querySelector('toolbar share-button button');
if (!actionsContainer || !shareButtonTrigger) {
return;
}
const botonCopiar = document.createElement('button');
botonCopiar.setAttribute('mat-icon-button', '');
botonCopiar.setAttribute('mattooltip', 'Copiar código');
botonCopiar.setAttribute('aria-label', 'Copiar código');
botonCopiar.className = 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger icon-button copiar-canvas-button mat-unthemed';
const icono = document.createElement('mat-icon');
icono.setAttribute('role', 'img');
icono.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
icono.textContent = 'content_copy';
botonCopiar.appendChild(icono);
botonCopiar.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
// Simula un clic en el botón "Compartir" para abrir el menú
shareButtonTrigger.click();
// Espera un instante para que el menú se renderice en el DOM
setTimeout(() => {
// Busca el botón de copiar real dentro del panel del menú que acaba de aparecer
const menuPanel = document.querySelector('.mat-mdc-menu-panel.mat-mdc-menu-panel');
if (menuPanel) {
const originalCopyButton = menuPanel.querySelector('copy-button button');
if (originalCopyButton) {
// Simula un clic en el botón de copiar original
originalCopyButton.click();
// Feedback visual de éxito en nuestro botón
icono.textContent = 'check';
icono.classList.add('copied-icon');
setTimeout(() => {
icono.textContent = 'content_copy';
icono.classList.remove('copied-icon');
}, 2000);
}
}
// El menú se cierra solo al hacer clic en una opción.
}, 50); // Un pequeño delay es suficiente
});
actionsContainer.insertBefore(botonCopiar, shareButtonTrigger.parentElement.parentElement);
}
/**
* Busca nuevos elementos en la página para agregarles los botones correspondientes.
*/
function procesarNuevosNodos() {
// --- Lógica para las respuestas del chat ---
document.querySelectorAll('model-response:not([data-copy-button-added])').forEach(container => {
if (container.querySelector('.markdown')) {
agregarBotonDeCopiaChat(container);
container.dataset.copyButtonAdded = 'true';
}
});
// --- Lógica para el panel de código (Canvas) ---
document.querySelectorAll('code-immersive-panel:not([data-canvas-copy-button-added])').forEach(panel => {
agregarBotonDeCopiaCanvas(panel);
panel.dataset.canvasCopyButtonAdded = 'true';
});
}
// El MutationObserver vigila por cambios en el DOM para ejecutar nuestro código.
const observer = new MutationObserver(() => {
setTimeout(procesarNuevosNodos, 500);
});
// Empezamos a observar el cuerpo del documento.
observer.observe(document.body, {
childList: true,
subtree: true
});
// Ejecutamos la función una vez al inicio.
setTimeout(procesarNuevosNodos, 1000);
})();