diff --git a/app/components/home.tsx b/app/components/home.tsx index 465ad0f1e..f069b1fc4 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -29,6 +29,9 @@ import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; import { type ClientApi, getClientApi } from "../client/api"; import { useAccessStore } from "../store"; +import { useSyncStore } from "../store/sync"; +import { showToast } from "./ui-lib"; +import Locale from "@/app/locales"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -149,6 +152,40 @@ export function WindowContent(props: { children: React.ReactNode }) { ); } +function useSyncOnStart() { + const syncStore = useSyncStore(); + const storeHasHydrated = useSyncStore((s) => s._hasHydrated); + useEffect(() => { + let running = true; + setTimeout(async () => { + if ( + !( + storeHasHydrated && + running && + syncStore.cloudSync() && + syncStore.autoSync.onStart + ) + ) { + return; + } + const dismissSyncingToast = showToast(Locale.Settings.Sync.IsSyncing); + try { + await syncStore.sync(); + dismissSyncingToast(); + showToast(Locale.Settings.Sync.Success); + } catch (e: unknown) { + dismissSyncingToast(); + showToast(Locale.Settings.Sync.Fail); + console.error("[Sync]", e); + } + }); + return () => { + running = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [storeHasHydrated]); +} + function Screen() { const config = useAppConfig(); const location = useLocation(); @@ -165,6 +202,7 @@ function Screen() { useEffect(() => { loadAsyncGoogleFont(); }, []); + useSyncOnStart(); if (isArtifact) { return ( diff --git a/app/components/settings.tsx b/app/components/settings.tsx index e2666b551..c44b4a7e2 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -477,6 +477,21 @@ function SyncConfigModal(props: { onClose?: () => void }) { )} + + + + { + syncStore.update( + (config) => + (config.autoSync.onStart = e.currentTarget.checked), + ); + }} + /> + + ); diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 4af37dbba..5d52bef59 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -229,13 +229,19 @@ export function showToast( content: string, action?: ToastProps["action"], delay = 3000, -) { +): () => void { const div = document.createElement("div"); div.className = styles.show; document.body.appendChild(div); const root = createRoot(div); + let closeCalled = false; const close = () => { + if (closeCalled) { + return; + } else { + closeCalled = true; + } div.classList.add(styles.hide); setTimeout(() => { @@ -249,6 +255,8 @@ export function showToast( }, delay); root.render(); + + return close; } export type InputProps = React.HTMLProps & { diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 9712593c6..7e8892e71 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -223,6 +223,7 @@ const cn = { CloudState: "云端数据", NotSyncYet: "还没有进行过同步", Success: "同步成功", + IsSyncing: "正在同步...", Fail: "同步失败", Config: { @@ -254,6 +255,10 @@ const cn = { UserName: "备份名称", Password: "UpStash Redis REST Token", }, + + AutoSync: { + OnStartup: "启动时自动同步", + }, }, LocalState: "本地数据", diff --git a/app/locales/en.ts b/app/locales/en.ts index ac8d3aed2..d4fe441ea 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -225,6 +225,7 @@ const en: LocaleType = { CloudState: "Last Update", NotSyncYet: "Not sync yet", Success: "Sync Success", + IsSyncing: "Sync in progress...", Fail: "Sync Fail", Config: { @@ -257,6 +258,10 @@ const en: LocaleType = { UserName: "Backup Name", Password: "UpStash Redis REST Token", }, + + AutoSync: { + OnStartup: "Sync on startup", + }, }, LocalState: "Local Data", diff --git a/app/store/sync.ts b/app/store/sync.ts index 8477c1e4b..c297699df 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -27,18 +27,22 @@ const DEFAULT_SYNC_STATE = { useProxy: true, proxyUrl: ApiPath.Cors as string, - webdav: { + [ProviderType.WebDAV]: { endpoint: "", username: "", password: "", }, - upstash: { + [ProviderType.UpStash]: { endpoint: "", username: STORAGE_KEY, apiKey: "", }, + autoSync: { + onStart: false, + }, + lastSyncTime: 0, lastProvider: "", }; @@ -46,9 +50,14 @@ const DEFAULT_SYNC_STATE = { export const useSyncStore = createPersistStore( DEFAULT_SYNC_STATE, (set, get) => ({ - cloudSync() { + cloudSync(): boolean { const config = get()[get().provider]; - return Object.values(config).every((c) => c.toString().length > 0); + if (!config) { + return false; + } + return Object.values(config).every( + (c) => c != null && c.toString().length > 0, + ); }, markSyncTime() { @@ -126,7 +135,7 @@ export const useSyncStore = createPersistStore( }), { name: StoreKey.Sync, - version: 1.2, + version: 1.3, migrate(persistedState, version) { const newState = persistedState as typeof DEFAULT_SYNC_STATE; @@ -144,6 +153,10 @@ export const useSyncStore = createPersistStore( } } + if (version < 1.3) { + newState.autoSync = { ...DEFAULT_SYNC_STATE.autoSync }; + } + return newState as any; }, },