From 75ced3fa0eb9e277e35ef67a5778b6d8d36e5802 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Wed, 10 Jun 2026 02:52:26 -0400 Subject: [PATCH] feat(App): implement lazy loading for RealityOverridePage and add a loading spinner for improved performance feat(MediaManager): lazy load MediaViewerEditor and enhance file viewer overlay with loading state feat(MediaViewerEditor): create a new component for handling various media types with mode switching fix(config): disable unnecessary drives and networks in configuration for better resource management --- src/app/App.tsx | 141 ++++++------- src/app/components/MediaManager.tsx | 222 +++----------------- src/app/components/MediaViewerEditor.tsx | 251 +++++++++++++++++++++++ src/imports/config.json | 58 +++--- 4 files changed, 373 insertions(+), 299 deletions(-) create mode 100644 src/app/components/MediaViewerEditor.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 02777c1..b708978 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,20 +1,24 @@ -import { useEffect, useState } from 'react'; +import { lazy, Suspense, useEffect, useState } from 'react'; import { Cpu, Settings, Wifi, Network, HardDrive, Activity, Search, Wrench, User, LogOut, Bell, FileText, AppWindow, Folder, Edit, Eye, Database, Upload, Download, Code2, LayoutList, Image, ChevronLeft, Loader2, Terminal, Link, Printer, Maximize2, Minimize2 } from 'lucide-react'; import { Toaster, toast } from 'sonner'; -import StatusPage from './components/StatusPage'; -import DevicesPage from './components/DevicesPage'; -import GeneralPage from './components/GeneralPage'; -import NetworkPage from './components/NetworkPage'; -import IECPage from './components/IECPage'; -import ToolsPage from './components/ToolsPage'; -import SearchOverlay from './components/SearchOverlay'; -import MediaManager from './components/MediaManager'; -import RealityOverridePage from './components/RealityOverridePage'; +import StatusPage from './components/StatusPage'; +import DevicesPage from './components/DevicesPage'; +import GeneralPage from './components/GeneralPage'; +import NetworkPage from './components/NetworkPage'; +import IECPage from './components/IECPage'; +import ToolsPage from './components/ToolsPage'; +import SearchOverlay from './components/SearchOverlay'; import RealityOverrideAdminPage from './components/RealityOverrideAdminPage'; +import MediaManager from './components/MediaManager'; import logoSvg from '../imports/logo.svg'; import { useSettings } from './settings'; import { WsProvider } from './ws'; +// Three.js lives only in RealityOverridePage — keep lazy so it doesn't load on startup. +// CodeMirror/syntax-highlighter/ReactMarkdown live in MediaViewerEditor — lazy-loaded +// inside MediaManager when the user first opens a file to view or edit. +const RealityOverridePage = lazy(() => import('./components/RealityOverridePage')); + type Page = 'status' | 'devices' | 'iec' | 'network' | 'general' | 'tools' | 'apps' | AppId; type AppId = @@ -41,6 +45,14 @@ type AppId = | 'reality-override' | 'reality-override-admin'; +function PageLoader() { + return ( +
+ +
+ ); +} + export default function App() { const [currentPage, setCurrentPage] = useState('status'); const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings(); @@ -90,17 +102,16 @@ export default function App() { }; const pages = { - status: , + status: , devices: setDevicesOpenId(null)} />, - iec: , - network: , - general: , - tools: , + iec: , + network: , + general: , + tools: , apps: (

Apps

- {/* Manangement Group */}

Management

@@ -110,7 +121,6 @@ export default function App() { } label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
- {/* Disk Group */}

Disk

@@ -123,7 +133,6 @@ export default function App() { } label="Write Disk Image" onClick={() => setCurrentPage('write-disk-image')} />
- {/* Cartridge Group */}

Cartridge

@@ -132,7 +141,6 @@ export default function App() { } label="Easy Flash Cart Builder" onClick={() => setCurrentPage('easy-flash-cart-builder')} />
- {/* Development Group */}

Development

@@ -143,7 +151,6 @@ export default function App() { } label="Petscii Editor" onClick={() => setCurrentPage('petscii-editor')} />
- {/* Display Group */}

Display

@@ -156,7 +163,6 @@ export default function App() {
), - // Individual app pages 'file-manager': setCurrentPage('apps')} config={config} @@ -170,26 +176,26 @@ export default function App() { config={config} setConfig={setConfig} />, - 'serial-console': setCurrentPage('apps')} />, - 'directory-editor': setCurrentPage('apps')} />, - 'sector-editor': setCurrentPage('apps')} />, - 'bam-editor': setCurrentPage('apps')} />, - 'disk-visualizer': setCurrentPage('apps')} />, - 'ramrom-explorer': setCurrentPage('apps')} />, - 'dump-disk-image': setCurrentPage('apps')} />, - 'write-disk-image': setCurrentPage('apps')} />, - 'prg-to-crt': setCurrentPage('apps')} />, - 'magic-desk-cart-builder': setCurrentPage('apps')} />, + 'serial-console': setCurrentPage('apps')} />, + 'directory-editor': setCurrentPage('apps')} />, + 'sector-editor': setCurrentPage('apps')} />, + 'bam-editor': setCurrentPage('apps')} />, + 'disk-visualizer': setCurrentPage('apps')} />, + 'ramrom-explorer': setCurrentPage('apps')} />, + 'dump-disk-image': setCurrentPage('apps')} />, + 'write-disk-image': setCurrentPage('apps')} />, + 'prg-to-crt': setCurrentPage('apps')} />, + 'magic-desk-cart-builder': setCurrentPage('apps')} />, 'easy-flash-cart-builder': setCurrentPage('apps')} />, - 'basic-editor': setCurrentPage('apps')} />, - 'assembler': setCurrentPage('apps')} />, - 'sprite-editor': setCurrentPage('apps')} />, - 'charset-editor': setCurrentPage('apps')} />, - 'petscii-editor': setCurrentPage('apps')} />, - 'idle-animation': setCurrentPage('apps')} />, - 'loading-animation': setCurrentPage('apps')} />, - 'reality-override': setCurrentPage('apps')} />, - 'reality-override-admin': setCurrentPage('apps')} /> + 'basic-editor': setCurrentPage('apps')} />, + 'assembler': setCurrentPage('apps')} />, + 'sprite-editor': setCurrentPage('apps')} />, + 'charset-editor': setCurrentPage('apps')} />, + 'petscii-editor': setCurrentPage('apps')} />, + 'idle-animation': setCurrentPage('apps')} />, + 'loading-animation': setCurrentPage('apps')} />, + 'reality-override': setCurrentPage('apps')} />, + 'reality-override-admin': setCurrentPage('apps')} />, }; // AppCard component for app grid @@ -263,10 +269,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) { {showProfileMenu && (
- - - -
{showSearch && ( - setShowSearch(false)} - /> + + setShowSearch(false)} + /> + )}
); } - diff --git a/src/app/components/MediaManager.tsx b/src/app/components/MediaManager.tsx index 15ae647..5eba12d 100644 --- a/src/app/components/MediaManager.tsx +++ b/src/app/components/MediaManager.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'; import { AlignLeft, ArrowLeft, @@ -26,7 +26,6 @@ import { Move, Pencil, RefreshCw, - Save, Search, SlidersHorizontal, Terminal, @@ -35,15 +34,9 @@ import { X, } from 'lucide-react'; import { MediaEntry, TEXT_EXTS, DOC_EXTS, CODE_EXTS, MD_EXTS, JSON_EXTS, XML_EXTS, IMAGE_EXTS, CONFIG_EXTS } from './MediaEntry'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import CodeMirror, { EditorView } from '@uiw/react-codemirror'; -import { oneDark } from '@codemirror/theme-one-dark'; -import HexEditor from './HexEditor'; -import CodeEditor from './CodeEditor'; -import ConfigEditor from './ConfigEditor'; +import type { ViewMode } from './MediaViewerEditor'; +const MediaViewerEditor = lazy(() => import('./MediaViewerEditor')); + import { copyPath, createFolder, @@ -71,9 +64,8 @@ import { // ─── Types ─────────────────────────────────────────────────────────────────── -type SortKey = 'name' | 'size' | 'date'; -type Clipboard = { op: 'copy' | 'move'; paths: string[] }; -type ViewMode = 'text' | 'markdown' | 'json' | 'xml' | 'hex' | 'image' | 'config' | 'code' | 'doc'; +type SortKey = 'name' | 'size' | 'date'; +type Clipboard = { op: 'copy' | 'move'; paths: string[] }; // Extension sets are imported from MediaEntry. @@ -111,17 +103,7 @@ const VIEWER_LABEL: Record = { code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', hex: 'Hex', image: 'Image', config: 'Config', doc: 'Document', }; -const EXT_TO_LANG: Record = { - asm: 'nasm', s: 'nasm', bas: 'basic', - js: 'javascript', ts: 'typescript', jsx: 'jsx', tsx: 'tsx', - css: 'css', scss: 'scss', - py: 'python', c: 'c', cpp: 'cpp', h: 'cpp', hpp: 'cpp', - lua: 'lua', sh: 'bash', bash: 'bash', - php: 'php', rb: 'ruby', rs: 'rust', go: 'go', - java: 'java', cs: 'csharp', kt: 'kotlin', sql: 'sql', pl: 'perl', -}; - -// ─── Viewer components ─────────────────────────────────────────────────────── +// ─── Viewer helpers ─────────────────────────────────────────────────────────── function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: string }) { const cls = className ?? 'w-4 h-4'; @@ -138,112 +120,6 @@ function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: strin } } -function MarkdownViewer({ text }: { text: string }) { - return ( -
-

