// ==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();
}
}
})();