Add import and export functionality for sync configuration

This commit is contained in:
wangyijing 2024-07-01 01:35:49 +08:00
parent c359b30763
commit b2e88e993b
6 changed files with 168 additions and 17 deletions

View File

@ -23,6 +23,14 @@
}
}
.import-config-modal {
.import-config-content {
width: 100%;
max-width: 100%;
margin-bottom: 10px;
}
}
.user-prompt-modal {
min-height: 40vh;

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from "react";
import styles from "./settings.module.scss";
import uiStyle from "./ui-lib.module.scss";
import ResetIcon from "../icons/reload.svg";
import AddIcon from "../icons/add.svg";
@ -72,6 +73,7 @@ import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { ProviderType } from "../utils/cloud";
import CancelIcon from "../icons/cancel.svg";
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
@ -467,6 +469,61 @@ function SyncConfigModal(props: { onClose?: () => void }) {
);
}
function ImportConfigModal(props: { onClose?: () => void }) {
const [importString, setImportString] = useState("");
const syncStore = useSyncStore();
return (
<div className="modal-mask">
<Modal
title={Locale.Settings.Sync.Config.ImportModal.Title}
onClose={() => props.onClose?.()}
actions={[
<IconButton
key="cancel"
onClick={() => {
props.onClose();
}}
icon={<CancelIcon />}
bordered
shadow
text={Locale.UI.Cancel}
/>,
<IconButton
key="confirm"
onClick={async () => {
try {
await syncStore.importSyncConfig(importString);
showToast(Locale.Settings.Sync.ImportSuccess);
props.onClose?.();
} catch (e) {
showToast(Locale.Settings.Sync.ImportFail);
console.log("[Sync] Failed to import sync config", e);
}
}}
icon={<ConfirmIcon />}
bordered
text={Locale.UI.Confirm}
/>,
]}
>
<div className={styles["import-config-modal"]}>
<textarea
className={uiStyle["modal-input"]}
autoFocus
placeholder={Locale.Settings.Sync.Config.ImportModal.Placeholder}
value={importString}
rows={props.rows ?? 3}
onInput={(e) => {
setImportString(e.currentTarget.value);
}}
/>
</div>
</Modal>
</div>
);
}
function SyncItems() {
const syncStore = useSyncStore();
const chatStore = useChatStore();
@ -477,6 +534,7 @@ function SyncItems() {
}, [syncStore]);
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const stateOverview = useMemo(() => {
const sessions = chatStore.sessions;
@ -512,19 +570,35 @@ function SyncItems() {
}}
/>
{couldSync && (
<IconButton
icon={<ResetIcon />}
text={Locale.UI.Sync}
onClick={async () => {
try {
await syncStore.sync();
showToast(Locale.Settings.Sync.Success);
} catch (e) {
showToast(Locale.Settings.Sync.Fail);
console.error("[Sync]", e);
}
}}
/>
<>
<IconButton
icon={<ResetIcon />}
text={Locale.UI.Sync}
onClick={async () => {
try {
await syncStore.sync();
showToast(Locale.Settings.Sync.Success);
} catch (e) {
showToast(Locale.Settings.Sync.Fail);
console.error("[Sync]", e);
}
}}
/>
<IconButton
icon={<UploadIcon />}
text={Locale.UI.Export}
onClick={() => {
syncStore.exportSyncConfig();
}}
/>
<IconButton
icon={<DownloadIcon />}
text={Locale.UI.Import}
onClick={() => {
setShowImportModal(true);
}}
/>
</>
)}
</div>
</ListItem>
@ -555,6 +629,10 @@ function SyncItems() {
{showSyncConfigModal && (
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
)}
{showImportModal && (
<ImportConfigModal onClose={() => setShowImportModal(false)} />
)}
</>
);
}

View File

@ -186,11 +186,20 @@ const cn = {
Success: "同步成功",
Fail: "同步失败",
ExportSuccess: "同步配置已复制到剪贴板",
ExportFail: "导出失败,请重试",
ImportSuccess: "同步配置导入成功",
ImportFail: "导入失败,请检查配置字符串",
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
ImportModal: {
Title: "导入同步配置",
Placeholder: "请输入同步配置",
},
SyncType: {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",

View File

@ -1,3 +1,4 @@
import pako from "pako";
import { getClientConfig } from "../config/client";
import { Updater } from "../typing";
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
@ -44,10 +45,51 @@ const DEFAULT_SYNC_STATE = {
lastSyncTime: 0,
lastProvider: "",
};
export const useSyncStore = createPersistStore(
DEFAULT_SYNC_STATE,
(set, get) => ({
async exportSyncConfig() {
const currentProvider = get().provider;
const exportData = {
provider: currentProvider,
config: get()[currentProvider],
useProxy: get().useProxy,
proxyUrl: get().proxyUrl,
};
const jsonString = JSON.stringify(exportData);
const compressed = pako.deflate(jsonString);
const encoded = btoa(String.fromCharCode.apply(null, compressed));
try {
await navigator.clipboard.writeText(encoded);
showToast(Locale.Settings.Sync.ExportSuccess);
} catch (e) {
console.log("[Sync] failed to copy", e);
showToast(Locale.Settings.Sync.ExportFail);
}
},
importSyncConfig(encodedString: string) {
try {
const decoded = atob(encodedString);
const decompressed = pako.inflate(
new Uint8Array(decoded.split("").map((char) => char.charCodeAt(0))),
{ to: "string" },
);
const importedData = JSON.parse(decompressed);
set({
provider: importedData.provider,
[importedData.provider]: importedData.config,
useProxy: importedData.useProxy,
proxyUrl: importedData.proxyUrl,
});
} catch (e) {
console.log("[Sync] failed to set sync config", e);
throw e;
}
},
cloudSync() {
const config = get()[get().provider];
return Object.values(config).every((c) => c.toString().length > 0);
@ -100,15 +142,17 @@ export const useSyncStore = createPersistStore(
const remoteState = await client.get(config.username);
if (!remoteState || remoteState === "") {
await client.set(config.username, JSON.stringify(localState));
console.log("[Sync] Remote state is empty, using local state instead.");
return
console.log(
"[Sync] Remote state is empty, using local state instead.",
);
return;
} else {
const parsedRemoteState = JSON.parse(
await client.get(config.username),
) as AppState;
mergeAppState(localState, parsedRemoteState);
setLocalAppState(localState);
}
}
} catch (e) {
console.log("[Sync] failed to get remote state", e);
throw e;

View File

@ -30,6 +30,7 @@
"nanoid": "^5.0.3",
"next": "^14.1.1",
"node-fetch": "^3.3.1",
"pako": "^2.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
@ -47,6 +48,7 @@
"devDependencies": {
"@tauri-apps/cli": "1.5.11",
"@types/node": "^20.11.30",
"@types/pako": "^2.0.3",
"@types/react": "^18.2.70",
"@types/react-dom": "^18.2.7",
"@types/react-katex": "^3.0.0",

View File

@ -1601,6 +1601,11 @@
dependencies:
undici-types "~5.26.4"
"@types/pako@^2.0.3":
version "2.0.3"
resolved "https://registry.npmmirror.com/@types/pako/-/pako-2.0.3.tgz#b6993334f3af27c158f3fe0dfeeba987c578afb1"
integrity sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -4971,6 +4976,11 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"
pako@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"