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;