Feat: Live thumbnails for office files
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m26s

This commit is contained in:
Elijah 2026-05-25 14:43:04 -07:00
parent b80abdbefb
commit 80d60d540a
10 changed files with 244 additions and 106 deletions

View file

@ -28,6 +28,7 @@ import FileInfoModal from './FileInfoModal';
import SharedFilesPage from './SharedFilesPage';
import OfficeHome from './OfficeHome';
import OnlyOfficeEditor from './OnlyOfficeEditor';
import ThumbnailImage from './ThumbnailImage';
interface FileItem {
name: string;
@ -172,40 +173,6 @@ function Tooltip({ children, text }: { children: React.ReactNode; text: string }
);
}
function ThumbnailImage({ checksum, fallbackIcon, downloadToken }: { checksum: string; fallbackIcon: React.ReactNode; downloadToken: string }) {
const [errorCount, setErrorCount] = useState(0);
const [key, setKey] = useState(0);
const [showFallback, setShowFallback] = useState(false);
return (
<>
<img
key={key}
src={downloadToken ? `${api.getThumbnailUrl(checksum, downloadToken)}${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 isMobile = useIsMobile();
const [sidebarOpen, setSidebarOpen] = useState(false);
@ -1159,7 +1126,8 @@ 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 isMedia = isImage || isVideo;
const isOffice = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(ext) || file.mime_type?.includes('officedocument');
const isMedia = isImage || isVideo || isOffice;
if (ext === 'pdf') {
return (
@ -1169,31 +1137,31 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
);
}
if (ext === 'docx' || ext === 'doc') {
return (
<div className={`${large ? 'w-20 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#2B579A]`}>
<FileText className={large ? "w-10 h-10 text-white" : "w-3 h-3 text-white"} />
</div>
);
}
if (ext === 'pptx' || ext === 'ppt') {
return (
<div className={`${large ? 'w-28 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#D24726]`}>
<LayoutTemplate className={large ? "w-10 h-10 text-white" : "w-3 h-3 text-white"} />
</div>
);
}
if (ext === 'xlsx' || ext === 'xls') {
return (
<div className={`${large ? 'w-28 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#107C41]`}>
<FileSpreadsheet className={large ? "w-10 h-10 text-white" : "w-3 h-3 text-white"} />
</div>
);
}
const fallbackIcon = (() => {
if (ext === 'docx' || ext === 'doc') {
return (
<div className={`${large ? 'w-20 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#2B579A]`}>
<FileText className={large ? "w-10 h-10 text-white" : "w-3 h-3 text-white"} />
</div>
);
}
if (ext === 'pptx' || ext === 'ppt') {
return (
<div className={`${large ? 'w-28 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#D24726]`}>
<LayoutTemplate className={large ? "w-10 h-10 text-white" : "w-3 h-3 text-white"} />
</div>
);
}
if (ext === 'xlsx' || ext === 'xls') {
return (
<div className={`${large ? 'w-28 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#107C41]`}>
<FileSpreadsheet className={large ? "w-10 h-10 text-white" : "w-3 h-3 text-white"} />
</div>
);
}
if (isImage) {
return (
<div className={`${large ? 'w-20 h-20 rounded-2xl' : 'w-5 h-5 rounded flex-shrink-0 aspect-square'} flex items-center justify-center shadow-sm text-white bg-[#3b82f6]`}>