diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 016264c..511ac17 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -15,6 +15,8 @@ use tauri::{ const EVENT_STATE_CHANGED: &str = "loomrouter://state-changed"; /// Event carrying a route the UI should navigate to, e.g. "/providers". const EVENT_NAVIGATE: &str = "loomrouter://navigate"; +/// Event requesting an explicit updater check in the main window. +const EVENT_CHECK_UPDATES: &str = "loomrouter://check-updates"; /// The pages the tray can jump to, as (menu id suffix, route, label). const PAGES: &[(&str, &str, &str)] = &[ @@ -168,6 +170,13 @@ fn build_menu( )?; let config_folder = MenuItem::with_id(app, "open-config", "Open Config Folder", true, None::<&str>)?; + let check_updates = MenuItem::with_id( + app, + "check-updates", + "Check for Updates", + true, + None::<&str>, + )?; let quit = MenuItem::with_id(app, "quit", "Quit LoomRouter", true, None::<&str>)?; // Only built after something failed, so it costs nothing in the normal @@ -205,6 +214,7 @@ fn build_menu( &open, &go_to, &config_folder, + &check_updates, &separators[3], &quit, ]); @@ -469,6 +479,12 @@ pub(crate) fn notify_state_changed(app: &tauri::AppHandle) { fn on_tray_menu_event(app: &tauri::AppHandle, id: &str) { match id { "show" => show_main_window(app), + "check-updates" => { + show_main_window(app); + if let Err(e) = app.emit(EVENT_CHECK_UPDATES, ()) { + tracing::warn!("check-updates event failed: {e}"); + } + } // AppHandle::exit() does not fire CloseRequested, so this is a // real quit (the window is not just hidden again). "quit" => app.exit(0), diff --git a/src/components/UpdateChecker.tsx b/src/components/UpdateChecker.tsx index b9c0641..60321e7 100644 --- a/src/components/UpdateChecker.tsx +++ b/src/components/UpdateChecker.tsx @@ -8,6 +8,9 @@ import type { Update } from '@tauri-apps/plugin-updater' type Phase = | { kind: 'idle' } + | { kind: 'checking' } + | { kind: 'current' } + | { kind: 'error' } | { kind: 'available'; version: string } | { kind: 'downloading'; percent: number } | { kind: 'ready' } @@ -21,6 +24,26 @@ export default function UpdateChecker() { const [update, setUpdate] = useState(null) const [dismissed, setDismissed] = useState(false) + const checkForUpdates = async (manual = false) => { + if (!isTauri) return + if (manual) { + setDismissed(false) + setPhase({ kind: 'checking' }) + } + try { + const { check } = await import('@tauri-apps/plugin-updater') + const found = await check() + if (found) { + setUpdate(found) + setPhase({ kind: 'available', version: found.version }) + } else if (manual) { + setPhase({ kind: 'current' }) + } + } catch { + if (manual) setPhase({ kind: 'error' }) + } + } + useEffect(() => { if (!isTauri) return let cancelled = false @@ -33,10 +56,20 @@ export default function UpdateChecker() { } }) .catch(() => { - // Offline or no release published yet: stay silent. + // Automatic checks stay silent when offline or already current. }) + let unlisten: (() => void) | undefined + import('@tauri-apps/api/event').then(({ listen }) => + listen('loomrouter://check-updates', () => { + if (!cancelled) void checkForUpdates(true) + }).then((dispose) => { + if (cancelled) dispose() + else unlisten = dispose + }), + ) return () => { cancelled = true + unlisten?.() } }, []) @@ -77,6 +110,9 @@ export default function UpdateChecker() { return (
+ {phase.kind === 'checking' && {s.updater.checking}} + {phase.kind === 'current' && {s.updater.current}} + {phase.kind === 'error' && {s.updater.error}} {phase.kind === 'available' && ( <> @@ -108,6 +144,14 @@ export default function UpdateChecker() { )} + {(phase.kind === 'current' || phase.kind === 'error') && ( + + )}
) } diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 09cb0a4..d9f2ce5 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -297,6 +297,9 @@ const en = { zoomReset: 'Reset zoom to 100%', }, updater: { + checking: 'Checking for updates...', + current: 'LoomRouter is up to date', + error: 'Could not check for updates', available: 'Version {{version}} is available', install: 'Download & install', downloading: 'Downloading update…', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 54c7388..7a71919 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -295,6 +295,9 @@ const es: DeepPartial = { zoomReset: 'Restablecer zoom al 100%', }, updater: { + checking: 'Buscando actualizaciones...', + current: 'LoomRouter está actualizado', + error: 'No se pudieron buscar actualizaciones', available: 'Versión {{version}} disponible', install: 'Descargar e instalar', downloading: 'Descargando actualización…', diff --git a/src/i18n/pt.ts b/src/i18n/pt.ts index 30bfeba..f378845 100644 --- a/src/i18n/pt.ts +++ b/src/i18n/pt.ts @@ -294,6 +294,9 @@ const pt: DeepPartial = { zoomReset: 'Redefinir zoom para 100%', }, updater: { + checking: 'Verificando atualizações...', + current: 'O LoomRouter está atualizado', + error: 'Não foi possível verificar atualizações', available: 'Versão {{version}} disponível', install: 'Baixar e instalar', downloading: 'Baixando atualização…', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index e7359d6..e17205b 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -293,6 +293,9 @@ const zh: DeepPartial = { zoomReset: '重置缩放为 100%', }, updater: { + checking: '正在检查更新...', + current: 'LoomRouter 已是最新版本', + error: '无法检查更新', available: '新版本 {{version}} 可用', install: '下载并安装', downloading: '正在下载更新…',