This commit is contained in:
April Flair 2024-09-05 09:08:40 +07:00 committed by GitHub
commit 7f307bc299
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 176 additions and 19 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";
@ -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 (
<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();
@ -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);
}}
/>
<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>
@ -568,6 +642,10 @@ function SyncItems() {
{showSyncConfigModal && (
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
)}
{showImportModal && (
<ImportConfigModal onClose={() => setShowImportModal(false)} />
)}
</>
);
}

View File

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

View File

@ -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",

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,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;

View File

@ -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",

View File

@ -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"