// ==UserScript== // @name Gemini 提示词选择器 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 在 Gemini 输入框下方添加图像比例、风格选择按钮以及提示词按钮 // @author Claude Opus 4.5; Claude Sonnet 4.5; Gemini 3 Pro; // @match https://gemini.google.com/* // @grant none // ==/UserScript== (function () { 'use strict'; // 可选的图像比例列表 const ASPECT_RATIOS = [ { ratio: '1:1', label: '1:1 正方形, 头像' }, { ratio: '9:16', label: '9:16 手机壁纸, 人像' }, { ratio: '16:9', label: '16:9 桌面壁纸, 风景' }, { ratio: '3:4', label: '3:4 经典比例, 拍照' }, { ratio: '4:3', label: '4:3 文章配图, 插画' }, { ratio: '3:2', label: '3:2 单反相机, 摄影' }, { ratio: '2:3', label: '2:3 社交媒体, 自拍' }, { ratio: '5:4', label: '5:4 艺术画作, 打印' }, { ratio: '4:5', label: '4:5 肖像模式' }, { ratio: '21:9', label: '21:9 电影宽屏' } ]; // 预设风格列表 const DEFAULT_STYLES = [ { name: '写实照片', prompt: '高清写实照片风格,注重细节和真实感' }, { name: '油画', prompt: '古典油画风格,色彩浓郁,笔触可见' }, { name: '水彩', prompt: '清新水彩画风格,色彩柔和透明' }, { name: '赛博朋克', prompt: '赛博朋克风格,霓虹灯光,未来主义' }, { name: '极简主义', prompt: '极简主义设计,简洁线条,留白艺术' }, { name: '动漫', prompt: '日式动漫风格,明亮色彩,精致线条' }, { name: '复古', prompt: '复古怀旧风格,暖色调,胶片质感' }, { name: '科幻', prompt: '科幻未来风格,科技感,宇宙元素' } ]; // 当前选择的比例和风格 let currentRatio = null; let currentStyle = null; let currentPrompt = null; // 预设提示词列表(用户要求不需要预设,仅保留自定义) const DEFAULT_PROMPTS = []; // 比例匹配的正则表达式 const ratioPattern = /,\s*(\d+:\d+)比例$/; // LocalStorage 键名 const STORAGE_KEY = 'gemini-custom-styles'; const STORAGE_KEY_FAV = 'gemini-favorite-styles'; const STORAGE_KEY_PROMPT = 'gemini-custom-prompts'; const STORAGE_KEY_FAV_PROMPT = 'gemini-favorite-prompts'; // 创建样式 function injectStyles() { const style = document.createElement('style'); style.textContent = ` /* 基础变量定义 - 默认深色模式 */ .ratio-selector-container, .style-selector-container, .prompt-selector-container, .custom-style-modal, .custom-prompt-modal { --g-selector-bg: rgba(255, 255, 255, 0.1); --g-selector-border: rgba(255, 255, 255, 0.2); --g-selector-text: #e3e3e3; --g-selector-hover-bg: rgba(255, 255, 255, 0.15); --g-selector-hover-border: rgba(255, 255, 255, 0.3); --g-selector-active-bg: rgba(138, 180, 248, 0.2); --g-selector-active-border: #8ab4f8; --g-dropdown-bg: #2d2d2d; --g-dropdown-border: rgba(255, 255, 255, 0.2); --g-dropdown-shadow: rgba(0, 0, 0, 0.4); --g-option-hover: rgba(255, 255, 255, 0.1); --g-option-selected-bg: rgba(138, 180, 248, 0.2); --g-option-selected-text: #8ab4f8; --g-option-text: #e3e3e3; --g-scrollbar-thumb: rgba(255, 255, 255, 0.3); --g-scrollbar-thumb-hover: rgba(255, 255, 255, 0.5); --g-btn-add-text: #8ab4f8; --g-btn-add-hover: rgba(138, 180, 248, 0.1); --g-btn-add-border: rgba(255, 255, 255, 0.1); --g-fav-star-inactive: #888888; --g-fav-star-active: #fbbc04; --g-fav-star-hover: #aaaaaa; --g-del-btn-inactive: #888888; --g-del-btn-hover: #ff6b6b; /* Modal Colors */ --g-modal-overlay: rgba(0, 0, 0, 0.7); --g-modal-bg: #2d2d2d; --g-modal-text: #e3e3e3; --g-modal-label: #b8b8b8; --g-modal-input-bg: #1e1e1e; --g-modal-input-border: rgba(255, 255, 255, 0.2); --g-modal-input-text: #e3e3e3; --g-modal-input-focus: #8ab4f8; --g-modal-btn-cancel-bg: rgba(255, 255, 255, 0.1); --g-modal-btn-cancel-text: #e3e3e3; --g-modal-btn-cancel-hover: rgba(255, 255, 255, 0.15); --g-modal-btn-danger-bg: #e53935; --g-modal-btn-danger-hover: #d32f2f; --g-modal-btn-danger-text: #ffffff; } /* 浅色模式变量覆盖 */ .ratio-selector-container.light-theme, .style-selector-container.light-theme, .prompt-selector-container.light-theme, .custom-style-modal.light-theme, .custom-prompt-modal.light-theme { --g-selector-bg: rgba(0, 0, 0, 0.05); --g-selector-border: rgba(0, 0, 0, 0.12); --g-selector-text: #444746; --g-selector-hover-bg: rgba(0, 0, 0, 0.08); --g-selector-hover-border: rgba(0, 0, 0, 0.3); --g-selector-active-bg: rgba(11, 87, 208, 0.1); --g-selector-active-border: #0b57d0; --g-dropdown-bg: #ffffff; --g-dropdown-border: rgba(0, 0, 0, 0.12); --g-dropdown-shadow: rgba(0, 0, 0, 0.15); --g-option-hover: rgba(0, 0, 0, 0.05); --g-option-selected-bg: rgba(11, 87, 208, 0.1); --g-option-selected-text: #0b57d0; --g-option-text: #1f1f1f; --g-scrollbar-thumb: rgba(0, 0, 0, 0.2); --g-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3); --g-btn-add-text: #0b57d0; --g-btn-add-hover: rgba(11, 87, 208, 0.1); --g-btn-add-border: rgba(0, 0, 0, 0.1); --g-fav-star-inactive: #9aa0a6; --g-fav-star-active: #fbbc04; --g-fav-star-hover: #5f6368; --g-del-btn-inactive: #9aa0a6; --g-del-btn-hover: #d93025; /* Modal Colors Light */ --g-modal-overlay: rgba(255, 255, 255, 0.6); --g-modal-bg: #ffffff; --g-modal-text: #1f1f1f; --g-modal-label: #444746; --g-modal-input-bg: #f0f4f9; --g-modal-input-border: rgba(0, 0, 0, 0.12); --g-modal-input-text: #1f1f1f; --g-modal-input-focus: #0b57d0; --g-modal-btn-cancel-bg: rgba(0, 0, 0, 0.05); --g-modal-btn-cancel-text: #444746; --g-modal-btn-cancel-hover: rgba(0, 0, 0, 0.1); } .ratio-selector-container { display: inline-flex !important; align-items: center !important; position: relative !important; margin-left: 8px !important; vertical-align: middle !important; z-index: 100 !important; } .style-selector-container, .prompt-selector-container { display: inline-flex !important; align-items: center !important; position: relative !important; margin-left: 8px !important; vertical-align: middle !important; z-index: 100 !important; animation: fadeInSlide 0.3s ease forwards; } .ratio-selector-container.hiding, .style-selector-container.hiding, .prompt-selector-container.hiding { animation: fadeOutSlide 0.2s ease forwards; pointer-events: none; } @keyframes fadeInSlide { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } @keyframes fadeOutSlide { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-10px); } } .ratio-selector-btn { display: flex; align-items: center; justify-content: center; padding: 6px 12px; border: 1px solid var(--g-selector-border); border-radius: 20px; background: var(--g-selector-bg); color: var(--g-selector-text); font-size: 13px; cursor: pointer; transition: all 0.2s ease; font-family: 'Google Sans', sans-serif; white-space: nowrap; } .ratio-selector-btn:hover { background: var(--g-selector-hover-bg); border-color: var(--g-selector-hover-border); } .ratio-selector-btn.active { background: var(--g-selector-active-bg); border-color: var(--g-selector-active-border); color: var(--g-option-selected-text); } .ratio-selector-btn::after { content: '▼'; font-size: 10px; margin-left: 6px; transition: transform 0.2s ease; } .ratio-selector-btn.expanded::after { transform: rotate(180deg); } .ratio-dropdown { position: absolute; left: 0; bottom: 100%; margin-bottom: 4px; background: var(--g-dropdown-bg); border: 1px solid var(--g-dropdown-border); border-radius: 12px; box-shadow: 0 4px 16px var(--g-dropdown-shadow); overflow: hidden; opacity: 0; visibility: hidden; transform: translateY(10px); transition: all 0.2s ease; z-index: 10000; min-width: 200px; } .ratio-dropdown.show { opacity: 1; visibility: visible; transform: translateY(0); } .ratio-dropdown-scroll { max-height: 200px; overflow-y: auto; padding: 4px 0; } .ratio-dropdown-scroll::-webkit-scrollbar { width: 6px; } .ratio-dropdown-scroll::-webkit-scrollbar-track { background: transparent; } .ratio-dropdown-scroll::-webkit-scrollbar-thumb { background: var(--g-scrollbar-thumb); border-radius: 3px; } .ratio-dropdown-scroll::-webkit-scrollbar-thumb:hover { background: var(--g-scrollbar-thumb-hover); } .ratio-option { padding: 10px 8px; color: var(--g-option-text); font-size: 13px; cursor: pointer; transition: background 0.15s ease; white-space: nowrap; font-family: 'Google Sans', sans-serif; display: flex; align-items: center; } .ratio-option:hover { background: var(--g-option-hover); } .ratio-option.selected { background: var(--g-option-selected-bg); color: var(--g-option-selected-text); } .ratio-check { width: 16px; margin-right: 4px; opacity: 0; font-weight: bold; flex-shrink: 0; } .ratio-option.selected .ratio-check { opacity: 1; } .ratio-value { width: 40px; text-align: left; margin-right: 12px; font-feature-settings: 'tnum'; font-weight: 500; flex-shrink: 0; } .ratio-desc { flex: 1; text-align: left; opacity: 0.9; overflow: hidden; text-overflow: ellipsis; } /* 风格选择器样式(复用比例选择器样式) */ .style-selector-container, .prompt-selector-container { display: inline-flex; align-items: center; position: relative; margin-left: 8px; vertical-align: middle; } .style-selector-btn { display: flex; align-items: center; justify-content: center; padding: 6px 12px; border: 1px solid var(--g-selector-border); border-radius: 20px; background: var(--g-selector-bg); color: var(--g-selector-text); font-size: 13px; cursor: pointer; transition: all 0.2s ease; font-family: 'Google Sans', sans-serif; white-space: nowrap; } .style-selector-btn:hover { background: var(--g-selector-hover-bg); border-color: var(--g-selector-hover-border); } .style-selector-btn.active { background: var(--g-selector-active-bg); border-color: var(--g-selector-active-border); color: var(--g-option-selected-text); } .style-selector-btn::after { content: '▼'; font-size: 10px; margin-left: 6px; transition: transform 0.2s ease; } .style-selector-btn.expanded::after { transform: rotate(180deg); } .style-dropdown { position: absolute; left: 0; bottom: 100%; margin-bottom: 4px; background: var(--g-dropdown-bg); border: 1px solid var(--g-dropdown-border); border-radius: 12px; box-shadow: 0 4px 16px var(--g-dropdown-shadow); overflow: hidden; opacity: 0; visibility: hidden; transform: translateY(10px); transition: all 0.2s ease; z-index: 10000; min-width: 180px; } .style-dropdown.show { opacity: 1; visibility: visible; transform: translateY(0); } .style-dropdown-scroll { max-height: 250px; overflow-y: auto; padding: 4px 0; } .style-dropdown-scroll::-webkit-scrollbar { width: 6px; } .style-dropdown-scroll::-webkit-scrollbar-track { background: transparent; } .style-dropdown-scroll::-webkit-scrollbar-thumb { background: var(--g-scrollbar-thumb); border-radius: 3px; } .style-dropdown-scroll::-webkit-scrollbar-thumb:hover { background: var(--g-scrollbar-thumb-hover); } .style-option { padding: 8px 16px; color: var(--g-option-text); font-size: 13px; cursor: pointer; transition: background 0.15s ease; font-family: 'Google Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; } .style-option:hover { background: var(--g-option-hover); } .style-option.selected { background: var(--g-option-selected-bg); color: var(--g-option-selected-text); } /* 左侧内容容器 */ .style-option-content { display: flex; align-items: center; flex: 1; } .style-option.selected .style-option-content::before { content: '✓ '; margin-right: 4px; } /* 收藏星星按钮 */ .style-fav-btn { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; color: var(--g-fav-star-inactive); font-size: 16px; margin-right: 8px; cursor: pointer; transition: all 0.2s ease; user-select: none; } .style-fav-btn:hover { color: var(--g-fav-star-hover); transform: scale(1.1); } .style-fav-btn.active { color: var(--g-fav-star-active); } .style-fav-btn::before { content: '★'; } /* 删除按钮 */ .style-del-btn { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; color: var(--g-fav-star-inactive); font-size: 14px; margin-left: 4px; cursor: pointer; transition: all 0.2s ease; opacity: 1; border-radius: 4px; flex-shrink: 0; } .style-del-btn:hover { color: #ff4444; background: rgba(255, 0, 0, 0.1); } .style-del-btn svg { width: 14px; height: 14px; fill: currentColor; } /* 选中时,左对齐文字,星星在最左边 */ .style-option { padding-left: 8px; /* 给星星留出空间 */ } .add-style-btn { padding: 12px 16px; color: var(--g-btn-add-text); font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.15s ease; border-bottom: 1px solid var(--g-btn-add-border); text-align: center; font-family: 'Google Sans', sans-serif; } .add-style-btn:hover { background: var(--g-btn-add-hover); } .add-style-btn::before { content: '+ '; font-size: 16px; } /* 弹窗样式 */ .custom-style-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--g-modal-overlay); display: none; align-items: center; justify-content: center; z-index: 20000; } .custom-style-modal.show { display: flex; } .modal-content { background: var(--g-modal-bg); border-radius: 16px; padding: 24px; width: 90%; max-width: 450px; box-shadow: 0 8px 32px var(--g-dropdown-shadow); animation: modalSlideIn 0.3s ease; } @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } } .modal-header { font-size: 18px; font-weight: 500; color: var(--g-modal-text); margin-bottom: 20px; font-family: 'Google Sans', sans-serif; } .modal-form-group { margin-bottom: 16px; } .modal-label { display: block; font-size: 13px; color: var(--g-modal-label); margin-bottom: 8px; font-family: 'Google Sans', sans-serif; } .modal-input { width: 100%; padding: 10px 12px; font-size: 14px; background: var(--g-modal-input-bg); border: 1px solid var(--g-modal-input-border); border-radius: 8px; color: var(--g-modal-input-text); outline: none; transition: border-color 0.2s ease; font-family: 'Google Sans', sans-serif; box-sizing: border-box; } .modal-input:focus { border-color: var(--g-modal-input-focus); } .modal-textarea { width: 100%; min-height: 80px; padding: 10px 12px; font-size: 14px; background: var(--g-modal-input-bg); border: 1px solid var(--g-modal-input-border); border-radius: 8px; color: var(--g-modal-input-text); outline: none; transition: border-color 0.2s ease; resize: vertical; font-family: 'Google Sans', sans-serif; box-sizing: border-box; } .modal-textarea:focus { border-color: var(--g-modal-input-focus); } .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; } .modal-btn { padding: 8px 20px; font-size: 14px; border: none; border-radius: 20px; cursor: pointer; transition: all 0.2s ease; font-family: 'Google Sans', sans-serif; font-weight: 500; } .modal-btn-cancel { background: var(--g-modal-btn-cancel-bg); color: var(--g-modal-btn-cancel-text); } .modal-btn-cancel:hover { background: var(--g-modal-btn-cancel-hover); } .modal-btn-confirm { background: #8ab4f8; color: #1e1e1e; } .modal-btn-confirm:hover { background: #a8c7fa; } `; document.head.appendChild(style); } // ========== LocalStorage 工具函数 ========== // 加载自定义风格 function loadCustomStyles() { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : []; } catch (e) { console.error('加载自定义风格失败:', e); return []; } } // 保存自定义风格 function saveCustomStyles(styles) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(styles)); } catch (e) { console.error('保存自定义风格失败:', e); } } // 获取所有风格(预设 + 自定义) function getAllStyles() { const customStyles = loadCustomStyles(); return [...DEFAULT_STYLES, ...customStyles]; } // 添加新风格 function addCustomStyle(name, prompt) { const customStyles = loadCustomStyles(); customStyles.push({ name, prompt }); saveCustomStyles(customStyles); } // 删除自定义风格 function deleteCustomStyle(name) { let customStyles = loadCustomStyles(); customStyles = customStyles.filter(s => s.name !== name); saveCustomStyles(customStyles); // 同时从收藏中移除 let favorites = loadFavorites(); if (favorites.includes(name)) { favorites = favorites.filter(f => f !== name); saveFavorites(favorites); } } // ========== 收藏功能工具函数 ========== // 加载收藏列表 function loadFavorites() { try { const saved = localStorage.getItem(STORAGE_KEY_FAV); return saved ? JSON.parse(saved) : []; } catch (e) { console.error('加载收藏列表失败:', e); return []; } } // 保存收藏列表 function saveFavorites(favorites) { try { localStorage.setItem(STORAGE_KEY_FAV, JSON.stringify(favorites)); } catch (e) { console.error('保存收藏列表失败:', e); } } // 切换收藏状态 function toggleFavorite(styleName) { let favorites = loadFavorites(); if (favorites.includes(styleName)) { favorites = favorites.filter(name => name !== styleName); } else { favorites.push(styleName); } saveFavorites(favorites); return favorites; // 返回最新列表 } // ========== 提示词 Function Helper ========== function loadCustomPrompts() { try { const saved = localStorage.getItem(STORAGE_KEY_PROMPT); return saved ? JSON.parse(saved) : []; } catch (e) { console.error('加载自定义提示词失败:', e); return []; } } function saveCustomPrompts(prompts) { try { localStorage.setItem(STORAGE_KEY_PROMPT, JSON.stringify(prompts)); } catch (e) { console.error('保存自定义提示词失败:', e); } } function getAllPrompts() { const customPrompts = loadCustomPrompts(); return [...DEFAULT_PROMPTS, ...customPrompts]; } function addCustomPrompt(name, prompt) { const customPrompts = loadCustomPrompts(); customPrompts.push({ name, prompt }); saveCustomPrompts(customPrompts); } function deleteCustomPrompt(name) { let customPrompts = loadCustomPrompts(); customPrompts = customPrompts.filter(s => s.name !== name); saveCustomPrompts(customPrompts); let favorites = loadFavoritePrompts(); if (favorites.includes(name)) { favorites = favorites.filter(f => f !== name); saveFavoritePrompts(favorites); } } function loadFavoritePrompts() { try { const saved = localStorage.getItem(STORAGE_KEY_FAV_PROMPT); return saved ? JSON.parse(saved) : []; } catch (e) { console.error('加载收藏提示词列表失败:', e); return []; } } function saveFavoritePrompts(favorites) { try { localStorage.setItem(STORAGE_KEY_FAV_PROMPT, JSON.stringify(favorites)); } catch (e) { console.error('保存收藏提示词列表失败:', e); } } function toggleFavoritePrompt(promptName) { let favorites = loadFavoritePrompts(); if (favorites.includes(promptName)) { favorites = favorites.filter(name => name !== promptName); } else { favorites.push(promptName); } saveFavoritePrompts(favorites); return favorites; } // ========== 输入框操作函数 ========== // 获取输入框元素 function getInputElement() { return document.querySelector('.ql-editor.textarea'); } // ... (getInputText 到 updateStyleInInput 函数保持不变) ... function getInputText() { const editor = getInputElement(); if (!editor) return ''; return editor.innerText.trim(); } function setInputText(text) { const editor = getInputElement(); if (!editor) return; // 清空并设置新内容(使用 DOM API 避免 TrustedHTML 问题) while (editor.firstChild) { editor.removeChild(editor.firstChild); } const p = document.createElement('p'); p.textContent = text; editor.appendChild(p); // 触发输入事件以更新 Gemini 的内部状态 const inputEvent = new Event('input', { bubbles: true, cancelable: true }); editor.dispatchEvent(inputEvent); // 将光标移动到末尾 const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(editor); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } function updateRatioInInput(newRatio) { let text = getInputText(); if (ratioPattern.test(text)) { // 替换已有的比例 text = text.replace(ratioPattern, `, ${newRatio}比例`); } else { // 添加新的比例 if (text) { text = text + `, ${newRatio}比例`; } else { text = `${newRatio}比例`; } } setInputText(text); currentRatio = newRatio; } function updateStyleInInput(styleName, stylePrompt) { let text = getInputText(); // 直接在末尾添加风格提示词 if (text) { text = text + `, ${stylePrompt}`; } else { text = stylePrompt; } setInputText(text); currentStyle = styleName; } function updatePromptInInput(promptName, promptContent) { let text = getInputText(); if (text) { text = text + ` ${promptContent}`; } else { text = promptContent; } setInputText(text); currentPrompt = promptName; } // ... (createRatioSelector 到 createCustomStyleModal 函数保持不变) ... function createRatioSelector() { // ... (原函数内容不变,这里只是占位,下面会完整替换 style selector 部分) const container = document.createElement('div'); container.className = 'ratio-selector-container'; const btn = document.createElement('button'); btn.className = 'ratio-selector-btn'; btn.textContent = '比例'; btn.type = 'button'; const dropdown = document.createElement('div'); dropdown.className = 'ratio-dropdown'; const scrollContainer = document.createElement('div'); scrollContainer.className = 'ratio-dropdown-scroll'; ASPECT_RATIOS.forEach(item => { const option = document.createElement('div'); option.className = 'ratio-option'; option.title = item.label; const checkSpan = document.createElement('span'); checkSpan.className = 'ratio-check'; checkSpan.textContent = '✓'; const valueSpan = document.createElement('span'); valueSpan.className = 'ratio-value'; valueSpan.textContent = item.ratio; const descSpan = document.createElement('span'); descSpan.className = 'ratio-desc'; descSpan.textContent = item.label.substring(item.ratio.length).trim(); option.appendChild(checkSpan); option.appendChild(valueSpan); option.appendChild(descSpan); option.dataset.ratio = item.ratio; option.addEventListener('click', (e) => { e.stopPropagation(); scrollContainer.querySelectorAll('.ratio-option').forEach(opt => { opt.classList.remove('selected'); }); option.classList.add('selected'); updateRatioInInput(item.ratio); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scrollContainer.appendChild(option); }); dropdown.appendChild(scrollContainer); btn.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = dropdown.classList.contains('show'); // 关闭其他所有下拉菜单 (Ratio Button Logic) document.querySelectorAll('.style-dropdown.show, .prompt-dropdown.show').forEach(d => { d.classList.remove('show'); }); document.querySelectorAll('.style-selector-btn.expanded, .prompt-selector-btn.expanded').forEach(b => { b.classList.remove('expanded'); }); if (isExpanded) { dropdown.classList.remove('show'); btn.classList.remove('expanded'); } else { dropdown.classList.add('show'); btn.classList.add('expanded'); } }); container.appendChild(btn); container.appendChild(dropdown); return container; } function createCustomStyleModal(onStyleAdded) { const modal = document.createElement('div'); modal.className = 'custom-style-modal'; const modalContent = document.createElement('div'); modalContent.className = 'modal-content'; // 使用 DOM API 构建弹窗内容(避免 TrustedHTML 问题) const header = document.createElement('div'); header.className = 'modal-header'; header.textContent = '添加自定义风格'; // 风格名称输入 const formGroup1 = document.createElement('div'); formGroup1.className = 'modal-form-group'; const label1 = document.createElement('label'); label1.className = 'modal-label'; label1.textContent = '风格名称'; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'modal-input'; nameInput.id = 'style-name-input'; nameInput.placeholder = '例如:蒸汽波'; formGroup1.appendChild(label1); formGroup1.appendChild(nameInput); // 风格提示词输入 const formGroup2 = document.createElement('div'); formGroup2.className = 'modal-form-group'; const label2 = document.createElement('label'); label2.className = 'modal-label'; label2.textContent = '风格提示词'; const promptInput = document.createElement('textarea'); promptInput.className = 'modal-textarea'; promptInput.id = 'style-prompt-input'; promptInput.placeholder = '例如:蒸汽波美学,粉紫色调,复古电脑图形,80年代风格'; formGroup2.appendChild(label2); formGroup2.appendChild(promptInput); // 按钮区域 const actions = document.createElement('div'); actions.className = 'modal-actions'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'modal-btn modal-btn-cancel'; cancelBtn.textContent = '取消'; const confirmBtn = document.createElement('button'); confirmBtn.className = 'modal-btn modal-btn-confirm'; confirmBtn.textContent = '确认'; actions.appendChild(cancelBtn); actions.appendChild(confirmBtn); // 组装弹窗 modalContent.appendChild(header); modalContent.appendChild(formGroup1); modalContent.appendChild(formGroup2); modalContent.appendChild(actions); modal.appendChild(modalContent); const closeModal = () => { modal.classList.remove('show'); nameInput.value = ''; promptInput.value = ''; }; modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); cancelBtn.addEventListener('click', closeModal); confirmBtn.addEventListener('click', () => { const name = nameInput.value.trim(); const prompt = promptInput.value.trim(); if (!name || !prompt) { alert('请填写完整的风格名称和提示词'); return; } addCustomStyle(name, prompt); if (onStyleAdded) { onStyleAdded(); } closeModal(); }); const handleEnter = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirmBtn.click(); } }; nameInput.addEventListener('keydown', handleEnter); return modal; } // 创建风格选择器 function createStyleSelector() { const container = document.createElement('div'); container.className = 'style-selector-container'; // 创建主按钮 const btn = document.createElement('button'); btn.className = 'style-selector-btn'; btn.textContent = '风格'; btn.type = 'button'; // 创建下拉菜单 const dropdown = document.createElement('div'); dropdown.className = 'style-dropdown'; const scrollContainer = document.createElement('div'); scrollContainer.className = 'style-dropdown-scroll'; // 渲染风格选项 const renderStyleOptions = () => { // 清空容器(使用 DOM API 避免 TrustedHTML 问题) while (scrollContainer.firstChild) { scrollContainer.removeChild(scrollContainer.firstChild); } const favorites = loadFavorites(); // 添加"+"按钮 const addBtn = document.createElement('div'); addBtn.className = 'add-style-btn'; addBtn.textContent = '添加自定义风格'; addBtn.addEventListener('click', (e) => { e.stopPropagation(); modal.classList.add('show'); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scrollContainer.appendChild(addBtn); // 获取所有风格并排序 let allStyles = getAllStyles(); // 排序:收藏的在顶部,其他按原顺序 allStyles.sort((a, b) => { const aFav = favorites.includes(a.name); const bFav = favorites.includes(b.name); if (aFav && !bFav) return -1; if (!aFav && bFav) return 1; return 0; // 保持原有相对顺序 }); allStyles.forEach(style => { const isFav = favorites.includes(style.name); // 判断是否为自定义风格(不在默认列表里的) const isCustom = !DEFAULT_STYLES.some(ds => ds.name === style.name); const option = document.createElement('div'); option.className = 'style-option'; // 收藏星标 const favBtn = document.createElement('div'); favBtn.className = `style-fav-btn ${isFav ? 'active' : ''}`; favBtn.title = isFav ? '取消置顶' : '收藏并置顶'; favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(style.name); renderStyleOptions(); // 重新渲染列表(会自动重排序) }); // 选项内容容器 const content = document.createElement('div'); content.className = 'style-option-content'; content.textContent = style.name; // 组装 option.appendChild(favBtn); option.appendChild(content); // 如果是自定义风格,添加删除按钮 if (isCustom) { const delBtn = document.createElement('div'); delBtn.className = 'style-del-btn'; delBtn.title = '删除此自定义风格'; // 使用文本图标(避免 TrustedHTML 问题) delBtn.textContent = '🗑'; delBtn.addEventListener('click', (e) => { e.stopPropagation(); // 弹出原生确认框 if (confirm(`确定要删除自定义风格 "${style.name}" 吗?`)) { deleteCustomStyle(style.name); renderStyleOptions(); // 删除后重新渲染 } }); option.appendChild(delBtn); } option.dataset.styleName = style.name; option.dataset.stylePrompt = style.prompt; option.addEventListener('click', (e) => { e.stopPropagation(); // 更新选中状态 scrollContainer.querySelectorAll('.style-option').forEach(opt => { opt.classList.remove('selected'); }); option.classList.add('selected'); // 更新输入框 updateStyleInInput(style.name, style.prompt); // 关闭下拉菜单 dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scrollContainer.appendChild(option); }); }; // 初始化渲染 renderStyleOptions(); dropdown.appendChild(scrollContainer); // 创建弹窗 const modal = createCustomStyleModal(() => { renderStyleOptions(); // 重新渲染选项列表 }); document.body.appendChild(modal); // 按钮点击事件 btn.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = dropdown.classList.contains('show'); // 关闭其他所有下拉菜单 (Style Button Logic) document.querySelectorAll('.ratio-dropdown.show, .prompt-dropdown.show').forEach(d => { d.classList.remove('show'); }); document.querySelectorAll('.ratio-selector-btn.expanded, .prompt-selector-btn.expanded').forEach(b => { b.classList.remove('expanded'); }); if (!isExpanded) { dropdown.classList.add('show'); btn.classList.add('expanded'); } }); container.appendChild(btn); container.appendChild(dropdown); return container; } function createCustomPromptModal(onPromptAdded) { const modal = document.createElement('div'); modal.className = 'custom-prompt-modal custom-style-modal'; // Reuse styles const modalContent = document.createElement('div'); modalContent.className = 'modal-content'; const header = document.createElement('div'); header.className = 'modal-header'; header.textContent = '添加自定义提示词'; const formGroup1 = document.createElement('div'); formGroup1.className = 'modal-form-group'; const label1 = document.createElement('label'); label1.className = 'modal-label'; label1.textContent = '提示词名称'; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'modal-input'; nameInput.placeholder = '例如:润色'; formGroup1.appendChild(label1); formGroup1.appendChild(nameInput); const formGroup2 = document.createElement('div'); formGroup2.className = 'modal-form-group'; const label2 = document.createElement('label'); label2.className = 'modal-label'; label2.textContent = '提示词内容'; const promptInput = document.createElement('textarea'); promptInput.className = 'modal-textarea'; promptInput.placeholder = '例如:请对上述文本进行润色,使其更加流畅。'; formGroup2.appendChild(label2); formGroup2.appendChild(promptInput); const actions = document.createElement('div'); actions.className = 'modal-actions'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'modal-btn modal-btn-cancel'; cancelBtn.textContent = '取消'; const confirmBtn = document.createElement('button'); confirmBtn.className = 'modal-btn modal-btn-confirm'; confirmBtn.textContent = '确认'; actions.appendChild(cancelBtn); actions.appendChild(confirmBtn); modalContent.appendChild(header); modalContent.appendChild(formGroup1); modalContent.appendChild(formGroup2); modalContent.appendChild(actions); modal.appendChild(modalContent); const closeModal = () => { modal.classList.remove('show'); nameInput.value = ''; promptInput.value = ''; }; modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); cancelBtn.addEventListener('click', closeModal); confirmBtn.addEventListener('click', () => { const name = nameInput.value.trim(); const prompt = promptInput.value.trim(); if (!name || !prompt) { alert('请填写完整的提示词名称和内容'); return; } addCustomPrompt(name, prompt); if (onPromptAdded) onPromptAdded(); closeModal(); }); const handleEnter = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirmBtn.click(); } }; nameInput.addEventListener('keydown', handleEnter); return modal; } function createPromptSelector() { const container = document.createElement('div'); container.className = 'prompt-selector-container'; const btn = document.createElement('button'); btn.className = 'prompt-selector-btn style-selector-btn'; // Add specific class btn.textContent = '提示词'; btn.type = 'button'; const dropdown = document.createElement('div'); dropdown.className = 'prompt-dropdown style-dropdown'; // Add specific class const scrollContainer = document.createElement('div'); scrollContainer.className = 'style-dropdown-scroll'; const renderPromptOptions = () => { while (scrollContainer.firstChild) { scrollContainer.removeChild(scrollContainer.firstChild); } const favorites = loadFavoritePrompts(); const addBtn = document.createElement('div'); addBtn.className = 'add-style-btn'; addBtn.textContent = '添加自定义提示词'; addBtn.addEventListener('click', (e) => { e.stopPropagation(); modal.classList.add('show'); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scrollContainer.appendChild(addBtn); let allPrompts = getAllPrompts(); allPrompts.sort((a, b) => { const aFav = favorites.includes(a.name); const bFav = favorites.includes(b.name); if (aFav && !bFav) return -1; if (!aFav && bFav) return 1; return 0; }); allPrompts.forEach(p => { const isFav = favorites.includes(p.name); const isCustom = !DEFAULT_PROMPTS.some(dp => dp.name === p.name); const option = document.createElement('div'); option.className = 'style-option'; const favBtn = document.createElement('div'); favBtn.className = `style-fav-btn ${isFav ? 'active' : ''}`; favBtn.title = isFav ? '取消置顶' : '收藏并置顶'; favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavoritePrompt(p.name); renderPromptOptions(); }); const content = document.createElement('div'); content.className = 'style-option-content'; content.textContent = p.name; option.appendChild(favBtn); option.appendChild(content); if (isCustom) { const delBtn = document.createElement('div'); delBtn.className = 'style-del-btn'; delBtn.title = '删除此自定义提示词'; delBtn.textContent = '🗑'; delBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`确定要删除自定义提示词 "${p.name}" 吗?`)) { deleteCustomPrompt(p.name); renderPromptOptions(); } }); option.appendChild(delBtn); } option.addEventListener('click', (e) => { e.stopPropagation(); scrollContainer.querySelectorAll('.style-option').forEach(opt => { opt.classList.remove('selected'); }); option.classList.add('selected'); updatePromptInInput(p.name, p.prompt); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scrollContainer.appendChild(option); }); }; renderPromptOptions(); dropdown.appendChild(scrollContainer); const modal = createCustomPromptModal(() => { renderPromptOptions(); }); document.body.appendChild(modal); btn.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = dropdown.classList.contains('show'); // 关闭其他所有下拉菜单 (Prompt Button Logic) document.querySelectorAll('.ratio-dropdown.show, .style-dropdown.show').forEach(d => { d.classList.remove('show'); }); document.querySelectorAll('.ratio-selector-btn.expanded, .style-selector-btn.expanded').forEach(b => { b.classList.remove('expanded'); }); if (!isExpanded) { dropdown.classList.add('show'); btn.classList.add('expanded'); } else { dropdown.classList.remove('show'); btn.classList.remove('expanded'); } }); container.appendChild(btn); container.appendChild(dropdown); return container; } // 查找并插入选择器 function insertSelector() { // 查找所有工具栏区域 - CHANGED: target trailing-actions-wrapper const trailingActionsWrappers = document.querySelectorAll('.trailing-actions-wrapper'); if (trailingActionsWrappers.length === 0) { return false; } let insertedOrUpdated = false; trailingActionsWrappers.forEach((wrapper) => { // 检查是否可见 if (!wrapper.offsetParent && wrapper.style.display !== 'flex') { return; } const existingRatio = wrapper.querySelector('.ratio-selector-container'); const existingStyle = wrapper.querySelector('.style-selector-container'); const existingPrompt = wrapper.querySelector('.prompt-selector-container'); // 1. Handle Ratio/Style (常驻按钮) // 插入位置:prepend to wrapper (before Thinking button) let insertionPoint = wrapper.firstChild; if (!existingRatio) { const ratioSelector = createRatioSelector(); wrapper.insertBefore(ratioSelector, insertionPoint); insertedOrUpdated = true; } if (!existingStyle) { const styleSelector = createStyleSelector(); // 如果 ratio 存在,插在 ratio 后,否则插在最前 const ratio = wrapper.querySelector('.ratio-selector-container'); const stylePoint = ratio ? ratio.nextSibling : wrapper.firstChild; wrapper.insertBefore(styleSelector, stylePoint); insertedOrUpdated = true; } // 2. Handle Prompt (Persistent) - 只在不存在时创建 if (!existingPrompt) { const promptSelector = createPromptSelector(); // 插在 Style 后面 const style = wrapper.querySelector('.style-selector-container'); const promptPoint = style ? style.nextSibling : (wrapper.querySelector('.ratio-selector-container') ? wrapper.querySelector('.ratio-selector-container').nextSibling : wrapper.firstChild); wrapper.insertBefore(promptSelector, promptPoint); insertedOrUpdated = true; } if (insertedOrUpdated) { // 强制重置主题缓存以确保新元素获得正确主题 lastThemeIsLight = null; updateTheme(); } }); return insertedOrUpdated; } // ========== 主题自动检测 ========== // 缓存上次主题状态,避免重复更新 let lastThemeIsLight = null; // 检查并更新主题 function updateTheme() { // 获取 body 背景颜色并计算亮度 const bodyColor = window.getComputedStyle(document.body).backgroundColor; const rgb = bodyColor.match(/\d+/g); if (!rgb) return; // 计算亮度 (YIQ公式) const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; const isLight = brightness > 128; // 如果主题没有变化,跳过更新 if (lastThemeIsLight === isLight) return; lastThemeIsLight = isLight; const containers = document.querySelectorAll('.ratio-selector-container, .style-selector-container, .prompt-selector-container, .custom-style-modal, .custom-prompt-modal'); containers.forEach(el => { if (isLight) { el.classList.add('light-theme'); } else { el.classList.remove('light-theme'); } }); } // ========== 性能优化工具 ========== // 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 全局点击处理器(只注册一次) let globalClickHandlerRegistered = false; function setupGlobalClickHandler() { if (globalClickHandlerRegistered) return; globalClickHandlerRegistered = true; document.addEventListener('click', () => { // 关闭所有下拉菜单 document.querySelectorAll('.ratio-dropdown.show, .style-dropdown.show, .prompt-dropdown.show').forEach(d => { d.classList.remove('show'); }); document.querySelectorAll('.ratio-selector-btn.expanded, .style-selector-btn.expanded, .prompt-selector-btn.expanded').forEach(b => { b.classList.remove('expanded'); }); }); } // 防抖版本的 insertSelector const debouncedInsertSelector = debounce(() => { insertSelector(); }, 100); // 防抖版本的主题更新 const debouncedUpdateTheme = debounce(() => { updateTheme(); }, 50); // 初始化 function init() { injectStyles(); setupGlobalClickHandler(); // 尝试立即插入 if (!insertSelector()) { // 使用防抖的 MutationObserver 回调 const debouncedCallback = debounce(() => { insertSelector(); }, 75); const observer = new MutationObserver((mutations) => { // 检查是否有相关的 DOM 变化 let hasRelevantChange = false; for (const mutation of mutations) { if (mutation.type === 'childList') { // 检查是否涉及工具栏区域 const target = mutation.target; if (target.classList && ( target.classList.contains('leading-actions-wrapper') || target.querySelector && target.querySelector('.leading-actions-wrapper') )) { hasRelevantChange = true; break; } // 检查添加的节点 for (const node of mutation.addedNodes) { if (node.nodeType === 1 && ( node.classList?.contains('leading-actions-wrapper') || node.querySelector?.('.leading-actions-wrapper') )) { hasRelevantChange = true; break; } } } if (hasRelevantChange) break; } if (hasRelevantChange) { debouncedCallback(); } }); observer.observe(document.body, { childList: true, subtree: true }); } updateTheme(); // 观察 body 属性变化(深浅模式切换) const themeObserver = new MutationObserver(() => { debouncedUpdateTheme(); }); themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] }); // 定期检查确保选择器存在和主题正确(处理 SPA 页面切换) setInterval(() => { debouncedInsertSelector(); }, 1500); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();