ultisuite-client/components/drive/file-thumbnail.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

185 lines
4.8 KiB
TypeScript

"use client"
import { useEffect, useRef, useState } from "react"
import { useQueryClient } from "@tanstack/react-query"
import { Play } from "lucide-react"
import type { DriveFileInfo } from "@/lib/api/types"
import { useDrivePreviewThumb } from "@/lib/api/hooks/use-drive-preview-thumb"
import {
usePublicSharePreviewThumb,
type PublicShareThumbContext,
} from "@/lib/api/hooks/use-public-share-preview-thumb"
import { DriveFileTypeIcon } from "@/lib/drive/drive-file-icon"
import {
drivePreviewKind,
driveServerThumbnail,
isOfficeFormat,
} from "@/lib/drive/drive-preview"
import { cn } from "@/lib/utils"
function useInView(rootMargin = "200px") {
const ref = useRef<HTMLDivElement>(null)
const [inView, setInView] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setInView(true)
observer.disconnect()
}
},
{ rootMargin }
)
observer.observe(el)
return () => observer.disconnect()
}, [rootMargin])
return { ref, inView }
}
function PreviewContent({
url,
display,
darkInvert,
onError,
}: {
url: string
display: "image" | "video"
darkInvert?: boolean
onError: () => void
}) {
if (display === "video") {
return (
<>
<video
src={url}
muted
playsInline
preload="metadata"
className="h-full w-full object-cover"
onLoadedData={(e) => {
const v = e.currentTarget
if (v.currentTime === 0) v.currentTime = 0.1
}}
onError={onError}
/>
<div
className="pointer-events-none absolute inset-0 flex items-center justify-center bg-black/20"
aria-hidden
>
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-black/55 shadow-md">
<Play className="ml-0.5 h-4 w-4 fill-white text-white" />
</div>
</div>
</>
)
}
return (
<img
src={url}
alt=""
className={cn(
"h-full w-full object-cover",
darkInvert && "dark:invert dark:hue-rotate-180",
)}
draggable={false}
onError={onError}
/>
)
}
export function FileThumbnail({
file,
variant = "grid",
inSharedView,
publicShare,
className,
}: {
file: DriveFileInfo
variant?: "list" | "grid"
inSharedView?: boolean
publicShare?: PublicShareThumbContext
className?: string
}) {
const { ref, inView } = useInView()
const [failed, setFailed] = useState(false)
const retriedRef = useRef(false)
const queryClient = useQueryClient()
const previewKind = drivePreviewKind(file)
const canPreview =
file.type === "file" &&
previewKind !== "audio" &&
(driveServerThumbnail(file) || previewKind !== null)
const showPreview = canPreview && !failed
const authThumb = useDrivePreviewThumb(file, !publicShare && inView && showPreview)
const publicThumb = usePublicSharePreviewThumb(file, publicShare, inView && showPreview)
const { data, isLoading } = publicShare ? publicThumb : authThumb
const showIcon = !showPreview || failed || (!data && !isLoading)
const darkInvertThumb = isOfficeFormat(file)
const iconSize = variant === "list" ? "md" : "lg"
const sizeClass =
variant === "list" ? "h-10 w-10 rounded-lg" : "aspect-[4/3] w-full rounded-lg"
useEffect(() => {
retriedRef.current = false
setFailed(false)
}, [file.path, file.etag])
const handlePreviewError = () => {
if (!retriedRef.current && data?.url.startsWith("blob:")) {
retriedRef.current = true
void queryClient.invalidateQueries({
queryKey: publicShare
? ["public-share", "preview-thumb", publicShare.token, file.path, file.etag]
: ["drive", "preview-thumb", file.path, file.etag],
})
return
}
setFailed(true)
}
return (
<div
ref={ref}
className={cn(
"relative shrink-0 overflow-hidden bg-[#f8f9fa] dark:bg-muted/30",
sizeClass,
className
)}
>
{showIcon ? (
<div className="flex h-full w-full items-center justify-center">
<DriveFileTypeIcon
file={file}
inSharedView={inSharedView}
size={iconSize}
className={isLoading && showPreview ? "opacity-40" : undefined}
/>
</div>
) : null}
{showPreview && isLoading ? (
<div className="absolute inset-0 animate-pulse bg-muted" aria-hidden />
) : null}
{showPreview && data ? (
<div className="absolute inset-0 overflow-hidden bg-white dark:bg-transparent">
<PreviewContent
url={data.url}
display={data.display}
darkInvert={darkInvertThumb}
onError={handlePreviewError}
/>
</div>
) : null}
</div>
)
}