Files
goose/ui/desktop/src/components/settings/app/UpdateSection.tsx
2025-06-12 11:25:44 -07:00

271 lines
8.1 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Button } from '../../ui/button';
import { Loader2, Download, CheckCircle, AlertCircle } from 'lucide-react';
type UpdateStatus =
| 'idle'
| 'checking'
| 'downloading'
| 'installing'
| 'success'
| 'error'
| 'ready';
interface UpdateInfo {
currentVersion: string;
latestVersion?: string;
isUpdateAvailable?: boolean;
error?: string;
}
interface UpdateEventData {
version?: string;
percent?: number;
}
export default function UpdateSection() {
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({
currentVersion: '',
});
const [progress, setProgress] = useState<number>(0);
useEffect(() => {
// Get current version on mount
const currentVersion = window.electron.getVersion();
setUpdateInfo((prev) => ({ ...prev, currentVersion }));
// Check if there's already an update state from the auto-check
window.electron.getUpdateState().then((state) => {
if (state) {
console.log('Found existing update state:', state);
setUpdateInfo((prev) => ({
...prev,
isUpdateAvailable: state.updateAvailable,
latestVersion: state.latestVersion,
}));
}
});
// Listen for updater events
window.electron.onUpdaterEvent((event) => {
console.log('Updater event:', event);
switch (event.event) {
case 'checking-for-update':
setUpdateStatus('checking');
break;
case 'update-available':
setUpdateStatus('idle');
setUpdateInfo((prev) => ({
...prev,
latestVersion: (event.data as UpdateEventData)?.version,
isUpdateAvailable: true,
}));
break;
case 'update-not-available':
setUpdateStatus('idle');
setUpdateInfo((prev) => ({
...prev,
isUpdateAvailable: false,
}));
break;
case 'download-progress':
setUpdateStatus('downloading');
setProgress((event.data as UpdateEventData)?.percent || 0);
break;
case 'update-downloaded':
setUpdateStatus('ready');
setProgress(100);
break;
case 'error':
setUpdateStatus('error');
setUpdateInfo((prev) => ({
...prev,
error: String(event.data || 'An error occurred'),
}));
setTimeout(() => setUpdateStatus('idle'), 5000);
break;
}
});
}, []);
const checkForUpdates = async () => {
setUpdateStatus('checking');
setProgress(0);
try {
const result = await window.electron.checkForUpdates();
if (result.error) {
throw new Error(result.error);
}
// If we successfully checked and no update is available, show success
if (!result.error && updateInfo.isUpdateAvailable === false) {
setUpdateStatus('success');
setTimeout(() => setUpdateStatus('idle'), 3000);
}
// The actual status will be handled by the updater events
} catch (error) {
console.error('Error checking for updates:', error);
setUpdateInfo((prev) => ({
...prev,
error: error instanceof Error ? error.message : 'Failed to check for updates',
}));
setUpdateStatus('error');
setTimeout(() => setUpdateStatus('idle'), 5000);
}
};
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 = () => {
setUpdateStatus('installing');
// This will quit the app and install the update
window.electron.installUpdate();
};
const getStatusMessage = () => {
switch (updateStatus) {
case 'checking':
return 'Checking for updates...';
case 'downloading':
return `Downloading update... ${Math.round(progress)}%`;
case 'installing':
return 'Installing update...';
case 'ready':
return 'Update downloaded and ready to install!';
case 'success':
return updateInfo.isUpdateAvailable === false
? 'You are running the latest version!'
: 'Update available!';
case 'error':
return updateInfo.error || 'An error occurred';
default:
if (updateInfo.isUpdateAvailable) {
return `Version ${updateInfo.latestVersion} is available`;
}
return '';
}
};
const getStatusIcon = () => {
switch (updateStatus) {
case 'checking':
case 'downloading':
case 'installing':
return <Loader2 className="w-4 h-4 animate-spin" />;
case 'success':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'error':
return <AlertCircle className="w-4 h-4 text-red-500" />;
case 'ready':
return <CheckCircle className="w-4 h-4 text-blue-500" />;
default:
return updateInfo.isUpdateAvailable ? <Download className="w-4 h-4" /> : null;
}
};
return (
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-medium text-textStandard">Updates</h2>
</div>
<div className="pb-8">
<p className="text-sm text-textStandard mb-6">
Current version: {updateInfo.currentVersion || 'Loading...'}
{updateInfo.latestVersion && updateInfo.isUpdateAvailable && (
<span className="text-textSubtle"> {updateInfo.latestVersion} available</span>
)}
{updateInfo.currentVersion && updateInfo.isUpdateAvailable === false && ' (up to date)'}
</p>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<Button
onClick={checkForUpdates}
disabled={updateStatus !== 'idle' && updateStatus !== 'error'}
variant="outline"
size="sm"
className="text-xs"
>
Check for Updates
</Button>
{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
<Button
onClick={downloadAndInstallUpdate}
variant="default"
size="sm"
className="text-xs"
>
<Download className="w-3 h-3 mr-1" />
Download Update
</Button>
)}
{updateStatus === 'ready' && (
<Button onClick={installUpdate} variant="default" size="sm" className="text-xs">
Install & Restart
</Button>
)}
</div>
{getStatusMessage() && (
<div className="flex items-center gap-2 text-xs text-textSubtle">
{getStatusIcon()}
<span>{getStatusMessage()}</span>
</div>
)}
{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>
)}
{/* Update information */}
{updateInfo.isUpdateAvailable && (
<div className="text-xs text-textSubtle mt-4 space-y-1">
<p>Update will be downloaded and automatically extracted to your Downloads folder.</p>
<p className="text-xs text-amber-600">
After download, move the Goose app to /Applications to complete the update.
</p>
</div>
)}
</div>
</div>
</div>
);
}