u
This commit is contained in:
@@ -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));
|
||||||
|
this.url = thumbHashToDataURL(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
export function isImageLoaded(fullHash) {
|
render() {
|
||||||
if (!fullHash) return false;
|
return (
|
||||||
return loadedImageHashes.has(fullHash) || imageCache.has(fullHash);
|
<Box
|
||||||
}
|
component="img"
|
||||||
|
src={this.url}
|
||||||
export function markImageLoaded(fullHash, img) {
|
aria-hidden
|
||||||
if (!fullHash) return;
|
sx={{
|
||||||
loadedImageHashes.add(fullHash);
|
display: 'block',
|
||||||
if (img) {
|
width: '100%',
|
||||||
imageCache.set(fullHash, img);
|
height: '100%',
|
||||||
|
...this.props.sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
component="canvas"
|
|
||||||
ref={canvasRef}
|
|
||||||
aria-hidden
|
|
||||||
sx={{
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user