feat(CodeEditor, MediaManager): add 'code' mode support and enhance file extension handling
This commit is contained in:
parent
61b5c6dc39
commit
48f0de753e
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user