Skip to content

Commit abc611f

Browse files
committed
✨ feat: 添加全局设置功能,支持弹窗行为和快捷键管理
1 parent 75bded8 commit abc611f

File tree

8 files changed

+481
-8
lines changed

8 files changed

+481
-8
lines changed

entrypoints/content/components/PromptSelector.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { extractVariables } from "../utils/variableParser";
66
import { showVariableInput } from "./VariableInput";
77
import { isDarkMode } from "@/utils/tools";
88
import { getCategories } from "@/utils/categoryUtils";
9+
import { getGlobalSetting } from "@/utils/globalSettings";
910
import { t } from "@/utils/i18n";
1011

1112
interface PromptSelectorProps {
@@ -26,14 +27,16 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
2627
const [categories, setCategories] = useState<Category[]>([]);
2728
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
2829
const [categoriesMap, setCategoriesMap] = useState<Record<string, Category>>({});
30+
const [closeOnOutsideClick, setCloseOnOutsideClick] = useState(true);
2931
const searchInputRef = useRef<HTMLInputElement>(null);
3032
const listRef = useRef<HTMLDivElement>(null);
3133
const modalRef = useRef<HTMLDivElement>(null);
3234

33-
// 加载分类列表
35+
// 加载分类列表和全局设置
3436
useEffect(() => {
35-
const loadCategories = async () => {
37+
const loadData = async () => {
3638
try {
39+
// 加载分类列表
3740
const categoriesList = await getCategories();
3841
const enabledCategories = categoriesList.filter(cat => cat.enabled);
3942
setCategories(enabledCategories);
@@ -44,12 +47,21 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
4447
categoryMap[cat.id] = cat;
4548
});
4649
setCategoriesMap(categoryMap);
50+
51+
// 加载全局设置
52+
try {
53+
const closeModalOnOutsideClick = await getGlobalSetting('closeModalOnOutsideClick');
54+
setCloseOnOutsideClick(closeModalOnOutsideClick);
55+
} catch (err) {
56+
console.warn('Failed to load global settings:', err);
57+
setCloseOnOutsideClick(true); // 默认启用
58+
}
4759
} catch (err) {
4860
console.error(t('loadCategoriesFailed'), err);
4961
}
5062
};
5163

52-
loadCategories();
64+
loadData();
5365
}, []);
5466

5567
// 过滤提示列表 - 同时考虑搜索词和分类筛选
@@ -408,7 +420,7 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
408420

