One-click backups for AT Protocol

feat: add open in browser

Turtlepaw 29040224 227ed58d

Changed files
+69 -5
src
routes
+69 -5
src/routes/Home.tsx
··· 13 import { createBackupDir, getBackupDir } from "@/lib/paths"; 14 import { settingsManager } from "@/lib/settings"; 15 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 16 - import { openPath } from "@tauri-apps/plugin-opener"; 17 import { 18 ChevronDown, 19 FileText, ··· 25 LoaderCircleIcon, 26 Package, 27 Settings as SettingsIcon, 28 User, 29 Users, 30 } from "lucide-react"; 31 import { useEffect, useRef, useState } from "react"; 32 import { toast } from "sonner"; 33 import Settings from "./Settings"; 34 35 export function Home({ 36 profile, ··· 451 <div className="flex items-center justify-between mb-2"> 452 <div className="flex items-center gap-2"> 453 {getRecordTypeIcon(type)} 454 - <span className="text-white/80 text-sm font-medium"> 455 - {type} 456 - </span> 457 </div> 458 <div className="flex items-center gap-2"> 459 <span className="text-white font-semibold"> 460 {count.toLocaleString()} 461 </span> 462 - <span className="text-white/60 text-xs"> 463 ({percentage.toFixed(1)}%) 464 </span> 465 </div> ··· 479 </div> 480 ); 481 }
··· 13 import { createBackupDir, getBackupDir } from "@/lib/paths"; 14 import { settingsManager } from "@/lib/settings"; 15 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 16 + import { openPath, openUrl } from "@tauri-apps/plugin-opener"; 17 import { 18 ChevronDown, 19 FileText, ··· 25 LoaderCircleIcon, 26 Package, 27 Settings as SettingsIcon, 28 + SquareArrowOutUpRight, 29 User, 30 Users, 31 } from "lucide-react"; 32 import { useEffect, useRef, useState } from "react"; 33 import { toast } from "sonner"; 34 import Settings from "./Settings"; 35 + import { 36 + Dialog, 37 + DialogClose, 38 + DialogContent, 39 + DialogDescription, 40 + DialogFooter, 41 + DialogHeader, 42 + DialogTitle, 43 + DialogTrigger, 44 + } from "@/components/ui/dialog"; 45 46 export function Home({ 47 profile, ··· 462 <div className="flex items-center justify-between mb-2"> 463 <div className="flex items-center gap-2"> 464 {getRecordTypeIcon(type)} 465 + <Dialog> 466 + <DialogTrigger className="gap-2 p-0 text-inherit hover:underline flex items-center cursor-pointer"> 467 + <span className="text-white/80 text-sm font-medium"> 468 + {type} 469 + </span> 470 + </DialogTrigger> 471 + <DialogContent> 472 + <DialogHeader> 473 + <DialogTitle> 474 + Open link in browser 475 + </DialogTitle> 476 + <DialogDescription> 477 + This will open the URL in your browser: 478 + <div className="border-[1.8px] border-[--secondary] rounded-sm px-3 py-2 mt-3 text-base"> 479 + https:// 480 + <span className="font-semibold"> 481 + {getUrlFromNamespace(type)} 482 + </span> 483 + </div> 484 + </DialogDescription> 485 + <DialogFooter> 486 + <DialogClose> 487 + <Button 488 + variant="outline" 489 + className="cursor-pointer" 490 + > 491 + Cancel 492 + </Button> 493 + </DialogClose> 494 + <DialogClose> 495 + <Button 496 + onClick={() => 497 + openUrl( 498 + `https://${getUrlFromNamespace( 499 + type 500 + )}` 501 + ) 502 + } 503 + className="cursor-pointer" 504 + > 505 + Open{" "} 506 + <SquareArrowOutUpRight size={14} /> 507 + </Button> 508 + </DialogClose> 509 + </DialogFooter> 510 + </DialogHeader> 511 + </DialogContent> 512 + </Dialog> 513 </div> 514 <div className="flex items-center gap-2"> 515 <span className="text-white font-semibold"> 516 {count.toLocaleString()} 517 </span> 518 + <span className="text-white/60 text-sm"> 519 ({percentage.toFixed(1)}%) 520 </span> 521 </div> ··· 535 </div> 536 ); 537 } 538 + 539 + function getUrlFromNamespace(type: string) { 540 + const parts = type.split("."); 541 + if (parts.length >= 2) { 542 + return `${parts[1]}.${parts[0]}`; 543 + } 544 + return type; // Not enough parts to swap 545 + }