Skip to content

Commit 2f0717b

Browse files
Merge pull request #196 from nomandhoni-cs/develop
feat: add version display and update checker to app
2 parents f7257aa + df93bce commit 2f0717b

2 files changed

Lines changed: 233 additions & 75 deletions

File tree

src/components/UpdateChecker.tsx

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { useState, useEffect } from "react";
2+
import { check } from "@tauri-apps/plugin-updater";
3+
import { relaunch } from "@tauri-apps/plugin-process";
4+
import { getVersion } from "@tauri-apps/api/app";
5+
import toast from "react-hot-toast";
6+
import {
7+
FaCheckCircle,
8+
FaSyncAlt,
9+
FaRocket,
10+
FaCloudDownloadAlt,
11+
} from "react-icons/fa";
12+
13+
interface UpdateCheckerProps {
14+
onUpdateAvailable?: (version: string) => void;
15+
}
16+
17+
export function UpdateChecker({ onUpdateAvailable }: UpdateCheckerProps) {
18+
const [version, setVersion] = useState<string>("");
19+
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
20+
const [isDownloading, setIsDownloading] = useState(false);
21+
const [downloadProgress, setDownloadProgress] = useState(0);
22+
const [updateAvailable, setUpdateAvailable] = useState(false);
23+
const [latestVersion, setLatestVersion] = useState<string>("");
24+
25+
useEffect(() => {
26+
getVersion().then(setVersion);
27+
// Auto-check for updates on mount (silent)
28+
checkForUpdates(true);
29+
// eslint-disable-next-line react-hooks/exhaustive-deps
30+
}, []);
31+
32+
const checkForUpdates = async (silent = false) => {
33+
setIsCheckingUpdate(true);
34+
try {
35+
const update = await check();
36+
37+
if (update) {
38+
setUpdateAvailable(true);
39+
setLatestVersion(update.version);
40+
onUpdateAvailable?.(update.version);
41+
42+
if (!silent) {
43+
toast.success(`Update available: v${update.version}`, {
44+
duration: 5000,
45+
});
46+
}
47+
} else {
48+
setUpdateAvailable(false);
49+
setLatestVersion("");
50+
51+
if (!silent) {
52+
toast.success("No updates found. You're on the latest version.");
53+
}
54+
}
55+
} catch (error) {
56+
console.error("Error checking for updates:", error);
57+
if (!silent) toast.error("Update check failed. Please try again.");
58+
} finally {
59+
setIsCheckingUpdate(false);
60+
}
61+
};
62+
63+
const downloadAndInstall = async () => {
64+
setIsDownloading(true);
65+
setDownloadProgress(0);
66+
67+
try {
68+
const update = await check();
69+
if (!update) {
70+
toast("No update available to install.");
71+
return;
72+
}
73+
74+
toast.loading(`Downloading v${update.version}...`, { id: "download" });
75+
76+
let downloaded = 0;
77+
let contentLength = 0;
78+
79+
await update.downloadAndInstall((event) => {
80+
switch (event.event) {
81+
case "Started":
82+
contentLength = event.data.contentLength ?? 0;
83+
setDownloadProgress(0);
84+
break;
85+
86+
case "Progress":
87+
downloaded += event.data.chunkLength ?? 0;
88+
if (contentLength > 0) {
89+
setDownloadProgress(Math.round((downloaded / contentLength) * 100));
90+
}
91+
break;
92+
93+
case "Finished":
94+
setDownloadProgress(100);
95+
break;
96+
}
97+
});
98+
99+
toast.success("Update installed. Restarting app…", { id: "download" });
100+
101+
setTimeout(async () => {
102+
await relaunch();
103+
}, 1000);
104+
} catch (error) {
105+
console.error("Error downloading update:", error);
106+
toast.error("Update install failed. Please try again.", { id: "download" });
107+
} finally {
108+
setIsDownloading(false);
109+
}
110+
};
111+
112+
return (
113+
<div className="px-2 pt-2 space-y-1.5">
114+
<div className="flex items-center justify-between text-[10px] text-muted-foreground/60">
115+
<span className="font-heading font-medium flex items-center gap-1">
116+
<FaCheckCircle className="text-[11px]" />
117+
<span className="text-muted-foreground/90">v{version}</span>
118+
</span>
119+
120+
<button
121+
onClick={() => checkForUpdates(false)}
122+
disabled={isCheckingUpdate}
123+
className="font-heading font-medium hover:text-primary transition-colors disabled:opacity-50 flex items-center gap-1"
124+
aria-label="Check for updates"
125+
>
126+
<FaSyncAlt className={`text-[11px] ${isCheckingUpdate ? "animate-spin" : ""}`} />
127+
{isCheckingUpdate ? "Checking…" : "Check for updates"}
128+
</button>
129+
</div>
130+
131+
{updateAvailable && (
132+
<button
133+
onClick={downloadAndInstall}
134+
disabled={isDownloading}
135+
className="w-full flex items-center justify-center gap-1.5 text-[10px] font-heading font-semibold text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 transition-colors disabled:opacity-50"
136+
aria-label={`Install update version ${latestVersion}`}
137+
>
138+
{isDownloading ? (
139+
<>
140+
<FaCloudDownloadAlt className="text-[11px]" />
141+
{downloadProgress > 0
142+
? `Downloading update… ${downloadProgress}%`
143+
: "Preparing update…"}
144+
</>
145+
) : (
146+
<>
147+
<FaRocket className="text-[11px]" />
148+
{`Install update (v${latestVersion})`}
149+
</>
150+
)}
151+
</button>
152+
)}
153+
</div>
154+
);
155+
}

