/* eslint-disable react-hooks/exhaustive-deps */ import React, { Suspense, lazy, useState, useEffect, useRef } from "react"; import { cn } from "@termix/plugin-sdk/ui"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { FileText, Image as ImageIcon, Film, Music, File as FileIcon, Code, AlertCircle, Download, Eye, Edit, Save, RotateCcw, Keyboard, ExternalLink, Settings, Search, ZoomIn, ZoomOut, } from "lucide-react"; import { SiJavascript, SiTypescript, SiPython, SiCplusplus, SiC, SiDotnet, SiPhp, SiRuby, SiGo, SiRust, SiHtml5, SiSass, SiVuedotjs, SiSvelte, SiMarkdown, SiGnubash, SiMysql, SiDocker, } from "react-icons/si"; import { Button } from "@termix/plugin-sdk/ui"; import { Kbd, KbdKey } from "@termix/plugin-sdk/ui"; import type { CodeEditorHandle } from "./CodeEditor.tsx"; import { loadAudioPreview, loadCodeEditor, loadImagePreview, loadMarkdownRenderer, loadPdfPreview, resolveFilePreviewKind, } from "../file-preview-preload"; const CodeEditor = lazy(loadCodeEditor); const ImagePreview = lazy(loadImagePreview); const MarkdownRenderer = lazy(loadMarkdownRenderer); const PdfPreview = lazy(loadPdfPreview); const AudioPreview = lazy(loadAudioPreview); interface FileItem { name: string; type: "file" | "directory" | "link"; path: string; size?: number; modified?: string; permissions?: string; owner?: string; group?: string; } interface FileViewerProps { file: FileItem; content?: string; savedContent?: string; isLoading?: boolean; isSaving?: boolean; isEditable?: boolean; resetKey?: number; onContentChange?: (content: string) => void; onSave?: (content: string) => void; onRevert?: () => void; onDownload?: () => void; onOpenExternal?: () => void; onChooseExternalEditor?: () => void; onMediaDimensionsChange?: (dimensions: { width: number; height: number; }) => void; } function getLanguageIcon(filename: string): React.ReactNode { const ext = filename.split(".").pop()?.toLowerCase() || ""; const baseName = filename.toLowerCase(); if (["dockerfile"].includes(baseName)) { return ; } if (["makefile", "rakefile", "gemfile"].includes(baseName)) { return ; } const iconMap: Record = { js: , jsx: , ts: , tsx: , py: , java: , cpp: , c: , cs: , php: , rb: , go: , rs: , html: , css: , scss: , sass: , less: , json: , xml: , yaml: , yml: , toml: , sql: , sh: , bash: , zsh: , vue: , svelte: , md: , conf: , ini: , }; return iconMap[ext] || ; } function getFileType(filename: string): { type: string; icon: React.ReactNode; color: string; } { const kind = resolveFilePreviewKind(filename); if (kind === "image") { return { type: "image", icon: , color: "text-green-500", }; } else if (kind === "video") { return { type: "video", icon: , color: "text-purple-500", }; } else if (kind === "audio") { return { type: "audio", icon: , color: "text-pink-500", }; } else if (kind === "markdown") { return { type: "markdown", icon: , color: "text-blue-600", }; } else if (kind === "pdf") { return { type: "pdf", icon: , color: "text-red-600", }; } else if (kind === "text") { return { type: "text", icon: , color: "text-blue-500", }; } else if (kind === "code") { return { type: "code", icon: getLanguageIcon(filename), color: "text-yellow-500", }; } else { return { type: "unknown", icon: , color: "text-foreground-subtle", }; } } function formatFileSize(bytes?: number, t?: (key: string) => string): string { if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size"; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; } function PreviewFallback({ label }: { label: string }) { return (

{label}

); } export function FileViewer({ file, content = "", savedContent = "", isLoading = false, isSaving = false, isEditable = false, resetKey, onContentChange, onSave, onRevert, onDownload, onOpenExternal, onChooseExternalEditor, onMediaDimensionsChange, }: FileViewerProps) { const { t } = useTranslation(); const [editedContent, setEditedContent] = useState(content); const [, setOriginalContent] = useState(savedContent || content); const [hasChanges, setHasChanges] = useState(false); const [showLargeFileWarning, setShowLargeFileWarning] = useState(false); const [forceShowAsText, setForceShowAsText] = useState(false); const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false); const [editorFocused, setEditorFocused] = useState(false); const [markdownEditMode, setMarkdownEditMode] = useState(false); const [editorFontSize, setEditorFontSize] = useState(() => { const stored = localStorage.getItem("fileManagerEditorFontSize"); return stored ? parseInt(stored, 10) : 14; }); const editorRef = useRef(null); const MIN_FONT_SIZE = 8; const MAX_FONT_SIZE = 32; const decreaseFontSize = () => { setEditorFontSize((prev) => { const next = Math.max(MIN_FONT_SIZE, prev - 1); localStorage.setItem("fileManagerEditorFontSize", String(next)); return next; }); }; const increaseFontSize = () => { setEditorFontSize((prev) => { const next = Math.min(MAX_FONT_SIZE, prev + 1); localStorage.setItem("fileManagerEditorFontSize", String(next)); return next; }); }; const fileTypeInfo = getFileType(file.name); const WARNING_SIZE = 50 * 1024 * 1024; const MAX_SIZE = Number.MAX_SAFE_INTEGER; const shouldShowAsText = fileTypeInfo.type === "text" || fileTypeInfo.type === "code" || (fileTypeInfo.type === "unknown" && (forceShowAsText || !file.size || file.size <= WARNING_SIZE)); const isLargeFile = file.size && file.size > WARNING_SIZE; const isTooLarge = file.size && file.size > MAX_SIZE; useEffect(() => { setEditedContent(content); if (savedContent) { setOriginalContent(savedContent); } }, [file.name, file.path, resetKey]); useEffect(() => { setHasChanges(content !== savedContent); if (fileTypeInfo.type === "unknown" && isLargeFile && !forceShowAsText) { setShowLargeFileWarning(true); } else { setShowLargeFileWarning(false); } }, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText]); const handleContentChange = (newContent: string) => { setEditedContent(newContent); setHasChanges(newContent !== savedContent); onContentChange?.(newContent); }; const handleSave = () => { if (isSaving) return; onSave?.(editedContent); }; const handleRevert = () => { if (onRevert) { onRevert(); } else { setEditedContent(savedContent); setHasChanges(false); } }; useEffect(() => { if (!editorFocused || !isEditable) return; const handleKeyDown = (e: KeyboardEvent) => { const isCtrl = e.ctrlKey || e.metaKey; if (isCtrl && e.key.toLowerCase() === "s") { e.preventDefault(); e.stopPropagation(); handleSave(); } }; document.addEventListener("keydown", handleKeyDown, true); return () => { document.removeEventListener("keydown", handleKeyDown, true); }; }, [editorFocused, isEditable, handleSave]); const handleConfirmOpenAsText = () => { setForceShowAsText(true); setShowLargeFileWarning(false); }; const handleCancelOpenAsText = () => { setShowLargeFileWarning(false); }; if (isLoading) { return (

{t("fileManager.loadingFile")}

); } return (
{fileTypeInfo.icon}

{file.name}

{formatFileSize(file.size, t)} {file.modified && ( {t("fileManager.modified")}: {file.modified} )} {fileTypeInfo.type.toUpperCase()}
{isEditable && ( )} {isEditable && (
{editorFontSize}px
)} {isEditable && ( )} {hasChanges && ( <> )} {onDownload && ( )} {isEditable && onOpenExternal && (
{onChooseExternalEditor && ( )}
)}
{showKeyboardShortcuts && isEditable && (

{t("fileManager.keyboardShortcuts")}

{t("fileManager.searchAndReplace")}

{t("fileManager.search")} Ctrl+F
{t("fileManager.replace")} Ctrl+H
{t("fileManager.findNext")} F3
{t("fileManager.findPrevious")} Shift+F3

{t("fileManager.editing")}

{t("fileManager.save")} Ctrl+S
{t("fileManager.selectAll")} Ctrl+A
{t("fileManager.undo")} Ctrl+Z
{t("fileManager.redo")} Ctrl+Y
{t("fileManager.toggleComment")} Ctrl+/
{t("fileManager.autoComplete")} Ctrl+Space
{t("fileManager.moveLineUp")} Alt+↑
{t("fileManager.moveLineDown")} Alt+↓
)}
{showLargeFileWarning && (

{t("fileManager.largeFileWarning")}

{t("fileManager.largeFileWarningDesc", { size: formatFileSize(file.size, t), })}

{isTooLarge ? (

{t("fileManager.fileTooLarge")}

) : (

{t("fileManager.confirmOpenAsText")}

)}
{!isTooLarge && ( )}
)} {fileTypeInfo.type === "image" && !showLargeFileWarning && ( } > )} {shouldShowAsText && !showLargeFileWarning && (
{isEditable ? ( } > setEditorFocused(true)} onBlur={() => setEditorFocused(false)} placeholder={t("fileManager.startTyping")} fontSize={editorFontSize} /> ) : (
{editedContent || content || t("fileManager.fileIsEmpty")}
)}
)} {fileTypeInfo.type === "video" && !showLargeFileWarning && (
{(() => { const ext = file.name.split(".").pop()?.toLowerCase() || ""; const mimeType = (() => { switch (ext) { case "mp4": return "video/mp4"; case "webm": return "video/webm"; case "mkv": return "video/x-matroska"; case "avi": return "video/x-msvideo"; case "mov": return "video/quicktime"; case "wmv": return "video/x-ms-wmv"; case "flv": return "video/x-flv"; default: return "video/mp4"; } })(); const videoUrl = `data:${mimeType};base64,${content}`; return (
); })()}
)} {fileTypeInfo.type === "markdown" && !showLargeFileWarning && (
{markdownEditMode ? ( <>