import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import CloseIcon from '@mui/icons-material/Close'; import CheckIcon from '@mui/icons-material/Done'; import DownloadIcon from '@mui/icons-material/GetApp'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import WarningIcon from '@mui/icons-material/Warning'; import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Grid, IconButton, Link, Table, TableBody, TableCell, TableRow, Typography } from '@mui/material'; import * as SystemApi from 'api/system'; import { API, callAction } from 'api/app'; import { getDevVersion, getStableVersion } from 'api/system'; import { dialogStyle } from 'CustomTheme'; import { useRequest } from 'alova/client'; import type { APIcall } from 'app/main/types'; import SystemMonitor from 'app/status/SystemMonitor'; import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import type { TranslationFunctions } from 'i18n/i18n-types'; import { prettyDateTime } from 'utils/time'; // Constants moved outside component to avoid recreation const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/'; const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md'; const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/'; const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md'; // Types for better type safety interface VersionData { emsesp_version: string; arduino_version: string; esp_platform: string; flash_chip_size: number; psram: boolean; build_flags?: string; } interface UpgradeCheckData { emsesp_version: string; dev_upgradeable: boolean; stable_upgradeable: boolean; } interface VersionInfo { name: string; published_at?: string; } // Memoized components for better performance const VersionInfoDialog = memo( ({ showVersionInfo, latestVersion, latestDevVersion, locale, LL, onClose }: { showVersionInfo: number; latestVersion?: VersionInfo; latestDevVersion?: VersionInfo; locale: string; LL: TranslationFunctions; onClose: () => void; }) => { if (showVersionInfo === 0) return null; const isStable = showVersionInfo === 1; const version = isStable ? latestVersion : latestDevVersion; const relNotesUrl = isStable ? STABLE_RELNOTES_URL : DEV_RELNOTES_URL; return ( {LL.FIRMWARE_VERSION_INFO()} {LL.TYPE(0)} {isStable ? LL.STABLE() : LL.DEVELOPMENT()} {LL.VERSION()} {version?.name} {version?.published_at && ( Build Date {prettyDateTime(locale, new Date(version.published_at))} )}
); } ); const InstallDialog = memo( ({ openInstallDialog, fetchDevVersion, latestVersion, latestDevVersion, downloadOnly, platform, LL, onClose, onInstall }: { openInstallDialog: boolean; fetchDevVersion: boolean; latestVersion?: VersionInfo; latestDevVersion?: VersionInfo; downloadOnly: boolean; platform: string; LL: TranslationFunctions; onClose: () => void; onInstall: (url: string) => void; }) => { const binURL = useMemo(() => { if (!latestVersion || !latestDevVersion) return ''; const version = fetchDevVersion ? latestDevVersion : latestVersion; const filename = `EMS-ESP-${version.name.replaceAll('.', '_')}-${platform}.bin`; return fetchDevVersion ? `${DEV_URL}${filename}` : `${STABLE_URL}v${version.name}/${filename}`; }, [fetchDevVersion, latestVersion, latestDevVersion, platform]); return ( {`${LL.UPDATE()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`} {LL.INSTALL_VERSION( downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(), fetchDevVersion ? latestDevVersion?.name : latestVersion?.name )} {!downloadOnly && ( )} ); } ); // Helper function moved outside component const getPlatform = (data: VersionData): string => { return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`; }; const Version = () => { const { LL, locale } = useI18nContext(); const { me } = useContext(AuthenticatedContext); // State management const [restarting, setRestarting] = useState(false); const [openInstallDialog, setOpenInstallDialog] = useState(false); const [usingDevVersion, setUsingDevVersion] = useState(false); const [fetchDevVersion, setFetchDevVersion] = useState(false); const [devUpgradeAvailable, setDevUpgradeAvailable] = useState(false); const [stableUpgradeAvailable, setStableUpgradeAvailable] = useState(false); const [internetLive, setInternetLive] = useState(false); const [downloadOnly, setDownloadOnly] = useState(false); const [showVersionInfo, setShowVersionInfo] = useState(0); // API calls with optimized error handling const { send: sendCheckUpgrade } = useRequest( (versions: string) => callAction({ action: 'checkUpgrade', param: versions }), { immediate: false } ).onSuccess((event) => { const data = event.data as UpgradeCheckData; setDevUpgradeAvailable(data.dev_upgradeable); setStableUpgradeAvailable(data.stable_upgradeable); }); const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus).onSuccess((event) => { const systemData = event.data as VersionData; if (systemData.arduino_version.startsWith('Tasmota')) { setDownloadOnly(true); } setUsingDevVersion(systemData.emsesp_version.includes('dev')); }); const { send: sendUploadURL } = useRequest( (url: string) => callAction({ action: 'uploadURL', param: url }), { immediate: false } ); const { data: latestVersion } = useRequest(getStableVersion); const { data: latestDevVersion } = useRequest(getDevVersion); const { send: sendAPI } = useRequest((data: APIcall) => API(data), { immediate: false }); // Memoized values const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]); const isDev = useMemo( () => data?.emsesp_version.includes('dev') ?? false, [data?.emsesp_version] ); const doRestart = useCallback(async () => { setRestarting(true); await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( (error: Error) => { toast.error(error.message); } ); }, [sendAPI]); const installFirmwareURL = useCallback( async (url: string) => { await sendUploadURL(url).catch((error: Error) => { toast.error(error.message); }); setRestarting(true); }, [sendUploadURL] ); const showFirmwareDialog = useCallback((useDevVersion: boolean) => { setFetchDevVersion(useDevVersion); setOpenInstallDialog(true); }, []); const closeInstallDialog = useCallback(() => { setOpenInstallDialog(false); }, []); const handleVersionInfoClose = useCallback(() => { setShowVersionInfo(0); }, []); // Effect for checking upgrades useEffect(() => { if (latestVersion && latestDevVersion) { const versions = `${latestDevVersion.name},${latestVersion.name}`; sendCheckUpgrade(versions) .catch((error: Error) => { toast.error(`Failed to check for upgrades: ${error.message}`); }) .finally(() => { setInternetLive(true); }); } }, [latestVersion, latestDevVersion, sendCheckUpgrade]); useLayoutTitle('EMS-ESP Firmware'); // Memoized button rendering logic const showButtons = useCallback( (showingDev: boolean) => { const choice = showingDev ? !usingDevVersion ? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT()) : devUpgradeAvailable ? LL.UPDATE_AVAILABLE() : undefined : usingDevVersion ? LL.SWITCH_RELEASE_TYPE(LL.STABLE()) : stableUpgradeAvailable ? LL.UPDATE_AVAILABLE() : undefined; if (!choice) { return ( <> {LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())} ); } if (!me.admin) return null; return ( ); }, [ usingDevVersion, devUpgradeAvailable, stableUpgradeAvailable, me.admin, LL, showFirmwareDialog ] ); const content = useMemo(() => { if (!data) { return ; } return ( <> {LL.THIS_VERSION()} {LL.VERSION()} {data.emsesp_version} {data.build_flags && (   ({data.build_flags}) )} {LL.PLATFORM()} {platform}   ( {data.psram ? ( ) : ( )} PSRAM) {LL.RELEASE_TYPE()} } slotProps={{ typography: { color: 'grey' } }} checked={!isDev} label={LL.STABLE()} sx={{ '& .MuiSvgIcon-root': { fontSize: 16 } }} /> } slotProps={{ typography: { color: 'grey' } }} checked={isDev} label={LL.DEVELOPMENT()} sx={{ '& .MuiSvgIcon-root': { fontSize: 16 } }} /> {internetLive ? ( <> {LL.AVAILABLE_VERSION()} {LL.STABLE()} {latestVersion?.name} setShowVersionInfo(1)} aria-label={LL.FIRMWARE_VERSION_INFO()} > {showButtons(false)} {LL.DEVELOPMENT()} {latestDevVersion?.name} setShowVersionInfo(2)} aria-label={LL.FIRMWARE_VERSION_INFO()} > {showButtons(true)} ) : ( {LL.INTERNET_CONNECTION_REQUIRED()} )} {me.admin && ( <> {LL.UPLOAD()} )} ); }, [ data, error, loadData, LL, platform, isDev, internetLive, latestVersion, latestDevVersion, showVersionInfo, locale, openInstallDialog, fetchDevVersion, downloadOnly, me.admin, showButtons, handleVersionInfoClose, closeInstallDialog, installFirmwareURL, doRestart ]); return {restarting ? : content}; }; export default memo(Version);