import pako from "pako"; import { getClientConfig } from "../config/client"; import { Updater } from "../typing"; import { ApiPath, STORAGE_KEY, StoreKey } from "../constant"; import { createPersistStore } from "../utils/store"; import { AppState, getLocalAppState, GetStoreState, mergeAppState, setLocalAppState, } from "../utils/sync"; import { downloadAs, readFromFile } from "../utils"; import { showToast } from "../components/ui-lib"; import Locale from "../locales"; import { createSyncClient, ProviderType } from "../utils/cloud"; import { corsPath } from "../utils/cors"; export interface WebDavConfig { server: string; username: string; password: string; } const isApp = !!getClientConfig()?.isApp; export type SyncStore = GetStoreState; const DEFAULT_SYNC_STATE = { provider: ProviderType.WebDAV, useProxy: true, proxyUrl: corsPath(ApiPath.Cors), webdav: { endpoint: "", username: "", password: "", }, upstash: { endpoint: "", username: STORAGE_KEY, apiKey: "", }, 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); }, markSyncTime() { set({ lastSyncTime: Date.now(), lastProvider: get().provider }); }, export() { const state = getLocalAppState(); const datePart = isApp ? `${new Date().toLocaleDateString().replace(/\//g, "_")} ${new Date() .toLocaleTimeString() .replace(/:/g, "_")}` : new Date().toLocaleString(); const fileName = `Backup-${datePart}.json`; downloadAs(JSON.stringify(state), fileName); }, async import() { const rawContent = await readFromFile(); try { const remoteState = JSON.parse(rawContent) as AppState; const localState = getLocalAppState(); mergeAppState(localState, remoteState); setLocalAppState(localState); location.reload(); } catch (e) { console.error("[Import]", e); showToast(Locale.Settings.Sync.ImportFailed); } }, getClient() { const provider = get().provider; const client = createSyncClient(provider, get()); return client; }, async sync() { const localState = getLocalAppState(); const provider = get().provider; const config = get()[provider]; const client = this.getClient(); try { 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; } 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; } await client.set(config.username, JSON.stringify(localState)); this.markSyncTime(); }, async check() { const client = this.getClient(); return await client.check(); }, }), { name: StoreKey.Sync, version: 1.2, migrate(persistedState, version) { const newState = persistedState as typeof DEFAULT_SYNC_STATE; if (version < 1.1) { newState.upstash.username = STORAGE_KEY; } if (version < 1.2) { if ( (persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl === "/api/cors/" ) { newState.proxyUrl = ""; } } return newState as any; }, }, );