src/components/app-sidebar.tsx

Lines changed: 78 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
IoCalendar,
88
IoDesktop,
99
IoSettings,
10-
IoDocumentText,
1110
IoChatbubble,
1211
IoInformationCircle,
1312
IoFlame,
1413
IoCheckmarkCircle,
1514
IoSparkles,
15+
IoKey,
1616
} from "react-icons/io5";
1717
import { motion } from "framer-motion";
1818
import {
@@ -31,6 +31,7 @@ import {
3131
import { usePremiumFeatures } from "../contexts/PremiumFeaturesContext";
3232
import { LucideListTodo } from "lucide-react";
3333
import { PiMonitorFill } from "react-icons/pi";
34+
import { UpdateChecker } from "./UpdateChecker";
3435

3536
// ── 1. Grouped Navigation Data ──
3637

@@ -47,7 +48,7 @@ const proNav = [
4748

4849
const systemNav = [
4950
{ title: "Settings", url: "/allSettings", icon: IoSettings },
50-
{ title: "Activate License", url: "/activatelicense", icon: IoDocumentText },
51+
{ title: "Activate License", url: "/activatelicense", icon: IoKey },
5152
{
5253
title: "Submit Feedback",
5354
url: "https://tally.so/r/wo0ZrN",
@@ -213,24 +214,70 @@ export function AppSidebar() {
213214

214215
{/* ── Footer ── */}
215216
<SidebarFooter className="p-2 pb-3">
216-
<SidebarMenu>
217-
{isPaidUser ? (
218-
<SidebarMenuItem>
219-
<SidebarMenuButton
220-
tooltip="Pro Active"
221-
className="relative overflow-hidden cursor-default border border-green-500/25 dark:border-green-400/20 bg-green-500/5 dark:bg-green-400/5 hover:bg-green-500/5 dark:hover:bg-green-400/5 group"
222-
>
223-
{/* Green sweep */}
217+
<SidebarMenu> {isPaidUser ? (
218+
<SidebarMenuItem>
219+
<SidebarMenuButton
220+
tooltip="Pro Active"
221+
className="relative overflow-hidden cursor-default border border-green-500/25 dark:border-green-400/20 bg-green-500/5 dark:bg-green-400/5 hover:bg-green-500/5 dark:hover:bg-green-400/5 group"
222+
>
223+
{/* Green sweep */}
224+
<motion.div
225+
className="absolute inset-0 z-0 pointer-events-none"
226+
style={{
227+
background:
228+
"linear-gradient(90deg, transparent 0%, rgba(34,197,94,0.25) 40%, rgba(74,222,128,0.35) 50%, rgba(34,197,94,0.25) 60%, transparent 100%)",
229+
backgroundSize: "200% 100%",
230+
}}
231+
animate={{ backgroundPosition: ["200% 0", "-200% 0"] }}
232+
transition={{
233+
duration: 3,
234+
repeat: Infinity,
235+
ease: "easeInOut",
236+
}}
237+
/>
238+
239+
{/* White shimmer */}
240+
<motion.div
241+
className="absolute inset-0 z-0 pointer-events-none"
242+
style={{
243+
background:
244+
"linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.12) 45%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.12) 55%, transparent 70%)",
245+
backgroundSize: "200% 100%",
246+
}}
247+
animate={{ backgroundPosition: ["200% 0", "-200% 0"] }}
248+
transition={{
249+
duration: 2.5,
250+
repeat: Infinity,
251+
ease: "easeInOut",
252+
delay: 0.8,
253+
}}
254+
/>
255+
256+
<IoCheckmarkCircle className="relative z-10 text-[1.1rem] drop-shadow-[0_0_4px_rgba(34,197,94,0.5)] shrink-0 text-green-500 dark:text-green-400" />
257+
<span className="relative z-10 font-heading text-[13px] font-semibold tracking-wide text-green-700 dark:text-green-400">
258+
Activated
259+
</span>
260+
</SidebarMenuButton>
261+
</SidebarMenuItem>
262+
) : (
263+
<SidebarMenuItem>
264+
<SidebarMenuButton
265+
asChild
266+
tooltip="Unlock Premium"
267+
className="relative overflow-hidden border border-amber-500/25 dark:border-amber-400/20 bg-amber-500/5 dark:bg-amber-400/5 hover:border-amber-500/50 dark:hover:border-amber-400/35 shadow-sm transition-all group"
268+
>
269+
<Link to="https://blinkeye.app/en/pricing" target="_blank">
270+
{/* Amber/rose sweep */}
224271
<motion.div
225272
className="absolute inset-0 z-0 pointer-events-none"
226273
style={{
227274
background:
228-
"linear-gradient(90deg, transparent 0%, rgba(34,197,94,0.25) 40%, rgba(74,222,128,0.35) 50%, rgba(34,197,94,0.25) 60%, transparent 100%)",
275+
"linear-gradient(90deg, transparent 0%, rgba(245,158,11,0.25) 35%, rgba(225,29,72,0.3) 50%, rgba(245,158,11,0.25) 65%, transparent 100%)",
229276
backgroundSize: "200% 100%",
230277
}}
231278
animate={{ backgroundPosition: ["200% 0", "-200% 0"] }}
232279
transition={{
233-
duration: 3,
280+
duration: 2.5,
234281
repeat: Infinity,
235282
ease: "easeInOut",
236283
}}
@@ -241,80 +288,36 @@ export function AppSidebar() {
241288
className="absolute inset-0 z-0 pointer-events-none"
242289
style={{
243290
background:
244-
"linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.12) 45%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.12) 55%, transparent 70%)",
291+
"linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.12) 45%, rgba(255,255,255,0.22) 50%, rgba(255,255,255,0.12) 55%, transparent 70%)",
245292
backgroundSize: "200% 100%",
246293
}}
247294
animate={{ backgroundPosition: ["200% 0", "-200% 0"] }}
248295
transition={{
249-
duration: 2.5,
296+
duration: 2,
250297
repeat: Infinity,
251298
ease: "easeInOut",
252-
delay: 0.8,
299+
delay: 0.6,
253300
}}
254301
/>
255302

256-
<IoCheckmarkCircle className="relative z-10 text-[1.1rem] drop-shadow-[0_0_4px_rgba(34,197,94,0.5)] shrink-0 text-green-500 dark:text-green-400" />
257-
<span className="relative z-10 font-heading text-[13px] font-semibold tracking-wide text-green-700 dark:text-green-400">
258-
Activated
259-
</span>
260-
</SidebarMenuButton>
261-
</SidebarMenuItem>
262-
) : (
263-
<SidebarMenuItem>
264-
<SidebarMenuButton
265-
asChild
266-
tooltip="Unlock Premium"
267-
className="relative overflow-hidden border border-amber-500/25 dark:border-amber-400/20 bg-amber-500/5 dark:bg-amber-400/5 hover:border-amber-500/50 dark:hover:border-amber-400/35 shadow-sm transition-all group"
268-
>
269-
<Link to="https://blinkeye.app/en/pricing" target="_blank">
270-
{/* Amber/rose sweep */}
271-
<motion.div
272-
className="absolute inset-0 z-0 pointer-events-none"
273-
style={{
274-
background:
275-
"linear-gradient(90deg, transparent 0%, rgba(245,158,11,0.25) 35%, rgba(225,29,72,0.3) 50%, rgba(245,158,11,0.25) 65%, transparent 100%)",
276-
backgroundSize: "200% 100%",
277-
}}
278-
animate={{ backgroundPosition: ["200% 0", "-200% 0"] }}
279-
transition={{
280-
duration: 2.5,
281-
repeat: Infinity,
282-
ease: "easeInOut",
283-
}}
284-
/>
285-
286-
{/* White shimmer */}
287-
<motion.div
288-
className="absolute inset-0 z-0 pointer-events-none"
289-
style={{
290-
background:
291-
"linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.12) 45%, rgba(255,255,255,0.22) 50%, rgba(255,255,255,0.12) 55%, transparent 70%)",
292-
backgroundSize: "200% 100%",
293-
}}
294-
animate={{ backgroundPosition: ["200% 0", "-200% 0"] }}
295-
transition={{
296-
duration: 2,
297-
repeat: Infinity,
298-
ease: "easeInOut",
299-
delay: 0.6,
300-
}}
301-
/>
302-
303-
{/* Hover glow */}
304-
<div className="absolute inset-0 z-0 bg-amber-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
303+
{/* Hover glow */}
304+
<div className="absolute inset-0 z-0 bg-amber-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
305305

306-
<IoSparkles
307-
className="relative z-10 text-[1.1rem] drop-shadow-[0_0_4px_rgba(245,158,11,0.5)] shrink-0"
308-
style={{ fill: "url(#amberGradient)" }}
309-
/>
310-
<span className="relative z-10 font-heading text-[13px] font-bold tracking-wide text-amber-700 dark:text-amber-400 group-hover:text-amber-800 dark:group-hover:text-amber-300 transition-colors">
311-
Unlock Premium
312-
</span>
313-
</Link>
314-
</SidebarMenuButton>
315-
</SidebarMenuItem>
316-
)}
306+
<IoSparkles
307+
className="relative z-10 text-[1.1rem] drop-shadow-[0_0_4px_rgba(245,158,11,0.5)] shrink-0"
308+
style={{ fill: "url(#amberGradient)" }}
309+
/>
310+
<span className="relative z-10 font-heading text-[13px] font-bold tracking-wide text-amber-700 dark:text-amber-400 group-hover:text-amber-800 dark:group-hover:text-amber-300 transition-colors">
311+
Unlock Premium
312+
</span>
313+
</Link>
314+
</SidebarMenuButton>
315+
</SidebarMenuItem>
316+
)}
317317
</SidebarMenu>
318+
319+
{/* ── Version & Update Check ── */}
320+
<UpdateChecker />
318321
</SidebarFooter>
319322
</Sidebar>
320323
</>

0 commit comments

Comments
 (0)