- Removed the default border style from the Thumbnail component to streamline its appearance. - Updated the Thumbnail component to dynamically set the width in the card styles, enhancing layout flexibility. - Modified the fetchFileThumbnail function in ApiServerContext to include an optional parameter for handling 404 errors, improving error management during thumbnail fetching. - Added a thumbnail property to the GCodeFile model, enabling thumbnail support for GCode files.
189 lines
4.5 KiB
JavaScript
189 lines
4.5 KiB
JavaScript
import { useState, useEffect, useContext, useRef, memo } from 'react'
|
|
import { Flex, Card } from 'antd'
|
|
import { FileOutlined } from '@ant-design/icons'
|
|
import { decode } from 'blurhash'
|
|
import PropTypes from 'prop-types'
|
|
import { ApiServerContext } from '../context/ApiServerContext'
|
|
|
|
const getFileId = (file) =>
|
|
file?._id || (typeof file === 'string' ? file : null)
|
|
|
|
const getBlurHash = (file) => file?.metaData?.blurHash || null
|
|
|
|
const Thumbnail = function Thumbnail({
|
|
file,
|
|
size = 64,
|
|
alt = '',
|
|
fallback = null,
|
|
style = {},
|
|
className
|
|
}) {
|
|
const { fetchFileThumbnail } = useContext(ApiServerContext)
|
|
const fetchFileThumbnailRef = useRef(fetchFileThumbnail)
|
|
fetchFileThumbnailRef.current = fetchFileThumbnail
|
|
|
|
const fileId = getFileId(file)
|
|
const blurHash = getBlurHash(file)
|
|
|
|
const [thumbnailUrl, setThumbnailUrl] = useState(null)
|
|
const [blurHashUrl, setBlurHashUrl] = useState(null)
|
|
const thumbnailUrlRef = useRef(null)
|
|
const loadedKeyRef = useRef(null)
|
|
|
|
const imageStyle = {
|
|
width: size,
|
|
height: size,
|
|
borderRadius: '5px',
|
|
objectFit: 'cover',
|
|
...style
|
|
}
|
|
|
|
const cardStyle = {
|
|
borderRadius: '5px',
|
|
width: size,
|
|
height: size,
|
|
...style
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!blurHash) {
|
|
setBlurHashUrl(null)
|
|
return
|
|
}
|
|
|
|
try {
|
|
const pixels = decode(blurHash, 32, 32)
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = 32
|
|
canvas.height = 32
|
|
const ctx = canvas.getContext('2d')
|
|
const imageData = ctx.createImageData(32, 32)
|
|
imageData.data.set(pixels)
|
|
ctx.putImageData(imageData, 0, 0)
|
|
setBlurHashUrl(canvas.toDataURL())
|
|
} catch {
|
|
setBlurHashUrl(null)
|
|
}
|
|
}, [blurHash])
|
|
|
|
useEffect(() => {
|
|
if (!fileId) {
|
|
if (thumbnailUrlRef.current) {
|
|
URL.revokeObjectURL(thumbnailUrlRef.current)
|
|
thumbnailUrlRef.current = null
|
|
}
|
|
loadedKeyRef.current = null
|
|
setThumbnailUrl(null)
|
|
return
|
|
}
|
|
|
|
const loadKey = `${fileId}-${size}`
|
|
if (loadedKeyRef.current === loadKey && thumbnailUrlRef.current) {
|
|
return
|
|
}
|
|
|
|
let cancelled = false
|
|
const fileObject =
|
|
typeof file === 'object' && file !== null ? file : { _id: fileId }
|
|
|
|
const loadThumbnail = async () => {
|
|
try {
|
|
const fileURL = await fetchFileThumbnailRef.current(fileObject, size)
|
|
if (!cancelled) {
|
|
if (thumbnailUrlRef.current) {
|
|
URL.revokeObjectURL(thumbnailUrlRef.current)
|
|
}
|
|
thumbnailUrlRef.current = fileURL || null
|
|
loadedKeyRef.current = loadKey
|
|
setThumbnailUrl(fileURL || null)
|
|
} else if (fileURL) {
|
|
URL.revokeObjectURL(fileURL)
|
|
}
|
|
} catch {
|
|
if (!cancelled) {
|
|
setThumbnailUrl(null)
|
|
}
|
|
}
|
|
}
|
|
|
|
loadThumbnail()
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [file, fileId, size])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (thumbnailUrlRef.current) {
|
|
URL.revokeObjectURL(thumbnailUrlRef.current)
|
|
thumbnailUrlRef.current = null
|
|
}
|
|
loadedKeyRef.current = null
|
|
}
|
|
}, [])
|
|
|
|
if (thumbnailUrl) {
|
|
return (
|
|
<img
|
|
src={thumbnailUrl}
|
|
alt={alt}
|
|
className={className}
|
|
style={imageStyle}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (fileId && blurHashUrl) {
|
|
return (
|
|
<img
|
|
src={blurHashUrl}
|
|
alt=''
|
|
aria-hidden
|
|
className={className}
|
|
style={imageStyle}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
className={className}
|
|
style={cardStyle}
|
|
styles={{ body: { padding: 0, height: '100%', width: style?.width } }}
|
|
>
|
|
<Flex justify='center' align='center' style={{ height: '100%' }}>
|
|
{fallback || (
|
|
<FileOutlined
|
|
style={{
|
|
fontSize: Math.round(size * 0.22),
|
|
color: 'var(--color-text-secondary)'
|
|
}}
|
|
/>
|
|
)}
|
|
</Flex>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
Thumbnail.propTypes = {
|
|
file: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
|
size: PropTypes.oneOf([64, 128, 256]),
|
|
alt: PropTypes.string,
|
|
fallback: PropTypes.node,
|
|
style: PropTypes.object,
|
|
className: PropTypes.string
|
|
}
|
|
|
|
const areEqual = (prevProps, nextProps) => {
|
|
return (
|
|
getFileId(prevProps.file) === getFileId(nextProps.file) &&
|
|
getBlurHash(prevProps.file) === getBlurHash(nextProps.file) &&
|
|
prevProps.size === nextProps.size &&
|
|
prevProps.alt === nextProps.alt &&
|
|
JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
|
|
)
|
|
}
|
|
|
|
export default memo(Thumbnail, areEqual)
|