import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Link } from 'react-router'; 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, Dialog, DialogActions, DialogContent, DialogTitle, Grid, IconButton, 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 PartitionData { partition: string; version: string; install_date?: string; size: number; } interface VersionData { emsesp_version: string; arduino_version: string; esp_platform: string; flash_chip_size: number; psram: boolean; build_flags?: string; partition: string; partitions: PartitionData[]; developer_mode: boolean; } 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, partitionVersion, partition, currentPartition, size, locale, LL, onClose }: { showVersionInfo: number; latestVersion?: VersionInfo; latestDevVersion?: VersionInfo; partitionVersion?: VersionInfo | undefined; partition: string; currentPartition: string; size: number; locale: string; LL: TranslationFunctions; onClose: () => void; }) => { if (showVersionInfo === 0) return null; const isStable = showVersionInfo === 1; const isDev = showVersionInfo === 2; const isPartition = showVersionInfo === 3; const version = isStable ? latestVersion : isDev ? latestDevVersion : partitionVersion; const relNotesUrl = isStable ? STABLE_RELNOTES_URL : isDev ? DEV_RELNOTES_URL : ''; return ( {LL.FIRMWARE_VERSION_INFO()} {LL.VERSION()} {isPartition ? typeof version === 'string' ? version : version?.name : version?.name} {isPartition ? LL.TYPE(0) : LL.RELEASE_TYPE()} {partition === currentPartition && LL.ACTIVE() + ' '} {isStable ? LL.STABLE() : isDev ? LL.DEVELOPMENT() : 'Partition ' + LL.VERSION()} {isPartition && ( Partition {partition} )} {isPartition && ( Size {size} KB )} {version?.published_at && ( {isPartition ? 'Install Date' : 'Build Date'} {prettyDateTime(locale, new Date(version.published_at))} )}
{!isPartition && ( )}
); } ); const InstallDialog = memo( ({ openInstallDialog, fetchDevVersion, latestVersion, latestDevVersion, upgradeImportantMessageType, downloadOnly, platform, LL, onClose, onInstall }: { openInstallDialog: boolean; fetchDevVersion: boolean; latestVersion?: VersionInfo; latestDevVersion?: VersionInfo; upgradeImportantMessageType: number; 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.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`} {LL.INSTALL_VERSION( downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(), fetchDevVersion ? latestDevVersion?.name : latestVersion?.name )} {upgradeImportantMessageType === 1 && LL.UPGRADE_IMPORTANT_MESSAGES_1()} {upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()} {LL.ONLINE_HELP()} {!downloadOnly && ( )} ); } ); const InstallPartitionDialog = memo( ({ openInstallPartitionDialog, version, partition, LL, onClose, onInstall }: { openInstallPartitionDialog: boolean; version: string; partition: string; LL: TranslationFunctions; onClose: () => void; onInstall: (partition: string) => void; }) => { return ( {LL.INSTALL()} {LL.STORED_VERSIONS()} {LL.INSTALL_VERSION(LL.INSTALL(), version)} ); } ); // 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 [partitionVersion, setPartitionVersion] = useState( undefined ); const [partition, setPartition] = useState(''); const [openInstallPartitionDialog, setOpenInstallPartitionDialog] = 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); // 1 = stable, 2 = dev, 3 = partition const [firmwareSize, setFirmwareSize] = useState(0); 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 { send: sendSetPartition } = useRequest( (partition: string) => callAction({ action: 'setPartition', param: partition }), { immediate: false } ).onError((error) => { toast.error(String(error.error?.message || 'An error occurred')); }); 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 }); const [upgradeImportantMessageType, setUpgradeImportantMessageType] = useState(0); const { send: checkUpgradeImportantMessages } = useRequest( (version: string) => callAction({ action: 'upgradeImportantMessages', param: version }), { immediate: false } ) .onSuccess((event) => { const upgradeImportantMessageType_n = ( event.data as { upgradeImportantMessageType: number } ).upgradeImportantMessageType; setUpgradeImportantMessageType(upgradeImportantMessageType_n); }) .onError((error) => { toast.error(String(error.error?.message || 'An error occurred')); }); // Memoized values const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]); // Memoize filtered partitions to avoid recomputing on every render const otherPartitions = useMemo( () => data?.partitions.filter((p) => p.partition !== data.partition) ?? [], [data] ); const setPartitionVersionInfo = useCallback( (partition: string) => { setShowVersionInfo(3); // search for the partition in the data.partitions array const partitionData = data?.partitions.find((p) => p.partition === partition); if (partitionData) { setPartitionVersion({ name: partitionData.version, published_at: partitionData.install_date ?? '' }); setPartition(partitionData.partition); setFirmwareSize(partitionData.size); } }, [data] ); const doRestart = useCallback(async () => { await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( (error: Error) => { toast.error(error.message); } ); setRestarting(true); }, [sendAPI]); const installFirmwareURL = useCallback( async (url: string) => { await sendUploadURL(url).catch((error: Error) => { toast.error(error.message); }); await doRestart(); }, [sendUploadURL, doRestart] ); const installPartitionFirmware = useCallback( async (partition: string) => { await sendSetPartition(partition).catch((error: Error) => { toast.error(error.message); }); setRestarting(true); }, [sendSetPartition] ); const showPartitionDialog = useCallback( (version: string, partition: string, install_date: string) => { setOpenInstallPartitionDialog(true); setPartitionVersion({ name: version, published_at: install_date }); setPartition(partition); }, [] ); const showFirmwareDialog = useCallback( (useDevVersion: boolean) => { setFetchDevVersion(useDevVersion); void checkUpgradeImportantMessages( useDevVersion ? latestDevVersion?.name : latestVersion?.name ); setOpenInstallDialog(true); }, [latestDevVersion, latestVersion, fetchDevVersion] ); const closeInstallDialog = useCallback(() => { setOpenInstallDialog(false); }, []); const closeInstallPartitionDialog = useCallback(() => { setOpenInstallPartitionDialog(false); }, []); const handleVersionInfoClose = useCallback(() => { setShowVersionInfo(0); setPartitionVersion(undefined); setPartition(''); }, []); // check upgrades - only once when both versions are available const upgradeCheckedRef = useRef(false); useEffect(() => { if (latestVersion && latestDevVersion && !upgradeCheckedRef.current) { upgradeCheckedRef.current = true; 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}) )} setPartitionVersionInfo(data.partition)} aria-label={LL.FIRMWARE_VERSION_INFO()} > {LL.PLATFORM()} {platform}   ( {data.psram ? ( ) : ( )} PSRAM) {internetLive ? ( <> {LL.AVAILABLE_VERSION()} {otherPartitions.length > 0 && data.developer_mode && ( <> {LL.STORED_VERSIONS()} {otherPartitions.map((partition) => ( {partition.version} setPartitionVersionInfo(partition.partition) } aria-label={LL.FIRMWARE_VERSION_INFO()} > ))} )} {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, internetLive, latestVersion, latestDevVersion, showVersionInfo, locale, openInstallDialog, fetchDevVersion, downloadOnly, me.admin, showButtons, handleVersionInfoClose, closeInstallDialog, installFirmwareURL, doRestart, otherPartitions, setPartitionVersionInfo, showPartitionDialog, partitionVersion, partition, firmwareSize, closeInstallPartitionDialog, installPartitionFirmware ]); return restarting ? : {content}; }; export default memo(Version);