import { memo, useContext, useMemo, 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 PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; 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 { 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 type { VersionInfo } from '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; } // Memoized components for better performance const VersionInfoDialog = memo( ({ showVersionInfo, latestVersion, latestDevVersion, partitionVersion, partition, currentPartition, size, locale, LL, onClose }: { showVersionInfo: number; latestVersion: VersionInfo | undefined; latestDevVersion: VersionInfo | undefined; 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?.version : version?.version} {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 && version.date && ( {isPartition ? 'Install Date' : 'Build Date'} {prettyDateTime(locale, new Date(version.date))} )}
{!isPartition && ( )}
); } ); const InstallDialog = memo( ({ openInstallDialog, fetchDevVersion, latestVersion, latestDevVersion, upgradeImportantMessageType, downloadOnly, platform, LL, onClose, onInstall }: { openInstallDialog: boolean; fetchDevVersion: boolean; latestVersion: VersionInfo | undefined; latestDevVersion: VersionInfo | undefined; upgradeImportantMessageType: number; downloadOnly: boolean; platform: string; LL: TranslationFunctions; onClose: () => void; onInstall: (url: string) => void; }) => { const binURL = (() => { if (!latestVersion || !latestDevVersion) return ''; const version = fetchDevVersion ? latestDevVersion : latestVersion; const filename = `EMS-ESP-${version.version.replaceAll('.', '_')}-${platform}.bin`; return fetchDevVersion ? `${DEV_URL}${filename}` : `${STABLE_URL}v${version.version}/${filename}`; })(); return ( {`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`} {LL.INSTALL_VERSION( downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(), fetchDevVersion ? latestDevVersion?.version : latestVersion?.version )} {upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()} {upgradeImportantMessageType === 1 && ( <> {LL.UPGRADE_IMPORTANT_MESSAGES_1()} {LL.DOWNLOAD_SYSTEM_BACKUP()} )} {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, versions } = useContext(AuthenticatedContext); const [restarting, setRestarting] = useState(false); const [confirmFactoryReset, setConfirmFactoryReset] = useState(false); const [confirmRestart, setConfirmRestart] = useState(false); const [openInstallDialog, setOpenInstallDialog] = useState(false); const [partitionVersion, setPartitionVersion] = useState( undefined ); const [partition, setPartition] = useState(''); const [openInstallPartitionDialog, setOpenInstallPartitionDialog] = useState(false); const [fetchDevVersion, setFetchDevVersion] = useState(false); const [downloadOnly, setDownloadOnly] = useState(false); const [showVersionInfo, setShowVersionInfo] = useState(0); // 1 = stable, 2 = dev, 3 = partition const [firmwareSize, setFirmwareSize] = useState(0); const latestVersion = useMemo( () => versions?.stable ? { version: versions.stable.version, date: versions.stable.date } : undefined, [versions?.stable] ); const latestDevVersion = useMemo( () => versions?.dev ? { version: versions.dev.version, date: versions.dev.date } : undefined, [versions?.dev] ); const usingDevVersion = versions?.current?.type === 'dev'; const stableUpgradeAvailable = versions?.stable?.upgradeable ?? false; const devUpgradeAvailable = versions?.dev?.upgradeable ?? false; const internetLive = Boolean(versions?.stable || versions?.dev); 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); } }); const { send: sendUploadURL } = useRequest( (url: string) => callAction({ action: 'uploadURL', param: url }), { immediate: false } ); 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')); }); const platform = data ? getPlatform(data) : ''; const otherPartitions = data?.partitions.filter((p) => p.partition !== data.partition) ?? []; const setPartitionVersionInfo = (partition: string) => { setShowVersionInfo(3); const partitionData = data?.partitions.find((p) => p.partition === partition); if (partitionData) { setPartitionVersion({ version: partitionData.version, date: partitionData.install_date ?? '' }); setPartition(partitionData.partition); setFirmwareSize(partitionData.size); } }; const doRestart = async () => { setConfirmRestart(false); await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( (error: Error) => { toast.error(error.message); } ); setRestarting(true); }; const doFormat = async () => { await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => { setRestarting(true); setConfirmFactoryReset(false); }); }; const handleFactoryResetClose = () => setConfirmFactoryReset(false); const handleFactoryResetClick = () => setConfirmFactoryReset(true); const handleRestartClose = () => setConfirmRestart(false); const handleRestartClick = () => setConfirmRestart(true); const installFirmwareURL = async (url: string) => { await sendUploadURL(url).catch((error: Error) => { toast.error(error.message); }); await doRestart(); }; const installPartitionFirmware = async (partition: string) => { await sendSetPartition(partition).catch((error: Error) => { toast.error(error.message); }); setRestarting(true); }; const showPartitionDialog = ( version: string, partition: string, install_date: string ) => { setOpenInstallPartitionDialog(true); setPartitionVersion({ version: version, date: install_date }); setPartition(partition); }; const showFirmwareDialog = (useDevVersion: boolean) => { setFetchDevVersion(useDevVersion); const targetVersion = useDevVersion ? latestDevVersion?.version : latestVersion?.version; if (targetVersion) { void checkUpgradeImportantMessages(targetVersion); } setOpenInstallDialog(true); }; const closeInstallDialog = () => setOpenInstallDialog(false); const closeInstallPartitionDialog = () => setOpenInstallPartitionDialog(false); const handleVersionInfoClose = () => { setShowVersionInfo(0); setPartitionVersion(undefined); setPartition(''); }; useLayoutTitle('EMS-ESP Firmware'); const showButtons = (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; const isUpdateAvailable = choice === LL.UPDATE_AVAILABLE(); return ( ); }; if (restarting) { return ; } 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?.version} setShowVersionInfo(1)} aria-label={LL.FIRMWARE_VERSION_INFO()} > {showButtons(false)} {LL.DEVELOPMENT()} {latestDevVersion?.version} setShowVersionInfo(2)} aria-label={LL.FIRMWARE_VERSION_INFO()} > {showButtons(true)} ) : ( {LL.INTERNET_CONNECTION_REQUIRED()} )} {me.admin && ( <> {LL.UPLOAD()} )} {me.admin && ( <> {LL.FACTORY_RESET()} {LL.SYSTEM_FACTORY_TEXT_DIALOG()} {LL.RESTART()} {LL.RESTART_CONFIRM()} {data.developer_mode && ( )} )} ); }; export default memo(Version);