gemini 代码底部增加复制按钮

Adds a "Copy Code" button, centered at the bottom of specified code block structures.

当前为 2025-05-08 提交的版本,查看 最新版本

// ==UserScript==
// @name         gemini 代码底部增加复制按钮
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Adds a "Copy Code" button, centered at the bottom of specified code block structures.
// @author       cores
// @match        https://gemini.google.com/app/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const CUSTOM_BUTTON_CLASS = 'custom-bottom-copy-button';
    const FOOTER_CLASS = 'custom-code-block-footer';
    const PROCESSED_MARKER_CLASS = 'custom-centered-copy-added'; // Renamed for clarity

    function createAndAddButton(codeBlockElement) {
        // Check if already processed by this version of the script
        if (codeBlockElement.classList.contains(PROCESSED_MARKER_CLASS)) {
            return;
        }

        const codeContentElement = codeBlockElement.querySelector('code[data-test-id="code-content"], pre code');

        if (!codeContentElement) {
            // console.warn('[Centered Copy Button] Code content element not found in:', codeBlockElement);
            return;
        }

        const copyButton = document.createElement('button');
        copyButton.textContent = '复制';
        copyButton.className = CUSTOM_BUTTON_CLASS;
        copyButton.setAttribute('aria-label', '复制');

        copyButton.addEventListener('click', async (event) => {
            event.stopPropagation();

            const codeText = codeContentElement.innerText;
            const originalButtonText = copyButton.textContent;

            try {
                await navigator.clipboard.writeText(codeText);
                copyButton.textContent = '已复制';
            } catch (err) {
                try {
                    GM_setClipboard(codeText);
                    copyButton.textContent = '已复制 (GM)!';
                } catch (gmErr) {
                    copyButton.textContent = '复制失败';
                    alert('无法复制代码到剪贴板。请手动复制。');
                }
            }

            setTimeout(() => {
                copyButton.textContent = originalButtonText;
            }, 2500);
        });

        let footerDiv = codeBlockElement.querySelector('.' + FOOTER_CLASS);
        if (!footerDiv) {
            footerDiv = document.createElement('div');
            footerDiv.className = FOOTER_CLASS;
            codeBlockElement.appendChild(footerDiv);
        }
        // Clear previous content if any (e.g. from old version of script if classname was same)
        // footerDiv.innerHTML = '';
        footerDiv.appendChild(copyButton);

        codeBlockElement.classList.add(PROCESSED_MARKER_CLASS);
    }

    function processAllCodeBlocks() {
        const codeBlocks = document.querySelectorAll('div.code-block');
        codeBlocks.forEach(block => {
            createAndAddButton(block);
        });
    }


    // Add styles for the new button and its centering container (footer)
    GM_addStyle(`
        .${FOOTER_CLASS} {
            display: flex;
            justify-content: center; /* MODIFIED: Center the button */
            align-items: center;     /* Vertically center if footer has extra height */
            padding: 8px 0px;        /* Padding top/bottom; no side padding needed for centering one item */
            margin-top: 8px;         /* Space between code block content and button area */
            /* border-top: 1px solid #e0e0e0; */ /* Optional: Uncomment for a separator line */
            /* background-color: #f9f9f9; */    /* Optional: Uncomment for a distinct footer background */
        }
        .${CUSTOM_BUTTON_CLASS} {
            background-color: transparent;  /* MODIFIED: Transparent background */
            color: #007bff;                 /* MODIFIED: Text color (e.g., blue) */
            border: 1px solid #007bff;      /* MODIFIED: Border to make button visible */
            padding: 7px 15px;              /* Adjusted padding for border */
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 13px;
            border-radius: 4px;
            cursor: pointer;
            transition: color 0.3s ease, border-color 0.3s ease, transform 0.1s ease; /*MODIFIED: transition properties */
        }
        .${CUSTOM_BUTTON_CLASS}:hover {
            color: #0056b3;                 /* MODIFIED: Darker text color on hover */
            border-color: #0056b3;          /* MODIFIED: Darker border color on hover */
            /* background-color: rgba(0, 123, 255, 0.05); */ /* Optional: very subtle background on hover */
        }
        .${CUSTOM_BUTTON_CLASS}:active {
            color: #004085;                 /* MODIFIED: Even darker text when clicked */
            border-color: #004085;          /* MODIFIED: Even darker border when clicked */
            transform: scale(0.98);         /* Slight press effect */
        }
    `);


    // Initial run to add buttons to existing code blocks
    processAllCodeBlocks();

    // Use MutationObserver to handle dynamically loaded code blocks
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches && node.matches('div.code-block')) {
                            createAndAddButton(node);
                        }
                        node.querySelectorAll('div.code-block').forEach(createAndAddButton);
                    }
                });
            }
        }
    });

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

})();