409421
// 点击背景关闭弹窗
410422
const handleBackgroundClick = (e: React.MouseEvent) => {
411-
if (e.target === e.currentTarget) {
423+
if (e.target === e.currentTarget && closeOnOutsideClick) {
412424
onClose();
413425
}
414426
};

entrypoints/content/components/VariableInput.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { PromptItemWithVariables, EditableElement } from "@/utils/types";
44
import { getPromptSelectorStyles } from "../utils/styles";
55
import { extractVariables, replaceVariables } from "../utils/variableParser";
66
import { isDarkMode } from "@/utils/tools";
7+
import { getGlobalSetting } from "@/utils/globalSettings";
78
import { t } from "@/utils/i18n";
89

910

@@ -161,8 +162,9 @@ const VariableInput: React.FC<VariableInputProps> = ({
161162
Object.fromEntries(variables.map(v => [v, '']))
162163
);
163164

164-
// 预览状态
165+
// 预览状态和全局设置
165166
const [previewContent, setPreviewContent] = useState(prompt.content);
167+
const [closeOnOutsideClick, setCloseOnOutsideClick] = useState(true);
166168
const firstInputRef = useRef<HTMLTextAreaElement>(null);
167169
const formRef = useRef<HTMLFormElement>(null);
168170

@@ -185,13 +187,26 @@ const VariableInput: React.FC<VariableInputProps> = ({
185187
onSubmit(processedContent);
186188
};
187189

188-
// 组件挂载时聚焦第一个输入框
190+
// 组件挂载时聚焦第一个输入框和加载全局设置
189191
useEffect(() => {
190192
setTimeout(() => {
191193
if (firstInputRef.current) {
192194
firstInputRef.current.focus();
193195
}
194196
}, 100);
197+
198+
// 加载全局设置
199+
const loadGlobalSettings = async () => {
200+
try {
201+
const closeModalOnOutsideClick = await getGlobalSetting('closeModalOnOutsideClick');
202+
setCloseOnOutsideClick(closeModalOnOutsideClick);
203+
} catch (err) {
204+
console.warn('Failed to load global settings:', err);
205+
setCloseOnOutsideClick(true); // 默认启用
206+
}
207+
};
208+
209+
loadGlobalSettings();
195210
}, []);
196211

197212
// 如果没有变量,直接提交
@@ -225,7 +240,7 @@ const VariableInput: React.FC<VariableInputProps> = ({
225240

226241
// 点击背景关闭弹窗
227242
const handleBackgroundClick = (e: React.MouseEvent) => {
228-
if (e.target === e.currentTarget) {
243+
if (e.target === e.currentTarget && closeOnOutsideClick) {
229244
onCancel();
230245
}
231246
};
@@ -357,7 +372,7 @@ export function showVariableInput(
357372
// 添加到documentElement(html元素),而不是body
358373
document.documentElement.appendChild(container);
359374

360-
// 创建包装组件来处理暗黑模式
375+
// 创建包装组件来处理暗黑模式和全局设置
361376
const ThemeWrapper = () => {
362377
const [isDark, setIsDark] = useState(isDarkMode());
363378

entrypoints/options/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import PromptManager from "./components/PromptManager";
55
import CategoryManager from "./components/CategoryManager";
66
import NotionIntegrationPage from "./components/NotionIntegrationPage";
77
import GoogleAuthPage from "./components/GoogleAuthPage";
8+
import GlobalSettings from "./components/GlobalSettings";
89
import ToastContainer from "./components/ToastContainer";
910
import "./App.css";
1011
import "~/assets/tailwind.css";
@@ -95,6 +96,7 @@ const App = () => {
9596
<Routes>
9697
<Route path="/" element={<PromptManager />} />
9798
<Route path="/categories" element={<CategoryManager />} />
99+
<Route path="/settings" element={<GlobalSettings />} />
98100
<Route path="/integrations/notion" element={<NotionIntegrationPage />} />
99101
<Route path="/integrations/google" element={<GoogleAuthPage />} />
100102
</Routes>
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { browser } from '#imports';
3+
import { getGlobalSettings, updateGlobalSettings, type GlobalSettings } from '@/utils/globalSettings';
4+
import { t } from '@/utils/i18n';
5+
6+
const GlobalSettingsPage: React.FC = () => {
7+
const [settings, setSettings] = useState<GlobalSettings>({
8+
closeModalOnOutsideClick: true,
9+
});
10+
const [isLoading, setIsLoading] = useState(true);
11+
const [isSaving, setIsSaving] = useState(false);
12+
const [shortcuts, setShortcuts] = useState<{ [key: string]: string }>({});
13+
14+
// 加载设置
15+
useEffect(() => {
16+
const loadSettings = async () => {
17+
try {
18+
setIsLoading(true);
19+
const globalSettings = await getGlobalSettings();
20+
setSettings(globalSettings);
21+
22+
// 获取快捷键设置
23+
try {
24+
const commands = await browser.commands.getAll();
25+
const shortcutMap: { [key: string]: string } = {};
26+
commands.forEach(command => {
27+
if (command.name && command.shortcut) {
28+
shortcutMap[command.name] = command.shortcut;
29+
}
30+
});
31+
setShortcuts(shortcutMap);
32+
} catch (error) {
33+
console.warn('Unable to get shortcuts:', error);
34+
}
35+
} catch (error) {
36+
console.error('Failed to load settings:', error);
37+
} finally {
38+
setIsLoading(false);
39+
}
40+
};
41+
42+
loadSettings();
43+
}, []);
44+
45+
// 更新设置
46+
const handleSettingChange = async (key: keyof GlobalSettings, value: any) => {
47+
try {
48+
setIsSaving(true);
49+
const newSettings = { ...settings, [key]: value };
50+
setSettings(newSettings);
51+
await updateGlobalSettings({ [key]: value });
52+
} catch (error) {
53+
console.error('Failed to update setting:', error);
54+
// 恢复原设置
55+
setSettings(prev => ({ ...prev, [key]: settings[key] }));
56+
} finally {
57+
setIsSaving(false);
58+
}
59+
};
60+
61+
// 打开浏览器快捷键设置页
62+
const openShortcutSettings = () => {
63+
const isChrome = navigator.userAgent.includes('Chrome');
64+
const isFirefox = navigator.userAgent.includes('Firefox');
65+
66+
if (isChrome) {
67+
browser.tabs.create({ url: 'chrome://extensions/shortcuts' });
68+
} else if (isFirefox) {
69+
browser.tabs.create({ url: 'about:addons' });
70+
} else {
71+
// 其他浏览器的通用方法
72+
alert(t('pleaseManuallyNavigateToShortcuts'));
73+
}
74+
};
75+
76+
if (isLoading) {
77+
return (
78+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
79+
<div className="flex justify-center items-center min-h-screen">
80+
<div className="text-center space-y-4">
81+
<div className="relative">
82+
<div className="w-16 h-16 mx-auto">
83+
<div className="absolute inset-0 border-4 border-blue-200 dark:border-blue-800 rounded-full"></div>
84+
<div className="absolute inset-0 border-4 border-blue-600 dark:border-blue-400 rounded-full border-t-transparent animate-spin"></div>
85+
</div>
86+
</div>
87+
<div className="space-y-2">
88+
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-300">{t('loading')}</h3>
89+
<p className="text-sm text-gray-500 dark:text-gray-400">{t('loadingGlobalSettings')}</p>
90+
</div>
91+
</div>
92+
</div>
93+
</div>
94+
);
95+
}
96+
97+
return (
98+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
99+
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
100+
{/* 页面头部 */}
101+
<div className="mb-10">
102+
<div className="flex items-center space-x-3 mb-4">
103+
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
104+
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
106+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
107+
</svg>
108+
</div>
109+
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 via-blue-900 to-indigo-900 dark:from-gray-100 dark:via-blue-100 dark:to-indigo-100 bg-clip-text text-transparent">
110+
{t('globalSettings')}
111+
</h1>
112+
</div>
113+
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-3xl">
114+
{t('globalSettingsDescription')}
115+
</p>
116+
</div>
117+
118+
<div className="space-y-6">
119+
{/* 弹窗行为设置 */}
120+
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm border border-white/20 dark:border-gray-700/50 shadow-xl rounded-2xl p-8">
121+
<div className="flex items-center space-x-3 mb-6">
122+
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center">
123+
<svg className="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
124+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
125+
</svg>
126+
</div>
127+
<h2 className="text-xl font-semibold text-gray-800 dark:text-gray-200">{t('modalBehavior')}</h2>
128+
</div>
129+
130+
<div className="space-y-4">
131+
<div className="flex items-center justify-between">
132+
<div className="flex-1">
133+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
134+
{t('closeModalOnOutsideClick')}
135+
</h3>
136+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
137+
{t('closeModalOnOutsideClickDescription')}
138+
</p>
139+
</div>
140+
<label className="relative inline-flex items-center cursor-pointer ml-4">
141+
<input
142+
type="checkbox"
143+
checked={settings.closeModalOnOutsideClick}
144+
onChange={(e) => handleSettingChange('closeModalOnOutsideClick', e.target.checked)}
145+
disabled={isSaving}
146+
className="sr-only peer"
147+
/>
148+
<div className="relative w-11 h-6 bg-gray-200 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-1/2 after:right-1/2 after:-translate-y-1/2 after:bg-white after:border-gray-300 dark:after:border-gray-600 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 disabled:opacity-50"></div>
149+
</label>
150+
</div>
151+
</div>
152+
</div>
153+
154+
{/* 快捷键设置 */}
155+
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm border border-white/20 dark:border-gray-700/50 shadow-xl rounded-2xl p-8">
156+
<div className="flex items-center space-x-3 mb-6">
157+
<div className="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center">
158+
<svg className="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
159+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
160+
</svg>
161+
</div>
162+
<h2 className="text-xl font-semibold text-gray-800 dark:text-gray-200">{t('keyboardShortcuts')}</h2>
163+
</div>
164+
165+
<div className="space-y-4">
166+
<p className="text-sm text-gray-600 dark:text-gray-400">
167+
{t('shortcutsDescription')}
168+
</p>
169+
170+
{/* 显示当前快捷键 */}
171+
<div className="space-y-3">
172+
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
173+
<div>
174+
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
175+
{t('openPromptSelector')}
176+
</h4>
177+
<p className="text-xs text-gray-500 dark:text-gray-400">
178+
{t('openPromptSelectorDescription')}
179+
</p>
180+
</div>
181+
<div className="flex items-center space-x-2">
182+
{shortcuts['open-prompt-selector'] ? (
183+
<kbd className="px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600">
184+
{shortcuts['open-prompt-selector']}
185+
</kbd>
186+
) : (
187+
<span className="text-xs text-gray-400 dark:text-gray-500">
188+
{t('notSet')}
189+
</span>
190+
)}
191+
</div>
192+
</div>
193+
194+
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
195+
<div>
196+
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
197+
{t('openOptionsPage')}
198+
</h4>
199+
<p className="text-xs text-gray-500 dark:text-gray-400">
200+
{t('openOptionsPageDescription')}
201+
</p>
202+
</div>
203+
<div className="flex items-center space-x-2">
204+
{shortcuts['open-options'] ? (
205+
<kbd className="px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600">
206+
{shortcuts['open-options']}
207+
</kbd>
208+
) : (
209+
<span className="text-xs text-gray-400 dark:text-gray-500">
210+
{t('notSet')}
211+
</span>
212+
)}
213+
</div>
214+
</div>
215+
</div>
216+
217+
{/* 设置快捷键按钮 */}
218+
<div className="flex justify-center pt-4">
219+
<button
220+
onClick={openShortcutSettings}
221+
className="inline-flex items-center px-6 py-3 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white text-sm font-medium rounded-xl transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 shadow-green-500/25"
222+
>
223+
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
224+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
225+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
226+
</svg>
227+
{t('manageShortcuts')}
228+
</button>
229+
</div>
230+
</div>
231+
</div>
232+
233+
{/* 帮助信息 */}
234+
<div className="bg-blue-50/80 dark:bg-blue-900/30 backdrop-blur-sm border border-blue-200/50 dark:border-blue-800/50 rounded-2xl p-6">
235+
<div className="flex items-start space-x-3">
236+
<div className="flex-shrink-0">
237+
<svg className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
238+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
239+
</svg>
240+
</div>
241+
<div>
242+
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-200 mb-1">
243+
{t('helpAndTips')}
244+
</h3>
245+
<div className="text-sm text-blue-800 dark:text-blue-300 space-y-2">
246+
<p>{t('globalSettingsHelpText1')}</p>
247+
<p>{t('globalSettingsHelpText2')}</p>
248+
</div>
249+
</div>
250+
</div>
251+
</div>
252+
</div>
253+
</div>
254+
</div>
255+
);
256+
};
257+
258+
export default GlobalSettingsPage;

0 commit comments

Comments
 (0)