Feat/automatic update installation (#5345)

Signed-off-by: AdemolaAri <ademola.ari@gmail.com>
This commit is contained in:
Ademola Arigbabuwo
2025-12-01 14:50:41 -05:00
committed by GitHub
parent 8e9b6c9bc6
commit c3c71a2ff4
5 changed files with 224 additions and 66 deletions
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { Button } from '../../ui/button';
import { Loader2, Download, CheckCircle, AlertCircle } from 'lucide-react';
@@ -29,6 +29,9 @@ export default function UpdateSection() {
currentVersion: '',
});
const [progress, setProgress] = useState<number>(0);
const [isUsingGitHubFallback, setIsUsingGitHubFallback] = useState<boolean>(false);
const progressTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const lastProgressRef = React.useRef<number>(0); // Track last progress to prevent backward jumps
useEffect(() => {
// Get current version on mount
@@ -47,6 +50,11 @@ export default function UpdateSection() {
}
});
// Check if using GitHub fallback
window.electron.isUsingGitHubFallback().then((isGitHub) => {
setIsUsingGitHubFallback(isGitHub);
});
// Listen for updater events
window.electron.onUpdaterEvent((event) => {
console.log('Updater event:', event);
@@ -63,6 +71,10 @@ export default function UpdateSection() {
latestVersion: (event.data as UpdateEventData)?.version,
isUpdateAvailable: true,
}));
// Check if GitHub fallback is being used
window.electron.isUsingGitHubFallback().then((isGitHub) => {
setIsUsingGitHubFallback(isGitHub);
});
break;
case 'update-not-available':
@@ -73,10 +85,29 @@ export default function UpdateSection() {
}));
break;
case 'download-progress':
case 'download-progress': {
setUpdateStatus('downloading');
setProgress((event.data as UpdateEventData)?.percent || 0);
// Get the new progress value (ensure it's a valid number)
const rawPercent = (event.data as UpdateEventData)?.percent;
const newProgress = typeof rawPercent === 'number' ? Math.round(rawPercent) : 0;
// Only update if progress increased (prevents backward jumps from out-of-order events)
if (newProgress > lastProgressRef.current) {
lastProgressRef.current = newProgress;
// Cancel any pending update
if (progressTimeoutRef.current) {
clearTimeout(progressTimeoutRef.current);
}
// Use a small delay to batch rapid updates
progressTimeoutRef.current = setTimeout(() => {
setProgress(newProgress);
}, 50); // 50ms delay for smoother batching
}
break;
}
case 'update-downloaded':
setUpdateStatus('ready');
@@ -93,11 +124,19 @@ export default function UpdateSection() {
break;
}
});
// Cleanup timeout on unmount
return () => {
if (progressTimeoutRef.current) {
clearTimeout(progressTimeoutRef.current);
}
};
}, []);
const checkForUpdates = async () => {
setUpdateStatus('checking');
setProgress(0);
lastProgressRef.current = 0; // Reset progress tracking for new download
try {
const result = await window.electron.checkForUpdates();
@@ -123,29 +162,6 @@ export default function UpdateSection() {
}
};
const downloadAndInstallUpdate = async () => {
setUpdateStatus('downloading');
setProgress(0);
try {
const result = await window.electron.downloadUpdate();
if (!result.success) {
throw new Error(result.error || 'Failed to download update');
}
// The download progress and completion will be handled by updater events
} catch (error) {
console.error('Error downloading update:', error);
setUpdateInfo((prev) => ({
...prev,
error: error instanceof Error ? error.message : 'Failed to download update',
}));
setUpdateStatus('error');
setTimeout(() => setUpdateStatus('idle'), 5000);
}
};
const installUpdate = () => {
window.electron.installUpdate();
};
@@ -216,13 +232,6 @@ export default function UpdateSection() {
Check for Updates
</Button>
{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
<Button onClick={downloadAndInstallUpdate} variant="secondary" size="sm">
<Download className="w-3 h-3 mr-1" />
Download Update
</Button>
)}
{updateStatus === 'ready' && (
<Button onClick={installUpdate} variant="default" size="sm">
Install & Restart
@@ -238,21 +247,57 @@ export default function UpdateSection() {
)}
{updateStatus === 'downloading' && (
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
<div className="w-full mt-2">
<div className="flex justify-between text-xs text-text-muted mb-1">
<span>Downloading update...</span>
<span>{progress}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
<div
className="bg-blue-500 h-2 rounded-full transition-[width] duration-150 ease-out"
style={{ width: `${Math.max(progress, 0)}%`, minWidth: progress > 0 ? '8px' : '0' }}
/>
</div>
</div>
)}
{/* Update information */}
{updateInfo.isUpdateAvailable && (
{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
<div className="text-xs text-text-muted mt-4 space-y-1">
<p>Update will be downloaded to your Downloads folder.</p>
<p className="text-xs text-amber-600">
Note: After downloading, you'll need to close the app and manually install the update.
</p>
<p>Update will be downloaded automatically in the background.</p>
{isUsingGitHubFallback ? (
<p className="text-xs text-amber-600">
After download, you'll need to manually install the update.
</p>
) : (
<p className="text-xs text-green-600">
The update will be installed automatically when you quit the app.
</p>
)}
</div>
)}
{updateStatus === 'ready' && (
<div className="text-xs text-text-muted mt-4 space-y-1">
{isUsingGitHubFallback ? (
<>
<p className="text-xs text-green-600">
Update is ready! Click "Install & Restart" for installation instructions.
</p>
<p className="text-xs text-text-muted">
Manual installation required for this update method.
</p>
</>
) : (
<>
<p className="text-xs text-green-600">
Update is ready! It will be installed when you quit Goose.
</p>
<p className="text-xs text-text-muted">
Or click "Install & Restart" to update now.
</p>
</>
)}
</div>
)}
</div>