One-click backups for AT Protocol

feat: show download progress

Turtlepaw 4ccdf747 90bb5f3e

Changed files
+112 -75
src
routes
+112 -75
src/routes/Home.tsx
··· 39 39 profile: ProfileViewDetailed; 40 40 onLogout: () => void; 41 41 }) { 42 - const [isDirLoading, setDirLoading] = useState(false); 43 42 const [refreshTrigger, setRefreshTrigger] = useState(0); 44 43 const [showSettings, setShowSettings] = useState(false); 45 44 ··· 96 95 97 96 <div className="bg-card rounded-lg p-4 mb-4"> 98 97 <p className="mb-2 text-white">Backups</p> 99 - <div className="flex gap-2"> 100 - <Button 101 - variant="outline" 102 - className="cursor-pointer" 103 - onClick={async () => { 104 - try { 105 - setDirLoading(true); 106 - await createBackupDir(); 107 - const appDataDirPath = await getBackupDir(); 108 - openPath(appDataDirPath); 109 - } finally { 110 - setDirLoading(false); 111 - } 112 - }} 113 - disabled={isDirLoading} 114 - > 115 - {isDirLoading ? ( 116 - <LoaderCircleIcon className="w-4 h-4 animate-spin mr-2" /> 117 - ) : ( 118 - <FolderOpen className="w-4 h-4 mr-2" /> 119 - )} 120 - Open backups 121 - </Button> 122 - 123 - <StartBackup onBackupComplete={handleBackupComplete} /> 124 - 125 - {/* <Button 126 - variant="outline" 127 - className="cursor-pointer" 128 - onClick={async () => { 129 - await BackgroundTestService.testBackgroundBackup(); 130 - }} 131 - > 132 - Test Background 133 - </Button> */} 134 - </div> 98 + <StartBackup onBackupComplete={handleBackupComplete} /> 135 99 </div> 136 100 137 101 <Backups refreshTrigger={refreshTrigger} /> ··· 142 106 function StartBackup({ onBackupComplete }: { onBackupComplete: () => void }) { 143 107 const [isLoading, setIsLoading] = useState(false); 144 108 const [stage, setStage] = useState<BackupStage | null>(null); 145 - const [_, setProgress] = useState<number | undefined>(); 109 + const [progress, setProgress] = useState<number | undefined>(); 110 + const [isDirLoading, setDirLoading] = useState(false); 146 111 const { agent } = useAuth(); 147 112 113 + const formatStage = (stage: BackupStage | null): string => { 114 + if (!stage) return "Initializing backup..."; 115 + 116 + switch (stage) { 117 + case "blobs": 118 + return "Downloading blobs..."; 119 + case "cleanup": 120 + return "Cleaning up..."; 121 + case "complete": 122 + return "Backup complete!"; 123 + case "fetching": 124 + return "Downloading account archive..."; 125 + case "writing": 126 + return "Downloading account archive..."; 127 + default: 128 + return "Processing..."; 129 + } 130 + }; 131 + 148 132 return ( 149 - <Button 150 - variant="outline" 151 - className="cursor-pointer" 152 - onClick={async () => { 153 - try { 154 - setIsLoading(true); 155 - if (agent == null) { 156 - toast("Agent not initialized, try to reload the app."); 157 - return; 158 - } 159 - const manager = new BackupAgent(agent!!, { 160 - onProgress: (progress) => { 161 - setStage(progress.stage); 162 - setProgress(progress.progress); 163 - }, 164 - }); 165 - await manager.startBackup(); 166 - await settingsManager.setLastBackupDate(new Date().toISOString()); 167 - toast("Backup complete!"); 168 - onBackupComplete(); // Trigger refresh 169 - } catch (err: any) { 170 - toast(err.toString()); 171 - console.error(err); 172 - } finally { 173 - setIsLoading(false); 174 - } 175 - }} 176 - disabled={isLoading} 177 - > 178 - {isLoading ? ( 179 - <> 180 - <LoaderCircleIcon className="animate-spin text-white/80" /> 181 - <span className="capitalize">{stage}</span> 182 - </> 183 - ) : ( 184 - <span>Backup now</span> 133 + <div className="space-y-4"> 134 + <div className="flex gap-2"> 135 + <Button 136 + variant="outline" 137 + className="cursor-pointer" 138 + onClick={async () => { 139 + try { 140 + setDirLoading(true); 141 + await createBackupDir(); 142 + const appDataDirPath = await getBackupDir(); 143 + openPath(appDataDirPath); 144 + } finally { 145 + setDirLoading(false); 146 + } 147 + }} 148 + disabled={isDirLoading} 149 + > 150 + {isDirLoading ? ( 151 + <LoaderCircleIcon className="w-4 h-4 animate-spin mr-2" /> 152 + ) : ( 153 + <FolderOpen className="w-4 h-4 mr-2" /> 154 + )} 155 + Open backups 156 + </Button> 157 + 158 + <Button 159 + variant="outline" 160 + className="cursor-pointer" 161 + onClick={async () => { 162 + try { 163 + setIsLoading(true); 164 + if (agent == null) { 165 + toast("Agent not initialized, try to reload the app."); 166 + return; 167 + } 168 + 169 + const manager = new BackupAgent(agent!!, { 170 + onProgress: (progress) => { 171 + setStage(progress.stage); 172 + setProgress(progress.progress); 173 + }, 174 + }); 175 + await manager.startBackup(); 176 + await settingsManager.setLastBackupDate(new Date().toISOString()); 177 + toast("Backup complete!"); 178 + onBackupComplete(); 179 + } catch (err: any) { 180 + toast(err.toString()); 181 + console.error(err); 182 + } finally { 183 + setIsLoading(false); 184 + setStage(null); 185 + setProgress(undefined); 186 + } 187 + }} 188 + disabled={isLoading} 189 + > 190 + {isLoading ? ( 191 + <> 192 + <LoaderCircleIcon className="w-4 h-4 animate-spin mr-2" /> 193 + Backing up... 194 + </> 195 + ) : ( 196 + "Backup now" 197 + )} 198 + </Button> 199 + </div> 200 + 201 + {/* Clean backup progress card with animations */} 202 + {isLoading && ( 203 + <div className="bg-card border rounded-lg p-4 animate-in slide-in-from-top-2 duration-300"> 204 + <div className="flex items-center justify-between mb-3"> 205 + <div className="flex items-center gap-3"> 206 + <div className="w-8 h-8 rounded-md bg-primary/10 flex items-center justify-center"> 207 + <HardDrive className="w-4 h-4 text-primary animate-pulse" /> 208 + </div> 209 + <div> 210 + <h3 className="font-medium">{formatStage(stage)}</h3> 211 + </div> 212 + </div> 213 + </div> 214 + 215 + <div className="space-y-1.5"> 216 + <Progress 217 + value={progress} 218 + className="h-2 transition-all duration-500 ease-out" 219 + /> 220 + </div> 221 + </div> 185 222 )} 186 - </Button> 223 + </div> 187 224 ); 188 225 } 189 226