One-click backups for AT Protocol

fix: logins and single instance and stats

Turtlepaw 7297128f a1421733

Changed files
+135 -57
src
components
lib
routes
src-tauri
+5
bun.lock
··· 16 16 "@radix-ui/react-scroll-area": "^1.2.9", 17 17 "@radix-ui/react-slot": "^1.2.3", 18 18 "@radix-ui/react-switch": "^1.2.5", 19 + "@radix-ui/react-tooltip": "^1.2.7", 19 20 "@tailwindcss/vite": "^4.1.11", 20 21 "@tauri-apps/api": "^2", 21 22 "@tauri-apps/plugin-autostart": "~2", ··· 460 461 461 462 "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="], 462 463 464 + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw=="], 465 + 463 466 "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], 464 467 465 468 "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], ··· 477 480 "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], 478 481 479 482 "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], 483 + 484 + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], 480 485 481 486 "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], 482 487
+4 -2
package.json
··· 22 22 "@radix-ui/react-scroll-area": "^1.2.9", 23 23 "@radix-ui/react-slot": "^1.2.3", 24 24 "@radix-ui/react-switch": "^1.2.5", 25 + "@radix-ui/react-tooltip": "^1.2.7", 25 26 "@tailwindcss/vite": "^4.1.11", 26 27 "@tauri-apps/api": "^2", 27 28 "@tauri-apps/plugin-autostart": "~2", ··· 60 61 }, 61 62 "trustedDependencies": [ 62 63 "@tailwindcss/oxide" 63 - ] 64 - } 64 + ], 65 + "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" 66 + }
+3 -4
src-tauri/Cargo.toml
··· 16 16 17 17 [build-dependencies] 18 18 tauri-build = { version = "2", features = [] } 19 - #tauri = { version = "2.0.0", features = [ "tray-icon", "api-all", "devtools" ] } 19 + #tauri = { version = "2.0.0", features = [ "tray-i+con", "api-all", "devtools" ] } 20 20 21 21 [dependencies] 22 22 tauri = { version = "2", features = ["tray-icon", "devtools"] } ··· 31 31 tokio = { version = "1", features = ["full"] } 32 32 chrono = { version = "0.4", features = ["serde"] } 33 33 tauri-plugin-websocket = "2" 34 - 35 - [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies] 36 - tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] } 34 + tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } 37 35 38 36 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 39 37 tauri-plugin-autostart = "2" 38 + tauri-plugin-single-instance = "2" 40 39 tauri-plugin-updater = "2" 41 40 42 41 [profile.release]
+1 -1
src-tauri/src/background.rs
··· 2 2 use serde_json::json; 3 3 use std::sync::Arc; 4 4 use std::time::{Duration, Instant}; 5 - use tauri::{AppHandle, Emitter, Manager, State}; 5 + use tauri::{AppHandle, Emitter}; 6 6 use tauri_plugin_store::StoreExt; 7 7 use tokio::sync::Mutex; 8 8 use tokio::time::sleep;
+48 -48
src-tauri/src/lib.rs
··· 17 17 18 18 #[cfg_attr(mobile, tauri::mobile_entry_point)] 19 19 pub fn run() { 20 - let mut builder = tauri::Builder::default() 20 + let builder = tauri::Builder::default() 21 21 .plugin(tauri_plugin_websocket::init()) 22 22 .plugin(tauri_plugin_shell::init()) 23 23 .plugin(tauri_plugin_process::init()) ··· 30 30 .plugin(tauri_plugin_store::Builder::new().build()) 31 31 .plugin(tauri_plugin_deep_link::init()) 32 32 .plugin(tauri_plugin_opener::init()) 33 + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { 34 + println!("A new app instance was opened with {argv:?} and the deep link event was already triggered."); 35 + 36 + let _ = app.get_webview_window("main") 37 + .expect("no main window") 38 + .set_focus(); 39 + })) 33 40 .invoke_handler(tauri::generate_handler![ 34 41 greet, 35 42 start_background_scheduler, 36 43 stop_background_scheduler 37 44 ]) 38 - .on_menu_event(|app, event| match event.id.as_ref() { 39 - "quit" => { 40 - std::process::exit(0); 41 - } 42 - "show" => { 43 - let window = app.get_webview_window("main").unwrap(); 44 - window.show().unwrap(); 45 - window.set_focus().unwrap(); 46 - } 47 - "hide" => { 48 - let window = app.get_webview_window("main").unwrap(); 49 - window.hide().unwrap(); 50 - } 51 - "backup_now" => { 52 - // Emit event to trigger backup 53 - app.emit("perform-backup", ()).unwrap(); 54 - } 55 - _ => { 56 - println!("menu item {:?} not handled", event.id); 57 - } 58 - }) 59 - .on_tray_icon_event(|tray, event| match event { 60 - TrayIconEvent::Click { 61 - button: MouseButton::Left, 62 - button_state: MouseButtonState::Up, 63 - .. 64 - } => { 65 - println!("left click pressed and released"); 66 - // in this example, let's show and focus the main window when the tray is clicked 67 - let app = tray.app_handle(); 68 - if let Some(window) = app.get_webview_window("main") { 69 - let _ = window.show(); 70 - let _ = window.set_focus(); 71 - } 72 - } 73 - _ => { 74 - println!("unhandled event {event:?}"); 75 - } 76 - }) 45 + // .on_menu_event(|app, event| match event.id.as_ref() { 46 + // "quit" => { 47 + // std::process::exit(0); 48 + // } 49 + // "show" => { 50 + // let window = app.get_webview_window("main").unwrap(); 51 + // window.show().unwrap(); 52 + // window.set_focus().unwrap(); 53 + // } 54 + // "hide" => { 55 + // let window = app.get_webview_window("main").unwrap(); 56 + // window.hide().unwrap(); 57 + // } 58 + // "backup_now" => { 59 + // // Emit event to trigger backup 60 + // app.emit("perform-backup", ()).unwrap(); 61 + // } 62 + // _ => { 63 + // println!("menu item {:?} not handled", event.id); 64 + // } 65 + // }) 66 + // .on_tray_icon_event(|tray, event| match event { 67 + // TrayIconEvent::Click { 68 + // button: MouseButton::Left, 69 + // button_state: MouseButtonState::Up, 70 + // .. 71 + // } => { 72 + // println!("left click pressed and released"); 73 + // // in this example, let's show and focus the main window when the tray is clicked 74 + // let app = tray.app_handle(); 75 + // if let Some(window) = app.get_webview_window("main") { 76 + // let _ = window.show(); 77 + // let _ = window.set_focus(); 78 + // } 79 + // } 80 + // _ => { 81 + // println!("unhandled event {event:?}"); 82 + // } 83 + // }) 77 84 .setup(|app| { 78 85 #[cfg(any(windows, target_os = "linux"))] 79 86 { 80 87 app.deep_link().register_all()?; 81 88 } 82 - let tray = create_system_tray(app); 89 + //let tray = create_system_tray(app); 83 90 84 91 Ok(()) 85 92 }); 86 - 87 - #[cfg(desktop)] 88 - { 89 - builder = builder.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| { 90 - println!("A new app instance was opened with {argv:?} and the deep link event was already triggered."); 91 - })); 92 - } 93 93 94 94 builder 95 95 .run(tauri::generate_context!())
+61
src/components/ui/tooltip.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function TooltipProvider({ 9 + delayDuration = 0, 10 + ...props 11 + }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { 12 + return ( 13 + <TooltipPrimitive.Provider 14 + data-slot="tooltip-provider" 15 + delayDuration={delayDuration} 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + function Tooltip({ 22 + ...props 23 + }: React.ComponentProps<typeof TooltipPrimitive.Root>) { 24 + return ( 25 + <TooltipProvider> 26 + <TooltipPrimitive.Root data-slot="tooltip" {...props} /> 27 + </TooltipProvider> 28 + ); 29 + } 30 + 31 + function TooltipTrigger({ 32 + ...props 33 + }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { 34 + return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; 35 + } 36 + 37 + function TooltipContent({ 38 + className, 39 + sideOffset = 0, 40 + children, 41 + ...props 42 + }: React.ComponentProps<typeof TooltipPrimitive.Content>) { 43 + return ( 44 + <TooltipPrimitive.Portal> 45 + <TooltipPrimitive.Content 46 + data-slot="tooltip-content" 47 + sideOffset={sideOffset} 48 + className={cn( 49 + "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", 50 + className 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> 56 + </TooltipPrimitive.Content> 57 + </TooltipPrimitive.Portal> 58 + ); 59 + } 60 + 61 + export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+2 -2
src/lib/stats.ts
··· 13 13 export async function getCarStats(carData: Uint8Array): Promise<CarStats> { 14 14 try { 15 15 // Parse the CAR file 16 - await using repo = RepoReader.fromUint8Array(carData); 16 + const repo = RepoReader.fromUint8Array(carData); 17 17 18 18 let totalBlocks = 0; 19 19 let totalSize = 0; ··· 29 29 // Try to decode the block as a record 30 30 if (record) { 31 31 // Count different record types 32 - const type = (record.record as any)["$type"] 32 + const type = (record.record as any)["$type"]; 33 33 if (type) { 34 34 recordTypes[type] = (recordTypes[type] || 0) + 1; 35 35 recordCount++;
+10
src/routes/Home.tsx
··· 22 22 Heart, 23 23 History, 24 24 Images, 25 + Info, 25 26 LoaderCircleIcon, 26 27 Package, 27 28 Settings as SettingsIcon, ··· 31 32 import { useEffect, useRef, useState } from "react"; 32 33 import { toast } from "sonner"; 33 34 import Settings from "./Settings"; 35 + import { 36 + Tooltip, 37 + TooltipContent, 38 + TooltipTrigger, 39 + } from "@/components/ui/tooltip.tsx"; 34 40 35 41 export function Home({ 36 42 profile, ··· 288 294 <div className="flex items-center gap-2 mb-4"> 289 295 <History className="w-5 h-5" /> 290 296 <p className="text-white text-lg font-semibold">Previous backups</p> 297 + 298 + <p className="text-white/60"> 299 + (only the 3 most recent backups are saved) 300 + </p> 291 301 </div> 292 302 293 303 {backups.length === 0 ? (
+1
src/routes/Login.tsx
··· 26 26 useEffect(() => { 27 27 const initOAuthClient = async () => { 28 28 try { 29 + console.log("waiting for deep links"); 29 30 // Set up deep link handler 30 31 await onOpenUrl(async (urls) => { 31 32 console.log("deep link received:", urls);