1+ import React , { useState , useEffect } from 'react' ;
2+ import {
3+ AlertDialog ,
4+ AlertDialogAction ,
5+ AlertDialogCancel ,
6+ AlertDialogContent ,
7+ AlertDialogDescription ,
8+ AlertDialogFooter ,
9+ AlertDialogHeader ,
10+ AlertDialogTitle ,
11+ } from './ui/alert-dialog' ;
12+ import { Progress } from './ui/progress' ;
13+ import { Button } from './ui/button' ;
14+ import { Download , RefreshCw , CheckCircle } from 'lucide-react' ;
15+ import { api } from '@/trpc/react' ;
16+ import { toast } from 'sonner' ;
17+
18+ interface UpdateDialogProps {
19+ isOpen : boolean ;
20+ onClose : ( ) => void ;
21+ updateInfo ?: {
22+ version : string ;
23+ releaseNotes ?: string ;
24+ } ;
25+ }
26+
27+ export function UpdateDialog ( { isOpen, onClose, updateInfo } : UpdateDialogProps ) {
28+ const [ isDownloading , setIsDownloading ] = useState ( false ) ;
29+ const [ downloadProgress , setDownloadProgress ] = useState ( 0 ) ;
30+
31+ // tRPC queries for update status
32+ const isCheckingQuery = api . updater . isCheckingForUpdate . useQuery ( undefined , {
33+ enabled : isOpen ,
34+ refetchInterval : isOpen ? 1000 : false , // Poll every second when dialog is open
35+ } ) ;
36+ const isUpdateAvailableQuery = api . updater . isUpdateAvailable . useQuery ( undefined , {
37+ enabled : isOpen ,
38+ refetchInterval : isOpen ? 1000 : false ,
39+ } ) ;
40+
41+ const utils = api . useUtils ( ) ;
42+
43+ // tRPC mutations
44+ const checkForUpdatesMutation = api . updater . checkForUpdates . useMutation ( {
45+ onSuccess : ( ) => {
46+ toast . success ( 'Update check completed' ) ;
47+ utils . updater . isUpdateAvailable . invalidate ( ) ;
48+ utils . updater . isCheckingForUpdate . invalidate ( ) ;
49+ } ,
50+ onError : ( error ) => {
51+ console . error ( 'Error checking for updates:' , error ) ;
52+ toast . error ( 'Failed to check for updates' ) ;
53+ }
54+ } ) ;
55+
56+ const downloadUpdateMutation = api . updater . downloadUpdate . useMutation ( {
57+ onSuccess : ( ) => {
58+ toast . success ( 'Update download started' ) ;
59+ } ,
60+ onError : ( error ) => {
61+ console . error ( 'Error downloading update:' , error ) ;
62+ toast . error ( 'Failed to download update' ) ;
63+ setIsDownloading ( false ) ;
64+ }
65+ } ) ;
66+
67+ const quitAndInstallMutation = api . updater . quitAndInstall . useMutation ( {
68+ onError : ( error ) => {
69+ console . error ( 'Error installing update:' , error ) ;
70+ toast . error ( 'Failed to install update' ) ;
71+ }
72+ } ) ;
73+
74+ // Get status from queries
75+ const isCheckingForUpdates = isCheckingQuery . data || false ;
76+ const updateAvailable = isUpdateAvailableQuery . data || false ;
77+
78+ // Subscribe to download progress via tRPC
79+ api . updater . onDownloadProgress . useSubscription ( undefined , {
80+ enabled : isOpen && isDownloading ,
81+ onData : ( progress ) => {
82+ setDownloadProgress ( Math . round ( progress . percent || 0 ) ) ;
83+ } ,
84+ onError : ( error ) => {
85+ console . error ( 'Download progress subscription error:' , error ) ;
86+ }
87+ } ) ;
88+
89+ const handleCheckForUpdates = async ( ) => {
90+ checkForUpdatesMutation . mutate ( { userInitiated : true } ) ;
91+ } ;
92+
93+ const handleDownloadUpdate = async ( ) => {
94+ setIsDownloading ( true ) ;
95+ setDownloadProgress ( 0 ) ;
96+ downloadUpdateMutation . mutate ( ) ;
97+ } ;
98+
99+ const handleInstallUpdate = async ( ) => {
100+ quitAndInstallMutation . mutate ( ) ;
101+ } ;
102+
103+ if ( ! updateAvailable && ! isCheckingForUpdates && ! isDownloading ) {
104+ return (
105+ < AlertDialog open = { isOpen } onOpenChange = { onClose } >
106+ < AlertDialogContent >
107+ < AlertDialogHeader >
108+ < AlertDialogTitle className = "flex items-center gap-2" >
109+ < RefreshCw className = "h-5 w-5" />
110+ Check for Updates
111+ </ AlertDialogTitle >
112+ < AlertDialogDescription >
113+ Click below to check for the latest version of Amical.
114+ </ AlertDialogDescription >
115+ </ AlertDialogHeader >
116+ < AlertDialogFooter >
117+ < AlertDialogCancel onClick = { onClose } > Cancel</ AlertDialogCancel >
118+ < AlertDialogAction onClick = { handleCheckForUpdates } >
119+ Check for Updates
120+ </ AlertDialogAction >
121+ </ AlertDialogFooter >
122+ </ AlertDialogContent >
123+ </ AlertDialog >
124+ ) ;
125+ }
126+
127+ if ( isCheckingForUpdates ) {
128+ return (
129+ < AlertDialog open = { isOpen } onOpenChange = { onClose } >
130+ < AlertDialogContent >
131+ < AlertDialogHeader >
132+ < AlertDialogTitle className = "flex items-center gap-2" >
133+ < RefreshCw className = "h-5 w-5 animate-spin" />
134+ Checking for Updates...
135+ </ AlertDialogTitle >
136+ < AlertDialogDescription >
137+ Please wait while we check for the latest version.
138+ </ AlertDialogDescription >
139+ </ AlertDialogHeader >
140+ < AlertDialogFooter >
141+ < AlertDialogCancel onClick = { onClose } > Cancel</ AlertDialogCancel >
142+ </ AlertDialogFooter >
143+ </ AlertDialogContent >
144+ </ AlertDialog >
145+ ) ;
146+ }
147+
148+ if ( isDownloading ) {
149+ return (
150+ < AlertDialog open = { isOpen } onOpenChange = { ( ) => { } } >
151+ < AlertDialogContent >
152+ < AlertDialogHeader >
153+ < AlertDialogTitle className = "flex items-center gap-2" >
154+ < Download className = "h-5 w-5" />
155+ Downloading Update...
156+ </ AlertDialogTitle >
157+ < AlertDialogDescription >
158+ { updateInfo ?. version && (
159+ < > Downloading version { updateInfo . version } . Please wait...</ >
160+ ) }
161+ </ AlertDialogDescription >
162+ </ AlertDialogHeader >
163+ < div className = "py-4" >
164+ < Progress value = { downloadProgress } className = "w-full" />
165+ < p className = "text-sm text-muted-foreground mt-2 text-center" >
166+ { downloadProgress } % complete
167+ </ p >
168+ </ div >
169+ < AlertDialogFooter >
170+ < Button variant = "outline" disabled >
171+ Downloading...
172+ </ Button >
173+ </ AlertDialogFooter >
174+ </ AlertDialogContent >
175+ </ AlertDialog >
176+ ) ;
177+ }
178+
179+ if ( downloadProgress === 100 && ! isDownloading ) {
180+ return (
181+ < AlertDialog open = { isOpen } onOpenChange = { ( ) => { } } >
182+ < AlertDialogContent >
183+ < AlertDialogHeader >
184+ < AlertDialogTitle className = "flex items-center gap-2" >
185+ < CheckCircle className = "h-5 w-5 text-green-500" />
186+ Update Ready
187+ </ AlertDialogTitle >
188+ < AlertDialogDescription >
189+ { updateInfo ?. version && (
190+ < >
191+ Version { updateInfo . version } has been downloaded and is ready to install.
192+ The app will restart to complete the installation.
193+ </ >
194+ ) }
195+ </ AlertDialogDescription >
196+ </ AlertDialogHeader >
197+ < AlertDialogFooter >
198+ < AlertDialogCancel onClick = { onClose } > Install Later</ AlertDialogCancel >
199+ < AlertDialogAction onClick = { handleInstallUpdate } >
200+ Restart & Install
201+ </ AlertDialogAction >
202+ </ AlertDialogFooter >
203+ </ AlertDialogContent >
204+ </ AlertDialog >
205+ ) ;
206+ }
207+
208+ return (
209+ < AlertDialog open = { isOpen } onOpenChange = { onClose } >
210+ < AlertDialogContent >
211+ < AlertDialogHeader >
212+ < AlertDialogTitle className = "flex items-center gap-2" >
213+ < Download className = "h-5 w-5" />
214+ Update Available
215+ </ AlertDialogTitle >
216+ < AlertDialogDescription >
217+ { updateInfo ?. version && (
218+ < >
219+ A new version ({ updateInfo . version } ) is available for download.
220+ { updateInfo . releaseNotes && (
221+ < div className = "mt-2 p-2 bg-muted rounded text-sm" >
222+ { updateInfo . releaseNotes }
223+ </ div >
224+ ) }
225+ </ >
226+ ) }
227+ </ AlertDialogDescription >
228+ </ AlertDialogHeader >
229+ < AlertDialogFooter >
230+ < AlertDialogCancel onClick = { onClose } > Later</ AlertDialogCancel >
231+ < AlertDialogAction onClick = { handleDownloadUpdate } >
232+ Download Now
233+ </ AlertDialogAction >
234+ </ AlertDialogFooter >
235+ </ AlertDialogContent >
236+ </ AlertDialog >
237+ ) ;
238+ }
0 commit comments