One-click backups for AT Protocol
1import { useState, useEffect } from "react";
2import "./App.css";
3import { Button } from "./components/ui/button";
4import LoginPage from "./routes/Login";
5import { getCurrentWindow } from "@tauri-apps/api/window";
6import { LoaderCircleIcon } from "lucide-react";
7import { AuthProvider, useAuth } from "./Auth";
8import { initializeLocalStorage } from "./localstorage_ployfill";
9import { Home } from "./routes/Home";
10import { ThemeProvider } from "./theme-provider";
11import { toast, Toaster } from "sonner";
12import { ScrollArea } from "./components/ui/scroll-area";
13import { check, Update } from "@tauri-apps/plugin-updater";
14import { relaunch } from "@tauri-apps/plugin-process";
15import {
16 BackgroundBackupService,
17 handleBackgroundBackup,
18} from "./lib/backgroundBackup";
19import {
20 Dialog,
21 DialogClose,
22 DialogContent,
23 DialogDescription,
24 DialogFooter,
25 DialogHeader,
26 DialogTitle,
27} from "@/components/ui/dialog";
28import { Progress } from "./components/ui/progress";
29import { MarkdownRenderer } from "./components/ui/markdown-renderer";
30
31function AppContent() {
32 const { isLoading, isAuthenticated, profile, client, login, logout, agent } =
33 useAuth();
34 const appWindow = getCurrentWindow();
35
36 const [isLocalStorageReady, setIsLocalStorageReady] = useState(false);
37 const [update, setUpdate] = useState<Update | null>(null);
38 const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
39
40 useEffect(() => {
41 const initStorage = async () => {
42 try {
43 await initializeLocalStorage();
44 setIsLocalStorageReady(true);
45 } catch (error) {
46 console.error("Failed to initialize localStorage:", error);
47 setIsLocalStorageReady(true); // Continue anyway
48 }
49 };
50
51 initStorage();
52 }, []);
53
54 // Background backup service initialization
55 useEffect(() => {
56 if (!isAuthenticated || !agent) return;
57
58 const backgroundService = BackgroundBackupService.getInstance();
59 backgroundService.initialize();
60
61 // Listen for background backup requests
62 const handleBackgroundBackupRequest = () => {
63 handleBackgroundBackup(agent);
64 };
65
66 window.addEventListener(
67 "background-backup-requested",
68 handleBackgroundBackupRequest
69 );
70
71 return () => {
72 window.removeEventListener(
73 "background-backup-requested",
74 handleBackgroundBackupRequest
75 );
76 backgroundService.stop();
77 };
78 }, [isAuthenticated, agent]);
79
80 // Auto-backup functionality (for when app is open)
81 // useEffect(() => {
82 // if (!isAuthenticated || !agent) return;
83
84 // let intervalId: ReturnType<typeof setInterval> | null = null;
85
86 // const checkAndPerformBackup = async () => {
87 // try {
88 // const lastBackupDate = await settingsManager.getLastBackupDate();
89 // const frequency = await settingsManager.getBackupFrequency();
90
91 // if (!lastBackupDate) {
92 // // No previous backup, so we should do one
93 // await performBackup();
94 // return;
95 // }
96
97 // const lastBackup = new Date(lastBackupDate);
98 // const now = new Date();
99 // const timeDiff = now.getTime() - lastBackup.getTime();
100
101 // if (frequency === "daily") {
102 // // Check if 24 hours have passed
103 // const oneDay = 24 * 60 * 60 * 1000;
104 // if (timeDiff >= oneDay) {
105 // await performBackup();
106 // }
107 // } else if (frequency === "weekly") {
108 // // Check if 7 days have passed
109 // const oneWeek = 7 * 24 * 60 * 60 * 1000;
110 // if (timeDiff >= oneWeek) {
111 // await performBackup();
112 // }
113 // }
114 // } catch (error) {
115 // console.error("Error in automatic backup check:", error);
116 // }
117 // };
118
119 // const performBackup = async () => {
120 // try {
121 // console.log("Automatic backup due, starting backup...");
122 // const manager = new BackupAgent(agent);
123 // await manager.startBackup();
124
125 // // Update the last backup date
126 // await settingsManager.setLastBackupDate(new Date().toISOString());
127
128 // console.log("Automatic backup completed successfully");
129 // } catch (error) {
130 // console.error("Automatic backup failed:", error);
131 // }
132 // };
133
134 // // Check immediately when authenticated
135 // checkAndPerformBackup();
136
137 // // Set up interval to check every hour
138 // intervalId = setInterval(checkAndPerformBackup, 60 * 60 * 1000);
139
140 // return () => {
141 // if (intervalId) {
142 // clearInterval(intervalId);
143 // }
144 // };
145 // }, [isAuthenticated, agent]);
146
147 useEffect(() => {
148 const checkUpdates = async () => {
149 const update = await check();
150 if (update) {
151 console.log(
152 `found update ${update.version} from ${update.date} with notes ${update.body}`
153 );
154 setUpdate(update);
155 } else {
156 console.log("no updates");
157 }
158 };
159
160 checkUpdates();
161 const unlistenVisible = appWindow.listen("tauri://focus", () => {
162 checkUpdates();
163 });
164
165 return () => {
166 // Cleanup listeners
167 unlistenVisible.then((unlisten) => unlisten());
168 };
169 }, []);
170
171 return (
172 <>
173 <div className="titlebar hide-scroll" data-tauri-drag-region>
174 <div className="controls pr-[4px]">
175 <Button
176 variant="ghost"
177 id="titlebar-minimize"
178 title="minimize"
179 onClick={() => {
180 appWindow.minimize();
181 }}
182 >
183 <svg
184 xmlns="http://www.w3.org/2000/svg"
185 width="24"
186 height="24"
187 viewBox="0 0 24 24"
188 >
189 <path fill="currentColor" d="M19 13H5v-2h14z" />
190 </svg>
191 </Button>
192 <Button
193 id="titlebar-maximize"
194 title="maximize"
195 onClick={() => {
196 appWindow.toggleMaximize();
197 }}
198 >
199 <svg
200 xmlns="http://www.w3.org/2000/svg"
201 width="24"
202 height="24"
203 viewBox="0 0 24 24"
204 >
205 <path fill="currentColor" d="M4 4h16v16H4zm2 4v10h12V8z" />
206 </svg>
207 </Button>
208 <Button
209 id="titlebar-close"
210 title="close"
211 onClick={() => {
212 appWindow.hide();
213 }}
214 >
215 <svg
216 xmlns="http://www.w3.org/2000/svg"
217 width="24"
218 height="24"
219 viewBox="0 0 24 24"
220 >
221 <path
222 fill="currentColor"
223 d="M13.46 12L19 17.54V19h-1.46L12 13.46L6.46 19H5v-1.46L10.54 12L5 6.46V5h1.46L12 10.54L17.54 5H19v1.46z"
224 />
225 </svg>
226 </Button>
227 </div>
228 </div>
229 <div className="flex flex-col h-screen overflow-hidden">
230 <main className="flex-1 overflow-y-auto custom-scroll">
231 <Dialog
232 open={update != null}
233 onOpenChange={(it) => {
234 if (it == false) setUpdate(null);
235 }}
236 >
237 {/* <DialogTrigger>Open</DialogTrigger> */}
238 <DialogContent>
239 <DialogHeader>
240 <DialogTitle>
241 New update available ({update?.currentVersion} ➜{" "}
242 {update?.version})
243 </DialogTitle>
244 <DialogDescription>
245 <MarkdownRenderer
246 children={update?.body ?? "No details provided"}
247 />
248 </DialogDescription>
249 <DialogFooter className="mt-4">
250 {downloadProgress == null ? (
251 <>
252 <DialogClose asChild className="cursor-pointer">
253 <Button variant="outline">Skip</Button>
254 </DialogClose>
255 <Button
256 className="cursor-pointer"
257 onClick={async () => {
258 if (update == null) toast("Failed: update not found");
259 toast("Downloading new update...");
260 let downloaded = 0;
261 let contentLength = 0;
262 // alternatively we could also call update.download() and update.install() separately
263 await update!!.downloadAndInstall((event) => {
264 switch (event.event) {
265 case "Started":
266 //@ts-expect-error
267 contentLength = event.data.contentLength;
268 setDownloadProgress(0);
269 console.log(
270 `started downloading ${event.data.contentLength} bytes`
271 );
272 break;
273 case "Progress":
274 downloaded += event.data.chunkLength;
275 setDownloadProgress(downloaded / contentLength);
276 console.log(
277 `downloaded ${downloaded} from ${contentLength}`
278 );
279 break;
280 case "Finished":
281 setDownloadProgress(100);
282 console.log("download finished");
283 break;
284 }
285 });
286
287 toast("Update ready, restarting...");
288 await relaunch();
289 }}
290 >
291 Download
292 </Button>
293 </>
294 ) : (
295 <Progress value={downloadProgress} className="w-full" />
296 )}
297 </DialogFooter>
298 </DialogHeader>
299 </DialogContent>
300 </Dialog>
301
302 <ScrollArea>
303 {isLoading || !isLocalStorageReady ? (
304 <div className="fixed inset-0 flex items-center justify-center">
305 <LoaderCircleIcon className="animate-spin text-white/80" />
306 </div>
307 ) : isAuthenticated ? (
308 <Home profile={profile!!} onLogout={logout} />
309 ) : (
310 <LoginPage onLogin={login} client={client} />
311 )}
312 </ScrollArea>
313
314 <Toaster />
315 </main>
316 </div>
317 </>
318 );
319}
320
321function App() {
322 return (
323 <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
324 <AuthProvider>
325 <AppContent />
326 </AuthProvider>
327 </ThemeProvider>
328 );
329}
330
331export default App;