// ==UserScript==
// @name [Chat] Template Text Folders [20251014] +fix1.0
// @namespace 0_V userscripts/[Chat] Template Text Folders
// @version [20251014]
// @description 在AI页面上添加预设文本文件夹和按钮,提升输入效率。
// @update-log insertTextSmart Fixed
//
// @match https://chatgpt.com/*
// @match https://chat01.ai/*
//
// @match https://claude.*/*
// @match https://*.fuclaude.com/*
//
// @match https://gemini.google.com/*
// @match https://aistudio.google.com/*
//
// @match https://copilot.microsoft.com/*
//
// @match https://grok.com/*
// @match https://grok.dairoot.cn/*
//
// @match https://chat.deepseek.com/*
// @match https://chat.mistral.ai/*
//
// @match https://*.perplexity.ai/*
//
// @match https://poe.com/*
// @match https://kagi.com/assistant*
// @match https://lmarena.ai/*
// @match https://app.chathub.gg/*
// @match https://monica.im/home*
//
// @match https://setapp.typingcloud.com/*
//
// @match https://*.mynanian.top/*
// @match https://free.share-ai.top/*
// @match https://chat.kelaode.ai/*
//
// @match https://*.aivvm.*/*
// @match https://linux.do/discourse-ai/ai-bot/*
//
// @match https://cursor.com/*
//
// @match https://www.notion.so/*
//
// @grant none
// @icon https://github.com/0-V-linuxdo/Chat_Template_Text_Folders/raw/refs/heads/main/Icon.svg
// @downloadURL https://update.greasyfork.icu/scripts/552640/%5BChat%5D%20Template%20Text%20Folders%20%5B20251014%5D%20%2Bfix10.user.js
// @updateURL https://update.greasyfork.icu/scripts/552640/%5BChat%5D%20Template%20Text%20Folders%20%5B20251014%5D%20%2Bfix10.meta.js
// ==/UserScript==
(function () {
'use strict';
console.log("🎉 [Chat] Template Text Folders [20251013] 🎉");
let trustedHTMLPolicy = null;
const resolveTrustedTypes = () => {
if (trustedHTMLPolicy) {
return trustedHTMLPolicy;
}
const globalObj = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
const trustedTypesAPI = globalObj && globalObj.trustedTypes ? globalObj.trustedTypes : null;
if (!trustedTypesAPI) {
return null;
}
try {
trustedHTMLPolicy = trustedTypesAPI.createPolicy('chat_template_text_folders_policy', {
createHTML: (input) => input
});
} catch (error) {
console.warn('[Chat] Template Text Folders Trusted Types policy creation failed', error);
trustedHTMLPolicy = null;
}
return trustedHTMLPolicy;
};
const setTrustedHTML = (element, html) => {
if (!element) {
return;
}
const value = typeof html === 'string' ? html : (html == null ? '' : String(html));
const policy = resolveTrustedTypes();
if (policy) {
element.innerHTML = policy.createHTML(value);
} else {
element.innerHTML = value;
}
};
const UI_HOST_ID = 'cttf-ui-host';
let latestThemeValues = null;
let uiShadowRoot = null;
let uiMainLayer = null;
let uiOverlayLayer = null;
const ensureUIRoot = () => {
if (uiShadowRoot && uiShadowRoot.host && uiShadowRoot.host.isConnected) {
return uiShadowRoot;
}
if (!document.body) {
return null;
}
let hostElement = document.getElementById(UI_HOST_ID);
if (!hostElement) {
hostElement = document.createElement('div');
hostElement.id = UI_HOST_ID;
document.body.appendChild(hostElement);
}
uiShadowRoot = hostElement.shadowRoot;
if (!uiShadowRoot) {
uiShadowRoot = hostElement.attachShadow({ mode: 'open' });
const baseStyle = document.createElement('style');
baseStyle.textContent = `
:host {
all: initial;
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
:host *, :host *::before, :host *::after {
box-sizing: border-box;
font-family: inherit;
}
.cttf-dialog,
.cttf-dialog * {
scrollbar-width: thin;
scrollbar-color: var(--cttf-scrollbar-thumb, rgba(120, 120, 120, 0.6)) transparent;
}
.cttf-dialog::-webkit-scrollbar,
.cttf-dialog *::-webkit-scrollbar {
width: 6px;
height: 6px;
background: transparent;
}
.cttf-dialog::-webkit-scrollbar-track,
.cttf-dialog *::-webkit-scrollbar-track {
background: transparent;
border: none;
margin: 0;
}
.cttf-dialog::-webkit-scrollbar-thumb,
.cttf-dialog *::-webkit-scrollbar-thumb {
background-color: var(--cttf-scrollbar-thumb, rgba(120, 120, 120, 0.6));
border-radius: 999px;
border: none;
}
.cttf-dialog::-webkit-scrollbar-corner,
.cttf-dialog *::-webkit-scrollbar-corner {
background: transparent;
}
.cttf-dialog::-webkit-scrollbar-button,
.cttf-dialog *::-webkit-scrollbar-button {
display: none;
}
.hide-scrollbar {
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
`;
uiShadowRoot.appendChild(baseStyle);
}
if (!uiMainLayer || !uiMainLayer.isConnected) {
uiMainLayer = uiShadowRoot.getElementById('cttf-main-layer');
if (!uiMainLayer) {
uiMainLayer = document.createElement('div');
uiMainLayer.id = 'cttf-main-layer';
uiMainLayer.style.position = 'fixed';
uiMainLayer.style.inset = '0';
uiMainLayer.style.pointerEvents = 'none';
uiShadowRoot.appendChild(uiMainLayer);
}
}
if (!uiOverlayLayer || !uiOverlayLayer.isConnected) {
uiOverlayLayer = uiShadowRoot.getElementById('cttf-overlay-layer');
if (!uiOverlayLayer) {
uiOverlayLayer = document.createElement('div');
uiOverlayLayer.id = 'cttf-overlay-layer';
uiOverlayLayer.style.position = 'fixed';
uiOverlayLayer.style.inset = '0';
uiOverlayLayer.style.pointerEvents = 'none';
uiOverlayLayer.style.zIndex = '20000';
uiShadowRoot.appendChild(uiOverlayLayer);
}
}
if (latestThemeValues && hostElement) {
Object.entries(latestThemeValues).forEach(([key, value]) => {
hostElement.style.setProperty(toCSSVariableName(key), value);
});
}
return uiShadowRoot;
};
const getShadowRoot = () => ensureUIRoot();
const getMainLayer = () => {
const root = ensureUIRoot();
return root ? uiMainLayer : null;
};
const getOverlayLayer = () => {
const root = ensureUIRoot();
return root ? uiOverlayLayer : null;
};
const appendToMainLayer = (node) => {
const container = getMainLayer();
return container ? container.appendChild(node) : document.body.appendChild(node);
};
const appendToOverlayLayer = (node) => {
const container = getOverlayLayer();
if (container) {
container.appendChild(node);
} else {
document.body.appendChild(node);
}
return node;
};
const queryUI = (selector) => {
const root = getShadowRoot();
return root ? root.querySelector(selector) : document.querySelector(selector);
};
const toCSSVariableName = (key) => `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
/**
* 自动根据内容调整 textarea 高度,确保上下内边距空间充足。
* @param {HTMLTextAreaElement} textarea
* @param {{minRows?: number, maxRows?: number}} options
*/
const autoResizeTextarea = (textarea, options = {}) => {
if (!textarea) return;
const { minRows = 1, maxRows = 5 } = options;
textarea.style.height = 'auto';
const styles = window.getComputedStyle(textarea);
const lineHeight = parseFloat(styles.lineHeight) || (parseFloat(styles.fontSize) * 1.2) || 20;
const paddingTop = parseFloat(styles.paddingTop) || 0;
const paddingBottom = parseFloat(styles.paddingBottom) || 0;
const borderTop = parseFloat(styles.borderTopWidth) || 0;
const borderBottom = parseFloat(styles.borderBottomWidth) || 0;
const minHeight = (lineHeight * minRows) + paddingTop + paddingBottom + borderTop + borderBottom;
const maxHeight = (lineHeight * maxRows) + paddingTop + paddingBottom + borderTop + borderBottom;
let targetHeight = textarea.scrollHeight;
if (targetHeight < minHeight) {
targetHeight = minHeight;
} else if (targetHeight > maxHeight) {
targetHeight = maxHeight;
textarea.style.overflowY = 'auto';
} else {
textarea.style.overflowY = 'hidden';
}
textarea.style.minHeight = `${minHeight}px`;
textarea.style.maxHeight = `${maxHeight}px`;
textarea.style.height = `${targetHeight}px`;
};
const SVG_NS = 'http://www.w3.org/2000/svg';
const createAutoFaviconIcon = () => {
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('viewBox', '0 0 32 32');
svg.setAttribute('data-name', 'Layer 1');
svg.setAttribute('id', 'Layer_1');
svg.setAttribute('fill', '#000000');
svg.setAttribute('xmlns', SVG_NS);
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
svg.style.width = '18px';
svg.style.height = '18px';
svg.style.display = 'block';
const segments = [
{ d: 'M23.75,16A7.7446,7.7446,0,0,1,8.7177,18.6259L4.2849,22.1721A13.244,13.244,0,0,0,29.25,16', fill: '#00ac47' },
{ d: 'M23.75,16a7.7387,7.7387,0,0,1-3.2516,6.2987l4.3824,3.5059A13.2042,13.2042,0,0,0,29.25,16', fill: '#4285f4' },
{ d: 'M8.25,16a7.698,7.698,0,0,1,.4677-2.6259L4.2849,9.8279a13.177,13.177,0,0,0,0,12.3442l4.4328-3.5462A7.698,7.698,0,0,1,8.25,16Z', fill: '#ffba00' },
{ d: 'M16,8.25a7.699,7.699,0,0,1,4.558,1.4958l4.06-3.7893A13.2152,13.2152,0,0,0,4.2849,9.8279l4.4328,3.5462A7.756,7.756,0,0,1,16,8.25Z', fill: '#ea4435' },
{ d: 'M29.25,15v1L27,19.5H16.5V14H28.25A1,1,0,0,1,29.25,15Z', fill: '#4285f4' }
];
segments.forEach(({ d, fill }) => {
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('d', d);
path.setAttribute('fill', fill);
svg.appendChild(path);
});
return svg;
};
// 用于统一创建 overlay + dialog,样式与默认逻辑保持一致
// 复用时只需传入自定义的内容与回调,外观也可统一
function createUnifiedDialog(options) {
const {
title = '弹窗标题',
width = '400px',
maxHeight = '80vh',
onClose = null, // 关闭时的回调
closeOnOverlayClick = true
} = options;
// 创建overlay
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'var(--overlay-bg, rgba(0,0,0,0.5))';
overlay.style.backdropFilter = 'blur(2px)';
overlay.style.zIndex = '12000';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.3s ease';
// 创建dialog
const dialog = document.createElement('div');
dialog.classList.add('cttf-dialog');
dialog.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
dialog.style.color = 'var(--text-color, #333333)';
dialog.style.borderRadius = '4px';
dialog.style.padding = '24px';
dialog.style.boxShadow = '0 8px 24px var(--shadow-color, rgba(0,0,0,0.1))';
dialog.style.border = '1px solid var(--border-color, #e5e7eb)';
dialog.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
dialog.style.width = width;
dialog.style.maxWidth = '95vw';
dialog.style.maxHeight = maxHeight;
dialog.style.overflowY = 'auto';
dialog.style.transform = 'scale(0.95)';
// 标题
const titleEl = document.createElement('h2');
titleEl.textContent = title;
titleEl.style.margin = '0';
titleEl.style.marginBottom = '12px';
titleEl.style.fontSize = '18px';
titleEl.style.fontWeight = '600';
dialog.appendChild(titleEl);
// 向overlay添加dialog
overlay.appendChild(dialog);
// 将 overlay 挂载到 Shadow DOM 覆盖层
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
// 入场动画
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
if (closeOnOverlayClick) {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
if (onClose) onClose();
overlay.remove();
}
});
} else {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
e.stopPropagation();
e.preventDefault();
}
});
}
return { overlay, dialog };
}
// 主题样式配置(使用CSS变量)
const applyThemeToHost = (themeValues) => {
const root = ensureUIRoot();
const host = root ? root.host : null;
if (!host) {
return;
}
Object.entries(themeValues).forEach(([key, value]) => {
host.style.setProperty(toCSSVariableName(key), value);
});
};
const setCSSVariables = (currentTheme) => {
latestThemeValues = currentTheme;
const apply = () => applyThemeToHost(currentTheme);
if (document.body) {
apply();
} else {
window.addEventListener('DOMContentLoaded', apply, { once: true });
}
};
const theme = {
light: {
folderBg: 'rgba(255, 255, 255, 0.8)',
dialogBg: '#ffffff',
textColor: '#333333',
borderColor: '#e5e7eb',
shadowColor: 'rgba(0, 0, 0, 0.1)',
buttonBg: '#f3f4f6',
buttonHoverBg: '#e5e7eb',
dangerColor: '#ef4444',
successColor: '#22c55e',
addColor: '#fd7e14',
primaryColor: '#3B82F6',
infoColor: '#6366F1',
cancelColor: '#6B7280',
overlayBg: 'rgba(0, 0, 0, 0.5)',
tabBg: '#f3f4f6',
tabActiveBg: '#3B82F6',
tabHoverBg: '#e5e7eb',
tabBorder: '#e5e7eb'
},
dark: {
folderBg: 'rgba(31, 41, 55, 0.8)',
dialogBg: '#1f2937',
textColor: '#e5e7eb',
borderColor: '#374151',
shadowColor: 'rgba(0, 0, 0, 0.3)',
buttonBg: '#374151',
buttonHoverBg: '#4b5563',
dangerColor: '#dc2626',
successColor: '#16a34a',
addColor: '#fd7e14',
primaryColor: '#2563EB',
infoColor: '#4F46E5',
cancelColor: '#4B5563',
overlayBg: 'rgba(0, 0, 0, 0.7)',
tabBg: '#374151',
tabActiveBg: '#2563EB',
tabHoverBg: '#4b5563',
tabBorder: '#4b5563'
}
};
const isDarkMode = () => window.matchMedia('(prefers-color-scheme: dark)').matches;
const getCurrentTheme = () => isDarkMode() ? theme.dark : theme.light;
setCSSVariables(getCurrentTheme());
const styles = {
overlay: {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'var(--overlay-bg, rgba(0, 0, 0, 0.5))',
backdropFilter: 'blur(2px)',
zIndex: 10000,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transition: 'background-color 0.3s ease, opacity 0.3s ease'
},
dialog: {
position: 'relative',
backgroundColor: 'var(--dialog-bg, #ffffff)',
color: 'var(--text-color, #333333)',
borderRadius: '4px',
padding: '24px',
boxShadow: '0 8px 24px var(--shadow-color, rgba(0,0,0,0.1))',
border: '1px solid var(--border-color, #e5e7eb)',
transition: 'transform 0.3s ease, opacity 0.3s ease',
maxWidth: '90vw',
maxHeight: '80vh',
overflow: 'auto'
},
button: {
padding: '8px 16px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.2s ease, color 0.2s ease',
fontSize: '14px',
fontWeight: '500',
backgroundColor: 'var(--button-bg, #f3f4f6)',
color: 'var(--text-color, #333333)'
}
};
// 默认按钮
const userProvidedButtons = {
"Review": {
type: "template",
text: "You are a code review expert:\n\n{clipboard}\n\nProvide constructive feedback and improvements.\n",
color: "#E6E0FF",
textColor: "#333333",
autoSubmit: false // 新增字段
},
// ... (其他默认按钮保持不变)
"解释": {
type: "template",
text: "Explain the following code concisely:\n\n{clipboard}\n\nFocus on key functionality and purpose.\n",
color: "#ffebcc",
textColor: "#333333",
autoSubmit: false // 新增字段
}
};
// 默认工具按钮
const defaultToolButtons = {
"剪切": {
type: "tool",
action: "cut",
color: "#FFC1CC",
textColor: "#333333"
},
"复制": {
type: "tool",
action: "copy",
color: "#C1FFD7",
textColor: "#333333"
},
"粘贴": {
type: "tool",
action: "paste",
color: "#C1D8FF",
textColor: "#333333"
},
"清空": {
type: "tool",
action: "clear",
color: "#FFD1C1",
textColor: "#333333"
}
};
const TOOL_DEFAULT_ICONS = {
cut: '✂️',
copy: '📋',
paste: '📥',
clear: '✖️'
};
const generateDomainFavicon = (domain) => {
if (!domain) return '';
const trimmed = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(trimmed)}&sz=32`;
};
const createFaviconElement = (faviconUrl, label, fallbackEmoji = '🌐', options = {}) => {
const { withBackground = true, size = 32 } = options || {};
const normalizedSize = Math.max(16, Math.min(48, Number(size) || 32));
const contentSize = Math.max(12, normalizedSize - 4);
const emojiFontSize = Math.max(10, normalizedSize - 10);
const borderRadius = Math.round(normalizedSize / 4);
const wrapper = document.createElement('div');
wrapper.style.width = `${normalizedSize}px`;
wrapper.style.height = `${normalizedSize}px`;
wrapper.style.borderRadius = `${borderRadius}px`;
wrapper.style.overflow = 'hidden';
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.style.justifyContent = 'center';
wrapper.style.backgroundColor = withBackground ? 'rgba(148, 163, 184, 0.15)' : 'transparent';
wrapper.style.flexShrink = '0';
if (faviconUrl) {
const img = document.createElement('img');
img.src = faviconUrl;
img.alt = label || 'site icon';
img.style.width = `${contentSize}px`;
img.style.height = `${contentSize}px`;
img.style.objectFit = 'cover';
img.referrerPolicy = 'no-referrer';
img.loading = 'lazy';
img.onerror = () => {
setTrustedHTML(wrapper, '');
const emoji = document.createElement('span');
emoji.textContent = fallbackEmoji;
emoji.style.fontSize = `${emojiFontSize}px`;
wrapper.appendChild(emoji);
};
wrapper.appendChild(img);
} else {
const emoji = document.createElement('span');
emoji.textContent = fallbackEmoji;
emoji.style.fontSize = `${emojiFontSize}px`;
wrapper.appendChild(emoji);
}
return wrapper;
};
// 默认配置
const defaultConfig = {
folders: {
"默认": {
color: "#3B82F6",
textColor: "#ffffff",
hidden: false, // 新增隐藏状态字段
buttons: userProvidedButtons
},
"🖱️": {
color: "#FFD700", // 金色,可根据需求调整
textColor: "#ffffff",
hidden: false, // 新增隐藏状态字段
buttons: defaultToolButtons
}
},
folderOrder: ["默认", "🖱️"],
domainAutoSubmitSettings: [
{
domain: "chatgpt.com",
name: "ChatGPT",
method: "模拟点击提交按钮",
favicon: generateDomainFavicon("chatgpt.com")
},
{
domain: "chathub.gg",
name: "ChatHub",
method: "Enter",
favicon: generateDomainFavicon("chathub.gg")
}
],
/**
* domainStyleSettings: 数组,每个元素结构示例:
* {
* domain: "chatgpt.com",
* name: "ChatGPT自定义样式",
* height: 90,
* cssCode: ".some-class { color: red; }"
* }
*/
domainStyleSettings: []
};
defaultConfig.buttonBarHeight = 40;
defaultConfig.buttonBarBottomSpacing = 0;
let buttonConfig = JSON.parse(localStorage.getItem('chatGPTButtonFoldersConfig')) || JSON.parse(JSON.stringify(defaultConfig));
if (!Array.isArray(buttonConfig.domainStyleSettings)) {
buttonConfig.domainStyleSettings = [];
}
if (typeof buttonConfig.buttonBarHeight !== 'number') {
buttonConfig.buttonBarHeight = defaultConfig.buttonBarHeight;
}
if (typeof buttonConfig.buttonBarBottomSpacing !== 'number') {
buttonConfig.buttonBarBottomSpacing = defaultConfig.buttonBarBottomSpacing;
}
buttonConfig.buttonBarBottomSpacing = Math.max(-200, Math.min(200, Number(buttonConfig.buttonBarBottomSpacing) || 0));
// 若本地无此字段,则初始化
if (!buttonConfig.domainAutoSubmitSettings) {
buttonConfig.domainAutoSubmitSettings = JSON.parse(
JSON.stringify(defaultConfig.domainAutoSubmitSettings)
);
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
}
// 确保所有按钮都有'type'字段
const ensureButtonTypes = () => {
let updated = false;
Object.entries(buttonConfig.folders).forEach(([folderName, folderConfig]) => {
// 确保文件夹有hidden字段
if (typeof folderConfig.hidden !== 'boolean') {
folderConfig.hidden = false;
updated = true;
}
Object.entries(folderConfig.buttons).forEach(([btnName, btnConfig]) => {
if (!btnConfig.type) {
if (folderName === "🖱️") {
btnConfig.type = "tool";
updated = true;
} else {
btnConfig.type = "template";
updated = true;
}
}
// 确保 'autoSubmit' 字段存在,对于模板按钮
if (btnConfig.type === "template" && typeof btnConfig.autoSubmit !== 'boolean') {
btnConfig.autoSubmit = false;
updated = true;
}
if (btnConfig.type === "template" && typeof btnConfig.favicon !== 'string') {
btnConfig.favicon = '';
updated = true;
}
});
});
if (updated) {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
console.log("✅ 已确保所有按钮具有'type'、'autoSubmit'、'favicon'配置,以及文件夹具有'hidden'字段。");
}
};
const ensureDomainMetadata = () => {
let updated = false;
(buttonConfig.domainAutoSubmitSettings || []).forEach(rule => {
if (!rule.favicon) {
rule.favicon = generateDomainFavicon(rule.domain);
updated = true;
}
});
(buttonConfig.domainStyleSettings || []).forEach(item => {
if (!item.favicon) {
item.favicon = generateDomainFavicon(item.domain);
updated = true;
}
if (typeof item.bottomSpacing !== 'number') {
item.bottomSpacing = buttonConfig.buttonBarBottomSpacing;
updated = true;
} else {
const clamped = Math.max(-200, Math.min(200, Number(item.bottomSpacing) || 0));
if (clamped !== item.bottomSpacing) {
item.bottomSpacing = clamped;
updated = true;
}
}
});
if (updated) {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
console.log("✅ 已为自动化与样式配置补全 favicon 信息。");
}
};
ensureButtonTypes();
ensureDomainMetadata();
// 确保工具文件夹存在并包含必要的工具按钮
const ensureToolFolder = () => {
const toolFolderName = "🖱️";
if (!buttonConfig.folders[toolFolderName]) {
buttonConfig.folders[toolFolderName] = {
color: "#FFD700",
textColor: "#ffffff",
buttons: defaultToolButtons
};
buttonConfig.folderOrder.push(toolFolderName);
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
console.log(`✅ 工具文件夹 "${toolFolderName}" 已添加到配置中。`);
} else {
// 确保工具按钮存在
Object.entries(defaultToolButtons).forEach(([btnName, btnCfg]) => {
if (!buttonConfig.folders[toolFolderName].buttons[btnName]) {
buttonConfig.folders[toolFolderName].buttons[btnName] = btnCfg;
console.log(`✅ 工具按钮 "${btnName}" 已添加到文件夹 "${toolFolderName}"。`);
}
});
}
};
ensureToolFolder();
// 变量:防止重复提交
let isSubmitting = false;
// 占位函数,避免在真正实现前调用报错
let applyDomainStyles = () => {};
let updateButtonBarLayout = () => {};
const getAllTextareas = (root = document) => {
let textareas = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
let node = walker.nextNode();
while (node) {
if (node.tagName && (node.tagName.toLowerCase() === 'textarea' || node.getAttribute('contenteditable') === 'true')) {
textareas.push(node);
}
if (node.shadowRoot) {
textareas = textareas.concat(getAllTextareas(node.shadowRoot));
}
node = walker.nextNode();
}
return textareas;
};
/**
* 增强版插入文本到textarea或contenteditable元素中,支持现代编辑器
* @param {HTMLElement} target - 目标元素
* @param {string} finalText - 要插入的文本
* @param {boolean} replaceAll - 是否替换全部内容
*/
const insertTextSmart = (target, finalText, replaceAll = false) => {
const normalizedText = finalText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
if (target.tagName.toLowerCase() === 'textarea') {
// 处理textarea - 保持原有逻辑
if (replaceAll) {
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
nativeSetter.call(target, normalizedText);
target.selectionStart = target.selectionEnd = normalizedText.length;
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertReplacementText',
data: normalizedText,
});
target.dispatchEvent(inputEvent);
} else {
const start = target.selectionStart;
const end = target.selectionEnd;
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
nativeSetter.call(target, target.value.substring(0, start) + normalizedText + target.value.substring(end));
target.selectionStart = target.selectionEnd = start + normalizedText.length;
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: normalizedText,
});
target.dispatchEvent(inputEvent);
}
target.focus();
} else if (target.getAttribute('contenteditable') === 'true') {
// 增强的contenteditable处理
insertIntoContentEditable(target, normalizedText, replaceAll);
}
};
/**
* 专门处理contenteditable元素的文本插入
* @param {HTMLElement} target - contenteditable元素
* @param {string} text - 要插入的文本
* @param {boolean} replaceAll - 是否替换全部内容
*/
const insertIntoContentEditable = (target, text, replaceAll) => {
// 检测编辑器类型
const editorType = detectEditorType(target);
target.focus();
if (replaceAll) {
// 替换全部内容
clearContentEditable(target, editorType);
}
// 插入文本
insertTextIntoEditor(target, text, editorType);
// 触发事件和调整高度
triggerEditorEvents(target, text, replaceAll);
adjustEditorHeight(target, editorType);
};
/**
* 检测编辑器类型
* @param {HTMLElement} target
* @returns {string} 编辑器类型
*/
const detectEditorType = (target) => {
// 检测ProseMirror
if (target.classList.contains('ProseMirror') ||
target.closest('.ProseMirror') ||
target.querySelector('.ProseMirror-trailingBreak')) {
return 'prosemirror';
}
// 检测其他特殊编辑器
if (target.hasAttribute('data-placeholder') ||
target.querySelector('[data-placeholder]')) {
return 'modern';
}
// 默认简单contenteditable
return 'simple';
};
/**
* 清空contenteditable内容
* @param {HTMLElement} target
* @param {string} editorType
*/
const clearContentEditable = (target, editorType) => {
if (editorType === 'prosemirror') {
// ProseMirror需要保持基本结构
const firstP = target.querySelector('p');
if (firstP) {
setTrustedHTML(firstP, '
');
// 删除其他段落
const otherPs = target.querySelectorAll('p:not(:first-child)');
otherPs.forEach(p => p.remove());
} else {
setTrustedHTML(target, '
');
}
} else {
setTrustedHTML(target, '');
}
};
/**
* 向编辑器插入文本
* @param {HTMLElement} target
* @param {string} text
* @param {string} editorType
*/
const insertTextIntoEditor = (target, text, editorType) => {
const selection = window.getSelection();
if (editorType === 'prosemirror') {
insertIntoProseMirror(target, text, selection);
} else {
insertIntoSimpleEditor(target, text, selection);
}
// 确保光标位置正确
const range = document.createRange();
range.selectNodeContents(target);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
};
/**
* 向ProseMirror编辑器插入文本
* @param {HTMLElement} target
* @param {string} text
* @param {Selection} selection
*/
const insertIntoProseMirror = (target, text, selection) => {
const lines = text.split('\n');
let currentP = target.querySelector('p');
if (!currentP) {
currentP = document.createElement('p');
target.appendChild(currentP);
}
// 清除现有内容但保持结构
const trailingBreak = currentP.querySelector('.ProseMirror-trailingBreak');
if (trailingBreak) {
trailingBreak.remove();
}
lines.forEach((line, index) => {
if (index > 0) {
// 创建新段落
currentP = document.createElement('p');
target.appendChild(currentP);
}
if (line.trim() === '') {
// 空行需要br
const br = document.createElement('br');
br.className = 'ProseMirror-trailingBreak';
currentP.appendChild(br);
} else {
// 有内容的行
currentP.appendChild(document.createTextNode(line));
if (index === lines.length - 1) {
// 最后一行添加trailing break
const br = document.createElement('br');
br.className = 'ProseMirror-trailingBreak';
currentP.appendChild(br);
}
}
});
// 移除is-empty类
target.classList.remove('is-empty', 'is-editor-empty');
target.querySelectorAll('p').forEach(p => {
p.classList.remove('is-empty', 'is-editor-empty');
});
};
/**
* 向简单编辑器插入文本
* @param {HTMLElement} target
* @param {string} text
* @param {Selection} selection
*/
const insertIntoSimpleEditor = (target, text, selection) => {
const lines = text.split('\n');
const fragment = document.createDocumentFragment();
lines.forEach((line, index) => {
if (line === '') {
fragment.appendChild(document.createElement('br'));
} else {
fragment.appendChild(document.createTextNode(line));
}
if (index < lines.length - 1) {
fragment.appendChild(document.createElement('br'));
}
});
// 使用Selection API插入
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(fragment);
range.collapse(false);
} else {
target.appendChild(fragment);
}
};
/**
* 触发编辑器事件
* @param {HTMLElement} target
* @param {string} text
* @param {boolean} replaceAll
*/
const triggerEditorEvents = (target, text, replaceAll) => {
// 触发多种事件确保兼容性
const events = [
new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: replaceAll ? 'insertReplacementText' : 'insertText',
data: text
}),
new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: replaceAll ? 'insertReplacementText' : 'insertText',
data: text
}),
new Event('change', { bubbles: true }),
new KeyboardEvent('keyup', { bubbles: true }),
new Event('blur', { bubbles: true }),
new Event('focus', { bubbles: true })
];
events.forEach(event => {
try {
target.dispatchEvent(event);
} catch (e) {
console.warn('事件触发失败:', e);
}
});
// 特殊处理:触发compositionend事件(某些框架需要)
try {
const compositionEvent = new CompositionEvent('compositionend', {
bubbles: true,
data: text
});
target.dispatchEvent(compositionEvent);
} catch (e) {
// CompositionEvent可能不被支持,忽略错误
}
};
/**
* 调整编辑器高度
* @param {HTMLElement} target
* @param {string} editorType
*/
const adjustEditorHeight = (target, editorType) => {
// 查找可能需要调整的容器
const containers = [
target,
target.parentElement,
target.closest('[style*="height"]'),
target.closest('[style*="max-height"]'),
target.closest('.overflow-hidden'),
target.closest('[style*="overflow"]')
].filter(Boolean);
containers.forEach(container => {
try {
// 移除可能阻止显示的样式
if (container.style.height && container.style.height !== 'auto') {
const currentHeight = parseInt(container.style.height);
if (currentHeight < 100) { // 只调整明显过小的高度
container.style.height = 'auto';
container.style.minHeight = currentHeight + 'px';
}
}
// 确保显示滚动条
if (container.style.overflowY === 'hidden') {
container.style.overflowY = 'auto';
}
// 对于特定的编辑器容器,强制最小高度
if (editorType === 'prosemirror' && container === target) {
container.style.minHeight = '3rem';
}
} catch (e) {
console.warn('高度调整失败:', e);
}
});
// 触发resize事件
setTimeout(() => {
try {
window.dispatchEvent(new Event('resize'));
target.dispatchEvent(new Event('resize'));
} catch (e) {
console.warn('resize事件触发失败:', e);
}
}, 100);
};
/**
* 轮询检测输入框内容是否与预期文本一致。
* @param {HTMLElement} element - 要检测的textarea或contenteditable元素。
* @param {string} expectedText - 期望出现的文本。
* @param {number} interval - 轮询时间间隔(毫秒)。
* @param {number} maxWait - 最大等待时长(毫秒),超时后reject。
* @returns {Promise} - 匹配成功resolve,否则reject。
*/
async function waitForContentMatch(element, expectedText, interval = 100, maxWait = 3000) {
return new Promise((resolve, reject) => {
let elapsed = 0;
const timer = setInterval(() => {
elapsed += interval;
const currentVal = (element.tagName.toLowerCase() === 'textarea')
? element.value
: element.innerText; // contenteditable时用innerText
if (currentVal === expectedText) {
clearInterval(timer);
resolve();
} else if (elapsed >= maxWait) {
clearInterval(timer);
reject(new Error("waitForContentMatch: 超时,输入框内容未能匹配预期文本"));
}
}, interval);
});
}
// 定义等待提交按钮的函数
const waitForSubmitButton = async (maxAttempts = 10, delay = 300) => {
for (let i = 0; i < maxAttempts; i++) {
const submitButton = document.querySelector('button[type="submit"], button[data-testid="send-button"]');
if (submitButton && !submitButton.disabled && submitButton.offsetParent !== null) {
return submitButton;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
return null;
};
// 定义等待时间和尝试次数
const SUBMIT_WAIT_MAX_ATTEMPTS = 10;
const SUBMIT_WAIT_DELAY = 300; // 毫秒
const waitForElementBySelector = async (selector, maxAttempts = SUBMIT_WAIT_MAX_ATTEMPTS, delay = SUBMIT_WAIT_DELAY) => {
if (!selector) return null;
for (let i = 0; i < maxAttempts; i++) {
let element = null;
try {
element = document.querySelector(selector);
} catch (error) {
console.warn(`⚠️ 自定义选择器 "${selector}" 解析失败:`, error);
return null;
}
if (element) {
const isDisabled = typeof element.disabled === 'boolean' && element.disabled;
if (!isDisabled) {
return element;
}
}
await new Promise(resolve => setTimeout(resolve, delay));
}
return null;
};
function simulateEnterKey() {
const eventInit = {
bubbles: true,
cancelable: true,
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13
};
const keyboardEvent = new KeyboardEvent('keydown', eventInit);
document.activeElement.dispatchEvent(keyboardEvent);
}
function simulateCmdEnterKey() {
const eventInit = {
bubbles: true,
cancelable: true,
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
metaKey: true
};
const keyboardEvent = new KeyboardEvent('keydown', eventInit);
document.activeElement.dispatchEvent(keyboardEvent);
}
function simulateCtrlEnterKey() {
const eventInit = {
bubbles: true,
cancelable: true,
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
ctrlKey: true
};
const keyboardEvent = new KeyboardEvent('keydown', eventInit);
document.activeElement.dispatchEvent(keyboardEvent);
}
// 定义多种提交方式
const submitForm = async () => {
if (isSubmitting) {
console.warn("⚠️ 提交正在进行中,跳过重复提交。");
return false;
}
isSubmitting = true;
try {
const domainRules = buttonConfig.domainAutoSubmitSettings || [];
const currentURL = window.location.href;
const matchedRule = domainRules.find(rule => currentURL.includes(rule.domain));
if (matchedRule) {
console.log("🔎 检测到本域名匹配的自动提交规则:", matchedRule);
switch (matchedRule.method) {
case "Enter": {
simulateEnterKey();
isSubmitting = false;
return true;
}
case "Cmd+Enter": {
const variant = matchedRule.methodAdvanced && matchedRule.methodAdvanced.variant === 'ctrl'
? 'ctrl'
: 'cmd';
if (variant === 'ctrl') {
simulateCtrlEnterKey();
console.log("✅ 已根据自动化规则,触发 Ctrl + Enter 提交。");
} else {
simulateCmdEnterKey();
console.log("✅ 已根据自动化规则,触发 Cmd + Enter 提交。");
}
isSubmitting = false;
return true;
}
case "模拟点击提交按钮": {
const advanced = matchedRule.methodAdvanced || {};
const selector = typeof advanced.selector === 'string' ? advanced.selector.trim() : '';
if (advanced.variant === 'selector' && selector) {
const customButton = await waitForElementBySelector(selector, SUBMIT_WAIT_MAX_ATTEMPTS, SUBMIT_WAIT_DELAY);
if (customButton) {
customButton.click();
console.log(`✅ 已根据自动化规则,自定义选择器 "${selector}" 提交。`);
isSubmitting = false;
return true;
}
console.warn(`⚠️ 自定义选择器 "${selector}" 未匹配到提交按钮,尝试默认规则。`);
}
const submitButton = await waitForSubmitButton(SUBMIT_WAIT_MAX_ATTEMPTS, SUBMIT_WAIT_DELAY);
if (submitButton) {
submitButton.click();
console.log("✅ 已根据自动化规则,模拟点击提交按钮。");
isSubmitting = false;
return true;
} else {
console.warn("⚠️ 未找到提交按钮,进入fallback...");
}
break;
}
default:
console.warn("⚠️ 未知自动提交方式,进入fallback...");
break;
}
}
// 1. 尝试键盘快捷键提交
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const submitKeys = isMac ? ['Enter', 'Meta+Enter'] : ['Enter', 'Control+Enter'];
for (const keyCombo of submitKeys) {
const [key, modifier] = keyCombo.split('+');
const eventInit = {
bubbles: true,
cancelable: true,
key: key,
code: key,
keyCode: key.charCodeAt(0),
which: key.charCodeAt(0),
};
if (modifier === 'Meta') eventInit.metaKey = true;
if (modifier === 'Control') eventInit.ctrlKey = true;
const keyboardEvent = new KeyboardEvent('keydown', eventInit);
document.activeElement.dispatchEvent(keyboardEvent);
console.log(`尝试通过键盘快捷键提交表单:${keyCombo}`);
// 等待短暂时间,查看是否提交成功
await new Promise(resolve => setTimeout(resolve, 500));
// 检查是否页面已提交(可以通过某种标识来确认)
// 这里假设页面会有某种变化,如URL变化或特定元素出现
// 由于具体实现不同,这里仅提供日志
}
// 2. 尝试点击提交按钮
const submitButton = await waitForSubmitButton(SUBMIT_WAIT_MAX_ATTEMPTS, SUBMIT_WAIT_DELAY);
if (submitButton) {
submitButton.click();
console.log("✅ 自动提交已通过点击提交按钮触发。");
return true;
} else {
console.warn("⚠️ 未找到提交按钮,尝试其他提交方式。");
}
// 3. 尝试调用JavaScript提交函数
// 需要知道具体的提交函数名称,这里假设为 `submitForm`
// 根据实际情况调整函数名称
try {
if (typeof submitForm === 'function') {
submitForm();
console.log("✅ 自动提交已通过调用JavaScript函数触发。");
return true;
} else {
console.warn("⚠️ 未找到名为 'submitForm' 的提交函数。");
}
} catch (error) {
console.error("调用JavaScript提交函数失败:", error);
}
// 4. 确保事件监听器触发
// 重新触发 'submit' 事件
try {
const form = document.querySelector('form');
if (form) {
const submitEvent = new Event('submit', {
bubbles: true,
cancelable: true
});
form.dispatchEvent(submitEvent);
console.log("✅ 自动提交已通过触发 'submit' 事件触发。");
return true;
} else {
console.warn("⚠️ 未找到表单元素,无法触发 'submit' 事件。");
}
} catch (error) {
console.error("触发 'submit' 事件失败:", error);
}
console.warn("⚠️ 所有自动提交方式均未成功。");
return false;
} finally {
isSubmitting = false;
}
};
const formatButtonDisplayLabel = (label) => {
if (typeof label !== 'string') {
return '';
}
const firstSpaceIndex = label.indexOf(' ');
if (firstSpaceIndex > 0 && firstSpaceIndex < label.length - 1) {
const leadingSegment = label.slice(0, firstSpaceIndex);
const remainingText = label.slice(firstSpaceIndex + 1);
// 如果前缀没有字母或数字(通常是emoji/符号),且长度不超过4,则将首个空格替换为不换行空格
const hasAlphaNumeric = /[0-9A-Za-z\u4E00-\u9FFF]/.test(leadingSegment);
if (!hasAlphaNumeric && leadingSegment.length <= 4 && remainingText.trim().length > 0) {
return `${leadingSegment}\u00A0${remainingText}`;
}
}
return label;
};
const extractButtonIconParts = (label) => {
if (typeof label !== 'string') {
return { iconSymbol: '', textLabel: '' };
}
const trimmedStart = label.trimStart();
if (!trimmedStart) {
return { iconSymbol: '', textLabel: '' };
}
const firstSpaceIndex = trimmedStart.indexOf(' ');
if (firstSpaceIndex > 0) {
const leadingSegment = trimmedStart.slice(0, firstSpaceIndex);
const remaining = trimmedStart.slice(firstSpaceIndex + 1).trimStart();
const hasAlphaNumeric = /[0-9A-Za-z\u4E00-\u9FFF]/.test(leadingSegment);
if (!hasAlphaNumeric) {
return {
iconSymbol: leadingSegment,
textLabel: remaining || trimmedStart
};
}
}
const charUnits = Array.from(trimmedStart);
const firstChar = charUnits[0] || '';
if (firstChar && !/[0-9A-Za-z\u4E00-\u9FFF]/.test(firstChar)) {
const remaining = trimmedStart.slice(firstChar.length).trimStart();
return {
iconSymbol: firstChar,
textLabel: remaining || trimmedStart
};
}
return {
iconSymbol: '',
textLabel: trimmedStart
};
};
const createCustomButtonElement = (name, config) => {
const button = document.createElement('button');
const { iconSymbol, textLabel } = extractButtonIconParts(name);
const labelForDisplay = textLabel || name || '';
const displayLabel = formatButtonDisplayLabel(labelForDisplay);
let fallbackSymbolSource = iconSymbol || (Array.from(labelForDisplay.trim())[0] || '🔖');
if (config.type === 'tool' && TOOL_DEFAULT_ICONS[config.action]) {
fallbackSymbolSource = TOOL_DEFAULT_ICONS[config.action];
}
button.textContent = '';
button.setAttribute('data-original-label', name);
button.type = 'button';
button.style.backgroundColor = config.color;
button.style.color = config.textColor || '#333333';
button.style.border = '1px solid rgba(0,0,0,0.1)';
button.style.borderRadius = '4px';
button.style.padding = '6px 12px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.transition = 'all 0.2s ease';
button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.05)';
button.style.marginBottom = '6px';
button.style.width = 'fit-content';
button.style.textAlign = 'left';
button.style.display = 'block';
const contentWrapper = document.createElement('span');
contentWrapper.style.display = 'inline-flex';
contentWrapper.style.alignItems = 'center';
contentWrapper.style.gap = '8px';
const iconWrapper = document.createElement('span');
iconWrapper.style.display = 'inline-flex';
iconWrapper.style.alignItems = 'center';
iconWrapper.style.justifyContent = 'center';
iconWrapper.style.width = '18px';
iconWrapper.style.height = '18px';
iconWrapper.style.flexShrink = '0';
iconWrapper.style.borderRadius = '4px';
iconWrapper.style.overflow = 'hidden';
const createFallbackIcon = (symbol) => {
const fallbackSpan = document.createElement('span');
fallbackSpan.textContent = symbol;
fallbackSpan.style.fontSize = '14px';
fallbackSpan.style.lineHeight = '1';
fallbackSpan.style.display = 'inline-flex';
fallbackSpan.style.alignItems = 'center';
fallbackSpan.style.justifyContent = 'center';
return fallbackSpan;
};
const faviconUrl = (config && typeof config.favicon === 'string') ? config.favicon.trim() : '';
if (faviconUrl) {
const img = document.createElement('img');
img.src = faviconUrl;
img.alt = (labelForDisplay || name || '').trim() || 'icon';
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain';
img.loading = 'lazy';
img.referrerPolicy = 'no-referrer';
img.decoding = 'async';
img.onerror = () => {
iconWrapper.textContent = '';
iconWrapper.appendChild(createFallbackIcon(fallbackSymbolSource));
};
iconWrapper.appendChild(img);
} else {
iconWrapper.appendChild(createFallbackIcon(fallbackSymbolSource));
}
const textSpan = document.createElement('span');
textSpan.textContent = displayLabel;
textSpan.style.display = 'inline-flex';
textSpan.style.alignItems = 'center';
contentWrapper.appendChild(iconWrapper);
contentWrapper.appendChild(textSpan);
button.appendChild(contentWrapper);
// 鼠标悬停显示按钮模板文本
button.title = config.text || '';
// 确保嵌套元素不会拦截点击或拖拽事件
contentWrapper.style.pointerEvents = 'none';
textSpan.style.pointerEvents = 'none';
iconWrapper.style.pointerEvents = 'none';
return button;
};
const extractTemplateVariables = (text = '') => {
if (typeof text !== 'string' || !text.includes('{')) {
return [];
}
const matches = new Set();
const fallbackMatches = text.match(/\{\{[A-Za-z0-9_-]+\}\|\{[A-Za-z0-9_-]+\}\}/g) || [];
fallbackMatches.forEach(match => matches.add(match));
let sanitized = text;
fallbackMatches.forEach(match => {
sanitized = sanitized.split(match).join(' ');
});
const singleMatches = sanitized.match(/\{[A-Za-z0-9_-]+\}/g) || [];
singleMatches.forEach(match => matches.add(match));
return Array.from(matches);
};
// 引入全局变量来跟踪当前打开的文件夹
const currentlyOpenFolder = {
name: null,
element: null
};
const createCustomButton = (name, config, folderName) => {
const button = createCustomButtonElement(name, config, folderName);
button.setAttribute('draggable', 'true');
button.setAttribute('data-button-name', name);
button.setAttribute('data-folder-name', folderName);
button.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('application/json', JSON.stringify({
buttonName: name,
sourceFolder: folderName,
config: config
}));
e.currentTarget.style.opacity = '0.5';
});
button.addEventListener('dragend', (e) => {
e.currentTarget.style.opacity = '1';
});
button.addEventListener('mousedown', (e) => {
e.preventDefault();
const focusedElement = document.activeElement;
if (focusedElement && (focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
setTimeout(() => focusedElement.focus(), 0);
}
});
button.addEventListener('mouseenter', () => {
button.style.transform = 'scale(1.05)';
button.style.boxShadow = '0 3px 6px rgba(0,0,0,0.1)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)';
button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.05)';
});
// 处理按钮点击事件
button.addEventListener('click', async (e) => {
e.preventDefault();
if (config.type === "template") {
const focusedElement = document.activeElement;
if (!focusedElement || !(focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
console.warn("当前未聚焦到有效的 textarea 或 contenteditable 元素。");
return;
}
// 检查模板是否需要剪切板内容
const needsClipboard = config.text.includes('{clipboard}') || config.text.includes('{{inputboard}|{clipboard}}');
let clipboardText = '';
if (needsClipboard) {
try {
clipboardText = await navigator.clipboard.readText();
} catch (err) {
console.error("无法访问剪贴板内容:", err);
alert("无法访问剪贴板内容。请检查浏览器权限。");
return;
}
}
// 改进的内容获取方式
let inputBoxText = '';
if (focusedElement.tagName.toLowerCase() === 'textarea') {
inputBoxText = focusedElement.value;
} else {
// 遍历 contenteditable 元素的子节点,正确处理换行
const childNodes = Array.from(focusedElement.childNodes);
const textParts = [];
let lastWasBr = false;
childNodes.forEach((node, index) => {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.trim() === '') {
if (!lastWasBr && index > 0) {
textParts.push('\n');
}
} else {
textParts.push(node.textContent);
lastWasBr = false;
}
} else if (node.nodeName === 'BR') {
textParts.push('\n');
lastWasBr = true;
} else if (node.nodeName === 'P' || node.nodeName === 'DIV') {
if (node.textContent.trim() === '') {
textParts.push('\n');
} else {
if (textParts.length > 0) {
textParts.push('\n');
}
textParts.push(node.textContent);
}
lastWasBr = false;
}
});
inputBoxText = textParts.join('');
}
const selectionText = window.getSelection().toString();
// 全新的安全变量替换逻辑 - 使用占位符防止嵌套替换
let finalText = config.text;
// 创建变量映射表 - 只有在需要时才包含剪切板内容
const variableMap = {
'{{inputboard}|{clipboard}}': inputBoxText.trim() || clipboardText,
'{clipboard}': clipboardText,
'{inputboard}': inputBoxText,
'{selection}': selectionText
};
// 按照优先级顺序进行替换(复合变量优先)
const replacementOrder = [
'{{inputboard}|{clipboard}}',
'{clipboard}',
'{inputboard}',
'{selection}'
];
// 使用安全的占位符机制,确保一次性替换,避免嵌套
const placeholderMap = new Map();
let placeholderCounter = 0;
// 第一阶段:将模板中的变量替换为唯一占位符
replacementOrder.forEach(variable => {
if (finalText.includes(variable)) {
const placeholder = `__SAFE_PLACEHOLDER_${placeholderCounter++}__`;
placeholderMap.set(placeholder, variableMap[variable]);
// 使用 split().join() 确保精确匹配,避免正则表达式问题
finalText = finalText.split(variable).join(placeholder);
}
});
// 第二阶段:将占位符替换为实际值(此时不会再有嵌套问题)
placeholderMap.forEach((value, placeholder) => {
finalText = finalText.split(placeholder).join(value);
});
// 统一换行符
finalText = finalText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const containsInputboard = config.text.includes("{inputboard}") ||
config.text.includes("{{inputboard}|{clipboard}}");
if (containsInputboard) {
insertTextSmart(focusedElement, finalText, true);
console.log(`✅ 使用 {inputboard} 变量,输入框内容已被替换。`);
} else {
insertTextSmart(focusedElement, finalText, false);
console.log(`✅ 插入了预设文本。`);
}
// 若开启autoSubmit,则先检测是否完成替换,再延时后提交
if (config.autoSubmit) {
try {
// 1. 等待输入框内容与 finalText 匹配,最多等待3秒
await waitForContentMatch(focusedElement, finalText, 100, 3000);
// 2. 再额外等待500ms,确保渲染/加载稳定
await new Promise(resolve => setTimeout(resolve, 500));
// 3. 调用自动提交
const success = await submitForm();
if (success) {
console.log("✅ 自动提交成功(已确认内容替换完成)。");
} else {
console.warn("⚠️ 自动提交失败。");
}
} catch (error) {
console.error("自动提交前检测文本匹配超时或错误:", error);
}
}
} else if (config.type === "tool") {
const focusedElement = document.activeElement;
if (!focusedElement || !(focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
console.warn("当前未聚焦到有效的 textarea 或 contenteditable 元素。");
return;
}
switch (config.action) {
case "cut":
handleCut(focusedElement);
break;
case "copy":
handleCopy(focusedElement);
break;
case "paste":
handlePaste(focusedElement);
break;
case "clear":
handleClear(focusedElement);
break;
default:
console.warn(`未知的工具按钮动作: ${config.action}`);
}
}
// 立即关闭弹窗
if (currentlyOpenFolder.name === folderName && currentlyOpenFolder.element) {
currentlyOpenFolder.element.style.display = 'none';
currentlyOpenFolder.name = null;
currentlyOpenFolder.element = null;
console.log(`✅ 弹窗 "${folderName}" 已立即关闭。`);
} else {
console.warn(`⚠️ 弹窗 "${folderName}" 未被识别为当前打开的弹窗。`);
}
});
return button;
};
// 工具按钮动作处理
const handleCut = (element) => {
let text = '';
if (element.tagName.toLowerCase() === 'textarea') {
text = element.value;
insertTextSmart(element, '', true);
} else {
const textContent = [];
const childNodes = Array.from(element.childNodes);
childNodes.forEach((node, index) => {
if (node.nodeType === Node.TEXT_NODE) {
textContent.push(node.textContent);
} else if (node.nodeName === 'BR') {
textContent.push('\n');
} else if (node.nodeName === 'P' || node.nodeName === 'DIV') {
if (index > -1) textContent.push('\n');
textContent.push(node.textContent);
}
});
text = textContent.join('');
insertTextSmart(element, '', true);
}
if (text) {
navigator.clipboard.writeText(text).then(() => {
console.log("✅ 已剪切输入框内容到剪贴板。");
showTemporaryFeedback(element, '剪切成功');
}).catch(err => {
console.error("剪切失败:", err);
alert("剪切失败,请检查浏览器权限。");
});
}
};
const handleCopy = (element) => {
let text = '';
if (element.tagName.toLowerCase() === 'textarea') {
text = element.value;
} else {
const textContent = [];
const childNodes = Array.from(element.childNodes);
childNodes.forEach((node, index) => {
if (node.nodeType === Node.TEXT_NODE) {
textContent.push(node.textContent);
} else if (node.nodeName === 'BR') {
textContent.push('\n');
} else if (node.nodeName === 'P' || node.nodeName === 'DIV') {
if (index > -1) textContent.push('\n');
textContent.push(node.textContent);
}
});
text = textContent.join('');
}
if (text) {
navigator.clipboard.writeText(text).then(() => {
console.log("✅ 已复制输入框内容到剪贴板。");
showTemporaryFeedback(element, '复制成功');
}).catch(err => {
console.error("复制失败:", err);
alert("复制失败,请检查浏览器权限。");
});
}
};
const handlePaste = async (element) => {
try {
const clipboardText = await navigator.clipboard.readText();
insertTextSmart(element, clipboardText);
console.log("✅ 已粘贴剪贴板内容到输入框。");
showTemporaryFeedback(element, '粘贴成功');
} catch (err) {
console.error("粘贴失败:", err);
alert("粘贴失败,请检查浏览器权限。");
}
};
const handleClear = (element) => {
insertTextSmart(element, '', true);
console.log("✅ 输入框内容已清空。");
showTemporaryFeedback(element, '清空成功');
};
const showTemporaryFeedback = (element, message) => {
const feedback = document.createElement('span');
feedback.textContent = message;
feedback.style.position = 'absolute';
feedback.style.bottom = '10px';
feedback.style.right = '10px';
feedback.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
feedback.style.color = '#fff';
feedback.style.padding = '4px 8px';
feedback.style.borderRadius = '4px';
feedback.style.zIndex = '10001';
element.parentElement.appendChild(feedback);
setTimeout(() => {
feedback.remove();
}, 1500);
};
const createFolderButton = (folderName, folderConfig) => {
const folderButton = document.createElement('button');
folderButton.innerText = folderName;
folderButton.type = 'button';
folderButton.style.backgroundColor = folderConfig.color;
folderButton.style.color = folderConfig.textColor || '#ffffff';
folderButton.style.border = 'none';
folderButton.style.borderRadius = '4px';
folderButton.style.padding = '6px 12px';
folderButton.style.cursor = 'pointer';
folderButton.style.fontSize = '14px';
folderButton.style.fontWeight = '500';
folderButton.style.transition = 'all 0.2s ease';
folderButton.style.position = 'relative';
folderButton.style.display = 'inline-flex';
folderButton.style.alignItems = 'center';
folderButton.style.whiteSpace = 'nowrap';
folderButton.style.zIndex = '99';
folderButton.classList.add('folder-button');
folderButton.setAttribute('data-folder', folderName);
folderButton.addEventListener('mousedown', (e) => {
e.preventDefault();
});
folderButton.addEventListener('mouseleave', () => {
folderButton.style.transform = 'scale(1)';
folderButton.style.boxShadow = 'none';
});
const buttonListContainer = document.createElement('div');
buttonListContainer.style.position = 'fixed';
buttonListContainer.style.display = 'none';
buttonListContainer.style.flexDirection = 'column';
buttonListContainer.style.backgroundColor = 'var(--folder-bg, rgba(255, 255, 255, 0.8))';
buttonListContainer.style.backdropFilter = 'blur(5px)';
buttonListContainer.style.border = `1px solid var(--border-color, #e5e7eb)`;
buttonListContainer.style.borderRadius = '8px';
buttonListContainer.style.padding = '10px';
buttonListContainer.style.paddingBottom = '2.5px';
buttonListContainer.style.boxShadow = `0 4px 12px var(--shadow-color, rgba(0,0,0,0.1))`;
buttonListContainer.style.zIndex = '100';
buttonListContainer.style.maxHeight = '800px';
buttonListContainer.style.overflowY = 'auto';
buttonListContainer.style.transition = 'all 0.3s ease';
buttonListContainer.classList.add('button-list');
buttonListContainer.setAttribute('data-folder-list', folderName);
buttonListContainer.style.pointerEvents = 'auto';
Object.entries(folderConfig.buttons).forEach(([name, config]) => {
const customButton = createCustomButton(name, config, folderName);
buttonListContainer.appendChild(customButton);
});
folderButton.addEventListener('click', (e) => {
e.preventDefault();
// Toggle popup visibility
if (currentlyOpenFolder.name === folderName) {
// 如果当前文件夹已经打开,则关闭它
buttonListContainer.style.display = 'none';
currentlyOpenFolder.name = null;
currentlyOpenFolder.element = null;
console.log(`🔒 弹窗 "${folderName}" 已关闭。`);
} else {
// 关闭其他文件夹的弹窗
if (currentlyOpenFolder.element) {
currentlyOpenFolder.element.style.display = 'none';
console.log(`🔒 弹窗 "${currentlyOpenFolder.name}" 已关闭。`);
}
// 打开当前文件夹的弹窗
buttonListContainer.style.display = 'flex';
currentlyOpenFolder.name = folderName;
currentlyOpenFolder.element = buttonListContainer;
console.log(`🔓 弹窗 "${folderName}" 已打开。`);
// 动态定位弹窗位置
const rect = folderButton.getBoundingClientRect();
buttonListContainer.style.bottom = `40px`;
buttonListContainer.style.left = `${rect.left + window.scrollX - 20}px`;
console.log(`📍 弹窗位置设置为 Bottom: 40px, Left: ${rect.left + window.scrollX - 20}px`);
}
});
document.addEventListener('click', (e) => {
const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
const clickedInsideButton = path.includes(folderButton);
const clickedInsideList = path.includes(buttonListContainer);
if (!clickedInsideButton && !clickedInsideList) {
// 点击了其他地方,关闭弹窗
if (buttonListContainer.style.display !== 'none') {
buttonListContainer.style.display = 'none';
if (currentlyOpenFolder.name === folderName) {
currentlyOpenFolder.name = null;
currentlyOpenFolder.element = null;
console.log(`🔒 弹窗 "${folderName}" 已关闭(点击外部区域)。`);
}
}
}
});
appendToMainLayer(buttonListContainer);
return folderButton;
};
const toggleFolder = (folderName, state) => {
const buttonList = queryUI(`.button-list[data-folder-list="${folderName}"]`);
if (!buttonList) {
console.warn(`⚠️ 未找到与文件夹 "${folderName}" 关联的弹窗。`);
return;
}
if (state) {
// 打开当前文件夹的弹窗
buttonList.style.display = 'flex';
currentlyOpenFolder.name = folderName;
currentlyOpenFolder.element = buttonList;
console.log(`🔓 弹窗 "${folderName}" 已打开(toggleFolder)。`);
} else {
// 关闭当前文件夹的弹窗
buttonList.style.display = 'none';
if (currentlyOpenFolder.name === folderName) {
currentlyOpenFolder.name = null;
currentlyOpenFolder.element = null;
console.log(`🔒 弹窗 "${folderName}" 已关闭(toggleFolder)。`);
}
}
// 关闭其他文件夹的弹窗
const root = getShadowRoot();
const allButtonLists = root ? Array.from(root.querySelectorAll('.button-list')) : [];
allButtonLists.forEach(bl => {
if (bl.getAttribute('data-folder-list') !== folderName) {
bl.style.display = 'none';
const fname = bl.getAttribute('data-folder-list');
if (currentlyOpenFolder.name === fname) {
currentlyOpenFolder.name = null;
currentlyOpenFolder.element = null;
console.log(`🔒 弹窗 "${fname}" 已关闭(toggleFolder 关闭其他弹窗)。`);
}
}
});
};
const closeExistingOverlay = (overlay) => {
if (overlay && overlay.parentElement) {
// 添加关闭动画
overlay.style.opacity = '0';
// 立即标记为已关闭,避免重复操作
overlay.setAttribute('data-closing', 'true');
// 延时移除DOM元素,确保动画完成
setTimeout(() => {
if (overlay.parentElement && overlay.getAttribute('data-closing') === 'true') {
overlay.parentElement.removeChild(overlay);
console.log("🔒 弹窗已关闭并从DOM中移除");
}
}, 300);
} else {
console.warn("⚠️ 尝试关闭不存在的弹窗");
}
};
let currentConfirmOverlay = null;
let currentSettingsOverlay = null;
let isSettingsFolderPanelCollapsed = false;
let settingsDialogMainContainer = null;
let currentConfigOverlay = null; // 新增的独立配置设置弹窗
let currentStyleOverlay = null;
const showDeleteFolderConfirmDialog = (folderName, rerenderFn) => {
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
const folderConfig = buttonConfig.folders[folderName];
if (!folderConfig) {
alert(`文件夹 "${folderName}" 不存在。`);
return;
}
// 构建文件夹内自定义按钮的垂直预览列表
let buttonsPreviewHTML = '';
Object.entries(folderConfig.buttons).forEach(([btnName, btnCfg]) => {
buttonsPreviewHTML += `
${btnName}
`;
});
const overlay = document.createElement('div');
overlay.classList.add('confirm-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 11000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('confirm-dialog', 'cttf-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 20px 24px 16px 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 400px;
max-width: 90vw;
`;
setTrustedHTML(dialog, `
🗑️ 确认删除文件夹 "${folderName}"?
❗️ 注意:此操作无法撤销!
(删除文件夹将同时删除其中的所有自定义按钮!)
1️⃣ 文件夹按钮外观:
按钮名称: ${folderName}
背景颜色: ${folderConfig.color}
文字颜色: ${folderConfig.textColor}
2️⃣ 文件夹内,全部自定义按钮:
${buttonsPreviewHTML}
`);
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
// 动画效果
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
dialog.querySelector('#cancelDeleteFolder').addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
dialog.querySelector('#confirmDeleteFolder').addEventListener('click', () => {
delete buttonConfig.folders[folderName];
const idx = buttonConfig.folderOrder.indexOf(folderName);
if (idx > -1) buttonConfig.folderOrder.splice(idx, 1);
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
if (rerenderFn) rerenderFn();
console.log(`🗑️ 文件夹 "${folderName}" 已删除。`);
// 更新按钮栏
updateButtonContainer();
});
};
// 修改 删除按钮确认对话框,增加显示按钮名称、颜色信息及样式预览
const showDeleteButtonConfirmDialog = (folderName, btnName, rerenderFn) => {
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
const btnCfg = buttonConfig.folders[folderName].buttons[btnName];
if (!btnCfg) {
alert(`按钮 "${btnName}" 不存在于文件夹 "${folderName}" 中。`);
return;
}
const overlay = document.createElement('div');
overlay.classList.add('confirm-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 11000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('confirm-dialog', 'cttf-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 20px 24px 16px 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 400px;
max-width: 90vw;
`;
setTrustedHTML(dialog, `
🗑️ 确认删除按钮 "${btnName}"?
❗️ 注意:此操作无法撤销!
1️⃣ 自定义按钮外观:
按钮名称: ${btnName}
按钮背景颜色: ${btnCfg.color}
按钮文字颜色: ${btnCfg.textColor}
2️⃣ 按钮对应的文本模板:
`);
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
// 动画效果
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
dialog.querySelector('#cancelDeleteButton').addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
dialog.querySelector('#confirmDeleteButton').addEventListener('click', () => {
delete buttonConfig.folders[folderName].buttons[btnName];
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
if (rerenderFn) rerenderFn();
console.log(`🗑️ 按钮 "${btnName}" 已删除。`);
// 更新按钮栏
updateButtonContainer();
updateCounters(); // 更新所有计数器
});
};
const showButtonEditDialog = (folderName, btnName = '', btnConfig = {}, rerenderFn) => {
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
// 禁止编辑/删除工具文件夹中的工具按钮
if (folderName === "🖱️" && btnConfig.type === "tool") {
alert('工具文件夹中的工具按钮无法编辑或删除。');
return;
}
const isEdit = btnName !== '';
// Create overlay and dialog containers
const overlay = document.createElement('div');
overlay.classList.add('edit-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 11000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('edit-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 500px;
max-width: 90vw;
`;
const initialName = btnName || '';
const initialColor = btnConfig.color || '#FFC1CC';
const initialTextColor = btnConfig.textColor || '#333333';
const initialAutoSubmit = btnConfig.autoSubmit || false; // 新增字段
const initialFavicon = typeof btnConfig.favicon === 'string' ? btnConfig.favicon : '';
// 预览部分
const previewSection = `
${isEdit ? '✏️ 编辑按钮:' : '🆕 新建按钮:'}
`;
// Tab content for text template
const textTemplateTab = `
`;
// Tab content for style settings
const styleSettingsTab = `
`;
// 新增的提交设置子标签页
const submitSettingsTab = `
`;
// Tab navigation
const tabNavigation = `
`;
// Footer buttons
const footerButtons = `
`;
// Combine all sections
setTrustedHTML(dialog, `
${previewSection}
${tabNavigation}
${textTemplateTab}
${styleSettingsTab}
${submitSettingsTab}
${footerButtons}
`);
// Add tab switching functionality
const setupTabs = () => {
const tabButtons = dialog.querySelectorAll('.tab-button');
const tabContents = dialog.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabId = button.dataset.tab;
// Update button styles
tabButtons.forEach(btn => {
if (btn === button) {
btn.style.backgroundColor = 'var(--primary-color, #3B82F6)';
btn.style.color = 'white';
btn.style.borderBottom = '2px solid var(--primary-color, #3B82F6)';
} else {
btn.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
btn.style.color = 'var(--text-color, #333333)';
btn.style.borderBottom = '2px solid transparent';
}
});
// Show/hide content
tabContents.forEach(content => {
content.style.display = content.id === tabId ? 'block' : 'none';
});
});
});
};
// Rest of the existing dialog setup code...
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
// Setup tabs
setupTabs();
// Setup preview updates
const setupPreviewUpdates = () => {
const previewButton = dialog.querySelector('#previewButton');
const buttonNameInput = dialog.querySelector('#buttonName');
const buttonColorInput = dialog.querySelector('#buttonColor');
const buttonTextColorInput = dialog.querySelector('#buttonTextColor');
const autoSubmitCheckbox = dialog.querySelector('#autoSubmitCheckbox'); // 新增引用
const buttonFaviconInput = dialog.querySelector('#buttonFaviconInput');
const buttonFaviconPreview = dialog.querySelector('#buttonFaviconPreview');
const updateFaviconPreview = () => {
if (!buttonFaviconPreview) return;
const currentName = buttonNameInput?.value.trim() || initialName || '';
const faviconValue = buttonFaviconInput?.value.trim() || '';
const { iconSymbol } = extractButtonIconParts(currentName);
const fallbackSymbol = iconSymbol || (Array.from(currentName.trim())[0] || '🔖');
const previewElement = createFaviconElement(
faviconValue,
currentName,
fallbackSymbol,
{ withBackground: false }
);
setTrustedHTML(buttonFaviconPreview, '');
buttonFaviconPreview.appendChild(previewElement);
};
buttonNameInput?.addEventListener('input', (e) => {
previewButton.textContent = e.target.value || '预览按钮';
updateFaviconPreview();
});
buttonColorInput?.addEventListener('input', (e) => {
previewButton.style.backgroundColor = e.target.value;
});
buttonTextColorInput?.addEventListener('input', (e) => {
previewButton.style.color = e.target.value;
});
// 监听“自动提交”开关变化
autoSubmitCheckbox?.addEventListener('change', (e) => {
console.log(`✅ 自动提交开关已设置为 ${e.target.checked}`);
});
if (buttonFaviconInput) {
autoResizeTextarea(buttonFaviconInput, { minRows: 1, maxRows: 4 });
buttonFaviconInput.addEventListener('input', () => {
autoResizeTextarea(buttonFaviconInput, { minRows: 1, maxRows: 4 });
updateFaviconPreview();
});
}
updateFaviconPreview();
};
setupPreviewUpdates();
// Setup quick insert buttons
const setupQuickInsert = () => {
const buttonText = dialog.querySelector('#buttonText');
const quickInsertButtons = dialog.querySelector('#quickInsertButtons');
quickInsertButtons?.addEventListener('click', (e) => {
const button = e.target.closest('button[data-insert]');
if (!button) return;
e.preventDefault();
const insertText = button.dataset.insert;
const start = buttonText.selectionStart;
const end = buttonText.selectionEnd;
buttonText.value = buttonText.value.substring(0, start) +
insertText +
buttonText.value.substring(end);
buttonText.selectionStart = buttonText.selectionEnd = start + insertText.length;
buttonText.focus();
});
quickInsertButtons?.addEventListener('mousedown', (e) => {
if (e.target.closest('button[data-insert]')) {
e.preventDefault();
}
});
};
setupQuickInsert();
// Animation effect
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Setup buttons
dialog.querySelector('#cancelButtonEdit')?.addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
dialog.querySelector('#saveButtonEdit')?.addEventListener('click', () => {
const newBtnName = dialog.querySelector('#buttonName').value.trim();
const newBtnColor = dialog.querySelector('#buttonColor').value;
const newBtnTextColor = dialog.querySelector('#buttonTextColor').value;
const newBtnText = dialog.querySelector('#buttonText').value.trim();
const autoSubmit = dialog.querySelector('#autoSubmitCheckbox')?.checked || false; // 获取自动提交状态
const newBtnFavicon = (dialog.querySelector('#buttonFaviconInput')?.value || '').trim();
if (!newBtnName) {
alert('请输入按钮名称!');
return;
}
if (!isValidColor(newBtnColor) || !isValidColor(newBtnTextColor)) {
alert('请选择有效的颜色!');
return;
}
if (newBtnName !== btnName && buttonConfig.folders[folderName].buttons[newBtnName]) {
alert('按钮名称已存在!');
return;
}
// Get all buttons order
const currentButtons = { ...buttonConfig.folders[folderName].buttons };
if (btnConfig.type === "tool") {
// 工具按钮不允许更改类型和动作
buttonConfig.folders[folderName].buttons[newBtnName] = {
type: "tool",
action: btnConfig.action,
color: newBtnColor,
textColor: newBtnTextColor
};
} else {
// 处理模板按钮
// Handle button rename
if (btnName && newBtnName !== btnName) {
const newButtons = {};
Object.keys(currentButtons).forEach(key => {
if (key === btnName) {
newButtons[newBtnName] = {
text: newBtnText,
color: newBtnColor,
textColor: newBtnTextColor,
type: "template",
autoSubmit: autoSubmit,
favicon: newBtnFavicon
};
} else {
newButtons[key] = currentButtons[key];
}
});
buttonConfig.folders[folderName].buttons = newButtons;
} else {
// Update existing button
if (btnName) {
buttonConfig.folders[folderName].buttons[btnName] = {
text: newBtnText,
color: newBtnColor,
textColor: newBtnTextColor,
type: "template",
autoSubmit: autoSubmit,
favicon: newBtnFavicon
};
} else {
// Create new button
buttonConfig.folders[folderName].buttons[newBtnName] = {
text: newBtnText,
color: newBtnColor,
textColor: newBtnTextColor,
type: "template",
autoSubmit: autoSubmit,
favicon: newBtnFavicon
};
}
}
}
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
if (rerenderFn) rerenderFn();
console.log(`✅ 按钮 "${newBtnName}" 已保存。`);
updateButtonContainer();
updateCounters(); // 更新所有计数器
});
};
function isValidColor(color) {
const s = new Option().style;
s.color = color;
return s.color !== '';
}
const showFolderEditDialog = (folderName = '', folderConfig = {}, rerenderFn) => {
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
const isNew = !folderName;
const overlay = document.createElement('div');
overlay.classList.add('folder-edit-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 11000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('folder-edit-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 500px;
max-width: 90vw;
`;
const initialName = folderName || '';
const initialColor = folderConfig.color || '#3B82F6';
const initialTextColor = folderConfig.textColor || '#ffffff';
// 预览部分
const previewSection = `
${isNew ? '🆕 新建文件夹:' : '✏️ 编辑文件夹:'}
`;
// 设置部分
const settingsSection = `
`;
// 底部按钮
const footerButtons = `
`;
// Combine all sections
setTrustedHTML(dialog, `
${previewSection}
${settingsSection}
${footerButtons}
`);
// 添加事件监听器
const setupPreviewUpdates = () => {
const previewButton = dialog.querySelector('#previewButton');
const folderNameInput = dialog.querySelector('#folderNameInput');
const folderColorInput = dialog.querySelector('#folderColorInput');
const folderTextColorInput = dialog.querySelector('#folderTextColorInput');
folderNameInput?.addEventListener('input', (e) => {
previewButton.textContent = e.target.value || '预览文件夹';
});
folderColorInput?.addEventListener('input', (e) => {
previewButton.style.backgroundColor = e.target.value;
});
folderTextColorInput?.addEventListener('input', (e) => {
previewButton.style.color = e.target.value;
});
};
setupPreviewUpdates();
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
// Animation effect
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Setup buttons
dialog.querySelector('#cancelFolderEdit').addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
// 在showFolderEditDialog函数的保存按钮点击事件中
dialog.querySelector('#saveFolderEdit').addEventListener('click', () => {
const newFolderName = dialog.querySelector('#folderNameInput').value.trim();
const newColor = dialog.querySelector('#folderColorInput').value;
const newTextColor = dialog.querySelector('#folderTextColorInput').value;
if (!newFolderName) {
alert('请输入文件夹名称');
return;
}
if (isNew && buttonConfig.folders[newFolderName]) {
alert("该文件夹已存在!");
return;
}
if (!isNew && newFolderName !== folderName && buttonConfig.folders[newFolderName]) {
alert("该文件夹已存在!");
return;
}
if (!isNew && newFolderName !== folderName) {
const oldButtons = buttonConfig.folders[folderName].buttons;
buttonConfig.folders[newFolderName] = {
...buttonConfig.folders[folderName],
color: newColor,
textColor: newTextColor,
buttons: { ...oldButtons }
};
delete buttonConfig.folders[folderName];
const idx = buttonConfig.folderOrder.indexOf(folderName);
if (idx > -1) {
buttonConfig.folderOrder[idx] = newFolderName;
}
} else {
buttonConfig.folders[newFolderName] = buttonConfig.folders[newFolderName] || { buttons: {} };
buttonConfig.folders[newFolderName].color = newColor;
buttonConfig.folders[newFolderName].textColor = newTextColor;
// 确保新建文件夹有hidden字段且默认为false
if (typeof buttonConfig.folders[newFolderName].hidden !== 'boolean') {
buttonConfig.folders[newFolderName].hidden = false;
}
// 在isNew分支中把新建的文件夹名加入folderOrder
if (isNew) {
buttonConfig.folderOrder.push(newFolderName);
}
}
// 确保所有按钮都有'type'字段和'autoSubmit'字段
Object.entries(buttonConfig.folders).forEach(([folderName, folderCfg]) => {
Object.entries(folderCfg.buttons).forEach(([btnName, btnCfg]) => {
if (!btnCfg.type) {
if (folderName === "🖱️") {
btnCfg.type = "tool";
} else {
btnCfg.type = "template";
}
}
if (btnCfg.type === "template" && typeof btnCfg.autoSubmit !== 'boolean') {
btnCfg.autoSubmit = false;
}
});
});
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
if (rerenderFn) rerenderFn(newFolderName);
console.log(`✅ 文件夹 "${newFolderName}" 已保存。`);
updateButtonContainer();
updateCounters(); // 更新所有计数器
});
};
const createSettingsButton = () => {
const button = document.createElement('button');
button.innerText = '⚙️';
button.type = 'button';
button.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
button.style.color = 'var(--text-color, #333333)';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.marginLeft = '10px';
button.addEventListener('click', showUnifiedSettingsDialog);
return button;
};
const createCutButton = () => {
const button = document.createElement('button');
button.innerText = '✂️';
button.type = 'button';
button.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
button.style.color = 'var(--text-color, #333333)';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.marginLeft = '10px';
button.title = '剪切输入框内容';
// 阻止mousedown默认行为以维持输入框焦点
button.addEventListener('mousedown', (e) => {
e.preventDefault();
});
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const focusedElement = document.activeElement;
if (!focusedElement || !(focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
console.warn("当前未聚焦到有效的 textarea 或 contenteditable 元素。");
return;
}
let text = '';
if (focusedElement.tagName.toLowerCase() === 'textarea') {
text = focusedElement.value;
// 清空textarea内容
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
nativeSetter.call(focusedElement, '');
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'deleteContent'
});
focusedElement.dispatchEvent(inputEvent);
} else {
// 处理contenteditable元素
const childNodes = Array.from(focusedElement.childNodes);
const textParts = [];
childNodes.forEach((node, index) => {
if (node.nodeType === Node.TEXT_NODE) {
textParts.push(node.textContent);
} else if (node.nodeName === 'BR') {
textParts.push('\n');
} else if (node.nodeName === 'P' || node.nodeName === 'DIV') {
if (index > -1) textParts.push('\n');
textParts.push(node.textContent);
}
});
text = textParts.join('');
// 清空contenteditable内容
setTrustedHTML(focusedElement, '');
}
if (text) {
navigator.clipboard.writeText(text).then(() => {
console.log("✅ 已剪切输入框内容到剪贴板。");
showTemporaryFeedback(focusedElement, '剪切成功');
}).catch(err => {
console.error("剪切失败:", err);
alert("剪切失败,请检查浏览器权限。");
});
}
// 确保输入框保持焦点
focusedElement.focus();
// 如果是textarea,还需要设置光标位置到开始处
if (focusedElement.tagName.toLowerCase() === 'textarea') {
focusedElement.selectionStart = focusedElement.selectionEnd = 0;
}
console.log("✅ 输入框内容已清空。");
showTemporaryFeedback(focusedElement, '清空成功');
});
return button;
};
const createCopyButton = () => {
const button = document.createElement('button');
button.innerText = '🅲';
button.type = 'button';
button.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
button.style.color = 'var(--text-color, #333333)';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.marginLeft = '10px';
button.title = '复制输入框内容';
// 阻止mousedown默认行为以维持输入框焦点
button.addEventListener('mousedown', (e) => {
e.preventDefault();
});
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const focusedElement = document.activeElement;
if (!focusedElement || !(focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
console.warn("当前未聚焦到有效的 textarea 或 contenteditable 元素。");
return;
}
let text = '';
if (focusedElement.tagName.toLowerCase() === 'textarea') {
text = focusedElement.value;
} else {
const textContent = [];
const childNodes = Array.from(focusedElement.childNodes);
childNodes.forEach((node, index) => {
if (node.nodeType === Node.TEXT_NODE) {
textContent.push(node.textContent);
} else if (node.nodeName === 'BR') {
textContent.push('\n');
} else if (node.nodeName === 'P' || node.nodeName === 'DIV') {
if (index > -1) textContent.push('\n');
textContent.push(node.textContent);
}
});
text = textContent.join('');
}
if (text) {
navigator.clipboard.writeText(text).then(() => {
console.log("✅ 已复制输入框内容到剪贴板。");
showTemporaryFeedback(focusedElement, '复制成功');
}).catch(err => {
console.error("复制失败:", err);
alert("复制失败,请检查浏览器权限。");
});
}
// 确保输入框保持焦点
focusedElement.focus();
});
return button;
};
const createPasteButton = () => {
const button = document.createElement('button');
button.innerText = '🆅';
button.type = 'button';
button.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
button.style.color = 'var(--text-color, #333333)';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.marginLeft = '10px';
button.title = '粘贴剪切板内容';
// 阻止mousedown默认行为以维持输入框焦点
button.addEventListener('mousedown', (e) => {
e.preventDefault();
});
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const focusedElement = document.activeElement;
if (!focusedElement || !(focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
console.warn("当前未聚焦到有效的 textarea 或 contenteditable 元素。");
return;
}
try {
const clipboardText = await navigator.clipboard.readText();
// 使用现有的insertTextSmart函数插入文本
insertTextSmart(focusedElement, clipboardText);
// 添加视觉反馈
const originalText = button.innerText;
button.innerText = '✓';
button.style.backgroundColor = 'var(--success-color, #22c55e)';
button.style.color = 'white';
setTimeout(() => {
button.innerText = originalText;
button.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
button.style.color = 'var(--text-color, #333333)';
}, 1000);
console.log("✅ 已粘贴剪贴板内容到输入框。");
} catch (err) {
console.error("访问剪切板失败:", err);
alert("粘贴失败,请检查浏览器权限。");
}
// 确保输入框保持焦点
focusedElement.focus();
});
return button;
};
const createClearButton = () => {
const button = document.createElement('button');
button.innerText = '✖️';
button.type = 'button';
button.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
button.style.color = 'var(--text-color, #333333)';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.marginLeft = '10px';
button.title = '清空输入框';
// 添加mousedown事件处理器来阻止焦点切换
button.addEventListener('mousedown', (e) => {
e.preventDefault();
});
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // 阻止事件冒泡
const focusedElement = document.activeElement;
if (!focusedElement || !(focusedElement.tagName === 'TEXTAREA' || focusedElement.getAttribute('contenteditable') === 'true')) {
console.warn("当前未聚焦到有效的 textarea 或 contenteditable 元素。");
return;
}
// 使用现有的insertTextSmart函数清空内容
insertTextSmart(focusedElement, '', true);
// 确保立即重新聚焦
focusedElement.focus();
// 如果是textarea,还需要设置光标位置到开始处
if (focusedElement.tagName.toLowerCase() === 'textarea') {
focusedElement.selectionStart = focusedElement.selectionEnd = 0;
}
console.log("✅ 输入框内容已清空。");
showTemporaryFeedback(focusedElement, '清空成功');
});
return button;
};
// 新增的配置设置按钮和弹窗
const createConfigSettingsButton = () => {
const button = document.createElement('button');
button.innerText = '🛠️ 配置管理';
button.type = 'button';
button.style.backgroundColor = 'var(--info-color, #4F46E5)';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.addEventListener('click', showConfigSettingsDialog);
return button;
};
function exportConfig() {
const date = new Date();
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
const hh = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const ss = String(date.getSeconds()).padStart(2, '0');
const fileName = `[Chat] Template Text Folders「${yyyy}-${mm}-${dd}」「${hh}:${minutes}:${ss}」.json`;
const dataStr = JSON.stringify(buttonConfig, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
console.log("📤 配置已导出。");
}
// 新增:显示导入配置预览确认对话框
function showImportConfirmDialog(importedConfig, onConfirm, onCancel) {
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
// 计算导入配置的统计信息
const importFolderCount = Object.keys(importedConfig.folders || {}).length;
const importButtonCount = Object.values(importedConfig.folders || {}).reduce((sum, folder) => {
return sum + Object.keys(folder.buttons || {}).length;
}, 0);
// 计算当前配置的统计信息
const currentFolderCount = Object.keys(buttonConfig.folders).length;
const currentButtonCount = Object.values(buttonConfig.folders).reduce((sum, folder) => {
return sum + Object.keys(folder.buttons).length;
}, 0);
const overlay = document.createElement('div');
overlay.classList.add('import-confirm-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 13000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('import-confirm-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 8px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 480px;
max-width: 90vw;
transform: scale(0.95);
position: relative;
z-index: 13001;
`;
setTrustedHTML(dialog, `
📥 确认导入配置
📊 配置对比
当前配置
${currentFolderCount}
个文件夹
${currentButtonCount}
个按钮
导入配置
${importFolderCount}
个文件夹
${importButtonCount}
个按钮
⚠️
注意:导入配置将完全替换当前配置,此操作无法撤销!
`);
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
// 动画效果
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// 按钮事件
dialog.querySelector('#cancelImport').addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
if (onCancel) onCancel();
});
dialog.querySelector('#confirmImport').addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
// 添加短暂延时,确保弹窗关闭动画完成后再执行导入
setTimeout(() => {
if (onConfirm) {
onConfirm();
}
}, 100);
});
// 点击外部忽略
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
e.stopPropagation();
e.preventDefault();
}
});
}
function importConfig(rerenderFn) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const importedConfig = JSON.parse(evt.target.result);
if (importedConfig && typeof importedConfig === 'object') {
if (!importedConfig.folders || !importedConfig.folderOrder) {
alert('导入的配置文件无效!缺少必要字段。');
return;
}
// 显示预览确认对话框
showImportConfirmDialog(
importedConfig,
() => {
// 用户确认导入
try {
// 替换现有配置
buttonConfig = importedConfig;
// 确保所有按钮都有'type'字段和'autoSubmit'字段
Object.entries(buttonConfig.folders).forEach(([folderName, folderConfig]) => {
// 确保文件夹有hidden字段
if (typeof folderConfig.hidden !== 'boolean') {
folderConfig.hidden = false;
}
Object.entries(folderConfig.buttons).forEach(([btnName, btnCfg]) => {
if (!btnCfg.type) {
if (folderName === "🖱️") {
btnCfg.type = "tool";
} else {
btnCfg.type = "template";
}
}
if (btnCfg.type === "template" && typeof btnCfg.autoSubmit !== 'boolean') {
btnCfg.autoSubmit = false;
}
});
});
// 保存配置
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
// 重新渲染设置面板(如果打开)
if (rerenderFn) {
// 重置选中的文件夹为第一个
selectedFolderName = buttonConfig.folderOrder[0] || null;
rerenderFn();
}
console.log("📥 配置已成功导入。");
// 更新按钮栏
updateButtonContainer();
// 应用新配置下的域名样式
try { applyDomainStyles(); } catch (_) {}
// 立即更新所有计数器
setTimeout(() => {
updateCounters();
console.log("📊 导入后计数器已更新。");
// 延时执行回调函数,确保所有渲染完成
setTimeout(() => {
if (rerenderFn) {
rerenderFn();
}
}, 150);
}, 100);
} catch (error) {
console.error('导入配置时发生错误:', error);
alert('导入配置时发生错误,请检查文件格式。');
}
},
() => {
// 用户取消导入
console.log("❌ 用户取消了配置导入。");
}
);
} else {
alert('导入的配置文件内容无效!');
}
} catch (error) {
console.error('解析配置文件失败:', error);
alert('导入的配置文件解析失败!请确认文件格式正确。');
}
};
reader.readAsText(file);
});
input.click();
}
// 新增的单独配置设置弹窗
const showConfigSettingsDialog = () => {
if (currentConfigOverlay) {
closeExistingOverlay(currentConfigOverlay);
}
const overlay = document.createElement('div');
overlay.classList.add('config-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 12000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('config-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 400px;
max-width: 90vw;
`;
setTrustedHTML(dialog, `
🛠️ 配置管理
`);
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfigOverlay = overlay;
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
closeExistingOverlay(overlay);
if (currentConfigOverlay === overlay) {
currentConfigOverlay = null;
}
console.log("✅ 配置管理弹窗已通过点击外部关闭");
}
});
// 动画效果
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
dialog.querySelector('#importConfigBtn').addEventListener('click', () => {
importConfig(() => {
// 重新渲染主设置面板
if (currentSettingsOverlay) {
selectedFolderName = buttonConfig.folderOrder[0] || null;
renderFolderList();
renderButtonList();
// 确保计数器也被更新
setTimeout(() => {
updateCounters();
}, 50);
}
// 导入成功后关闭配置管理弹窗
if (currentConfigOverlay) {
closeExistingOverlay(currentConfigOverlay);
currentConfigOverlay = null;
console.log("✅ 配置管理弹窗已自动关闭");
}
});
});
dialog.querySelector('#exportConfigBtn').addEventListener('click', () => {
exportConfig();
// 导出完成后关闭配置管理弹窗
setTimeout(() => {
if (currentConfigOverlay) {
closeExistingOverlay(currentConfigOverlay);
currentConfigOverlay = null;
console.log("✅ 配置管理弹窗已在导出后关闭");
}
}, 500); // 给导出操作一些时间完成
});
dialog.querySelector('#resetSettingsBtn').addEventListener('click', () => {
if (confirm('确认重置所有配置为默认设置吗?')) {
// 先关闭配置管理弹窗,提升用户体验
if (currentConfigOverlay) {
closeExistingOverlay(currentConfigOverlay);
currentConfigOverlay = null;
console.log("✅ 配置管理弹窗已在重置前关闭");
}
// 执行配置重置
buttonConfig = JSON.parse(JSON.stringify(defaultConfig));
// 重置folderOrder
buttonConfig.folderOrder = Object.keys(buttonConfig.folders);
// 确保所有按钮都有'type'字段和'autoSubmit'字段
Object.entries(buttonConfig.folders).forEach(([folderName, folderConfig]) => {
Object.entries(folderConfig.buttons).forEach(([btnName, btnCfg]) => {
if (!btnCfg.type) {
if (folderName === "🖱️") {
btnCfg.type = "tool";
} else {
btnCfg.type = "template";
}
}
if (btnCfg.type === "template" && typeof btnCfg.autoSubmit !== 'boolean') {
btnCfg.autoSubmit = false;
}
});
});
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
// 重新渲染设置面板(如果还打开着)
if (currentSettingsOverlay) {
selectedFolderName = buttonConfig.folderOrder[0] || null;
renderFolderList();
renderButtonList();
}
console.log("🔄 配置已重置为默认设置。");
// 更新按钮栏
updateButtonContainer();
// 重置后应用默认/匹配样式
try { applyDomainStyles(); } catch (_) {}
// 立即更新计数器
setTimeout(() => {
updateCounters();
console.log("📊 重置后计数器已更新。");
// 在所有更新完成后显示成功提示
setTimeout(() => {
alert('已重置为默认配置');
}, 50);
}, 100);
}
});
};
let selectedFolderName = buttonConfig.folderOrder[0] || null; // 在设置面板中使用
let folderListContainer, buttonListContainer; // 在渲染函数中定义
const renderFolderList = () => {
if (!folderListContainer) return;
setTrustedHTML(folderListContainer, '');
const foldersArray = buttonConfig.folderOrder.map(fname => [fname, buttonConfig.folders[fname]]).filter(([f,c])=>c);
foldersArray.forEach(([fname, fconfig]) => {
const folderItem = document.createElement('div');
folderItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-radius: 4px;
margin: 4px 0;
background-color: ${selectedFolderName === fname ? (isDarkMode() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0,0,0,0.1)') : 'transparent'};
cursor: move;
direction: ltr;
min-height: 36px;
`;
folderItem.classList.add('folder-item');
folderItem.setAttribute('draggable', 'true');
folderItem.setAttribute('data-folder', fname);
const {container: leftInfo, folderPreview} = (function createFolderPreview(fname, fconfig){
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '8px';
container.style.flex = '1';
container.style.minWidth = '0'; // 允许收缩
container.style.paddingRight = '8px';
const folderPreview = document.createElement('button');
folderPreview.textContent = fname;
folderPreview.style.backgroundColor = fconfig.color;
folderPreview.style.color = fconfig.textColor || '#ffffff';
folderPreview.style.border = 'none';
folderPreview.style.borderRadius = '4px';
folderPreview.style.padding = '4px 8px';
folderPreview.style.fontSize = '14px';
folderPreview.style.cursor = 'grab';
folderPreview.style.whiteSpace = 'nowrap';
folderPreview.style.overflow = 'hidden';
folderPreview.style.textOverflow = 'ellipsis';
folderPreview.style.maxWidth = '140px';
container.appendChild(folderPreview);
return {container, folderPreview};
})(fname, fconfig);
const rightBtns = document.createElement('div');
rightBtns.style.display = 'flex';
rightBtns.style.gap = '4px'; // 增加按钮间的间距
rightBtns.style.alignItems = 'center';
rightBtns.style.width = '130px'; // 与标签栏保持一致的宽度
rightBtns.style.justifyContent = 'flex-start'; // 改为左对齐
rightBtns.style.paddingLeft = '8px'; // 添加左侧padding与标签栏对齐
rightBtns.style.paddingRight = '12px'; // 添加右侧padding
// 创建隐藏状态勾选框容器
const hiddenCheckboxContainer = document.createElement('div');
hiddenCheckboxContainer.style.display = 'flex';
hiddenCheckboxContainer.style.alignItems = 'center';
hiddenCheckboxContainer.style.justifyContent = 'center';
hiddenCheckboxContainer.style.width = '36px'; // 与标签栏"显示"列宽度一致
hiddenCheckboxContainer.style.marginRight = '4px'; // 添加右边距
hiddenCheckboxContainer.style.padding = '2px';
hiddenCheckboxContainer.style.borderRadius = '3px';
hiddenCheckboxContainer.style.cursor = 'pointer';
hiddenCheckboxContainer.title = '勾选后该文件夹将在主界面显示';
const hiddenCheckbox = document.createElement('input');
hiddenCheckbox.type = 'checkbox';
hiddenCheckbox.checked = !fconfig.hidden; // 勾选表示显示
hiddenCheckbox.style.cursor = 'pointer';
hiddenCheckbox.style.accentColor = 'var(--primary-color, #3B82F6)';
hiddenCheckbox.style.margin = '0';
hiddenCheckbox.style.transform = 'scale(1.1)'; // 稍微放大勾选框以便操作
// 删除了checkboxText元素,不再显示"显示"文字
// 关键修复:先添加change事件监听器到checkbox
hiddenCheckbox.addEventListener('change', (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
const newHiddenState = !hiddenCheckbox.checked;
fconfig.hidden = newHiddenState;
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
console.log(`✅ 文件夹 "${fname}" 的隐藏状态已设置为 ${fconfig.hidden}`);
updateButtonContainer();
});
// 为checkbox添加click事件,确保优先处理
hiddenCheckbox.addEventListener('click', (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
});
// 容器点击事件,点击容器时切换checkbox状态
hiddenCheckboxContainer.addEventListener('click', (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
// 如果点击的不是checkbox本身,则手动切换checkbox状态
if (e.target !== hiddenCheckbox) {
hiddenCheckbox.checked = !hiddenCheckbox.checked;
// 手动触发change事件
const changeEvent = new Event('change', { bubbles: false });
hiddenCheckbox.dispatchEvent(changeEvent);
}
});
hiddenCheckboxContainer.appendChild(hiddenCheckbox);
// 不再添加checkboxText
// 创建编辑按钮
const editFolderBtn = document.createElement('button');
editFolderBtn.textContent = '✏️';
editFolderBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: var(--primary-color, #3B82F6);
width: 36px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
`;
editFolderBtn.addEventListener('click', (e) => {
e.stopPropagation();
showFolderEditDialog(fname, fconfig, (newFolderName) => {
selectedFolderName = newFolderName;
renderFolderList();
renderButtonList();
});
});
const deleteFolderBtn = document.createElement('button');
deleteFolderBtn.textContent = '🗑️';
deleteFolderBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: var(--danger-color, #ef4444);
width: 36px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
`;
deleteFolderBtn.addEventListener('click', (e) => {
e.stopPropagation();
showDeleteFolderConfirmDialog(fname, () => {
const allFolders = buttonConfig.folderOrder;
selectedFolderName = allFolders[0] || null;
renderFolderList();
renderButtonList();
updateCounters(); // 更新所有计数器
});
});
rightBtns.appendChild(hiddenCheckboxContainer);
rightBtns.appendChild(editFolderBtn);
rightBtns.appendChild(deleteFolderBtn);
folderItem.appendChild(leftInfo);
folderItem.appendChild(rightBtns);
// 修改folderItem的点击事件,排除右侧按钮区域
folderItem.addEventListener('click', (e) => {
// 如果点击的是右侧按钮区域,不触发文件夹选择
if (rightBtns.contains(e.target)) {
return;
}
selectedFolderName = fname;
renderFolderList();
renderButtonList();
});
folderItem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', fname);
folderItem.style.opacity = '0.5';
});
folderItem.addEventListener('dragover', (e) => {
e.preventDefault();
});
folderItem.addEventListener('dragenter', () => {
folderItem.style.border = `2px solid var(--primary-color, #3B82F6)`;
});
folderItem.addEventListener('dragleave', () => {
folderItem.style.border = 'none';
});
folderItem.addEventListener('drop', (e) => {
e.preventDefault();
const draggedFolder = e.dataTransfer.getData('text/plain');
if (draggedFolder && draggedFolder !== fname) {
const draggedIndex = buttonConfig.folderOrder.indexOf(draggedFolder);
const targetIndex = buttonConfig.folderOrder.indexOf(fname);
if (draggedIndex > -1 && targetIndex > -1) {
const [removed] = buttonConfig.folderOrder.splice(draggedIndex, 1);
buttonConfig.folderOrder.splice(targetIndex, 0, removed);
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
renderFolderList();
renderButtonList();
console.log(`🔄 文件夹顺序已更新:${draggedFolder} 移动到 ${fname} 前。`);
// 更新按钮栏
updateButtonContainer();
}
}
// Check if a button is being dropped onto this folder
const buttonData = e.dataTransfer.getData('application/json');
if (buttonData) {
try {
const { buttonName: draggedBtnName, sourceFolder } = JSON.parse(buttonData);
if (draggedBtnName && sourceFolder && sourceFolder !== fname) {
// Move the button from sourceFolder to fname
const button = buttonConfig.folders[sourceFolder].buttons[draggedBtnName];
if (button) {
// Remove from source
delete buttonConfig.folders[sourceFolder].buttons[draggedBtnName];
// Add to target
buttonConfig.folders[fname].buttons[draggedBtnName] = button;
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
renderFolderList();
renderButtonList();
console.log(`🔄 按钮 "${draggedBtnName}" 已从 "${sourceFolder}" 移动到 "${fname}"。`);
// Update button container
updateButtonContainer();
}
}
} catch (error) {
console.error("解析拖放数据失败:", error);
}
}
folderItem.style.border = 'none';
});
folderItem.addEventListener('dragend', () => {
folderItem.style.opacity = '1';
});
folderListContainer.appendChild(folderItem);
});
};
// 升级:更新所有计数显示的函数
const updateCounters = () => {
// 计算统计数据
const totalFolders = Object.keys(buttonConfig.folders).length;
const totalButtons = Object.values(buttonConfig.folders).reduce((sum, folder) => {
return sum + Object.keys(folder.buttons).length;
}, 0);
// 更新文件夹总数计数
const folderCountBadge = queryUI('#folderCountBadge');
if (folderCountBadge) {
folderCountBadge.textContent = totalFolders.toString();
folderCountBadge.title = `共有 ${totalFolders} 个文件夹`;
}
// 更新按钮总数计数
const totalButtonCountBadge = queryUI('#totalButtonCountBadge');
if (totalButtonCountBadge) {
totalButtonCountBadge.textContent = totalButtons.toString();
totalButtonCountBadge.title = `所有文件夹共有 ${totalButtons} 个按钮`;
}
// 更新当前文件夹按钮数计数
if (selectedFolderName && buttonConfig.folders[selectedFolderName]) {
const currentFolderButtonCount = Object.keys(buttonConfig.folders[selectedFolderName].buttons).length;
const currentFolderBadge = queryUI('#currentFolderButtonCount');
if (currentFolderBadge) {
currentFolderBadge.textContent = currentFolderButtonCount.toString();
currentFolderBadge.title = `"${selectedFolderName}" 文件夹有 ${currentFolderButtonCount} 个按钮`;
}
}
console.log(`📊 计数器已更新: ${totalFolders}个文件夹, ${totalButtons}个按钮总数`);
};
const renderButtonList = () => {
if (!buttonListContainer) return;
setTrustedHTML(buttonListContainer, '');
if (!selectedFolderName) return;
const currentFolderConfig = buttonConfig.folders[selectedFolderName];
if (!currentFolderConfig) return;
const rightHeader = document.createElement('div');
rightHeader.style.display = 'flex';
rightHeader.style.justifyContent = 'space-between';
rightHeader.style.alignItems = 'center';
rightHeader.style.marginBottom = '8px';
const folderNameLabel = document.createElement('h3');
folderNameLabel.style.display = 'flex';
folderNameLabel.style.alignItems = 'center';
folderNameLabel.style.gap = '10px';
folderNameLabel.style.margin = '0';
const folderNameText = document.createElement('span');
setTrustedHTML(folderNameText, `➤ ${selectedFolderName}`);
const buttonCountBadge = document.createElement('span');
buttonCountBadge.id = 'currentFolderButtonCount';
buttonCountBadge.style.cssText = `
background-color: var(--info-color, #6366F1);
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
cursor: help;
transition: all 0.2s ease;
`;
// 计算当前文件夹的按钮数量
const buttonCount = Object.keys(currentFolderConfig.buttons).length;
buttonCountBadge.textContent = buttonCount.toString();
buttonCountBadge.title = `"${selectedFolderName}" 文件夹有 ${buttonCount} 个按钮`;
// 添加hover效果
buttonCountBadge.addEventListener('mouseenter', () => {
buttonCountBadge.style.transform = 'scale(1.15)';
buttonCountBadge.style.boxShadow = '0 2px 5px rgba(0,0,0,0.15)';
});
buttonCountBadge.addEventListener('mouseleave', () => {
buttonCountBadge.style.transform = 'scale(1)';
buttonCountBadge.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
});
folderNameLabel.appendChild(folderNameText);
folderNameLabel.appendChild(buttonCountBadge);
const addNewButtonBtn = document.createElement('button');
Object.assign(addNewButtonBtn.style, styles.button, {
backgroundColor: 'var(--add-color, #fd7e14)',
color: 'white',
borderRadius: '4px'
});
addNewButtonBtn.textContent = '+ 新建按钮';
addNewButtonBtn.addEventListener('click', () => {
showButtonEditDialog(selectedFolderName, '', {}, () => {
renderButtonList();
});
});
rightHeader.appendChild(folderNameLabel);
rightHeader.appendChild(addNewButtonBtn);
buttonListContainer.appendChild(rightHeader);
// 新增:创建包含标签栏和内容的容器,滚动条将出现在此容器右侧
const contentWithHeaderContainer = document.createElement('div');
contentWithHeaderContainer.style.cssText = `
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 4px;
`;
// 创建按钮列表标签栏 - 固定在滚动容器顶部
const buttonHeaderBar = document.createElement('div');
buttonHeaderBar.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
background-color: var(--button-bg, #f3f4f6);
border-bottom: 1px solid var(--border-color, #e5e7eb);
border-radius: 4px 4px 0 0;
font-size: 12px;
font-weight: 500;
color: var(--text-color, #333333);
position: sticky;
top: 0;
z-index: 2;
flex-shrink: 0;
`;
const leftButtonHeaderLabel = document.createElement('div');
leftButtonHeaderLabel.textContent = '按钮预览';
leftButtonHeaderLabel.style.flex = '1';
leftButtonHeaderLabel.style.textAlign = 'left';
leftButtonHeaderLabel.style.paddingLeft = 'calc(8px + 1em)';
const rightButtonHeaderLabels = document.createElement('div');
rightButtonHeaderLabels.style.display = 'flex';
rightButtonHeaderLabels.style.gap = '4px';
rightButtonHeaderLabels.style.alignItems = 'center';
rightButtonHeaderLabels.style.width = '240px';
rightButtonHeaderLabels.style.paddingLeft = '8px';
rightButtonHeaderLabels.style.paddingRight = '12px';
const variableLabel = document.createElement('div');
variableLabel.textContent = '变量';
variableLabel.style.width = '110px';
variableLabel.style.textAlign = 'center';
variableLabel.style.fontSize = '12px';
variableLabel.style.marginLeft = '-1em';
const autoSubmitLabel = document.createElement('div');
autoSubmitLabel.textContent = '自动提交';
autoSubmitLabel.style.width = '64px';
autoSubmitLabel.style.textAlign = 'center';
autoSubmitLabel.style.fontSize = '12px';
autoSubmitLabel.style.marginLeft = 'calc(-0.5em)';
const editButtonLabel = document.createElement('div');
editButtonLabel.textContent = '修改';
editButtonLabel.style.width = '40px';
editButtonLabel.style.textAlign = 'center';
editButtonLabel.style.fontSize = '12px';
const deleteButtonLabel = document.createElement('div');
deleteButtonLabel.textContent = '删除';
deleteButtonLabel.style.width = '36px';
deleteButtonLabel.style.textAlign = 'center';
deleteButtonLabel.style.fontSize = '12px';
rightButtonHeaderLabels.appendChild(variableLabel);
rightButtonHeaderLabels.appendChild(autoSubmitLabel);
rightButtonHeaderLabels.appendChild(editButtonLabel);
rightButtonHeaderLabels.appendChild(deleteButtonLabel);
buttonHeaderBar.appendChild(leftButtonHeaderLabel);
buttonHeaderBar.appendChild(rightButtonHeaderLabels);
// 修改:内容区域不再需要自己的滚动条和边框
const btnScrollArea = document.createElement('div');
btnScrollArea.style.cssText = `
flex: 1;
padding: 8px;
overflow-y: visible;
min-height: 0;
`;
const currentFolderButtons = Object.entries(currentFolderConfig.buttons);
const createButtonPreview = (btnName, btnCfg) => {
const btnEl = createCustomButtonElement(btnName, btnCfg);
btnEl.style.marginBottom = '0px';
btnEl.style.marginRight = '8px';
btnEl.style.cursor = 'grab';
btnEl.style.flexShrink = '1';
btnEl.style.minWidth = '0';
btnEl.style.maxWidth = '100%';
btnEl.style.whiteSpace = 'normal';
btnEl.style.wordBreak = 'break-word';
btnEl.style.overflow = 'visible';
btnEl.style.lineHeight = '1.4';
btnEl.style.overflowWrap = 'anywhere';
btnEl.style.display = 'inline-flex';
btnEl.style.flexWrap = 'wrap';
btnEl.style.alignItems = 'center';
btnEl.style.justifyContent = 'flex-start';
btnEl.style.columnGap = '6px';
btnEl.style.rowGap = '2px';
btnEl.style.alignSelf = 'flex-start';
return btnEl;
};
currentFolderButtons.forEach(([btnName, cfg]) => {
const btnItem = document.createElement('div');
btnItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
padding: 4px;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 4px;
background-color: var(--button-bg, #f3f4f6);
cursor: move;
width: 100%;
box-sizing: border-box;
overflow: visible;
`;
btnItem.setAttribute('draggable', 'true');
btnItem.setAttribute('data-button-name', btnName);
const leftPart = document.createElement('div');
leftPart.style.display = 'flex';
leftPart.style.alignItems = 'flex-start';
leftPart.style.gap = '8px';
leftPart.style.flex = '1';
leftPart.style.minWidth = '0';
leftPart.style.overflow = 'visible';
const previewWrapper = document.createElement('div');
previewWrapper.style.display = 'flex';
previewWrapper.style.alignItems = 'flex-start';
previewWrapper.style.flex = '1 1 auto';
previewWrapper.style.maxWidth = '100%';
previewWrapper.style.minWidth = '0';
previewWrapper.style.overflow = 'visible';
previewWrapper.style.alignSelf = 'flex-start';
const btnPreview = createButtonPreview(btnName, cfg);
previewWrapper.appendChild(btnPreview);
leftPart.appendChild(previewWrapper);
const opsDiv = document.createElement('div');
opsDiv.style.display = 'flex';
opsDiv.style.gap = '4px';
opsDiv.style.alignItems = 'center';
opsDiv.style.width = '240px';
opsDiv.style.paddingLeft = '8px';
opsDiv.style.paddingRight = '12px';
opsDiv.style.flexShrink = '0';
const variableInfoContainer = document.createElement('div');
variableInfoContainer.style.display = 'flex';
variableInfoContainer.style.alignItems = 'center';
variableInfoContainer.style.justifyContent = 'center';
variableInfoContainer.style.flexDirection = 'column';
variableInfoContainer.style.width = '110px';
variableInfoContainer.style.fontSize = '12px';
variableInfoContainer.style.lineHeight = '1.2';
variableInfoContainer.style.wordBreak = 'break-word';
variableInfoContainer.style.textAlign = 'center';
variableInfoContainer.style.color = 'var(--text-color, #333333)';
if (cfg.type === 'template') {
const variablesUsed = extractTemplateVariables(cfg.text || '');
if (variablesUsed.length > 0) {
const displayText = variablesUsed.join('、');
variableInfoContainer.textContent = displayText;
variableInfoContainer.title = `模板变量: ${displayText}`;
} else {
variableInfoContainer.textContent = '无';
variableInfoContainer.title = '未使用模板变量';
}
} else {
variableInfoContainer.textContent = '—';
variableInfoContainer.title = '工具按钮不使用模板变量';
}
// 创建"自动提交"开关容器
const autoSubmitContainer = document.createElement('div');
autoSubmitContainer.style.display = 'flex';
autoSubmitContainer.style.alignItems = 'center';
autoSubmitContainer.style.justifyContent = 'center';
autoSubmitContainer.style.width = '60px';
const autoSubmitCheckbox = document.createElement('input');
autoSubmitCheckbox.type = 'checkbox';
autoSubmitCheckbox.checked = cfg.autoSubmit || false;
autoSubmitCheckbox.style.cursor = 'pointer';
autoSubmitCheckbox.style.accentColor = 'var(--primary-color, #3B82F6)';
autoSubmitCheckbox.style.margin = '0';
autoSubmitCheckbox.style.transform = 'scale(1.1)';
autoSubmitCheckbox.addEventListener('change', () => {
cfg.autoSubmit = autoSubmitCheckbox.checked;
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
console.log(`✅ 按钮 "${btnName}" 的自动提交已设置为 ${autoSubmitCheckbox.checked}`);
});
autoSubmitContainer.appendChild(autoSubmitCheckbox);
// 创建编辑按钮
const editBtn = document.createElement('button');
editBtn.textContent = '✏️';
editBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
color: var(--primary-color, #3B82F6);
font-size: 14px;
width: 36px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
`;
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
showButtonEditDialog(selectedFolderName, btnName, cfg, () => {
renderButtonList();
});
});
// 创建删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
color: var(--danger-color, #ef4444);
font-size: 14px;
width: 36px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
`;
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
showDeleteButtonConfirmDialog(selectedFolderName, btnName, () => {
renderButtonList();
});
});
opsDiv.appendChild(variableInfoContainer);
opsDiv.appendChild(autoSubmitContainer);
opsDiv.appendChild(editBtn);
opsDiv.appendChild(deleteBtn);
btnItem.appendChild(leftPart);
btnItem.appendChild(opsDiv);
btnItem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('application/json', JSON.stringify({
buttonName: btnName,
sourceFolder: selectedFolderName
}));
btnItem.style.opacity = '0.5';
});
btnItem.addEventListener('dragover', (e) => {
e.preventDefault();
});
btnItem.addEventListener('dragenter', () => {
btnItem.style.border = `2px solid var(--primary-color, #3B82F6)`;
});
btnItem.addEventListener('dragleave', () => {
btnItem.style.border = `1px solid var(--border-color, #e5e7eb)`;
});
btnItem.addEventListener('drop', (e) => {
e.preventDefault();
const data = JSON.parse(e.dataTransfer.getData('application/json'));
const { buttonName: draggedBtnName } = data;
if (draggedBtnName && draggedBtnName !== btnName) {
const buttonsKeys = Object.keys(buttonConfig.folders[selectedFolderName].buttons);
const draggedIndex = buttonsKeys.indexOf(draggedBtnName);
const targetIndex = buttonsKeys.indexOf(btnName);
if (draggedIndex > -1 && targetIndex > -1) {
const reordered = [...buttonsKeys];
reordered.splice(draggedIndex, 1);
reordered.splice(targetIndex, 0, draggedBtnName);
const newOrderedMap = {};
reordered.forEach(k => {
newOrderedMap[k] = buttonConfig.folders[selectedFolderName].buttons[k];
});
buttonConfig.folders[selectedFolderName].buttons = newOrderedMap;
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
renderButtonList();
console.log(`🔄 按钮顺序已更新:${draggedBtnName} 移动到 ${btnName} 前。`);
// 更新按钮栏
updateButtonContainer();
}
}
btnItem.style.border = `1px solid var(--border-color, #e5e7eb)`;
});
btnItem.addEventListener('dragend', () => {
btnItem.style.opacity = '1';
});
btnScrollArea.appendChild(btnItem);
});
// 修改:将标签栏和内容区域添加到新的容器中
contentWithHeaderContainer.appendChild(buttonHeaderBar);
contentWithHeaderContainer.appendChild(btnScrollArea);
// 修改:将新容器添加到主容器中
buttonListContainer.appendChild(contentWithHeaderContainer);
};
function updateButtonBarHeight(newHeight) {
const clamped = Math.min(150, Math.max(50, newHeight)); // 限制范围
buttonConfig.buttonBarHeight = clamped;
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
// 更新容器高度
const container = queryUI('.folder-buttons-container');
if (container) {
container.style.height = clamped + 'px';
try {
updateButtonBarLayout(container, clamped);
} catch (err) {
console.warn('更新按钮栏布局失败:', err);
}
}
console.log("🔧 按钮栏高度已更新为", clamped, "px");
try {
applyDomainStyles();
} catch (err) {
console.warn('应用域名样式失败:', err);
}
}
const showUnifiedSettingsDialog = () => {
if (settingsDialogMainContainer) {
settingsDialogMainContainer.style.minHeight = '';
settingsDialogMainContainer = null;
}
if (currentSettingsOverlay) {
closeExistingOverlay(currentSettingsOverlay);
currentSettingsOverlay = null;
}
const overlay = document.createElement('div');
overlay.classList.add('settings-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 11000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('settings-dialog');
dialog.classList.add('cttf-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 920px;
max-width: 95vw;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.marginBottom = '16px';
const title = document.createElement('h2');
title.style.display = 'flex';
title.style.alignItems = 'center';
title.style.gap = '12px';
title.style.margin = '0';
title.style.fontSize = '20px';
title.style.fontWeight = '600';
const titleText = document.createElement('span');
titleText.textContent = "⚙️ 设置面板";
const collapseToggleBtn = document.createElement('button');
collapseToggleBtn.type = 'button';
collapseToggleBtn.style.cssText = `
background-color: transparent;
color: var(--text-color, #333333);
border: none;
border-radius: 4px;
padding: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
`;
collapseToggleBtn.title = '折叠左侧设置区域';
collapseToggleBtn.setAttribute('aria-label', collapseToggleBtn.title);
const collapseToggleSVG = ``;
setTrustedHTML(collapseToggleBtn, collapseToggleSVG);
collapseToggleBtn.style.flex = '0 0 auto';
collapseToggleBtn.style.flexShrink = '0';
collapseToggleBtn.style.width = '28px';
collapseToggleBtn.style.height = '28px';
collapseToggleBtn.style.minWidth = '28px';
collapseToggleBtn.style.minHeight = '28px';
collapseToggleBtn.style.maxWidth = '28px';
collapseToggleBtn.style.maxHeight = '28px';
collapseToggleBtn.style.padding = '0';
collapseToggleBtn.style.lineHeight = '0';
collapseToggleBtn.style.boxSizing = 'border-box';
collapseToggleBtn.style.aspectRatio = '1 / 1';
const collapseToggleIcon = collapseToggleBtn.querySelector('svg');
if (collapseToggleIcon) {
collapseToggleIcon.style.width = '16px';
collapseToggleIcon.style.height = '16px';
collapseToggleIcon.style.display = 'block';
collapseToggleIcon.style.flex = '0 0 auto';
}
// 计数器容器
const countersContainer = document.createElement('div');
countersContainer.style.display = 'flex';
countersContainer.style.gap = '8px';
countersContainer.style.alignItems = 'center';
// 文件夹总数计数器(圆形)
const folderCountBadge = document.createElement('span');
folderCountBadge.id = 'folderCountBadge';
folderCountBadge.style.cssText = `
background-color: var(--primary-color, #3B82F6);
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: help;
transition: all 0.2s ease;
`;
// 按钮总数计数器(圆形)
const totalButtonCountBadge = document.createElement('span');
totalButtonCountBadge.id = 'totalButtonCountBadge';
totalButtonCountBadge.style.cssText = `
background-color: var(--success-color, #22c55e);
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: help;
transition: all 0.2s ease;
`;
// 计算初始数据
const totalFolders = Object.keys(buttonConfig.folders).length;
const totalButtons = Object.values(buttonConfig.folders).reduce((sum, folder) => {
return sum + Object.keys(folder.buttons).length;
}, 0);
// 设置计数和提示
folderCountBadge.textContent = totalFolders.toString();
folderCountBadge.title = `共有 ${totalFolders} 个文件夹`;
totalButtonCountBadge.textContent = totalButtons.toString();
totalButtonCountBadge.title = `所有文件夹共有 ${totalButtons} 个按钮`;
// 添加hover效果
[folderCountBadge, totalButtonCountBadge].forEach(badge => {
badge.addEventListener('mouseenter', () => {
badge.style.transform = 'scale(1.1)';
badge.style.boxShadow = '0 3px 6px rgba(0,0,0,0.15)';
});
badge.addEventListener('mouseleave', () => {
badge.style.transform = 'scale(1)';
badge.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
});
});
countersContainer.appendChild(folderCountBadge);
countersContainer.appendChild(totalButtonCountBadge);
title.appendChild(collapseToggleBtn);
title.appendChild(titleText);
title.appendChild(countersContainer);
const headerBtnsWrapper = document.createElement('div');
headerBtnsWrapper.style.display = 'flex';
headerBtnsWrapper.style.gap = '10px';
// 新建自动化按钮
const automationBtn = document.createElement('button');
automationBtn.innerText = '⚡ 自动化';
automationBtn.type = 'button';
automationBtn.style.backgroundColor = 'var(--info-color, #4F46E5)';
automationBtn.style.color = 'white';
automationBtn.style.border = 'none';
automationBtn.style.borderRadius = '4px';
automationBtn.style.padding = '5px 10px';
automationBtn.style.cursor = 'pointer';
automationBtn.style.fontSize = '14px';
automationBtn.addEventListener('click', () => {
showAutomationSettingsDialog();
});
headerBtnsWrapper.appendChild(automationBtn);
// 样式管理按钮
const styleMgmtBtn = document.createElement('button');
styleMgmtBtn.innerText = '🎨 样式管理';
styleMgmtBtn.type = 'button';
styleMgmtBtn.style.backgroundColor = 'var(--info-color, #4F46E5)';
styleMgmtBtn.style.color = 'white';
styleMgmtBtn.style.border = 'none';
styleMgmtBtn.style.borderRadius = '4px';
styleMgmtBtn.style.padding = '5px 10px';
styleMgmtBtn.style.cursor = 'pointer';
styleMgmtBtn.style.fontSize = '14px';
styleMgmtBtn.addEventListener('click', () => {
showStyleSettingsDialog();
});
headerBtnsWrapper.appendChild(styleMgmtBtn);
// 原有创建配置管理按钮
const openConfigBtn = createConfigSettingsButton();
headerBtnsWrapper.appendChild(openConfigBtn);
// 原有保存关闭按钮
const saveSettingsBtn = document.createElement('button');
Object.assign(saveSettingsBtn.style, styles.button, {
backgroundColor: 'var(--success-color, #22c55e)',
color: 'white',
borderRadius: '4px'
});
saveSettingsBtn.textContent = '💾 关闭并保存';
saveSettingsBtn.addEventListener('click', () => {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
// 关闭所有相关弹窗
if (currentConfigOverlay) {
closeExistingOverlay(currentConfigOverlay);
currentConfigOverlay = null;
}
if (settingsDialogMainContainer) {
settingsDialogMainContainer.style.minHeight = '';
settingsDialogMainContainer = null;
}
closeExistingOverlay(overlay);
currentSettingsOverlay = null;
attachButtons();
console.log("✅ 设置已保存并关闭设置面板。");
updateButtonContainer();
});
headerBtnsWrapper.appendChild(saveSettingsBtn);
header.appendChild(title);
header.appendChild(headerBtnsWrapper);
const mainContainer = document.createElement('div');
mainContainer.style.display = 'flex';
mainContainer.style.flex = '1';
mainContainer.style.overflow = 'hidden';
mainContainer.style.flexWrap = 'nowrap';
mainContainer.style.overflowX = 'auto';
mainContainer.style.borderTop = `1px solid var(--border-color, #e5e7eb)`;
settingsDialogMainContainer = mainContainer;
const folderPanel = document.createElement('div');
folderPanel.style.display = 'flex';
folderPanel.style.flexDirection = 'column';
folderPanel.style.width = '280px';
folderPanel.style.minWidth = '280px';
folderPanel.style.marginRight = '12px';
folderPanel.style.overflowY = 'auto';
folderPanel.style.padding = '2px 8px 4px 2px';
// 新增:创建文件夹列表标签栏
const folderHeaderBar = document.createElement('div');
folderHeaderBar.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
background-color: var(--button-bg, #f3f4f6);
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 4px 4px 0 0;
margin: 0 0 -1px 0;
font-size: 12px;
font-weight: 500;
color: var(--text-color, #333333);
border-bottom: 1px solid var(--border-color, #e5e7eb);
position: sticky;
top: 0;
z-index: 1;
`;
const leftHeaderLabel = document.createElement('div');
leftHeaderLabel.textContent = '文件夹名称';
leftHeaderLabel.style.flex = '1';
leftHeaderLabel.style.textAlign = 'left';
leftHeaderLabel.style.paddingLeft = 'calc(8px + 1em)';
const rightHeaderLabels = document.createElement('div');
rightHeaderLabels.style.display = 'flex';
rightHeaderLabels.style.gap = '0px';
rightHeaderLabels.style.alignItems = 'center';
rightHeaderLabels.style.width = '140px'; // 增加宽度以提供更多间距
rightHeaderLabels.style.paddingLeft = '8px'; // 添加左侧padding,向左移动标签
rightHeaderLabels.style.paddingRight = '12px'; // 增加右侧间距
const showLabel = document.createElement('div');
showLabel.textContent = '显示';
showLabel.style.width = '36px'; // 稍微减小宽度
showLabel.style.textAlign = 'center';
showLabel.style.fontSize = '12px';
showLabel.style.marginRight = '4px'; // 添加右边距
const editLabel = document.createElement('div');
editLabel.textContent = '修改';
editLabel.style.width = '36px'; // 稍微减小宽度
editLabel.style.textAlign = 'center';
editLabel.style.fontSize = '12px';
editLabel.style.marginRight = '4px'; // 添加右边距
const deleteLabel = document.createElement('div');
deleteLabel.textContent = '删除';
deleteLabel.style.width = '36px'; // 稍微减小宽度
deleteLabel.style.textAlign = 'center';
deleteLabel.style.fontSize = '12px';
rightHeaderLabels.appendChild(showLabel);
rightHeaderLabels.appendChild(editLabel);
rightHeaderLabels.appendChild(deleteLabel);
folderHeaderBar.appendChild(leftHeaderLabel);
folderHeaderBar.appendChild(rightHeaderLabels);
folderListContainer = document.createElement('div');
folderListContainer.style.flex = '1';
folderListContainer.style.overflowY = 'auto';
folderListContainer.style.padding = '8px';
folderListContainer.style.direction = 'rtl';
folderListContainer.style.border = '1px solid var(--border-color, #e5e7eb)';
folderListContainer.style.borderTop = 'none';
folderListContainer.style.borderRadius = '0 0 4px 4px';
const folderAddContainer = document.createElement('div');
folderAddContainer.style.padding = '8px';
folderAddContainer.style.display = 'flex';
folderAddContainer.style.justifyContent = 'center';
const addNewFolderBtn = document.createElement('button');
Object.assign(addNewFolderBtn.style, styles.button, {
backgroundColor: 'var(--add-color, #fd7e14)',
color: 'white',
borderRadius: '4px'
});
addNewFolderBtn.textContent = '+ 新建文件夹';
addNewFolderBtn.addEventListener('click', () => {
showFolderEditDialog('', {}, (newFolderName) => {
selectedFolderName = newFolderName;
renderFolderList();
renderButtonList();
console.log(`🆕 新建文件夹 "${newFolderName}" 已添加。`);
});
});
folderAddContainer.appendChild(addNewFolderBtn);
folderPanel.appendChild(folderHeaderBar);
folderPanel.appendChild(folderListContainer);
folderPanel.appendChild(folderAddContainer);
buttonListContainer = document.createElement('div');
buttonListContainer.style.flex = '1';
buttonListContainer.style.overflowY = 'auto';
buttonListContainer.style.display = 'flex';
buttonListContainer.style.flexDirection = 'column';
buttonListContainer.style.padding = '8px 8px 4px 8px';
buttonListContainer.style.minWidth = '520px'; // 加宽右侧区域以提供更多内容空间
const updateFolderPanelVisibility = () => {
const container = settingsDialogMainContainer || mainContainer;
if (isSettingsFolderPanelCollapsed) {
if (container) {
const currentHeight = container.offsetHeight;
if (currentHeight > 0) {
container.style.minHeight = `${currentHeight}px`;
} else {
window.requestAnimationFrame(() => {
if (!isSettingsFolderPanelCollapsed) return;
const activeContainer = settingsDialogMainContainer || container;
if (!activeContainer) return;
const measuredHeight = activeContainer.offsetHeight;
if (measuredHeight > 0) {
activeContainer.style.minHeight = `${measuredHeight}px`;
}
});
}
}
folderPanel.style.display = 'none';
collapseToggleBtn.title = '展开左侧设置区域';
collapseToggleBtn.setAttribute('aria-label', '展开左侧设置区域');
} else {
folderPanel.style.display = 'flex';
collapseToggleBtn.title = '折叠左侧设置区域';
collapseToggleBtn.setAttribute('aria-label', '折叠左侧设置区域');
if (container) {
container.style.minHeight = '';
}
}
};
collapseToggleBtn.addEventListener('click', () => {
isSettingsFolderPanelCollapsed = !isSettingsFolderPanelCollapsed;
updateFolderPanelVisibility();
});
updateFolderPanelVisibility();
renderFolderList();
renderButtonList();
mainContainer.appendChild(folderPanel);
mainContainer.appendChild(buttonListContainer);
const footer = document.createElement('div');
footer.style.display = 'none';
dialog.appendChild(header);
dialog.appendChild(mainContainer);
dialog.appendChild(footer);
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentSettingsOverlay = overlay;
// 动画效果
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
};
let currentAutomationOverlay = null;
/**
*
* 弹窗:自动化设置,显示所有 domainAutoSubmitSettings,并可删除、点击添加
*/
function showAutomationSettingsDialog() {
// 若已存在则先关闭
if (currentAutomationOverlay) {
closeExistingOverlay(currentAutomationOverlay);
}
// 使用 createUnifiedDialog 统一创建 overlay + dialog
const { overlay, dialog } = createUnifiedDialog({
title: '⚡ 自动化设置',
width: '750px', // 保留你想要的宽度
onClose: () => {
currentAutomationOverlay = null;
},
closeOnOverlayClick: false
});
currentAutomationOverlay = overlay;
// 这里是新写法:在 dialog 里 appendChild 内部内容
// 注意,createUnifiedDialog 已经注入了 overlay 与动画
// 1) 构建内容区, 并插入到 dialog
const infoDiv = document.createElement('div');
infoDiv.style.textAlign = 'right';
infoDiv.style.marginBottom = '10px';
// 原先的 "关闭并保存" 按钮
const closeAutomationBtn = document.createElement('button');
closeAutomationBtn.id = 'closeAutomationBtn';
closeAutomationBtn.textContent = '💾 关闭并保存';
closeAutomationBtn.style.cssText = `
background-color: var(--success-color, #22c55e);
color: #fff;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
position: absolute; /* 若想固定在右上角,可再自行定位 */
top: 20px;
right: 20px;
`;
closeAutomationBtn.addEventListener('click', () => {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
closeExistingOverlay(overlay);
currentAutomationOverlay = null;
});
infoDiv.appendChild(closeAutomationBtn);
dialog.appendChild(infoDiv);
// 2) 列表容器 + 渲染 domainAutoSubmitSettings
const listContainer = document.createElement('div');
listContainer.style.cssText = `
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--dialog-bg, #ffffff);
max-height: 320px;
`;
const listHeader = document.createElement('div');
listHeader.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background-color: var(--button-bg, #f3f4f6);
border-bottom: 1px solid var(--border-color, #e5e7eb);
font-size: 12px;
font-weight: 500;
color: var(--text-color, #333333);
flex-shrink: 0;
`;
const headerColumns = [
{ label: '图标', flex: '0 0 32px', justify: 'center' },
{ label: '网站|网址', flex: '1 1 0%', justify: 'flex-start', paddingLeft: '8px' },
{ label: '提交方式', flex: '0 0 120px', justify: 'center' },
{ label: '修改', flex: '0 0 40px', justify: 'center' },
{ label: '删除', flex: '0 0 40px', justify: 'center' }
];
headerColumns.forEach(({ label, flex, justify, paddingLeft }) => {
const column = document.createElement('div');
column.textContent = label;
column.style.display = 'flex';
column.style.alignItems = 'center';
column.style.justifyContent = justify;
column.style.flex = flex;
column.style.fontSize = '12px';
column.style.fontWeight = '600';
if (paddingLeft) {
column.style.paddingLeft = paddingLeft;
}
listHeader.appendChild(column);
});
const listBody = document.createElement('div');
listBody.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
overflow-y: auto;
max-height: 260px;
`;
listBody.classList.add('hide-scrollbar');
listContainer.appendChild(listHeader);
listContainer.appendChild(listBody);
dialog.appendChild(listContainer);
const keyboardMethodPattern = /(enter|shift|caps|ctrl|control|cmd|meta|option|alt|space|tab|esc|escape|delete|backspace|home|end|page ?up|page ?down|arrow|up|down|left|right)/i;
const createKeyCapElement = (label) => {
const keyEl = document.createElement('span');
keyEl.textContent = label;
keyEl.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
padding: 3px 8px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: linear-gradient(180deg, rgba(17,17,17,0.95), rgba(45,45,45,0.95));
box-shadow: inset 0 -1px 0 rgba(255,255,255,0.12), 0 2px 4px rgba(0,0,0,0.45);
font-size: 12px;
font-weight: 600;
color: #ffffff;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
line-height: 1.2;
white-space: nowrap;
`;
return keyEl;
};
const createMethodDisplay = (rawMethod) => {
const methodValue = (rawMethod || '').trim();
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
container.style.gap = '6px';
container.style.flexWrap = 'wrap';
container.style.maxWidth = '100%';
container.style.fontSize = '12px';
container.style.fontWeight = '600';
if (!methodValue) {
const placeholder = document.createElement('span');
placeholder.textContent = '-';
placeholder.style.color = 'var(--muted-text-color, #6b7280)';
placeholder.style.fontWeight = '500';
container.appendChild(placeholder);
return container;
}
if (methodValue === '模拟点击提交按钮') {
const clickBadge = document.createElement('span');
clickBadge.textContent = '模拟点击';
clickBadge.style.cssText = `
padding: 4px 12px;
border-radius: 20px;
background: linear-gradient(180deg, rgba(253,224,71,0.85), rgba(251,191,36,0.9));
border: 1px solid rgba(217,119,6,0.4);
box-shadow: inset 0 -1px 0 rgba(217,119,6,0.35), 0 1px 2px rgba(217,119,6,0.25);
color: rgba(120,53,15,0.95);
font-weight: 700;
font-size: 12px;
letter-spacing: 0.02em;
white-space: nowrap;
`;
container.appendChild(clickBadge);
return container;
}
const shouldUseKeyStyle = keyboardMethodPattern.test(methodValue) || methodValue.includes('+') || methodValue.includes('/');
if (!shouldUseKeyStyle) {
const pill = document.createElement('span');
pill.textContent = methodValue;
pill.style.cssText = `
padding: 4px 10px;
background-color: rgba(59,130,246,0.12);
color: var(--primary-color, #3B82F6);
border-radius: 999px;
white-space: nowrap;
`;
container.appendChild(pill);
return container;
}
const combos = methodValue.split('/').map(segment => segment.trim()).filter(Boolean);
combos.forEach((combo, comboIdx) => {
if (comboIdx > 0) {
const divider = document.createElement('span');
divider.textContent = '/';
divider.style.color = 'var(--muted-text-color, #6b7280)';
divider.style.fontSize = '11px';
divider.style.fontWeight = '600';
container.appendChild(divider);
}
const comboWrapper = document.createElement('div');
comboWrapper.style.display = 'flex';
comboWrapper.style.alignItems = 'center';
comboWrapper.style.justifyContent = 'center';
comboWrapper.style.gap = '4px';
const keys = combo.split('+').map(part => part.trim()).filter(Boolean);
if (!keys.length) {
keys.push(combo);
}
keys.forEach((keyLabel, keyIdx) => {
if (keyIdx > 0) {
const plusSign = document.createElement('span');
plusSign.textContent = '+';
plusSign.style.color = 'var(--muted-text-color, #6b7280)';
plusSign.style.fontSize = '11px';
plusSign.style.fontWeight = '600';
comboWrapper.appendChild(plusSign);
}
comboWrapper.appendChild(createKeyCapElement(keyLabel));
});
container.appendChild(comboWrapper);
});
return container;
};
const showAutomationRuleDeleteConfirmDialog = (rule, onConfirm) => {
if (!rule) {
if (typeof onConfirm === 'function') {
onConfirm();
}
return;
}
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
const overlay = document.createElement('div');
overlay.classList.add('confirm-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 13000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('confirm-dialog', 'cttf-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 420px;
max-width: 90vw;
`;
const ruleName = rule.name || rule.domain || '未命名规则';
const ruleDomain = rule.domain || '(未指定网址)';
const faviconUrl = rule.favicon || generateDomainFavicon(rule.domain);
setTrustedHTML(dialog, `
🗑️ 确认删除自动化规则 "${ruleName}"?
❗️ 注意:此操作无法撤销!
${ruleName}
${ruleDomain}
自动提交方式:
`);
const methodPlaceholder = dialog.querySelector('.cttf-automation-method-container');
if (methodPlaceholder) {
const methodDisplay = createMethodDisplay(rule.method);
methodDisplay.style.justifyContent = 'flex-start';
methodPlaceholder.replaceWith(methodDisplay);
}
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
const cancelBtn = dialog.querySelector('#cancelAutomationRuleDelete');
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
}
const confirmBtn = dialog.querySelector('#confirmAutomationRuleDelete');
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
if (typeof onConfirm === 'function') {
onConfirm();
}
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
}
};
function renderDomainRules() {
setTrustedHTML(listBody, '');
const rules = buttonConfig.domainAutoSubmitSettings;
let metadataPatched = false;
if (!rules.length) {
const emptyState = document.createElement('div');
emptyState.textContent = '暂无自动化规则,点击下方“+ 新建”开始配置。';
emptyState.style.cssText = `
padding: 18px;
border-radius: 6px;
border: 1px dashed var(--border-color, #e5e7eb);
background-color: var(--button-bg, #f3f4f6);
color: var(--muted-text-color, #6b7280);
font-size: 13px;
text-align: center;
`;
listBody.appendChild(emptyState);
return;
}
rules.forEach((rule, idx) => {
const item = document.createElement('div');
item.style.cssText = `
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 6px;
background-color: var(--button-bg, #f3f4f6);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
`;
item.addEventListener('mouseenter', () => {
item.style.borderColor = 'var(--primary-color, #3B82F6)';
item.style.boxShadow = '0 3px 8px rgba(0,0,0,0.1)';
});
item.addEventListener('mouseleave', () => {
item.style.borderColor = 'var(--border-color, #e5e7eb)';
item.style.boxShadow = 'none';
});
const faviconUrl = rule.favicon || generateDomainFavicon(rule.domain);
if (!rule.favicon && rule.domain) {
rule.favicon = faviconUrl;
metadataPatched = true;
}
const faviconBadge = createFaviconElement(faviconUrl, rule.name || rule.domain, '🌐', { withBackground: false, size: 26 });
faviconBadge.title = rule.domain || '';
const iconColumn = document.createElement('div');
iconColumn.style.display = 'flex';
iconColumn.style.alignItems = 'center';
iconColumn.style.justifyContent = 'center';
iconColumn.style.flex = '0 0 30px';
iconColumn.appendChild(faviconBadge);
const infoColumn = document.createElement('div');
infoColumn.style.display = 'flex';
infoColumn.style.flexDirection = 'column';
infoColumn.style.gap = '4px';
infoColumn.style.minWidth = '0';
infoColumn.style.flex = '1 1 0%';
const nameEl = document.createElement('span');
nameEl.textContent = rule.name || rule.domain || '未命名规则';
nameEl.style.fontWeight = '600';
nameEl.style.fontSize = '14px';
nameEl.style.color = 'var(--text-color, #1f2937)';
const domainEl = document.createElement('span');
domainEl.textContent = rule.domain || '';
domainEl.style.fontSize = '12px';
domainEl.style.color = 'var(--muted-text-color, #6b7280)';
domainEl.style.whiteSpace = 'nowrap';
domainEl.style.overflow = 'hidden';
domainEl.style.textOverflow = 'ellipsis';
domainEl.style.maxWidth = '260px';
domainEl.title = rule.domain || '';
infoColumn.appendChild(nameEl);
infoColumn.appendChild(domainEl);
const methodDisplay = createMethodDisplay(rule.method || '-');
const editBtn = document.createElement('button');
editBtn.textContent = '✏️';
editBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
color: var(--primary-color, #3B82F6);
font-size: 14px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
`;
editBtn.addEventListener('mouseenter', () => {
editBtn.style.backgroundColor = 'rgba(59,130,246,0.12)';
});
editBtn.addEventListener('mouseleave', () => {
editBtn.style.backgroundColor = 'transparent';
});
editBtn.addEventListener('click', () => {
const ruleToEdit = buttonConfig.domainAutoSubmitSettings[idx];
showDomainRuleEditorDialog(ruleToEdit, (newData) => {
buttonConfig.domainAutoSubmitSettings[idx] = newData;
renderDomainRules();
});
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
color: var(--danger-color, #ef4444);
font-size: 14px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
`;
deleteBtn.addEventListener('mouseenter', () => {
deleteBtn.style.backgroundColor = 'rgba(239,68,68,0.12)';
});
deleteBtn.addEventListener('mouseleave', () => {
deleteBtn.style.backgroundColor = 'transparent';
});
deleteBtn.addEventListener('click', () => {
const ruleToDelete = buttonConfig.domainAutoSubmitSettings[idx];
showAutomationRuleDeleteConfirmDialog(ruleToDelete, () => {
buttonConfig.domainAutoSubmitSettings.splice(idx, 1);
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
renderDomainRules();
});
});
const methodColumn = document.createElement('div');
methodColumn.style.display = 'flex';
methodColumn.style.alignItems = 'center';
methodColumn.style.justifyContent = 'center';
methodColumn.style.flex = '0 0 120px';
methodColumn.appendChild(methodDisplay);
const editColumn = document.createElement('div');
editColumn.style.display = 'flex';
editColumn.style.alignItems = 'center';
editColumn.style.justifyContent = 'center';
editColumn.style.flex = '0 0 40px';
editColumn.appendChild(editBtn);
const deleteColumn = document.createElement('div');
deleteColumn.style.display = 'flex';
deleteColumn.style.alignItems = 'center';
deleteColumn.style.justifyContent = 'center';
deleteColumn.style.flex = '0 0 40px';
deleteColumn.appendChild(deleteBtn);
item.appendChild(iconColumn);
item.appendChild(infoColumn);
item.appendChild(methodColumn);
item.appendChild(editColumn);
item.appendChild(deleteColumn);
listBody.appendChild(item);
});
if (metadataPatched) {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
}
}
renderDomainRules();
// 3) 新建按钮
const addDiv = document.createElement('div');
addDiv.style.marginTop = '12px';
addDiv.style.textAlign = 'left';
const addBtn = document.createElement('button');
addBtn.textContent = '+ 新建';
addBtn.style.cssText = `
background-color: var(--add-color, #fd7e14);
color: #fff;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
`;
addBtn.addEventListener('click', () => {
showDomainRuleEditorDialog({}, (newData) => {
buttonConfig.domainAutoSubmitSettings.push(newData);
renderDomainRules();
});
});
addDiv.appendChild(addBtn);
dialog.appendChild(addDiv);
}
function showStyleSettingsDialog() {
// 若已存在则关闭
if (currentStyleOverlay) {
closeExistingOverlay(currentStyleOverlay);
}
// 使用统一弹窗
const { overlay, dialog } = createUnifiedDialog({
title: '🎨 样式管理',
width: '750px',
onClose: () => {
currentStyleOverlay = null;
},
closeOnOverlayClick: false
});
currentStyleOverlay = overlay;
// 说明文字
const desc = document.createElement('p');
desc.textContent = '您可根据不同网址,自定义按钮栏高度和注入CSS样式。';
dialog.appendChild(desc);
// 列表容器
const styleListContainer = document.createElement('div');
styleListContainer.style.cssText = `
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--dialog-bg, #ffffff);
max-height: 320px;
margin-bottom: 12px;
`;
const styleHeader = document.createElement('div');
styleHeader.style.cssText = `
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 6px 12px;
background-color: var(--button-bg, #f3f4f6);
border-bottom: 1px solid var(--border-color, #e5e7eb);
font-size: 12px;
font-weight: 500;
color: var(--text-color, #333333);
flex-shrink: 0;
`;
const headerColumns = [
{ label: '图标', flex: '0 0 32px', textAlign: 'center' },
{ label: '网站|网址', flex: '0.7 1 0%', textAlign: 'left', paddingLeft: '4px' },
{ label: '自定义css', flex: '3 1 0%', textAlign: 'center' },
{ label: '高度|底部', flex: '0 0 110px', textAlign: 'center' },
{ label: '修改', flex: '0 0 40px', textAlign: 'center' },
{ label: '删除', flex: '0 0 40px', textAlign: 'center' }
];
headerColumns.forEach((col) => {
const column = document.createElement('div');
column.textContent = col.label;
column.style.display = 'flex';
column.style.alignItems = 'center';
column.style.justifyContent = col.textAlign === 'right' ? 'flex-end'
: col.textAlign === 'center' ? 'center'
: 'flex-start';
column.style.textAlign = col.textAlign;
column.style.flex = col.flex;
column.style.fontSize = '12px';
column.style.fontWeight = '600';
if (col.paddingLeft) {
column.style.paddingLeft = col.paddingLeft;
}
styleHeader.appendChild(column);
});
const styleListBody = document.createElement('div');
styleListBody.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
overflow-y: auto;
max-height: 260px;
`;
styleListBody.classList.add('hide-scrollbar');
styleListContainer.appendChild(styleHeader);
styleListContainer.appendChild(styleListBody);
dialog.appendChild(styleListContainer);
const showStyleRuleDeleteConfirmDialog = (styleItem, onConfirm) => {
if (!styleItem) {
if (typeof onConfirm === 'function') {
onConfirm();
}
return;
}
if (currentConfirmOverlay) {
closeExistingOverlay(currentConfirmOverlay);
}
const overlay = document.createElement('div');
overlay.classList.add('confirm-overlay');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--overlay-bg, rgba(0, 0, 0, 0.5));
backdrop-filter: blur(2px);
z-index: 13000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.classList.add('confirm-dialog', 'cttf-dialog');
dialog.style.cssText = `
background-color: var(--dialog-bg, #ffffff);
color: var(--text-color, #333333);
border-radius: 4px;
padding: 24px;
box-shadow: 0 8px 24px var(--shadow-color, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e5e7eb);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 420px;
max-width: 90vw;
`;
const styleName = styleItem.name || styleItem.domain || '未命名样式';
const styleDomain = styleItem.domain || '(未指定网址)';
const styleHeight = styleItem.height ? `${styleItem.height}px` : '默认高度';
const rawStyleBottomSpacing = (typeof styleItem.bottomSpacing === 'number') ? styleItem.bottomSpacing : buttonConfig.buttonBarBottomSpacing;
const clampedStyleBottomSpacing = Math.max(-200, Math.min(200, Number(rawStyleBottomSpacing) || 0));
const styleBottomSpacing = `${clampedStyleBottomSpacing}px`;
const faviconUrl = styleItem.favicon || generateDomainFavicon(styleItem.domain);
const cssRaw = (styleItem.cssCode || '').trim();
const cssContent = cssRaw || '(未配置自定义 CSS)';
const cssLineCount = cssContent.split('\n').length;
const cssTextareaHeight = Math.min(Math.max(cssLineCount, 6), 24) * 18;
const escapeHtml = (str = '') => String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
setTrustedHTML(dialog, `
🗑️
确认删除样式 "${escapeHtml(styleName)}"?
❗️ 注意:此操作无法撤销!
${escapeHtml(styleName)}
${escapeHtml(styleDomain)}
按钮栏高度
${escapeHtml(styleHeight)}
距页面底部
${escapeHtml(styleBottomSpacing)}
`);
overlay.appendChild(dialog);
overlay.style.pointerEvents = 'auto';
appendToOverlayLayer(overlay);
currentConfirmOverlay = overlay;
setTimeout(() => {
overlay.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
dialog.querySelector('#cancelStyleRuleDelete')?.addEventListener('click', () => {
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
dialog.querySelector('#confirmStyleRuleDelete')?.addEventListener('click', () => {
if (typeof onConfirm === 'function') {
onConfirm();
}
closeExistingOverlay(overlay);
currentConfirmOverlay = null;
});
};
function renderDomainStyles() {
setTrustedHTML(styleListBody, '');
const styles = buttonConfig.domainStyleSettings;
let metadataPatched = false;
if (!styles.length) {
const emptyState = document.createElement('div');
emptyState.textContent = '尚未配置任何样式,点击下方“+ 新建”添加。';
emptyState.style.cssText = `
padding: 18px;
border-radius: 6px;
border: 1px dashed var(--border-color, #e5e7eb);
background-color: var(--button-bg, #f3f4f6);
color: var(--muted-text-color, #6b7280);
font-size: 13px;
text-align: center;
`;
styleListBody.appendChild(emptyState);
return;
}
styles.forEach((item, idx) => {
const row = document.createElement('div');
row.style.cssText = `
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 6px;
background-color: var(--button-bg, #f3f4f6);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
`;
row.addEventListener('mouseenter', () => {
row.style.borderColor = 'var(--info-color, #6366F1)';
row.style.boxShadow = '0 3px 8px rgba(0,0,0,0.1)';
});
row.addEventListener('mouseleave', () => {
row.style.borderColor = 'var(--border-color, #e5e7eb)';
row.style.boxShadow = 'none';
});
const faviconUrl = item.favicon || generateDomainFavicon(item.domain);
if (!item.favicon && item.domain) {
item.favicon = faviconUrl;
metadataPatched = true;
}
const faviconBadge = createFaviconElement(faviconUrl, item.name || item.domain, '🎨', { withBackground: false, size: 26 });
faviconBadge.title = item.domain || '自定义样式';
const iconColumn = document.createElement('div');
iconColumn.style.display = 'flex';
iconColumn.style.alignItems = 'center';
iconColumn.style.justifyContent = 'center';
iconColumn.style.flex = '0 0 30px';
iconColumn.appendChild(faviconBadge);
const siteColumn = document.createElement('div');
siteColumn.style.display = 'flex';
siteColumn.style.flexDirection = 'column';
siteColumn.style.gap = '4px';
siteColumn.style.minWidth = '100px';
siteColumn.style.flex = '0.7 1 0%';
const nameEl = document.createElement('span');
nameEl.textContent = item.name || '未命名样式';
nameEl.style.fontWeight = '600';
nameEl.style.fontSize = '14px';
nameEl.style.color = 'var(--text-color, #1f2937)';
const domainEl = document.createElement('span');
domainEl.textContent = item.domain || '未设置域名';
domainEl.style.fontSize = '12px';
domainEl.style.color = 'var(--muted-text-color, #6b7280)';
domainEl.style.whiteSpace = 'nowrap';
domainEl.style.overflow = 'hidden';
domainEl.style.textOverflow = 'ellipsis';
domainEl.style.maxWidth = '100%';
const cssSnippet = (item.cssCode || '').replace(/\s+/g, ' ').trim();
const snippetText = cssSnippet
? (cssSnippet.length > 80 ? `${cssSnippet.slice(0, 80)}…` : cssSnippet)
: '无自定义CSS';
const cssPreview = document.createElement('code');
cssPreview.textContent = snippetText;
cssPreview.style.cssText = `
display: block;
width: 100%;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
color: var(--muted-text-color, #6b7280);
background-color: rgba(17,24,39,0.04);
border-radius: 4px;
padding: 4px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
`;
cssPreview.title = item.cssCode || '无自定义CSS';
siteColumn.appendChild(nameEl);
siteColumn.appendChild(domainEl);
const cssColumn = document.createElement('div');
cssColumn.style.cssText = `
flex: 3 1 0%;
min-width: 0;
max-width: 100%;
display: flex;
align-items: center;
padding-right: 12px;
`;
cssColumn.appendChild(cssPreview);
const heightColumn = document.createElement('div');
heightColumn.style.display = 'flex';
heightColumn.style.alignItems = 'center';
heightColumn.style.justifyContent = 'center';
heightColumn.style.flex = '0 0 110px';
heightColumn.style.gap = '6px';
heightColumn.style.flexWrap = 'wrap';
const heightBadge = document.createElement('span');
heightBadge.textContent = item.height ? `${item.height}px` : '默认高度';
heightBadge.style.cssText = `
padding: 4px 10px;
background-color: rgba(16,185,129,0.12);
color: var(--success-color, #22c55e);
border-radius: 999px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
`;
const bottomSpacingValue = (typeof item.bottomSpacing === 'number') ? item.bottomSpacing : buttonConfig.buttonBarBottomSpacing;
const clampedBottomSpacingValue = Math.max(-200, Math.min(200, Number(bottomSpacingValue) || 0));
const bottomBadge = document.createElement('span');
bottomBadge.textContent = `${clampedBottomSpacingValue}px`;
bottomBadge.title = '按钮栏距页面底部间距';
bottomBadge.style.cssText = `
padding: 4px 10px;
background-color: rgba(59,130,246,0.12);
color: var(--primary-color, #3B82F6);
border-radius: 999px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
`;
const editBtn = document.createElement('button');
editBtn.textContent = '✏️';
editBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
color: var(--primary-color, #3B82F6);
font-size: 14px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
`;
editBtn.addEventListener('mouseenter', () => {
editBtn.style.backgroundColor = 'rgba(59,130,246,0.12)';
});
editBtn.addEventListener('mouseleave', () => {
editBtn.style.backgroundColor = 'transparent';
});
editBtn.addEventListener('click', () => {
showEditDomainStyleDialog(idx);
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
color: var(--danger-color, #ef4444);
font-size: 14px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
`;
deleteBtn.addEventListener('mouseenter', () => {
deleteBtn.style.backgroundColor = 'rgba(239,68,68,0.12)';
});
deleteBtn.addEventListener('mouseleave', () => {
deleteBtn.style.backgroundColor = 'transparent';
});
deleteBtn.addEventListener('click', () => {
const styleToDelete = buttonConfig.domainStyleSettings[idx];
showStyleRuleDeleteConfirmDialog(styleToDelete, () => {
buttonConfig.domainStyleSettings.splice(idx, 1);
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
renderDomainStyles();
// 删除后应用默认/其他匹配样式
try { applyDomainStyles(); } catch (_) {}
});
});
heightColumn.appendChild(heightBadge);
heightColumn.appendChild(bottomBadge);
const editColumn = document.createElement('div');
editColumn.style.display = 'flex';
editColumn.style.alignItems = 'center';
editColumn.style.justifyContent = 'center';
editColumn.style.flex = '0 0 40px';
editColumn.appendChild(editBtn);
const deleteColumn = document.createElement('div');
deleteColumn.style.display = 'flex';
deleteColumn.style.alignItems = 'center';
deleteColumn.style.justifyContent = 'center';
deleteColumn.style.flex = '0 0 40px';
deleteColumn.appendChild(deleteBtn);
row.appendChild(iconColumn);
row.appendChild(siteColumn);
row.appendChild(cssColumn);
row.appendChild(heightColumn);
row.appendChild(editColumn);
row.appendChild(deleteColumn);
styleListBody.appendChild(row);
});
if (metadataPatched) {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
}
}
renderDomainStyles();
// 新建
const addStyleBtn = document.createElement('button');
addStyleBtn.textContent = '+ 新建';
addStyleBtn.style.cssText = `
background-color: var(--add-color, #fd7e14);
color: #fff;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
margin-bottom: 12px;
`;
addStyleBtn.addEventListener('click', () => {
showEditDomainStyleDialog(); // 新建
});
dialog.appendChild(addStyleBtn);
// 右上角关闭并保存
const closeSaveBtn = document.createElement('button');
closeSaveBtn.textContent = '💾 关闭并保存';
closeSaveBtn.style.cssText = `
background-color: var(--success-color, #22c55e);
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
`;
closeSaveBtn.addEventListener('click', () => {
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
// 关闭前应用一次,确保当前页面即时生效
try { applyDomainStyles(); } catch (_) {}
closeExistingOverlay(overlay);
currentStyleOverlay = null;
});
dialog.style.position = 'relative';
dialog.appendChild(closeSaveBtn);
}
/**
* 新建/编辑域名样式对话框
* @param {number} index - 可选,若存在则为编辑,否则新建
*/
let currentAddDomainOverlay = null; // 保持原有声明
function showEditDomainStyleDialog(index) {
if (currentAddDomainOverlay) {
closeExistingOverlay(currentAddDomainOverlay);
}
const isEdit = typeof index === 'number';
const styleItem = isEdit
? { ...buttonConfig.domainStyleSettings[index] }
: {
domain: window.location.hostname,
name: document.title || '新样式',
height: 40,
bottomSpacing: buttonConfig.buttonBarBottomSpacing,
cssCode: '',
favicon: generateDomainFavicon(window.location.hostname)
};
const presetStyleDomain = styleItem.domain || '';
if (!styleItem.favicon) {
styleItem.favicon = generateDomainFavicon(presetStyleDomain);
}
if (typeof styleItem.bottomSpacing !== 'number') {
styleItem.bottomSpacing = buttonConfig.buttonBarBottomSpacing;
}
const { overlay, dialog } = createUnifiedDialog({
title: isEdit ? '✏️ 编辑自定义样式' : '🆕 新建自定义样式',
width: '480px',
onClose: () => {
currentAddDomainOverlay = null;
},
closeOnOverlayClick: false
});
currentAddDomainOverlay = overlay;
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
container.style.marginBottom = '16px';
container.style.padding = '16px';
container.style.borderRadius = '6px';
container.style.border = '1px solid var(--border-color, #e5e7eb)';
container.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
const tabsHeader = document.createElement('div');
tabsHeader.style.display = 'flex';
tabsHeader.style.gap = '8px';
tabsHeader.style.flexWrap = 'wrap';
const tabConfig = [
{ id: 'basic', label: '基础信息' },
{ id: 'layout', label: '布局设置' },
{ id: 'css', label: '自定义 CSS' }
];
const tabButtons = [];
const tabPanels = new Map();
const tabsBody = document.createElement('div');
tabsBody.style.position = 'relative';
tabConfig.forEach(({ id, label }) => {
const button = document.createElement('button');
button.type = 'button';
button.dataset.tabId = id;
button.textContent = label;
button.style.padding = '8px 14px';
button.style.borderRadius = '20px';
button.style.border = '1px solid var(--border-color, #d1d5db)';
button.style.backgroundColor = 'transparent';
button.style.color = 'var(--muted-text-color, #6b7280)';
button.style.cursor = 'pointer';
button.style.fontSize = '13px';
button.style.fontWeight = '500';
button.addEventListener('click', () => setActiveTab(id));
tabButtons.push(button);
tabsHeader.appendChild(button);
const panel = document.createElement('div');
panel.dataset.tabId = id;
panel.style.display = 'none';
panel.style.flexDirection = 'column';
panel.style.gap = '12px';
tabPanels.set(id, panel);
tabsBody.appendChild(panel);
});
container.appendChild(tabsHeader);
container.appendChild(tabsBody);
function setActiveTab(targetId) {
tabButtons.forEach((button) => {
const isActive = button.dataset.tabId === targetId;
button.style.backgroundColor = isActive ? 'var(--dialog-bg, #ffffff)' : 'transparent';
button.style.color = isActive ? 'var(--text-color, #1f2937)' : 'var(--muted-text-color, #6b7280)';
button.style.fontWeight = isActive ? '600' : '500';
button.style.boxShadow = isActive ? '0 2px 6px rgba(15, 23, 42, 0.08)' : 'none';
});
tabPanels.forEach((panel, panelId) => {
panel.style.display = panelId === targetId ? 'flex' : 'none';
});
}
setActiveTab('basic');
const nameLabel = document.createElement('label');
nameLabel.textContent = '备注名称:';
nameLabel.style.display = 'flex';
nameLabel.style.flexDirection = 'column';
nameLabel.style.gap = '6px';
nameLabel.style.fontSize = '13px';
nameLabel.style.fontWeight = '600';
nameLabel.style.color = 'var(--text-color, #1f2937)';
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = styleItem.name || '';
nameInput.style.width = '100%';
nameInput.style.height = '40px';
nameInput.style.padding = '0 12px';
nameInput.style.border = '1px solid var(--border-color, #d1d5db)';
nameInput.style.borderRadius = '6px';
nameInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
nameInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
nameInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
nameInput.style.outline = 'none';
nameInput.style.fontSize = '14px';
nameLabel.appendChild(nameInput);
tabPanels.get('basic').appendChild(nameLabel);
const domainLabel = document.createElement('label');
domainLabel.textContent = '网址:';
domainLabel.style.display = 'flex';
domainLabel.style.flexDirection = 'column';
domainLabel.style.gap = '6px';
domainLabel.style.fontSize = '13px';
domainLabel.style.fontWeight = '600';
domainLabel.style.color = 'var(--text-color, #1f2937)';
const domainInput = document.createElement('input');
domainInput.type = 'text';
domainInput.value = styleItem.domain || '';
domainInput.style.width = '100%';
domainInput.style.height = '40px';
domainInput.style.padding = '0 12px';
domainInput.style.border = '1px solid var(--border-color, #d1d5db)';
domainInput.style.borderRadius = '6px';
domainInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
domainInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
domainInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
domainInput.style.outline = 'none';
domainInput.style.fontSize = '14px';
domainLabel.appendChild(domainInput);
tabPanels.get('basic').appendChild(domainLabel);
const faviconLabel2 = document.createElement('label');
faviconLabel2.textContent = '站点图标:';
faviconLabel2.style.display = 'flex';
faviconLabel2.style.flexDirection = 'column';
faviconLabel2.style.gap = '6px';
faviconLabel2.style.fontSize = '13px';
faviconLabel2.style.fontWeight = '600';
faviconLabel2.style.color = 'var(--text-color, #1f2937)';
const faviconFieldWrapper2 = document.createElement('div');
faviconFieldWrapper2.style.display = 'flex';
faviconFieldWrapper2.style.alignItems = 'flex-start';
faviconFieldWrapper2.style.gap = '12px';
const faviconPreviewHolder2 = document.createElement('div');
faviconPreviewHolder2.style.width = '40px';
faviconPreviewHolder2.style.height = '40px';
faviconPreviewHolder2.style.borderRadius = '10px';
faviconPreviewHolder2.style.display = 'flex';
faviconPreviewHolder2.style.alignItems = 'center';
faviconPreviewHolder2.style.justifyContent = 'center';
faviconPreviewHolder2.style.backgroundColor = 'transparent';
faviconPreviewHolder2.style.flexShrink = '0';
const faviconControls2 = document.createElement('div');
faviconControls2.style.display = 'flex';
faviconControls2.style.flexDirection = 'column';
faviconControls2.style.gap = '8px';
faviconControls2.style.flex = '1';
const faviconInput2 = document.createElement('textarea');
faviconInput2.rows = 1;
faviconInput2.style.flex = '1 1 auto';
faviconInput2.style.padding = '10px 12px';
faviconInput2.style.border = '1px solid var(--border-color, #d1d5db)';
faviconInput2.style.borderRadius = '6px';
faviconInput2.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
faviconInput2.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
faviconInput2.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
faviconInput2.style.outline = 'none';
faviconInput2.style.fontSize = '14px';
faviconInput2.style.lineHeight = '1.5';
faviconInput2.style.resize = 'vertical';
faviconInput2.style.overflowY = 'hidden';
faviconInput2.placeholder = '可填写自定义图标地址';
faviconInput2.value = styleItem.favicon || '';
const resizeFaviconTextarea2 = () => autoResizeTextarea(faviconInput2, { minRows: 1, maxRows: 4 });
const faviconActionsRow2 = document.createElement('div');
faviconActionsRow2.style.display = 'flex';
faviconActionsRow2.style.alignItems = 'center';
faviconActionsRow2.style.gap = '8px';
faviconActionsRow2.style.flexWrap = 'nowrap';
faviconActionsRow2.style.fontSize = '12px';
faviconActionsRow2.style.color = 'var(--muted-text-color, #6b7280)';
faviconActionsRow2.style.justifyContent = 'flex-start';
const faviconHelp2 = document.createElement('span');
faviconHelp2.textContent = '留空时系统将使用该网址的默认 Favicon。';
faviconHelp2.style.flex = '1';
faviconHelp2.style.minWidth = '0';
faviconHelp2.style.marginRight = '12px';
const autoFaviconBtn2 = document.createElement('button');
autoFaviconBtn2.type = 'button';
autoFaviconBtn2.setAttribute('aria-label', '自动获取站点图标');
autoFaviconBtn2.title = '自动获取站点图标';
autoFaviconBtn2.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
autoFaviconBtn2.style.color = '#fff';
autoFaviconBtn2.style.border = '1px solid var(--border-color, #d1d5db)';
autoFaviconBtn2.style.borderRadius = '50%';
autoFaviconBtn2.style.width = '32px';
autoFaviconBtn2.style.height = '32px';
autoFaviconBtn2.style.display = 'flex';
autoFaviconBtn2.style.alignItems = 'center';
autoFaviconBtn2.style.justifyContent = 'center';
autoFaviconBtn2.style.cursor = 'pointer';
autoFaviconBtn2.style.boxShadow = '0 2px 6px rgba(59, 130, 246, 0.16)';
autoFaviconBtn2.style.flexShrink = '0';
autoFaviconBtn2.style.padding = '0';
const autoFaviconIcon2 = createAutoFaviconIcon();
autoFaviconBtn2.appendChild(autoFaviconIcon2);
faviconActionsRow2.appendChild(faviconHelp2);
faviconActionsRow2.appendChild(autoFaviconBtn2);
faviconControls2.appendChild(faviconInput2);
faviconControls2.appendChild(faviconActionsRow2);
faviconFieldWrapper2.appendChild(faviconPreviewHolder2);
faviconFieldWrapper2.appendChild(faviconControls2);
faviconLabel2.appendChild(faviconFieldWrapper2);
tabPanels.get('basic').appendChild(faviconLabel2);
let faviconManuallyEdited2 = false;
const updateStyleFaviconPreview = () => {
const imgUrl = faviconInput2.value.trim() || generateDomainFavicon(domainInput.value.trim());
setTrustedHTML(faviconPreviewHolder2, '');
faviconPreviewHolder2.appendChild(
createFaviconElement(
imgUrl,
nameInput.value.trim() || domainInput.value.trim() || '样式',
'🎨',
{ withBackground: false }
)
);
};
updateStyleFaviconPreview();
resizeFaviconTextarea2();
requestAnimationFrame(resizeFaviconTextarea2);
const getStyleFallbackFavicon = () => generateDomainFavicon(domainInput.value.trim());
autoFaviconBtn2.addEventListener('click', () => {
const autoUrl = getStyleFallbackFavicon();
faviconInput2.value = autoUrl;
faviconManuallyEdited2 = false;
updateStyleFaviconPreview();
resizeFaviconTextarea2();
});
domainInput.addEventListener('input', () => {
if (!faviconManuallyEdited2) {
faviconInput2.value = getStyleFallbackFavicon();
}
updateStyleFaviconPreview();
resizeFaviconTextarea2();
});
faviconInput2.addEventListener('input', () => {
faviconManuallyEdited2 = true;
updateStyleFaviconPreview();
resizeFaviconTextarea2();
});
nameInput.addEventListener('input', updateStyleFaviconPreview);
const heightLabel = document.createElement('label');
heightLabel.textContent = '按钮栏高度 (px):';
heightLabel.style.display = 'flex';
heightLabel.style.flexDirection = 'column';
heightLabel.style.gap = '6px';
heightLabel.style.fontSize = '13px';
heightLabel.style.fontWeight = '600';
heightLabel.style.color = 'var(--text-color, #1f2937)';
const heightInput = document.createElement('input');
heightInput.type = 'number';
heightInput.min = '20';
heightInput.max = '200';
heightInput.step = '1';
heightInput.value = styleItem.height;
heightInput.style.width = '100%';
heightInput.style.height = '40px';
heightInput.style.padding = '0 12px';
heightInput.style.border = '1px solid var(--border-color, #d1d5db)';
heightInput.style.borderRadius = '6px';
heightInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
heightInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
heightInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
heightInput.style.outline = 'none';
heightInput.style.fontSize = '14px';
heightLabel.appendChild(heightInput);
tabPanels.get('layout').appendChild(heightLabel);
const bottomSpacingLabel = document.createElement('label');
bottomSpacingLabel.textContent = '按钮距页面底部间距 (px):';
bottomSpacingLabel.style.display = 'flex';
bottomSpacingLabel.style.flexDirection = 'column';
bottomSpacingLabel.style.gap = '6px';
bottomSpacingLabel.style.fontSize = '13px';
bottomSpacingLabel.style.fontWeight = '600';
bottomSpacingLabel.style.color = 'var(--text-color, #1f2937)';
const bottomSpacingInput = document.createElement('input');
bottomSpacingInput.type = 'number';
bottomSpacingInput.min = '-200';
bottomSpacingInput.max = '200';
bottomSpacingInput.step = '1';
bottomSpacingInput.value = styleItem.bottomSpacing ?? buttonConfig.buttonBarBottomSpacing ?? 0;
bottomSpacingInput.style.width = '100%';
bottomSpacingInput.style.height = '40px';
bottomSpacingInput.style.padding = '0 12px';
bottomSpacingInput.style.border = '1px solid var(--border-color, #d1d5db)';
bottomSpacingInput.style.borderRadius = '6px';
bottomSpacingInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
bottomSpacingInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
bottomSpacingInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
bottomSpacingInput.style.outline = 'none';
bottomSpacingInput.style.fontSize = '14px';
bottomSpacingLabel.appendChild(bottomSpacingInput);
tabPanels.get('layout').appendChild(bottomSpacingLabel);
const cssLabel = document.createElement('label');
cssLabel.textContent = '自定义 CSS:';
cssLabel.style.display = 'flex';
cssLabel.style.flexDirection = 'column';
cssLabel.style.gap = '6px';
cssLabel.style.fontSize = '13px';
cssLabel.style.fontWeight = '600';
cssLabel.style.color = 'var(--text-color, #1f2937)';
const cssTextarea = document.createElement('textarea');
cssTextarea.value = styleItem.cssCode || '';
cssTextarea.style.width = '100%';
cssTextarea.style.minHeight = '120px';
cssTextarea.style.padding = '12px';
cssTextarea.style.border = '1px solid var(--border-color, #d1d5db)';
cssTextarea.style.borderRadius = '6px';
cssTextarea.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
cssTextarea.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
cssTextarea.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
cssTextarea.style.outline = 'none';
cssTextarea.style.resize = 'vertical';
cssTextarea.style.fontSize = '13px';
cssTextarea.style.lineHeight = '1.5';
cssLabel.appendChild(cssTextarea);
tabPanels.get('css').appendChild(cssLabel);
dialog.appendChild(container);
const footer2 = document.createElement('div');
footer2.style.display = 'flex';
footer2.style.justifyContent = 'space-between';
footer2.style.alignItems = 'center';
footer2.style.gap = '12px';
footer2.style.marginTop = '20px';
footer2.style.paddingTop = '20px';
footer2.style.borderTop = '1px solid var(--border-color, #e5e7eb)';
const cancelBtn2 = document.createElement('button');
cancelBtn2.textContent = '取消';
cancelBtn2.style.backgroundColor = 'var(--cancel-color, #6B7280)';
cancelBtn2.style.color = '#fff';
cancelBtn2.style.border = 'none';
cancelBtn2.style.borderRadius = '4px';
cancelBtn2.style.padding = '8px 16px';
cancelBtn2.style.fontSize = '14px';
cancelBtn2.style.cursor = 'pointer';
cancelBtn2.addEventListener('click', () => {
closeExistingOverlay(overlay);
currentAddDomainOverlay = null;
});
footer2.appendChild(cancelBtn2);
const saveBtn2 = document.createElement('button');
saveBtn2.textContent = isEdit ? '保存' : '创建';
saveBtn2.style.backgroundColor = 'var(--success-color,#22c55e)';
saveBtn2.style.color = '#fff';
saveBtn2.style.border = 'none';
saveBtn2.style.borderRadius = '4px';
saveBtn2.style.padding = '8px 16px';
saveBtn2.style.fontSize = '14px';
saveBtn2.style.cursor = 'pointer';
saveBtn2.addEventListener('click', () => {
const sanitizedDomain = domainInput.value.trim();
const updatedItem = {
domain: sanitizedDomain,
name: nameInput.value.trim() || '未命名样式',
height: parseInt(heightInput.value, 10) || 40,
bottomSpacing: (() => {
const parsed = Number(bottomSpacingInput.value);
if (Number.isFinite(parsed)) {
return Math.max(-200, Math.min(200, parsed));
}
return buttonConfig.buttonBarBottomSpacing;
})(),
cssCode: cssTextarea.value,
favicon: faviconInput2.value.trim() || generateDomainFavicon(sanitizedDomain)
};
if (isEdit) {
buttonConfig.domainStyleSettings[index] = updatedItem;
} else {
buttonConfig.domainStyleSettings.push(updatedItem);
}
localStorage.setItem('chatGPTButtonFoldersConfig', JSON.stringify(buttonConfig));
// 保存后立即生效
try { applyDomainStyles(); } catch (_) {}
closeExistingOverlay(overlay);
currentAddDomainOverlay = null;
showStyleSettingsDialog(); // 刷新列表
});
footer2.appendChild(saveBtn2);
dialog.appendChild(footer2);
}
// =============== [新增] showDomainRuleEditorDialog 统一新建/编辑弹窗 ===============
function showDomainRuleEditorDialog(ruleData, onSave) {
// ruleData 若为空对象,则视为新建,否则编辑
// 统一使用 createUnifiedDialog
const isEdit = !!ruleData && ruleData.domain;
const presetDomain = isEdit ? (ruleData.domain || '') : (window.location.hostname || '');
const presetFavicon = (isEdit && ruleData.favicon) ? ruleData.favicon : generateDomainFavicon(presetDomain);
const { overlay, dialog } = createUnifiedDialog({
title: isEdit ? '✏️ 编辑自动化规则' : '🆕 新建新网址规则',
width: '480px',
onClose: () => {
// 关闭时的回调可写在此
},
closeOnOverlayClick: false
});
function createAutoSubmitMethodConfigUI(initialMethod = 'Enter', initialAdvanced = null) {
const methodSection = document.createElement('div');
methodSection.style.display = 'flex';
methodSection.style.flexDirection = 'column';
methodSection.style.gap = '8px';
const titleRow = document.createElement('div');
titleRow.style.display = 'flex';
titleRow.style.alignItems = 'center';
titleRow.style.justifyContent = 'space-between';
const methodTitle = document.createElement('div');
methodTitle.textContent = '自动提交方式:';
methodTitle.style.fontSize = '13px';
methodTitle.style.fontWeight = '600';
methodTitle.style.color = 'var(--text-color, #1f2937)';
titleRow.appendChild(methodTitle);
const expandButton = document.createElement('button');
expandButton.type = 'button';
expandButton.title = '展开/折叠高级选项';
expandButton.textContent = '▼';
expandButton.style.width = '28px';
expandButton.style.height = '28px';
expandButton.style.padding = '0';
expandButton.style.display = 'flex';
expandButton.style.alignItems = 'center';
expandButton.style.justifyContent = 'center';
expandButton.style.border = '1px solid transparent';
expandButton.style.borderRadius = '4px';
expandButton.style.background = 'transparent';
expandButton.style.cursor = 'pointer';
expandButton.style.transition = 'background-color 0.2s ease, border-color 0.2s ease';
expandButton.addEventListener('mouseenter', () => {
expandButton.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
expandButton.style.borderColor = 'var(--border-color, #d1d5db)';
});
expandButton.addEventListener('mouseleave', () => {
expandButton.style.backgroundColor = 'transparent';
expandButton.style.borderColor = 'transparent';
});
titleRow.appendChild(expandButton);
methodSection.appendChild(titleRow);
const methodOptionsWrapper = document.createElement('div');
methodOptionsWrapper.style.display = 'flex';
methodOptionsWrapper.style.flexWrap = 'wrap';
methodOptionsWrapper.style.gap = '15px';
methodSection.appendChild(methodOptionsWrapper);
const advancedContainer = document.createElement('div');
advancedContainer.style.display = 'none';
advancedContainer.style.flexDirection = 'column';
advancedContainer.style.gap = '10px';
advancedContainer.style.marginTop = '8px';
advancedContainer.style.padding = '12px';
advancedContainer.style.borderRadius = '6px';
advancedContainer.style.border = '1px solid var(--border-color, #d1d5db)';
advancedContainer.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
advancedContainer.style.boxShadow = 'inset 0 1px 2px rgba(15, 23, 42, 0.04)';
advancedContainer.style.transition = 'opacity 0.2s ease';
advancedContainer.style.opacity = '0';
methodSection.appendChild(advancedContainer);
const methodOptions = [
{ value: 'Enter', text: 'Enter' },
{ value: 'Cmd+Enter', text: 'Cmd+Enter' },
{ value: '模拟点击提交按钮', text: '模拟点击提交按钮' }
];
const methodRadioName = `autoSubmitMethod_${Math.random().toString(36).slice(2, 8)}`;
const uniqueSuffix = Math.random().toString(36).slice(2, 8);
const getDefaultAdvancedForMethod = (method) => {
if (method === 'Cmd+Enter') {
return { variant: 'cmd' };
}
if (method === '模拟点击提交按钮') {
return { variant: 'default', selector: '' };
}
return null;
};
const normalizeAdvancedForMethod = (method, advanced) => {
const defaults = getDefaultAdvancedForMethod(method);
if (!defaults) return null;
const normalized = { ...defaults };
if (advanced && typeof advanced === 'object') {
if (method === 'Cmd+Enter') {
if (advanced.variant && ['cmd', 'ctrl'].includes(advanced.variant)) {
normalized.variant = advanced.variant;
}
} else if (method === '模拟点击提交按钮') {
if (advanced.variant && ['default', 'selector'].includes(advanced.variant)) {
normalized.variant = advanced.variant;
}
if (advanced.selector && typeof advanced.selector === 'string') {
normalized.selector = advanced.selector;
}
}
}
if (method === '模拟点击提交按钮' && normalized.variant !== 'selector') {
normalized.selector = '';
}
return normalized;
};
let selectedMethod = initialMethod || methodOptions[0].value;
if (!methodOptions.some(option => option.value === selectedMethod)) {
methodOptions.push({ value: selectedMethod, text: selectedMethod });
}
let advancedState = normalizeAdvancedForMethod(selectedMethod, initialAdvanced);
const shouldExpandInitially = () => {
if (!advancedState) return false;
if (selectedMethod === 'Cmd+Enter') {
return advancedState.variant === 'ctrl';
}
if (selectedMethod === '模拟点击提交按钮') {
return advancedState.variant === 'selector' && advancedState.selector;
}
return false;
};
let isExpanded = shouldExpandInitially();
const renderAdvancedContent = () => {
setTrustedHTML(advancedContainer, '');
if (!isExpanded) {
advancedContainer.style.display = 'none';
advancedContainer.style.opacity = '0';
return;
}
advancedContainer.style.display = 'flex';
advancedContainer.style.opacity = '1';
const advancedTitle = document.createElement('div');
advancedTitle.textContent = '高级选项:';
advancedTitle.style.fontSize = '12px';
advancedTitle.style.fontWeight = '600';
advancedTitle.style.opacity = '0.75';
advancedContainer.appendChild(advancedTitle);
if (selectedMethod === 'Enter') {
const tip = document.createElement('div');
tip.textContent = 'Enter 提交方式没有额外配置。';
tip.style.fontSize = '12px';
tip.style.color = 'var(--muted-text-color, #6b7280)';
advancedContainer.appendChild(tip);
return;
}
if (selectedMethod === 'Cmd+Enter') {
const variants = [
{ value: 'cmd', label: 'Cmd + Enter', desc: '使用 macOS / Meta 键组合模拟提交' },
{ value: 'ctrl', label: 'Ctrl + Enter', desc: '使用 Windows / Linux 控制键组合模拟提交' }
];
const variantGroup = document.createElement('div');
variantGroup.style.display = 'flex';
variantGroup.style.flexDirection = 'column';
variantGroup.style.gap = '8px';
const variantRadioName = `autoSubmitCmdVariant_${uniqueSuffix}`;
variants.forEach(variant => {
const label = document.createElement('label');
label.style.display = 'flex';
label.style.alignItems = 'flex-start';
label.style.gap = '8px';
label.style.cursor = 'pointer';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = variantRadioName;
radio.value = variant.value;
radio.checked = advancedState?.variant === variant.value;
radio.style.marginTop = '2px';
radio.style.cursor = 'pointer';
radio.addEventListener('change', () => {
if (radio.checked) {
advancedState = { variant: variant.value };
}
});
const textContainer = document.createElement('div');
textContainer.style.display = 'flex';
textContainer.style.flexDirection = 'column';
textContainer.style.gap = '2px';
const labelText = document.createElement('span');
labelText.textContent = variant.label;
labelText.style.fontSize = '13px';
labelText.style.fontWeight = '600';
const descText = document.createElement('span');
descText.textContent = variant.desc;
descText.style.fontSize = '12px';
descText.style.opacity = '0.75';
textContainer.appendChild(labelText);
textContainer.appendChild(descText);
label.appendChild(radio);
label.appendChild(textContainer);
variantGroup.appendChild(label);
});
advancedContainer.appendChild(variantGroup);
return;
}
if (selectedMethod === '模拟点击提交按钮') {
const variants = [
{ value: 'default', label: '默认方法', desc: '自动匹配常见的提交按钮进行点击。' },
{ value: 'selector', label: '自定义 CSS 选择器', desc: '使用自定义选择器定位需要点击的提交按钮。' }
];
const variantGroup = document.createElement('div');
variantGroup.style.display = 'flex';
variantGroup.style.flexDirection = 'column';
variantGroup.style.gap = '8px';
const variantRadioName = `autoSubmitClickVariant_${uniqueSuffix}`;
variants.forEach(variant => {
const label = document.createElement('label');
label.style.display = 'flex';
label.style.alignItems = 'flex-start';
label.style.gap = '8px';
label.style.cursor = 'pointer';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = variantRadioName;
radio.value = variant.value;
radio.checked = advancedState?.variant === variant.value;
radio.style.marginTop = '2px';
radio.style.cursor = 'pointer';
radio.addEventListener('change', () => {
if (radio.checked) {
advancedState = normalizeAdvancedForMethod(selectedMethod, { variant: variant.value, selector: advancedState?.selector || '' });
renderAdvancedContent();
}
});
const textContainer = document.createElement('div');
textContainer.style.display = 'flex';
textContainer.style.flexDirection = 'column';
textContainer.style.gap = '2px';
const labelText = document.createElement('span');
labelText.textContent = variant.label;
labelText.style.fontSize = '13px';
labelText.style.fontWeight = '600';
const descText = document.createElement('span');
descText.textContent = variant.desc;
descText.style.fontSize = '12px';
descText.style.opacity = '0.75';
textContainer.appendChild(labelText);
textContainer.appendChild(descText);
label.appendChild(radio);
label.appendChild(textContainer);
variantGroup.appendChild(label);
});
advancedContainer.appendChild(variantGroup);
if (advancedState?.variant === 'selector') {
const selectorInput = document.createElement('input');
selectorInput.type = 'text';
selectorInput.placeholder = '如:button.send-btn 或 form button[type="submit"]';
selectorInput.value = advancedState.selector || '';
selectorInput.style.width = '100%';
selectorInput.style.height = '40px';
selectorInput.style.padding = '0 12px';
selectorInput.style.border = '1px solid var(--border-color, #d1d5db)';
selectorInput.style.borderRadius = '6px';
selectorInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
selectorInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
selectorInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
selectorInput.style.outline = 'none';
selectorInput.style.fontSize = '14px';
selectorInput.addEventListener('input', () => {
advancedState = normalizeAdvancedForMethod(selectedMethod, {
variant: 'selector',
selector: selectorInput.value
});
});
advancedContainer.appendChild(selectorInput);
const hint = document.createElement('div');
hint.textContent = '请输入能唯一定位提交按钮的 CSS 选择器。';
hint.style.fontSize = '12px';
hint.style.color = 'var(--muted-text-color, #6b7280)';
advancedContainer.appendChild(hint);
}
return;
}
const tip = document.createElement('div');
tip.textContent = '当前提交方式没有可配置的高级选项。';
tip.style.fontSize = '12px';
tip.style.color = 'var(--muted-text-color, #6b7280)';
advancedContainer.appendChild(tip);
};
methodOptions.forEach(option => {
const radioLabel = document.createElement('label');
radioLabel.style.display = 'inline-flex';
radioLabel.style.alignItems = 'center';
radioLabel.style.cursor = 'pointer';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = methodRadioName;
radio.value = option.value;
radio.checked = selectedMethod === option.value;
radio.style.marginRight = '6px';
radio.style.cursor = 'pointer';
radio.addEventListener('change', () => {
if (radio.checked) {
selectedMethod = option.value;
advancedState = normalizeAdvancedForMethod(selectedMethod, null);
renderAdvancedContent();
}
});
radioLabel.appendChild(radio);
radioLabel.appendChild(document.createTextNode(option.text));
methodOptionsWrapper.appendChild(radioLabel);
});
expandButton.addEventListener('click', () => {
isExpanded = !isExpanded;
expandButton.textContent = isExpanded ? '▲' : '▼';
expandButton.setAttribute('aria-expanded', String(isExpanded));
renderAdvancedContent();
});
expandButton.setAttribute('aria-expanded', String(isExpanded));
expandButton.textContent = isExpanded ? '▲' : '▼';
renderAdvancedContent();
return {
container: methodSection,
getConfig: () => {
const normalized = normalizeAdvancedForMethod(selectedMethod, advancedState);
let advancedForSave = null;
if (selectedMethod === 'Cmd+Enter' && normalized && normalized.variant && normalized.variant !== 'cmd') {
advancedForSave = { variant: normalized.variant };
} else if (selectedMethod === '模拟点击提交按钮' && normalized) {
if (normalized.variant === 'selector') {
advancedForSave = {
variant: 'selector',
selector: typeof normalized.selector === 'string' ? normalized.selector : ''
};
} else if (normalized.variant !== 'default') {
advancedForSave = { variant: normalized.variant };
}
}
return {
method: selectedMethod,
advanced: advancedForSave
};
}
};
}
// 创建表单容器
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
container.style.marginBottom = '16px';
container.style.padding = '16px';
container.style.borderRadius = '6px';
container.style.border = '1px solid var(--border-color, #e5e7eb)';
container.style.backgroundColor = 'var(--button-bg, #f3f4f6)';
// 网址
const domainLabel = document.createElement('label');
domainLabel.textContent = '网址:';
domainLabel.style.display = 'flex';
domainLabel.style.flexDirection = 'column';
domainLabel.style.gap = '6px';
domainLabel.style.fontSize = '13px';
domainLabel.style.fontWeight = '600';
domainLabel.style.color = 'var(--text-color, #1f2937)';
const domainInput = document.createElement('input');
domainInput.type = 'text';
domainInput.style.width = '100%';
domainInput.style.height = '40px';
domainInput.style.padding = '0 12px';
domainInput.style.border = '1px solid var(--border-color, #d1d5db)';
domainInput.style.borderRadius = '6px';
domainInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
domainInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
domainInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
domainInput.style.outline = 'none';
domainInput.style.fontSize = '14px';
domainInput.value = presetDomain;
domainLabel.appendChild(domainInput);
container.appendChild(domainLabel);
let nameInputRef = null;
// 备注名称
const nameLabel = document.createElement('label');
nameLabel.textContent = '备注名称:';
nameLabel.style.display = 'flex';
nameLabel.style.flexDirection = 'column';
nameLabel.style.gap = '6px';
nameLabel.style.fontSize = '13px';
nameLabel.style.fontWeight = '600';
nameLabel.style.color = 'var(--text-color, #1f2937)';
nameInputRef = document.createElement('input');
nameInputRef.type = 'text';
nameInputRef.style.width = '100%';
nameInputRef.style.height = '40px';
nameInputRef.style.padding = '0 12px';
nameInputRef.style.border = '1px solid var(--border-color, #d1d5db)';
nameInputRef.style.borderRadius = '6px';
nameInputRef.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
nameInputRef.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
nameInputRef.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
nameInputRef.style.outline = 'none';
nameInputRef.style.fontSize = '14px';
nameInputRef.value = isEdit ? (ruleData.name || '') : (document.title || '新网址规则');
nameLabel.appendChild(nameInputRef);
container.appendChild(nameLabel);
// favicon
const faviconLabel = document.createElement('label');
faviconLabel.textContent = '站点图标:';
faviconLabel.style.display = 'flex';
faviconLabel.style.flexDirection = 'column';
faviconLabel.style.gap = '6px';
faviconLabel.style.fontSize = '13px';
faviconLabel.style.fontWeight = '600';
faviconLabel.style.color = 'var(--text-color, #1f2937)';
const faviconFieldWrapper = document.createElement('div');
faviconFieldWrapper.style.display = 'flex';
faviconFieldWrapper.style.alignItems = 'flex-start';
faviconFieldWrapper.style.gap = '12px';
const faviconPreviewHolder = document.createElement('div');
faviconPreviewHolder.style.width = '40px';
faviconPreviewHolder.style.height = '40px';
faviconPreviewHolder.style.borderRadius = '10px';
faviconPreviewHolder.style.display = 'flex';
faviconPreviewHolder.style.alignItems = 'center';
faviconPreviewHolder.style.justifyContent = 'center';
faviconPreviewHolder.style.backgroundColor = 'transparent';
faviconPreviewHolder.style.flexShrink = '0';
const faviconControls = document.createElement('div');
faviconControls.style.display = 'flex';
faviconControls.style.flexDirection = 'column';
faviconControls.style.gap = '8px';
faviconControls.style.flex = '1';
const faviconInput = document.createElement('textarea');
faviconInput.rows = 1;
faviconInput.style.flex = '1 1 auto';
faviconInput.style.padding = '10px 12px';
faviconInput.style.border = '1px solid var(--border-color, #d1d5db)';
faviconInput.style.borderRadius = '6px';
faviconInput.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
faviconInput.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.03)';
faviconInput.style.transition = 'border-color 0.2s ease, box-shadow 0.2s ease';
faviconInput.style.outline = 'none';
faviconInput.style.fontSize = '14px';
faviconInput.style.lineHeight = '1.5';
faviconInput.style.resize = 'vertical';
faviconInput.style.overflowY = 'hidden';
faviconInput.placeholder = 'https:// 或 data:image/svg+xml;base64...';
faviconInput.value = presetFavicon || '';
const resizeFaviconTextarea = () => autoResizeTextarea(faviconInput, { minRows: 1, maxRows: 4 });
const faviconActionsRow = document.createElement('div');
faviconActionsRow.style.display = 'flex';
faviconActionsRow.style.alignItems = 'center';
faviconActionsRow.style.gap = '8px';
faviconActionsRow.style.flexWrap = 'nowrap';
faviconActionsRow.style.fontSize = '12px';
faviconActionsRow.style.color = 'var(--muted-text-color, #6b7280)';
faviconActionsRow.style.justifyContent = 'flex-start';
const faviconHelp = document.createElement('span');
faviconHelp.textContent = '留空时将自动根据网址生成 Google Favicon。';
faviconHelp.style.flex = '1';
faviconHelp.style.minWidth = '0';
faviconHelp.style.marginRight = '12px';
const autoFaviconBtn = document.createElement('button');
autoFaviconBtn.type = 'button';
autoFaviconBtn.setAttribute('aria-label', '自动获取站点图标');
autoFaviconBtn.title = '自动获取站点图标';
autoFaviconBtn.style.backgroundColor = 'var(--dialog-bg, #ffffff)';
autoFaviconBtn.style.border = '1px solid var(--border-color, #d1d5db)';
autoFaviconBtn.style.borderRadius = '50%';
autoFaviconBtn.style.width = '32px';
autoFaviconBtn.style.height = '32px';
autoFaviconBtn.style.display = 'flex';
autoFaviconBtn.style.alignItems = 'center';
autoFaviconBtn.style.justifyContent = 'center';
autoFaviconBtn.style.cursor = 'pointer';
autoFaviconBtn.style.boxShadow = '0 2px 6px rgba(59, 130, 246, 0.16)';
autoFaviconBtn.style.flexShrink = '0';
autoFaviconBtn.style.padding = '0';
const autoFaviconIcon = createAutoFaviconIcon();
autoFaviconBtn.appendChild(autoFaviconIcon);
faviconActionsRow.appendChild(faviconHelp);
faviconActionsRow.appendChild(autoFaviconBtn);
faviconControls.appendChild(faviconInput);
faviconControls.appendChild(faviconActionsRow);
faviconFieldWrapper.appendChild(faviconPreviewHolder);
faviconFieldWrapper.appendChild(faviconControls);
faviconLabel.appendChild(faviconFieldWrapper);
container.appendChild(faviconLabel);
let faviconManuallyEdited = false;
const updateFaviconPreview = () => {
const currentFavicon = faviconInput.value.trim();
setTrustedHTML(faviconPreviewHolder, '');
faviconPreviewHolder.appendChild(
createFaviconElement(
currentFavicon || generateDomainFavicon(domainInput.value.trim()),
(nameInputRef ? nameInputRef.value.trim() : '') || domainInput.value.trim() || '自动化',
'⚡',
{ withBackground: false }
)
);
};
const getFallbackFavicon = () => generateDomainFavicon(domainInput.value.trim());
autoFaviconBtn.addEventListener('click', () => {
const autoUrl = getFallbackFavicon();
faviconInput.value = autoUrl;
faviconManuallyEdited = false;
updateFaviconPreview();
resizeFaviconTextarea();
});
domainInput.addEventListener('input', () => {
if (!faviconManuallyEdited) {
faviconInput.value = getFallbackFavicon();
}
updateFaviconPreview();
resizeFaviconTextarea();
});
faviconInput.addEventListener('input', () => {
faviconManuallyEdited = true;
updateFaviconPreview();
resizeFaviconTextarea();
});
nameInputRef.addEventListener('input', updateFaviconPreview);
updateFaviconPreview();
resizeFaviconTextarea();
requestAnimationFrame(resizeFaviconTextarea);
const methodConfigUI = createAutoSubmitMethodConfigUI(
(isEdit && ruleData.method) ? ruleData.method : 'Enter',
isEdit ? ruleData.methodAdvanced : null
);
container.appendChild(methodConfigUI.container);
// 确认 & 取消 按钮
const btnRow = document.createElement('div');
btnRow.style.display = 'flex';
btnRow.style.justifyContent = 'space-between';
btnRow.style.alignItems = 'center';
btnRow.style.gap = '12px';
btnRow.style.marginTop = '20px';
btnRow.style.paddingTop = '20px';
btnRow.style.borderTop = '1px solid var(--border-color, #e5e7eb)';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.style.backgroundColor = 'var(--cancel-color,#6B7280)';
cancelBtn.style.color = '#fff';
cancelBtn.style.border = 'none';
cancelBtn.style.borderRadius = '4px';
cancelBtn.style.padding = '8px 16px';
cancelBtn.style.fontSize = '14px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.addEventListener('click', () => {
overlay.remove();
});
const confirmBtn = document.createElement('button');
confirmBtn.textContent = '确认';
confirmBtn.style.backgroundColor = 'var(--success-color,#22c55e)';
confirmBtn.style.color = '#fff';
confirmBtn.style.border = 'none';
confirmBtn.style.borderRadius = '4px';
confirmBtn.style.padding = '8px 16px';
confirmBtn.style.fontSize = '14px';
confirmBtn.style.cursor = 'pointer';
confirmBtn.addEventListener('click', () => {
const sanitizedDomain = domainInput.value.trim();
const sanitizedName = nameInputRef.value.trim();
const methodConfig = methodConfigUI.getConfig();
const methodAdvanced = methodConfig.advanced;
const newData = {
domain: sanitizedDomain,
name: sanitizedName,
method: methodConfig.method,
favicon: faviconInput.value.trim() || generateDomainFavicon(sanitizedDomain)
};
if(!newData.domain || !newData.name) {
alert('请输入网址和备注名称!');
return;
}
if (methodConfig.method === '模拟点击提交按钮' && methodAdvanced && methodAdvanced.variant === 'selector') {
const trimmedSelector = methodAdvanced.selector ? methodAdvanced.selector.trim() : '';
if (!trimmedSelector) {
alert('请输入有效的 CSS 选择器!');
return;
}
try {
document.querySelector(trimmedSelector);
} catch (err) {
alert('CSS 选择器语法错误,请检查后再试!');
return;
}
methodAdvanced.selector = trimmedSelector;
}
if (methodAdvanced) {
newData.methodAdvanced = methodAdvanced;
}
// 回调保存
if(onSave) onSave(newData);
// 关闭
overlay.remove();
});
btnRow.appendChild(cancelBtn);
btnRow.appendChild(confirmBtn);
// 组装
dialog.appendChild(container);
dialog.appendChild(btnRow);
}
function isValidDomainInput(str) {
// 简易:包含'.' 不含空格 即视为有效
if (str.includes(' ')) return false;
if (!str.includes('.')) return false;
return true;
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
setCSSVariables(getCurrentTheme());
updateStylesOnThemeChange();
console.log("🌓 主题模式已切换,样式已更新。");
});
const createButtonContainer = () => {
const root = getShadowRoot();
let existingContainer = root ? root.querySelector('.folder-buttons-container') : null;
if (existingContainer) {
// 使用updateButtonContainer来处理已存在的容器
updateButtonContainer();
return existingContainer;
}
// 创建新容器
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('folder-buttons-container');
buttonContainer.style.pointerEvents = 'auto';
// 设置固定定位和位置
buttonContainer.style.position = 'fixed';
buttonContainer.style.right = '0px';
buttonContainer.style.width = '100%';
buttonContainer.style.zIndex = '1000'; // 确保按钮容器始终显示在顶层
// 基本样式
buttonContainer.style.display = 'flex';
buttonContainer.style.flexWrap = 'nowrap'; // 改为不换行
buttonContainer.style.overflowX = 'auto'; // 横向滚动
buttonContainer.style.overflowY = 'hidden'; // 禁止纵向滚动
buttonContainer.style.gap = '10px';
buttonContainer.style.marginTop = '0px';
buttonContainer.style.height = buttonConfig.buttonBarHeight + 'px';
// 滚动条处理
buttonContainer.style.scrollbarWidth = 'none'; // for Firefox
buttonContainer.style.msOverflowStyle = 'none'; // for IE/Edge
buttonContainer.classList.add('hide-scrollbar'); // 用于自定义::-webkit-scrollbar
// 内容布局
buttonContainer.style.justifyContent = 'center';
buttonContainer.style.alignItems = 'center';
buttonContainer.style.padding = '6px 15px';
// 移除原有的背景色和阴影,设置为透明
buttonContainer.style.backgroundColor = 'transparent';
buttonContainer.style.boxShadow = 'none';
buttonContainer.style.borderRadius = '4px';
// 添加所有未隐藏的文件夹按钮
buttonConfig.folderOrder.forEach((name) => {
const config = buttonConfig.folders[name];
if (config && !config.hidden) { // 只显示未隐藏的文件夹
const folderButton = createFolderButton(name, config);
buttonContainer.appendChild(folderButton);
}
});
// 按顺序添加功能按钮
// 现在所有工具按钮都在 '🖱️' 文件夹内,不再直接添加
// 仅添加设置和清空按钮
buttonContainer.appendChild(createSettingsButton());
buttonContainer.appendChild(createClearButton());
// 初始记录 paddingY,确保偏移计算有默认值
buttonContainer.dataset.barPaddingY = '6';
applyBarBottomSpacing(
buttonContainer,
buttonConfig.buttonBarBottomSpacing,
buttonConfig.buttonBarBottomSpacing
);
return buttonContainer;
};
const updateButtonContainer = () => {
const root = getShadowRoot();
let existingContainer = root ? root.querySelector('.folder-buttons-container') : null;
if (existingContainer) {
// 保存所有功能按钮的引用
const settingsButton = existingContainer.querySelector('button:nth-last-child(2)');
const clearButton = existingContainer.querySelector('button:last-child');
// 清空容器
setTrustedHTML(existingContainer, '');
// 重新添加未隐藏的文件夹按钮
buttonConfig.folderOrder.forEach((name) => {
const config = buttonConfig.folders[name];
if (config && !config.hidden) { // 只显示未隐藏的文件夹
const folderButton = createFolderButton(name, config);
existingContainer.appendChild(folderButton);
}
});
// 按正确顺序重新添加功能按钮
if (settingsButton) existingContainer.appendChild(settingsButton);
if (clearButton) existingContainer.appendChild(clearButton);
console.log("✅ 按钮栏已更新(已过滤隐藏文件夹)。");
} else {
console.warn("⚠️ 未找到按钮容器,无法更新按钮栏。");
}
try {
applyDomainStyles();
} catch (err) {
console.warn('应用域名样式失败:', err);
}
};
const attachButtonsToTextarea = (textarea) => {
// 仅附加一次按钮容器
let buttonContainer = queryUI('.folder-buttons-container');
if (!buttonContainer) {
buttonContainer = createButtonContainer();
// 插入按钮容器到 textarea 的父元素之后
// 根据ChatGPT的DOM结构,可能需要调整插入位置
// textarea.parentElement.insertBefore(buttonContainer, textarea.nextSibling);
// console.log("✅ 按钮容器已附加到 textarea 元素。");
appendToMainLayer(buttonContainer);
// 创建后立即根据域名样式调整高度/注入CSS
try { applyDomainStyles(); } catch (_) {}
console.log("✅ 按钮容器已固定到窗口底部。");
} else {
console.log("ℹ️ 按钮容器已存在,跳过附加。");
}
textarea.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
};
let attachTimeout;
const attachButtons = () => {
if (attachTimeout) clearTimeout(attachTimeout);
attachTimeout = setTimeout(() => {
const textareas = getAllTextareas();
console.log(`🔍 扫描到 ${textareas.length} 个 textarea 或 contenteditable 元素。`);
if (textareas.length === 0) {
console.warn("⚠️ 未找到任何 textarea 或 contenteditable 元素。");
return;
}
attachButtonsToTextarea(textareas[textareas.length - 1]);
console.log("✅ 按钮已附加到最新的 textarea 或 contenteditable 元素。");
}, 300);
};
const observeShadowRoots = (node) => {
if (node.shadowRoot) {
const shadowObserver = new MutationObserver(() => {
attachButtons();
});
shadowObserver.observe(node.shadowRoot, {
childList: true,
subtree: true,
});
node.shadowRoot.querySelectorAll('*').forEach(child => observeShadowRoots(child));
}
};
const clampBarSpacingValue = (value, fallback = 0) => {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return Math.max(-200, Math.min(200, parsed));
}
const fallbackParsed = Number(fallback);
if (Number.isFinite(fallbackParsed)) {
return Math.max(-200, Math.min(200, fallbackParsed));
}
return 0;
};
const applyBarBottomSpacing = (container, spacing, fallbackSpacing = 0) => {
if (!container) return 0;
const desiredSpacing = clampBarSpacingValue(spacing, fallbackSpacing);
const paddingY = Number(container.dataset.barPaddingY) || 0;
const adjustedBottom = desiredSpacing - paddingY;
container.style.transform = 'translateY(0)';
container.style.bottom = `${adjustedBottom}px`;
container.dataset.barBottomSpacing = String(desiredSpacing);
return desiredSpacing;
};
// 根据目标高度调整底部按钮栏的布局和内部按钮尺寸
updateButtonBarLayout = (container, targetHeight) => {
if (!container) return;
const numericHeight = Number(targetHeight);
if (!Number.isFinite(numericHeight) || numericHeight <= 0) return;
const barHeight = Math.max(32, Math.round(numericHeight));
const scale = Math.max(0.6, Math.min(2.5, barHeight / 40));
const paddingYBase = Math.round(6 * scale);
const paddingYMax = Math.max(4, Math.floor((barHeight - 24) / 2));
const paddingY = Math.min(Math.max(4, Math.min(20, paddingYBase)), paddingYMax);
const paddingX = Math.max(12, Math.min(48, Math.round(15 * scale)));
const gapSize = Math.max(6, Math.min(28, Math.round(10 * scale)));
container.style.padding = `${paddingY}px ${paddingX}px`;
container.style.gap = `${gapSize}px`;
const innerHeight = Math.max(20, barHeight - paddingY * 2);
const fontSize = Math.max(12, Math.min(22, Math.round(14 * scale)));
let verticalPadding = Math.max(4, Math.min(18, Math.round(6 * scale)));
const maxVerticalPadding = Math.max(4, Math.floor((innerHeight - fontSize) / 2));
if (verticalPadding > maxVerticalPadding) {
verticalPadding = Math.max(4, maxVerticalPadding);
}
const horizontalPadding = Math.max(12, Math.min(56, Math.round(12 * scale)));
const borderRadius = Math.max(4, Math.min(20, Math.round(4 * scale)));
const lineHeight = Math.max(fontSize + 2, innerHeight - verticalPadding * 2);
const buttons = Array.from(container.children).filter(node => node.tagName === 'BUTTON');
buttons.forEach(btn => {
btn.style.minHeight = `${innerHeight}px`;
btn.style.height = `${innerHeight}px`;
btn.style.padding = `${verticalPadding}px ${horizontalPadding}px`;
btn.style.fontSize = `${fontSize}px`;
btn.style.borderRadius = `${borderRadius}px`;
btn.style.lineHeight = `${lineHeight}px`;
if (!btn.style.display) btn.style.display = 'inline-flex';
if (!btn.style.alignItems) btn.style.alignItems = 'center';
});
container.dataset.barHeight = String(barHeight);
container.dataset.barPaddingY = String(verticalPadding);
};
// 应用当前域名样式(高度 + 自定义 CSS),可在多处复用
applyDomainStyles = () => {
try {
const container = queryUI('.folder-buttons-container');
const currentHost = window.location.hostname || '';
// 若容器未创建,先跳过
if (!container) return;
const fallbackSpacing = clampBarSpacingValue(
typeof buttonConfig.buttonBarBottomSpacing === 'number'
? buttonConfig.buttonBarBottomSpacing
: (defaultConfig && typeof defaultConfig.buttonBarBottomSpacing === 'number'
? defaultConfig.buttonBarBottomSpacing
: 0)
);
// 清理当前域名下已注入的旧样式,避免重复叠加
try {
document.querySelectorAll('style[data-domain-style]').forEach(el => {
const d = el.getAttribute('data-domain-style') || '';
if (d && currentHost.includes(d)) {
el.remove();
}
});
} catch (e) {
console.warn('清理旧样式失败:', e);
}
const matchedStyle = (buttonConfig.domainStyleSettings || []).find(s => s && currentHost.includes(s.domain));
if (matchedStyle) {
// 1) 按域名样式设置按钮栏高度
const clamped = Math.min(200, Math.max(20, matchedStyle.height || buttonConfig.buttonBarHeight || (defaultConfig && defaultConfig.buttonBarHeight) || 40));
container.style.height = clamped + 'px';
updateButtonBarLayout(container, clamped);
console.log(`✅ 已根据 ${matchedStyle.name} 设置按钮栏高度:${clamped}px`);
applyBarBottomSpacing(container, matchedStyle.bottomSpacing, fallbackSpacing);
// 2) 注入自定义 CSS(若有)
if (matchedStyle.cssCode) {
const styleEl = document.createElement('style');
styleEl.setAttribute('data-domain-style', matchedStyle.domain);
styleEl.textContent = matchedStyle.cssCode;
document.head.appendChild(styleEl);
console.log(`✅ 已注入自定义CSS至 来自:${matchedStyle.name}`);
}
} else {
// 未匹配到样式时,回退到全局按钮栏高度
const fallback = (buttonConfig && typeof buttonConfig.buttonBarHeight === 'number')
? buttonConfig.buttonBarHeight
: (defaultConfig && defaultConfig.buttonBarHeight) || 40;
const clampedDefault = Math.min(200, Math.max(20, fallback));
container.style.height = clampedDefault + 'px';
updateButtonBarLayout(container, clampedDefault);
console.log(`ℹ️ 未匹配到样式规则,使用默认按钮栏高度:${clampedDefault}px`);
applyBarBottomSpacing(container, fallbackSpacing, fallbackSpacing);
}
} catch (err) {
console.warn('应用域名样式时出现问题:', err);
}
};
const initialize = () => {
attachButtons();
const observer = new MutationObserver((mutations) => {
let triggered = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
observeShadowRoots(node);
triggered = true;
}
});
}
});
if (triggered) {
attachButtons();
console.log("🔔 DOM 发生变化,尝试重新附加按钮。");
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
console.log("🔔 MutationObserver 已启动,监听 DOM 变化。");
// 先尝试一次;再延迟一次,保证容器创建完成后也能生效
try { applyDomainStyles(); } catch (_) {}
setTimeout(() => { try { applyDomainStyles(); } catch (_) {} }, 350);
};
window.addEventListener('load', () => {
console.log("⏳ 页面已完全加载,开始初始化脚本。");
initialize();
});
// 动态更新样式以适应主题变化
const updateStylesOnThemeChange = () => {
// Since we're using CSS variables, the styles are updated automatically
// Just update the button container to apply new styles
updateButtonContainer();
// 重新应用一次域名样式,防止主题切换后高度或注入样式丢失
try { applyDomainStyles(); } catch (_) {}
};
// Initial setting of CSS variables
setCSSVariables(getCurrentTheme());
})();