FEATURE: Comprehensive multimedia file handling with professional components
- Integrated react-markdown with GitHub Flavored Markdown support - Added react-pdf for PDF viewing with full navigation controls - Implemented react-syntax-highlighter for code syntax highlighting - Added dual-pane Markdown editor with live preview capability - Fixed PDF.js worker configuration with local fallback - Enhanced internationalization support for all multimedia controls - Removed unsupported download buttons from Markdown editor - Resolved version compatibility issues between PDF API and worker 技术改进 Claude Code生成 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1975
package-lock.json
generated
1975
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,7 @@
|
||||
"nanoid": "^5.1.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pdfjs-dist": "^5.4.149",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@@ -94,11 +95,15 @@
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-pdf": "^10.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
"react-player": "^3.3.3",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"react-simple-keyboard": "^3.8.120",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ssh2": "^1.16.0",
|
||||
|
||||
29
public/pdf.worker.min.js
vendored
Normal file
29
public/pdf.worker.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -668,6 +668,13 @@
|
||||
"connectToSsh": "Connect to SSH to use file operations",
|
||||
"uploadFile": "Upload File",
|
||||
"downloadFile": "Download",
|
||||
"edit": "Edit",
|
||||
"preview": "Preview",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"pageXOfY": "Page {{current}} of {{total}}",
|
||||
"zoomOut": "Zoom Out",
|
||||
"zoomIn": "Zoom In",
|
||||
"newFile": "New File",
|
||||
"newFolder": "New Folder",
|
||||
"rename": "Rename",
|
||||
|
||||
@@ -691,6 +691,13 @@
|
||||
"connectToSsh": "连接 SSH 以使用文件操作",
|
||||
"uploadFile": "上传文件",
|
||||
"downloadFile": "下载",
|
||||
"edit": "编辑",
|
||||
"preview": "预览",
|
||||
"previous": "上一页",
|
||||
"next": "下一页",
|
||||
"pageXOfY": "第 {{current}} 页,共 {{total}} 页",
|
||||
"zoomOut": "缩小",
|
||||
"zoomIn": "放大",
|
||||
"newFile": "新建文件",
|
||||
"newFolder": "新建文件夹",
|
||||
"rename": "重命名",
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Code,
|
||||
AlertCircle,
|
||||
Download,
|
||||
Eye,
|
||||
Edit,
|
||||
Save,
|
||||
RotateCcw,
|
||||
Keyboard,
|
||||
@@ -55,6 +57,14 @@ import "react-photo-view/dist/react-photo-view.css";
|
||||
import ReactPlayer from "react-player";
|
||||
import AudioPlayer from "react-h5-audio-player";
|
||||
import "react-h5-audio-player/lib/styles.css";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark as syntaxTheme } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
|
||||
// Use local PDF.js worker to avoid CDN issues
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
|
||||
|
||||
interface FileItem {
|
||||
name: string;
|
||||
@@ -142,6 +152,8 @@ function getFileType(filename: string): {
|
||||
const videoExts = ["mp4", "avi", "mkv", "mov", "wmv", "flv", "webm"];
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "aac", "m4a"];
|
||||
const textExts = ["txt", "readme"];
|
||||
const markdownExts = ["md", "markdown", "mdown", "mkdn", "mdx"];
|
||||
const pdfExts = ["pdf"];
|
||||
const codeExts = [
|
||||
"js",
|
||||
"ts",
|
||||
@@ -173,7 +185,6 @@ function getFileType(filename: string): {
|
||||
"sql",
|
||||
"vue",
|
||||
"svelte",
|
||||
"md",
|
||||
];
|
||||
|
||||
if (imageExts.includes(ext)) {
|
||||
@@ -194,6 +205,18 @@ function getFileType(filename: string): {
|
||||
icon: <Music className="w-6 h-6" />,
|
||||
color: "text-pink-500",
|
||||
};
|
||||
} else if (markdownExts.includes(ext)) {
|
||||
return {
|
||||
type: "markdown",
|
||||
icon: <FileText className="w-6 h-6" />,
|
||||
color: "text-blue-600",
|
||||
};
|
||||
} else if (pdfExts.includes(ext)) {
|
||||
return {
|
||||
type: "pdf",
|
||||
icon: <FileText className="w-6 h-6" />,
|
||||
color: "text-red-600",
|
||||
};
|
||||
} else if (textExts.includes(ext)) {
|
||||
return {
|
||||
type: "text",
|
||||
@@ -295,6 +318,11 @@ export function FileViewer({
|
||||
const [editorFocused, setEditorFocused] = useState(false);
|
||||
const [imageLoadError, setImageLoadError] = useState(false);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
const [numPages, setNumPages] = useState<number | null>(null);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [pdfScale, setPdfScale] = useState(1.2);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
const [markdownEditMode, setMarkdownEditMode] = useState(false);
|
||||
|
||||
const fileTypeInfo = getFileType(file.name);
|
||||
|
||||
@@ -853,6 +881,433 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Markdown file editor with live preview */}
|
||||
{fileTypeInfo.type === "markdown" && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Markdown toolbar */}
|
||||
<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">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onSave?.(editedContent)}
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{t("fileManager.save")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Markdown content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{markdownEditMode ? (
|
||||
<>
|
||||
{/* Editor pane */}
|
||||
<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="Start writing your markdown content..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview pane */}
|
||||
<div className="flex-1 overflow-auto bg-muted/10">
|
||||
<div className="p-4">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="rounded-lg"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-sm font-mono" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-2xl font-bold mb-4 mt-6 text-foreground border-b border-border pb-2">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-xl font-semibold mb-3 mt-5 text-foreground border-b border-border pb-1">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-lg font-semibold mb-2 mt-4 text-foreground">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-base font-semibold mb-2 mt-3 text-foreground">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="mb-3 text-foreground leading-relaxed">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="mb-3 ml-4 list-disc text-foreground">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="mb-3 ml-4 list-decimal text-foreground">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 text-foreground">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-blue-500 pl-3 mb-3 italic text-muted-foreground bg-muted/30 py-1">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="mb-3 overflow-x-auto">
|
||||
<table className="min-w-full border border-border rounded-lg text-sm">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-muted">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr className="border-b border-border">
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-3 py-2 text-left font-semibold text-foreground">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-3 py-2 text-foreground">
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{editedContent || "Nothing to preview yet..."}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Full preview mode */
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="rounded-lg"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-sm font-mono" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-3xl font-bold mb-6 mt-8 text-foreground border-b border-border pb-2">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-2xl font-semibold mb-4 mt-6 text-foreground border-b border-border pb-1">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xl font-semibold mb-3 mt-4 text-foreground">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-lg font-semibold mb-2 mt-3 text-foreground">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="mb-4 text-foreground leading-relaxed">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="mb-4 ml-6 list-disc text-foreground">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="mb-4 ml-6 list-decimal text-foreground">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 text-foreground">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-blue-500 pl-4 mb-4 italic text-muted-foreground bg-muted/30 py-2">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="mb-4 overflow-x-auto">
|
||||
<table className="min-w-full border border-border rounded-lg">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-muted">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr className="border-b border-border">
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-2 text-left font-semibold text-foreground">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-2 text-foreground">
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{editedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PDF file preview with react-pdf */}
|
||||
{fileTypeInfo.type === "pdf" && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
{/* PDF Controls */}
|
||||
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
|
||||
disabled={pageNumber <= 1}
|
||||
>
|
||||
{t("fileManager.previous")}
|
||||
</Button>
|
||||
<span className="text-sm text-foreground px-3 py-1 bg-background rounded border">
|
||||
{t("fileManager.pageXOfY", { current: pageNumber, total: numPages || 0 })}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPageNumber(Math.min(numPages || 1, pageNumber + 1))}
|
||||
disabled={!numPages || pageNumber >= numPages}
|
||||
>
|
||||
{t("fileManager.next")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPdfScale(Math.max(0.5, pdfScale - 0.2))}
|
||||
>
|
||||
{t("fileManager.zoomOut")}
|
||||
</Button>
|
||||
<span className="text-sm text-foreground px-3 py-1 bg-background rounded border min-w-[80px] text-center">
|
||||
{Math.round(pdfScale * 100)}%
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPdfScale(Math.min(3.0, pdfScale + 0.2))}
|
||||
>
|
||||
{t("fileManager.zoomIn")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{onDownload && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onDownload}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{t("fileManager.download")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PDF Content */}
|
||||
<div className="flex-1 overflow-auto p-6 bg-gray-100 dark:bg-gray-900">
|
||||
<div className="flex justify-center">
|
||||
{pdfError ? (
|
||||
<div className="text-center text-muted-foreground p-8">
|
||||
<AlertCircle className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" />
|
||||
<h3 className="text-lg font-medium mb-2">Cannot load PDF</h3>
|
||||
<p className="text-sm mb-4">
|
||||
There was an error loading this PDF file.
|
||||
</p>
|
||||
{onDownload && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onDownload}
|
||||
className="flex items-center gap-2 mx-auto"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{t("fileManager.download")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Document
|
||||
file={`data:application/pdf;base64,${content}`}
|
||||
onLoadSuccess={({ numPages }) => {
|
||||
setNumPages(numPages);
|
||||
setPdfError(false);
|
||||
|
||||
// Notify parent about PDF dimensions for window sizing
|
||||
if (onMediaDimensionsChange) {
|
||||
onMediaDimensionsChange({
|
||||
width: 800,
|
||||
height: 600
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLoadError={(error) => {
|
||||
console.error('PDF load error:', error);
|
||||
setPdfError(true);
|
||||
}}
|
||||
loading={
|
||||
<div className="text-center p-8">
|
||||
<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">Loading PDF...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Page
|
||||
pageNumber={pageNumber}
|
||||
scale={pdfScale}
|
||||
className="shadow-lg"
|
||||
loading={
|
||||
<div className="text-center p-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-xs text-muted-foreground">Loading page...</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Document>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio file preview with react-h5-audio-player */}
|
||||
{fileTypeInfo.type === "audio" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
|
||||
Reference in New Issue
Block a user