Implement code highlighting, thumbnail retries, and pinned order fix
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m29s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m29s
This commit is contained in:
parent
745d959c9b
commit
2b49e90b81
5 changed files with 371 additions and 16 deletions
|
|
@ -151,6 +151,40 @@ function Tooltip({ children, text }: { children: React.ReactNode; text: string }
|
|||
);
|
||||
}
|
||||
|
||||
function ThumbnailImage({ checksum, fallbackIcon }: { checksum: string; fallbackIcon: React.ReactNode }) {
|
||||
const [errorCount, setErrorCount] = useState(0);
|
||||
const [key, setKey] = useState(0);
|
||||
const [showFallback, setShowFallback] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
key={key}
|
||||
src={`${api.getThumbnailUrl(checksum)}${errorCount > 0 ? `?t=${key}` : ''}`}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
style={{ display: showFallback ? 'none' : 'block' }}
|
||||
onError={(e) => {
|
||||
if (errorCount < 5) {
|
||||
e.currentTarget.style.display = 'none';
|
||||
setTimeout(() => {
|
||||
setErrorCount(c => c + 1);
|
||||
setKey(Date.now());
|
||||
}, 2000);
|
||||
} else {
|
||||
setShowFallback(true);
|
||||
}
|
||||
}}
|
||||
onLoad={(e) => {
|
||||
e.currentTarget.style.display = 'block';
|
||||
setShowFallback(false);
|
||||
}}
|
||||
/>
|
||||
{showFallback && fallbackIcon}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||
const [currentPath, setCurrentPath] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -202,6 +236,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
||||
const [pinnedExpanded, setPinnedExpanded] = useState(false);
|
||||
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
|
||||
const [dragOverPinnedItem, setDragOverPinnedItem] = useState<string | null>(null);
|
||||
const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; currentX: number; currentY: number } | null>(null);
|
||||
const [initialSelectedFiles, setInitialSelectedFiles] = useState<Set<string>>(new Set());
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
|
@ -806,17 +841,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
if (isMedia && file.checksum && viewMode === 'grid') {
|
||||
return (
|
||||
<div className="relative w-full h-full flex items-center justify-center overflow-hidden rounded-md">
|
||||
<img
|
||||
src={api.getThumbnailUrl(file.checksum)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
const next = e.currentTarget.nextElementSibling as HTMLElement;
|
||||
if (next) next.style.display = 'block';
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'none' }}>{fallbackIcon}</div>
|
||||
<ThumbnailImage checksum={file.checksum} fallbackIcon={fallbackIcon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -977,16 +1002,24 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOverPinnedItem(file.path);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setDragOverPinnedItem(null);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOverPinnedItem(null);
|
||||
const source = e.dataTransfer.getData('text/plain');
|
||||
if (source && source !== file.path) {
|
||||
handlePinnedDragEnd(source, file.path);
|
||||
}
|
||||
}}
|
||||
className="group flex items-center justify-between px-10 py-2 text-sm font-medium cursor-pointer rounded-lg transition-colors hover:bg-white/5 gap-3"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
style={{
|
||||
color: 'var(--color-text-secondary)',
|
||||
borderTop: dragOverPinnedItem === file.path ? '2px solid var(--color-accent)' : '2px solid transparent',
|
||||
}}
|
||||
onClick={() => { setActiveSection('files'); navigateTo(file.path); }}
|
||||
>
|
||||
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 [&>svg]:w-4 [&>svg]:h-4">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||
import { X, Download, File, Image as ImageIcon, Film, Music, FileText, Code, Loader2 } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
const DynamicPdfViewer = dynamic(() => import('./PdfViewer'), { ssr: false });
|
||||
|
||||
|
|
@ -83,9 +85,9 @@ export default function FilePreview({ file, onClose }: FilePreviewProps) {
|
|||
exit={{ scale: 0.95, opacity: 0, y: 20 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`relative flex flex-col max-w-[95vw] max-h-[95vh] rounded-2xl overflow-hidden shadow-2xl ${
|
||||
(previewType === 'image' || previewType === 'video' || previewType === 'pdf') ? 'w-fit' : 'w-full'
|
||||
(previewType === 'image' || previewType === 'video' || previewType === 'pdf' || previewType === 'audio') ? 'w-fit' : 'w-full'
|
||||
} ${
|
||||
(previewType === 'image' || previewType === 'video') ? 'h-fit' : 'h-full'
|
||||
(previewType === 'image' || previewType === 'video' || previewType === 'audio') ? 'h-fit' : 'h-full'
|
||||
}`}
|
||||
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
|
|
@ -173,6 +175,17 @@ export default function FilePreview({ file, onClose }: FilePreviewProps) {
|
|||
Download File
|
||||
</a>
|
||||
</div>
|
||||
) : previewType === 'code' ? (
|
||||
<div className="w-full h-full overflow-auto rounded-lg" style={{ backgroundColor: 'var(--color-bg-secondary)', border: '1px solid var(--color-border-subtle)' }}>
|
||||
<SyntaxHighlighter
|
||||
language={file.name.split('.').pop()?.toLowerCase() || 'text'}
|
||||
style={vscDarkPlus}
|
||||
customStyle={{ margin: 0, background: 'transparent', height: '100%', fontSize: '14px' }}
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{content || ''}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<pre
|
||||
className="w-full h-full p-4 overflow-auto text-sm rounded-lg"
|
||||
|
|
|
|||
Reference in a new issue