2026-09-15 08:47:05 -04:00
|
|
|
import { useCallback } from 'react';
|
2026-09-23 20:52:16 -04:00
|
|
|
import { atom, useAtom, useSetAtom } from 'jotai';
|
|
|
|
|
import { useTauriEvent } from './useTauri';
|
|
|
|
|
import { UpdatePhase, parseUpdateError } from '../utils/updateErrors';
|
2026-06-10 20:31:35 -04:00
|
|
|
|
|
|
|
|
type TauriInternals = { invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> };
|
|
|
|
|
const tauriInvoke = (): TauriInternals['invoke'] | undefined =>
|
|
|
|
|
(window as unknown as { __TAURI_INTERNALS__?: TauriInternals }).__TAURI_INTERNALS__?.invoke;
|
|
|
|
|
|
2026-09-23 20:52:16 -04:00
|
|
|
/** Detail of the native `lotus-update-progress` event (see lib.rs `install_update`). */
|
|
|
|
|
export type UpdateProgress =
|
|
|
|
|
| {
|
|
|
|
|
phase: 'downloading';
|
|
|
|
|
attempt: number;
|
|
|
|
|
maxAttempts: number;
|
|
|
|
|
downloaded: number;
|
|
|
|
|
total: number | null;
|
|
|
|
|
}
|
|
|
|
|
| { phase: 'retrying'; attempt: number; maxAttempts: number; waitSecs: number; error: string }
|
|
|
|
|
| { phase: 'installing' };
|
|
|
|
|
|
|
|
|
|
export type UpdateStatus =
|
2026-06-10 20:31:35 -04:00
|
|
|
| { state: 'idle' }
|
|
|
|
|
| { state: 'checking' }
|
|
|
|
|
| { state: 'up-to-date' }
|
|
|
|
|
| { state: 'available'; version: string }
|
2026-09-23 20:52:16 -04:00
|
|
|
| { state: 'installing'; progress?: UpdateProgress }
|
|
|
|
|
| { state: 'error'; phase: UpdatePhase; message: string };
|
|
|
|
|
|
|
|
|
|
export type UpdateFailure = { phase: UpdatePhase; message: string };
|
2026-06-10 20:31:35 -04:00
|
|
|
|
2026-09-15 08:47:05 -04:00
|
|
|
// Module-level so the result of a manual "Check for updates" survives closing
|
|
|
|
|
// and reopening Settings (it used to be component state, so the "update
|
|
|
|
|
// available" answer was thrown away with the tab and had to be re-checked).
|
|
|
|
|
// A transient 'checking'/'installing' state is reset if the tab unmounts
|
|
|
|
|
// mid-request by the resolve/reject below, so nothing gets stuck.
|
|
|
|
|
const updateStatusAtom = atom<UpdateStatus>({ state: 'idle' });
|
|
|
|
|
|
2026-09-23 20:52:16 -04:00
|
|
|
/**
|
|
|
|
|
* Mirror native download progress into the status. Mounted once (in
|
|
|
|
|
* TauriUpdateFeature) so the listener isn't duplicated per consumer.
|
|
|
|
|
*/
|
|
|
|
|
export function useTauriUpdateProgress(): void {
|
|
|
|
|
const setStatus = useSetAtom(updateStatusAtom);
|
|
|
|
|
useTauriEvent<UpdateProgress>('lotus-update-progress', (progress) =>
|
|
|
|
|
setStatus((prev) => (prev.state === 'installing' ? { state: 'installing', progress } : prev)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 20:31:35 -04:00
|
|
|
export function useTauriUpdater() {
|
|
|
|
|
const isTauri = !!tauriInvoke();
|
2026-09-15 08:47:05 -04:00
|
|
|
const [status, setStatus] = useAtom(updateStatusAtom);
|
2026-06-10 20:31:35 -04:00
|
|
|
|
|
|
|
|
const check = useCallback(async () => {
|
|
|
|
|
const invoke = tauriInvoke();
|
|
|
|
|
if (!invoke) return;
|
|
|
|
|
setStatus({ state: 'checking' });
|
|
|
|
|
try {
|
|
|
|
|
const result = (await invoke('check_for_update')) as { available: boolean; version?: string };
|
|
|
|
|
setStatus(
|
|
|
|
|
result.available && result.version
|
|
|
|
|
? { state: 'available', version: result.version }
|
|
|
|
|
: { state: 'up-to-date' },
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
2026-09-23 20:52:16 -04:00
|
|
|
setStatus({ state: 'error', ...parseUpdateError(String(e), 'check') });
|
2026-06-10 20:31:35 -04:00
|
|
|
}
|
2026-09-15 08:47:05 -04:00
|
|
|
}, [setStatus]);
|
2026-06-10 20:31:35 -04:00
|
|
|
|
2026-09-23 20:52:16 -04:00
|
|
|
/**
|
|
|
|
|
* Resolves `undefined` when nothing needed installing (a successful install
|
|
|
|
|
* restarts the app instead), or the failure.
|
|
|
|
|
*/
|
|
|
|
|
const install = useCallback(async (): Promise<UpdateFailure | undefined> => {
|
2026-06-10 20:31:35 -04:00
|
|
|
const invoke = tauriInvoke();
|
2026-09-23 20:52:16 -04:00
|
|
|
if (!invoke) return undefined;
|
2026-06-10 20:31:35 -04:00
|
|
|
setStatus({ state: 'installing' });
|
|
|
|
|
try {
|
|
|
|
|
await invoke('install_update');
|
2026-09-23 20:52:16 -04:00
|
|
|
// On a successful install the native side restarts the app, so this
|
2026-07-02 20:56:27 -04:00
|
|
|
// resolve is only reached when nothing was installed (no update found) —
|
|
|
|
|
// don't leave the UI stuck on "installing".
|
|
|
|
|
setStatus({ state: 'up-to-date' });
|
2026-09-23 20:52:16 -04:00
|
|
|
return undefined;
|
2026-06-10 20:31:35 -04:00
|
|
|
} catch (e) {
|
2026-09-23 20:52:16 -04:00
|
|
|
const failure = parseUpdateError(String(e), 'download');
|
|
|
|
|
setStatus({ state: 'error', ...failure });
|
|
|
|
|
return failure;
|
2026-06-10 20:31:35 -04:00
|
|
|
}
|
2026-09-15 08:47:05 -04:00
|
|
|
}, [setStatus]);
|
2026-06-10 20:31:35 -04:00
|
|
|
|
|
|
|
|
return { isTauri, status, check, install };
|
|
|
|
|
}
|