import { useCallback, useEffect, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { useJsonRpc } from "@hooks/useJsonRpc"; import { UpdateState, useUpdateStore } from "@hooks/stores"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; import { SystemVersionInfo, useVersion } from "@hooks/useVersion"; import { Button } from "@components/Button"; import Card from "@components/Card"; import LoadingSpinner from "@components/LoadingSpinner"; import { m } from "@localizations/messages.js"; 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 progressBarRef = useRef(null); useEffect(() => { abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; const animationTimer = setTimeout(() => { setProgressWidth("100%"); }, 0); getVersionInfo() .then(versionInfo => { // Add a small delay to ensure it's not just flickering return new Promise(resolve => setTimeout(() => resolve(versionInfo), 600)); }) .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 (

{m.general_update_checking_title()}

{m.general_update_checking_description()}

); } function UpdatingDeviceState({ otaState, onMinimizeUpgradeDialog, }: { otaState: UpdateState["otaState"]; onMinimizeUpgradeDialog: () => void; }) { 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`]; 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 }); } else if (!verfiedAt) { return m.general_update_status_verifying({ update_type }); } else if (!updatedAt) { return m.general_update_status_installing({ update_type }); } else { return m.general_update_status_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")) ); }; const systemOverallProgress = calculateOverallProgress("system"); const systemUpdateStatus = getUpdateStatus("system"); const appOverallProgress = calculateOverallProgress("app"); const appUpdateStatus = getUpdateStatus("app"); return (

{m.general_update_updating_title()}

{m.general_update_updating_description()}

{areAllUpdatesComplete() ? (
{m.general_update_rebooting()}
) : ( <> {!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
)} {otaState.systemUpdatePending && (

{m.general_update_system_update_title()}

{systemOverallProgress < 100 ? ( ) : ( )}
{systemUpdateStatus}{" "} {systemOverallProgress < 100 ? ({`${systemOverallProgress}%`}) : null}
)} {otaState.appUpdatePending && ( <> {otaState.systemUpdatePending && (
)}

{m.general_update_app_update_title()}

{appOverallProgress < 100 ? ( ) : ( )}
{appUpdateStatus}{" "} {appOverallProgress < 100 ? ({`${appOverallProgress}%`}) : null}
)} )}
); } 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 })}

)}
); }