import React, { useState, useEffect, useCallback, useRef } from 'react'; type UpdateState = 'idle' | 'available' | 'downloading' | 'ready' | 'error'; const CHECK_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes const DISABLED_KEY = 'oj-auto-update-disabled'; export function isAutoUpdateDisabled(): boolean { try { return localStorage.getItem(DISABLED_KEY) === '1'; } catch { return false; } } export function setAutoUpdateDisabled(disabled: boolean): void { try { if (disabled) { localStorage.setItem(DISABLED_KEY, '1'); } else { localStorage.removeItem(DISABLED_KEY); } } catch {} } export function UpdateChecker() { const [state, setState] = useState('idle'); const [version, setVersion] = useState(''); const [progress, setProgress] = useState(0); const [errorMsg, setErrorMsg] = useState(''); const [dismissed, setDismissed] = useState(false); const updateRef = useRef(null); const checkForUpdate = useCallback(async () => { if (isAutoUpdateDisabled()) return; try { const { check } = await import('@tauri-apps/plugin-updater'); const update = await check(); if (update) { updateRef.current = update; setVersion(update.version); setState('available'); setDismissed(false); } } catch { // Silently ignore — likely running in browser or no update available } }, []); useEffect(() => { if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) { return; } if (isAutoUpdateDisabled()) return; // Local dev escape hatch: skip the auto-update poll if explicitly // disabled. Vite exposes any ``VITE_``-prefixed env var on // ``import.meta.env``, so a frontend dev can ``export // VITE_OPENJARVIS_NO_UPDATER=1`` before ``npm run tauri dev`` to // silence the 30-min poll. See docs/desktop-auto-update.md. const noUpdater = (import.meta as any).env?.VITE_OPENJARVIS_NO_UPDATER; if (noUpdater === '1' || noUpdater === 'true') { return; } checkForUpdate(); const interval = setInterval(checkForUpdate, CHECK_INTERVAL_MS); return () => clearInterval(interval); }, [checkForUpdate]); const handleDownload = useCallback(async () => { const update = updateRef.current; if (!update) return; setState('downloading'); setProgress(0); try { let downloaded = 0; const contentLength = update.contentLength ?? 0; await update.downloadAndInstall((event: any) => { if (event.event === 'Progress') { downloaded += event.data?.chunkLength ?? 0; if (contentLength > 0) { setProgress(Math.min(100, Math.round((downloaded / contentLength) * 100))); } } else if (event.event === 'Finished') { setProgress(100); } }); setState('ready'); } catch (e: any) { setErrorMsg(e?.message || 'Download failed'); setState('error'); setTimeout(() => setState('idle'), 5000); } }, []); const handleRelaunch = useCallback(async () => { try { const { relaunch } = await import('@tauri-apps/plugin-process'); await relaunch(); } catch { setErrorMsg('Please restart the application manually'); setState('error'); setTimeout(() => setState('idle'), 5000); } }, []); const handleDisable = useCallback(() => { setAutoUpdateDisabled(true); setState('idle'); setDismissed(false); }, []); if (state === 'idle' || dismissed) return null; return (
{state === 'available' && (
Update available: v{version}
)} {state === 'downloading' && (
Downloading update... {progress}%
)} {state === 'ready' && (
Update installed.
)} {state === 'error' && (
Update error: {errorMsg}
)}
); } const styles: Record = { banner: { padding: '10px 24px', backgroundColor: '#181825', borderBottom: '1px solid #313244', }, row: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', fontSize: '13px', }, actions: { display: 'flex', gap: '8px', alignItems: 'center', }, primaryBtn: { padding: '4px 14px', border: 'none', borderRadius: '4px', backgroundColor: '#89b4fa', color: '#1e1e2e', fontSize: '12px', fontWeight: 600, cursor: 'pointer', }, successBtn: { padding: '4px 14px', border: 'none', borderRadius: '4px', backgroundColor: '#a6e3a1', color: '#1e1e2e', fontSize: '12px', fontWeight: 600, cursor: 'pointer', }, secondaryBtn: { padding: '4px 14px', border: '1px solid #45475a', borderRadius: '4px', backgroundColor: 'transparent', color: '#a6adc8', fontSize: '12px', cursor: 'pointer', }, muteBtn: { padding: '0', border: 'none', backgroundColor: 'transparent', color: '#585b70', fontSize: '11px', cursor: 'pointer', textDecoration: 'underline', }, progressBar: { flex: 1, maxWidth: '300px', height: '6px', backgroundColor: '#313244', borderRadius: '3px', overflow: 'hidden', }, progressFill: { height: '100%', backgroundColor: '#89b4fa', borderRadius: '3px', transition: 'width 0.3s ease', }, };