import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router"; import { useJsonRpc } from "@hooks/useJsonRpc"; import { UpdateState, useUpdateStore } from "@hooks/stores"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; import { useVersion } from "@hooks/useVersion"; import { Button } from "@components/Button"; import Card from "@components/Card"; import LoadingSpinner from "@components/LoadingSpinner"; import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard"; import { m } from "@localizations/messages.js"; import { sleep } from "@/utils"; import { SystemVersionInfo } from "@/utils/jsonrpc"; export default function SettingsGeneralUpdateRoute() { const navigate = useNavigate(); const location = useLocation(); const { updateSuccess } = location.state || {}; const { setModalView, otaState } = useUpdateStore(); const { send } = useJsonRpc(); const onConfirmUpdate = useCallback(() => { send("tryUpdate", {}); setModalView("updating"); }, [send, setModalView]); useEffect(() => { if (otaState.updating) { setModalView("updating"); } else if (otaState.error) { setModalView("error"); } else if (updateSuccess) { setModalView("updateCompleted"); } else { setModalView("loading"); } }, [otaState.updating, otaState.error, setModalView, updateSuccess]); return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; } export function Dialog({ onClose, onConfirmUpdate, }: Readonly<{ onClose: () => void; onConfirmUpdate: () => void; }>) { const { navigateTo } = useDeviceUiNavigation(); const [versionInfo, setVersionInfo] = useState(null); const { modalView, setModalView, otaState } = useUpdateStore(); const onFinishedLoading = useCallback( (versionInfo: SystemVersionInfo) => { const hasUpdate = versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable; setVersionInfo(versionInfo); if (hasUpdate) { setModalView("updateAvailable"); } else { setModalView("upToDate"); } }, [setModalView], ); return (
{modalView === "error" && ( setModalView("loading")} /> )} {modalView === "loading" && ( )} {modalView === "updateAvailable" && ( )} {modalView === "updating" && ( navigateTo("/")} /> )} {modalView === "upToDate" && ( setModalView("loading")} onClose={onClose} /> )} {modalView === "updateCompleted" && }
); } function LoadingState({ onFinished, onCancelCheck, }: { onFinished: (versionInfo: SystemVersionInfo) => void; onCancelCheck: () => void; }) { const [progressWidth, setProgressWidth] = useState("0%"); const abortControllerRef = useRef(null); const { getVersionInfo } = useVersion(); const { setModalView } = useUpdateStore(); const progressBarRef = useRef(null); useEffect(() => { abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; const animationTimer = setTimeout(() => { // we start the progress bar animation after a tiny delay to avoid react warnings setProgressWidth("100%"); }, 0); getVersionInfo() .then(async versionInfo => { // Add a small delay to ensure it's not just flickering await sleep(600); return versionInfo }) .then(versionInfo => { if (!signal.aborted) { onFinished(versionInfo); } }) .catch(error => { if (!signal.aborted) { console.error("LoadingState: Error fetching version info", error); setModalView("error"); } }); return () => { clearTimeout(animationTimer); abortControllerRef.current?.abort(); }; }, [getVersionInfo, onFinished, setModalView]); return (

{m.general_update_checking_title()}

{m.general_update_checking_description()}

); } function UpdatingDeviceState({ otaState, onMinimizeUpgradeDialog, }: { otaState: UpdateState["otaState"]; onMinimizeUpgradeDialog: () => void; }) { interface ProgressSummary { system: UpdatePart; app: UpdatePart; areAllUpdatesComplete: boolean; }; const progress = useMemo(() => { 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; } if (type === "app") { // App: 55% download, 54% verification, 1% update(There is no "real" update for the app) return Math.round(Math.min( downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01, 100, )); } else { // System: 10% download, 10% verification, 80% update return Math.round(Math.min( downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8, 100, )); } }; const getUpdateStatus = (type: "system" | "app") => { const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`]; const verifiedAt = otaState[`${type}VerifiedAt`]; const updatedAt = otaState[`${type}UpdatedAt`]; const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_application_type()); if (!otaState.metadataFetchedAt) { return m.general_update_status_fetching(); } else if (!downloadFinishedAt) { return m.general_update_status_downloading({ update_type: update_type() }); } else if (!verifiedAt) { return m.general_update_status_verifying({ update_type: update_type() }); } else if (!updatedAt) { return m.general_update_status_installing({ update_type: update_type() }); } else { return m.general_update_status_awaiting_reboot(); } }; const isUpdateComplete = (type: "system" | "app") => { return !!otaState[`${type}UpdatedAt`]; }; const systemUpdatePending = otaState.systemUpdatePending const systemUpdateComplete = isUpdateComplete("system"); const appUpdatePending = otaState.appUpdatePending const appUpdateComplete = isUpdateComplete("app"); let areAllUpdatesComplete: boolean; if (!systemUpdatePending && !appUpdatePending) { areAllUpdatesComplete = false; } else if (systemUpdatePending && appUpdatePending) { areAllUpdatesComplete = systemUpdateComplete && appUpdateComplete; } else { areAllUpdatesComplete = systemUpdatePending ? systemUpdateComplete : appUpdateComplete; } return { system: { pending: systemUpdatePending, status: getUpdateStatus("system"), progress: calculateOverallProgress("system"), complete: systemUpdateComplete, }, app: { pending: appUpdatePending, status: getUpdateStatus("app"), progress: calculateOverallProgress("app"), complete: appUpdateComplete, }, areAllUpdatesComplete, }; }, [otaState]); return (

{m.general_update_updating_title()}

{m.general_update_updating_description()}

{progress.areAllUpdatesComplete ? (
{m.general_update_rebooting()}
) : ( <> {!(progress.system.pending || progress.app.pending) && (
)} {progress.system.pending && ( )} {progress.system.pending && progress.app.pending && (
)} {progress.app.pending && ( )} )}
); } function SystemUpToDateState({ checkUpdate, onClose, }: { checkUpdate: () => void; onClose: () => void; }) { return (

{m.general_update_up_to_date_title()}

{m.general_update_up_to_date_description()}

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

{m.general_update_available_title()}

{m.general_update_available_description()}

{versionInfo?.systemUpdateAvailable ? ( <> {m.general_update_system_type()}: {versionInfo?.remote?.systemVersion}
) : null} {versionInfo?.appUpdateAvailable ? ( <> {m.general_update_application_type()}: {versionInfo?.remote?.appVersion} ) : null}

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

{m.general_update_completed_title()}

{m.general_update_completed_description()}

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

{m.general_update_error_title()}

{m.general_update_error_description()}

{errorMessage && (

{m.general_update_error_details({ errorMessage })}

)}
); }