| 1 | import { useCallback, useEffect, useState, type ReactNode } from "react"; |
| 2 | import { Copy, Check } from "lucide-react"; |
| 3 | import { app } from "../lib/bridge"; |
| 4 | import { useT } from "../lib/i18n"; |
| 5 | import { HistoricalImportList } from "./HistoricalImportList"; |
| 6 | import { HistoricalRecoveryList } from "./HistoricalRecoveryList"; |
| 7 | import { useManagementT } from "../lib/managementLocale"; |
| 8 | |
| 9 | type StorageSettingsView = Awaited<ReturnType<typeof app.StorageSettings>>; |
| 10 | |
| 11 | export function StorageSettingsPage() { |
| 12 | const t = useT(); |
| 13 | const m = useManagementT(); |
| 14 | const [view, setView] = useState<StorageSettingsView | null>(null); |
| 15 | const [loading, setLoading] = useState(true); |
| 16 | const [failed, setFailed] = useState(false); |
| 17 | |
| 18 | const load = useCallback(async () => { |
| 19 | setLoading(true); |
| 20 | setFailed(false); |
| 21 | try { |
| 22 | setView(await app.StorageSettings()); |
| 23 | } catch { |
| 24 | setView(null); |
| 25 | setFailed(true); |
| 26 | } finally { |
| 27 | setLoading(false); |
| 28 | } |
| 29 | }, []); |
| 30 | |
| 31 | useEffect(() => { |
| 32 | void load(); |
| 33 | }, [load]); |
| 34 | |
| 35 | if (loading) return <div className="empty">{t("settings.loading")}</div>; |
| 36 | if (failed || !view) { |
| 37 | return ( |
| 38 | <div className="banner banner--error settings-load-error" role="alert"> |
| 39 | <span>{t("settings.loadFailed")}</span> |
| 40 | <button className="btn btn--small" type="button" onClick={() => void load()}>{t("common.retry")}</button> |
| 41 | </div> |
| 42 | ); |
| 43 | } |
| 44 | |
| 45 | return (<> |
| 46 | <section className="settings-section"> |
| 47 | <div className="settings-section__head"> |
| 48 | <div> |
| 49 | <div className="settings-section__title">{t("settings.storageTitle")}</div> |
| 50 | <div className="settings-section__desc">{t("settings.storageHint")}</div> |
| 51 | </div> |
| 52 | </div> |
| 53 | <div className="settings-section__body"> |
| 54 | <StoragePathField label={t("settings.defaultWorkspace")} hint={t("settings.defaultWorkspaceHint")} value={view.defaultWorkspace} /> |
| 55 | <StoragePathField label={t("settings.storageState")} value={view.statePath} /> |
| 56 | <StoragePathField label={t("settings.storageCache")} value={view.cachePath} /> |
| 57 | <StoragePathField label={t("settings.storageExtensions")} value={view.extensionsPath} /> |
| 58 | </div> |
| 59 | </section> |
| 60 | <section className="settings-section"> |
| 61 | <div className="settings-section__head"><div> |
| 62 | <div className="settings-section__title">{m("historicalTitle")}</div> |
| 63 | <div className="settings-section__desc">{m("historicalDescription")}</div> |
| 64 | </div></div> |
| 65 | <div className="settings-section__body"> |
| 66 | <HistoricalImportList active /> |
| 67 | <details><summary>{t("history.recoveryReview")}</summary><HistoricalRecoveryList active /></details> |
| 68 | </div> |
| 69 | </section> |
| 70 | </>); |
| 71 | } |
| 72 | |
| 73 | function StoragePathField({ label, hint, value }: { label: ReactNode; hint?: ReactNode; value: string }) { |
| 74 | const t = useT(); |
| 75 | const [copied, setCopied] = useState(false); |
| 76 | const [copyError, setCopyError] = useState(false); |
| 77 | return ( |
| 78 | <div className="settings-field"> |
| 79 | <div className="settings-field__copy"> |
| 80 | <div className="settings-field__label">{label}</div> |
| 81 | {hint && <div className="settings-field__hint"><span className="settings-field__hint-line">{hint}</span></div>} |
| 82 | </div> |
| 83 | <div className="settings-field__control settings-path-value"> |
| 84 | <input className="mem-input" value={value} placeholder={t("common.none")} aria-label={String(label)} readOnly /> |
| 85 | <button className="btn settings-icon-button" disabled={!value} title={t(copied ? "richLink.copied" : "common.copy")} aria-label={`${t("common.copy")}: ${String(label)}`} onClick={async () => { try { await navigator.clipboard.writeText(value); setCopied(true); setCopyError(false); } catch { setCopyError(true); } }}>{copied ? <Check size={16} /> : <Copy size={16} />}</button> |
| 86 | {copyError && <span role="alert">{t("settings.hooksJsonClipboardUnavailable")}</span>} |
| 87 | </div> |
| 88 | </div> |
| 89 | ); |
| 90 | } |
| 91 |