- 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.
185 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|