feat(updates): add auto-update notifications and handlers (#8)
Add update-related UI and IPC handling to App. Integrate i18n and sonner to show localized toast notifications update lifecycle events (available, download started, progress, downloaded, errors, and restart prompt). Use a ref to guard against concurrent download attempts and read auto-update preference from settings atom. Wire IPC event names and services, and ensure graceful error handling and user-triggered download/restart actions. This enables better user feedback for updates and supports both automatic and manual update flows.
This commit is contained in:
@@ -2,19 +2,27 @@ import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||
import { Sidebar } from '@renderer/components/ui/sidebar'
|
||||
import { Toaster } from '@renderer/components/ui/sonner'
|
||||
import { TitleBar } from '@renderer/components/ui/title-bar'
|
||||
import { useAtom } from 'jotai'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ipcServices } from './lib/ipc'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { ipcEvents, ipcServices } from './lib/ipc'
|
||||
import { About } from './pages/About'
|
||||
import { Home } from './pages/Home'
|
||||
import { Settings } from './pages/Settings'
|
||||
import { SupportedSites } from './pages/SupportedSites'
|
||||
import { settingsAtom } from './store/settings'
|
||||
|
||||
type Page = 'home' | 'settings' | 'about' | 'sites'
|
||||
|
||||
function AppContent() {
|
||||
const [currentPage, setCurrentPage] = useState<Page>('home')
|
||||
const [platform, setPlatform] = useState<string>('')
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const { t } = useTranslation()
|
||||
const autoUpdateEnabled = settings.autoUpdate
|
||||
const updateDownloadInProgressRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get platform info to determine if we should show title bar
|
||||
@@ -31,6 +39,130 @@ function AppContent() {
|
||||
getPlatform()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window?.api) {
|
||||
return
|
||||
}
|
||||
|
||||
const showRestartPrompt = () => {
|
||||
toast.info(t('about.notifications.restartToUpdate'), {
|
||||
action: {
|
||||
label: t('about.notifications.restartNowAction'),
|
||||
onClick: () => {
|
||||
void ipcServices.update.quitAndInstall()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetDownloadState = () => {
|
||||
if (updateDownloadInProgressRef.current) {
|
||||
updateDownloadInProgressRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const startUpdateDownload = async () => {
|
||||
if (updateDownloadInProgressRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
updateDownloadInProgressRef.current = true
|
||||
toast.info(t('about.notifications.downloadStarted'))
|
||||
|
||||
try {
|
||||
const result = await ipcServices.update.downloadUpdate()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error ?? 'Unknown error')
|
||||
}
|
||||
} catch (error) {
|
||||
updateDownloadInProgressRef.current = false
|
||||
const message =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: t('about.notifications.unknownErrorFallback')
|
||||
toast.error(t('about.notifications.updateError', { error: message }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateAvailable = (rawInfo: unknown) => {
|
||||
const info = (rawInfo ?? {}) as { version?: string }
|
||||
const versionLabel = info.version ?? ''
|
||||
toast.success(t('about.notifications.updateAvailable', { version: versionLabel }))
|
||||
|
||||
if (autoUpdateEnabled) {
|
||||
void startUpdateDownload()
|
||||
} else {
|
||||
toast(t('about.notifications.downloadUpdate', { version: versionLabel }), {
|
||||
action: {
|
||||
label: t('about.notifications.manualDownloadAction'),
|
||||
onClick: () => {
|
||||
void startUpdateDownload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateDownloaded = (rawInfo: unknown) => {
|
||||
const info = (rawInfo ?? {}) as { version?: string }
|
||||
resetDownloadState()
|
||||
|
||||
const versionLabel = info?.version ?? ''
|
||||
const downloadedMessage = versionLabel
|
||||
? t('about.notifications.updateDownloadedVersion', { version: versionLabel })
|
||||
: t('about.notifications.updateDownloaded')
|
||||
toast.success(downloadedMessage)
|
||||
|
||||
showRestartPrompt()
|
||||
}
|
||||
|
||||
const handleUpdateError = (rawMessage: unknown) => {
|
||||
const message = typeof rawMessage === 'string' ? rawMessage : ''
|
||||
resetDownloadState()
|
||||
|
||||
const errorMessage = message || t('about.notifications.unknownErrorFallback')
|
||||
toast.error(t('about.notifications.updateError', { error: errorMessage }))
|
||||
}
|
||||
|
||||
const handleDownloadProgress = (rawProgress: unknown) => {
|
||||
const progress = (rawProgress ?? {}) as { percent?: number }
|
||||
if (typeof progress?.percent === 'number') {
|
||||
console.info('Update download progress:', progress.percent.toFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateNotification = (rawPayload: unknown) => {
|
||||
const payload = (rawPayload ?? {}) as { body?: string; version?: string }
|
||||
const versionLabel = payload.version ?? ''
|
||||
const downloadedMessage = versionLabel
|
||||
? t('about.notifications.updateDownloadedVersion', { version: versionLabel })
|
||||
: t('about.notifications.updateDownloaded')
|
||||
|
||||
toast.info(payload?.body ?? downloadedMessage, {
|
||||
action: {
|
||||
label: t('about.notifications.restartNowAction'),
|
||||
onClick: () => {
|
||||
void ipcServices.update.quitAndInstall()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ipcEvents.on('update:available', handleUpdateAvailable)
|
||||
ipcEvents.on('update:downloaded', handleUpdateDownloaded)
|
||||
ipcEvents.on('update:error', handleUpdateError)
|
||||
ipcEvents.on('update:download-progress', handleDownloadProgress)
|
||||
ipcEvents.on('update:show-notification', handleUpdateNotification)
|
||||
|
||||
return () => {
|
||||
ipcEvents.removeListener('update:available', handleUpdateAvailable)
|
||||
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
|
||||
ipcEvents.removeListener('update:error', handleUpdateError)
|
||||
ipcEvents.removeListener('update:download-progress', handleDownloadProgress)
|
||||
ipcEvents.removeListener('update:show-notification', handleUpdateNotification)
|
||||
}
|
||||
}, [autoUpdateEnabled, t])
|
||||
|
||||
const renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case 'home':
|
||||
|
||||
@@ -27,11 +27,15 @@
|
||||
"downloadError": "Failed to download update",
|
||||
"downloadStarted": "Download started...",
|
||||
"downloadUpdate": "Download and install update {{version}}?",
|
||||
"manualDownloadAction": "Download now",
|
||||
"noUpdatesAvailable": "You're using the latest version",
|
||||
"restartToUpdate": "Restart now to install update?",
|
||||
"restartNowAction": "Restart now",
|
||||
"updateAvailable": "Update available: {{version}}",
|
||||
"updateDownloaded": "Update downloaded, restart to install",
|
||||
"updateError": "Failed to check for updates: {{error}}"
|
||||
"updateDownloadedVersion": "Update {{version}} downloaded, restart to install",
|
||||
"updateError": "Failed to check for updates: {{error}}",
|
||||
"unknownErrorFallback": "Unknown error"
|
||||
},
|
||||
"preferencesDescription": "Tune update settings without leaving this page.",
|
||||
"preferencesTitle": "Quick Toggles",
|
||||
|
||||
Reference in New Issue
Block a user