diff --git a/App.tsx b/App.tsx index 78902bf..1bee915 100644 --- a/App.tsx +++ b/App.tsx @@ -181,6 +181,10 @@ const App: React.FC = () => { const autoCheckIntervalRef = useRef(null); const isAutoCheckingRef = useRef(false); const [updateReady, setUpdateReady] = useState(false); + const [updateStatus, setUpdateStatus] = useState(null); + const [isUpdateBannerVisible, setIsUpdateBannerVisible] = useState(false); + const [autoInstallScheduled, setAutoInstallScheduled] = useState(false); + const autoInstallTimeoutRef = useRef(null); // New states for deeper VCS integration const [detailedStatuses, setDetailedStatuses] = useState>({}); @@ -254,6 +258,70 @@ const App: React.FC = () => { onCancel: () => {}, }); + const cancelAutoInstall = useCallback(() => { + if (autoInstallTimeoutRef.current) { + window.clearTimeout(autoInstallTimeoutRef.current); + autoInstallTimeoutRef.current = null; + } + setAutoInstallScheduled(false); + }, []); + + const handleRestartAndUpdate = useCallback(() => { + cancelAutoInstall(); + if (window.electronAPI?.restartAndInstallUpdate) { + setToast({ message: 'Restarting to install the latest update…', type: 'info' }); + window.electronAPI.restartAndInstallUpdate(); + } else { + setToast({ message: 'Could not restart. Please restart the app manually.', type: 'error' }); + } + }, [cancelAutoInstall, setToast]); + + const scheduleAutoInstall = useCallback(() => { + cancelAutoInstall(); + autoInstallTimeoutRef.current = window.setTimeout(() => { + setAutoInstallScheduled(false); + handleRestartAndUpdate(); + }, 8000); + setAutoInstallScheduled(true); + }, [cancelAutoInstall, handleRestartAndUpdate]); + + const handleAutoInstallPreferenceChange = useCallback((mode: GlobalSettings['autoInstallUpdates']) => { + if (mode === settings.autoInstallUpdates) { + return; + } + saveSettings({ ...settings, autoInstallUpdates: mode }); + if (mode === 'manual') { + cancelAutoInstall(); + setToast({ + message: 'Automatic installation disabled. Use the Update Ready controls when you want to apply the update.', + type: 'info', + }); + } else { + setToast({ + message: 'Updates will now install automatically as soon as they finish downloading.', + type: 'success', + }); + } + }, [cancelAutoInstall, saveSettings, settings, setToast]); + + const handleDeferUpdate = useCallback(() => { + if (settings.autoInstallUpdates === 'auto') { + handleAutoInstallPreferenceChange('manual'); + } + cancelAutoInstall(); + setIsUpdateBannerVisible(false); + setToast({ + message: 'Update postponed. Use the “Update Ready” control in the title bar to install whenever you are ready.', + type: 'info', + }); + }, [cancelAutoInstall, handleAutoInstallPreferenceChange, settings.autoInstallUpdates, setToast]); + + const handleShowUpdateDetails = useCallback(() => { + if (updateReady) { + setIsUpdateBannerVisible(true); + } + }, [updateReady]); + useEffect(() => { if (!instrumentation) { return; @@ -467,31 +535,74 @@ const App: React.FC = () => { // Effect for auto-updater useEffect(() => { const handleUpdateStatus = (_event: any, data: UpdateStatusMessage) => { - logger.info(`Update status change received: ${data.status}`, data); - switch (data.status) { - case 'available': - setToast({ message: data.message, type: 'info' }); - break; - case 'downloaded': - setToast({ message: data.message, type: 'success' }); - setUpdateReady(true); - break; - case 'error': - setToast({ message: data.message, type: 'error' }); - break; - } + logger.info(`Update status change received: ${data.status}`, data); + setUpdateStatus(data); + + switch (data.status) { + case 'checking': + cancelAutoInstall(); + setUpdateReady(false); + setIsUpdateBannerVisible(false); + break; + case 'available': + setToast({ message: data.message, type: 'info' }); + break; + case 'downloaded': + setUpdateReady(true); + setIsUpdateBannerVisible(true); + if (settings.autoInstallUpdates === 'auto') { + setToast({ message: 'Update downloaded. Restarting automatically in a few seconds…', type: 'info' }); + scheduleAutoInstall(); + } else { + cancelAutoInstall(); + setToast({ message: data.message, type: 'success' }); + } + break; + case 'error': + cancelAutoInstall(); + setUpdateReady(false); + setIsUpdateBannerVisible(false); + setToast({ message: data.message, type: 'error' }); + break; + default: + break; + } }; if (window.electronAPI?.onUpdateStatusChange) { - window.electronAPI.onUpdateStatusChange(handleUpdateStatus); + window.electronAPI.onUpdateStatusChange(handleUpdateStatus); } return () => { - if (window.electronAPI?.removeUpdateStatusChangeListener) { - window.electronAPI.removeUpdateStatusChangeListener(handleUpdateStatus); - } + if (window.electronAPI?.removeUpdateStatusChangeListener) { + window.electronAPI.removeUpdateStatusChangeListener(handleUpdateStatus); + } }; - }, [logger]); + }, [logger, cancelAutoInstall, scheduleAutoInstall, settings.autoInstallUpdates, setToast]); + + useEffect(() => { + if (!updateReady) { + return; + } + + if (settings.autoInstallUpdates === 'auto') { + if (!autoInstallScheduled) { + scheduleAutoInstall(); + } + } else if (autoInstallScheduled) { + cancelAutoInstall(); + } + }, [ + updateReady, + settings.autoInstallUpdates, + autoInstallScheduled, + scheduleAutoInstall, + cancelAutoInstall, + ]); + + useEffect(() => () => { + cancelAutoInstall(); + }, [cancelAutoInstall]); // Effect to check local paths useEffect(() => { @@ -1319,15 +1430,6 @@ const App: React.FC = () => { } }, []); - const handleRestartAndUpdate = useCallback(() => { - if (window.electronAPI?.restartAndInstallUpdate) { - window.electronAPI.restartAndInstallUpdate(); - } else { - setToast({ message: 'Could not restart. Please restart the app manually.', type: 'error' }); - } - }, []); - - const latestLog = useMemo(() => { const allLogs = Object.values(logs).flat(); if (allLogs.length === 0) return null; @@ -1428,9 +1530,22 @@ const App: React.FC = () => { isCheckingAll={isCheckingAll} onToggleAllCategories={toggleAllCategoriesCollapse} canCollapseAll={canCollapseAll} + updateReady={updateReady} + onInstallUpdate={handleRestartAndUpdate} + onShowUpdateDetails={handleShowUpdateDetails} />
- {updateReady && } + {updateReady && isUpdateBannerVisible && updateStatus?.status === 'downloaded' && ( + + )}
void; canCollapseAll: boolean; + updateReady: boolean; + onInstallUpdate: () => void; + onShowUpdateDetails: () => void; } const TitleBar: React.FC = ({ @@ -32,6 +36,9 @@ const TitleBar: React.FC = ({ isCheckingAll, onToggleAllCategories, canCollapseAll, + updateReady, + onInstallUpdate, + onShowUpdateDetails, }) => { const { settings, saveSettings } = useSettings(); const isEditing = activeView === 'edit-repository'; @@ -55,6 +62,8 @@ const TitleBar: React.FC = ({ const newRepoTooltip = useTooltip('Add New Repository'); const checkUpdatesTooltip = useTooltip('Check all repositories for updates'); const expandCollapseTooltip = useTooltip(canCollapseAll ? 'Collapse all categories' : 'Expand all categories'); + const installUpdateTooltip = useTooltip('Restart to apply the update now'); + const updateDetailsTooltip = useTooltip('Show update installation options'); const noDragStyle = { WebkitAppRegion: 'no-drag' } as React.CSSProperties; @@ -109,6 +118,36 @@ const TitleBar: React.FC = ({ )}
+ {updateReady && ( +
+
+ )}
+
+
+ Application Updates +

Keep Git Automation Dashboard current

+

Control how the app checks for new releases and whether updates install themselves once downloaded.

+
+
+ +
+

Installation preference

+
+ + +
+
+
+
+

Automatic Update Checks