wrolpi/app/src/App.js
Roland c95afc7f62 Add ESP32 firmware flasher
Browser-based ESP32/ESP8266 flasher (esptool-js + Web Serial); nothing is
uploaded to WROLPi, the flash is written straight to the device over USB.

Firmware sources (themed, tabbed): a saved set, the WROLPi media directory,
or a local file. Local files can be dropped anywhere on the page, which jumps
to the "Add from computer" dropzone. The media picker searches .bin files by
path and can detect the connected chip to filter firmware to matching images.

- Indexed file_group.suffix + path filter for fast .bin search (alembic
  migration backfills via split_path_stem_and_suffix)
- modules/flasher: chip detection from ESP image headers, /search endpoint,
  and flasher.yaml saved configurations (/configs CRUD)
- Flash progress modal with ETA, connect timeout, boot-mode hint, auto-
  scrolling logs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:34:04 -06:00

128 lines
5.8 KiB
JavaScript

import React, {useContext} from 'react';
import './App.css';
import {NavBar} from "./components/Nav";
import {createBrowserRouter, createRoutesFromElements, Link, Outlet, Route, RouterProvider} from "react-router";
import {VideosTabLayout, VideosPage, VideosSettingsPage, VideosStatistics, VideoWrapper} from "./components/Videos";
import AdminRoute from "./components/admin/AdminRoute";
import {Container} from "semantic-ui-react";
import 'semantic-ui-offline/semantic.min.css';
import {MoreRoute} from "./components/Apps";
import {InventoryRoute} from "./components/inventory/InventoryRoute";
import {ArchiveRoute} from "./components/Archive";
import {FilesRoute} from "./components/Files";
import {QueryProvider, StatusProvider} from "./hooks/customHooks";
import {FileWorkerStatusProvider} from "./contexts/FileWorkerStatusContext";
import {ChannelEditPage, ChannelNewPage, ChannelsPage} from "./components/Channels";
import {MapRoute} from "./components/Map";
import {MediaContextProvider, mediaStyles, StatusContext, ThemeContext} from "./contexts/contexts";
import {Header, ThemeProvider} from "./components/Theme";
import {DashboardPage} from "./DashboardPage";
import {DonatePage} from "./components/DonatePage";
import {useEventsInterval} from "./Events";
import {SemanticToastContainer} from "react-semantic-toasts-2";
import {FilePreviewProvider} from "./components/FilePreview";
import {TagsProvider} from "./Tags";
import {ZimRoute} from "./components/Zim";
import {PlaylistsRoute} from "./components/Playlists";
import {DocsRoute} from "./components/Docs";
import {FlasherRoute} from "./components/Flasher";
import ErrorBoundary from "./components/ErrorBoundary";
import {KeyboardShortcutsProvider} from "./components/KeyboardShortcutsProvider";
function PageNotFound() {
const {t} = useContext(ThemeContext);
return <Container fluid>
<Header as='h1'>Page Not Found!</Header>
<p {...t}>The page you requested cannot be found</p>
</Container>
}
function Dot() {
return <>&nbsp;•&nbsp;</>
}
function Footer() {
const {t} = useContext(ThemeContext);
const {status} = useContext(StatusContext);
let version;
try {
version = status?.version ? `v${status.version}` : null;
} catch (e) {
// Not logging because this is not that important.
}
return <Container textAlign='center' style={{marginTop: '1.5em', marginBottom: '1em', ...t}}>
<span {...t}>
WROLPi {version} <Dot/>
<a href='https://wrolpi.org' target='_blank' rel='nofollow noreferrer'>WROLPi.org</a> <Dot/>
<Link to='/donate'>Donate</Link>
</span>
</Container>
}
function Root() {
return <QueryProvider>
<ThemeProvider>
<TagsProvider>
<KeyboardShortcutsProvider>
<FilePreviewProvider>
<header>
<NavBar/>
</header>
<ErrorBoundary>
<Outlet/>
</ErrorBoundary>
<Footer/>
</FilePreviewProvider>
</KeyboardShortcutsProvider>
</TagsProvider>
</ThemeProvider>
</QueryProvider>
}
const router = createBrowserRouter(createRoutesFromElements(<Route
path='/'
element={<Root/>}
errorElement={<PageNotFound/>}
>
<Route index element={<ErrorBoundary><DashboardPage/></ErrorBoundary>}/>
<Route path='search/*' element={<ErrorBoundary><DashboardPage/></ErrorBoundary>}/>
<Route path='donate' element={<DonatePage/>}/>
<Route path='videos'>
<Route element={<ErrorBoundary><VideosTabLayout/></ErrorBoundary>}>
<Route index element={<ErrorBoundary><VideosPage/></ErrorBoundary>}/>
<Route path='channel' element={<ErrorBoundary><ChannelsPage/></ErrorBoundary>}/>
<Route path='channel/new' element={<ErrorBoundary><ChannelNewPage/></ErrorBoundary>}/>
<Route path='channel/:channelId/edit' element={<ErrorBoundary><ChannelEditPage/></ErrorBoundary>}/>
<Route path='channel/:channelId/video' element={<ErrorBoundary><VideosPage/></ErrorBoundary>}/>
<Route path='settings' element={<ErrorBoundary><VideosSettingsPage/></ErrorBoundary>}/>
<Route path='statistics' element={<ErrorBoundary><VideosStatistics/></ErrorBoundary>}/>
</Route>
<Route path=':fileGroupId' element={<ErrorBoundary><VideoWrapper/></ErrorBoundary>}/>
</Route>
<Route path="admin/*" element={<ErrorBoundary><AdminRoute/></ErrorBoundary>}/>
<Route path="more/*" element={<ErrorBoundary><MoreRoute/></ErrorBoundary>}/>
<Route path="inventory/*" element={<ErrorBoundary><InventoryRoute/></ErrorBoundary>}/>
<Route path='archives/*' element={<ErrorBoundary><ArchiveRoute/></ErrorBoundary>}/>
<Route path='docs/*' element={<ErrorBoundary><DocsRoute/></ErrorBoundary>}/>
<Route path='map/*' element={<ErrorBoundary><MapRoute/></ErrorBoundary>}/>
<Route path='zim/*' element={<ErrorBoundary><ZimRoute/></ErrorBoundary>}/>
<Route path='playlists/*' element={<ErrorBoundary><PlaylistsRoute/></ErrorBoundary>}/>
<Route path='files/*' element={<ErrorBoundary><FilesRoute/></ErrorBoundary>}/>
<Route path='flasher/*' element={<ErrorBoundary><FlasherRoute/></ErrorBoundary>}/>
</Route>));
export default function App() {
useEventsInterval();
return <StatusProvider>
<FileWorkerStatusProvider>
{/* Context and style to handle switching between mobile/computer. */}
<style>{mediaStyles}</style>
{/* Toasts can be on any page. */}
<SemanticToastContainer position='top-right'/>
<MediaContextProvider>
<RouterProvider router={router}/>
</MediaContextProvider>
</FileWorkerStatusProvider>
</StatusProvider>
}