Files
Termix/src/ui/features/file-manager/components/FileViewer.tsx
T

955 lines
33 KiB
TypeScript
Raw Normal View History

2026-06-04 15:16:53 -04:00
/* eslint-disable react-hooks/exhaustive-deps */
2026-05-06 15:12:07 -05:00
import React, { Suspense, lazy, useState, useEffect, useRef } from "react";
+1
2025-12-31 22:20:12 -06:00
import { cn } from "@/lib/utils.ts";
2025-10-01 15:40:10 -05:00
import { useTranslation } from "react-i18next";
import {
FileText,
Image as ImageIcon,
Film,
Music,
File as FileIcon,
Code,
AlertCircle,
Download,
Eye,
Edit,
Save,
RotateCcw,
Keyboard,
2026-06-29 13:28:26 -05:00
ExternalLink,
Settings,
2025-10-01 15:40:10 -05:00
Search,
2026-06-21 15:55:41 -05:00
ZoomIn,
ZoomOut,
2025-10-01 15:40:10 -05:00
} 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";
2026-05-11 01:23:11 -05:00
import { Button } from "@/components/button.tsx";
2026-06-04 15:16:53 -04:00
import { Kbd, KbdKey } from "@/components/kbd.tsx";
2026-05-06 15:12:07 -05:00
import type { CodeEditorHandle } from "./CodeEditor.tsx";
+5
2026-08-19 14:12:06 -05:00
import {
loadAudioPreview,
loadCodeEditor,
loadImagePreview,
loadMarkdownRenderer,
loadPdfPreview,
resolveFilePreviewKind,
} from "../file-preview-preload";
2025-10-01 15:40:10 -05:00
+5
2026-08-19 14:12:06 -05:00
const CodeEditor = lazy(loadCodeEditor);
const ImagePreview = lazy(loadImagePreview);
const MarkdownRenderer = lazy(loadMarkdownRenderer);
const PdfPreview = lazy(loadPdfPreview);
const AudioPreview = lazy(loadAudioPreview);
2025-10-01 15:40:10 -05:00
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;
+8
2026-07-19 12:29:52 -05:00
isSaving?: boolean;
2025-10-01 15:40:10 -05:00
isEditable?: boolean;
2026-06-21 15:55:41 -05:00
resetKey?: number;
2025-10-01 15:40:10 -05:00
onContentChange?: (content: string) => void;
onSave?: (content: string) => void;
onRevert?: () => void;
onDownload?: () => void;
2026-06-29 13:28:26 -05:00
onOpenExternal?: () => void;
onChooseExternalEditor?: () => void;
2025-10-01 15:40:10 -05:00
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 <SiDocker className="w-6 h-6 text-blue-400" />;
}
if (["makefile", "rakefile", "gemfile"].includes(baseName)) {
return <SiRuby className="w-6 h-6 text-red-500" />;
}
const iconMap: Record<string, React.ReactNode> = {
js: <SiJavascript className="w-6 h-6 text-yellow-400" />,
jsx: <SiJavascript className="w-6 h-6 text-yellow-400" />,
ts: <SiTypescript className="w-6 h-6 text-blue-500" />,
tsx: <SiTypescript className="w-6 h-6 text-blue-500" />,
py: <SiPython className="w-6 h-6 text-blue-400" />,
+5
2026-03-08 18:02:14 -05:00
java: <Code className="w-6 h-6 text-red-500" />,
2025-10-01 15:40:10 -05:00
cpp: <SiCplusplus className="w-6 h-6 text-blue-600" />,
c: <SiC className="w-6 h-6 text-blue-700" />,
cs: <SiDotnet className="w-6 h-6 text-purple-600" />,
php: <SiPhp className="w-6 h-6 text-indigo-500" />,
rb: <SiRuby className="w-6 h-6 text-red-500" />,
go: <SiGo className="w-6 h-6 text-cyan-500" />,
rs: <SiRust className="w-6 h-6 text-orange-600" />,
html: <SiHtml5 className="w-6 h-6 text-orange-500" />,
+5
2026-03-08 18:02:14 -05:00
css: <Code className="w-6 h-6 text-blue-500" />,
2025-10-01 15:40:10 -05:00
scss: <SiSass className="w-6 h-6 text-pink-500" />,
sass: <SiSass className="w-6 h-6 text-pink-500" />,
+5
2026-03-08 18:02:14 -05:00
less: <Code className="w-6 h-6 text-blue-600" />,
json: <Code className="w-6 h-6 text-yellow-500" />,
xml: <Code className="w-6 h-6 text-orange-500" />,
yaml: <Code className="w-6 h-6 text-red-400" />,
yml: <Code className="w-6 h-6 text-red-400" />,
toml: <Code className="w-6 h-6 text-orange-400" />,
2025-10-01 15:40:10 -05:00
sql: <SiMysql className="w-6 h-6 text-blue-500" />,
+1
2025-12-31 22:20:12 -06:00
sh: <SiGnubash className="w-6 h-6 text-foreground" />,
bash: <SiGnubash className="w-6 h-6 text-foreground" />,
+5
2026-03-08 18:02:14 -05:00
zsh: <Code className="w-6 h-6 text-foreground" />,
2025-10-01 15:40:10 -05:00
vue: <SiVuedotjs className="w-6 h-6 text-green-500" />,
svelte: <SiSvelte className="w-6 h-6 text-orange-500" />,
+1
2025-12-31 22:20:12 -06:00
md: <SiMarkdown className="w-6 h-6 text-muted-foreground" />,
+5
2026-03-08 18:02:14 -05:00
conf: <Code className="w-6 h-6 text-muted-foreground" />,
+1
2025-12-31 22:20:12 -06:00
ini: <Code className="w-6 h-6 text-muted-foreground" />,
2025-10-01 15:40:10 -05:00
};
return iconMap[ext] || <Code className="w-6 h-6 text-yellow-500" />;
}
function getFileType(filename: string): {
type: string;
icon: React.ReactNode;
color: string;
} {
+5
2026-08-19 14:12:06 -05:00
const kind = resolveFilePreviewKind(filename);
2025-10-01 15:40:10 -05:00
+5
2026-08-19 14:12:06 -05:00
if (kind === "image") {
2025-10-01 15:40:10 -05:00
return {
type: "image",
icon: <ImageIcon className="w-6 h-6" />,
color: "text-green-500",
};
+5
2026-08-19 14:12:06 -05:00
} else if (kind === "video") {
2025-10-01 15:40:10 -05:00
return {
type: "video",
icon: <Film className="w-6 h-6" />,
color: "text-purple-500",
};
+5
2026-08-19 14:12:06 -05:00
} else if (kind === "audio") {
2025-10-01 15:40:10 -05:00
return {
type: "audio",
icon: <Music className="w-6 h-6" />,
color: "text-pink-500",
};
+5
2026-08-19 14:12:06 -05:00
} else if (kind === "markdown") {
2025-10-01 15:40:10 -05:00
return {
type: "markdown",
icon: <FileText className="w-6 h-6" />,
color: "text-blue-600",
};
+5
2026-08-19 14:12:06 -05:00
} else if (kind === "pdf") {
2025-10-01 15:40:10 -05:00
return {
type: "pdf",
icon: <FileText className="w-6 h-6" />,
color: "text-red-600",
};
+5
2026-08-19 14:12:06 -05:00
} else if (kind === "text") {
2025-10-01 15:40:10 -05:00
return {
type: "text",
icon: <FileText className="w-6 h-6" />,
color: "text-blue-500",
};
+5
2026-08-19 14:12:06 -05:00
} else if (kind === "code") {
2025-10-01 15:40:10 -05:00
return {
type: "code",
icon: getLanguageIcon(filename),
color: "text-yellow-500",
};
} else {
return {
type: "unknown",
icon: <FileIcon className="w-6 h-6" />,
+1
2025-12-31 22:20:12 -06:00
color: "text-foreground-subtle",
2025-10-01 15:40:10 -05:00
};
}
}
+7
2025-11-05 10:36:16 -06:00
function formatFileSize(bytes?: number, t?: (key: string) => string): string {
2025-10-01 15:40:10 -05:00
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]}`;
}
2026-05-06 15:12:07 -05:00
function PreviewFallback({ label }: { label: string }) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
<p className="text-sm text-muted-foreground">{label}</p>
</div>
</div>
);
}
2025-10-01 15:40:10 -05:00
export function FileViewer({
file,
content = "",
savedContent = "",
isLoading = false,
+8
2026-07-19 12:29:52 -05:00
isSaving = false,
2025-10-01 15:40:10 -05:00
isEditable = false,
2026-06-21 15:55:41 -05:00
resetKey,
2025-10-01 15:40:10 -05:00
onContentChange,
onSave,
onRevert,
onDownload,
2026-06-29 13:28:26 -05:00
onOpenExternal,
onChooseExternalEditor,
2025-10-01 15:40:10 -05:00
onMediaDimensionsChange,
}: FileViewerProps) {
const { t } = useTranslation();
const [editedContent, setEditedContent] = useState(content);
+7
2025-11-05 10:36:16 -06:00
const [, setOriginalContent] = useState(savedContent || content);
2025-10-01 15:40:10 -05:00
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);
2026-06-21 15:55:41 -05:00
const [editorFontSize, setEditorFontSize] = useState<number>(() => {
const stored = localStorage.getItem("fileManagerEditorFontSize");
return stored ? parseInt(stored, 10) : 14;
});
2026-05-06 15:12:07 -05:00
const editorRef = useRef<CodeEditorHandle | null>(null);
2025-10-01 15:40:10 -05:00
2026-06-21 15:55:41 -05:00
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;
});
};
2025-10-01 15:40:10 -05:00
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);
}
2026-06-21 15:55:41 -05:00
}, [file.name, file.path, resetKey]);
useEffect(() => {
2025-10-01 15:40:10 -05:00
setHasChanges(content !== savedContent);
if (fileTypeInfo.type === "unknown" && isLargeFile && !forceShowAsText) {
setShowLargeFileWarning(true);
} else {
setShowLargeFileWarning(false);
}
2026-06-21 15:55:41 -05:00
}, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText]);
2025-10-01 15:40:10 -05:00
const handleContentChange = (newContent: string) => {
setEditedContent(newContent);
setHasChanges(newContent !== savedContent);
onContentChange?.(newContent);
};
const handleSave = () => {
+8
2026-07-19 12:29:52 -05:00
if (isSaving) return;
2025-10-01 15:40:10 -05:00
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 (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
+1
2025-12-31 22:20:12 -06:00
<p className="text-sm text-muted-foreground">Loading file...</p>
2025-10-01 15:40:10 -05:00
</div>
</div>
);
}
return (
<div className="h-full flex flex-col bg-background">
<div className="flex-shrink-0 bg-card border-b border-border p-4">
+8
2026-07-19 12:29:52 -05:00
<div className="flex flex-wrap items-center justify-between gap-3">
2025-10-01 15:40:10 -05:00
<div className="flex items-center gap-3">
<div className={cn("p-2 rounded-lg bg-muted", fileTypeInfo.color)}>
{fileTypeInfo.icon}
</div>
<div>
<h3 className="font-medium text-foreground">{file.name}</h3>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>{formatFileSize(file.size, t)}</span>
{file.modified && (
<span>
{t("fileManager.modified")}: {file.modified}
</span>
)}
<span
className={cn(
"px-2 py-1 rounded-full text-xs",
fileTypeInfo.color,
"bg-muted",
)}
>
{fileTypeInfo.type.toUpperCase()}
</span>
</div>
</div>
</div>
+8
2026-07-19 12:29:52 -05:00
<div className="ml-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-2">
2025-10-01 15:40:10 -05:00
{isEditable && (
<Button
variant="ghost"
size="sm"
2026-05-06 15:12:07 -05:00
onClick={() => editorRef.current?.openSearchPanel()}
2025-10-01 15:40:10 -05:00
className="flex items-center gap-2"
title={t("fileManager.searchInFile")}
>
<Search className="w-4 h-4" />
</Button>
)}
2026-06-21 15:55:41 -05:00
{isEditable && (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={decreaseFontSize}
disabled={editorFontSize <= MIN_FONT_SIZE}
title={t("fileManager.decreaseFontSize")}
className="w-7 h-7 p-0"
>
<ZoomOut className="w-4 h-4" />
</Button>
<span className="text-xs text-muted-foreground w-8 text-center select-none">
{editorFontSize}px
</span>
<Button
variant="ghost"
size="sm"
onClick={increaseFontSize}
disabled={editorFontSize >= MAX_FONT_SIZE}
title={t("fileManager.increaseFontSize")}
className="w-7 h-7 p-0"
>
<ZoomIn className="w-4 h-4" />
</Button>
</div>
)}
2025-10-01 15:40:10 -05:00
{isEditable && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowKeyboardShortcuts(!showKeyboardShortcuts)}
className="flex items-center gap-2"
title={t("fileManager.showKeyboardShortcuts")}
>
<Keyboard className="w-4 h-4" />
</Button>
)}
{hasChanges && (
<>
<Button
variant="outline"
size="sm"
onClick={handleRevert}
+8
2026-07-19 12:29:52 -05:00
disabled={isSaving}
2025-10-01 15:40:10 -05:00
className="flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
Revert
</Button>
<Button
variant="default"
size="sm"
onClick={handleSave}
+8
2026-07-19 12:29:52 -05:00
disabled={isSaving}
2025-10-01 15:40:10 -05:00
className="flex items-center gap-2"
>
<Save className="w-4 h-4" />
Save
</Button>
</>
)}
{onDownload && (
<Button
variant="outline"
size="sm"
onClick={onDownload}
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
{t("fileManager.download")}
</Button>
)}
2026-06-29 13:28:26 -05:00
{isEditable && onOpenExternal && (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={onOpenExternal}
className="flex items-center gap-2"
>
<ExternalLink className="w-4 h-4" />
{t("fileManager.openExternalEditor")}
</Button>
{onChooseExternalEditor && (
<Button
variant="ghost"
size="sm"
onClick={onChooseExternalEditor}
className="h-8 w-8 p-0"
title={t("fileManager.chooseExternalEditor")}
>
<Settings className="w-4 h-4" />
</Button>
)}
</div>
)}
2025-10-01 15:40:10 -05:00
</div>
</div>
</div>
{showKeyboardShortcuts && isEditable && (
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold">
{t("fileManager.keyboardShortcuts")}
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setShowKeyboardShortcuts(false)}
className="h-6 w-6 p-0"
>
×
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div className="space-y-2">
<h4 className="font-medium text-muted-foreground">
{t("fileManager.searchAndReplace")}
</h4>
<div className="space-y-1">
<div className="flex justify-between">
<span>{t("fileManager.search")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+F
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.replace")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+H
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.findNext")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
F3
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.findPrevious")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Shift+F3
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium text-muted-foreground">
{t("fileManager.editing")}
</h4>
<div className="space-y-1">
<div className="flex justify-between">
<span>{t("fileManager.save")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+S
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.selectAll")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+A
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.undo")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+Z
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.redo")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+Y
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.toggleComment")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+/
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.autoComplete")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Ctrl+Space
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.moveLineUp")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Alt+↑
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
<div className="flex justify-between">
<span>{t("fileManager.moveLineDown")}</span>
2026-05-06 15:12:07 -05:00
<Kbd className="px-2 py-1 bg-background rounded text-xs">
<KbdKey className="px-2 py-1 bg-background rounded text-xs">
Alt+↓
</KbdKey>
</Kbd>
2025-10-01 15:40:10 -05:00
</div>
</div>
</div>
</div>
</div>
)}
<div className="flex-1 overflow-hidden">
{showLargeFileWarning && (
<div className="h-full flex items-center justify-center bg-background">
<div className="bg-card border border-destructive/30 rounded-lg p-6 max-w-md mx-4 shadow-lg">
<div className="flex items-start gap-3 mb-4">
<AlertCircle className="w-6 h-6 text-destructive flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-medium text-foreground mb-2">
{t("fileManager.largeFileWarning")}
</h3>
<p className="text-sm text-muted-foreground mb-3">
{t("fileManager.largeFileWarningDesc", {
size: formatFileSize(file.size, t),
})}
</p>
{isTooLarge ? (
<div className="bg-destructive/10 border border-destructive/30 rounded p-3 mb-4">
<p className="text-sm text-destructive font-medium">
File is too large (&gt; 10MB) and cannot be opened as
text for security reasons.
</p>
</div>
) : (
<p className="text-sm text-muted-foreground mb-4">
Do you want to continue opening this file as text? This
may slow down your browser.
</p>
)}
</div>
</div>
<div className="flex gap-3">
{!isTooLarge && (
<Button
variant="default"
size="sm"
onClick={handleConfirmOpenAsText}
className="flex items-center gap-2"
>
<FileText className="w-4 h-4" />
Open as Text
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={onDownload}
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
{t("fileManager.downloadInstead")}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleCancelOpenAsText}
>
Cancel
</Button>
</div>
</div>
</div>
)}
{fileTypeInfo.type === "image" && !showLargeFileWarning && (
2026-05-06 15:12:07 -05:00
<Suspense fallback={<PreviewFallback label="Loading image..." />}>
<ImagePreview
content={content}
fileName={file.name}
onDownload={onDownload}
onMediaDimensionsChange={onMediaDimensionsChange}
/>
</Suspense>
2025-10-01 15:40:10 -05:00
)}
{shouldShowAsText && !showLargeFileWarning && (
<div className="h-full flex flex-col">
{isEditable ? (
2026-05-06 15:12:07 -05:00
<Suspense
fallback={<PreviewFallback label="Loading editor..." />}
>
<CodeEditor
ref={editorRef}
fileName={file.name}
value={editedContent}
onChange={handleContentChange}
onFocus={() => setEditorFocused(true)}
onBlur={() => setEditorFocused(false)}
placeholder={t("fileManager.startTyping")}
2026-06-21 15:55:41 -05:00
fontSize={editorFontSize}
2026-05-06 15:12:07 -05:00
/>
</Suspense>
2025-10-01 15:40:10 -05:00
) : (
+1
2025-12-31 22:20:12 -06:00
<div className="h-full p-4 font-mono text-sm whitespace-pre-wrap overflow-auto thin-scrollbar bg-background text-foreground">
2025-10-01 15:40:10 -05:00
{editedContent || content || t("fileManager.fileIsEmpty")}
</div>
)}
</div>
)}
{fileTypeInfo.type === "video" && !showLargeFileWarning && (
<div className="p-6 flex items-center justify-center h-full">
<div className="w-full max-w-4xl">
{(() => {
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 (
<div className="relative">
<video
controls
className="w-full rounded-lg shadow-sm"
style={{
maxHeight: "calc(100vh - 200px)",
backgroundColor: "#000",
}}
preload="metadata"
onError={(e) => {
console.error(
"Video playback error:",
e.currentTarget.error,
);
}}
onLoadedMetadata={(e) => {
const video = e.currentTarget;
if (
onMediaDimensionsChange &&
video.videoWidth &&
video.videoHeight
) {
onMediaDimensionsChange({
width: video.videoWidth,
height: video.videoHeight,
});
}
}}
>
<source src={videoUrl} type={mimeType} />
<div className="text-center text-muted-foreground p-4">
<AlertCircle className="w-8 h-8 mx-auto mb-2" />
<p>
Your browser does not support video playback for this
format.
</p>
{onDownload && (
<Button
variant="outline"
onClick={onDownload}
className="mt-2 flex items-center gap-2 mx-auto"
>
<Download className="w-4 h-4" />
Download to play externally
</Button>
)}
</div>
</video>
</div>
);
})()}
</div>
</div>
)}
{fileTypeInfo.type === "markdown" && !showLargeFileWarning && (
<div className="h-full flex flex-col">
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button
variant={markdownEditMode ? "default" : "outline"}
size="sm"
onClick={() => setMarkdownEditMode(true)}
>
<Edit className="w-4 h-4 mr-1" />
{t("fileManager.edit")}
</Button>
<Button
variant={!markdownEditMode ? "default" : "outline"}
size="sm"
onClick={() => setMarkdownEditMode(false)}
>
<Eye className="w-4 h-4 mr-1" />
{t("fileManager.preview")}
</Button>
</div>
<div className="flex items-center gap-2"></div>
</div>
</div>
<div className="flex-1 flex overflow-hidden">
{markdownEditMode ? (
<>
<div className="flex-1 border-r border-border">
<div className="h-full p-4 bg-background">
<textarea
value={editedContent}
onChange={(e) => {
setEditedContent(e.target.value);
onContentChange?.(e.target.value);
}}
className="w-full h-full resize-none border-0 bg-transparent text-foreground font-mono text-sm leading-relaxed focus:outline-none focus:ring-0"
placeholder={t("fileManager.startWritingMarkdown")}
/>
</div>
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="flex-1 overflow-auto thin-scrollbar bg-muted/10">
2025-10-01 15:40:10 -05:00
<div className="p-4">
2026-05-06 15:12:07 -05:00
<Suspense
fallback={
<PreviewFallback label="Loading preview..." />
}
2025-10-01 15:40:10 -05:00
>
2026-05-06 15:12:07 -05:00
<MarkdownRenderer
compact
content={editedContent || "Nothing to preview yet..."}
/>
</Suspense>
2025-10-01 15:40:10 -05:00
</div>
</div>
</>
) : (
+1
2025-12-31 22:20:12 -06:00
<div className="flex-1 overflow-auto thin-scrollbar p-6">
2025-10-01 15:40:10 -05:00
<div className="max-w-4xl mx-auto">
2026-05-06 15:12:07 -05:00
<Suspense
fallback={<PreviewFallback label="Loading preview..." />}
2025-10-01 15:40:10 -05:00
>
2026-05-06 15:12:07 -05:00
<MarkdownRenderer content={editedContent} />
</Suspense>
2025-10-01 15:40:10 -05:00
</div>
</div>
)}
</div>
</div>
)}
{fileTypeInfo.type === "pdf" && !showLargeFileWarning && (
2026-05-06 15:12:07 -05:00
<Suspense
fallback={<PreviewFallback label="Loading PDF viewer..." />}
>
<PdfPreview
content={content}
onDownload={onDownload}
onMediaDimensionsChange={onMediaDimensionsChange}
/>
</Suspense>
2025-10-01 15:40:10 -05:00
)}
{fileTypeInfo.type === "audio" && !showLargeFileWarning && (
2026-05-06 15:12:07 -05:00
<Suspense
fallback={<PreviewFallback label="Loading audio player..." />}
>
<AudioPreview
file={file}
content={content}
color={fileTypeInfo.color}
onMediaDimensionsChange={onMediaDimensionsChange}
/>
</Suspense>
2025-10-01 15:40:10 -05:00
)}
{fileTypeInfo.type === "unknown" &&
!shouldShowAsText &&
!showLargeFileWarning && (
<div className="h-full flex items-center justify-center">
<div className="text-center text-muted-foreground">
<AlertCircle className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-medium mb-2">
Cannot preview this file type
</h3>
<p className="text-sm mb-4">
This file type is not supported for preview. You can download
it to view in an external application.
</p>
{onDownload && (
<Button
variant="outline"
onClick={onDownload}
className="flex items-center gap-2 mx-auto"
>
<Download className="w-4 h-4" />
{t("fileManager.downloadFile")}
</Button>
)}
</div>
</div>
)}
</div>
<div className="flex-shrink-0 bg-muted/50 border-t border-border px-4 py-2 text-xs text-muted-foreground">
<div className="flex justify-between items-center">
<span>{file.path}</span>
{hasChanges && (
<span className="text-orange-600 font-medium">
● Unsaved changes
</span>
)}
</div>
</div>
</div>
);
}