1import { RefreshIcon } from "~/components/ui/refresh-icon";
2import { CheckIcon, DownloadIcon, RotateCwIcon } from "lucide-react";
3
4const DOWNLOAD_PROGRESS_RADIUS = 14;
5const DOWNLOAD_PROGRESS_CIRCUMFERENCE = 2 * Math.PI * DOWNLOAD_PROGRESS_RADIUS;
6
7export type DesktopUpdateStatusIconState =
8 | "idle"
9 | "checking"
10 | "available"
11 | "downloading"
12 | "downloaded";
13
14function normalizeDesktopUpdateDownloadPercent(percent: number | null): number {
15 if (percent === null || !Number.isFinite(percent)) return 0;
16 return Math.min(100, Math.max(0, percent));
17}
18
19function DesktopUpdateAvailableIcon() {
20 return (
21 <span className="relative grid size-4 place-items-center">
22 <DownloadIcon className="size-4" />
23 <span
24 aria-hidden="true"
25 className="absolute -top-0.5 -right-0.5 size-1.5 rounded-full bg-current ring-2 ring-sidebar-control-surface"
26 />
27 </span>
28 );
29}
30
31function DesktopUpdateDownloadingIcon({ percent }: { readonly percent: number | null }) {
32 const normalizedPercent = normalizeDesktopUpdateDownloadPercent(percent);
33 const progressOffset = DOWNLOAD_PROGRESS_CIRCUMFERENCE * (1 - normalizedPercent / 100);
34
35 return (
36 <span className="relative grid size-8 place-items-center">
37 <svg
38 aria-hidden="true"
39 className="pointer-events-none absolute inset-0 size-full -rotate-90"
40 viewBox="0 0 32 32"
41 >
42 <circle
43 cx="16"
44 cy="16"
45 r={DOWNLOAD_PROGRESS_RADIUS}
46 fill="none"
47 className="stroke-current/22"
48 strokeWidth="1.5"
49 />
50 <circle
51 cx="16"
52 cy="16"
53 r={DOWNLOAD_PROGRESS_RADIUS}
54 fill="none"
55 stroke="currentColor"
56 strokeDasharray={DOWNLOAD_PROGRESS_CIRCUMFERENCE}
57 strokeDashoffset={progressOffset}
58 strokeLinecap="round"
59 strokeWidth="1.5"
60 className="transition-[stroke-dashoffset] duration-300 ease-out motion-reduce:transition-none"
61 />
62 </svg>
63 <DownloadIcon className="size-4" />
64 </span>
65 );
66}
67
68function DesktopUpdateDownloadedIcon() {
69 return (
70 <span className="relative grid size-4 place-items-center">
71 <RotateCwIcon className="size-4" />
72 <span className="absolute -right-1 -bottom-1 grid size-2.5 place-items-center rounded-full bg-foreground text-background ring-2 ring-background">
73 <CheckIcon className="size-2" strokeWidth={3} />
74 </span>
75 </span>
76 );
77}
78
79export function DesktopUpdateStatusIcon({
80 downloadPercent,
81 isCheckAnimating,
82 status,
83}: {
84 readonly downloadPercent?: number | null;
85 readonly isCheckAnimating?: boolean;
86 readonly status: DesktopUpdateStatusIconState;
87}) {
88 if (status === "available") return <DesktopUpdateAvailableIcon />;
89 if (status === "downloading") {
90 return <DesktopUpdateDownloadingIcon percent={downloadPercent ?? null} />;
91 }
92 if (status === "downloaded") return <DesktopUpdateDownloadedIcon />;
93
94 return (
95 <RefreshIcon
96 className="size-4"
97 refreshing={status === "checking" && isCheckAnimating === true}
98 />
99 );
100}