diff --git a/app/src/components/Flasher.js b/app/src/components/Flasher.js
index 17ba230c..fe125a69 100644
--- a/app/src/components/Flasher.js
+++ b/app/src/components/Flasher.js
@@ -1,27 +1,30 @@
-import React, {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react';
+import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {Route, Routes} from "react-router";
-import {Checkbox, Label, Message, Select, Button as SButton, Icon as SIcon} from "semantic-ui-react";
import {ESPLoader, Transport} from "esptool-js";
import {useDropzone} from "react-dropzone";
import _ from "lodash";
import {encodeMediaPath, humanFileSize, PageContainer, useTitle} from "./Common";
import {
Button,
- darkTheme,
+ Checkbox,
Divider,
- Form,
+ Group,
Header,
Icon,
- List,
- Loader,
+ IconButton,
+ Label,
+ Loading,
+ Message,
Modal,
+ Panel,
Progress,
- Segment,
- Tab,
+ Select,
+ Stack,
Table,
- TextArea
-} from "./Theme";
-import {ThemeContext} from "../contexts/contexts";
+ Tabs,
+ Textarea,
+ TextInput,
+} from "./ui";
import {deleteFlasherConfig, flasherSearch, getFlasherConfigs, saveFlasherConfig} from "../api";
// The suffix used to find flashable ESP32 firmware in the media directory.
@@ -50,6 +53,10 @@ const KNOWN_ESP_CHIPS = [
// colors when they render at the same width. The first eight cover today's largest same-width group (the eight
// 8-character ESP32-* names); the extras let a same-width group grow past eight before colors would repeat.
// (Truly colorblind-distinct qualitative colors top out around ten, so beyond that distinctness is best-effort.)
+//
+// These are deliberately NOT theme tokens: this is a fixed categorical palette used to visually tell many ESP
+// chip families apart, the same way a chart's series colors stay fixed regardless of theme. See the report to
+// the user for the resulting night-mode tension (non-red hues on these chips, unlike the rest of the UI).
const CHIP_COLORS = [
'#4477aa', '#ee6677', '#228833', '#ccbb44', '#66ccee', '#aa3377', '#bbbbbb', '#000000',
'#ee7733', '#332288', '#999933', '#882255',
@@ -115,10 +122,10 @@ export function espImageChipId(header) {
// bundled into the frontend build, so this works fully offline once the page has loaded.
const BAUD_OPTIONS = [
- {key: '115200', value: 115200, text: '115200 (safe)'},
- {key: '230400', value: 230400, text: '230400'},
- {key: '460800', value: 460800, text: '460800'},
- {key: '921600', value: 921600, text: '921600 (fast)'},
+ {value: 115200, text: '115200 (safe)'},
+ {value: 230400, text: '230400'},
+ {value: 460800, text: '460800'},
+ {value: 921600, text: '921600 (fast)'},
];
// Web Serial is only available in a secure context (HTTPS or localhost) on Chromium-based browsers.
@@ -218,10 +225,10 @@ export function withTimeout(promise, ms, message) {
// How long to wait for a device to connect before giving up (esptool-js retries can otherwise stall the UI).
const CONNECT_TIMEOUT_MS = 30_000;
-// A read-only log TextArea that auto-scrolls to the newest output — but only when the user is already at the
+// A read-only log Textarea that auto-scrolls to the newest output — but only when the user is already at the
// bottom, so scrolling up to read earlier lines isn't yanked back down. Starts at the bottom on mount (e.g. when
// a modal opens).
-function AutoScrollLog({value, ...props}) {
+function AutoScrollLog({value, style, ...props}) {
const containerRef = useRef(null);
const atBottomRef = useRef(true);
@@ -241,13 +248,18 @@ function AutoScrollLog({value, ...props}) {
}, [value]);
return
-
+
;
}
+// Tab keys for the firmware-source picker, in display order: saved sets first, then the media picker, then a
+// local file.
+const TAB_SAVED = 'saved';
+const TAB_WROLPI = 'wrolpi';
+const TAB_COMPUTER = 'computer';
+
export function FlasherPage() {
useTitle('Flasher');
- const {t, theme} = useContext(ThemeContext);
// Each entry is {name, size, address, file?: File, mediaPath?: string}. A local file has `file`; a media
// firmware has `mediaPath`. Default offset 0x0 suits a single merged firmware image.
@@ -283,7 +295,7 @@ export function FlasherPage() {
const [flashError, setFlashError] = useState('');
// Which firmware-source tab is active (controlled so a file drop can switch to "Add from computer").
- const [activeTab, setActiveTab] = useState(0);
+ const [activeTab, setActiveTab] = useState(TAB_SAVED);
// Saved firmware configurations (flasher.yaml).
const [savedConfigs, setSavedConfigs] = useState([]);
@@ -331,15 +343,14 @@ export function FlasherPage() {
}
};
- // The "Add from computer" tab (index 2 in firmwarePanes) hosts the dropzone; a drop anywhere on the page or a
- // click on the zone lands here.
- const COMPUTER_TAB_INDEX = 2;
+ // The "Add from computer" tab hosts the dropzone; a drop anywhere on the page or a click on the zone lands
+ // here.
const onDrop = React.useCallback((droppedFiles) => {
// Only take .bin firmware; anything else dropped is ignored.
const bins = (droppedFiles || []).filter(file => file.name.toLowerCase().endsWith(FIRMWARE_SUFFIX));
if (bins.length > 0) {
addFiles(bins);
- setActiveTab(COMPUTER_TAB_INDEX);
+ setActiveTab(TAB_COMPUTER);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// The dropzone root wraps the whole page so firmware can be dropped anywhere. noClick/noKeyboard because the
@@ -354,7 +365,7 @@ export function FlasherPage() {
// While the user drags a file over the page, jump to the "Add from computer" tab so the drop target is shown.
useEffect(() => {
if (isDragActive) {
- setActiveTab(COMPUTER_TAB_INDEX);
+ setActiveTab(TAB_COMPUTER);
}
}, [isDragActive]);
@@ -644,15 +655,11 @@ export function FlasherPage() {
if (!webSerialSupported()) {
return Flasher
-
-
-
- Web Serial is not available
-
Flashing requires the Web Serial API, which is only available in Chromium-based browsers
- (Chrome, Edge, Brave) served over a secure connection (HTTPS or localhost).
-
Open this page in Chrome or Edge over https:// to flash your device. Firefox
- and iOS are not supported.
-
+
+
Flashing requires the Web Serial API, which is only available in Chromium-based browsers
+ (Chrome, Edge, Brave) served over a secure connection (HTTPS or localhost).
+
Open this page in Chrome or Edge over https:// to flash your device. Firefox
+ and iOS are not supported.
;
}
@@ -664,409 +671,414 @@ export function FlasherPage() {
const totalFlashBytes = files.reduce((sum, item) => sum + (item.size || 0), 0);
const flashEtaText = humanDuration(estimateFlashSeconds(totalFlashBytes, baudrate));
- // The nested tab menu isn't inverted automatically; give it the inverted style in dark mode so it matches.
- const tabMenu = theme === darkTheme ? {inverted: true, attached: true} : {attached: true};
-
// A connect failure needs a download-mode reminder shown right where the user acted — both in the Connect
- // segment and next to the "Filter files by detecting device" button so it isn't missed off-screen.
+ // panel and next to the "Filter files by detecting device" button so it isn't missed off-screen.
const connectErrorMessage = () => (error && bootHint) &&
- {
- setError('');
- setBootHint(false);
- }} style={{marginTop: '1em'}}>
- Could not connect to the device
-
{error}
-
- Many ESP boards must be put into download mode manually before flashing. Hold the
- {' '}BOOT (or IO0) button, briefly press RESET (EN), then release
- BOOT and try again. Also check the USB cable is a data cable and no other tab has the port open.
-
+ Many ESP boards must be put into download mode manually before flashing. Hold the
+ {' '}BOOT (or IO0) button, briefly press RESET (EN), then release
+ BOOT and try again. Also check the USB cable is a data cable and no other tab has the port open.
+
+
+
;
- // The firmware source is chosen via three tabs (see the ordered array below).
- const computerPane = {
- menuItem: 'Add from computer',
- render: () =>
- {/* Clickable drop target (files may also be dropped anywhere on the page, which routes here). */}
-
+ {/* Clickable drop target (files may also be dropped anywhere on the page, which routes here). */}
+
+
+ {isDragActive
+ ? <>Drop .bin firmware to add it>
+ : <>Click here, or drop .bin firmware here>}
+
+
+
+ A single merged image is flashed at offset 0x0. For multi-part firmware, add each
+ part and set its offset (e.g. bootloader 0x1000, partitions 0x8000,
+ app 0x10000).
+
+ >;
+
+ const wrolpiPaneContent = <>
+
+ Not sure which file you need? Plug in your device and let WROLPi show only the firmware that
+ matches it.
+
+ {/* Always available so a user who swaps to a different ESP device can re-detect it. */}
+
+ {/* Show a connect failure here too, so the user doesn't have to scroll to the Connect panel. */}
+ {connectErrorMessage()}
+ {deviceChip &&
+
+ Showing firmware for {deviceChip}.
+
+ }
+ }
+ placeholder='Filter firmware by path (e.g. a folder name)...'
+ value={mediaFilter}
+ disabled={busy}
+ onChange={(e) => handleMediaFilterChange(e.currentTarget.value)}
+ />
+
+ Configure the firmware files and offsets, then save them as a named set to re-flash later
+ (e.g. a Meshtastic T-Deck: firmware at 0x0, littlefs at 0xc90000).
+
-
-
- {isDragActive
- ? <>Drop .bin firmware to add it>
- : <>Click here, or drop .bin firmware here>}
-
-
-
- A single merged image is flashed at offset 0x0. For multi-part firmware, add each
- part and set its offset (e.g. bootloader 0x1000, partitions 0x8000,
- app 0x10000).
-
- ,
- };
- const wrolpiPane = {
- menuItem: 'Choose from your WROLPi',
- render: () =>
-
- Not sure which file you need? Plug in your device and let WROLPi show only the firmware that
- matches it.
-
- {/* Always available so a user who swaps to a different ESP device can re-detect it. */}
-
- {/* Show a connect failure here too, so the user doesn't have to scroll to the Connect segment. */}
- {connectErrorMessage()}
- {deviceChip &&
-
- {/* Plain (non-inverted) icon: Message backgrounds are light, so a themed white icon
- vanishes. */}
-
- Showing firmware for {deviceChip}.
- applyDeviceChip(null)}
- disabled={busy} style={{marginLeft: '1em'}}>
- Show all firmware
-
- }
- handleMediaFilterChange(value)}
- />
-
-
- Configure the firmware files and offsets, then save them as a named set to re-flash later
- (e.g. a Meshtastic T-Deck: firmware at 0x0, littlefs at 0xc90000).
-
}
- ,
- };
- // Tab order: saved sets first, then the media picker, then a local file.
- const firmwarePanes = [savedPane, wrolpiPane, computerPane];
+
{/* The dropzone root wraps the whole page: firmware .bin files can be dropped anywhere and are routed to
the "Add from computer" tab. noClick is set, so this wrapper never opens the file dialog itself. */}
- Flasher
-
- Flash firmware onto an ESP32/ESP8266 device directly from your browser over USB. Choose one or more
- firmware .bin files — from your computer or from your WROLPi's media directory —
- connect your device, then flash. The flash happens entirely in your browser and is written straight to
- the device over USB.
-
+ Flasher
+
+ Flash firmware onto an ESP32/ESP8266 device directly from your browser over USB. Choose one or
+ more firmware .bin files — from your computer or from your WROLPi's media
+ directory — connect your device, then flash. The flash happens entirely in your browser and
+ is written straight to the device over USB.
+
- {/* Connection errors render inside the Connect segment (below), where the user just clicked. Other
- errors show here at the top. */}
- {error && !bootHint && setError('')}>
- Error
-
{error}
- }
+ {/* Connection errors render inside the Connect panel (below), where the user just clicked. Other
+ errors show here at the top. */}
+ {error && !bootHint && setError('')}>
+
{error}
+ }
-
- 1. Firmware files
- setActiveTab(activeIndex)}
- />
+
+ 1. Firmware files
+
+
+ Saved Firmwares
+ Choose from your WROLPi
+ Add from computer
+
+ {savedPaneContent}
+ {wrolpiPaneContent}
+ {computerPaneContent}
+
- {/* The selected firmware is shown below the source tabs so it stays visible regardless of which tab
- (computer / WROLPi / saved) the user is on. */}
- {files.length > 0 && <>
-
- Selected firmware
-
- {/* Save lives here (not in the Saved Firmwares tab) so the current selection can be saved from
- any source tab. */}
-
- >}
-
+ {/* The selected firmware is shown below the source tabs so it stays visible regardless of which
+ tab (computer / WROLPi / saved) the user is on. */}
+ {files.length > 0 && <>
+
+ Selected firmware
+
+ {/* Save lives here (not in the Saved Firmwares tab) so the current selection can be saved
+ from any source tab. */}
+
+ >}
+
- setSaveModalOpen(false)} size='tiny'>
- Save firmware configuration
-
- setSaveModalOpen(false)} size='sm'>
+ Save firmware configuration
+
+ setSaveName(value)}
+ onChange={(e) => setSaveName(e.currentTarget.value)}
/>
-
+
Saving {saveableFiles.length} firmware file(s) from your WROLPi. An existing configuration
with the same name will be replaced.