mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
232 lines
6.3 KiB
TypeScript
232 lines
6.3 KiB
TypeScript
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<UpdateState>('idle');
|
|
const [version, setVersion] = useState('');
|
|
const [progress, setProgress] = useState(0);
|
|
const [errorMsg, setErrorMsg] = useState('');
|
|
const [dismissed, setDismissed] = useState(false);
|
|
const updateRef = useRef<any>(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 (
|
|
<div style={styles.banner}>
|
|
{state === 'available' && (
|
|
<div style={styles.row}>
|
|
<span>Update available: <strong>v{version}</strong></span>
|
|
<div style={styles.actions}>
|
|
<button style={styles.primaryBtn} onClick={handleDownload}>Download</button>
|
|
<button style={styles.secondaryBtn} onClick={() => setDismissed(true)}>Later</button>
|
|
<button style={styles.muteBtn} onClick={handleDisable}>Disable auto-updates</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{state === 'downloading' && (
|
|
<div style={styles.row}>
|
|
<span>Downloading update... {progress}%</span>
|
|
<div style={styles.progressBar}>
|
|
<div style={{ ...styles.progressFill, width: `${progress}%` }} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{state === 'ready' && (
|
|
<div style={styles.row}>
|
|
<span style={{ color: '#a6e3a1' }}>Update installed.</span>
|
|
<div style={styles.actions}>
|
|
<button style={styles.successBtn} onClick={handleRelaunch}>Relaunch now</button>
|
|
<button style={styles.secondaryBtn} onClick={() => setDismissed(true)}>Later</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{state === 'error' && (
|
|
<div style={styles.row}>
|
|
<span style={{ color: '#f38ba8' }}>Update error: {errorMsg}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const styles: Record<string, React.CSSProperties> = {
|
|
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',
|
|
},
|
|
};
|