您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自定义 ChatGPT 气泡:纯色/线性/放射/弥散光渐变、磨砂、外发光、描边、圆角/内边距……支持 Alt+G 打开面板;移动端优化与节流。
// ==UserScript== // @name ChatGPT Bubble Theme Pro // @namespace https://greasyforks.org/zh-CN/users/1503226-loom29 // @version 1.9.3 // @author Ech0 // @description 自定义 ChatGPT 气泡:纯色/线性/放射/弥散光渐变、磨砂、外发光、描边、圆角/内边距……支持 Alt+G 打开面板;移动端优化与节流。 // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // ==/UserScript== (function () { "use strict"; const IS_MOBILE = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || (window.matchMedia && matchMedia("(max-width: 820px)").matches); const SAFE_MODE = !!IS_MOBILE; /*** 性能:样式节流 + 归拢开关 ***/ let NEED_ENSURE = true; let styleTimer = null; const requestStyleUpdate = () => { if (styleTimer) return; styleTimer = setTimeout(() => { styleTimer = null; applyStyle(); }, IS_MOBILE ? 90 : 45); }; /*** 主题与默认值 ***/ const THEMES = { "海盐": { mode:"diffuse", angle:135, colors:["#89d0d2","#a8ced7","#88b9ce"], weights:[34,33,33], alpha:0.25, blur:10, glow:0, outlineColor:"#78afb0", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2d3d43", soft:10, autoText:false }, "春日": { mode:"diffuse", angle:130, colors:["#d5e7ab","#e2e29d","#b4d4ab"], weights:[34,33,33], alpha:0.45, blur:9, glow:0, outlineColor:"#b0cda7", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2e3f27", autoText:false }, "蜜桃": { mode:"diffuse", angle:125, colors:["#f2d8d4","#ebbeb3","#f4d5b8"], weights:[34,33,33], alpha:0.40, blur:8, glow:0, outlineColor:"#e6a08e", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#47352e", autoText:false }, "洋芋": { mode:"diffuse", angle:125, colors:["#9898c5","#b7cfe5","#b9b5c5"], weights:[34,33,33], alpha:0.30, blur:8, glow:0, outlineColor:"#9189be", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#4d4860", autoText:false }, "奶油": { mode:"diffuse", angle:125, colors:["#f0e6d0","#e2dfca","#ecd8c1"], weights:[34,33,33], alpha:0.30, blur:8, glow:0, outlineColor:"#c8b493", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#5c5951", autoText:false }, "湖畔": { mode:"diffuse", angle:135, colors:["#76b9d5","#a9d3d6","#34b7a1"], weights:[34,33,33], alpha:0.70, blur:10, glow:0, outlineColor:"#65b3b8", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#09161b", soft:20, autoText:false }, "热异常": { mode:"linear", angle:180, colors:["#dcd6d6","#eab06d","#e95f28"], weights:[27,47,26], alpha:1, blur:9, glow:0, outlineColor:"#610505", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2e3f27", soft:20, autoText:true }, "水泥": { mode:"diffuse", angle:135, colors:["#4d5b6f","#626e7a","#4f5863"], weights:[34,33,33], alpha:0.85, blur:10, glow:0, outlineColor:"#0d3f5e", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true }, "墨玉": { mode:"diffuse", angle:135, colors:["#3c5639","#274b37","#2a5026"], weights:[34,33,33], alpha:0.75, blur:10, glow:0, outlineColor:"#0d631f", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true }, "红酒": { mode:"diffuse", angle:135, colors:["#640707","#2f0909","#570a29"], weights:[34,33,33], alpha:0.90, blur:10, glow:0, outlineColor:"#651406", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true }, "纯黑": { mode:"solid", angle:135, colors:["#000000","#000000","#000000"], weights:[100,0,0], alpha:1.00, blur:10, glow:0, outlineColor:"#000000", outlineAlpha:0, outlineWidth:0, radius:16, padV:8, padH:12, textColor:"#ffffff", soft:10, autoText:true } }; const DEFAULTS = { targets: { user:true, assistant:false, sendbtn:true }, followOfSendBtn: "user", user: structuredClone(THEMES["海盐"]), assistant: structuredClone(THEMES["海盐"]), lastTheme: { user:"海盐", assistant:"海盐" }, overrides: { user:{}, assistant:{} }, customThemes: {}, galleryOrder: Object.keys(THEMES), panel: { x:null, y:null, w:560, h:560 } }; // —— 这里只声明一次,避免重复声明告警 —— const PANEL_ID = "ech0-theme-panel"; const STYLE_ID = "ech0-theme-style"; const clone = (x)=>JSON.parse(JSON.stringify(x)); const clamp=(v,min,max)=>Math.min(max,Math.max(min,v)); const pad2=(n)=>n.toString(16).padStart(2,"0"); const hexToRgb=(hex)=>{ let h=(hex||"").replace("#","").trim(); if(h.length===3) h=h.split("").map(x=>x+x).join(""); const n=parseInt(h||"000000",16); return { r:(n>>16)&255, g:(n>>8)&255, b:n&255 }; }; const rgba=(hex,a)=>{ const { r,g,b }=hexToRgb(hex); return `rgba(${r},${g},${b},${clamp(a,0,1)})`; }; const load=()=>{ try{ const s=GM_getValue("ech0_theme_pro"); if(s) return Object.assign(clone(DEFAULTS), s);}catch{} return clone(DEFAULTS); }; const save=(cfg)=>{ try{ GM_setValue("ech0_theme_pro", cfg); }catch{} }; let CFG = load(); // 提前加载,保证后续调用可用 function hslToRgb(h,s,l){ h=((h%360)+360)%360; s/=100; l/=100; const c=(1-Math.abs(2*l-1))*s, x=c*(1-Math.abs((h/60)%2-1)), m=l-c/2; let r=0,g=0,b=0; if(h<60){ r=c; g=x; } else if(h<120){ r=x; g=c; } else if(h<180){ g=c; b=x; } else if(h<240){ g=x; b=c; } else if(h<300){ r=x; b=c; } else { r=c; b=x; } return { r:Math.round((r+m)*255), g:Math.round((g+m)*255), b:Math.round((b+m)*255) }; } function parseColorToHex(s){ if(!s) return null; s=s.trim(); let m=s.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/); if(m){ let h=m[1]; if(h.length===3) h=h.split("").map(x=>x+x).join(""); return "#"+h.toLowerCase(); } m=s.match(/^rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i); if(m){ const r=clamp(Math.round(+m[1]),0,255), g=clamp(Math.round(+m[2]),0,255), b=clamp(Math.round(+m[3]),0,255); return "#"+pad2(r)+pad2(g)+pad2(b); } m=s.match(/^hsla?\s*\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)$/i); if(m){ const { r,g,b }=hslToRgb(+m[1],+m[2],+m[3]); return "#"+pad2(r)+pad2(g)+pad2(b); } return null; } function relLuma(hex){ const { r,g,b }=hexToRgb(hex); const sr=[r,g,b].map(v=>v/255).map(v=>v<=0.04045? v/12.92 : Math.pow((v+0.055)/1.055,2.4)); return 0.2126*sr[0]+0.7152*sr[1]+0.0722*sr[2]; } function themeLuma(s){ const w=s.weights||[1,1,1], sum=w.reduce((a,b)=>a+b,0)||1; const cols=(s.colors||[]).filter(Boolean); let L=0; cols.forEach((c,i)=>{ L+=relLuma(c)*(w[i]||0)/sum; }); return L; } const pickAutoTextColor=(s)=> themeLuma(s)>0.55 ? "#000000" : "#ffffff"; const SOFT=10, MAX_SOFT=50; function makeBackground(s){ const cols=(s.colors||[]).filter(Boolean); const c1=cols[0]||"#000000", c2=cols[1]||c1, c3=cols[2]||c2; const a1=rgba(c1,s.alpha), a2=rgba(c2,s.alpha), a3=rgba(c3,s.alpha); const w=(s.weights||[100,0,0]).map(n=>Math.max(0,+n||0)); const sum=w.reduce((a,b)=>a+b,0); if(s.mode==="solid") return a1; const softBase=Math.max(0,Math.min((s.soft??SOFT),MAX_SOFT)); if(sum<=0) return a1; if(s.mode==="diffuse"){ const k1=w[0]/sum, k2=w[1]/sum, k3=w[2]/sum; const angle=typeof s.angle==="number"?s.angle:135; return [ `radial-gradient(60% 60% at 0% 0%, ${a1} ${Math.round(k1*60)}%, transparent 60%)`, `radial-gradient(60% 60% at 100% 0%, ${a2} ${Math.round(k2*60)}%, transparent 60%)`, `radial-gradient(60% 60% at 0% 100%, ${a3} ${Math.round(k3*60)}%, transparent 60%)`, `linear-gradient(${angle}deg, ${a1}, ${a2}, ${a3})` ].join(","); } if(s.mode==="radial"){ const r1=w[0]/sum*100, r2=(w[0]+w[1])/sum*100; const t1=Math.min(softBase,r1,(r2-r1)/2), t2=Math.min(softBase,(r2-r1)/2,100-r2); return `radial-gradient(120% 100% at 0% 0%, ${a1} 0%, ${a1} ${Math.max(0,r1-t1)}%, ${a2} ${Math.min(100,r1+t1)}%, ${a2} ${Math.max(0,r2-t2)}%, ${a3} ${Math.min(100,r2+t2)}%, ${a3} 100%)`; } const p1=w[0]*100/sum, p2=(w[0]+w[1])*100/sum; const t1=Math.min(softBase,p1,(p2-p1)/2,100-p1); const t2=Math.min(softBase,(p2-p1)/2,100-p2); const angle=typeof s.angle==="number"?s.angle:135; if(p1<=0 && p2<=0) return `linear-gradient(${angle}deg, ${a3} 0%, ${a3} 100%)`; if(p1>=100) return `linear-gradient(${angle}deg, ${a1} 0%, ${a1} 100%)`; return `linear-gradient(${angle}deg, ${a1} 0%, ${a1} ${Math.max(0,p1-t1)}%, ${a2} ${Math.min(100,p1+t1)}%, ${a2} ${Math.max(0,p2-t2)}%, ${a3} ${Math.min(100,p2+t2)}%, ${a3} 100%)`; } const CANDIDATES=[":scope > .markdown",":scope .markdown",":scope .prose",":scope .whitespace-pre-wrap"]; const getCandidateBlocks=(el)=> Array.from(el.querySelectorAll(CANDIDATES.join(","))).filter(n=>!n.closest(".ech0-bubble")); const containsAll=(c,nodes)=> nodes.every(n=>c.contains(n)); function findCommonContainer(msgEl,nodes){ if(!nodes.length) return null; for(let c=nodes[0].parentElement; c && c!==msgEl; c=c.parentElement){ if(containsAll(c,nodes)) return c; } return msgEl; } function wrapIntoBubble(container,nodes){ if(!container) return; let shell=container.querySelector(":scope > .ech0-bubble"); if(!shell){ shell=document.createElement("div"); shell.className="ech0-bubble"; container.insertBefore(shell,container.firstChild); } const tops=[]; nodes.forEach(n=>{ let t=n; while(t.parentElement && t.parentElement!==container) t=t.parentElement; if(!tops.includes(t)) tops.push(t); }); tops.sort((a,b)=>a.compareDocumentPosition(b)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1); tops.forEach(t=>{ if(t!==shell) shell.appendChild(t); }); } function glueOneMessage(msgEl){ const nodes=getCandidateBlocks(msgEl); if(!nodes.length) return; const c=findCommonContainer(msgEl,nodes); if (SAFE_MODE){ c.classList.add("ech0-bubble-s"); return; } wrapIntoBubble(c,nodes); } function ensureShells(role){ document.querySelectorAll(`[data-message-author-role="${role}"]`).forEach(glueOneMessage); } const SEND_BTN_SEL=[ 'button[data-testid="send-button"]', 'form button[type="submit"]', 'button[aria-label="Send"]', '[data-testid="composer-send-button"] button' ].join(","); const makeGlow=(hex,str)=> str>0?`0 6px 18px ${rgba(hex,clamp(str*0.55,0,1))}, 0 0 18px ${rgba(hex,clamp(str*0.35,0,1))}`:"none"; function cssForRole(role,s){ const bg=makeBackground(s); const outline=rgba(s.outlineColor,s.outlineAlpha); const border=(s.outlineAlpha<=0 || s.outlineWidth<=0)?"none":`${s.outlineWidth}px solid ${outline}`; const shadow=makeGlow(s.outlineColor,s.glow); const pfx=role==="user"?"user":"asst"; const isLight=themeLuma(s)>0.55; const preBg=isLight?"rgba(0,0,0,.08)":"rgba(255,255,255,.12)"; const preBorder=isLight?"1px solid rgba(0,0,0,.12)":"1px solid rgba(255,255,255,.16)"; const text=s.autoText?pickAutoTextColor(s):(s.textColor||"#101418"); const blurPx = IS_MOBILE ? Math.min(s.blur, 4) : s.blur; // 移动端 blur 限幅 const bubbleSel = SAFE_MODE ? `.ech0-bubble-s` : `.ech0-bubble`; const clearHost = SAFE_MODE ? "" : ` [data-message-author-role="${role}"], [data-message-author-role="${role}"] > div, [data-message-author-role="${role}"] .text-message, [data-message-author-role="${role}"] .relative{ background:transparent!important; box-shadow:none!important; border-color:transparent!important; }`; return ` :root{ --ech0-${pfx}-text:${text}; } ${clearHost} [data-message-author-role="${role}"] ${bubbleSel}{ display:inline-block; width:fit-content; max-width:100%; background:${bg}!important; color:var(--ech0-${pfx}-text)!important; border:${border}!important; border-radius:${s.radius}px!important; padding:${s.padV}px ${s.padH}px!important; box-shadow:${shadow}!important; -webkit-backdrop-filter:blur(${blurPx}px)!important; backdrop-filter:blur(${blurPx}px)!important; } [data-message-author-role="${role}"] ${bubbleSel} :not(pre):not(code){ color:var(--ech0-${pfx}-text)!important; -webkit-text-fill-color:var(--ech0-${pfx}-text)!important; } [data-message-author-role="${role}"] ${bubbleSel} pre{ background:${preBg}!important; border:${preBorder}!important; border-radius:10px!important; }`; } function applyStyle(){ if (NEED_ENSURE){ if (CFG.targets.user) ensureShells("user"); if (CFG.targets.assistant) ensureShells("assistant"); NEED_ENSURE = false; } let css=` .ech0-bubble{ background:transparent; background-clip:padding-box; } .ech0-bubble-s{ background:transparent; background-clip:padding-box; } #${PANEL_ID} .hd{ position:sticky; top:0; z-index:5; } #${PANEL_ID} .previewRow{ position:relative; z-index:1; } `; if(CFG.targets.user) css+=cssForRole("user",CFG.user); if(CFG.targets.assistant) css+=cssForRole("assistant",CFG.assistant); if(CFG.targets.sendbtn){ const s=CFG.followOfSendBtn==="assistant"?CFG.assistant:CFG.user; const bg=makeBackground(s); const outline=rgba(s.outlineColor,s.outlineAlpha); const border=(s.outlineAlpha<=0 || s.outlineWidth<=0)?"none":`${s.outlineWidth}px solid ${outline}`; const textColor = s.autoText ? pickAutoTextColor(s) : (s.textColor || "#101418"); const blurPx = IS_MOBILE ? Math.min(s.blur, 4) : s.blur; css+=` ${SEND_BTN_SEL}{ background:${bg}!important; border:${border}!important; border-radius:9999px!important; -webkit-backdrop-filter:blur(${blurPx}px)!important; backdrop-filter:blur(${blurPx}px)!important; color:${textColor}!important; } ${SEND_BTN_SEL} svg{ color:${textColor}!important; --send-stroke: 0.10px; } ${SEND_BTN_SEL} svg *{ filter:none!important; mix-blend-mode:normal!important; opacity:1!important; -webkit-mask-image:none!important; mask:none!important; fill:none!important; stroke:currentColor!important; stroke-linecap:round!important; stroke-linejoin:round!important; vector-effect:non-scaling-stroke!important; stroke-width:var(--send-stroke)!important; shape-rendering:geometricPrecision; paint-order:stroke fill markers; } `; } let node=document.getElementById(STYLE_ID); if(!node){ node=document.createElement("style"); node.id=STYLE_ID; document.head.appendChild(node); } node.textContent=css; updatePreview(); } function openPanel(){ if(document.getElementById(PANEL_ID)) return; const w=document.createElement("div"); w.id=PANEL_ID; w.innerHTML=` <div class="hd" id="ech0-hd"><strong>Bubble Theme Pro</strong><div class="sp"></div><button data-act="close" title="关闭">✕</button></div> <div class="bar"> <label><input type="checkbox" id="t-user"> 用户气泡</label> <label><input type="checkbox" id="t-assistant"> AI 气泡</label> <label><input type="checkbox" id="t-sendbtn"> 发送按钮</label> <span class="sep"></span> <label>发送按钮跟随 <select id="followSel"><option value="user">用户</option><option value="assistant">AI</option></select> </label> </div> <div class="tabs"> <button class="tab active" data-role="user">编辑:用户</button> <button class="tab" data-role="assistant">编辑:AI</button> <div class="sp"></div> <button class="chipEdit" id="editChipsBtn">编辑主题</button> </div> <div class="chipWall" id="chipWall"></div> <div class="previewRow"> <div class="previewLabel">预览:</div> <div class="previewWrap"><div id="ech0-preview" class="previewBubble">正在输入中.............</div></div> </div> <div class="grid"> <div class="col"> <div class="item"><label>背景类型</label> <select id="mode"><option value="linear">线性渐变</option><option value="radial">放射渐变</option><option value="diffuse">弥散光渐变</option><option value="solid">纯色</option></select> </div> <div class="item"><label>角度(线性)</label><div class="dual"><input type="range" id="angleR" min="0" max="360" step="1"><input type="number" id="angle" min="0" max="360" step="1"></div></div> <div class="item"><label>文字颜色</label> <div class="dualColor"> <input type="text" id="textHex" class="hex" placeholder="#RRGGBB"> <input type="color" id="textColor"> <label class="inline"><input type="checkbox" id="autoText"> 自动</label> </div> </div> <div class="item"><label>颜色1</label><div class="dualColor"><input type="text" id="h1" class="hex" placeholder="#RRGGBB"><input type="color" id="c1"></div></div> <div class="item"><label>颜色2</label><div class="dualColor"><input type="text" id="h2" class="hex" placeholder="#RRGGBB"><input type="color" id="c2"></div></div> <div class="item"><label>颜色3</label><div class="dualColor"><input type="text" id="h3" class="hex" placeholder="#RRGGBB"><input type="color" id="c3"></div></div> <div class="item"><label>配重1/2/3(%)</label> <div class="weights"> <div class="row"><input type="range" id="w1r" min="0" max="100" step="1"><input type="number" id="w1" min="0" max="100" step="1"></div> <div class="row"><input type="range" id="w2r" min="0" max="100" step="1"><input type="number" id="w2" min="0" max="100" step="1"></div> <div class="row"><input type="range" id="w3r" min="0" max="100" step="1" disabled><input type="number" id="w3" min="0" max="100" step="1" readonly></div> </div> </div> <div class="hint" id="weightHint">配重合计:0%(建议=100%)</div> </div> <div class="col"> <div class="item"><label>过渡羽化(%)</label><div class="dual"><input type="range" id="softR" min="0" max="50" step="1"><input type="number" id="soft" min="0" max="50" step="1"></div></div> <div class="item"><label>透明度</label><div class="dual"><input type="range" id="alphaR" min="0" max="1" step="0.01"><input type="number" id="alpha" min="0" max="1" step="0.01"></div></div> <div class="item"><label>磨砂(blur)</label><div class="dual"><input type="range" id="blurR" min="0" max="30" step="1"><input type="number" id="blur" min="0" max="30" step="1"></div></div> <div class="item"><label>发光强度</label><div class="dual"><input type="range" id="glowR" min="0" max="1" step="0.01"><input type="number" id="glow" min="0" max="1" step="0.01"></div></div> <div class="item"><label>描边色</label> <div class="dualColor"> <input type="text" id="outlineHex" class="hex" placeholder="#RRGGBB"> <input type="color" id="outlineColor"> </div> </div> <div class="item"><label>描边透明</label><div class="dual"><input type="range" id="outlineAlphaR" min="0" max="1" step="0.01"><input type="number" id="outlineAlpha" min="0" max="1" step="0.01"></div></div> <div class="item"><label>描边宽(px)</label><div class="dual"><input type="range" id="outlineWidthR" min="0" max="6" step="1"><input type="number" id="outlineWidth" min="0" max="6" step="1"></div></div> <div class="item"><label>圆角(px)</label><div class="dual"><input type="range" id="radiusR" min="0" max="40" step="1"><input type="number" id="radius" min="0" max="40" step="1"></div></div> <div class="item"><label>内边距-垂直</label><div class="dual"><input type="range" id="padVR" min="0" max="40" step="1"><input type="number" id="padV" min="0" max="40" step="1"></div></div> <div class="item"><label>内边距-水平</label><div class="dual"><input type="range" id="padHR" min="0" max="40" step="1"><input type="number" id="padH" min="0" max="40" step="1"></div></div> </div> </div> <div class="presets"> <div class="left"> <input id="presetName" placeholder="预设名称"> <button data-act="savePreset">另存为</button> </div> <div class="right"> <button data-act="save">保存</button> <button data-act="reset">重置默认</button> </div> </div> `; document.body.appendChild(w); w.querySelector('[data-act="close"]').addEventListener("click",(ev)=>{ ev.stopPropagation(); w.remove(); }); GM_addStyle(` #${PANEL_ID}{ position:fixed; z-index:999999; left:50%; top:60px; transform:translateX(-50%); width:${CFG.panel.w||560}px; height:${CFG.panel.h||560}px; background:#fff; color:#1b1f24; border:1px solid rgba(0,0,0,.12); border-radius:14px; box-shadow:0 16px 48px rgba(0,0,0,.16); overflow:auto; resize:both; font:13px/1.35 system-ui, Segoe UI, Arial; cursor:default; } #${PANEL_ID} .hd{ position:sticky; top:0; z-index:5; display:flex; align-items:center; gap:8px; padding:8px 10px; user-select:none; background:#fff; border-bottom:1px solid rgba(0,0,0,.06); border-top-left-radius:14px; border-top-right-radius:14px; cursor:default; } #${PANEL_ID} .sp{ flex:1; } #${PANEL_ID} .bar{ display:flex; align-items:center; gap:8px; padding:6px 10px; flex-wrap:wrap; cursor:default; } #${PANEL_ID} .sep{ width:1px; height:16px; background:rgba(0,0,0,.08); margin:0 4px; } #${PANEL_ID} .tabs{ display:flex; align-items:center; gap:6px; padding:6px 10px; border-top:1px solid rgba(0,0,0,.06); cursor:default; } #${PANEL_ID} .tab{ border:1px solid rgba(0,0,0,.12); background:#f5f7fb; border-radius:8px; padding:5px 8px; cursor:default; } #${PANEL_ID} .tab.active{ background:#e9eef9; } #${PANEL_ID} .chipEdit{ border:1px solid rgba(0,0,0,.12); background:#fff; border-radius:999px; padding:5px 10px; cursor:default; } #${PANEL_ID} .chipWall{ display:flex; flex-wrap:wrap; gap:6px; padding:0 10px 6px; border-bottom:1px solid rgba(0,0,0,.06); cursor:default; } #${PANEL_ID} .chip{ display:flex; align-items:center; gap:6px; padding:5px 8px; border:1px solid rgba(0,0,0,.12); background:#f7f9fc; border-radius:999px; cursor:default; user-select:none; } #${PANEL_ID} .chip .dot{ width:14px; height:14px; border-radius:999px; border:1px solid rgba(0,0,0,.12); background:linear-gradient(135deg,var(--c1),var(--c2),var(--c3)); } #${PANEL_ID} .chip .x{ display:none; width:16px; height:16px; border-radius:50%; border:1px solid rgba(0,0,0,.25); background:#fff; line-height:14px; text-align:center; font-size:11px; } #${PANEL_ID}.editing .chip .x{ display:inline-block; } #${PANEL_ID} .chip.active{ background:#e9eef9; border-color:rgba(59,130,246,.35); box-shadow: inset 0 0 0 2px rgba(59,130,246,.18); } #${PANEL_ID} .previewRow{ display:grid; grid-template-columns:56px 1fr; align-items:center; gap:8px; padding:6px 10px; } #${PANEL_ID} .previewBubble{ display:inline-block; padding:6px 10px; border-radius:12px; border:1px solid rgba(0,0,0,.12); width:fit-content; max-width:100%; white-space:nowrap; } #${PANEL_ID} .grid{ display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:6px 10px; } #${PANEL_ID} .col{ display:flex; flex-direction:column; gap:8px; } #${PANEL_ID} .item{ display:flex; align-items:center; gap:8px; cursor:default; } #${PANEL_ID} label{ width:116px; color:#3b4250; } #${PANEL_ID} .bar label{ width:auto!important; display:inline-flex; align-items:center; gap:8px; margin:0; white-space:nowrap; } #${PANEL_ID} #followSel{ min-width:92px; padding-right:22px; } #${PANEL_ID} .dual{ display:flex; align-items:center; gap:6px; } #${PANEL_ID} .dual input[type="range"]{ width:128px; } #${PANEL_ID} .dual input[type="number"]{ width:56px; height:26px; line-height:26px; font-size:13px; padding:4px 6px; border-radius:7px; cursor:default; } #${PANEL_ID} .dualColor{ display:flex; align-items:center; gap:6px; } #${PANEL_ID} .dualColor input.hex{ width:96px; height:26px; line-height:26px; font-size:13px; } #${PANEL_ID} .dualColor .inline{ display:inline-flex; align-items:center; gap:4px; font-size:12px; color:#64748b; margin-left:6px; } #${PANEL_ID} .dualColor input:disabled{ opacity:.6; } #${PANEL_ID} input[type="color"]{ width:34px; height:24px; border:none; background:transparent; cursor:default; } #${PANEL_ID} input[type="number"], #${PANEL_ID} select, #${PANEL_ID} input[type="text"]{ font-size:13px; height:26px; line-height:26px; padding:4px 6px; border-radius:7px; border:1px solid rgba(0,0,0,.14); background:#fff; cursor:default; } #${PANEL_ID} input[type="range"]{ -webkit-appearance:none; appearance:none; background:transparent; height:16px; cursor:default; } #${PANEL_ID} input[type="range"]::-webkit-slider-runnable-track{ height:3px; border-radius:2px; background:#d1d5db; } #${PANEL_ID} input[type="range"]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:12px; height:12px; border-radius:50%; background:#4b5563; border:none; margin-top:-4.5px; } #${PANEL_ID} .weights .row{ display:flex; align-items:center; gap:6px; margin-bottom:2px; } #${PANEL_ID} .weights input[type="range"]{ width:128px; } #${PANEL_ID} .weights input[type="number"]{ width:56px; height:26px; line-height:26px; font-size:13px; } #${PANEL_ID} .hint{ padding-left:116px; color:#6b7280; } #${PANEL_ID} .presets{ display:flex; justify-content:space-between; align-items:center; gap:8px; padding:8px 10px; border-top:1px solid rgba(0,0,0,.06); cursor:default; } #${PANEL_ID} .presets .left{ display:flex; align-items:center; gap:6px; } #${PANEL_ID} .presets input{ width:140px; cursor:default; } #${PANEL_ID} button{ background:#f5f7fb; border:1px solid rgba(0,0,0,.12); border-radius:8px; padding:5px 10px; cursor:default; } #${PANEL_ID} #followSel, #${PANEL_ID} #mode{ font-size:12px; height:26px; line-height:26px; padding:0 24px 0 8px; text-align:center; text-align-last:center; cursor:default; } #${PANEL_ID} #mode{ min-width:128px; } #${PANEL_ID} #followSel option, #${PANEL_ID} #mode option{ text-align:center; } @media (max-width: 820px){ #${PANEL_ID}{ left:50%!important; top:10px!important; transform:translateX(-50%)!important; width:min(94vw, ${CFG.panel.w||560}px)!important; height:min(78vh, ${CFG.panel.h||560}px)!important; max-width:96vw; max-height:82vh; -webkit-overflow-scrolling:touch; overscroll-behavior:contain; touch-action:auto; } #${PANEL_ID} .hd{ touch-action:none; } #${PANEL_ID} .grid{ grid-template-columns:1fr; } #${PANEL_ID} .previewRow{ grid-template-columns:48px 1fr; } }`); if(CFG.panel.x!=null && CFG.panel.y!=null){ w.style.left=CFG.panel.x+"px"; w.style.top=CFG.panel.y+"px"; w.style.transform="none"; } // 拖动 (function enableDrag(){ const hd = w.querySelector("#ech0-hd"); let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0; const clampToViewport = (nx,ny)=>{ const pad=8, r=w.getBoundingClientRect(), vw=innerWidth, vh=innerHeight; nx = Math.max(pad, Math.min(vw - r.width - pad, nx)); ny = Math.max(pad, Math.min(vh - r.height - pad, ny)); return { nx, ny }; }; const getPoint = (e) => (e.touches && e.touches[0]) || e; const isInteractive = (el) => el && el.closest && el.closest('[data-act="close"],button,select,input,textarea,a'); function onStart(e){ if (isInteractive(e.target)) return; const p=getPoint(e); dragging=true; const r=w.getBoundingClientRect(); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top; if (hd.setPointerCapture && e.pointerId!=null) hd.setPointerCapture(e.pointerId); e.preventDefault(); } function onMove(e){ if(!dragging) return; const p=getPoint(e); let nx=ox+(p.clientX-sx), ny=oy+(p.clientY-sy); ({nx,ny}=clampToViewport(nx,ny)); w.style.left=nx+"px"; w.style.top=ny+"px"; w.style.transform="none"; e.preventDefault(); } function onEnd(){ if(!dragging) return; dragging=false; const r=w.getBoundingClientRect(); CFG.panel.x=Math.round(r.left); CFG.panel.y=Math.round(r.top); CFG.panel.w=Math.round(r.width); CFG.panel.h=Math.round(r.height); save(CFG); } if (window.PointerEvent){ hd.addEventListener("pointerdown", onStart); addEventListener("pointermove", onMove); addEventListener("pointerup", onEnd); } else { hd.addEventListener("mousedown", onStart); addEventListener("mousemove", onMove); addEventListener("mouseup", onEnd); hd.addEventListener("touchstart", onStart, { passive:false }); addEventListener("touchmove", onMove, { passive:false }); addEventListener("touchend", onEnd); } addEventListener("resize", ()=>{ const r=w.getBoundingClientRect(), {nx,ny}=clampToViewport(r.left,r.top); w.style.left=Math.round(nx)+"px"; w.style.top=Math.round(ny)+"px";}); })(); const $=(s)=>w.querySelector(s); $("#t-user").checked=!!CFG.targets.user; $("#t-assistant").checked=!!CFG.targets.assistant; $("#t-sendbtn").checked=!!CFG.targets.sendbtn; $("#followSel").value=CFG.followOfSendBtn; ["t-user","t-assistant","t-sendbtn","followSel"].forEach(id=>{ $("#"+id).addEventListener("change",()=>{ CFG.targets.user=$("#t-user").checked; CFG.targets.assistant=$("#t-assistant").checked; CFG.targets.sendbtn=$("#t-sendbtn").checked; CFG.followOfSendBtn=$("#followSel").value; save(CFG); NEED_ENSURE = true; requestStyleUpdate(); }); }); let editMode=false, ACTIVE="user"; const isBuiltin=(name)=> name in THEMES; const chipTheme=(name)=> isBuiltin(name)?THEMES[name]:(CFG.customThemes[name]||THEMES["海盐"]); function galleryNames(){ const builtins=Object.keys(THEMES), customs=Object.keys(CFG.customThemes||{}), set=new Set([...builtins,...customs]); const arr=(CFG.galleryOrder||[]).filter(n=>set.has(n)); for(const n of set){ if(!arr.includes(n)) arr.push(n); } CFG.galleryOrder=arr; save(CFG); return arr; } function renderChips(){ const wall=$("#chipWall"); wall.innerHTML=""; const current=CFG.lastTheme[ACTIVE]; galleryNames().forEach(name=>{ const st=chipTheme(name), btn=document.createElement("div"); btn.className="chip"; btn.draggable=true; btn.dataset.name=name; if(name===current) btn.classList.add("active"); btn.innerHTML=`<span class="dot" style="--c1:${st.colors[0]};--c2:${st.colors[1]};--c3:${st.colors[2]}"></span><span class="nm">${name}</span>${isBuiltin(name)?"":'<span class="x" title="删除">×</span>'}`; wall.appendChild(btn); }); wall.parentElement.classList.toggle("editing",editMode); } function pickThemeByName(name){ CFG.lastTheme[ACTIVE]=name; const over=(CFG.overrides[ACTIVE]||{})[name]; CFG[ACTIVE]=clone(over||chipTheme(name)); fillForm(); save(CFG); renderChips(); } // 标签:用户 / AI w.querySelector(".tabs").addEventListener("click",(e)=>{ const t = e.target.closest(".tab"); if(!t) return; const role = t.dataset.role; if(!role || role === ACTIVE) return; w.querySelectorAll(".tabs .tab").forEach(x=>x.classList.remove("active")); t.classList.add("active"); ACTIVE = role; renderChips(); fillForm(); updatePreview(); }); $("#editChipsBtn").addEventListener("click",()=>{ editMode=!editMode; $("#editChipsBtn").textContent=editMode?"完成":"编辑主题"; renderChips(); }); $("#chipWall").addEventListener("click",(e)=>{ const chip=e.target.closest(".chip"); if(!chip) return; const name=chip.dataset.name; if(editMode){ if(e.target.classList.contains("x")){ if(isBuiltin(name)) return; if(confirm(`删除主题「${name}」?`)){ delete CFG.customThemes[name]; CFG.galleryOrder=CFG.galleryOrder.filter(n=>n!==name); save(CFG); renderChips(); } } }else{ pickThemeByName(name); }}); let dragName=null; $("#chipWall").addEventListener("dragstart",(e)=>{ const c=e.target.closest(".chip"); if(!c) return; dragName=c.dataset.name; e.dataTransfer.effectAllowed="move"; }); $("#chipWall").addEventListener("dragover",(e)=>{ if(!dragName) return; e.preventDefault(); }); $("#chipWall").addEventListener("drop",(e)=>{ e.preventDefault(); const c=e.target.closest(".chip"); if(!c || !dragName) return; const target=c.dataset.name; if(target===dragName) return; const arr=galleryNames(), a=arr.indexOf(dragName), b=arr.indexOf(target); arr.splice(b,0,arr.splice(a,1)[0]); CFG.galleryOrder=arr; save(CFG); dragName=null; renderChips(); }); $("#chipWall").addEventListener("dragend",()=>{ dragName=null; renderChips(); }); renderChips(); function bindHex(hexId,colorId){ const hex=$(hexId), col=$(colorId); const setHex = () => { hex.value = (col.value || "#000000").toLowerCase(); }; const setCol=()=>{ const p=parseColorToHex(hex.value); if(p){ col.value=p; hex.value=p; } }; setHex(); col.addEventListener("input",()=>{ setHex(); readForm(); requestStyleUpdate(); }); hex.addEventListener("input",()=>{ setCol(); readForm(); requestStyleUpdate(); }); hex.addEventListener("change",()=>{ setCol(); readForm(); requestStyleUpdate(); }); hex.addEventListener("paste",()=>{ setTimeout(()=>{ setCol(); readForm(); requestStyleUpdate(); },0); }); } function bindPair(rangeId,numId,onChange){ const r=$(rangeId), n=$(numId); function sync(from){ if(from==="r"){ n.value=r.value; } else { r.value=n.value; } onChange&&onChange(+n.value); } r.addEventListener("input",()=>sync("r")); n.addEventListener("input",()=>sync("n")); sync("n"); } function applyWeightUI(w1,w2){ const w3=clamp(100-w1-w2,0,100); $("#w1r").value=w1; $("#w1").value=w1; $("#w2r").value=w2; $("#w2").value=w2; $("#w3r").value=w3; $("#w3").value=w3; $("#weightHint").textContent=`配重合计:${w1+w2+w3}%(建议=100%)`; } function readWeightUIToModel(){ let w1=+$("#w1").value||0, w2=+$("#w2").value||0; if(w1<0) w1=0; if(w1>100) w1=100; if(w2<0) w2=0; if(w1+w2>100) w2=100-w1; const w3=100-w1-w2; applyWeightUI(w1,w2); const s=CFG[ACTIVE]; s.weights=[w1,w2,w3]; return s.weights; } $("#w1r").addEventListener("input",()=>{ $("#w1").value=$("#w1r").value; readWeightUIToModel(); requestStyleUpdate(); }); $("#w2r").addEventListener("input",()=>{ $("#w2").value=$("#w2r").value; readWeightUIToModel(); requestStyleUpdate(); }); $("#w1").addEventListener("input",()=>{ readWeightUIToModel(); requestStyleUpdate(); }); $("#w2").addEventListener("input",()=>{ readWeightUIToModel(); requestStyleUpdate(); }); function fillForm(){ const s=CFG[ACTIVE]; $("#mode").value=s.mode; $("#angle").value=s.angle; $("#angleR").value=s.angle; $("#textColor").value=s.textColor||"#101418"; $("#textHex").value=$("#textColor").value.toLowerCase(); $("#autoText").checked=!!s.autoText; const dis=!!s.autoText; $("#textHex").disabled=dis; $("#textColor").disabled=dis; $("#autoText").onchange=()=>{ const on=$("#autoText").checked; $("#textHex").disabled=on; $("#textColor").disabled=on; readForm(); requestStyleUpdate(); }; $("#c1").value=s.colors[0]||"#000000"; $("#h1").value=$("#c1").value.toLowerCase(); $("#c2").value=s.colors[1]||"#000000"; $("#h2").value=$("#c2").value.toLowerCase(); $("#c3").value=s.colors[2]||"#000000"; $("#h3").value=$("#c3").value.toLowerCase(); const w1=s.weights?.[0]??34, w2=s.weights?.[1]??33; applyWeightUI(w1,w2); $("#soft").value=s.soft??SOFT; $("#softR").value=$("#soft").value; $("#alpha").value=s.alpha; $("#alphaR").value=s.alpha; $("#blur").value=s.blur; $("#blurR").value=s.blur; $("#glow").value=s.glow; $("#glowR").value=$("#glow").value; $("#outlineColor").value=s.outlineColor; $("#outlineHex").value=$("#outlineColor").value.toLowerCase(); $("#outlineAlpha").value=s.outlineAlpha; $("#outlineAlphaR").value=$("#outlineAlpha").value; $("#outlineWidth").value=s.outlineWidth; $("#outlineWidthR").value=$("#outlineWidth").value; $("#radius").value=s.radius; $("#radiusR").value=s.radius; $("#padV").value=s.padV; $("#padVR").value=$("#padV").value; $("#padH").value=s.padH; $("#padHR").value=$("#padH").value; bindHex("#h1","#c1"); bindHex("#h2","#c2"); bindHex("#h3","#c3"); bindHex("#textHex","#textColor"); bindHex("#outlineHex","#outlineColor"); bindPair("#angleR","#angle",()=>{ CFG[ACTIVE].angle=+$("#angle").value; }); bindPair("#softR","#soft",()=>{ CFG[ACTIVE].soft=+$("#soft").value; }); bindPair("#alphaR","#alpha",()=>{ CFG[ACTIVE].alpha=+$("#alpha").value; }); bindPair("#blurR","#blur",()=>{ CFG[ACTIVE].blur=+$("#blur").value; }); bindPair("#glowR","#glow",()=>{ CFG[ACTIVE].glow=+$("#glow").value; }); bindPair("#outlineAlphaR","#outlineAlpha",()=>{ CFG[ACTIVE].outlineAlpha=+$("#outlineAlpha").value; }); bindPair("#outlineWidthR","#outlineWidth",()=>{ CFG[ACTIVE].outlineWidth=+$("#outlineWidth").value; }); bindPair("#radiusR","#radius",()=>{ CFG[ACTIVE].radius=+$("#radius").value; }); bindPair("#padVR","#padV",()=>{ CFG[ACTIVE].padV=+$("#padV").value; }); bindPair("#padHR","#padH",()=>{ CFG[ACTIVE].padH=+$("#padH").value; }); requestStyleUpdate(); } function readForm(){ const s=CFG[ACTIVE]; s.mode=$("#mode").value; s.angle=+$("#angle").value; s.autoText=$("#autoText").checked; const parsedText=parseColorToHex($("#textHex").value)||$("#textColor").value; s.textColor=parsedText||s.textColor||"#101418"; s.colors=[$("#c1").value,$("#c2").value,$("#c3").value].filter(Boolean); s.weights=readWeightUIToModel(); s.soft=+$("#soft").value; s.alpha=+$("#alpha").value; s.blur=+$("#blur").value; s.glow=+$("#glow").value; const parsedOutline=parseColorToHex($("#outlineHex").value)||$("#outlineColor").value; s.outlineColor=parsedOutline||"#000000"; s.outlineAlpha=+$("#outlineAlpha").value; s.outlineWidth=+$("#outlineWidth").value; s.radius=+$("#radius").value; s.padV=+$("#padV").value; s.padH=+$("#padH").value; const tname=CFG.lastTheme[ACTIVE]; if(!CFG.overrides[ACTIVE]) CFG.overrides[ACTIVE]={}; CFG.overrides[ACTIVE][tname]=clone(s); save(CFG); } w.querySelector(".grid").addEventListener("input",()=>{ readForm(); requestStyleUpdate(); }); w.querySelector(".grid").addEventListener("change",()=>{ readForm(); requestStyleUpdate(); }); w.querySelector('[data-act="save"]').addEventListener("click",()=>{ save(CFG); alert("已保存配置"); }); w.querySelector('[data-act="savePreset"]').addEventListener("click",()=>{ const name=$("#presetName").value.trim(); if(!name){ alert("请输入预设名称"); return; } if(THEMES[name] && !confirm("同名内置主题已存在,是否覆盖为自定义?")) return; if(!CFG.customThemes) CFG.customThemes={}; CFG.customThemes[name]=clone(CFG[ACTIVE]); CFG.galleryOrder=[...new Set([name,...CFG.galleryOrder])]; CFG.lastTheme[ACTIVE]=name; delete (CFG.overrides[ACTIVE]||{})[name]; save(CFG); renderChips(); pickThemeByName(name); alert("已另存为自定义主题"); }); w.querySelector('[data-act="reset"]').addEventListener("click",()=>{ const name=CFG.lastTheme[ACTIVE]; const source=isBuiltin(name)?THEMES[name]:CFG.customThemes[name]; if(!source){ alert("未找到默认主题定义"); return; } if(CFG.overrides[ACTIVE]) delete CFG.overrides[ACTIVE][name]; CFG[ACTIVE]=clone(source); save(CFG); fillForm(); requestStyleUpdate(); alert("已重置为默认"); }); fillForm(); } function updatePreview(){ const wp = document.getElementById("ech0-preview"); if (!wp) return; const active = document.querySelector(`#${PANEL_ID} .tab.active`); const role = active ? active.dataset.role : "user"; const s = role === "assistant" ? CFG.assistant : CFG.user; const bg = makeBackground(s); const outline = rgba(s.outlineColor, s.outlineAlpha); const border = (s.outlineAlpha <= 0 || s.outlineWidth <= 0) ? "none" : `${s.outlineWidth}px solid ${outline}`; const text = s.autoText ? (themeLuma(s)>0.55 ? "#000" : "#fff") : (s.textColor || "#111"); const blurPx = IS_MOBILE ? Math.min(s.blur, 4) : s.blur; wp.style.background = bg; wp.style.border = border; wp.style.borderRadius = s.radius + "px"; wp.style.padding = `${s.padV}px ${s.padH}px`; wp.style.color = text; wp.style.webkitTextFillColor = text; wp.style.webkitBackdropFilter = `blur(${blurPx}px)`; wp.style.backdropFilter = `blur(${blurPx}px)`; wp.style.boxShadow = makeGlow(s.outlineColor, s.glow); wp.textContent = "正在输入中............."; } // 面板开关(只此一处声明) const togglePanel = () => { const p = document.getElementById(PANEL_ID); if (p) { p.remove(); } else { openPanel(); } }; document.addEventListener("keydown", (e) => { if (e.altKey && e.key.toLowerCase() === "g") { e.preventDefault(); togglePanel(); } }); GM_registerMenuCommand("打开/关闭设置面板 (Alt+G)", togglePanel); // 初次渲染 NEED_ENSURE = true; requestStyleUpdate(); // 监听新节点:仅置位“需要归拢”,由节流器统一更新 const mo = new MutationObserver(() => { NEED_ENSURE = true; requestStyleUpdate(); }); mo.observe(document.querySelector("main") || document.documentElement, { childList: true, subtree: true }); })();