Fix PDF thumbnails, 3MF rendering, and add PDF infinite scroll
All checks were successful
Automated Container Build / build-and-push (push) Successful in 32s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 32s
This commit is contained in:
parent
9b0e60109f
commit
facb1c6f8b
5 changed files with 55 additions and 35 deletions
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
|||
"react": "^19.0.0",
|
||||
"react-19-stl-viewer": "^3.0.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-pdf": "^10.4.1",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"three": "^0.184.0",
|
||||
|
|
@ -3025,6 +3026,21 @@
|
|||
"react": "^19.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-intersection-observer": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.3.tgz",
|
||||
"integrity": "sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-pdf": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"react": "^19.0.0",
|
||||
"react-19-stl-viewer": "^3.0.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-pdf": "^10.4.1",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"three": "^0.184.0",
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext) || file.mime_type?.startsWith('image/');
|
||||
const isVideo = ['mp4', 'mkv', 'avi', 'mov', 'webm', 'ogg'].includes(ext) || file.mime_type?.startsWith('video/');
|
||||
const isOffice = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(ext) || file.mime_type?.includes('officedocument');
|
||||
const isOffice = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls', 'pdf'].includes(ext) || file.mime_type?.includes('officedocument') || file.mime_type === 'application/pdf';
|
||||
const isAudio = ['mp3', 'flac', 'wav', 'm4a', 'wma', 'aac'].includes(ext) || file.mime_type?.startsWith('audio/');
|
||||
const isMedia = isImage || isVideo || isOffice || isAudio;
|
||||
|
||||
|
|
@ -1240,7 +1240,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
})();
|
||||
|
||||
if (isMedia && file.checksum && viewMode === 'grid') {
|
||||
const docType = file.name.endsWith('.docx') ? 'docs' : file.name.endsWith('.pptx') ? 'slides' : (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) ? 'sheets' : null;
|
||||
const docType = (file.name.endsWith('.docx') || file.name.endsWith('.pdf')) ? 'docs' : file.name.endsWith('.pptx') ? 'slides' : (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) ? 'sheets' : null;
|
||||
return (
|
||||
<div className="relative w-full h-full flex items-center justify-center overflow-hidden rounded-md">
|
||||
<ThumbnailImage checksum={file.checksum} fallbackIcon={fallbackIcon} downloadToken={downloadToken || ''} docType={docType} />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
import { useState } from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut, Loader2 } from 'lucide-react';
|
||||
import { ZoomIn, ZoomOut, Loader2 } from 'lucide-react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
|
||||
|
|
@ -10,9 +11,31 @@ if (typeof window !== 'undefined') {
|
|||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
||||
}
|
||||
|
||||
function VirtualPage({ pageNumber, scale }: { pageNumber: number; scale: number }) {
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: false,
|
||||
rootMargin: '200% 0px', // load pages 2 screens ahead/behind
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} className="min-h-[800px] flex justify-center mb-6 w-full" style={{ minHeight: `${800 * scale}px` }}>
|
||||
{inView ? (
|
||||
<Page
|
||||
pageNumber={pageNumber}
|
||||
scale={scale}
|
||||
renderTextLayer={true}
|
||||
renderAnnotationLayer={true}
|
||||
className="bg-white shadow-xl"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-white shadow-xl w-[600px]" style={{ width: `${600 * scale}px` }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PdfViewer({ url }: { url: string }) {
|
||||
const [numPages, setNumPages] = useState<number>();
|
||||
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||
const [scale, setScale] = useState<number>(1.2);
|
||||
|
||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
||||
|
|
@ -24,25 +47,9 @@ export default function PdfViewer({ url }: { url: string }) {
|
|||
{/* PDF Toolbar */}
|
||||
<div className="flex items-center justify-between p-2 border-b" style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-elevated)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPageNumber(p => Math.max(1, p - 1))}
|
||||
disabled={pageNumber <= 1}
|
||||
className="p-1 rounded hover:bg-white/10 disabled:opacity-50"
|
||||
style={{ color: 'var(--color-text-primary)' }}
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm font-medium min-w-[80px] text-center" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{numPages ? `${pageNumber} / ${numPages}` : 'Loading...'}
|
||||
{numPages ? `${numPages} Pages` : 'Loading...'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPageNumber(p => (numPages ? Math.min(numPages, p + 1) : p))}
|
||||
disabled={pageNumber >= (numPages || 1)}
|
||||
className="p-1 rounded hover:bg-white/10 disabled:opacity-50"
|
||||
style={{ color: 'var(--color-text-primary)' }}
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
|
@ -76,18 +83,11 @@ export default function PdfViewer({ url }: { url: string }) {
|
|||
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-accent)' }} />
|
||||
</div>
|
||||
}
|
||||
className="flex flex-col items-center gap-6"
|
||||
className="flex flex-col items-center gap-2 w-full"
|
||||
>
|
||||
{numPages && (
|
||||
<Page
|
||||
key={`page_${pageNumber}`}
|
||||
pageNumber={pageNumber}
|
||||
scale={scale}
|
||||
renderTextLayer={true}
|
||||
renderAnnotationLayer={true}
|
||||
className="bg-white shadow-xl"
|
||||
/>
|
||||
)}
|
||||
{numPages && Array.from(new Array(numPages), (el, index) => (
|
||||
<VirtualPage key={`page_${index + 1}`} pageNumber={index + 1} scale={scale} />
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, Stage } from '@react-three/drei';
|
||||
import { OrbitControls, Center } from '@react-three/drei';
|
||||
import { ThreeMFLoader } from 'three-stdlib';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import * as THREE from 'three';
|
||||
|
|
@ -64,8 +64,11 @@ export default function ThreeMfPreview({ url }: ThreeMfPreviewProps) {
|
|||
|
||||
<div className={`w-full h-full transition-opacity duration-300 ${loading ? 'opacity-0' : 'opacity-100'}`}>
|
||||
<Canvas shadows dpr={[1, 2]} camera={{ position: [0, 0, 150], fov: 50 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 10]} intensity={1} castShadow />
|
||||
<directionalLight position={[-10, -10, -10]} intensity={0.2} />
|
||||
<React.Suspense fallback={null}>
|
||||
<Stage environment="city" intensity={0.6} adjustCamera>
|
||||
<Center>
|
||||
<Model
|
||||
url={url}
|
||||
onLoad={() => setLoading(false)}
|
||||
|
|
@ -75,7 +78,7 @@ export default function ThreeMfPreview({ url }: ThreeMfPreviewProps) {
|
|||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
</Stage>
|
||||
</Center>
|
||||
</React.Suspense>
|
||||
<OrbitControls makeDefault />
|
||||
</Canvas>
|
||||
|
|
|
|||
Reference in a new issue