{children}

, - h2: ({ children }) =>

{children}

, - h3: ({ children }) =>

{children}

, - p: ({ children }) =>

{children}

, - a: ({ href, children }) => {children}, - code: ({ className, children, ...props }) => { - const match = /language-(\w+)/.exec(className ?? ''); - const inline = !match && !String(children).includes('\n'); - return inline - ? {children} - : ( -
- - {String(children).replace(/\n$/, '')} - -
- ); - }, - pre: ({ children }) => <>{children}, - blockquote: ({ children }) =>
{children}
, - ul: ({ children }) =>
    {children}
, - ol: ({ children }) =>
    {children}
, - li: ({ children }) =>
  • {children}
  • , - hr: () =>
    , - table: ({ children }) => {children}
    , - th: ({ children }) => {children}, - td: ({ children }) => {children}, - strong: ({ children }) => {children}, - img: ({ src, alt }) => {alt}, - }} - > - {text} -
    -
    - ); -} - -const CM_THEME = EditorView.theme({ - '&': { height: '100%', background: '#0a0a0a' }, - '.cm-scroller': { overflow: 'auto', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: '1.5' }, - '.cm-content': { padding: '12px 0' }, - '.cm-focused': { outline: 'none' }, -}); - -function MarkdownEditor({ text, onSave }: { text: string; onSave?: (s: string) => Promise }) { - const [editMode, setEditMode] = useState(false); - const [editInitText, setEditInitText] = useState(''); - const [saving, setSaving] = useState(false); - const editorViewRef = useRef(null); - - const save = async () => { - if (!editorViewRef.current || !onSave) return; - setSaving(true); - try { await onSave(editorViewRef.current.state.doc.toString()); } - finally { setSaving(false); } - }; - - return ( -
    -
    - {onSave && ( - - )} - {editMode && onSave && ( - - )} - {editMode && Ctrl+Z/Y undo · Ctrl+F search} -
    - {editMode ? ( -
    - { editorViewRef.current = v; }} - /> -
    - ) : ( - - )} -
    - ); -} - // EntryIcon is imported from MediaEntry. // ─── ActionsModal ───────────────────────────────────────────────────────────── @@ -1229,71 +1105,27 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi } : undefined} /> - {/* ── File viewer overlay ── */} - {viewEntry && ( -
    - {/* Title + mode switcher */} -
    - - {viewEntry.name} -
    - {viewMode && availableViewers(viewEntry).map(mode => ( - - ))} -
    - + {/* ── File viewer overlay (lazy-loaded) ── */} + {viewEntry && viewMode && ( + +
    - -
    - {viewLoading && ( -
    - Loading… -
    - )} - {!viewLoading && viewMode === 'image' && viewImgUrl && ( -
    - {viewEntry.name} -
    - )} - {!viewLoading && viewMode === 'hex' && viewHexData && ( - saveViewFile(d)} /> - )} - {!viewLoading && viewMode === 'markdown' && viewText !== null && ( - saveViewFile(s)} /> - )} - {!viewLoading && (viewMode === 'text' || viewMode === 'json' || viewMode === 'xml') && viewText !== null && ( - saveViewFile(s)} /> - )} - {!viewLoading && viewMode === 'config' && viewText !== null && ( - saveViewFile(s)} /> - )} - {!viewLoading && viewMode === 'code' && viewText !== null && ( - saveViewFile(s)} - /> - )} -
    -
    + }> + void switchViewMode(m)} + onSave={saveViewFile} + onDownload={() => void downloadEntry(viewEntry)} + /> + )} {/* ── Mount dialog ── */} diff --git a/src/app/components/MediaViewerEditor.tsx b/src/app/components/MediaViewerEditor.tsx new file mode 100644 index 0000000..5fb293f --- /dev/null +++ b/src/app/components/MediaViewerEditor.tsx @@ -0,0 +1,251 @@ +import { useRef, useState } from 'react'; +import { + AlignLeft, + Book, + BookOpen, + Braces, + Code2, + Download, + Eye, + Hash, + Image as ImageIcon, + Loader2, + Pencil, + Save, + SlidersHorizontal, + Terminal, + X, +} from 'lucide-react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import CodeMirror, { EditorView } from '@uiw/react-codemirror'; +import { oneDark } from '@codemirror/theme-one-dark'; +import HexEditor from './HexEditor'; +import CodeEditor from './CodeEditor'; +import ConfigEditor from './ConfigEditor'; +import type { EntryInfo } from '../webdav'; + +export type ViewMode = 'text' | 'markdown' | 'json' | 'xml' | 'hex' | 'image' | 'config' | 'code' | 'doc'; + +const VIEWER_LABEL: Record = { + code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', + hex: 'Hex', image: 'Image', config: 'Config', doc: 'Document', +}; + +const EXT_TO_LANG: Record = { + asm: 'nasm', s: 'nasm', bas: 'basic', + js: 'javascript', ts: 'typescript', jsx: 'jsx', tsx: 'tsx', + css: 'css', scss: 'scss', + py: 'python', c: 'c', cpp: 'cpp', h: 'cpp', hpp: 'cpp', + lua: 'lua', sh: 'bash', bash: 'bash', + php: 'php', rb: 'ruby', rs: 'rust', go: 'go', + java: 'java', cs: 'csharp', kt: 'kotlin', sql: 'sql', pl: 'perl', +}; + +function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: string }) { + const cls = className ?? 'w-4 h-4'; + switch (mode) { + case 'text': return ; + case 'markdown': return ; + case 'json': return ; + case 'code': return ; + case 'xml': return ; + case 'hex': return ; + case 'image': return ; + case 'config': return ; + case 'doc': return ; + } +} + +function MarkdownViewer({ text }: { text: string }) { + return ( +
    +

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + p: ({ children }) =>

    {children}

    , + a: ({ href, children }) => {children}, + code: ({ className, children, ...props }) => { + const match = /language-(\w+)/.exec(className ?? ''); + const inline = !match && !String(children).includes('\n'); + return inline + ? {children} + : ( +
    + + {String(children).replace(/\n$/, '')} + +
    + ); + }, + pre: ({ children }) => <>{children}, + blockquote: ({ children }) =>
    {children}
    , + ul: ({ children }) =>
      {children}
    , + ol: ({ children }) =>
      {children}
    , + li: ({ children }) =>
  • {children}
  • , + hr: () =>
    , + table: ({ children }) => {children}
    , + th: ({ children }) => {children}, + td: ({ children }) => {children}, + strong: ({ children }) => {children}, + img: ({ src, alt }) => {alt}, + }} + > + {text} +
    +
    + ); +} + +const CM_THEME = EditorView.theme({ + '&': { height: '100%', background: '#0a0a0a' }, + '.cm-scroller': { overflow: 'auto', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: '1.5' }, + '.cm-content': { padding: '12px 0' }, + '.cm-focused': { outline: 'none' }, +}); + +function MarkdownEditor({ text, onSave }: { text: string; onSave?: (s: string) => Promise }) { + const [editMode, setEditMode] = useState(false); + const [editInitText, setEditInitText] = useState(''); + const [saving, setSaving] = useState(false); + const editorViewRef = useRef(null); + + const save = async () => { + if (!editorViewRef.current || !onSave) return; + setSaving(true); + try { await onSave(editorViewRef.current.state.doc.toString()); } + finally { setSaving(false); } + }; + + return ( +
    +
    + {onSave && ( + + )} + {editMode && onSave && ( + + )} + {editMode && Ctrl+Z/Y undo · Ctrl+F search} +
    + {editMode ? ( +
    + { editorViewRef.current = v; }} + /> +
    + ) : ( + + )} +
    + ); +} + +export interface MediaViewerEditorProps { + entry: EntryInfo; + mode: ViewMode; + availableModes: ViewMode[]; + text: string | null; + imgUrl: string | null; + hexData: Uint8Array | null; + loading: boolean; + onClose: () => void; + onSwitchMode: (mode: ViewMode) => void; + onSave: (content: string | Uint8Array) => Promise; + onDownload: () => void; +} + +export default function MediaViewerEditor({ + entry, mode, availableModes, text, imgUrl, hexData, loading, + onClose, onSwitchMode, onSave, onDownload, +}: MediaViewerEditorProps) { + return ( +
    + {/* Title + mode switcher */} +
    + + {entry.name} +
    + {availableModes.map(m => ( + + ))} +
    + +
    + +
    + {loading && ( +
    + Loading… +
    + )} + {!loading && mode === 'image' && imgUrl && ( +
    + {entry.name} +
    + )} + {!loading && mode === 'hex' && hexData && ( + void onSave(d)} /> + )} + {!loading && mode === 'markdown' && text !== null && ( + onSave(s)} /> + )} + {!loading && (mode === 'text' || mode === 'json' || mode === 'xml') && text !== null && ( + onSave(s)} /> + )} + {!loading && mode === 'config' && text !== null && ( + onSave(s)} /> + )} + {!loading && mode === 'code' && text !== null && ( + onSave(s)} + /> + )} +
    +
    + ); +} diff --git a/src/imports/config.json b/src/imports/config.json index b9892fb..83f2905 100644 --- a/src/imports/config.json +++ b/src/imports/config.json @@ -1,10 +1,10 @@ { "general": { - "appearance": "light|dark|auto", - "language": "en|es|de", - "timezone": "America/Los_Angeles", - "country": "US", "devicename": "Meatloaf", + "appearance": "light", + "language": "en", + "timezone": "America/New_York", + "country": "US", "hsioindex": -1, "rotationsounds": 1, "configenabled": 1, @@ -16,17 +16,17 @@ "reset_with_host": 0 }, "host": { - "model": "c64|c64c|c128|sx64|plus4|c16|cx16|foenix|dtv|pet", - "video": "ntsc|pal", + "model": "c64", + "video": "ntsc", "language": "en", - "kernal": "stock|jiffydos|dolphindos|speeddos", - "basic": "2|3|7|10" + "kernal": "Stock", + "basic": "BASIC 2.0" }, "hardware": { "ps2": 1, "userport": { "enabled": 0, - "mode": "serial|parallel|IEEE-488" + "mode": "" } }, "wifi": [ @@ -63,8 +63,8 @@ } }, "bluetooth": { - "devicename": "meatloaf", "enabled": 0, + "devicename": "meatloaf", "baud": 19200 }, "modem": { @@ -81,13 +81,13 @@ "enabled": 1, "vic20_mode": 0, "vdrive": 0, - "boot_disk": "/autoboot[.PRG|.D64|.D81|etc]", + "boot_disk": "", "rom": { - "enabled": 1, - "default": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd|dos1571|dos1571.jd|dos1581|dos1581.jd", - "d64": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd", - "d71": "dos1571|dos1571.jd", - "d81": "dos1581|dos1581.jd" + "enabled": 0, + "default": "", + "d64": "", + "d71": "", + "d81": "" }, "directory": { "force_0801": 1, @@ -149,43 +149,43 @@ "mode": 1 }, "9": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 }, "10": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 }, "11": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 }, "12": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 }, "13": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 }, "14": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 }, "15": { - "enabled": 1, + "enabled": 0, "type": "drive", "url": "/", "mode": 1 @@ -196,32 +196,32 @@ "url": "/sd" }, "17": { - "enabled": 1, + "enabled": 0, "type": "network", "url": "/" }, "18": { - "enabled": 1, + "enabled": 0, "type": "network", "url": "/" }, "19": { - "enabled": 1, + "enabled": 0, "type": "network", "url": "/" }, "20": { - "enabled": 1, + "enabled": 0, "type": "other", "name": "CP/m" }, "21": { - "enabled": 1, + "enabled": 0, "type": "other", "name": "S.A.M" }, "29": { - "enabled": 1, + "enabled": 0, "type": "other", "name": "Clock" },