diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss
index c6aec4203..c2cd517c3 100644
--- a/app/components/settings.module.scss
+++ b/app/components/settings.module.scss
@@ -23,6 +23,14 @@
}
}
+.import-config-modal {
+ .import-config-content {
+ width: 100%;
+ max-width: 100%;
+ margin-bottom: 10px;
+ }
+}
+
.user-prompt-modal {
min-height: 40vh;
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index db08b48a9..36ffdb84a 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -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 (
+
+
props.onClose?.()}
+ actions={[
+ {
+ props.onClose();
+ }}
+ icon={}
+ bordered
+ shadow
+ text={Locale.UI.Cancel}
+ />,
+ {
+ 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={}
+ bordered
+ text={Locale.UI.Confirm}
+ />,
+ ]}
+ >
+
+
+
+
+ );
+}
+
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 && (
- }
- 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);
- }
- }}
- />
+ <>
+ }
+ 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);
+ }
+ }}
+ />
+ }
+ text={Locale.UI.Export}
+ onClick={() => {
+ syncStore.exportSyncConfig();
+ }}
+ />
+ }
+ text={Locale.UI.Import}
+ onClick={() => {
+ setShowImportModal(true);
+ }}
+ />
+ >
)}
@@ -555,6 +629,10 @@ function SyncItems() {
{showSyncConfigModal && (
setShowSyncConfigModal(false)} />
)}
+
+ {showImportModal && (
+ setShowImportModal(false)} />
+ )}
>
);
}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 2ff94e32d..fbd9cdc79 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -186,11 +186,20 @@ const cn = {
Success: "同步成功",
Fail: "同步失败",
+ ExportSuccess: "同步配置已复制到剪贴板",
+ ExportFail: "导出失败,请重试",
+ ImportSuccess: "同步配置导入成功",
+ ImportFail: "导入失败,请检查配置字符串",
+
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
+ ImportModal: {
+ Title: "导入同步配置",
+ Placeholder: "请输入同步配置",
+ },
SyncType: {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
diff --git a/app/store/sync.ts b/app/store/sync.ts
index d3582e3c9..297997d48 100644
--- a/app/store/sync.ts
+++ b/app/store/sync.ts
@@ -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;
diff --git a/package.json b/package.json
index 4d06b0b14..fd1fbfc59 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/yarn.lock b/yarn.lock
index 72df8cafc..35c72d975 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"