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";
|
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";
|
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
|
|
|
|
|
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;
|
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" />,
|
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" />,
|
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" />,
|
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" />,
|
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" />,
|
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" />,
|
2025-12-31 22:20:12 -06:00
|
|
|
|
md: <SiMarkdown className="w-6 h-6 text-muted-foreground" />,
|
2026-03-08 18:02:14 -05:00
|
|
|
|
conf: <Code className="w-6 h-6 text-muted-foreground" />,
|
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;
|
|
|
|
|
|
} {
|
2026-08-19 14:12:06 -05:00
|
|
|
|
const kind = resolveFilePreviewKind(filename);
|
2025-10-01 15:40:10 -05:00
|
|
|
|
|
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",
|
|
|
|
|
|
};
|
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",
|
|
|
|
|
|
};
|
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",
|
|
|
|
|
|
};
|
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",
|
|
|
|
|
|
};
|
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",
|
|
|
|
|
|
};
|
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",
|
|
|
|
|
|
};
|
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" />,
|
2025-12-31 22:20:12 -06:00
|
|
|
|
color: "text-foreground-subtle",
|
2025-10-01 15:40:10 -05:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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,
|
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);
|
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 = () => {
|
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>
|
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">
|
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>
|
|
|
|
|
|
|
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}
|
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}
|
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 (> 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
|
|
|
|
) : (
|
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>
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|