WideScreen & Gemini Copy Button

Makes Claude, ChatGPT and Gemini chat interfaces wider, and replace Claude's default font. Adds a copy button to Gemini code blocks. | 扩展 Claude、ChatGPT 和 Gemini 布局,替换 Claude 字体,并为 Gemini 代码块添加复制按钮。

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

// ==UserScript==
// @name          WideScreen & Gemini Copy Button
// @namespace    https://example.com/Gemini
// @match        *://claude.ai/*
// @match        *://chatgpt.com/*
// @match        *://gemini.google.com/*
// @version      1.1
// @author       cores
// @license      MIT
// @description  Makes Claude, ChatGPT and Gemini chat interfaces wider, and replace Claude's default font. Adds a copy button to Gemini code blocks. | 扩展 Claude、ChatGPT 和 Gemini 布局,替换 Claude 字体,并为 Gemini 代码块添加复制按钮。
// ==/UserScript==

(function() {
    'use strict';

    // Detect which platform we're on
    const isGemini = window.location.hostname.includes('gemini.google.com');
    const isClaude = window.location.hostname.includes('claude.ai');
    const isChatGPT = window.location.hostname.includes('chatgpt.com');

    // Create a style element
    const style = document.createElement('style');

    // Common font styles for all platforms
    const commonFontStyles = `
        /* Common normalized font styles */
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
        }
    `;

    let platformSpecificStyles = '';
    let geminiCopyButtonFeatureStyles = ''; // Will hold CSS for Gemini's copy button

    // Set platform-specific CSS for widescreen
    if (isGemini) {
        platformSpecificStyles = `
            /* Gemini wide screen CSS */
            .chat-window,
            .chat-container,
            .conversation-container,
            .gemini-conversation-container {
                max-width: 95% !important;
                width: 95% !important;
            }
            .input-area-container,
            textarea,
            .prompt-textarea,
            .prompt-container {
                max-width: 95% !important;
                width: 95% !important;
            }
            textarea {
                width: 100% !important;
            }
            .max-w-3xl,
            .max-w-4xl,
            .max-w-screen-md {
                max-width: 95% !important;
            }
            .message-content,
            .user-message,
            .model-response {
                width: 100% !important;
                max-width: 100% !important;
            }
            .pre-fullscreen {
                height: auto !important;
            }
            .input-buttons-wrapper-top {
                right: 8px !important;
            }
        `;
    } else if (isClaude) {
        platformSpecificStyles = `
            /* Claude wide screen CSS */
            .max-w-screen-md, .max-w-3xl, .max-w-4xl {
                max-width: 95% !important;
            }
            .w-full.max-w-3xl, .w-full.max-w-4xl {
                max-width: 95% !important;
                width: 95% !important;
            }
            .w-full.max-w-3xl textarea {
                width: 100% !important;
            }
            .mx-auto {
                max-width: 95% !important;
            }
            [data-message-author-role] {
                width: 100% !important;
            }
            .absolute.right-0 {
                right: 10px !important;
            }
            /* Claude specific font fixes */
            p, h1, h2, h3, h4, h5, h6, span, div, textarea, input, button {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
                font-weight: 400 !important;
            }
            pre, code, .font-mono {
                font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
            }
            [data-message-author-role] p {
                font-size: 16px !important;
                line-height: 1.5 !important;
                letter-spacing: normal !important;
            }
            h1, h2, h3, h4, h5, h6 {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
            }
        `;
    } else if (isChatGPT) {
        platformSpecificStyles = `
            /* ChatGPT wide screen CSS */
            .mx-auto {
                max-width: 100% !important;
                width: auto !important;
            }
            .h-full {
                height: 100% !important;
            }
            .w-full {
                width: 100% !important;
            }
            .message-input, .input-area input, .input-area textarea {
                width: 100% !important;
            }
            .h-\\[116px\\] { /* Tailwind specific class, adjust if it changes */
                height: auto !important;
            }
        `;
    }

    // --- Gemini Copy Button Feature ---
    if (isGemini) {
        const GEMINI_BUTTON_CLASS = 'gemini-custom-md-icon-copy-button';
        const GEMINI_FOOTER_CLASS = 'gemini-custom-code-block-footer-centered';
        const GEMINI_PROCESSED_MARKER_CLASS = 'gemini-custom-md-icon-copy-added';

        const ICON_HTML_COPY_GEMINI = '<mat-icon role="img" fonticon="content_copy" class="mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color" aria-hidden="true" data-mat-icon-type="font" data-mat-icon-name="content_copy"></mat-icon>';
        const ICON_HTML_CHECK_GEMINI = '<mat-icon role="img" fonticon="check" class="mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color" aria-hidden="true" data-mat-icon-type="font" data-mat-icon-name="check"></mat-icon>';

        geminiCopyButtonFeatureStyles = `
            .${GEMINI_FOOTER_CLASS} {
                display: flex;
                justify-content: center;
                align-items: center;
                padding: 8px 0px;
                margin-top: 8px;
            }
            .${GEMINI_BUTTON_CLASS} {
                background-color: transparent;
                color: #5f6368;
                border: none;
                padding: 0;
                width: 40px;
                height: 40px;
                border-radius: 50%;
                cursor: pointer;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease;
                outline: none;
            }
            .${GEMINI_BUTTON_CLASS} .mat-icon {
                font-size: 24px;
                display: flex;
                align-items: center;
                justify-content: center;
                line-height: 1;
            }
            .${GEMINI_BUTTON_CLASS}:hover {
                background-color: rgba(0, 0, 0, 0.08);
                color: #202124;
            }
            .${GEMINI_BUTTON_CLASS}:active {
                background-color: rgba(0, 0, 0, 0.12);
                transform: scale(0.95);
            }
        `;

        function createGeminiCopyButton(codeBlockElement) {
            if (codeBlockElement.classList.contains(GEMINI_PROCESSED_MARKER_CLASS)) {
                return;
            }
            // Gemini specific selector for code content, assuming 'div.code-block' is the container
            const codeContentElement = codeBlockElement.querySelector('code[data-test-id="code-content"], pre code');
            if (!codeContentElement) {
                return;
            }

            const copyButton = document.createElement('button');
            copyButton.innerHTML = ICON_HTML_COPY_GEMINI;
            copyButton.className = GEMINI_BUTTON_CLASS;
            copyButton.setAttribute('aria-label', '复制代码');

            copyButton.addEventListener('click', async (event) => {
                event.stopPropagation();
                const codeText = codeContentElement.innerText;
                try {
                    await navigator.clipboard.writeText(codeText);
                    copyButton.innerHTML = ICON_HTML_CHECK_GEMINI;
                } catch (err) {
                    alert('无法复制代码。请在浏览器设置中允许剪贴板访问,或手动复制。');
                    console.warn("Clipboard write failed: ", err);
                }
                setTimeout(() => {
                    copyButton.innerHTML = ICON_HTML_COPY_GEMINI;
                }, 2500);
            });

            let footerDiv = codeBlockElement.querySelector('.' + GEMINI_FOOTER_CLASS);
            if (!footerDiv) {
                footerDiv = document.createElement('div');
                footerDiv.className = GEMINI_FOOTER_CLASS;
                codeBlockElement.appendChild(footerDiv);
            }
            footerDiv.appendChild(copyButton);
            codeBlockElement.classList.add(GEMINI_PROCESSED_MARKER_CLASS);
        }

        function processGeminiCodeBlocks() {
            // Gemini specific selector for the code block container
            const codeBlocks = document.querySelectorAll('div.code-block');
            codeBlocks.forEach(createGeminiCopyButton);
        }

        // Initial run & observer for Gemini copy buttons
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', processGeminiCodeBlocks);
        } else {
            processGeminiCodeBlocks();
        }

        const geminiCodeBlockObserver = 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')) {
                                createGeminiCopyButton(node);
                            }
                            node.querySelectorAll('div.code-block').forEach(createGeminiCopyButton);
                        }
                    });
                }
            }
        });
        // Observe document.body as code blocks can appear anywhere.
        // Ensure this doesn't conflict with other observers if they are too broad.
        // This specific observer is fine as its callback is focused.
        geminiCodeBlockObserver.observe(document.body, { childList: true, subtree: true });
    }
    // --- End Gemini Copy Button Feature ---


    // Combine all styles and append to head
    style.textContent = commonFontStyles + platformSpecificStyles + geminiCopyButtonFeatureStyles;
    document.head.appendChild(style);


    // --- Existing Widescreen Logic ---
    // Function to apply wide mode to inline styles (especially for Gemini)
    function applyWideModeToInlineStyles() {
        if (!isGemini) return; // Only needed for Gemini widescreen part

        const elements = document.querySelectorAll('[style*="max-width"]');
        elements.forEach(el => {
            if (el.classList.contains('side-panel') || el.classList.contains('navigation-panel')) {
                return;
            }
            // Check if it's not part of the new copy button feature elements, to avoid interference
            if (el.closest(`.${GEMINI_FOOTER_CLASS}`)) {
                return;
            }
            el.style.maxWidth = '95%';
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function() {
            applyWideModeToInlineStyles();
        });
    } else {
        applyWideModeToInlineStyles();
    }

    if (isGemini) { // This is the original observer for Gemini widescreen inline styles
        const geminiWidescreenObserver = new MutationObserver(function(mutations) {
            // Consider debouncing or more targeted checks if performance issues arise
            applyWideModeToInlineStyles();
        });
        geminiWidescreenObserver.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true, // Observing attributes might be intensive
            attributeFilter: ['style', 'class'] // Filter helps
        });
    }

    if (isClaude) { // Original observer for Claude input fixes
        const claudeObserver = new MutationObserver(function(mutations) {
            if (claudeObserver.timeoutId) { // Simple debounce
                return;
            }
            claudeObserver.timeoutId = setTimeout(function() {
                const inputElements = document.querySelectorAll('textarea, [role="textbox"], div[contenteditable="true"]');
                inputElements.forEach(el => {
                    if (el && !el.dataset.widthFixedForWide) { // Use a distinct dataset property
                        // Ensure this doesn't conflict with other styles
                        // This aims to make input areas wider
                        // el.style.width = '100%'; // This might be too aggressive depending on parent
                        el.style.maxWidth = '100%'; // Allow it to fill its now wider parent
                        el.dataset.widthFixedForWide = 'true';
                    }
                });
                delete claudeObserver.timeoutId;
            }, 500);
        });

        // Observe a relevant part of the DOM for Claude.
        // Observing document.body might be too broad if form isn't always present.
        // Waiting for a more specific container might be better or using a more robust initial selector.
        function startClaudeObserver() {
            const claudeForm = document.querySelector('form[enctype="multipart/form-data"], main form, body'); // Try to find a form, or fall back to body
            if (claudeForm) {
                 claudeObserver.observe(claudeForm, {
                    childList: true,
                    subtree: true,
                    attributes: false // Less intensive
                });
            } else {
                // Fallback if form isn't found immediately, maybe try again or observe body
                // For now, this means if the form isn't there, observer might not start effectively.
            }
        }
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', startClaudeObserver);
        } else {
            startClaudeObserver();
        }
    }
})();