feat(CodeEditor, MediaManager): add 'code' mode support and enhance file extension handling

This commit is contained in:
Jaime Idolpx 2026-06-09 03:41:23 -04:00
parent 61b5c6dc39
commit 48f0de753e
2 changed files with 34 additions and 8 deletions

View File

@ -7,11 +7,13 @@ import { Eye, Pencil, Save } from 'lucide-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
export type CodeMode = 'text' | 'json' | 'xml'; export type CodeMode = 'text' | 'json' | 'xml' | 'code';
interface CodeEditorProps { interface CodeEditorProps {
text: string; text: string;
mode: CodeMode; mode: CodeMode;
/** Prism language id for syntax highlighting in view mode (used when mode='code'). */
syntaxHighlightLang?: string;
readOnly?: boolean; readOnly?: boolean;
onSave?: (text: string) => Promise<void>; onSave?: (text: string) => Promise<void>;
} }
@ -27,10 +29,11 @@ const langExt: Record<CodeMode, any> = {
json: json(), json: json(),
xml: xml(), xml: xml(),
text: [], text: [],
code: [], // no CM lang pack needed; SyntaxHighlighter handles view-mode
}; };
const syntaxLang: Record<CodeMode, string> = { const syntaxLang: Record<CodeMode, string> = {
text: 'text', json: 'json', xml: 'xml', text: 'text', json: 'json', xml: 'xml', code: 'text',
}; };
function prettify(text: string, mode: CodeMode): string { function prettify(text: string, mode: CodeMode): string {
@ -40,7 +43,7 @@ function prettify(text: string, mode: CodeMode): string {
return text; return text;
} }
export default function CodeEditor({ text, mode, readOnly = false, onSave }: CodeEditorProps) { export default function CodeEditor({ text, mode, syntaxHighlightLang, readOnly = false, onSave }: CodeEditorProps) {
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [editInitText, setEditInitText] = useState(''); const [editInitText, setEditInitText] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -71,7 +74,7 @@ export default function CodeEditor({ text, mode, readOnly = false, onSave }: Cod
)} )}
<div className="flex-1 overflow-auto text-xs"> <div className="flex-1 overflow-auto text-xs">
<SyntaxHighlighter <SyntaxHighlighter
language={syntaxLang[mode]} language={syntaxHighlightLang ?? syntaxLang[mode]}
style={vscDarkPlus} style={vscDarkPlus}
customStyle={{ margin: 0, minHeight: '100%', background: '#0a0a0a', fontSize: '12px', lineHeight: '1.5' }} customStyle={{ margin: 0, minHeight: '100%', background: '#0a0a0a', fontSize: '12px', lineHeight: '1.5' }}
showLineNumbers showLineNumbers

View File

