返回 DeepSeek-Reasonix
BrowserControlSettingsPage.tsx
根目录 / desktop / frontend / src / components / BrowserControlSettingsPage.tsx
1 import { ErrorMessage } from "./ErrorMessage";
2 import { useCallback, useEffect, useRef, useState } from "react";
3 import { Cookie, Globe, ShieldCheck, Trash2 } from "lucide-react";
4 import { SettingsField, SettingsSection } from "./SettingsForm";
5 import { desktopHost, type BrowserControlState, type ChromeImportFailure } from "../lib/desktopHost";
6 import { useT, type DictKey } from "../lib/i18n";
7
8 // Each failure the shell can report gets its own sentence: "no Chrome profile"
9 // and "the keychain prompt was denied" need different user actions.
10 const IMPORT_FAILURE_KEYS: Record<ChromeImportFailure, DictKey> = {
11 "chrome-missing": "settings.browser.import.chromeMissing",
12 "profile-not-found": "settings.browser.import.profileNotFound",
13 "cookies-unreadable": "settings.browser.import.cookiesUnreadable",
14 "safe-storage-denied": "settings.browser.import.safeStorageDenied",
15 "safe-storage-unavailable": "settings.browser.import.safeStorageUnavailable",
16 "unsupported-platform": "settings.browser.import.unsupportedPlatform",
17 };
18
19 const WARNING_KEYS = {
20 "invalid-config": "settings.browser.warningInvalid",
21 "unreadable-config": "settings.browser.warningUnreadable",
22 "unsupported-version": "settings.browser.warningUnsupported",
23 } as const;
24
25 export function BrowserControlSettingsPage() {
26 const t = useT();
27 const host = desktopHost();
28 const api = host.native.browserControl;
29 // undefined = still loading; null = the shell has no browser-control store.
30 const [state, setState] = useState<BrowserControlState | null | undefined>(undefined);
31 const [pending, setPending] = useState<string | null>(null);
32 const [notice, setNotice] = useState<string | null>(null);
33 const [error, setError] = useState<string | null>(null);
34 const [confirmClearAll, setConfirmClearAll] = useState(false);
35 const mounted = useRef(true);
36
37 useEffect(() => {
38 mounted.current = true;
39 void api.get().then(
40 (value) => {
41 if (mounted.current) setState(value);
42 },
43 () => {
44 if (mounted.current) setError(t("settings.browser.loadFailed"));
45 },
46 );
47 return () => {
48 mounted.current = false;
49 };
50 }, [api, t]);
51
52 const run = useCallback(async (label: string, action: () => Promise<string>) => {
53 setPending(label);
54 setError(null);
55 setNotice(null);
56 try {
57 setNotice(await action());
58 } catch (failure) {
59 setError(failure instanceof Error ? failure.message : String(failure));
60 } finally {
61 setPending(null);
62 }
63 }, []);
64 const busy = pending !== null;
65
66 const setControl = (enabled: boolean) =>
67 void run("control", async () => {
68 setState(await api.setEnabled(enabled));
69 return t(enabled ? "settings.browser.controlOn" : "settings.browser.controlOff");
70 });
71
72 const setCertificates = (enabled: boolean) =>
73 void run("certificates", async () => {
74 setState(await api.setIgnoreCertificateErrors(enabled));
75 return t(enabled ? "settings.browser.certificatesOn" : "settings.browser.certificatesOff");
76 });
77
78 const importChrome = () =>
79 void run("import", async () => {
80 const outcome = await api.importChromeLogin();
81 if (!outcome.ok) throw new Error(t(IMPORT_FAILURE_KEYS[outcome.reason]));
82 return t("settings.browser.import.done", { profile: outcome.profile, cookies: outcome.cookies, skipped: outcome.skipped });
83 });
84
85 const clearCache = () =>
86 void run("clearCache", async () => {
87 await api.clearCache();
88 return t("settings.browser.clearCache.done");
89 });
90
91 const clearAll = () =>
92 void run("clearAll", async () => {
93 await api.clearAllData();
94 setConfirmClearAll(false);
95 return t("settings.browser.clearAll.done");
96 });
97
98 if (host.kind !== "electron") {
99 return (
100 <div className="banner banner--warning" role="status">
101 <span>{t("settings.browser.desktopOnly")}</span>
102 </div>
103 );
104 }
105 if (state === null) {
106 return (
107 <div className="banner banner--warning" role="status">
108 <span>{t("settings.browser.loadFailed")}</span>
109 </div>
110 );
111 }
112 if (!state) return <div className="empty">{t("settings.loading")}</div>;
113
114 return (
115 <>
116 {error && <div className="banner banner--error" role="alert"><span><ErrorMessage error={error} /></span></div>}
117 {notice && <div className="banner banner--success" role="status"><span>{notice}</span></div>}
118 {state.warning && <div className="banner banner--warning" role="status"><span>{t(WARNING_KEYS[state.warning])}</span></div>}
119 <SettingsSection title={t("settings.browser.basics")} description={t("settings.browser.basicsHint")}>
120 <SettingsField label={t("settings.browser.control")} hint={t("settings.browser.controlHint")} icon={<Globe size={18} />}>
121 <input
122 className="provider-capability-row__switch"
123 type="checkbox"
124 role="switch"
125 aria-label={t("settings.browser.control")}
126 checked={state.controlEnabled}
127 disabled={busy || !state.writable}
128 onChange={(event) => setControl(event.currentTarget.checked)}
129 />
130 </SettingsField>
131 <SettingsField label={t("settings.browser.import.title")} hint={t("settings.browser.import.hint")} icon={<Cookie size={18} />}>
132 <button className="btn btn--small" type="button" disabled={busy} onClick={importChrome}>
133 {t(pending === "import" ? "settings.browser.import.running" : "settings.browser.import.action")}
134 </button>
135 </SettingsField>
136 </SettingsSection>
137 <SettingsSection title={t("settings.browser.security")} description={t("settings.browser.securityHint")}>
138 <SettingsField label={t("settings.browser.ignoreCertificates")} hint={t("settings.browser.ignoreCertificatesHint")} icon={<ShieldCheck size={18} />}>
139 <input
140 className="provider-capability-row__switch"
141 type="checkbox"
142 role="switch"
143 aria-label={t("settings.browser.ignoreCertificates")}
144 checked={state.ignoreCertificateErrors}
145 disabled={busy || !state.writable}
146 onChange={(event) => setCertificates(event.currentTarget.checked)}
147 />
148 </SettingsField>
149 </SettingsSection>
150 <SettingsSection title={t("settings.browser.data")} description={t("settings.browser.dataHint")}>
151 <SettingsField label={t("settings.browser.clearCache.title")} hint={t("settings.browser.clearCache.hint")} icon={<Trash2 size={18} />}>
152 <button className="btn btn--small" type="button" disabled={busy} onClick={clearCache}>
153 {t("settings.browser.clearCache.action")}
154 </button>
155 </SettingsField>
156 <SettingsField label={t("settings.browser.clearAll.title")} hint={t("settings.browser.clearAll.hint")} icon={<Trash2 size={18} />}>
157 {confirmClearAll ? (
158 <div className="settings-inline-controls">
159 <button className="btn btn--small btn--danger" type="button" disabled={busy} onClick={clearAll}>
160 {t("settings.browser.clearAll.confirm")}
161 </button>
162 <button className="btn btn--small" type="button" disabled={busy} onClick={() => setConfirmClearAll(false)}>
163 {t("common.cancel")}
164 </button>
165 </div>
166 ) : (
167 <button className="btn btn--small btn--danger" type="button" disabled={busy} onClick={() => setConfirmClearAll(true)}>
168 {t("settings.browser.clearAll.action")}
169 </button>
170 )}
171 </SettingsField>
172 </SettingsSection>
173 </>
174 );
175 }
176
176 lines Plain Text