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 <[email protected]>
This commit is contained in:
ZacharyZcRandClaude committed 2025-09-25 11:26:38 +08:00
1 parent 71010333cd
commit 3766756cd7
6 files changed
+2476 -4

No files matched your search

+1972 -3
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -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
View File
@@ -0,0 +1,29 @@
/**
* @licstart The following is the entire license notice for the
* JavaScript code in this page
*
* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @licend The above is the entire license notice for the
* JavaScript code in this page
*/
/**
* pdfjsVersion = 5.3.93
* pdfjsBuild = cbeef3233
*/
const e=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),t=[.001,0,0,.001,0,0],a=1.35,r=.35,i=.25925925925925924,n=1,s=2,o=4,c=8,l=16,h=64,u=128,d=256,f="pdfjs_internal_editor_",g=3,p=9,m=13,b=15,y=101,w={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},x=0,S=4,k=1,C=2,v=3,F=1,T=2,O=3,M=4,D=5,R=6,N=7,E=8,L=9,j=10,_=11,U=12,X=13,q=14,H=15,W=16,z=17,$=20,G="Group",V="R",K=1,J=2,Y=4,Z=16,Q=32,ee=128,te=512,ae=1,re=2,ie=4096,ne=8192,se=32768,oe=65536,ce=131072,le=1048576,he=2097152,ue=8388608,de=16777216,fe=1,ge=2,pe=3,me=4,be=5,ye={E:"Mouse Enter",X:"Mouse Exit",D:"Mouse Down",U:"Mouse Up",Fo:"Focus",Bl:"Blur",PO:"PageOpen",PC:"PageClose",PV:"PageVisible",PI:"PageInvisible",K:"Keystroke",F:"Format",V:"Validate",C:"Calculate"},we={WC:"WillClose",WS:"WillSave",DS:"DidSave",WP:"WillPrint",DP:"DidPrint"},xe={O:"PageOpen",C:"PageClose"},Se=1,ke=5,Ae=1,Ce=2,ve=3,Fe=4,Ie=5,Te=6,Oe=7,Me=8,De=9,Be=10,Re=11,Ne=12,Ee=13,Pe=14,Le=15,je=16,_e=17,Ue=18,Xe=19,qe=20,He=21,We=22,ze=23,$e=24,Ge=25,Ve=26,Ke=27,Je=28,Ye=29,Ze=30,Qe=31,et=32,tt=33,at=34,rt=35,it=36,nt=37,st=38,ot=39,ct=40,lt=41,ht=42,ut=43,dt=44,ft=45,gt=46,pt=47,mt=48,bt=49,yt=50,wt=51,xt=52,St=53,kt=54,At=55,Ct=56,vt=57,Ft=58,It=59,Tt=60,Ot=61,Mt=62,Dt=63,Bt=64,Rt=65,Nt=66,Et=67,Pt=68,Lt=69,jt=70,_t=71,Ut=72,Xt=73,qt=74,Ht=75,Wt=76,zt=77,$t=80,Gt=81,Vt=83,Kt=84,Jt=85,Yt=86,Zt=87,Qt=88,ea=89,ta=90,aa=91,ra=92,ia=93,na=94,sa=0,oa=1,ca=2,la=3,ha=1,ua=2;let da=Se;function getVerbosityLevel(){return da}function info(e){da>=ke&&console.log(`Info: ${e}`)}function warn(e){da>=Se&&console.log(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,a=null){if(!e)return null;if(a&&"string"==typeof e){if(a.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(a.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const r=t?URL.parse(e,t):URL.parse(e);return function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(r)?r:null}function shadow(e,t,a,r=!1){Object.defineProperty(e,t,{value:a,enumerable:!r,configurable:!0,writable:!1});return a}const fa=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends fa{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends fa{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends fa{constructor(e){super(e,"InvalidPDFException")}}class ResponseException extends fa{constructor(e,t,a){super(e,"ResponseException");this.status=t;this.missing=a}}class FormatError extends fa{constructor(e){super(e,"FormatError")}}class AbortException extends fa{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,a=8192;if(t<a)return String.fromCharCode.apply(null,e);const r=[];for(let i=0;i<t;i+=a){const n=Math.min(i+a,t),s=e.subarray(i,n);r.push(String.fromCharCode.apply(null,s))}return r.join("")}function stringToBytes(e){"string"!=typeof e&&unreachable("Invalid argument for stringToBytes");const t=e.length,a=new Uint8Array(t);for(let r=0;r<t;++r)a[r]=255&e.charCodeAt(r);return a}function string32(e){return String.fromCharCode(e>>24&255,e>>16&255,e>>8&255,255&e)}function objectSize(e){return Object.keys(e).length}class FeatureTest{static get isLittleEndian(){return shadow(this,"isLittleEndian",function isLittleEndian(){const e=new Uint8Array(4);e[0]=1;return 1===new Uint32Array(e.buffer,0,1)[0]}())}static get isEvalSupported(){return shadow(this,"isEvalSupported",function isEvalSupported(){try{new Function("");return!0}catch{return!1}}())}static get isOffscreenCanvasSupported(){return shadow(this,"isOffscreenCanvasSupported","undefined"!=typeof OffscreenCanvas)}static get isImageDecoderSupported(){return shadow(this,"isImageDecoderSupported","undefined"!=typeof ImageDecoder)}static get platform(){const{platform:e,userAgent:t}=navigator;return shadow(this,"platform",{isAndroid:t.includes("Android"),isLinux:e.includes("Linux"),isMac:e.includes("Mac"),isWindows:e.includes("Win"),isFirefox:t.includes("Firefox")})}static get isCSSRoundSupported(){return shadow(this,"isCSSRoundSupported",globalThis.CSS?.supports?.("width: round(1.5px, 1px)"))}}const ga=Array.from(Array(256).keys(),(e=>e.toString(16).padStart(2,"0")));class Util{static makeHexColor(e,t,a){return`#${ga[e]}${ga[t]}${ga[a]}`}static scaleMinMax(e,t){let a;if(e[0]){if(e[0]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){a=t[1];Line truncated
/*webpackIgnore: true*/
/*@vite-ignore*/
t)).default()}catch(e){warn(`JpxImage#getJsModule: ${e}`)}e(a)}static async#U(e,t,a){const r="openjpeg.wasm";try{this.#E||(this.#j?this.#E=await fetchBinaryData(`${this.#F}${r}`):this.#E=await this.#P.sendWithPromise("FetchBinaryData",{type:"wasmFactory",filename:r}));return a((await WebAssembly.instantiate(this.#E,t)).instance)}catch(t){warn(`JpxImage#instantiateWasm: ${t}`);this.#_(e);return null}finally{this.#P=null}}static async decode(e,{numComponents:t=4,isIndexedColormap:a=!1,smaskInData:r=!1,reducePower:i=0}={}){if(!this.#L){const{promise:e,resolve:t}=Promise.withResolvers(),a=[e];this.#v?a.push(Ga({warn,instantiateWasm:this.#U.bind(this,t)})):this.#_(t);this.#L=Promise.race(a)}const n=await this.#L;if(!n)throw new JpxError("OpenJPEG failed to initialize");let s;try{const o=e.length;s=n._malloc(o);n.writeArrayToMemory(e,s);if(n._jp2_decode(s,o,t>0?t:0,!!a,!!r,i)){const{errorMessages:e}=n;if(e){delete n.errorMessages;throw new JpxError(e)}throw new JpxError("Unknown error")}const{imageData:c}=n;n.imageData=null;return c}finally{s&&n._free(s)}}static cleanup(){this.#L=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const a=t;t=e.getByte();if(65361===(a<<8|t)){e.skip(4);const t=e.getInt32()>>>0,a=e.getInt32()>>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0;e.skip(16);return{width:t-r,height:a-i,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}function addState(e,t,a,r,i){let n=e;for(let e=0,a=t.length-1;e<a;e++){const a=t[e];n=n[a]||=[]}n[t.at(-1)]={checkFn:a,iterateFn:r,processFn:i}}const Va=[];addState(Va,[Be,Ne,Yt,Re],null,(function iterateInlineImageGroup(e,t){const a=e.fnArray,r=(t-(e.iCurr-3))%4;switch(r){case 0:return a[t]===Be;case 1:return a[t]===Ne;case 2:return a[t]===Yt;case 3:return a[t]===Re}throw new Error(`iterateInlineImageGroup - invalid pos: ${r}`)}),(function foundInlineImageGroup(e,t){const a=e.fnArray,r=e.argsArray,i=e.iCurr,n=i-3,s=i-2,o=i-1,c=Math.min(Math.floor((t-n)/4),200);if(c<10)return t-(t-n)%4;let l=0;const h=[];let u=0,d=1,f=1;for(let e=0;e<c;e++){const t=r[s+(e<<2)],a=r[o+(e<<2)][0];if(d+a.width>1e3){l=Math.max(l,d);f+=u+2;d=0;u=0}h.push({transform:t,x:d,y:f,w:a.width,h:a.height});d+=a.width+2;u=Math.max(u,a.height)}const g=Math.max(l,d)+1,p=f+u+1,m=new Uint8Array(g*p*4),b=g<<2;for(let e=0;e<c;e++){const t=r[o+(e<<2)][0].data,a=h[e].w<<2;let i=0,n=h[e].x+h[e].y*g<<2;m.set(t.subarray(0,a),n-b);for(let r=0,s=h[e].h;r<s;r++){m.set(t.subarray(i,i+a),n);i+=a;n+=b}m.set(t.subarray(i-a,i),n);for(;n>=0;){t[n-4]=t[n];t[n-3]=t[n+1];t[n-2]=t[n+2];t[n-1]=t[n+3];t[n+a]=t[n+a-4];t[n+a+1]=t[n+a-3];t[n+a+2]=t[n+a-2];t[n+a+3]=t[n+a-1];n-=b}}const y={width:g,height:p};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(g,p);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(m.buffer),g,p),0,0);y.bitmap=e.transferToImageBitmap();y.data=null}else{y.kind=v;y.data=m}a.splice(n,4*c,Zt);r.splice(n,4*c,[y,h]);return n+1}));addState(Va,[Be,Ne,Vt,Re],null,(function iterateImageMaskGroup(e,t){const a=e.fnArray,r=(t-(e.iCurr-3))%4;switch(r){case 0:return a[t]===Be;case 1:return a[t]===Ne;case 2:return a[t]===Vt;case 3:return a[t]===Re}throw new Error(`iterateImageMaskGroup - invalid pos: ${r}`)}),(function foundImageMaskGroup(e,t){const a=e.fnArray,r=e.argsArray,i=e.iCurr,n=i-3,s=i-2,o=i-1;let c=Math.floor((t-n)/4);if(c<10)return t-(t-n)%4;let l,h,u=!1;const d=r[o][0],f=r[s][0],g=r[s][1],p=r[s][2],m=r[s][3];if(g===p){u=!0;l=s+4;let e=o+4;for(let t=1;t<c;t++,l+=4,e+=4){h=r[l];if(r[e][0]!==d||h[0]!==f||h[1]!==g||h[2]!==p||h[3]!==m){t<10?u=!1:c=t;break}}}if(u){c=Math.min(c,1e3);const e=new Float32Array(2*c);l=s;for(let t=0;t<c;t++,l+=4){h=r[l];e[t<<1]=h[4];e[1+(t<<1)]=h[5]}a.splice(n,4*c,ea);r.splice(n,4*c,[d,f,g,p,m,e])}else{c=Math.min(c,100);const e=[];for(let t=0;t<c;t++){h=r[s+(t<<2)];const a=r[o+(t<<2)][0];e.push({data:a.data,width:a.width,height:a.height,interpolate:a.interpolate,count:a.count,transform:h})}a.splice(n,4*c,Kt);r.splice(n,4*c,[e])}return n+1}));addState(Va,[Be,Ne,Jt,Re],(function(e){const t=e.argsArray,a=e.iCurr-2;return 0===t[a][1]&&0===t[a][2]}),(function iterateImageGroup(e,t){const a=e.fnArray,r=e.argsArray,i=(t-(e.iCurr-3))%4;switch(i){case 0:return a[t]===Be;case 1:if(a[t]!==Ne)return!1;const i=e.iCurr-2,n=r[i][0],s=r[i][3];return r[t][0]===n&&0===r[t][1]&&0===r[t][2]&&r[t][3]===s;case 2:if(a[t]!==Jt)return!1;const o=r[e.iCurr-1][0];return r[t][0]===o;case 3:return a[t]===Re}throw new Error(`iterateImageGroup - invalid pos: ${i}`)}),(function(e,t){const a=e.fnArray,r=e.argsArray,i=e.iCurr,n=i-3,s=i-2,o=r[i-1][0],c=r[s][0],l=r[s][3],h=Math.min(Math.floor((t-n)/4),1e3);if(h<3)return t-(t-n)%4;const u=new Float32Array(2*h);let d=s;for(let e=0;e<h;e++,d+=4){const t=r[d];u[e<<1]=t[4];u[1+(e<<1)]=t[5]}const f=[o,c,l,u];a.splice(n,4*h,Qt);r.splice(n,4*h,f);return n+1}));addState(Va,[Qe,nt,ht,dt,et],null,(function iterateShowTextGroup(e,t){const a=e.fnArray,r=e.argsArray,i=(t-(e.iCurr-Line truncated
+7
View File
@@ -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",
+7
View File
@@ -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">