text files edit
This commit is contained in:
@@ -154,8 +154,10 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
|||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (ev: KeyboardEvent) => {
|
const handler = (ev: KeyboardEvent) => {
|
||||||
const tag = (ev.target as HTMLElement).tagName;
|
const target = ev.target as HTMLElement;
|
||||||
|
const tag = target.tagName;
|
||||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
||||||
|
if (target.closest('.monaco-editor')) return;
|
||||||
|
|
||||||
if (ev.key === 'Escape') {
|
if (ev.key === 'Escape') {
|
||||||
if (viewPath) return;
|
if (viewPath) return;
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { CodeRenderer } from './renderers/CodeRenderer';
|
|||||||
import { MarkdownRenderer } from './renderers/MarkdownRenderer';
|
import { MarkdownRenderer } from './renderers/MarkdownRenderer';
|
||||||
import { TextRenderer } from './renderers/TextRenderer';
|
import { TextRenderer } from './renderers/TextRenderer';
|
||||||
import { ScrollToTopButton } from './renderers/ScrollToTopButton';
|
import { ScrollToTopButton } from './renderers/ScrollToTopButton';
|
||||||
|
import { EditorRenderer } from './renderers/EditorRenderer';
|
||||||
|
|
||||||
export const FileViewerBody = () => {
|
export const FileViewerBody = () => {
|
||||||
const { filePath, fileName, root, fileType, content, loading, error, autoPlay } = useFileViewer();
|
const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = useFileViewer();
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const videoSrc =
|
const videoSrc =
|
||||||
@@ -42,6 +43,8 @@ export const FileViewerBody = () => {
|
|||||||
<VideoRenderer src={videoSrc} fileName={fileName} />
|
<VideoRenderer src={videoSrc} fileName={fileName} />
|
||||||
) : fileType === 'audio' ? (
|
) : fileType === 'audio' ? (
|
||||||
<AudioRenderer src={getRawUrl(filePath, root)} fileName={fileName} autoPlay={autoPlay} />
|
<AudioRenderer src={getRawUrl(filePath, root)} fileName={fileName} autoPlay={autoPlay} />
|
||||||
|
) : content !== null && editing ? (
|
||||||
|
<EditorRenderer content={content} fileName={fileName} filePath={filePath} root={root} onSave={setContent} />
|
||||||
) : content !== null ? (
|
) : content !== null ? (
|
||||||
fileType === 'code' ? (
|
fileType === 'code' ? (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export type FileViewerContextValue = {
|
|||||||
extractAudioLoading: boolean;
|
extractAudioLoading: boolean;
|
||||||
extractLoading: boolean;
|
extractLoading: boolean;
|
||||||
autoPlay: boolean;
|
autoPlay: boolean;
|
||||||
|
editing: boolean;
|
||||||
|
setEditing: (editing: boolean) => void;
|
||||||
|
setContent: (content: string) => void;
|
||||||
handleReadAloud: () => void;
|
handleReadAloud: () => void;
|
||||||
handleOcr: () => void;
|
handleOcr: () => void;
|
||||||
handleTranscribe: () => void;
|
handleTranscribe: () => void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Download, Loader2, Music, Film, Image, FileType2, Volume2, ScanText, FileText, AudioLines, FolderArchive, Save } from 'lucide-react';
|
import { Download, Loader2, Music, Film, Image, FileType2, Volume2, ScanText, FileText, AudioLines, FolderArchive, Save, Pencil, Eye } from 'lucide-react';
|
||||||
import { getIcon } from 'material-file-icons';
|
import { getIcon } from 'material-file-icons';
|
||||||
import { getLang } from './file-types';
|
import { getLang } from './file-types';
|
||||||
import { useFileViewer } from './FileViewerContext';
|
import { useFileViewer } from './FileViewerContext';
|
||||||
@@ -22,6 +22,8 @@ export const FileViewerHeader = () => {
|
|||||||
handleDownload,
|
handleDownload,
|
||||||
handleSaveResult,
|
handleSaveResult,
|
||||||
saveResultLoading,
|
saveResultLoading,
|
||||||
|
editing,
|
||||||
|
setEditing,
|
||||||
} = useFileViewer();
|
} = useFileViewer();
|
||||||
|
|
||||||
const headerIcon =
|
const headerIcon =
|
||||||
@@ -46,6 +48,15 @@ export const FileViewerHeader = () => {
|
|||||||
<span className="text-[10px] font-mono uppercase tracking-wider shrink-0 opacity-60">
|
<span className="text-[10px] font-mono uppercase tracking-wider shrink-0 opacity-60">
|
||||||
{fileType === 'code' ? getLang(fileName) : fileType}
|
{fileType === 'code' ? getLang(fileName) : fileType}
|
||||||
</span>
|
</span>
|
||||||
|
{(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
|
||||||
|
<button
|
||||||
|
onClick={() => setEditing(!editing)}
|
||||||
|
className="p-1 rounded hover:bg-current/10 transition-colors cursor-pointer"
|
||||||
|
title={editing ? 'View' : 'Edit'}
|
||||||
|
>
|
||||||
|
{editing ? <Eye className="h-3.5 w-3.5" /> : <Pencil className="h-3.5 w-3.5" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
|
{(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
|
||||||
<button
|
<button
|
||||||
onClick={handleReadAloud}
|
onClick={handleReadAloud}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const FileViewerProvider = ({
|
|||||||
const [extractAudioLoading, setExtractAudioLoading] = useState(false);
|
const [extractAudioLoading, setExtractAudioLoading] = useState(false);
|
||||||
const [extractLoading, setExtractLoading] = useState(false);
|
const [extractLoading, setExtractLoading] = useState(false);
|
||||||
const [saveResultLoading, setSaveResultLoading] = useState(false);
|
const [saveResultLoading, setSaveResultLoading] = useState(false);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
const files = useFilesAPI(root);
|
const files = useFilesAPI(root);
|
||||||
const fileType = getFileType(fileName);
|
const fileType = getFileType(fileName);
|
||||||
|
|
||||||
@@ -161,6 +162,9 @@ export const FileViewerProvider = ({
|
|||||||
extractAudioLoading,
|
extractAudioLoading,
|
||||||
extractLoading,
|
extractLoading,
|
||||||
autoPlay,
|
autoPlay,
|
||||||
|
editing,
|
||||||
|
setEditing,
|
||||||
|
setContent,
|
||||||
handleReadAloud,
|
handleReadAloud,
|
||||||
handleOcr,
|
handleOcr,
|
||||||
handleTranscribe,
|
handleTranscribe,
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import Editor, { type OnMount } from '@monaco-editor/react';
|
||||||
|
import type { editor as MonacoEditor } from 'monaco-editor';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getLanguage } from '../../CodeEditor/language-map';
|
||||||
|
import { useFilesAPI } from '../../../hooks/useFilesAPI';
|
||||||
|
|
||||||
|
type EditorRendererProps = {
|
||||||
|
content: string;
|
||||||
|
fileName: string;
|
||||||
|
filePath: string;
|
||||||
|
root: string;
|
||||||
|
onSave?: (content: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorRenderer = ({ content, fileName, filePath, root, onSave }: EditorRendererProps) => {
|
||||||
|
const editorRef = useRef<MonacoEditor.IStandaloneCodeEditor | null>(null);
|
||||||
|
const { writeFile } = useFilesAPI(root);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
if (!editor) return;
|
||||||
|
const newContent = editor.getValue();
|
||||||
|
try {
|
||||||
|
await writeFile(filePath, newContent);
|
||||||
|
onSave?.(newContent);
|
||||||
|
toast.success('File saved');
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to save file');
|
||||||
|
}
|
||||||
|
}, [filePath, writeFile, onSave]);
|
||||||
|
|
||||||
|
const handleMount: OnMount = (editor, monaco) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||||
|
handleSave();
|
||||||
|
});
|
||||||
|
editor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (ev: KeyboardEvent) => {
|
||||||
|
if ((ev.ctrlKey || ev.metaKey) && ev.key === 's') {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handler);
|
||||||
|
return () => window.removeEventListener('keydown', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
language={getLanguage(fileName)}
|
||||||
|
defaultValue={content}
|
||||||
|
onMount={handleMount}
|
||||||
|
options={{
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 14,
|
||||||
|
tabSize: 2,
|
||||||
|
wordWrap: 'on',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
renderWhitespace: 'selection',
|
||||||
|
smoothScrolling: true,
|
||||||
|
cursorBlinking: 'smooth',
|
||||||
|
cursorSmoothCaretAnimation: 'on',
|
||||||
|
padding: { top: 8 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user