Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)] = &[
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -205,6 +214,7 @@ fn build_menu(
&open,
&go_to,
&config_folder,
&check_updates,
&separators[3],
&quit,
]);
Expand Down Expand Up @@ -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),
Expand Down
46 changes: 45 additions & 1 deletion src/components/UpdateChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand All @@ -21,6 +24,26 @@ export default function UpdateChecker() {
const [update, setUpdate] = useState<Update | null>(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
Expand All @@ -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?.()
}
}, [])

Expand Down Expand Up @@ -77,6 +110,9 @@ export default function UpdateChecker() {
return (
<div className="sticky top-0 z-10 flex items-center gap-3 border-b border-border bg-accent/90 px-4 py-2 text-sm backdrop-blur">
<RefreshCw className="h-4 w-4 shrink-0" />
{phase.kind === 'checking' && <span className="flex-1">{s.updater.checking}</span>}
{phase.kind === 'current' && <span className="flex-1">{s.updater.current}</span>}
{phase.kind === 'error' && <span className="flex-1">{s.updater.error}</span>}
{phase.kind === 'available' && (
<>
<span className="flex-1">
Expand Down Expand Up @@ -108,6 +144,14 @@ export default function UpdateChecker() {
</Button>
</>
)}
{(phase.kind === 'current' || phase.kind === 'error') && (
<button
onClick={() => setDismissed(true)}
className="text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</div>
)
}
3 changes: 3 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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…',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ const es: DeepPartial<Strings> = {
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…',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ const pt: DeepPartial<Strings> = {
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…',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ const zh: DeepPartial<Strings> = {
zoomReset: '重置缩放为 100%',
},
updater: {
checking: '正在检查更新...',
current: 'LoomRouter 已是最新版本',
error: '无法检查更新',
available: '新版本 {{version}} 可用',
install: '下载并安装',
downloading: '正在下载更新…',
Expand Down