mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-02-08 23:44:25 +08:00
Add import and export functionality for sync configuration
This commit is contained in:
@@ -23,6 +23,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.import-config-modal {
|
||||
.import-config-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-prompt-modal {
|
||||
min-height: 40vh;
|
||||
|
||||
|
||||
@@ -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)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,11 +186,20 @@ const cn = {
|
||||
Success: "同步成功",
|
||||
Fail: "同步失败",
|
||||
|
||||
ExportSuccess: "同步配置已复制到剪贴板",
|
||||
ExportFail: "导出失败,请重试",
|
||||
ImportSuccess: "同步配置导入成功",
|
||||
ImportFail: "导入失败,请检查配置字符串",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "配置云同步",
|
||||
Check: "检查可用性",
|
||||
},
|
||||
ImportModal: {
|
||||
Title: "导入同步配置",
|
||||
Placeholder: "请输入同步配置",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "同步类型",
|
||||
SubTitle: "选择喜爱的同步服务器",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user