mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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>
128 lines
5.8 KiB
JavaScript
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 <> • </>
|
|
}
|
|
|
|
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>
|
|
}
|