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
This commit is contained in:
parent
6817ddb491
commit
75ced3fa0e
|
|
@ -1,4 +1,4 @@
|
||||||
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 { 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 { Toaster, toast } from 'sonner';
|
||||||
import StatusPage from './components/StatusPage';
|
import StatusPage from './components/StatusPage';
|
||||||
|
|
@ -8,13 +8,17 @@ import NetworkPage from './components/NetworkPage';
|
||||||
import IECPage from './components/IECPage';
|
import IECPage from './components/IECPage';
|
||||||
import ToolsPage from './components/ToolsPage';
|
import ToolsPage from './components/ToolsPage';
|
||||||
import SearchOverlay from './components/SearchOverlay';
|
import SearchOverlay from './components/SearchOverlay';
|
||||||
import MediaManager from './components/MediaManager';
|
|
||||||
import RealityOverridePage from './components/RealityOverridePage';
|
|
||||||
import RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
|
import RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
|
||||||
|
import MediaManager from './components/MediaManager';
|
||||||
import logoSvg from '../imports/logo.svg';
|
import logoSvg from '../imports/logo.svg';
|
||||||
import { useSettings } from './settings';
|
import { useSettings } from './settings';
|
||||||
import { WsProvider } from './ws';
|
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 Page = 'status' | 'devices' | 'iec' | 'network' | 'general' | 'tools' | 'apps' | AppId;
|
||||||
|
|
||||||
type AppId =
|
type AppId =
|
||||||
|
|
@ -41,6 +45,14 @@ type AppId =
|
||||||
| 'reality-override'
|
| 'reality-override'
|
||||||
| 'reality-override-admin';
|
| 'reality-override-admin';
|
||||||
|
|
||||||
|
function PageLoader() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-neutral-400">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [currentPage, setCurrentPage] = useState<Page>('status');
|
const [currentPage, setCurrentPage] = useState<Page>('status');
|
||||||
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
||||||
|
|
@ -100,7 +112,6 @@ export default function App() {
|
||||||
<div className="max-w-3xl mx-auto py-8 px-4">
|
<div className="max-w-3xl mx-auto py-8 px-4">
|
||||||
<h1 className="text-2xl font-bold mb-6 text-center">Apps</h1>
|
<h1 className="text-2xl font-bold mb-6 text-center">Apps</h1>
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
{/* Manangement Group */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
|
@ -110,7 +121,6 @@ export default function App() {
|
||||||
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Disk Group */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Disk</h2>
|
<h2 className="text-lg font-semibold mb-4 text-blue-700">Disk</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
|
@ -123,7 +133,6 @@ export default function App() {
|
||||||
<AppCard icon={<Upload className="w-7 h-7" />} label="Write Disk Image" onClick={() => setCurrentPage('write-disk-image')} />
|
<AppCard icon={<Upload className="w-7 h-7" />} label="Write Disk Image" onClick={() => setCurrentPage('write-disk-image')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Cartridge Group */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Cartridge</h2>
|
<h2 className="text-lg font-semibold mb-4 text-blue-700">Cartridge</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
|
@ -132,7 +141,6 @@ export default function App() {
|
||||||
<AppCard icon={<LayoutList className="w-7 h-7" />} label="Easy Flash Cart Builder" onClick={() => setCurrentPage('easy-flash-cart-builder')} />
|
<AppCard icon={<LayoutList className="w-7 h-7" />} label="Easy Flash Cart Builder" onClick={() => setCurrentPage('easy-flash-cart-builder')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Development Group */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4 text-green-700">Development</h2>
|
<h2 className="text-lg font-semibold mb-4 text-green-700">Development</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
|
@ -143,7 +151,6 @@ export default function App() {
|
||||||
<AppCard icon={<Edit className="w-7 h-7" />} label="Petscii Editor" onClick={() => setCurrentPage('petscii-editor')} />
|
<AppCard icon={<Edit className="w-7 h-7" />} label="Petscii Editor" onClick={() => setCurrentPage('petscii-editor')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Display Group */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4 text-purple-700">Display</h2>
|
<h2 className="text-lg font-semibold mb-4 text-purple-700">Display</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
|
@ -156,7 +163,6 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
// Individual app pages
|
|
||||||
'file-manager': <MediaManager
|
'file-manager': <MediaManager
|
||||||
onBack={() => setCurrentPage('apps')}
|
onBack={() => setCurrentPage('apps')}
|
||||||
config={config}
|
config={config}
|
||||||
|
|
@ -189,7 +195,7 @@ export default function App() {
|
||||||
'idle-animation': <AppPage title="Idle Animation" onBack={() => setCurrentPage('apps')} />,
|
'idle-animation': <AppPage title="Idle Animation" onBack={() => setCurrentPage('apps')} />,
|
||||||
'loading-animation': <AppPage title="Loading Animation" onBack={() => setCurrentPage('apps')} />,
|
'loading-animation': <AppPage title="Loading Animation" onBack={() => setCurrentPage('apps')} />,
|
||||||
'reality-override': <RealityOverridePage onBack={() => setCurrentPage('apps')} />,
|
'reality-override': <RealityOverridePage onBack={() => setCurrentPage('apps')} />,
|
||||||
'reality-override-admin': <RealityOverrideAdminPage onBack={() => setCurrentPage('apps')} />
|
'reality-override-admin': <RealityOverrideAdminPage onBack={() => setCurrentPage('apps')} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
// AppCard component for app grid
|
// AppCard component for app grid
|
||||||
|
|
@ -263,10 +269,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
{showProfileMenu && (
|
{showProfileMenu && (
|
||||||
<div className="absolute right-0 top-12 bg-white rounded-lg shadow-lg border border-neutral-200 py-2 min-w-[200px] z-20">
|
<div className="absolute right-0 top-12 bg-white rounded-lg shadow-lg border border-neutral-200 py-2 min-w-[200px] z-20">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => { setShowProfileMenu(false); setCurrentPage('general'); }}
|
||||||
setShowProfileMenu(false);
|
|
||||||
setCurrentPage('general');
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2"
|
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Settings className="w-4 h-4 text-[#4d4d4d]" />
|
<Settings className="w-4 h-4 text-[#4d4d4d]" />
|
||||||
|
|
@ -302,43 +305,30 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
{pages[currentPage]}
|
{pages[currentPage]}
|
||||||
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<nav className="bg-[#4d4d4d] flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
<nav className="bg-[#4d4d4d] flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button onClick={() => setCurrentPage('status')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||||
onClick={() => setCurrentPage('status')}
|
|
||||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
|
||||||
>
|
|
||||||
<Activity className="w-5 h-5 text-white" />
|
<Activity className="w-5 h-5 text-white" />
|
||||||
<span className="text-xs text-white">Status</span>
|
<span className="text-xs text-white">Status</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setCurrentPage('devices')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||||
onClick={() => setCurrentPage('devices')}
|
|
||||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
|
||||||
>
|
|
||||||
<HardDrive className="w-5 h-5 text-white" />
|
<HardDrive className="w-5 h-5 text-white" />
|
||||||
<span className="text-xs text-white">Devices</span>
|
<span className="text-xs text-white">Devices</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setCurrentPage('iec')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||||
onClick={() => setCurrentPage('iec')}
|
|
||||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
|
||||||
>
|
|
||||||
<Cpu className="w-5 h-5 text-white" />
|
<Cpu className="w-5 h-5 text-white" />
|
||||||
<span className="text-xs text-white">IEC</span>
|
<span className="text-xs text-white">IEC</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setCurrentPage('network')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||||
onClick={() => setCurrentPage('network')}
|
|
||||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
|
||||||
>
|
|
||||||
<Network className="w-5 h-5 text-white" />
|
<Network className="w-5 h-5 text-white" />
|
||||||
<span className="text-xs text-white">Network</span>
|
<span className="text-xs text-white">Network</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setCurrentPage('tools')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||||
onClick={() => setCurrentPage('tools')}
|
|
||||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
|
||||||
>
|
|
||||||
<Wrench className="w-5 h-5 text-white" />
|
<Wrench className="w-5 h-5 text-white" />
|
||||||
<span className="text-xs text-white">System</span>
|
<span className="text-xs text-white">System</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -346,14 +336,15 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
<SearchOverlay
|
<SearchOverlay
|
||||||
config={config}
|
config={config}
|
||||||
setConfig={setConfig}
|
setConfig={setConfig}
|
||||||
onClose={() => setShowSearch(false)}
|
onClose={() => setShowSearch(false)}
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</WsProvider>
|
</WsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
|
@ -26,7 +26,6 @@ import {
|
||||||
Move,
|
Move,
|
||||||
Pencil,
|
Pencil,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Save,
|
|
||||||
Search,
|
Search,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
|
@ -35,15 +34,9 @@ import {
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { MediaEntry, TEXT_EXTS, DOC_EXTS, CODE_EXTS, MD_EXTS, JSON_EXTS, XML_EXTS, IMAGE_EXTS, CONFIG_EXTS } from './MediaEntry';
|
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 type { ViewMode } from './MediaViewerEditor';
|
||||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
const MediaViewerEditor = lazy(() => import('./MediaViewerEditor'));
|
||||||
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 {
|
import {
|
||||||
copyPath,
|
copyPath,
|
||||||
createFolder,
|
createFolder,
|
||||||
|
|
@ -73,7 +66,6 @@ import {
|
||||||
|
|
||||||
type SortKey = 'name' | 'size' | 'date';
|
type SortKey = 'name' | 'size' | 'date';
|
||||||
type Clipboard = { op: 'copy' | 'move'; paths: string[] };
|
type Clipboard = { op: 'copy' | 'move'; paths: string[] };
|
||||||
type ViewMode = 'text' | 'markdown' | 'json' | 'xml' | 'hex' | 'image' | 'config' | 'code' | 'doc';
|
|
||||||
|
|
||||||
// Extension sets are imported from MediaEntry.
|
// Extension sets are imported from MediaEntry.
|
||||||
|
|
||||||
|
|
@ -111,17 +103,7 @@ const VIEWER_LABEL: Record<ViewMode, string> = {
|
||||||
code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', hex: 'Hex', image: 'Image', config: 'Config', doc: 'Document',
|
code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', hex: 'Hex', image: 'Image', config: 'Config', doc: 'Document',
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXT_TO_LANG: Record<string, string> = {
|
// ─── Viewer helpers ───────────────────────────────────────────────────────────
|
||||||
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 ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: string }) {
|
function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: string }) {
|
||||||
const cls = className ?? 'w-4 h-4';
|
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 (
|
|
||||||
<div className="p-6 overflow-auto h-full text-neutral-200 text-sm leading-relaxed">
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
components={{
|
|
||||||
h1: ({ children }) => <h1 className="text-2xl font-bold mt-6 mb-3 text-white border-b border-neutral-700 pb-2">{children}</h1>,
|
|
||||||
h2: ({ children }) => <h2 className="text-xl font-bold mt-5 mb-2 text-white">{children}</h2>,
|
|
||||||
h3: ({ children }) => <h3 className="text-base font-semibold mt-4 mb-1 text-white">{children}</h3>,
|
|
||||||
p: ({ children }) => <p className="my-2">{children}</p>,
|
|
||||||
a: ({ href, children }) => <a href={href} className="text-blue-400 underline" target="_blank" rel="noopener noreferrer">{children}</a>,
|
|
||||||
code: ({ className, children, ...props }) => {
|
|
||||||
const match = /language-(\w+)/.exec(className ?? '');
|
|
||||||
const inline = !match && !String(children).includes('\n');
|
|
||||||
return inline
|
|
||||||
? <code className="bg-neutral-700 text-green-300 rounded px-1 text-xs font-mono" {...props}>{children}</code>
|
|
||||||
: (
|
|
||||||
<div className="my-3 rounded overflow-hidden text-xs">
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language={match?.[1] ?? 'text'}
|
|
||||||
style={vscDarkPlus}
|
|
||||||
customStyle={{ margin: 0, background: '#1a1a1a' }}
|
|
||||||
PreTag="div"
|
|
||||||
>
|
|
||||||
{String(children).replace(/\n$/, '')}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
pre: ({ children }) => <>{children}</>,
|
|
||||||
blockquote: ({ children }) => <blockquote className="border-l-4 border-neutral-500 pl-4 my-2 text-neutral-400 italic">{children}</blockquote>,
|
|
||||||
ul: ({ children }) => <ul className="list-disc pl-5 my-2 space-y-1">{children}</ul>,
|
|
||||||
ol: ({ children }) => <ol className="list-decimal pl-5 my-2 space-y-1">{children}</ol>,
|
|
||||||
li: ({ children }) => <li>{children}</li>,
|
|
||||||
hr: () => <hr className="border-neutral-600 my-4" />,
|
|
||||||
table: ({ children }) => <table className="border-collapse my-3 text-sm w-full">{children}</table>,
|
|
||||||
th: ({ children }) => <th className="border border-neutral-600 px-3 py-1 bg-neutral-800 font-semibold text-left">{children}</th>,
|
|
||||||
td: ({ children }) => <td className="border border-neutral-600 px-3 py-1">{children}</td>,
|
|
||||||
strong: ({ children }) => <strong className="text-white font-semibold">{children}</strong>,
|
|
||||||
img: ({ src, alt }) => <img src={src} alt={alt} className="max-w-full my-2 rounded" />,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<void> }) {
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
|
||||||
const [editInitText, setEditInitText] = useState('');
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const editorViewRef = useRef<EditorView | null>(null);
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
if (!editorViewRef.current || !onSave) return;
|
|
||||||
setSaving(true);
|
|
||||||
try { await onSave(editorViewRef.current.state.doc.toString()); }
|
|
||||||
finally { setSaving(false); }
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
|
|
||||||
{onSave && (
|
|
||||||
<button
|
|
||||||
onClick={() => { if (!editMode) setEditInitText(text); setEditMode(v => !v); }}
|
|
||||||
className={editMode
|
|
||||||
? 'px-2 py-1 rounded bg-amber-600 text-white inline-flex items-center gap-1'
|
|
||||||
: 'px-2 py-1 rounded bg-neutral-700 text-neutral-300 hover:bg-neutral-600 inline-flex items-center gap-1'}
|
|
||||||
>
|
|
||||||
{editMode ? <><Eye className="w-3.5 h-3.5" /> View</> : <><Pencil className="w-3.5 h-3.5" /> Edit</>}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{editMode && onSave && (
|
|
||||||
<button onClick={() => void save()} disabled={saving}
|
|
||||||
className="px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 inline-flex items-center gap-1">
|
|
||||||
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving…' : 'Save'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{editMode && <span className="text-neutral-600 ml-auto">Ctrl+Z/Y undo · Ctrl+F search</span>}
|
|
||||||
</div>
|
|
||||||
{editMode ? (
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
<CodeMirror
|
|
||||||
value={editInitText}
|
|
||||||
extensions={[CM_THEME]}
|
|
||||||
theme={oneDark}
|
|
||||||
height="100%"
|
|
||||||
onCreateEditor={v => { editorViewRef.current = v; }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<MarkdownViewer text={text} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EntryIcon is imported from MediaEntry.
|
// EntryIcon is imported from MediaEntry.
|
||||||
|
|
||||||
// ─── ActionsModal ─────────────────────────────────────────────────────────────
|
// ─── ActionsModal ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1229,71 +1105,27 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
|
||||||
} : undefined}
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ── File viewer overlay ── */}
|
{/* ── File viewer overlay (lazy-loaded) ── */}
|
||||||
{viewEntry && (
|
{viewEntry && viewMode && (
|
||||||
<div className="fixed inset-0 bg-neutral-950 z-50 flex flex-col">
|
<Suspense fallback={
|
||||||
{/* Title + mode switcher */}
|
<div className="fixed inset-0 bg-neutral-950 z-50 flex items-center justify-center">
|
||||||
<div className="bg-neutral-900 flex items-center px-4 py-2 gap-3 border-b border-neutral-700 flex-shrink-0">
|
<Loader2 className="w-6 h-6 animate-spin text-neutral-400" />
|
||||||
<button onClick={closeViewer} className="p-1.5 rounded hover:bg-neutral-700">
|
|
||||||
<X className="w-5 h-5 text-white" />
|
|
||||||
</button>
|
|
||||||
<span className="font-medium truncate flex-1 text-sm text-white">{viewEntry.name}</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{viewMode && availableViewers(viewEntry).map(mode => (
|
|
||||||
<button
|
|
||||||
key={mode}
|
|
||||||
onClick={() => void switchViewMode(mode)}
|
|
||||||
title={VIEWER_LABEL[mode]}
|
|
||||||
className={`px-2 py-1 rounded text-xs inline-flex items-center gap-1 transition-colors ${
|
|
||||||
viewMode === mode
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'text-neutral-400 hover:bg-neutral-700 hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<ViewerModeIcon mode={mode} className="w-3.5 h-3.5" />
|
|
||||||
<span className="hidden sm:inline">{VIEWER_LABEL[mode]}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => void downloadEntry(viewEntry)} className="p-1.5 rounded hover:bg-neutral-700 text-neutral-300 hover:text-white" title="Download">
|
}>
|
||||||
<Download className="w-4 h-4" />
|
<MediaViewerEditor
|
||||||
</button>
|
entry={viewEntry}
|
||||||
</div>
|
mode={viewMode}
|
||||||
|
availableModes={availableViewers(viewEntry)}
|
||||||
<div className="flex-1 overflow-hidden bg-neutral-950">
|
|
||||||
{viewLoading && (
|
|
||||||
<div className="h-full flex items-center justify-center gap-2 text-neutral-400">
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" /> Loading…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!viewLoading && viewMode === 'image' && viewImgUrl && (
|
|
||||||
<div className="h-full flex items-center justify-center overflow-auto p-4">
|
|
||||||
<img src={viewImgUrl} alt={viewEntry.name} className="max-w-full max-h-full object-contain" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!viewLoading && viewMode === 'hex' && viewHexData && (
|
|
||||||
<HexEditor key={viewEntry.path} data={viewHexData} onSave={d => saveViewFile(d)} />
|
|
||||||
)}
|
|
||||||
{!viewLoading && viewMode === 'markdown' && viewText !== null && (
|
|
||||||
<MarkdownEditor key={viewEntry.path} text={viewText} onSave={s => saveViewFile(s)} />
|
|
||||||
)}
|
|
||||||
{!viewLoading && (viewMode === 'text' || viewMode === 'json' || viewMode === 'xml') && viewText !== null && (
|
|
||||||
<CodeEditor key={viewEntry.path} text={viewText} mode={viewMode} onSave={s => saveViewFile(s)} />
|
|
||||||
)}
|
|
||||||
{!viewLoading && viewMode === 'config' && viewText !== null && (
|
|
||||||
<ConfigEditor key={viewEntry.path} text={viewText} onSave={s => saveViewFile(s)} />
|
|
||||||
)}
|
|
||||||
{!viewLoading && viewMode === 'code' && viewText !== null && (
|
|
||||||
<CodeEditor
|
|
||||||
key={viewEntry.path}
|
|
||||||
text={viewText}
|
text={viewText}
|
||||||
mode="code"
|
imgUrl={viewImgUrl}
|
||||||
syntaxHighlightLang={EXT_TO_LANG[viewEntry.name.split('.').pop()?.toLowerCase() ?? ''] ?? 'text'}
|
hexData={viewHexData}
|
||||||
onSave={s => saveViewFile(s)}
|
loading={viewLoading}
|
||||||
|
onClose={closeViewer}
|
||||||
|
onSwitchMode={m => void switchViewMode(m)}
|
||||||
|
onSave={saveViewFile}
|
||||||
|
onDownload={() => void downloadEntry(viewEntry)}
|
||||||
/>
|
/>
|
||||||
)}
|
</Suspense>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Mount dialog ── */}
|
{/* ── Mount dialog ── */}
|
||||||
|
|
|
||||||
251
src/app/components/MediaViewerEditor.tsx
Normal file
251
src/app/components/MediaViewerEditor.tsx
Normal file
|
|
@ -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<ViewMode, string> = {
|
||||||
|
code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML',
|
||||||
|
hex: 'Hex', image: 'Image', config: 'Config', doc: 'Document',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXT_TO_LANG: Record<string, string> = {
|
||||||
|
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 <AlignLeft className={cls} />;
|
||||||
|
case 'markdown': return <BookOpen className={cls} />;
|
||||||
|
case 'json': return <Braces className={cls} />;
|
||||||
|
case 'code': return <Terminal className={cls} />;
|
||||||
|
case 'xml': return <Code2 className={cls} />;
|
||||||
|
case 'hex': return <Hash className={cls} />;
|
||||||
|
case 'image': return <ImageIcon className={cls} />;
|
||||||
|
case 'config': return <SlidersHorizontal className={cls} />;
|
||||||
|
case 'doc': return <Book className={cls} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkdownViewer({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 overflow-auto h-full text-neutral-200 text-sm leading-relaxed">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
h1: ({ children }) => <h1 className="text-2xl font-bold mt-6 mb-3 text-white border-b border-neutral-700 pb-2">{children}</h1>,
|
||||||
|
h2: ({ children }) => <h2 className="text-xl font-bold mt-5 mb-2 text-white">{children}</h2>,
|
||||||
|
h3: ({ children }) => <h3 className="text-base font-semibold mt-4 mb-1 text-white">{children}</h3>,
|
||||||
|
p: ({ children }) => <p className="my-2">{children}</p>,
|
||||||
|
a: ({ href, children }) => <a href={href} className="text-blue-400 underline" target="_blank" rel="noopener noreferrer">{children}</a>,
|
||||||
|
code: ({ className, children, ...props }) => {
|
||||||
|
const match = /language-(\w+)/.exec(className ?? '');
|
||||||
|
const inline = !match && !String(children).includes('\n');
|
||||||
|
return inline
|
||||||
|
? <code className="bg-neutral-700 text-green-300 rounded px-1 text-xs font-mono" {...props}>{children}</code>
|
||||||
|
: (
|
||||||
|
<div className="my-3 rounded overflow-hidden text-xs">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language={match?.[1] ?? 'text'}
|
||||||
|
style={vscDarkPlus}
|
||||||
|
customStyle={{ margin: 0, background: '#1a1a1a' }}
|
||||||
|
PreTag="div"
|
||||||
|
>
|
||||||
|
{String(children).replace(/\n$/, '')}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pre: ({ children }) => <>{children}</>,
|
||||||
|
blockquote: ({ children }) => <blockquote className="border-l-4 border-neutral-500 pl-4 my-2 text-neutral-400 italic">{children}</blockquote>,
|
||||||
|
ul: ({ children }) => <ul className="list-disc pl-5 my-2 space-y-1">{children}</ul>,
|
||||||
|
ol: ({ children }) => <ol className="list-decimal pl-5 my-2 space-y-1">{children}</ol>,
|
||||||
|
li: ({ children }) => <li>{children}</li>,
|
||||||
|
hr: () => <hr className="border-neutral-600 my-4" />,
|
||||||
|
table: ({ children }) => <table className="border-collapse my-3 text-sm w-full">{children}</table>,
|
||||||
|
th: ({ children }) => <th className="border border-neutral-600 px-3 py-1 bg-neutral-800 font-semibold text-left">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-neutral-600 px-3 py-1">{children}</td>,
|
||||||
|
strong: ({ children }) => <strong className="text-white font-semibold">{children}</strong>,
|
||||||
|
img: ({ src, alt }) => <img src={src} alt={alt} className="max-w-full my-2 rounded" />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void> }) {
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [editInitText, setEditInitText] = useState('');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const editorViewRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (!editorViewRef.current || !onSave) return;
|
||||||
|
setSaving(true);
|
||||||
|
try { await onSave(editorViewRef.current.state.doc.toString()); }
|
||||||
|
finally { setSaving(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
|
||||||
|
{onSave && (
|
||||||
|
<button
|
||||||
|
onClick={() => { if (!editMode) setEditInitText(text); setEditMode(v => !v); }}
|
||||||
|
className={editMode
|
||||||
|
? 'px-2 py-1 rounded bg-amber-600 text-white inline-flex items-center gap-1'
|
||||||
|
: 'px-2 py-1 rounded bg-neutral-700 text-neutral-300 hover:bg-neutral-600 inline-flex items-center gap-1'}
|
||||||
|
>
|
||||||
|
{editMode ? <><Eye className="w-3.5 h-3.5" /> View</> : <><Pencil className="w-3.5 h-3.5" /> Edit</>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{editMode && onSave && (
|
||||||
|
<button onClick={() => void save()} disabled={saving}
|
||||||
|
className="px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 inline-flex items-center gap-1">
|
||||||
|
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving…' : 'Save'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{editMode && <span className="text-neutral-600 ml-auto">Ctrl+Z/Y undo · Ctrl+F search</span>}
|
||||||
|
</div>
|
||||||
|
{editMode ? (
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<CodeMirror
|
||||||
|
value={editInitText}
|
||||||
|
extensions={[CM_THEME]}
|
||||||
|
theme={oneDark}
|
||||||
|
height="100%"
|
||||||
|
onCreateEditor={v => { editorViewRef.current = v; }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<MarkdownViewer text={text} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
onDownload: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MediaViewerEditor({
|
||||||
|
entry, mode, availableModes, text, imgUrl, hexData, loading,
|
||||||
|
onClose, onSwitchMode, onSave, onDownload,
|
||||||
|
}: MediaViewerEditorProps) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-neutral-950 z-50 flex flex-col">
|
||||||
|
{/* Title + mode switcher */}
|
||||||
|
<div className="bg-neutral-900 flex items-center px-4 py-2 gap-3 border-b border-neutral-700 flex-shrink-0">
|
||||||
|
<button onClick={onClose} className="p-1.5 rounded hover:bg-neutral-700">
|
||||||
|
<X className="w-5 h-5 text-white" />
|
||||||
|
</button>
|
||||||
|
<span className="font-medium truncate flex-1 text-sm text-white">{entry.name}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{availableModes.map(m => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => onSwitchMode(m)}
|
||||||
|
title={VIEWER_LABEL[m]}
|
||||||
|
className={`px-2 py-1 rounded text-xs inline-flex items-center gap-1 transition-colors ${
|
||||||
|
mode === m
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-neutral-400 hover:bg-neutral-700 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ViewerModeIcon mode={m} className="w-3.5 h-3.5" />
|
||||||
|
<span className="hidden sm:inline">{VIEWER_LABEL[m]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={onDownload} className="p-1.5 rounded hover:bg-neutral-700 text-neutral-300 hover:text-white" title="Download">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden bg-neutral-950">
|
||||||
|
{loading && (
|
||||||
|
<div className="h-full flex items-center justify-center gap-2 text-neutral-400">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" /> Loading…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && mode === 'image' && imgUrl && (
|
||||||
|
<div className="h-full flex items-center justify-center overflow-auto p-4">
|
||||||
|
<img src={imgUrl} alt={entry.name} className="max-w-full max-h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && mode === 'hex' && hexData && (
|
||||||
|
<HexEditor key={entry.path} data={hexData} onSave={d => void onSave(d)} />
|
||||||
|
)}
|
||||||
|
{!loading && mode === 'markdown' && text !== null && (
|
||||||
|
<MarkdownEditor key={entry.path} text={text} onSave={s => onSave(s)} />
|
||||||
|
)}
|
||||||
|
{!loading && (mode === 'text' || mode === 'json' || mode === 'xml') && text !== null && (
|
||||||
|
<CodeEditor key={entry.path} text={text} mode={mode} onSave={s => onSave(s)} />
|
||||||
|
)}
|
||||||
|
{!loading && mode === 'config' && text !== null && (
|
||||||
|
<ConfigEditor key={entry.path} text={text} onSave={s => onSave(s)} />
|
||||||
|
)}
|
||||||
|
{!loading && mode === 'code' && text !== null && (
|
||||||
|
<CodeEditor
|
||||||
|
key={entry.path}
|
||||||
|
text={text}
|
||||||
|
mode="code"
|
||||||
|
syntaxHighlightLang={EXT_TO_LANG[entry.name.split('.').pop()?.toLowerCase() ?? ''] ?? 'text'}
|
||||||
|
onSave={s => onSave(s)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"appearance": "light|dark|auto",
|
|
||||||
"language": "en|es|de",
|
|
||||||
"timezone": "America/Los_Angeles",
|
|
||||||
"country": "US",
|
|
||||||
"devicename": "Meatloaf",
|
"devicename": "Meatloaf",
|
||||||
|
"appearance": "light",
|
||||||
|
"language": "en",
|
||||||
|
"timezone": "America/New_York",
|
||||||
|
"country": "US",
|
||||||
"hsioindex": -1,
|
"hsioindex": -1,
|
||||||
"rotationsounds": 1,
|
"rotationsounds": 1,
|
||||||
"configenabled": 1,
|
"configenabled": 1,
|
||||||
|
|
@ -16,17 +16,17 @@
|
||||||
"reset_with_host": 0
|
"reset_with_host": 0
|
||||||
},
|
},
|
||||||
"host": {
|
"host": {
|
||||||
"model": "c64|c64c|c128|sx64|plus4|c16|cx16|foenix|dtv|pet",
|
"model": "c64",
|
||||||
"video": "ntsc|pal",
|
"video": "ntsc",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"kernal": "stock|jiffydos|dolphindos|speeddos",
|
"kernal": "Stock",
|
||||||
"basic": "2|3|7|10"
|
"basic": "BASIC 2.0"
|
||||||
},
|
},
|
||||||
"hardware": {
|
"hardware": {
|
||||||
"ps2": 1,
|
"ps2": 1,
|
||||||
"userport": {
|
"userport": {
|
||||||
"enabled": 0,
|
"enabled": 0,
|
||||||
"mode": "serial|parallel|IEEE-488"
|
"mode": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wifi": [
|
"wifi": [
|
||||||
|
|
@ -63,8 +63,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bluetooth": {
|
"bluetooth": {
|
||||||
"devicename": "meatloaf",
|
|
||||||
"enabled": 0,
|
"enabled": 0,
|
||||||
|
"devicename": "meatloaf",
|
||||||
"baud": 19200
|
"baud": 19200
|
||||||
},
|
},
|
||||||
"modem": {
|
"modem": {
|
||||||
|
|
@ -81,13 +81,13 @@
|
||||||
"enabled": 1,
|
"enabled": 1,
|
||||||
"vic20_mode": 0,
|
"vic20_mode": 0,
|
||||||
"vdrive": 0,
|
"vdrive": 0,
|
||||||
"boot_disk": "/autoboot[.PRG|.D64|.D81|etc]",
|
"boot_disk": "",
|
||||||
"rom": {
|
"rom": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"default": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd|dos1571|dos1571.jd|dos1581|dos1581.jd",
|
"default": "",
|
||||||
"d64": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd",
|
"d64": "",
|
||||||
"d71": "dos1571|dos1571.jd",
|
"d71": "",
|
||||||
"d81": "dos1581|dos1581.jd"
|
"d81": ""
|
||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"force_0801": 1,
|
"force_0801": 1,
|
||||||
|
|
@ -149,43 +149,43 @@
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"10": {
|
"10": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"11": {
|
"11": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"12": {
|
"12": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"13": {
|
"13": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"14": {
|
"14": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
},
|
},
|
||||||
"15": {
|
"15": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "drive",
|
"type": "drive",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"mode": 1
|
"mode": 1
|
||||||
|
|
@ -196,32 +196,32 @@
|
||||||
"url": "/sd"
|
"url": "/sd"
|
||||||
},
|
},
|
||||||
"17": {
|
"17": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "network",
|
"type": "network",
|
||||||
"url": "/"
|
"url": "/"
|
||||||
},
|
},
|
||||||
"18": {
|
"18": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "network",
|
"type": "network",
|
||||||
"url": "/"
|
"url": "/"
|
||||||
},
|
},
|
||||||
"19": {
|
"19": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "network",
|
"type": "network",
|
||||||
"url": "/"
|
"url": "/"
|
||||||
},
|
},
|
||||||
"20": {
|
"20": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "other",
|
"type": "other",
|
||||||
"name": "CP/m"
|
"name": "CP/m"
|
||||||
},
|
},
|
||||||
"21": {
|
"21": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "other",
|
"type": "other",
|
||||||
"name": "S.A.M"
|
"name": "S.A.M"
|
||||||
},
|
},
|
||||||
"29": {
|
"29": {
|
||||||
"enabled": 1,
|
"enabled": 0,
|
||||||
"type": "other",
|
"type": "other",
|
||||||
"name": "Clock"
|
"name": "Clock"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user