您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A collapsible, draggable panel with warm theme presets to fully customize aistudio.google.com.
// ==UserScript== // @name AI Studio Themer // @namespace http://tampermonkey.net/ // @version 1.0 // @description A collapsible, draggable panel with warm theme presets to fully customize aistudio.google.com. // @author LetMeFixIt // @match https://aistudio.google.com/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // // --- --- --- --- --- --- --- THEME PRESETS --- --- --- --- --- --- --- // const PRESETS = { 'custom': { name: "Custom", colors: {} }, 'default_dark': { name: "Default Dark", colors: { background: '#2B2B2B', panels: '#212121', bubbles: '#363636', text: '#E0E0E0' } }, 'sunset': { name: "Sunset", colors: { background: '#3c2f2f', panels: '#4a3a3a', bubbles: '#5e4a4a', text: '#e8c4b8' } }, 'emberfall': { name: "Emberfall", colors: { background: '#382B2B', panels: '#453636', bubbles: '#5A4848', text: '#E8C4B8' } }, 'volcanic_dusk': { name: "Volcanic Dusk", colors: { background: '#2E2525', panels: '#3D3232', bubbles: '#524545', text: '#DDC2B9' } }, 'midnight_campfire': { name: "Midnight Campfire", colors: { background: '#242121', panels: '#312D2D', bubbles: '#454040', text: '#D1C0B9' } } }; const styleElementId = 'ai-studio-draggable-styler-v5'; let colorPickers = {}; // Globally scoped within the script // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // // --- --- --- --- --- --- STYLE INJECTION --- --- --- --- --- --- --- // function addStyle_TrustedHTML_Safe(css) { let styleElement = document.getElementById(styleElementId); if (styleElement) styleElement.remove(); styleElement = document.createElement('style'); styleElement.id = styleElementId; styleElement.appendChild(document.createTextNode(css)); document.head.appendChild(styleElement); } function applyCustomColors(bgColor, panelColor, bubbleColor, textColor) { const customStyles = ` /* Main Background */ body, ms-app, .makersuite-layout, .layout-main, ms-chunk-editor section.chunk-editor-main { background-color: ${bgColor} !important; } /* Side Panels & Dropdowns */ ms-navbar .nav-content, ms-right-side-panel > div, .mat-expansion-panel, .mat-mdc-select-panel, .mat-mdc-menu-panel { background-color: ${panelColor} !important; } /* Chat Bubbles & Inputs */ .chat-turn-container.user, .chat-turn-container.model, ms-autosize-textarea .textarea, .prompt-input-wrapper-container { background-color: ${bubbleColor} !important; border: 1px solid #4f4f4f !important; } /* Text Coloring (Comprehensive) */ body, p, span, h1, h2, h3, h4, a, label, ms-cmark-node, .token-container, ms-navbar, ms-right-side-panel, .mat-expansion-panel-header-title, .mat-expansion-panel-header-description, .run-settings-content .setting-label, .mat-mdc-slider-label, .model-display-name, .model-description, .mat-mdc-select-value-text, .mat-mdc-option-text, .mat-mdc-form-field, .section-title, .disclaimer, .toggle-with-description .description, button, .mat-mdc-menu-item, .mat-mdc-tab-labels .mdc-tab__text-label, .author-label, .documentation-link, .view-status-link { color: ${textColor} !important; } /* SVG Logo & Icons */ .lockup-logo path, .material-symbols-outlined { fill: ${textColor} !important; color: ${textColor} !important; } /* Code Block Text & Background */ ms-code-block pre, ms-code-block code, ms-code-block .container { background-color: #1a1a1a !important; } ms-code-block code, ms-code-block code span, .hljs, .hljs-keyword, .hljs-comment { color: ${textColor} !important; -webkit-text-fill-color: ${textColor} !important; } /* UI Cleanup */ .mat-expansion-panel-header, button[ms-button] { background-color: transparent !important; } ms-chat-session, .chat-view-container, .chat-container, ms-toolbar, ms-prompt-input-wrapper, footer, .run-settings-content { background-color: transparent !important; } .theme-panel-hidden { display: none !important; } `; addStyle_TrustedHTML_Safe(customStyles); } // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // // --- --- --- --- --- --- DRAGGABLE UI PANEL --- --- --- --- --- --- --- // function createDraggablePanel() { // --- Toggler Icon --- const toggler = document.createElement('div'); toggler.textContent = '🎨'; Object.assign(toggler.style, { position: 'fixed', bottom: '15px', left: '15px', zIndex: '9999', width: '40px', height: '40px', backgroundColor: '#333', border: '1px solid #666', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '24px', cursor: 'pointer', userSelect: 'none' }); // --- Panel Container --- const panel = document.createElement('div'); panel.id = 'theme-panel-container'; Object.assign(panel.style, { position: 'fixed', zIndex: '10000', backgroundColor: 'rgba(50, 50, 50, 0.9)', border: '1px solid #666', borderRadius: '10px', fontFamily: 'sans-serif', fontSize: '14px', backdropFilter: 'blur(5px)', boxShadow: '0 4px 12px rgba(0,0,0,0.4)' }); // --- Draggable Header (SAFE VERSION - NO innerHTML) --- const header = document.createElement('div'); Object.assign(header.style, { padding: '8px 12px', cursor: 'move', color: '#fff', backgroundColor: 'rgba(80, 80, 80, 0.7)', borderTopLeftRadius: '10px', borderTopRightRadius: '10px', borderBottom: '1px solid #666', userSelect: 'none', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }); const titleSpan = document.createElement('span'); titleSpan.textContent = '🎨 Theme'; const closeButton = document.createElement('span'); closeButton.textContent = '\u00D7'; // '×' symbol Object.assign(closeButton.style, { cursor: 'pointer', fontWeight: 'bold', fontSize: '20px' }); header.appendChild(titleSpan); header.appendChild(closeButton); panel.appendChild(header); const content = document.createElement('div'); Object.assign(content.style, { padding: '12px', lineHeight: '2.2' }); panel.appendChild(content); // --- Helper: Create a color picker row --- const createPicker = (labelText, key) => { const wrapper = document.createElement('div'); const label = document.createElement('label'); label.innerText = labelText; label.style.color = '#fff'; label.style.marginRight = '10px'; const input = document.createElement('input'); input.type = 'color'; input.addEventListener('input', () => { GM_setValue(key, input.value); GM_setValue('activePreset', 'custom'); document.getElementById('theme-preset-selector').value = 'custom'; updateColors(); }); wrapper.appendChild(label); wrapper.appendChild(input); colorPickers[key] = input; return wrapper; }; // --- Helper: Create Preset Selector --- const createPresetSelector = () => { const wrapper = document.createElement('div'); const label = document.createElement('label'); label.innerText = 'Presets:'; Object.assign(label.style, { color: '#fff', marginRight: '10px' }); const selector = document.createElement('select'); selector.id = 'theme-preset-selector'; selector.style.width = '120px'; Object.keys(PRESETS).forEach(key => { const option = document.createElement('option'); option.value = key; option.textContent = PRESETS[key].name; selector.appendChild(option); }); selector.addEventListener('change', (e) => applyPreset(e.target.value)); wrapper.appendChild(label); wrapper.appendChild(selector); return wrapper; }; content.appendChild(createPresetSelector()); content.appendChild(createPicker('Background:', 'customBackgroundColor')); content.appendChild(createPicker('Panels:', 'customPanelColor')); content.appendChild(createPicker('Bubbles:', 'customBubbleColor')); content.appendChild(createPicker('Text:', 'customTextColor')); // --- Dragging Logic --- let isDragging = false, offsetX, offsetY; header.addEventListener('mousedown', (e) => { if (e.target === closeButton) return; // Don't drag when clicking the close button isDragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let newX = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, e.clientX - offsetX)); let newY = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, e.clientY - offsetY)); panel.style.left = `${newX}px`; panel.style.top = `${newY}px`; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; panel.style.transition = ''; GM_setValue('panelPosition', { top: panel.style.top, left: panel.style.left }); }); // --- Toggling Logic --- const togglePanel = (show) => { panel.classList.toggle('theme-panel-hidden', !show); toggler.classList.toggle('theme-panel-hidden', show); GM_setValue('panelVisible', show); }; toggler.addEventListener('click', () => togglePanel(true)); closeButton.addEventListener('click', () => togglePanel(false)); // --- Restore Position & State & Append to Body --- const savedPosition = GM_getValue('panelPosition', { top: 'calc(100vh - 250px)', left: '15px' }); panel.style.top = savedPosition.top; panel.style.left = savedPosition.left; document.body.appendChild(panel); document.body.appendChild(toggler); togglePanel(GM_getValue('panelVisible', false)); } // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // // --- --- --- --- --- --- SCRIPT EXECUTION --- --- --- --- --- --- -- // function applyPreset(presetKey) { if (!PRESETS[presetKey] || presetKey === 'custom') { GM_setValue('activePreset', 'custom'); updateUIAfterColorChange(); // Ensure custom colors are applied if switching to custom return; } const { background, panels, bubbles, text } = PRESETS[presetKey].colors; GM_setValue('customBackgroundColor', background); GM_setValue('customPanelColor', panels); GM_setValue('customBubbleColor', bubbles); GM_setValue('customTextColor', text); GM_setValue('activePreset', presetKey); updateUIAfterColorChange(); } function updateColors() { const defaultTheme = PRESETS.midnight_campfire.colors; // Default to a nice warm theme const bgColor = GM_getValue('customBackgroundColor', defaultTheme.background); const panelColor = GM_getValue('customPanelColor', defaultTheme.panels); const bubbleColor = GM_getValue('customBubbleColor', defaultTheme.bubbles); const textColor = GM_getValue('customTextColor', defaultTheme.text); applyCustomColors(bgColor, panelColor, bubbleColor, textColor); } function updateUIAfterColorChange() { const defaultTheme = PRESETS.midnight_campfire.colors; colorPickers.customBackgroundColor.value = GM_getValue('customBackgroundColor', defaultTheme.background); colorPickers.customPanelColor.value = GM_getValue('customPanelColor', defaultTheme.panels); colorPickers.customBubbleColor.value = GM_getValue('customBubbleColor', defaultTheme.bubbles); colorPickers.customTextColor.value = GM_getValue('customTextColor', defaultTheme.text); updateColors(); } // --- Main Initialization --- function initialize() { createDraggablePanel(); const activePreset = GM_getValue('activePreset', 'midnight_campfire'); document.getElementById('theme-preset-selector').value = activePreset; applyPreset(activePreset); // This will handle both presets and custom settings // --- FIX: Mutation Observer for SPA Navigation --- // AI Studio is a Single Page App. This observer re-applies the theme // when the user navigates to a new chat without a full page reload. const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { // ms-chunk-editor is a reliable element that appears when a chat is loaded. if (node.nodeType === 1 && node.querySelector('ms-chunk-editor')) { // Re-apply colors to the new DOM elements after a brief delay // to ensure everything is rendered. setTimeout(updateColors, 150); return; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); } // Use a brief delay to ensure the page is fully ready for DOM manipulation. // This is more reliable than 'load' in complex web apps. setTimeout(initialize, 500); })();