import React, { useEffect, useState, useRef } from 'react'; import { X, Sparkles, Loader2, CheckCircle, RotateCcw } from 'lucide-react'; import { request as invoke } from '../utils/request'; import { useTranslation } from 'react-i18next'; import { check as tauriCheck } from '@tauri-apps/plugin-updater'; import { relaunch as tauriRelaunch } from '@tauri-apps/plugin-process'; import { isTauri } from '../utils/env'; import { showToast } from './common/ToastContainer'; interface UpdateInfo { has_update: boolean; latest_version: string; current_version: string; download_url: string; source?: string; } type UpdateState = 'checking' | 'downloading' | 'ready' | 'error' | 'none'; interface UpdateNotificationProps { onClose: () => void; } export const UpdateNotification: React.FC = ({ onClose }) => { const { t } = useTranslation(); const [updateInfo, setUpdateInfo] = useState(null); const [isVisible, setIsVisible] = useState(false); const [isClosing, setIsClosing] = useState(false); const [updateState, setUpdateState] = useState('checking'); const [downloadProgress, setDownloadProgress] = useState(0); const downloadStarted = useRef(false); useEffect(() => { checkAndDownload(); }, []); const checkAndDownload = async () => { try { // 1. Check for updates via backend const info = await invoke('check_for_updates'); if (!info.has_update) { onClose(); return; } setUpdateInfo(info); // 2. If not in Tauri — no auto-update possible if (!isTauri()) { console.warn('Auto update is only available in Tauri environment'); onClose(); return; } // 3. Start background download immediately if (downloadStarted.current) return; downloadStarted.current = true; setUpdateState('downloading'); setTimeout(() => setIsVisible(true), 100); const update = await tauriCheck(); if (!update) { // updater.json not ready yet or no update via native channel console.warn('Native updater returned null'); showToast(t('update_notification.toast.not_ready'), 'info'); handleClose(); return; } let downloaded = 0; let contentLength = 0; await update.downloadAndInstall((event) => { switch (event.event) { case 'Started': contentLength = event.data.contentLength || 0; break; case 'Progress': downloaded += event.data.chunkLength; if (contentLength > 0) { setDownloadProgress(Math.round((downloaded / contentLength) * 100)); } break; case 'Finished': break; } }); // 4. Download complete — show restart prompt setUpdateState('ready'); setDownloadProgress(100); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error('Auto update failed:', errorMsg); setUpdateState('error'); showToast(`${t('update_notification.toast.failed')}: ${errorMsg}`, 'error'); } }; const handleRestart = async () => { try { await tauriRelaunch(); } catch (error) { console.error('Relaunch failed:', error); } }; const handleClose = () => { setIsClosing(true); setIsVisible(false); setTimeout(onClose, 400); }; if (updateState === 'none') { return null; } return (
{updateState === 'ready' ? ( ) : ( )}

{updateState === 'ready' ? t('update_notification.ready') : t('update_notification.title')}

{updateInfo && (

v{updateInfo.latest_version}

)}
{(updateState === 'error' || updateState === 'ready') && ( )}
{/* Status message */}

{updateState === 'downloading' && t('update_notification.downloading')} {updateState === 'ready' && t('update_notification.restart_prompt')} {updateState === 'error' && `${t('update_notification.toast.failed')}`}

{/* Progress bar during download */} {updateState === 'downloading' && (

{downloadProgress}%

)} {/* Restart button when ready */} {updateState === 'ready' && (
)} {/* Error state — retry button */} {updateState === 'error' && ( )}
); };