diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index cac5210..93d66cb 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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('home') const [platform, setPlatform] = useState('') + 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': diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json index 4e0d891..53987a1 100644 --- a/src/renderer/src/locales/en.json +++ b/src/renderer/src/locales/en.json @@ -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",