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