Gemini &Gemini & chatgpt

Widescreen for Claude/ChatGPT/Gemini, Claude font fix & menu button. Gemini: code block copy & collapse buttons (header & footer). | 扩展三平台布局,Claude字体及菜单按钮。Gemini:代码块复制及头部/页脚折叠按钮。

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

// ==UserScript==
// @name         Gemini &Gemini & chatgpt
// @namespace    https://example.com/aiHelp
// @match        *://claude.ai/*
// @match        *://chatgpt.com/*
// @match        *://gemini.google.com/*
// @match        https://gemini.google.com/app/*
// @version      1.3.2
// @author       cores
// @license      MIT
// @description  Widescreen for Claude/ChatGPT/Gemini, Claude font fix & menu button. Gemini: code block copy & collapse buttons (header & footer). | 扩展三平台布局,Claude字体及菜单按钮。Gemini:代码块复制及头部/页脚折叠按钮。
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    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');

    const style = document.createElement('style');
    const commonFontStyles = `
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
        }
    `;

    let platformSpecificStyles = '';
    let geminiFeaturesStyles = '';
    let claudeSpecificJsExecuted = false;

    // --- Platform Specific Styles (保持不变) ---
    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\\] { height: auto !important; }`;
    }

    // --- Gemini Features (Copy Button & Collapse Buttons) ---
    if (isGemini) {
        const GEMINI_CODE_BLOCK_SELECTOR = 'div.code-block';
        const GEMINI_CODE_CONTENT_SELECTOR = 'code[data-test-id="code-content"], pre code';
        const GEMINI_CUSTOM_COPY_BUTTON_CLASS = 'gemini-custom-md-icon-copy-button'; // For custom copy button in footer
        const GEMINI_FOOTER_CLASS = 'gemini-custom-code-block-footer-centered';
        const ATTR_GEMINI_COPY_BUTTON_PROCESSED = 'data-gemini-copy-button-processed';

        const GEMINI_CODE_HEADER_SELECTOR = 'div.code-block-decoration.header-formatted.gds-title-s';
        const GEMINI_ORIGINAL_BUTTONS_CONTAINER_SELECTOR = 'div.buttons[class*="ng-star-inserted"]';
        const GEMINI_COLLAPSIBLE_PANEL_SELECTOR = '.formatted-code-block-internal-container';

        const ATTR_HEADER_COLLAPSE_PROCESSED = 'data-gemini-header-collapse-processed';
        const ATTR_FOOTER_COLLAPSE_PROCESSED = 'data-gemini-footer-collapse-processed';
        const CLASS_HEADER_COLLAPSE_BTN = 'userscript-gemini-header-collapse-btn';
        const CLASS_FOOTER_COLLAPSE_BTN = 'userscript-gemini-footer-collapse-btn';


        geminiFeaturesStyles = `
            .${GEMINI_FOOTER_CLASS} { display: flex; justify-content: center; align-items: center; padding: 8px 0px; margin-top: 8px; gap: 8px; /* Added gap for buttons in footer */ }
            .${GEMINI_CUSTOM_COPY_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; flex-wrap: wrap; transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease; outline: none; }
            .${GEMINI_CUSTOM_COPY_BUTTON_CLASS} .mat-icon { font-size: 24px; display: flex; align-items: center; justify-content: center; line-height: 1;flex-wrap:wrap; }
            .${GEMINI_CUSTOM_COPY_BUTTON_CLASS}:hover { background-color: rgba(0, 0, 0, 0.08); color: #202124; }
            .${GEMINI_CUSTOM_COPY_BUTTON_CLASS}:active { background-color: rgba(0, 0, 0, 0.12); transform: scale(0.95); }

            .${CLASS_HEADER_COLLAPSE_BTN} { margin-right: 4px; } /* Space for header collapse button */
            /* Footer collapse button will use GEMINI_CUSTOM_COPY_BUTTON_CLASS for base style, then its own class for identification */
            .${CLASS_FOOTER_COLLAPSE_BTN}.mat-mdc-icon-button { /* Ensure it matches other footer buttons if it were generic mdc */ }

            .userscript-collapsed-panel { display: none !important; }
        `;

        function createMaterialIconElement(iconName) {
            const iconElement = document.createElement('mat-icon');
            iconElement.setAttribute('role', 'img');
            iconElement.setAttribute('fonticon', iconName);
            iconElement.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
            iconElement.setAttribute('aria-hidden', 'true');
            iconElement.setAttribute('data-mat-icon-type', 'font');
            iconElement.setAttribute('data-mat-icon-name', iconName);
            iconElement.textContent = iconName;
            return iconElement;
        }

        function updateSingleCollapseButtonIcon(buttonElement, isPanelCollapsed) {
            if (!buttonElement) return;
            while (buttonElement.firstChild) buttonElement.removeChild(buttonElement.firstChild);
            if (isPanelCollapsed) {
                buttonElement.appendChild(createMaterialIconElement('keyboard_arrow_down'));
                buttonElement.setAttribute('aria-label', '展开代码块');
                buttonElement.setAttribute('mattooltip', '展开代码块');
            } else {
                buttonElement.appendChild(createMaterialIconElement('keyboard_arrow_up'));
                buttonElement.setAttribute('aria-label', '收起代码块');
                buttonElement.setAttribute('mattooltip', '收起代码块');
            }
        }

        function syncRelatedCollapseButtons(codeBlockElement, panelIsCollapsed) {
            const headerBtn = codeBlockElement.querySelector(`.${CLASS_HEADER_COLLAPSE_BTN}`);
            const footerBtn = codeBlockElement.querySelector(`.${CLASS_FOOTER_COLLAPSE_BTN}`);
            updateSingleCollapseButtonIcon(headerBtn, panelIsCollapsed);
            updateSingleCollapseButtonIcon(footerBtn, panelIsCollapsed);
        }

        function addGeminiCustomCopyButton(codeBlockElement) {
            if (codeBlockElement.getAttribute(ATTR_GEMINI_COPY_BUTTON_PROCESSED) === 'true') {
                const existingFooter = codeBlockElement.querySelector('.' + GEMINI_FOOTER_CLASS);
                const existingButton = existingFooter ? existingFooter.querySelector('.' + GEMINI_CUSTOM_COPY_BUTTON_CLASS) : null;
                return { copyButton: existingButton, footerDiv: existingFooter };
            }

            const codeContentElement = codeBlockElement.querySelector(GEMINI_CODE_CONTENT_SELECTOR);
            if (!codeContentElement) return { copyButton: null, footerDiv: null };

            const copyButton = document.createElement('button');
            copyButton.className = GEMINI_CUSTOM_COPY_BUTTON_CLASS;
            copyButton.setAttribute('aria-label', '复制代码');
            copyButton.setAttribute('title', '复制代码 (Userscript)');
            copyButton.appendChild(createMaterialIconElement('content_copy'));

            copyButton.addEventListener('click', async (event) => {
                event.stopPropagation();
                const codeText = codeContentElement.innerText;
                try {
                    await navigator.clipboard.writeText(codeText);
                    while (copyButton.firstChild) copyButton.removeChild(copyButton.firstChild);
                    copyButton.appendChild(createMaterialIconElement('check'));
                } catch (err) {
                    alert('无法复制代码。\nCould not copy code.');
                }
                setTimeout(() => {
                    if (copyButton.isConnected) {
                        while (copyButton.firstChild) copyButton.removeChild(copyButton.firstChild);
                        copyButton.appendChild(createMaterialIconElement('content_copy'));
                    }
                }, 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); // Append copy button first
            codeBlockElement.setAttribute(ATTR_GEMINI_COPY_BUTTON_PROCESSED, 'true');
            return { copyButton: copyButton, footerDiv: footerDiv };
        }

        function addGeminiHeaderCollapseButton(codeBlockElement, panelToCollapse) {
            if (codeBlockElement.getAttribute(ATTR_HEADER_COLLAPSE_PROCESSED) === 'true' || !panelToCollapse) return;

            const headerDiv = codeBlockElement.querySelector(GEMINI_CODE_HEADER_SELECTOR);
            if (!headerDiv) return;

            const existingButtonsDiv = headerDiv.querySelector(GEMINI_ORIGINAL_BUTTONS_CONTAINER_SELECTOR);
            const collapseButton = document.createElement('button');
            collapseButton.className = `mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger ${CLASS_HEADER_COLLAPSE_BTN}`;
            collapseButton.setAttribute('mat-icon-button', '');

            collapseButton.addEventListener('click', (event) => {
                event.stopPropagation();
                const isCurrentlyCollapsed = panelToCollapse.classList.toggle('userscript-collapsed-panel');
                syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed);
            });

            if (existingButtonsDiv && existingButtonsDiv.parentNode === headerDiv) {
                headerDiv.insertBefore(collapseButton, existingButtonsDiv);
            } else {
                headerDiv.prepend(collapseButton);
            }
            codeBlockElement.setAttribute(ATTR_HEADER_COLLAPSE_PROCESSED, 'true');
            // Initial icon will be set by a call to syncRelatedCollapseButtons later
        }

        function addGeminiFooterCollapseButton(codeBlockElement, panelToCollapse, footerDiv, copyButtonRef) {
            if (codeBlockElement.getAttribute(ATTR_FOOTER_COLLAPSE_PROCESSED) === 'true' || !panelToCollapse || !footerDiv) return;

            const collapseButton = document.createElement('button');
            // Use GEMINI_CUSTOM_COPY_BUTTON_CLASS as base for footer buttons for visual consistency
            collapseButton.className = `${GEMINI_CUSTOM_COPY_BUTTON_CLASS} ${CLASS_FOOTER_COLLAPSE_BTN}`;
            // No need for mat-icon-button attribute if GEMINI_CUSTOM_COPY_BUTTON_CLASS doesn't imply it and handles styling

            collapseButton.addEventListener('click', (event) => {
                event.stopPropagation();
                const isCurrentlyCollapsed = panelToCollapse.classList.toggle('userscript-collapsed-panel');
                syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed);
            });

            // Insert footer collapse button to the left of the copy button
            if (copyButtonRef && copyButtonRef.parentNode === footerDiv) {
                footerDiv.insertBefore(collapseButton, copyButtonRef);
            } else {
                footerDiv.appendChild(collapseButton); // Fallback: append if copy button isn't found in footer
            }
            codeBlockElement.setAttribute(ATTR_FOOTER_COLLAPSE_PROCESSED, 'true');
            // Initial icon will be set by a call to syncRelatedCollapseButtons later
        }


        function initializeGeminiCodeBlockFeatures(codeBlockElement) {
            const panelToCollapse = codeBlockElement.querySelector(GEMINI_COLLAPSIBLE_PANEL_SELECTOR);

            const { copyButton, footerDiv } = addGeminiCustomCopyButton(codeBlockElement);

            if (panelToCollapse) {
                addGeminiHeaderCollapseButton(codeBlockElement, panelToCollapse);
                if (footerDiv && copyButton) { // Ensure footer and copy button exist before adding footer collapse
                    addGeminiFooterCollapseButton(codeBlockElement, panelToCollapse, footerDiv, copyButton);
                }
                // Set initial state for all collapse buttons after they are potentially added
                syncRelatedCollapseButtons(codeBlockElement, panelToCollapse.classList.contains('userscript-collapsed-panel'));
            }
        }

        function observeGeminiCodeBlocks() {
            document.querySelectorAll(GEMINI_CODE_BLOCK_SELECTOR).forEach(initializeGeminiCodeBlockFeatures);
            const geminiCodeBlockObserver = new MutationObserver((mutationsList) => {
                mutationsList.forEach(mutation => {
                    if (mutation.type === 'childList') {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node.matches && node.matches(GEMINI_CODE_BLOCK_SELECTOR)) {
                                    initializeGeminiCodeBlockFeatures(node);
                                }
                                node.querySelectorAll(GEMINI_CODE_BLOCK_SELECTOR).forEach(initializeGeminiCodeBlockFeatures);
                            }
                        });
                    }
                });
            });
            geminiCodeBlockObserver.observe(document.body, { childList: true, subtree: true });
        }

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

    // --- Claude Specific JavaScript (保持不变) ---
    if (isClaude && !claudeSpecificJsExecuted) {
        const CLAUDE_USER_MENU_BUTTON_ID = 'userscript-claude-custom-button';
        const CLAUDE_SETTINGS_BUTTON_SELECTOR = 'button[data-testid="user-menu-settings"]';
        function addCustomButtonToClaudeMenu() {
            const settingsButton = document.querySelector(CLAUDE_SETTINGS_BUTTON_SELECTOR);
            if (!settingsButton) return;
            const menu = settingsButton.closest('div[role="menu"], div[data-radix-menu-content]');
            if (!menu || document.getElementById(CLAUDE_USER_MENU_BUTTON_ID)) return;
            const newButton = document.createElement('button');
            newButton.id = CLAUDE_USER_MENU_BUTTON_ID;
            newButton.className = settingsButton.className;
            newButton.setAttribute('role', 'menuitem');
            newButton.setAttribute('tabindex', '-1');
            newButton.setAttribute('data-orientation', 'vertical');
            newButton.textContent = "recents";
            newButton.addEventListener('click', () => { window.location.href = 'https://claude.ai/recents'; });
            if (settingsButton.parentNode) settingsButton.parentNode.insertBefore(newButton, settingsButton.nextSibling);
        }
        const claudeMenuObserver = new MutationObserver(addCustomButtonToClaudeMenu);
        claudeMenuObserver.observe(document.body, { childList: true, subtree: true });
        let claudeInputObserver;
        function startClaudeInputObserver() {
            if (claudeInputObserver) return;
            claudeInputObserver = new MutationObserver(function() {
                if (claudeInputObserver.timeoutId) return;
                claudeInputObserver.timeoutId = setTimeout(function() {
                    document.querySelectorAll('textarea, [role="textbox"], div[contenteditable="true"]').forEach(el => {
                        if (el && !el.dataset.widthFixedForWide) { el.style.maxWidth = '100%'; el.dataset.widthFixedForWide = 'true'; }
                    });
                    delete claudeInputObserver.timeoutId;
                }, 500);
            });
            const claudeForm = document.querySelector('form[enctype="multipart/form-data"], main form, body');
            if (claudeForm) claudeInputObserver.observe(claudeForm, { childList: true, subtree: true, attributes: false });
        }
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', startClaudeInputObserver);
        else startClaudeInputObserver();
        claudeSpecificJsExecuted = true;
    }

    // --- Append all styles to head ---
    style.textContent = commonFontStyles + platformSpecificStyles + geminiFeaturesStyles;
    document.head.appendChild(style);

    // --- Widescreen Logic for inline styles (Gemini - 保持不变) ---
    function applyWideModeToInlineStyles() {
        if (!isGemini) return;
        const elements = document.querySelectorAll('[style*="max-width"]');
        elements.forEach(el => {
            if (el.classList.contains('side-panel') || el.classList.contains('navigation-panel')) return;
            if (typeof GEMINI_FOOTER_CLASS !== 'undefined' && el.closest(`.${GEMINI_FOOTER_CLASS}`)) return;
            el.style.maxWidth = '95%';
        });
    }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyWideModeToInlineStyles);
    else applyWideModeToInlineStyles();
    if (isGemini) {
        const geminiWidescreenObserver = new MutationObserver(applyWideModeToInlineStyles);
        geminiWidescreenObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
    }

})();