@ -35,6 +35,7 @@ import {
RefreshCw, RefreshCw,
Save, Save,
Search, Search,
Terminal,
Trash2, Trash2,
Upload, Upload,
X, X,
@ -76,17 +77,17 @@ 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'; type ViewMode = 'text' | 'markdown' | 'json' | 'xml' | 'hex' | 'image' | 'config' | 'code';
// ─── Extension sets ────────────────────────────────────────────────────────── // ─── Extension sets ──────────────────────────────────────────────────────────
const TEXT_EXTS = new Set(['txt', 'cfg', 'ini', 'seq', 'log', 'csv', 'lst']); const TEXT_EXTS = new Set(['txt', 'cfg', 'ini', 'seq', 'log', 'csv', 'lst']);
const CODE_EXTS = new Set(['asm', 'bas', 's', 'js', 'css']); const CODE_EXTS = new Set(['asm', 'bas', 's', 'js', 'ts', 'jsx', 'tsx', 'css', 'scss', 'py', 'c', 'cpp', 'h', 'hpp', 'lua', 'sh', 'bash', 'php', 'rb', 'rs', 'go', 'java', 'cs', 'kt', 'sql', 'pl']);
const MD_EXTS = new Set(['md', 'markdown']); const MD_EXTS = new Set(['md', 'markdown']);
const JSON_EXTS = new Set(['json', 'webmanifest']); const JSON_EXTS = new Set(['json', 'webmanifest']);
const XML_EXTS = new Set(['xml', 'html', 'htm', 'rss', 'atom', 'xsl']); const XML_EXTS = new Set(['xml', 'html', 'htm', 'rss', 'atom', 'xsl']);
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico']); const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico']);
const AUDIO_EXTS = new Set(['sid', 'psid', 'mus', 'vgm']); const AUDIO_EXTS = new Set(['sid', 'psid', 'rsid', 'mus', 'vgm']);
const ROM_EXTS = new Set(['bin', 'rom', 'crt']); const ROM_EXTS = new Set(['bin', 'rom', 'crt']);
const TAPE_EXTS = new Set(['tap', 'htap', 't64', 'tcrt']); const TAPE_EXTS = new Set(['tap', 'htap', 't64', 'tcrt']);
const DISK_EXTS = new Set(['d41', 'd64', 'd71', 'd80', 'd81', 'd82', 'g64', 'g71', 'g81', 'p64', 'p71', 'p81', 'nib']); const DISK_EXTS = new Set(['d41', 'd64', 'd71', 'd80', 'd81', 'd82', 'g64', 'g71', 'g81', 'p64', 'p71', 'p81', 'nib']);
@ -102,6 +103,7 @@ function defaultViewMode(entry: EntryInfo): ViewMode {
if (MD_EXTS.has(ext)) return 'markdown'; if (MD_EXTS.has(ext)) return 'markdown';
if (JSON_EXTS.has(ext)) return 'json'; if (JSON_EXTS.has(ext)) return 'json';
if (XML_EXTS.has(ext)) return 'xml'; if (XML_EXTS.has(ext)) return 'xml';
if (CODE_EXTS.has(ext)) return 'code';
if (TEXT_EXTS.has(ext)) return 'text'; if (TEXT_EXTS.has(ext)) return 'text';
return 'hex'; return 'hex';
} }
@ -114,6 +116,7 @@ function availableViewers(entry: EntryInfo): ViewMode[] {
markdown: ['markdown', 'text', 'hex'], markdown: ['markdown', 'text', 'hex'],
json: ['json', 'text', 'hex'], json: ['json', 'text', 'hex'],
xml: ['xml', 'text', 'hex'], xml: ['xml', 'text', 'hex'],
code: ['code', 'text', 'hex'],
text: ['text', 'hex'], text: ['text', 'hex'],
hex: ['hex', 'text'], hex: ['hex', 'text'],
}; };
@ -121,7 +124,17 @@ function availableViewers(entry: EntryInfo): ViewMode[] {
} }
const VIEWER_LABEL: Record<ViewMode, string> = { const VIEWER_LABEL: Record<ViewMode, string> = {
text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', hex: 'Hex', image: 'Image', config: 'Config', code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', hex: 'Hex', image: 'Image', config: 'Config',
};
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 components ───────────────────────────────────────────────────────
@ -132,6 +145,7 @@ function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: strin
case 'text': return <AlignLeft className={cls} />; case 'text': return <AlignLeft className={cls} />;
case 'markdown': return <BookOpen className={cls} />; case 'markdown': return <BookOpen className={cls} />;
case 'json': return <Braces className={cls} />; case 'json': return <Braces className={cls} />;
case 'code': return <Terminal className={cls} />;
case 'xml': return <Code2 className={cls} />; case 'xml': return <Code2 className={cls} />;
case 'hex': return <Hash className={cls} />; case 'hex': return <Hash className={cls} />;
case 'image': return <ImageIcon className={cls} />; case 'image': return <ImageIcon className={cls} />;
@ -1319,6 +1333,15 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
{!viewLoading && viewMode === 'config' && viewText !== null && ( {!viewLoading && viewMode === 'config' && viewText !== null && (
<ConfigEditor key={viewEntry.path} text={viewText} onSave={s => saveViewFile(s)} /> <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>
</div> </div>
)} )}