This commit is contained in:
seb
2026-10-09 23:23:02 +02:00
parent 082771b9c3
commit 86069fa54c
4 changed files with 26 additions and 128 deletions

View File

@@ -1,126 +1,27 @@
import { useEffect, useRef } from 'react'; import { PureComponent } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { thumbHashToRGBA, thumbHashToDataURL } from 'thumbhash'; import { thumbHashToDataURL } from 'thumbhash';
const dataUrlCache = (globalThis.__THUMB_DATA_URL_CACHE__ ??= new Map()); export default class ThumbHashImage extends PureComponent {
const loadedImageHashes = (globalThis.__LOADED_IMAGE_HASHES__ ??= new Set()); constructor(props) {
export const imageCache = (globalThis.__IMAGE_CACHE__ ??= new Map()); super(props);
const bytes = Uint8Array.from(atob(props.thumbHash), (c) => c.charCodeAt(0));
export function isImageLoaded(fullHash) { this.url = thumbHashToDataURL(bytes);
if (!fullHash) return false;
return loadedImageHashes.has(fullHash) || imageCache.has(fullHash);
}
export function markImageLoaded(fullHash, img) {
if (!fullHash) return;
loadedImageHashes.add(fullHash);
if (img) {
imageCache.set(fullHash, img);
} }
}
export function getCachedImage(fullHash) {
if (!fullHash) return null;
const img = imageCache.get(fullHash);
if (img && img.complete && img.naturalWidth) {
return img;
}
return null;
}
export function getThumbHashDataUrl(thumbHash) {
if (!thumbHash || typeof thumbHash !== 'string') return null;
let url = dataUrlCache.get(thumbHash);
if (!url) {
try {
const bytes = Uint8Array.from(atob(thumbHash), (c) => c.charCodeAt(0));
url = thumbHashToDataURL(bytes);
dataUrlCache.set(thumbHash, url);
} catch (err) {
console.warn('[ThumbHashImage] thumbHashToDataURL failed:', err);
return null;
}
}
return url;
}
export function decodeThumbHash(thumbHash) {
if (!thumbHash || typeof thumbHash !== 'string') return null;
try {
const bytes = Uint8Array.from(atob(thumbHash), (c) => c.charCodeAt(0));
return thumbHashToRGBA(bytes);
} catch (err) {
console.warn('[ThumbHashImage] decode failed:', err);
return null;
}
}
export function getThumbHashDimensions(thumbHash) {
const decoded = decodeThumbHash(thumbHash);
if (!decoded) return null;
return { w: decoded.w, h: decoded.h };
}
function paintThumbHash(canvas, thumbHash, explicitWidth, explicitHeight) {
const parent = canvas.parentElement;
const rect = parent ? parent.getBoundingClientRect() : null;
const width = explicitWidth || rect?.width;
const height = explicitHeight || rect?.height;
if (!width || !height) return;
const decoded = decodeThumbHash(thumbHash);
if (!decoded) return;
const { w, h, rgba } = decoded;
const src = document.createElement('canvas');
src.width = w;
src.height = h;
src.getContext('2d').putImageData(
new ImageData(new Uint8ClampedArray(rgba), w, h),
0,
0,
);
canvas.width = Math.round(width);
canvas.height = Math.round(height);
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(src, 0, 0, canvas.width, canvas.height);
}
export default function ThumbHashImage({ thumbHash, width, height, sx }) {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !thumbHash) return;
paintThumbHash(canvas, thumbHash, width, height);
const parent = canvas.parentElement;
if (!parent || typeof ResizeObserver === 'undefined') return undefined;
const observer = new ResizeObserver(() => {
paintThumbHash(canvas, thumbHash, width, height);
});
observer.observe(parent);
return () => observer.disconnect();
}, [thumbHash, width, height]);
render() {
return ( return (
<Box <Box
component="canvas" component="img"
ref={canvasRef} src={this.url}
aria-hidden aria-hidden
sx={{ sx={{
display: 'block', display: 'block',
width: '100%', width: '100%',
height: '100%', height: '100%',
...sx, ...this.props.sx,
}} }}
/> />
); );
}
} }

View File

@@ -7,12 +7,6 @@ import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
import { formatPrice, getArticleGross } from '../../utils/price'; import { formatPrice, getArticleGross } from '../../utils/price';
import companyAutomat from '../../automats/companyAutomat'; import companyAutomat from '../../automats/companyAutomat';
import { articleSku } from '../article/articleRoute'; import { articleSku } from '../article/articleRoute';
import {
getThumbHashDataUrl,
getCachedImage,
isImageLoaded,
markImageLoaded,
} from '../article/ThumbHashImage';
import { BrandLabel } from '../brand'; import { BrandLabel } from '../brand';
import { thumb } from '../../api'; import { thumb } from '../../api';
import { import {

View File

@@ -10,7 +10,6 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import DeleteForeverOutlinedIcon from '@mui/icons-material/DeleteForeverOutlined'; import DeleteForeverOutlinedIcon from '@mui/icons-material/DeleteForeverOutlined';
import { getThumbHashDataUrl, isImageLoaded, markImageLoaded } from '../article/ThumbHashImage';
import { thumb } from '../../api'; import { thumb } from '../../api';
import { tileBaseSx } from 'dashgui/tiles'; import { tileBaseSx } from 'dashgui/tiles';
import { import {

View File

@@ -1,5 +1,6 @@
import { PureComponent, createRef } from 'react'; import { PureComponent, createRef } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import List from '@mui/material/List';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import FillSpace from 'dashgui/FillSpace'; import FillSpace from 'dashgui/FillSpace';
@@ -78,6 +79,9 @@ class ArtikelMarkenPage extends PureComponent {
<ArtikelMarkeNew rowRef={this.createRowRef} /> <ArtikelMarkeNew rowRef={this.createRowRef} />
</PanelTile> </PanelTile>
<PanelTile wide={2} minHeight={0} title="Test">
{brands[0] ? <ArtikelMarke brand={brands[0]} disabled /> : null}
</PanelTile>
</PanelTileGrid> </PanelTileGrid>
); );
} }