import Card, { GridCard } from "@/components/Card"; import { useCallback, useEffect, useRef, useState } from "react"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { Button } from "@components/Button"; import LogoBlueIcon from "@/assets/logo-blue.svg"; import LogoWhiteIcon from "@/assets/logo-white.svg"; import Modal from "@components/Modal"; import { UpdateState, useUpdateStore } from "@/hooks/stores"; import notifications from "@/notifications"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import LoadingSpinner from "./LoadingSpinner"; export interface SystemVersionInfo { local: { appVersion: string; systemVersion: string }; remote: { appVersion: string; systemVersion: string }; systemUpdateAvailable: boolean; appUpdateAvailable: boolean; } export default function UpdateDialog({ open, setOpen, }: { open: boolean; setOpen: (open: boolean) => void; }) { // We need to keep track of the update state in the dialog even if the dialog is minimized const { setModalView } = useUpdateStore(); const [send] = useJsonRpc(); const onConfirmUpdate = useCallback(() => { send("tryUpdate", {}); setModalView("updating"); }, [send, setModalView]); return ( setOpen(false)}> ); } export function Dialog({ setOpen, onConfirmUpdate, }: { setOpen: (open: boolean) => void; onConfirmUpdate: () => void; }) { const [versionInfo, setVersionInfo] = useState(null); const { modalView, setModalView, otaState } = useUpdateStore(); const onFinishedLoading = useCallback( async (versionInfo: SystemVersionInfo) => { const hasUpdate = versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable; setVersionInfo(versionInfo); if (hasUpdate) { setModalView("updateAvailable"); } else { setModalView("upToDate"); } }, [setModalView], ); // Reset modal view when dialog is opened useEffect(() => { setVersionInfo(null); }, [setModalView]); return (
{modalView === "error" && ( setOpen(false)} onRetryUpdate={() => setModalView("loading")} /> )} {modalView === "loading" && ( setOpen(false)} /> )} {modalView === "updateAvailable" && ( setOpen(false)} versionInfo={versionInfo!} /> )} {modalView === "updating" && ( { setOpen(false); }} /> )} {modalView === "upToDate" && ( setModalView("loading")} onClose={() => setOpen(false)} /> )} {modalView === "updateCompleted" && ( setOpen(false)} /> )}
); } function LoadingState({ onFinished, onCancelCheck, }: { onFinished: (versionInfo: SystemVersionInfo) => void; onCancelCheck: () => void; }) { const [progressWidth, setProgressWidth] = useState("0%"); const abortControllerRef = useRef(null); const [send] = useJsonRpc(); const getVersionInfo = useCallback(() => { return new Promise((resolve, reject) => { send("getUpdateStatus", {}, async resp => { if ("error" in resp) { notifications.error("Failed to check for updates"); reject(new Error("Failed to check for updates")); } else { const result = resp.result as SystemVersionInfo; resolve(result); } }); }); }, [send]); const progressBarRef = useRef(null); useEffect(() => { setProgressWidth("0%"); abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; const animationTimer = setTimeout(() => { setProgressWidth("100%"); }, 500); getVersionInfo() .then(versionInfo => { if (progressBarRef.current) { progressBarRef.current?.classList.add("!duration-1000"); } return new Promise(resolve => setTimeout(() => resolve(versionInfo), 1000)); }) .then(versionInfo => { if (!signal.aborted) { onFinished(versionInfo as SystemVersionInfo); } }) .catch(error => { if (!signal.aborted) { console.error("LoadingState: Error fetching version info", error); } }); return () => { clearTimeout(animationTimer); abortControllerRef.current?.abort(); }; }, [getVersionInfo, onFinished]); return (

Checking for updates...

We{"'"}re ensuring your device has the latest features and improvements.

); } function UpdatingDeviceState({ otaState, onMinimizeUpgradeDialog, }: { otaState: UpdateState["otaState"]; onMinimizeUpgradeDialog: () => void; }) { const formatProgress = (progress: number) => `${Math.round(progress)}%`; const calculateOverallProgress = (type: "system" | "app") => { const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100); const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100); const verificationProgress = Math.round( (otaState[`${type}VerificationProgress`] || 0) * 100, ); if (!downloadProgress && !updateProgress && !verificationProgress) { return 0; } console.log( `For ${type}:\n` + ` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` + ` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` + ` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`, ); if (type === "app") { // App: 65% download, 34% verification, 1% update(There is no "real" update for the app) return Math.min( downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01, 100, ); } else { // System: 10% download, 90% update return Math.min( downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8, 100, ); } }; const getUpdateStatus = (type: "system" | "app") => { const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`]; const verfiedAt = otaState[`${type}VerifiedAt`]; const updatedAt = otaState[`${type}UpdatedAt`]; if (!otaState.metadataFetchedAt) { return "Fetching update information..."; } else if (!downloadFinishedAt) { return `Downloading ${type} update...`; } else if (!verfiedAt) { return `Verifying ${type} update...`; } else if (!updatedAt) { return `Installing ${type} update...`; } else { return `Awaiting reboot`; } }; const isUpdateComplete = (type: "system" | "app") => { return !!otaState[`${type}UpdatedAt`]; }; const areAllUpdatesComplete = () => { if (otaState.systemUpdatePending && otaState.appUpdatePending) { return isUpdateComplete("system") && isUpdateComplete("app"); } return ( (otaState.systemUpdatePending && isUpdateComplete("system")) || (otaState.appUpdatePending && isUpdateComplete("app")) ); }; return (

Updating your device

Please don{"'"}t turn off your device. This process may take a few minutes.

{areAllUpdatesComplete() ? (
Rebooting to complete the update...
) : ( <> {!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
)} {otaState.systemUpdatePending && (

Linux System Update

{calculateOverallProgress("system") < 100 ? ( ) : ( )}
{getUpdateStatus("system")} {calculateOverallProgress("system") < 100 ? ( {formatProgress(calculateOverallProgress("system"))} ) : null}
)} {otaState.appUpdatePending && ( <> {otaState.systemUpdatePending && (
)}

App Update

{calculateOverallProgress("app") < 100 ? ( ) : ( )}
{getUpdateStatus("app")} {calculateOverallProgress("system") < 100 ? ( {formatProgress(calculateOverallProgress("app"))} ) : null}
)} )}
); } function SystemUpToDateState({ checkUpdate, onClose, }: { checkUpdate: () => void; onClose: () => void; }) { return (

System is up to date

Your system is running the latest version. No updates are currently available.

); } function UpdateAvailableState({ versionInfo, onConfirmUpdate, onClose, }: { versionInfo: SystemVersionInfo; onConfirmUpdate: () => void; onClose: () => void; }) { return (

Update available

A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.

{versionInfo?.systemUpdateAvailable ? ( <> System:{" "} {versionInfo?.remote.systemVersion}
) : null} {versionInfo?.appUpdateAvailable ? ( <> App: {versionInfo?.remote.appVersion} ) : null}

); } function UpdateCompletedState({ onClose }: { onClose: () => void }) { return (

Update Completed Successfully

Your device has been successfully updated to the latest version. Enjoy the new features and improvements!

); } function UpdateErrorState({ errorMessage, onClose, onRetryUpdate, }: { errorMessage: string | null; onClose: () => void; onRetryUpdate: () => void; }) { return (

Update Error

An error occurred while updating your device. Please try again later.

{errorMessage && (

Error details: {errorMessage}

)}
); }