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
141
src/app/App.tsx
141
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 (
|
||||
<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() {
|
||||
const [currentPage, setCurrentPage] = useState<Page>('status');
|
||||
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
||||
|
|
@ -90,17 +102,16 @@ export default function App() {
|
|||
};
|
||||
|
||||
const pages = {
|
||||
status: <StatusPage config={config} setConfig={setConfig} />,
|
||||
status: <StatusPage config={config} setConfig={setConfig} />,
|
||||
devices: <DevicesPage config={config} setConfig={setConfig} openDeviceId={devicesOpenId} onClearOpenDevice={() => setDevicesOpenId(null)} />,
|
||||
iec: <IECPage config={config} setConfig={setConfig} />,
|
||||
network: <NetworkPage config={config} setConfig={setConfig} />,
|
||||
general: <GeneralPage config={config} setConfig={setConfig} />,
|
||||
tools: <ToolsPage config={config} setConfig={setConfig} />,
|
||||
iec: <IECPage config={config} setConfig={setConfig} />,
|
||||
network: <NetworkPage config={config} setConfig={setConfig} />,
|
||||
general: <GeneralPage config={config} setConfig={setConfig} />,
|
||||
tools: <ToolsPage config={config} setConfig={setConfig} />,
|
||||
apps: (
|
||||
<div className="max-w-3xl mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">Apps</h1>
|
||||
<div className="space-y-10">
|
||||
{/* Manangement Group */}
|
||||
<div>
|
||||
<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">
|
||||
|
|
@ -110,7 +121,6 @@ export default function App() {
|
|||
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Disk Group */}
|
||||
<div>
|
||||
<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">
|
||||
|
|
@ -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')} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Cartridge Group */}
|
||||
<div>
|
||||
<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">
|
||||
|
|
@ -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')} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Development Group */}
|
||||
<div>
|
||||
<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">
|
||||
|
|
@ -143,7 +151,6 @@ export default function App() {
|
|||
<AppCard icon={<Edit className="w-7 h-7" />} label="Petscii Editor" onClick={() => setCurrentPage('petscii-editor')} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Display Group */}
|
||||
<div>
|
||||
<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">
|
||||
|
|
@ -156,7 +163,6 @@ export default function App() {
|
|||
</div>
|
||||
</div>
|
||||
),
|
||||
// Individual app pages
|
||||
'file-manager': <MediaManager
|
||||
onBack={() => setCurrentPage('apps')}
|
||||
config={config}
|
||||
|
|
@ -170,26 +176,26 @@ export default function App() {
|
|||
config={config}
|
||||
setConfig={setConfig}
|
||||
/>,
|
||||
'serial-console': <AppPage title="Serial Console" onBack={() => setCurrentPage('apps')} />,
|
||||
'directory-editor': <AppPage title="Directory Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'sector-editor': <AppPage title="Sector Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'bam-editor': <AppPage title="BAM Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'disk-visualizer': <AppPage title="Disk Visualizer" onBack={() => setCurrentPage('apps')} />,
|
||||
'ramrom-explorer': <AppPage title="RAM/ROM Explorer" onBack={() => setCurrentPage('apps')} />,
|
||||
'dump-disk-image': <AppPage title="Dump Disk Image" onBack={() => setCurrentPage('apps')} />,
|
||||
'write-disk-image': <AppPage title="Write Disk Image" onBack={() => setCurrentPage('apps')} />,
|
||||
'prg-to-crt': <AppPage title="PRG to CRT" onBack={() => setCurrentPage('apps')} />,
|
||||
'magic-desk-cart-builder': <AppPage title="Magic Desk Cart Builder" onBack={() => setCurrentPage('apps')} />,
|
||||
'serial-console': <AppPage title="Serial Console" onBack={() => setCurrentPage('apps')} />,
|
||||
'directory-editor': <AppPage title="Directory Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'sector-editor': <AppPage title="Sector Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'bam-editor': <AppPage title="BAM Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'disk-visualizer': <AppPage title="Disk Visualizer" onBack={() => setCurrentPage('apps')} />,
|
||||
'ramrom-explorer': <AppPage title="RAM/ROM Explorer" onBack={() => setCurrentPage('apps')} />,
|
||||
'dump-disk-image': <AppPage title="Dump Disk Image" onBack={() => setCurrentPage('apps')} />,
|
||||
'write-disk-image': <AppPage title="Write Disk Image" onBack={() => setCurrentPage('apps')} />,
|
||||
'prg-to-crt': <AppPage title="PRG to CRT" onBack={() => setCurrentPage('apps')} />,
|
||||
'magic-desk-cart-builder': <AppPage title="Magic Desk Cart Builder" onBack={() => setCurrentPage('apps')} />,
|
||||
'easy-flash-cart-builder': <AppPage title="Easy Flash Cart Builder" onBack={() => setCurrentPage('apps')} />,
|
||||
'basic-editor': <AppPage title="Basic Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'assembler': <AppPage title="Assembler" onBack={() => setCurrentPage('apps')} />,
|
||||
'sprite-editor': <AppPage title="Sprite Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'charset-editor': <AppPage title="Character Set Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'petscii-editor': <AppPage title="Petscii Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'idle-animation': <AppPage title="Idle Animation" onBack={() => setCurrentPage('apps')} />,
|
||||
'loading-animation': <AppPage title="Loading Animation" onBack={() => setCurrentPage('apps')} />,
|
||||
'reality-override': <RealityOverridePage onBack={() => setCurrentPage('apps')} />,
|
||||
'reality-override-admin': <RealityOverrideAdminPage onBack={() => setCurrentPage('apps')} />
|
||||
'basic-editor': <AppPage title="Basic Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'assembler': <AppPage title="Assembler" onBack={() => setCurrentPage('apps')} />,
|
||||
'sprite-editor': <AppPage title="Sprite Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'charset-editor': <AppPage title="Character Set Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'petscii-editor': <AppPage title="Petscii Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'idle-animation': <AppPage title="Idle Animation" onBack={() => setCurrentPage('apps')} />,
|
||||
'loading-animation': <AppPage title="Loading Animation" onBack={() => setCurrentPage('apps')} />,
|
||||
'reality-override': <RealityOverridePage onBack={() => setCurrentPage('apps')} />,
|
||||
'reality-override-admin': <RealityOverrideAdminPage onBack={() => setCurrentPage('apps')} />,
|
||||
};
|
||||
|
||||
// AppCard component for app grid
|
||||
|
|
@ -263,10 +269,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
{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">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowProfileMenu(false);
|
||||
setCurrentPage('general');
|
||||
}}
|
||||
onClick={() => { setShowProfileMenu(false); setCurrentPage('general'); }}
|
||||
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]" />
|
||||
|
|
@ -302,58 +305,46 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
{pages[currentPage]}
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
{pages[currentPage]}
|
||||
</Suspense>
|
||||
</main>
|
||||
|
||||
<nav className="bg-[#4d4d4d] flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => setCurrentPage('status')}
|
||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
||||
>
|
||||
<Activity className="w-5 h-5 text-white" />
|
||||
<button onClick={() => setCurrentPage('status')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||
<Activity className="w-5 h-5 text-white" />
|
||||
<span className="text-xs text-white">Status</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('devices')}
|
||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
||||
>
|
||||
<button onClick={() => setCurrentPage('devices')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||
<HardDrive className="w-5 h-5 text-white" />
|
||||
<span className="text-xs text-white">Devices</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('iec')}
|
||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
||||
>
|
||||
<Cpu className="w-5 h-5 text-white" />
|
||||
<button onClick={() => setCurrentPage('iec')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||
<Cpu className="w-5 h-5 text-white" />
|
||||
<span className="text-xs text-white">IEC</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('network')}
|
||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
||||
>
|
||||
<Network className="w-5 h-5 text-white" />
|
||||
<button onClick={() => setCurrentPage('network')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||
<Network className="w-5 h-5 text-white" />
|
||||
<span className="text-xs text-white">Network</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('tools')}
|
||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
||||
>
|
||||
<Wrench className="w-5 h-5 text-white" />
|
||||
<button onClick={() => setCurrentPage('tools')} className="flex-1 flex flex-col items-center gap-1 py-2">
|
||||
<Wrench className="w-5 h-5 text-white" />
|
||||
<span className="text-xs text-white">System</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{showSearch && (
|
||||
<SearchOverlay
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
onClose={() => setShowSearch(false)}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<SearchOverlay
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
onClose={() => setShowSearch(false)}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</WsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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',
|
||||
};
|
||||
|
||||
// ─── 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 (
|
||||
<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.
|
||||
|
||||
// ─── ActionsModal ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -1229,71 +1105,27 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
|
|||
} : undefined}
|
||||
/>
|
||||
|
||||
{/* ── File viewer overlay ── */}
|
||||
{viewEntry && (
|
||||
<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={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>
|
||||
<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" />
|
||||
</button>
|
||||
{/* ── File viewer overlay (lazy-loaded) ── */}
|
||||
{viewEntry && viewMode && (
|
||||
<Suspense fallback={
|
||||
<div className="fixed inset-0 bg-neutral-950 z-50 flex items-center justify-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-neutral-400" />
|
||||
</div>
|
||||
|
||||
<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}
|
||||
mode="code"
|
||||
syntaxHighlightLang={EXT_TO_LANG[viewEntry.name.split('.').pop()?.toLowerCase() ?? ''] ?? 'text'}
|
||||
onSave={s => saveViewFile(s)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<MediaViewerEditor
|
||||
entry={viewEntry}
|
||||
mode={viewMode}
|
||||
availableModes={availableViewers(viewEntry)}
|
||||
text={viewText}
|
||||
imgUrl={viewImgUrl}
|
||||
hexData={viewHexData}
|
||||
loading={viewLoading}
|
||||
onClose={closeViewer}
|
||||
onSwitchMode={m => void switchViewMode(m)}
|
||||
onSave={saveViewFile}
|
||||
onDownload={() => void downloadEntry(viewEntry)}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{/* ── 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": {
|
||||
"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"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user