Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export sync config #4902

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/components/settings.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
}
}

.import-config-modal {
.import-config-content {
width: 100%;
max-width: 100%;
margin-bottom: 10px;
}
}

.user-prompt-modal {
min-height: 40vh;

Expand Down
104 changes: 91 additions & 13 deletions app/components/settings.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -467,6 +469,61 @@ function SyncConfigModal(props: { onClose?: () => void }) {
);
}

function ImportConfigModal(props: { onClose?: () => void; rows?: number }) {
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);
}
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
}}
icon={<ConfirmIcon />}
bordered
text={Locale.UI.Confirm}
/>,
]}
>
<div className={styles["import-config-modal"]}>
<textarea
className={uiStyle["modal-input"]}
autoFocus
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
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();
Expand All @@ -477,6 +534,7 @@ function SyncItems() {
}, [syncStore]);

const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved

const stateOverview = useMemo(() => {
const sessions = chatStore.sessions;
Expand Down Expand Up @@ -511,20 +569,36 @@ function SyncItems() {
setShowSyncConfigModal(true);
}}
/>
<IconButton
icon={<DownloadIcon />}
text={Locale.UI.Import}
onClick={() => {
setShowImportModal(true);
}}
/>
{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();
}}
/>
</>
)}
</div>
</ListItem>
Expand Down Expand Up @@ -555,6 +629,10 @@ function SyncItems() {
{showSyncConfigModal && (
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
)}

{showImportModal && (
<ImportConfigModal onClose={() => setShowImportModal(false)} />
)}
</>
);
}
Expand Down
9 changes: 8 additions & 1 deletion app/locales/cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,19 @@ const cn = {
NotSyncYet: "还没有进行过同步",
Success: "同步成功",
Fail: "同步失败",

ExportSuccess: "同步配置已复制到剪贴板",
ExportFail: "同步配置导出失败,请重试",
ImportSuccess: "同步配置导入成功",
ImportFail: "同步配置导入失败,请检查配置字符串",
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
ImportModal: {
Title: "导入同步配置",
Placeholder: "请输入同步配置",
},
SyncType: {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
Expand Down
9 changes: 8 additions & 1 deletion app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,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",
Expand Down
53 changes: 49 additions & 4 deletions app/store/sync.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pako from "pako";
import { getClientConfig } from "../config/client";
import { Updater } from "../typing";
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
Expand Down Expand Up @@ -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);
}
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
},
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);
Expand Down Expand Up @@ -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);
}
}
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
console.log("[Sync] failed to get remote state", e);
throw e;
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading