您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Widescreen for Claude/ChatGPT/Gemini, Claude font fix & menu button. Gemini: code block copy & collapse buttons (header & footer). | 扩展三平台布局,Claude字体及菜单按钮。Gemini:代码块复制及头部/页脚折叠按钮。(重构版)
当前为
// ==UserScript== // @name Gemini & Claude & ChatGPT // @namespace https://example.com/aiHelp // @match *://claude.ai/* // @match *://chatgpt.com/* // @match *://gemini.google.com/* // @match https://gemini.google.com/app/* // @version 1.4.1 // @author cores (Refactored by Gemini AI) // @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 PLATFORM_HOSTS = { GEMINI: 'gemini.google.com', CLAUDE: 'claude.ai', CHATGPT: 'chatgpt.com' }; // Gemini 相关选择器和类名 const GEMINI_SELECTORS = { CODE_BLOCK: 'div.code-block', CODE_CONTENT: 'code[data-test-id="code-content"], pre code', CODE_HEADER: 'div.code-block-decoration.header-formatted.gds-title-s', ORIGINAL_BUTTONS_CONTAINER: 'div.buttons[class*="ng-star-inserted"]', COLLAPSIBLE_PANEL: '.formatted-code-block-internal-container', SIDE_PANEL: '.side-panel', // 用于排除宽屏调整 NAVIGATION_PANEL: '.navigation-panel', // 用于排除宽屏调整 QUERY_TEXT_COLLAPSED: 'div.query-text.gds-body-l.collapsed' // 新增:折叠的查询文本选择器 }; const GEMINI_CLASSES = { CUSTOM_COPY_BUTTON: 'userscript-gemini-custom-copy-button', FOOTER_CONTAINER: 'userscript-gemini-code-block-footer', HEADER_COLLAPSE_BUTTON: 'userscript-gemini-header-collapse-btn', FOOTER_COLLAPSE_BUTTON: 'userscript-gemini-footer-collapse-btn', COLLAPSED_PANEL: 'userscript-collapsed-panel', QUERY_TEXT_BUTTONS_CONTAINER: 'userscript-gemini-query-text-buttons', // 新增:查询文本按钮容器类 QUERY_TEXT_COPY_BUTTON: 'userscript-gemini-query-text-copy-button' // 新增:查询文本复制按钮类 }; const GEMINI_ATTRIBUTES = { COPY_BUTTON_PROCESSED: 'data-userscript-copy-processed', HEADER_COLLAPSE_PROCESSED: 'data-userscript-header-collapse-processed', FOOTER_COLLAPSE_PROCESSED: 'data-userscript-footer-collapse-processed', QUERY_TEXT_COPY_PROCESSED: 'data-userscript-query-text-copy-processed' // 新增:查询文本复制处理标记 }; // Claude 相关选择器和ID const CLAUDE_SELECTORS = { SETTINGS_BUTTON: 'button[data-testid="user-menu-settings"]', MENU_CONTAINER: 'div[role="menu"], div[data-radix-menu-content]', TEXT_INPUT_ELEMENTS: 'textarea, [role="textbox"], div[contenteditable="true"]', FORM_CONTAINER: 'form[enctype="multipart/form-data"], main form, body' }; const CLAUDE_IDS = { CUSTOM_MENU_BUTTON: 'userscript-claude-custom-recents-button' }; const CLAUDE_ATTRIBUTES = { WIDTH_FIXED: 'data-userscript-width-fixed' }; // --- 工具函数 --- const Utils = { /** * 创建 Material Design 图标元素 (用于 Gemini) * @param {string} iconName - 图标名称 (例如 'content_copy') * @returns {HTMLElement} mat-icon 元素 */ createMaterialIconElement: function(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; }, /** * 向文档头部添加样式 * @param {string} cssString - 要添加的 CSS 字符串 * @returns {HTMLStyleElement} 添加的 style 元素 */ addStylesToHead: function(cssString) { const styleElement = document.createElement('style'); styleElement.textContent = cssString; document.head.appendChild(styleElement); return styleElement; }, /** * 观察 DOM 变化并执行回调 * @param {Node} targetNode - 被观察的 DOM 节点 * @param {MutationObserverInit} observerOptions - MutationObserver 的配置选项 * @param {Function} callback - 当检测到变化时执行的回调函数 * @returns {MutationObserver} 创建的 MutationObserver 实例 */ observeDOMChanges: function(targetNode, observerOptions, callback) { const observer = new MutationObserver(callback); observer.observe(targetNode, observerOptions); return observer; } }; // --- 样式管理器 --- const StyleManager = { commonStyles: ` body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; } `, platformStyles: '', featureStyles: '', /** * 添加平台特定的 CSS * @param {string} css - 平台 CSS */ addPlatformStyles: function(css) { this.platformStyles += css; }, /** * 添加功能特定的 CSS (例如 Gemini 按钮) * @param {string} css - 功能 CSS */ addFeatureStyles: function(css) { this.featureStyles += css; }, /** * 将所有收集到的样式应用到文档 */ applyAllStyles: function() { Utils.addStylesToHead(this.commonStyles + this.platformStyles + this.featureStyles); } }; // --- 平台模块 --- // Gemini 模块 const GeminiModule = { isCurrent: window.location.hostname.includes(PLATFORM_HOSTS.GEMINI), copyButtonProcessedAttr: GEMINI_ATTRIBUTES.COPY_BUTTON_PROCESSED, headerCollapseProcessedAttr: GEMINI_ATTRIBUTES.HEADER_COLLAPSE_PROCESSED, footerCollapseProcessedAttr: GEMINI_ATTRIBUTES.FOOTER_COLLAPSE_PROCESSED, queryTextCopyProcessedAttr: GEMINI_ATTRIBUTES.QUERY_TEXT_COPY_PROCESSED, // 新增属性 /** * 获取 Gemini 平台的特定样式 * @returns {string} CSS 字符串 */ getStyles: function() { return ` /* Gemini 宽屏 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; } /* Gemini 代码块功能 CSS */ .${GEMINI_CLASSES.FOOTER_CONTAINER} { display: flex; justify-content: center; align-items: center; padding: 8px 0px; margin-top: 8px; gap: 8px; /* 页脚按钮之间的间距 */ } .${GEMINI_CLASSES.CUSTOM_COPY_BUTTON} { /* 代码块复制按钮和页脚折叠按钮的基础样式 */ background-color: transparent; color: #5f6368; /* Google 图标颜色 */ border: none; padding: 0; width: 40px; /* Material Design 图标按钮标准尺寸 */ 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_CLASSES.CUSTOM_COPY_BUTTON} .mat-icon { font-size: 24px; /* Material Design 图标标准尺寸 */ display: flex; align-items: center; justify-content:center; line-height: 1; flex-wrap:wrap; } .${GEMINI_CLASSES.CUSTOM_COPY_BUTTON}:hover { background-color: rgba(0, 0, 0, 0.08); /* Material Design 悬停效果 */ color: #202124; /* 深色图标悬停颜色 */ } .${GEMINI_CLASSES.CUSTOM_COPY_BUTTON}:active { background-color: rgba(0, 0, 0, 0.12); /* Material Design 点击效果 */ transform: scale(0.95); } .${GEMINI_CLASSES.HEADER_COLLAPSE_BUTTON} { margin-right: 4px; /* 头部折叠按钮与原始按钮的间距 */ } .${GEMINI_CLASSES.COLLAPSED_PANEL} { display: none !important; /* 隐藏折叠的内容 */ } /* Gemini 查询文本复制按钮的容器样式 - 模仿原生 "buttons" div */ .${GEMINI_CLASSES.QUERY_TEXT_BUTTONS_CONTAINER} { display: flex; /* Or 'inline-flex' if needed */ align-items: center; margin-left: 8px; /* 与查询文本的间距 */ flex-direction:column; } `; }, /** * 更新单个折叠按钮的图标和提示文本 * @param {HTMLElement} buttonElement - 按钮元素 * @param {boolean} isPanelCollapsed - 面板是否已折叠 */ updateSingleCollapseButtonIcon: function(buttonElement, isPanelCollapsed) { if (!buttonElement) return; while (buttonElement.firstChild) buttonElement.removeChild(buttonElement.firstChild); // 清空现有图标 const iconName = isPanelCollapsed ? 'keyboard_arrow_down' : 'keyboard_arrow_up'; const label = isPanelCollapsed ? '展开代码块 (Userscript)' : '收起代码块 (Userscript)'; buttonElement.appendChild(Utils.createMaterialIconElement(iconName)); buttonElement.setAttribute('aria-label', label); buttonElement.setAttribute('mattooltip', label); // Gemini 使用 mattooltip buttonElement.setAttribute('title', label); // 备用 title }, /** * 同步代码块头部和底部相关折叠按钮的图标状态 * @param {HTMLElement} codeBlockElement - 代码块元素 * @param {boolean} panelIsCollapsed - 面板是否已折叠 */ syncRelatedCollapseButtons: function(codeBlockElement, panelIsCollapsed) { const headerBtn = codeBlockElement.querySelector(`.${GEMINI_CLASSES.HEADER_COLLAPSE_BUTTON}`); const footerBtn = codeBlockElement.querySelector(`.${GEMINI_CLASSES.FOOTER_COLLAPSE_BUTTON}`); this.updateSingleCollapseButtonIcon(headerBtn, panelIsCollapsed); this.updateSingleCollapseButtonIcon(footerBtn, panelIsCollapsed); }, /** * 为 Gemini 代码块添加自定义复制按钮 * @param {HTMLElement} codeBlockElement - 代码块元素 * @returns {{copyButton: HTMLElement|null, footerDiv: HTMLElement|null}} 包含复制按钮和页脚容器的对象 */ addCustomCopyButton: function(codeBlockElement) { if (codeBlockElement.getAttribute(this.copyButtonProcessedAttr) === 'true') { const existingFooter = codeBlockElement.querySelector('.' + GEMINI_CLASSES.FOOTER_CONTAINER); const existingButton = existingFooter ? existingFooter.querySelector('.' + GEMINI_CLASSES.CUSTOM_COPY_BUTTON) : null; return { copyButton: existingButton, footerDiv: existingFooter }; } const codeContentElement = codeBlockElement.querySelector(GEMINI_SELECTORS.CODE_CONTENT); if (!codeContentElement) return { copyButton: null, footerDiv: null }; const copyButton = document.createElement('button'); copyButton.className = GEMINI_CLASSES.CUSTOM_COPY_BUTTON; // 使用通用样式 copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('title', '复制代码 (Userscript)'); const icon = Utils.createMaterialIconElement('content_copy'); copyButton.appendChild(icon); copyButton.addEventListener('click', async (event) => { event.stopPropagation(); // 防止事件冒泡 const codeText = codeContentElement.innerText; try { await navigator.clipboard.writeText(codeText); icon.textContent = 'check'; copyButton.setAttribute('aria-label', '已复制 (Userscript)'); copyButton.setAttribute('mattooltip', '已复制 (Userscript)'); } catch (err) { console.error('Userscript: 无法复制代码块', err); icon.textContent = 'error_outline'; copyButton.setAttribute('aria-label', '复制失败 (Userscript)'); copyButton.setAttribute('mattooltip', '复制失败 (Userscript)'); // 不在此处恢复图标,让用户看到错误状态,成功时才自动恢复 setTimeout(() => { // 错误状态也延时恢复,避免快速闪烁 if (icon.isConnected) { icon.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2500); return; } setTimeout(() => { if (icon.isConnected) { icon.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2500); }); let footerDiv = codeBlockElement.querySelector('.' + GEMINI_CLASSES.FOOTER_CONTAINER); if (!footerDiv) { footerDiv = document.createElement('div'); footerDiv.className = GEMINI_CLASSES.FOOTER_CONTAINER; const panel = codeBlockElement.querySelector(GEMINI_SELECTORS.COLLAPSIBLE_PANEL); if (panel && panel.nextSibling) { panel.parentNode.insertBefore(footerDiv, panel.nextSibling); } else if (panel) { panel.parentNode.appendChild(footerDiv); } else { codeBlockElement.appendChild(footerDiv); } } footerDiv.appendChild(copyButton); codeBlockElement.setAttribute(this.copyButtonProcessedAttr, 'true'); return { copyButton: copyButton, footerDiv: footerDiv }; }, /** * 为 Gemini 代码块头部添加折叠/展开按钮 * @param {HTMLElement} codeBlockElement - 代码块元素 * @param {HTMLElement} panelToCollapse - 需要折叠/展开的面板元素 */ addHeaderCollapseButton: function(codeBlockElement, panelToCollapse) { if (codeBlockElement.getAttribute(this.headerCollapseProcessedAttr) === 'true' || !panelToCollapse) return; const headerDiv = codeBlockElement.querySelector(GEMINI_SELECTORS.CODE_HEADER); if (!headerDiv) return; const collapseButton = document.createElement('button'); collapseButton.className = `mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger ${GEMINI_CLASSES.HEADER_COLLAPSE_BUTTON}`; collapseButton.setAttribute('mat-icon-button', ''); collapseButton.addEventListener('click', (event) => { event.stopPropagation(); const isCurrentlyCollapsed = panelToCollapse.classList.toggle(GEMINI_CLASSES.COLLAPSED_PANEL); this.syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed); }); const existingButtonsDiv = headerDiv.querySelector(GEMINI_SELECTORS.ORIGINAL_BUTTONS_CONTAINER); if (existingButtonsDiv && existingButtonsDiv.parentNode === headerDiv) { headerDiv.insertBefore(collapseButton, existingButtonsDiv); } else { headerDiv.prepend(collapseButton); } codeBlockElement.setAttribute(this.headerCollapseProcessedAttr, 'true'); }, /** * 为 Gemini 代码块页脚添加折叠/展开按钮 * @param {HTMLElement} codeBlockElement - 代码块元素 * @param {HTMLElement} panelToCollapse - 需要折叠/展开的面板元素 * @param {HTMLElement} footerDiv - 页脚容器元素 * @param {HTMLElement} copyButtonRef - 复制按钮的引用,用于定位 */ addFooterCollapseButton: function(codeBlockElement, panelToCollapse, footerDiv, copyButtonRef) { if (codeBlockElement.getAttribute(this.footerCollapseProcessedAttr) === 'true' || !panelToCollapse || !footerDiv) return; const collapseButton = document.createElement('button'); collapseButton.className = `${GEMINI_CLASSES.CUSTOM_COPY_BUTTON} ${GEMINI_CLASSES.FOOTER_COLLAPSE_BUTTON}`; collapseButton.addEventListener('click', (event) => { event.stopPropagation(); const isCurrentlyCollapsed = panelToCollapse.classList.toggle(GEMINI_CLASSES.COLLAPSED_PANEL); this.syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed); }); if (copyButtonRef && copyButtonRef.parentNode === footerDiv) { footerDiv.insertBefore(collapseButton, copyButtonRef); } else { footerDiv.appendChild(collapseButton); } codeBlockElement.setAttribute(this.footerCollapseProcessedAttr, 'true'); }, /** * 初始化单个 Gemini 代码块的功能 (复制、折叠) * @param {HTMLElement} codeBlockElement - 要初始化的代码块元素 */ initializeCodeBlockFeatures: function(codeBlockElement) { const panelToCollapse = codeBlockElement.querySelector(GEMINI_SELECTORS.COLLAPSIBLE_PANEL); const { copyButton, footerDiv } = this.addCustomCopyButton(codeBlockElement); if (panelToCollapse) { this.addHeaderCollapseButton(codeBlockElement, panelToCollapse); if (footerDiv && copyButton) { this.addFooterCollapseButton(codeBlockElement, panelToCollapse, footerDiv, copyButton); } this.syncRelatedCollapseButtons(codeBlockElement, panelToCollapse.classList.contains(GEMINI_CLASSES.COLLAPSED_PANEL)); } }, /** * 观察并初始化页面上所有 Gemini 代码块的功能 */ observeAndInitializeCodeBlocks: function() { document.querySelectorAll(GEMINI_SELECTORS.CODE_BLOCK).forEach(block => this.initializeCodeBlockFeatures(block)); Utils.observeDOMChanges(document.body, { childList: true, subtree: true }, (mutationsList) => { mutationsList.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches(GEMINI_SELECTORS.CODE_BLOCK)) { this.initializeCodeBlockFeatures(node); } node.querySelectorAll(GEMINI_SELECTORS.CODE_BLOCK).forEach(block => this.initializeCodeBlockFeatures(block)); } }); } }); }); }, /** * 为 Gemini 折叠的查询文本添加复制按钮 * @param {HTMLElement} queryTextElement - `query-text gds-body-l collapsed` 元素 */ addCopyButtonToQueryText: function(queryTextElement) { if (!queryTextElement || queryTextElement.getAttribute(this.queryTextCopyProcessedAttr) === 'true') { return; } if (!queryTextElement.parentNode || !queryTextElement.isConnected) { return; } const buttonsContainer = document.createElement('div'); // 使用 'buttons' 类以尝试匹配原生按钮容器的布局,并添加自定义类 buttonsContainer.className = `buttons ${GEMINI_CLASSES.QUERY_TEXT_BUTTONS_CONTAINER}`; const copyButton = document.createElement('button'); // 使用 Material Design 组件的类 copyButton.className = `mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger ${GEMINI_CLASSES.QUERY_TEXT_COPY_BUTTON}`; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); copyButton.setAttribute('title', '复制代码 (Userscript)'); // Fallback tooltip // copyButton.setAttribute('mat-icon-button', ''); // 通常由 Angular 处理,作为属性可能无效 const iconElement = Utils.createMaterialIconElement('content_copy'); copyButton.appendChild(iconElement); copyButton.addEventListener('click', async (event) => { event.stopPropagation(); const textToCopy = queryTextElement.innerText; try { await navigator.clipboard.writeText(textToCopy); iconElement.textContent = 'check'; copyButton.setAttribute('aria-label', '已复制 (Userscript)'); copyButton.setAttribute('mattooltip', '已复制 (Userscript)'); } catch (err) { console.error('Userscript: 无法复制查询文本', err); iconElement.textContent = 'error_outline'; copyButton.setAttribute('aria-label', '复制失败 (Userscript)'); copyButton.setAttribute('mattooltip', '复制失败 (Userscript)'); setTimeout(() => { if (iconElement.isConnected) { iconElement.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2000); // 错误状态也延时恢复 return; } setTimeout(() => { if (iconElement.isConnected) { iconElement.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2000); }); buttonsContainer.appendChild(copyButton); queryTextElement.parentNode.insertBefore(buttonsContainer, queryTextElement.nextSibling); queryTextElement.setAttribute(this.queryTextCopyProcessedAttr, 'true'); }, /** * 观察并为 Gemini 折叠的查询文本初始化复制按钮 */ observeAndInitializeQueryTextCopyButtons: function() { document.querySelectorAll(GEMINI_SELECTORS.QUERY_TEXT_COLLAPSED).forEach(el => this.addCopyButtonToQueryText(el)); Utils.observeDOMChanges(document.body, { childList: true, subtree: true }, (mutationsList) => { mutationsList.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches(GEMINI_SELECTORS.QUERY_TEXT_COLLAPSED)) { this.addCopyButtonToQueryText(node); } node.querySelectorAll(GEMINI_SELECTORS.QUERY_TEXT_COLLAPSED).forEach(el => this.addCopyButtonToQueryText(el)); } }); } }); }); }, /** * 应用宽屏模式到 Gemini 的内联样式元素 */ applyWidescreenToInlineStyles: function() { const elements = document.querySelectorAll('[style*="max-width"]'); elements.forEach(el => { if (el.closest(GEMINI_SELECTORS.SIDE_PANEL) || el.closest(GEMINI_SELECTORS.NAVIGATION_PANEL) || el.closest(`.${GEMINI_CLASSES.FOOTER_CONTAINER}`) || el.closest(`.${GEMINI_CLASSES.QUERY_TEXT_BUTTONS_CONTAINER}`)) { // 排除新按钮容器 return; } const currentMaxWidth = el.style.maxWidth; if (currentMaxWidth && (currentMaxWidth.includes('px') || currentMaxWidth.includes('rem') || currentMaxWidth.includes('em') || currentMaxWidth.includes('%'))) { if (parseInt(currentMaxWidth, 10) < 1200 && !currentMaxWidth.includes('95%')) { el.style.maxWidth = '95%'; } } }); }, /** * 观察并应用宽屏模式到 Gemini 的内联样式元素 */ observeAndApplyWidescreenInlineStyles: function() { this.applyWidescreenToInlineStyles(); Utils.observeDOMChanges(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }, () => this.applyWidescreenToInlineStyles()); }, /** * 初始化 Gemini 平台的所有特定功能 */ init: function() { StyleManager.addFeatureStyles(this.getStyles()); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.observeAndInitializeCodeBlocks(); this.observeAndApplyWidescreenInlineStyles(); this.observeAndInitializeQueryTextCopyButtons(); // 新增:初始化查询文本复制按钮 }); } else { this.observeAndInitializeCodeBlocks(); this.observeAndApplyWidescreenInlineStyles(); this.observeAndInitializeQueryTextCopyButtons(); // 新增:初始化查询文本复制按钮 } } }; // Claude 模块 const ClaudeModule = { isCurrent: window.location.hostname.includes(PLATFORM_HOSTS.CLAUDE), jsExecuted: false, getStyles: function() { return ` /* Claude 宽屏 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 特定字体修复 */ 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.6 !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; } `; }, addCustomButtonToMenu: function() { const settingsButton = document.querySelector(CLAUDE_SELECTORS.SETTINGS_BUTTON); if (!settingsButton) return; const menu = settingsButton.closest(CLAUDE_SELECTORS.MENU_CONTAINER); if (!menu || document.getElementById(CLAUDE_IDS.CUSTOM_MENU_BUTTON)) return; const newButton = document.createElement('button'); newButton.id = CLAUDE_IDS.CUSTOM_MENU_BUTTON; newButton.className = settingsButton.className; newButton.setAttribute('role', 'menuitem'); newButton.setAttribute('tabindex', '-1'); newButton.setAttribute('data-orientation', 'vertical'); newButton.textContent = "Recents (Userscript)"; newButton.addEventListener('click', () => { window.location.href = 'https://claude.ai/recents'; }); if (settingsButton.parentNode) settingsButton.parentNode.insertBefore(newButton, settingsButton.nextSibling); }, observeMenuForButtonAddition: function() { this.addCustomButtonToMenu(); Utils.observeDOMChanges(document.body, { childList: true, subtree: true }, () => this.addCustomButtonToMenu()); }, fixInputWidths: function() { document.querySelectorAll(CLAUDE_SELECTORS.TEXT_INPUT_ELEMENTS).forEach(el => { if (el && !el.getAttribute(CLAUDE_ATTRIBUTES.WIDTH_FIXED)) { el.style.maxWidth = '100%'; el.setAttribute(CLAUDE_ATTRIBUTES.WIDTH_FIXED, 'true'); } }); }, observeAndFixInputWidths: function() { let timeoutId; const debouncedFixWidths = () => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => this.fixInputWidths(), 300); }; this.fixInputWidths(); const formContainer = document.querySelector(CLAUDE_SELECTORS.FORM_CONTAINER) || document.body; Utils.observeDOMChanges(formContainer, { childList: true, subtree: true, attributes: false }, debouncedFixWidths); }, init: function() { if (this.jsExecuted) return; StyleManager.addPlatformStyles(this.getStyles()); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.observeMenuForButtonAddition(); this.observeAndFixInputWidths(); }); } else { this.observeMenuForButtonAddition(); this.observeAndFixInputWidths(); } this.jsExecuted = true; } }; // ChatGPT 模块 const ChatGPTModule = { isCurrent: window.location.hostname.includes(PLATFORM_HOSTS.CHATGPT), getStyles: function() { return ` /* ChatGPT 宽屏 CSS */ .text-base.gap-4.md\\:gap-6.md\\:max-w-2xl.lg\\:max-w-xl.xl\\:max-w-3xl.p-4.md\\:py-6.flex.lg\\:px-0.m-auto, .md\\:max-w-2xl.lg\\:max-w-xl.xl\\:max-w-3xl, main .text-token-text-primary .w-full .max-w-agw { max-width: 95% !important; margin-left: auto !important; margin-right: auto !important; } form .w-full { max-width: 95% !important; margin-left: auto !important; margin-right: auto !important; } .stretch { width: 100% !important; } .md\\:flex.md\\:items-end.md\\:gap-4 .w-full { max-width: 100% !important; } .relative.flex.h-full.max-w-full.flex-1.flex-col { max-width: 95% !important; margin: 0 auto !important; } `; }, init: function() { StyleManager.addPlatformStyles(this.getStyles()); } }; // --- 主逻辑:初始化和执行 --- function main() { if (GeminiModule.isCurrent) { GeminiModule.init(); } else if (ClaudeModule.isCurrent) { ClaudeModule.init(); } else if (ChatGPTModule.isCurrent) { ChatGPTModule.init(); } StyleManager.applyAllStyles(); } main(); })(); // ==UserScript== // @name Gemini & Claude & ChatGPT Enhancements (Refactored) // @namespace https://example.com/aiHelp // @match *://claude.ai/* // @match *://chatgpt.com/* // @match *://gemini.google.com/* // @match https://gemini.google.com/app/* // @version 1.4.1 // @author cores (Refactored by Gemini AI) // @license MIT // @description Widescreen for Claude/ChatGPT/Gemini, Claude font fix & menu button. Gemini: code block copy & collapse buttons (header & footer), copy button for collapsed query text. | 扩展三平台布局,Claude字体及菜单按钮。Gemini:代码块复制及头部/页脚折叠按钮,为折叠的查询文本添加复制按钮。(重构版) // @grant none // @downloadURL https://update.greasyfork.org/scripts/535343/Gemini%20Gemini%20%20chatgpt.user.js // @updateURL https://update.greasyfork.org/scripts/535343/Gemini%20Gemini%20%20chatgpt.meta.js // ==/UserScript== (function() { 'use strict'; // --- 常量定义 --- // 平台检测 const PLATFORM_HOSTS = { GEMINI: 'gemini.google.com', CLAUDE: 'claude.ai', CHATGPT: 'chatgpt.com' }; // Gemini 相关选择器和类名 const GEMINI_SELECTORS = { CODE_BLOCK: 'div.code-block', CODE_CONTENT: 'code[data-test-id="code-content"], pre code', CODE_HEADER: 'div.code-block-decoration.header-formatted.gds-title-s', ORIGINAL_BUTTONS_CONTAINER: 'div.buttons[class*="ng-star-inserted"]', COLLAPSIBLE_PANEL: '.formatted-code-block-internal-container', SIDE_PANEL: '.side-panel', // 用于排除宽屏调整 NAVIGATION_PANEL: '.navigation-panel', // 用于排除宽屏调整 QUERY_TEXT_COLLAPSED: 'div.query-text.gds-body-l.collapsed' // 新增:折叠的查询文本选择器 }; const GEMINI_CLASSES = { CUSTOM_COPY_BUTTON: 'userscript-gemini-custom-copy-button', FOOTER_CONTAINER: 'userscript-gemini-code-block-footer', HEADER_COLLAPSE_BUTTON: 'userscript-gemini-header-collapse-btn', FOOTER_COLLAPSE_BUTTON: 'userscript-gemini-footer-collapse-btn', COLLAPSED_PANEL: 'userscript-collapsed-panel', QUERY_TEXT_BUTTONS_CONTAINER: 'userscript-gemini-query-text-buttons', // 新增:查询文本按钮容器类 QUERY_TEXT_COPY_BUTTON: 'userscript-gemini-query-text-copy-button' // 新增:查询文本复制按钮类 }; const GEMINI_ATTRIBUTES = { COPY_BUTTON_PROCESSED: 'data-userscript-copy-processed', HEADER_COLLAPSE_PROCESSED: 'data-userscript-header-collapse-processed', FOOTER_COLLAPSE_PROCESSED: 'data-userscript-footer-collapse-processed', QUERY_TEXT_COPY_PROCESSED: 'data-userscript-query-text-copy-processed' // 新增:查询文本复制处理标记 }; // Claude 相关选择器和ID const CLAUDE_SELECTORS = { SETTINGS_BUTTON: 'button[data-testid="user-menu-settings"]', MENU_CONTAINER: 'div[role="menu"], div[data-radix-menu-content]', TEXT_INPUT_ELEMENTS: 'textarea, [role="textbox"], div[contenteditable="true"]', FORM_CONTAINER: 'form[enctype="multipart/form-data"], main form, body' }; const CLAUDE_IDS = { CUSTOM_MENU_BUTTON: 'userscript-claude-custom-recents-button' }; const CLAUDE_ATTRIBUTES = { WIDTH_FIXED: 'data-userscript-width-fixed' }; // --- 工具函数 --- const Utils = { /** * 创建 Material Design 图标元素 (用于 Gemini) * @param {string} iconName - 图标名称 (例如 'content_copy') * @returns {HTMLElement} mat-icon 元素 */ createMaterialIconElement: function(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; }, /** * 向文档头部添加样式 * @param {string} cssString - 要添加的 CSS 字符串 * @returns {HTMLStyleElement} 添加的 style 元素 */ addStylesToHead: function(cssString) { const styleElement = document.createElement('style'); styleElement.textContent = cssString; document.head.appendChild(styleElement); return styleElement; }, /** * 观察 DOM 变化并执行回调 * @param {Node} targetNode - 被观察的 DOM 节点 * @param {MutationObserverInit} observerOptions - MutationObserver 的配置选项 * @param {Function} callback - 当检测到变化时执行的回调函数 * @returns {MutationObserver} 创建的 MutationObserver 实例 */ observeDOMChanges: function(targetNode, observerOptions, callback) { const observer = new MutationObserver(callback); observer.observe(targetNode, observerOptions); return observer; } }; // --- 样式管理器 --- const StyleManager = { commonStyles: ` body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; } `, platformStyles: '', featureStyles: '', /** * 添加平台特定的 CSS * @param {string} css - 平台 CSS */ addPlatformStyles: function(css) { this.platformStyles += css; }, /** * 添加功能特定的 CSS (例如 Gemini 按钮) * @param {string} css - 功能 CSS */ addFeatureStyles: function(css) { this.featureStyles += css; }, /** * 将所有收集到的样式应用到文档 */ applyAllStyles: function() { Utils.addStylesToHead(this.commonStyles + this.platformStyles + this.featureStyles); } }; // --- 平台模块 --- // Gemini 模块 const GeminiModule = { isCurrent: window.location.hostname.includes(PLATFORM_HOSTS.GEMINI), copyButtonProcessedAttr: GEMINI_ATTRIBUTES.COPY_BUTTON_PROCESSED, headerCollapseProcessedAttr: GEMINI_ATTRIBUTES.HEADER_COLLAPSE_PROCESSED, footerCollapseProcessedAttr: GEMINI_ATTRIBUTES.FOOTER_COLLAPSE_PROCESSED, queryTextCopyProcessedAttr: GEMINI_ATTRIBUTES.QUERY_TEXT_COPY_PROCESSED, // 新增属性 /** * 获取 Gemini 平台的特定样式 * @returns {string} CSS 字符串 */ getStyles: function() { return ` /* Gemini 宽屏 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; } /* Gemini 代码块功能 CSS */ .${GEMINI_CLASSES.FOOTER_CONTAINER} { display: flex; justify-content: center; align-items: center; padding: 8px 0px; margin-top: 8px; gap: 8px; /* 页脚按钮之间的间距 */ } .${GEMINI_CLASSES.CUSTOM_COPY_BUTTON} { /* 代码块复制按钮和页脚折叠按钮的基础样式 */ background-color: transparent; color: #5f6368; /* Google 图标颜色 */ border: none; padding: 0; width: 40px; /* Material Design 图标按钮标准尺寸 */ 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_CLASSES.CUSTOM_COPY_BUTTON} .mat-icon { font-size: 24px; /* Material Design 图标标准尺寸 */ display: flex; align-items: center; justify-content: center; line-height: 1; } .${GEMINI_CLASSES.CUSTOM_COPY_BUTTON}:hover { background-color: rgba(0, 0, 0, 0.08); /* Material Design 悬停效果 */ color: #202124; /* 深色图标悬停颜色 */ } .${GEMINI_CLASSES.CUSTOM_COPY_BUTTON}:active { background-color: rgba(0, 0, 0, 0.12); /* Material Design 点击效果 */ transform: scale(0.95); } .${GEMINI_CLASSES.HEADER_COLLAPSE_BUTTON} { margin-right: 4px; /* 头部折叠按钮与原始按钮的间距 */ } .${GEMINI_CLASSES.COLLAPSED_PANEL} { display: none !important; /* 隐藏折叠的内容 */ } /* Gemini 查询文本复制按钮的容器样式 - 模仿原生 "buttons" div */ .${GEMINI_CLASSES.QUERY_TEXT_BUTTONS_CONTAINER} { display: flex; /* Or 'inline-flex' if needed */ align-items: center; margin-left: 8px; /* 与查询文本的间距 */ } `; }, /** * 更新单个折叠按钮的图标和提示文本 * @param {HTMLElement} buttonElement - 按钮元素 * @param {boolean} isPanelCollapsed - 面板是否已折叠 */ updateSingleCollapseButtonIcon: function(buttonElement, isPanelCollapsed) { if (!buttonElement) return; while (buttonElement.firstChild) buttonElement.removeChild(buttonElement.firstChild); // 清空现有图标 const iconName = isPanelCollapsed ? 'keyboard_arrow_down' : 'keyboard_arrow_up'; const label = isPanelCollapsed ? '展开代码块 (Userscript)' : '收起代码块 (Userscript)'; buttonElement.appendChild(Utils.createMaterialIconElement(iconName)); buttonElement.setAttribute('aria-label', label); buttonElement.setAttribute('mattooltip', label); // Gemini 使用 mattooltip buttonElement.setAttribute('title', label); // 备用 title }, /** * 同步代码块头部和底部相关折叠按钮的图标状态 * @param {HTMLElement} codeBlockElement - 代码块元素 * @param {boolean} panelIsCollapsed - 面板是否已折叠 */ syncRelatedCollapseButtons: function(codeBlockElement, panelIsCollapsed) { const headerBtn = codeBlockElement.querySelector(`.${GEMINI_CLASSES.HEADER_COLLAPSE_BUTTON}`); const footerBtn = codeBlockElement.querySelector(`.${GEMINI_CLASSES.FOOTER_COLLAPSE_BUTTON}`); this.updateSingleCollapseButtonIcon(headerBtn, panelIsCollapsed); this.updateSingleCollapseButtonIcon(footerBtn, panelIsCollapsed); }, /** * 为 Gemini 代码块添加自定义复制按钮 * @param {HTMLElement} codeBlockElement - 代码块元素 * @returns {{copyButton: HTMLElement|null, footerDiv: HTMLElement|null}} 包含复制按钮和页脚容器的对象 */ addCustomCopyButton: function(codeBlockElement) { if (codeBlockElement.getAttribute(this.copyButtonProcessedAttr) === 'true') { const existingFooter = codeBlockElement.querySelector('.' + GEMINI_CLASSES.FOOTER_CONTAINER); const existingButton = existingFooter ? existingFooter.querySelector('.' + GEMINI_CLASSES.CUSTOM_COPY_BUTTON) : null; return { copyButton: existingButton, footerDiv: existingFooter }; } const codeContentElement = codeBlockElement.querySelector(GEMINI_SELECTORS.CODE_CONTENT); if (!codeContentElement) return { copyButton: null, footerDiv: null }; const copyButton = document.createElement('button'); copyButton.className = GEMINI_CLASSES.CUSTOM_COPY_BUTTON; // 使用通用样式 copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('title', '复制代码 (Userscript)'); const icon = Utils.createMaterialIconElement('content_copy'); copyButton.appendChild(icon); copyButton.addEventListener('click', async (event) => { event.stopPropagation(); // 防止事件冒泡 const codeText = codeContentElement.innerText; try { await navigator.clipboard.writeText(codeText); icon.textContent = 'check'; copyButton.setAttribute('aria-label', '已复制 (Userscript)'); copyButton.setAttribute('mattooltip', '已复制 (Userscript)'); } catch (err) { console.error('Userscript: 无法复制代码块', err); icon.textContent = 'error_outline'; copyButton.setAttribute('aria-label', '复制失败 (Userscript)'); copyButton.setAttribute('mattooltip', '复制失败 (Userscript)'); // 不在此处恢复图标,让用户看到错误状态,成功时才自动恢复 setTimeout(() => { // 错误状态也延时恢复,避免快速闪烁 if (icon.isConnected) { icon.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2500); return; } setTimeout(() => { if (icon.isConnected) { icon.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2500); }); let footerDiv = codeBlockElement.querySelector('.' + GEMINI_CLASSES.FOOTER_CONTAINER); if (!footerDiv) { footerDiv = document.createElement('div'); footerDiv.className = GEMINI_CLASSES.FOOTER_CONTAINER; const panel = codeBlockElement.querySelector(GEMINI_SELECTORS.COLLAPSIBLE_PANEL); if (panel && panel.nextSibling) { panel.parentNode.insertBefore(footerDiv, panel.nextSibling); } else if (panel) { panel.parentNode.appendChild(footerDiv); } else { codeBlockElement.appendChild(footerDiv); } } footerDiv.appendChild(copyButton); codeBlockElement.setAttribute(this.copyButtonProcessedAttr, 'true'); return { copyButton: copyButton, footerDiv: footerDiv }; }, /** * 为 Gemini 代码块头部添加折叠/展开按钮 * @param {HTMLElement} codeBlockElement - 代码块元素 * @param {HTMLElement} panelToCollapse - 需要折叠/展开的面板元素 */ addHeaderCollapseButton: function(codeBlockElement, panelToCollapse) { if (codeBlockElement.getAttribute(this.headerCollapseProcessedAttr) === 'true' || !panelToCollapse) return; const headerDiv = codeBlockElement.querySelector(GEMINI_SELECTORS.CODE_HEADER); if (!headerDiv) return; const collapseButton = document.createElement('button'); collapseButton.className = `mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger ${GEMINI_CLASSES.HEADER_COLLAPSE_BUTTON}`; collapseButton.setAttribute('mat-icon-button', ''); collapseButton.addEventListener('click', (event) => { event.stopPropagation(); const isCurrentlyCollapsed = panelToCollapse.classList.toggle(GEMINI_CLASSES.COLLAPSED_PANEL); this.syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed); }); const existingButtonsDiv = headerDiv.querySelector(GEMINI_SELECTORS.ORIGINAL_BUTTONS_CONTAINER); if (existingButtonsDiv && existingButtonsDiv.parentNode === headerDiv) { headerDiv.insertBefore(collapseButton, existingButtonsDiv); } else { headerDiv.prepend(collapseButton); } codeBlockElement.setAttribute(this.headerCollapseProcessedAttr, 'true'); }, /** * 为 Gemini 代码块页脚添加折叠/展开按钮 * @param {HTMLElement} codeBlockElement - 代码块元素 * @param {HTMLElement} panelToCollapse - 需要折叠/展开的面板元素 * @param {HTMLElement} footerDiv - 页脚容器元素 * @param {HTMLElement} copyButtonRef - 复制按钮的引用,用于定位 */ addFooterCollapseButton: function(codeBlockElement, panelToCollapse, footerDiv, copyButtonRef) { if (codeBlockElement.getAttribute(this.footerCollapseProcessedAttr) === 'true' || !panelToCollapse || !footerDiv) return; const collapseButton = document.createElement('button'); collapseButton.className = `${GEMINI_CLASSES.CUSTOM_COPY_BUTTON} ${GEMINI_CLASSES.FOOTER_COLLAPSE_BUTTON}`; collapseButton.addEventListener('click', (event) => { event.stopPropagation(); const isCurrentlyCollapsed = panelToCollapse.classList.toggle(GEMINI_CLASSES.COLLAPSED_PANEL); this.syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed); }); if (copyButtonRef && copyButtonRef.parentNode === footerDiv) { footerDiv.insertBefore(collapseButton, copyButtonRef); } else { footerDiv.appendChild(collapseButton); } codeBlockElement.setAttribute(this.footerCollapseProcessedAttr, 'true'); }, /** * 初始化单个 Gemini 代码块的功能 (复制、折叠) * @param {HTMLElement} codeBlockElement - 要初始化的代码块元素 */ initializeCodeBlockFeatures: function(codeBlockElement) { const panelToCollapse = codeBlockElement.querySelector(GEMINI_SELECTORS.COLLAPSIBLE_PANEL); const { copyButton, footerDiv } = this.addCustomCopyButton(codeBlockElement); if (panelToCollapse) { this.addHeaderCollapseButton(codeBlockElement, panelToCollapse); if (footerDiv && copyButton) { this.addFooterCollapseButton(codeBlockElement, panelToCollapse, footerDiv, copyButton); } this.syncRelatedCollapseButtons(codeBlockElement, panelToCollapse.classList.contains(GEMINI_CLASSES.COLLAPSED_PANEL)); } }, /** * 观察并初始化页面上所有 Gemini 代码块的功能 */ observeAndInitializeCodeBlocks: function() { document.querySelectorAll(GEMINI_SELECTORS.CODE_BLOCK).forEach(block => this.initializeCodeBlockFeatures(block)); Utils.observeDOMChanges(document.body, { childList: true, subtree: true }, (mutationsList) => { mutationsList.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches(GEMINI_SELECTORS.CODE_BLOCK)) { this.initializeCodeBlockFeatures(node); } node.querySelectorAll(GEMINI_SELECTORS.CODE_BLOCK).forEach(block => this.initializeCodeBlockFeatures(block)); } }); } }); }); }, /** * 为 Gemini 折叠的查询文本添加复制按钮 * @param {HTMLElement} queryTextElement - `query-text gds-body-l collapsed` 元素 */ addCopyButtonToQueryText: function(queryTextElement) { if (!queryTextElement || queryTextElement.getAttribute(this.queryTextCopyProcessedAttr) === 'true') { return; } if (!queryTextElement.parentNode || !queryTextElement.isConnected) { return; } const buttonsContainer = document.createElement('div'); // 使用 'buttons' 类以尝试匹配原生按钮容器的布局,并添加自定义类 buttonsContainer.className = `buttons ${GEMINI_CLASSES.QUERY_TEXT_BUTTONS_CONTAINER}`; const copyButton = document.createElement('button'); // 使用 Material Design 组件的类 copyButton.className = `mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger ${GEMINI_CLASSES.QUERY_TEXT_COPY_BUTTON}`; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); copyButton.setAttribute('title', '复制代码 (Userscript)'); // Fallback tooltip // copyButton.setAttribute('mat-icon-button', ''); // 通常由 Angular 处理,作为属性可能无效 const iconElement = Utils.createMaterialIconElement('content_copy'); copyButton.appendChild(iconElement); copyButton.addEventListener('click', async (event) => { event.stopPropagation(); const textToCopy = queryTextElement.innerText; try { await navigator.clipboard.writeText(textToCopy); iconElement.textContent = 'check'; copyButton.setAttribute('aria-label', '已复制 (Userscript)'); copyButton.setAttribute('mattooltip', '已复制 (Userscript)'); } catch (err) { console.error('Userscript: 无法复制查询文本', err); iconElement.textContent = 'error_outline'; copyButton.setAttribute('aria-label', '复制失败 (Userscript)'); copyButton.setAttribute('mattooltip', '复制失败 (Userscript)'); setTimeout(() => { if (iconElement.isConnected) { iconElement.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2000); // 错误状态也延时恢复 return; } setTimeout(() => { if (iconElement.isConnected) { iconElement.textContent = 'content_copy'; copyButton.setAttribute('aria-label', '复制代码 (Userscript)'); copyButton.setAttribute('mattooltip', '复制代码 (Userscript)'); } }, 2000); }); buttonsContainer.appendChild(copyButton); queryTextElement.parentNode.insertBefore(buttonsContainer, queryTextElement.nextSibling); queryTextElement.setAttribute(this.queryTextCopyProcessedAttr, 'true'); }, /** * 观察并为 Gemini 折叠的查询文本初始化复制按钮 */ observeAndInitializeQueryTextCopyButtons: function() { document.querySelectorAll(GEMINI_SELECTORS.QUERY_TEXT_COLLAPSED).forEach(el => this.addCopyButtonToQueryText(el)); Utils.observeDOMChanges(document.body, { childList: true, subtree: true }, (mutationsList) => { mutationsList.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches(GEMINI_SELECTORS.QUERY_TEXT_COLLAPSED)) { this.addCopyButtonToQueryText(node); } node.querySelectorAll(GEMINI_SELECTORS.QUERY_TEXT_COLLAPSED).forEach(el => this.addCopyButtonToQueryText(el)); } }); } }); }); }, /** * 应用宽屏模式到 Gemini 的内联样式元素 */ applyWidescreenToInlineStyles: function() { const elements = document.querySelectorAll('[style*="max-width"]'); elements.forEach(el => { if (el.closest(GEMINI_SELECTORS.SIDE_PANEL) || el.closest(GEMINI_SELECTORS.NAVIGATION_PANEL) || el.closest(`.${GEMINI_CLASSES.FOOTER_CONTAINER}`) || el.closest(`.${GEMINI_CLASSES.QUERY_TEXT_BUTTONS_CONTAINER}`)) { // 排除新按钮容器 return; } const currentMaxWidth = el.style.maxWidth; if (currentMaxWidth && (currentMaxWidth.includes('px') || currentMaxWidth.includes('rem') || currentMaxWidth.includes('em') || currentMaxWidth.includes('%'))) { if (parseInt(currentMaxWidth, 10) < 1200 && !currentMaxWidth.includes('95%')) { el.style.maxWidth = '95%'; } } }); }, /** * 观察并应用宽屏模式到 Gemini 的内联样式元素 */ observeAndApplyWidescreenInlineStyles: function() { this.applyWidescreenToInlineStyles(); Utils.observeDOMChanges(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }, () => this.applyWidescreenToInlineStyles()); }, /** * 初始化 Gemini 平台的所有特定功能 */ init: function() { StyleManager.addFeatureStyles(this.getStyles()); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.observeAndInitializeCodeBlocks(); this.observeAndApplyWidescreenInlineStyles(); this.observeAndInitializeQueryTextCopyButtons(); // 新增:初始化查询文本复制按钮 }); } else { this.observeAndInitializeCodeBlocks(); this.observeAndApplyWidescreenInlineStyles(); this.observeAndInitializeQueryTextCopyButtons(); // 新增:初始化查询文本复制按钮 } } }; // Claude 模块 const ClaudeModule = { isCurrent: window.location.hostname.includes(PLATFORM_HOSTS.CLAUDE), jsExecuted: false, getStyles: function() { return ` /* Claude 宽屏 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 特定字体修复 */ 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.6 !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; } `; }, addCustomButtonToMenu: function() { const settingsButton = document.querySelector(CLAUDE_SELECTORS.SETTINGS_BUTTON); if (!settingsButton) return; const menu = settingsButton.closest(CLAUDE_SELECTORS.MENU_CONTAINER); if (!menu || document.getElementById(CLAUDE_IDS.CUSTOM_MENU_BUTTON)) return; const newButton = document.createElement('button'); newButton.id = CLAUDE_IDS.CUSTOM_MENU_BUTTON; newButton.className = settingsButton.className; newButton.setAttribute('role', 'menuitem'); newButton.setAttribute('tabindex', '-1'); newButton.setAttribute('data-orientation', 'vertical'); newButton.textContent = "Recents (Userscript)"; newButton.addEventListener('click', () => { window.location.href = 'https://claude.ai/recents'; }); if (settingsButton.parentNode) settingsButton.parentNode.insertBefore(newButton, settingsButton.nextSibling); }, observeMenuForButtonAddition: function() { this.addCustomButtonToMenu(); Utils.observeDOMChanges(document.body, { childList: true, subtree: true }, () => this.addCustomButtonToMenu()); }, fixInputWidths: function() { document.querySelectorAll(CLAUDE_SELECTORS.TEXT_INPUT_ELEMENTS).forEach(el => { if (el && !el.getAttribute(CLAUDE_ATTRIBUTES.WIDTH_FIXED)) { el.style.maxWidth = '100%'; el.setAttribute(CLAUDE_ATTRIBUTES.WIDTH_FIXED, 'true'); } }); }, observeAndFixInputWidths: function() { let timeoutId; const debouncedFixWidths = () => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => this.fixInputWidths(), 300); }; this.fixInputWidths(); const formContainer = document.querySelector(CLAUDE_SELECTORS.FORM_CONTAINER) || document.body; Utils.observeDOMChanges(formContainer, { childList: true, subtree: true, attributes: false }, debouncedFixWidths); }, init: function() { if (this.jsExecuted) return; StyleManager.addPlatformStyles(this.getStyles()); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.observeMenuForButtonAddition(); this.observeAndFixInputWidths(); }); } else { this.observeMenuForButtonAddition(); this.observeAndFixInputWidths(); } this.jsExecuted = true; } }; // ChatGPT 模块 const ChatGPTModule = { isCurrent: window.location.hostname.includes(PLATFORM_HOSTS.CHATGPT), getStyles: function() { return ` /* ChatGPT 宽屏 CSS */ .text-base.gap-4.md\\:gap-6.md\\:max-w-2xl.lg\\:max-w-xl.xl\\:max-w-3xl.p-4.md\\:py-6.flex.lg\\:px-0.m-auto, .md\\:max-w-2xl.lg\\:max-w-xl.xl\\:max-w-3xl, main .text-token-text-primary .w-full .max-w-agw { max-width: 95% !important; margin-left: auto !important; margin-right: auto !important; } form .w-full { max-width: 95% !important; margin-left: auto !important; margin-right: auto !important; } .stretch { width: 100% !important; } .md\\:flex.md\\:items-end.md\\:gap-4 .w-full { max-width: 100% !important; } .relative.flex.h-full.max-w-full.flex-1.flex-col { max-width: 95% !important; margin: 0 auto !important; } `; }, init: function() { StyleManager.addPlatformStyles(this.getStyles()); } }; // --- 主逻辑:初始化和执行 --- function main() { if (GeminiModule.isCurrent) { GeminiModule.init(); } else if (ClaudeModule.isCurrent) { ClaudeModule.init(); } else if (ChatGPTModule.isCurrent) { ChatGPTModule.init(); } StyleManager.applyAllStyles(); } main(); })();