Tom Butcher 12cae32e33 Enhance Thumbnail and ApiServerContext components for improved functionality
- 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.
2026-08-01 15:11:02 +01:00

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)