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 ca0a5a187..07baf45e1 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";
@@ -80,6 +81,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();
@@ -477,6 +479,61 @@ function SyncConfigModal(props: { onClose?: () => void }) {
);
}
+function ImportConfigModal(props: { onClose?: () => void; rows?: number }) {
+ 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();
@@ -487,6 +544,7 @@ function SyncItems() {
}, [syncStore]);
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
+ const [showImportModal, setShowImportModal] = useState(false);
const stateOverview = useMemo(() => {
const sessions = chatStore.sessions;
@@ -522,20 +580,36 @@ function SyncItems() {
setShowSyncConfigModal(true);
}}
/>
+ }
+ text={Locale.UI.Import}
+ onClick={() => {
+ setShowImportModal(true);
+ }}
+ />
{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();
+ }}
+ />
+ >
)}
@@ -568,6 +642,10 @@ function SyncItems() {
{showSyncConfigModal && (
setShowSyncConfigModal(false)} />
)}
+
+ {showImportModal && (
+ setShowImportModal(false)} />
+ )}
>
);
}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 9a3227d68..83c5db06e 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -196,12 +196,19 @@ const cn = {
NotSyncYet: "还没有进行过同步",
Success: "同步成功",
Fail: "同步失败",
-
+ ExportSuccess: "同步配置已复制到剪贴板",
+ ExportFail: "同步配置导出失败,请重试",
+ ImportSuccess: "同步配置导入成功",
+ ImportFail: "同步配置导入失败,请检查配置字符串",
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
+ ImportModal: {
+ Title: "导入同步配置",
+ Placeholder: "请输入同步配置",
+ },
SyncType: {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 77f3a700a..50b357c79 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -199,12 +199,19 @@ const en: LocaleType = {
NotSyncYet: "Not sync yet",
Success: "Sync Success",
Fail: "Sync Fail",
-
+ ExportSuccess: "Sync config copied to clipboard",
+ ExportFail: "Export failed, please retry",
+ ImportSuccess: "Sync config imported successfully",
+ ImportFail: "Import failed, please check config string",
Config: {
Modal: {
Title: "Config Sync",
Check: "Check Connection",
},
+ ImportModal: {
+ Title: "Import Sync Config",
+ Placeholder: "Enter sync config",
+ },
SyncType: {
Title: "Sync Type",
SubTitle: "Choose your favorite sync service",
diff --git a/app/store/sync.ts b/app/store/sync.ts
index d3582e3c9..3953c140d 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,52 @@ 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, Array.from(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 +143,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 1c6d78c20..e0d3b1215 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,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",
@@ -52,6 +53,7 @@
"@tauri-apps/cli": "1.5.11",
"@types/lodash-es": "^4.17.12",
"@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 1c7f834e8..466025576 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1735,6 +1735,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"
@@ -5199,6 +5204,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"