jsoneditor

This commit is contained in:
2026-02-23 09:33:40 +00:00
parent e90a49232b
commit 0bea406dcd
7 changed files with 85 additions and 3 deletions
@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useMemo, useRef } from 'react';
import { Loader2, FolderArchive } from 'lucide-react';
import { getLang, getRawUrl, getTranscodeUrl, needsTranscode, getArchiveBaseName } from './file-types';
import { useFileViewer } from './FileViewerContext';
@@ -11,6 +11,7 @@ import { MarkdownRenderer } from './renderers/MarkdownRenderer';
import { TextRenderer } from './renderers/TextRenderer';
import { ScrollToTopButton } from './renderers/ScrollToTopButton';
import { EditorRenderer } from './renderers/EditorRenderer';
import { JsonEditorRenderer } from './renderers/JsonEditorRenderer';
export const FileViewerBody = () => {
const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = useFileViewer();
@@ -19,6 +20,17 @@ export const FileViewerBody = () => {
const videoSrc =
fileType === 'video' ? (needsTranscode(fileName) ? getTranscodeUrl(filePath, root) : getRawUrl(filePath, root)) : '';
const isJson = useMemo(() => {
if (!content) return false;
if (/\.jsonc?$/i.test(fileName)) return true;
try {
const parsed = JSON.parse(content);
return typeof parsed === 'object' && parsed !== null;
} catch {
return false;
}
}, [content, fileName]);
return (
<div ref={scrollRef} className="h-full overflow-auto">
{loading ? (
@@ -43,8 +55,15 @@ export const FileViewerBody = () => {
<VideoRenderer src={videoSrc} fileName={fileName} />
) : fileType === 'audio' ? (
<AudioRenderer src={getRawUrl(filePath, root)} fileName={fileName} autoPlay={autoPlay} />
) : content !== null && editing && isJson ? (
<JsonEditorRenderer content={content} filePath={filePath} root={root} onSave={setContent} />
) : content !== null && editing ? (
<EditorRenderer content={content} fileName={fileName} filePath={filePath} root={root} onSave={setContent} />
) : content !== null && isJson ? (
<div>
<CodeRenderer content={content} lang="json" />
<ScrollToTopButton scrollContainer={scrollRef} />
</div>
) : content !== null ? (
fileType === 'code' ? (
<div>
@@ -16,6 +16,7 @@ export type FileViewerContextValue = {
extractAudioLoading: boolean;
extractLoading: boolean;
autoPlay: boolean;
editable: boolean;
editing: boolean;
setEditing: (editing: boolean) => void;
setContent: (content: string) => void;
@@ -22,6 +22,7 @@ export const FileViewerHeader = () => {
handleDownload,
handleSaveResult,
saveResultLoading,
editable,
editing,
setEditing,
} = useFileViewer();
@@ -48,7 +49,7 @@ export const FileViewerHeader = () => {
<span className="text-[10px] font-mono uppercase tracking-wider shrink-0 opacity-60">
{fileType === 'code' ? getLang(fileName) : fileType}
</span>
{(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
{editable && content && (
<button
onClick={() => setEditing(!editing)}
className="p-1 rounded hover:bg-current/10 transition-colors cursor-pointer"
@@ -36,6 +36,7 @@ export const FileViewerProvider = ({
const [editing, setEditing] = useState(false);
const files = useFilesAPI(root);
const fileType = getFileType(fileName);
const editable = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
useEffect(() => {
if (directContent !== undefined) {
@@ -162,6 +163,7 @@ export const FileViewerProvider = ({
extractAudioLoading,
extractLoading,
autoPlay,
editable,
editing,
setEditing,
setContent,
@@ -0,0 +1,51 @@
import { useCallback, useRef } from 'react';
import { JsonEditor } from '@visual-json/react';
import type { JsonValue } from '@visual-json/core';
import { toast } from 'sonner';
import { useFilesAPI } from '../../../hooks/useFilesAPI';
type JsonEditorRendererProps = {
content: string;
filePath: string;
root: string;
onSave?: (content: string) => void;
};
export const JsonEditorRenderer = ({ content, filePath, root, onSave }: JsonEditorRendererProps) => {
const { writeFile } = useFilesAPI(root);
const valueRef = useRef<JsonValue>(JSON.parse(content));
const handleChange = useCallback((value: JsonValue) => {
valueRef.current = value;
}, []);
const handleSave = useCallback(async () => {
const newContent = JSON.stringify(valueRef.current, null, 2);
try {
await writeFile(filePath, newContent);
onSave?.(newContent);
toast.success('File saved');
} catch {
toast.error('Failed to save file');
}
}, [filePath, writeFile, onSave]);
return (
<div
className="h-full"
onKeyDown={(ev) => {
if ((ev.ctrlKey || ev.metaKey) && ev.key === 's') {
ev.preventDefault();
handleSave();
}
}}
>
<JsonEditor
defaultValue={valueRef.current}
onChange={handleChange}
height="100%"
width="100%"
/>
</div>
);
};