This commit is contained in:
seb
2026-10-04 09:21:45 +02:00
parent 4edd417dab
commit da318223c1
8 changed files with 315 additions and 166 deletions

View File

@@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography';
import { Tile, tileBaseSx } from 'dashgui/tiles';
import { formatPrice } from '../../utils/price';
import { articleSku } from '../article/articleRoute';
import { decodeThumbHash } from '../article/ThumbHashImage';
import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage';
import { BrandLabel } from '../brand';
import { thumb } from '../../api';
@@ -100,12 +100,22 @@ class ArticleTileBackdrop extends PureComponent {
img = null;
observer = null;
constructor(props) {
super(props);
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
this.img = getCachedImage(props.fullHash);
}
componentDidMount() {
this.loadImage();
}
componentDidUpdate(prevProps) {
if (prevProps.thumbHash !== this.props.thumbHash) {
this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash);
}
if (prevProps.fullHash !== this.props.fullHash) {
this.img = getCachedImage(this.props.fullHash);
this.loadImage();
}
}
@@ -118,8 +128,15 @@ class ArticleTileBackdrop extends PureComponent {
loadImage() {
const { fullHash } = this.props;
if (fullHash) {
const cached = getCachedImage(fullHash);
if (cached) {
this.img = cached;
this.paint();
return;
}
const img = new Image();
img.onload = () => {
imageCache.set(fullHash, img);
if (this.img === img) {
this.paint();
}
@@ -127,15 +144,18 @@ class ArticleTileBackdrop extends PureComponent {
img.src = thumb.url(fullHash);
this.img = img;
if (img.complete && img.naturalWidth) {
imageCache.set(fullHash, img);
this.paint();
return;
}
}
this.paint();
}
setCanvas = (canvas) => {
this.canvasRef = canvas;
if (!this.img && this.props.fullHash) {
this.img = getCachedImage(this.props.fullHash);
}
if (canvas && typeof ResizeObserver !== 'undefined') {
this.observer?.disconnect();
this.observer = new ResizeObserver(() => this.paint());
@@ -157,8 +177,9 @@ class ArticleTileBackdrop extends PureComponent {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const img = this.img;
const img = this.img || getCachedImage(this.props.fullHash);
if (img && img.complete && img.naturalWidth && img.naturalHeight) {
this.img = img;
drawContainedWithStretchedEdges(
ctx,
img,
@@ -167,41 +188,49 @@ class ArticleTileBackdrop extends PureComponent {
dstW,
dstH,
);
return;
}
const { thumbHash } = this.props;
if (thumbHash) {
const decoded = decodeThumbHash(thumbHash);
if (decoded) {
const { w, h, rgba } = decoded;
const temp = document.createElement('canvas');
temp.width = w;
temp.height = h;
temp.getContext('2d').putImageData(
new ImageData(new Uint8ClampedArray(rgba), w, h),
0,
0,
);
drawContainedWithStretchedEdges(ctx, temp, w, h, dstW, dstH);
}
}
};
render() {
const hasCachedImage = Boolean(this.img && this.img.complete && this.img.naturalWidth);
return (
<Box
component="canvas"
ref={this.setCanvas}
aria-hidden
sx={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
display: 'block',
overflow: 'hidden',
}}
/>
>
{!hasCachedImage && this.thumbUrl ? (
<Box
component="img"
src={this.thumbUrl}
alt=""
aria-hidden
sx={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
) : null}
<Box
component="canvas"
ref={this.setCanvas}
aria-hidden
sx={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
display: 'block',
}}
/>
</Box>
);
}
}

View File

@@ -9,7 +9,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import ThumbHashImage from '../article/ThumbHashImage';
import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage';
import { thumb } from '../../api';
import { tileBaseSx } from 'dashgui/tiles';
@@ -195,36 +195,56 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
* ThumbHash is shown immediately as a placeholder; the thumb fades in once loaded.
*/
class CategoryBackdrop extends PureComponent {
state = { thumbLoaded: false };
constructor(props) {
super(props);
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
this.isCached = Boolean(getCachedImage(props.fullHash));
}
componentDidUpdate(prevProps) {
if (prevProps.thumbHash !== this.props.thumbHash) {
this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash);
}
if (prevProps.fullHash !== this.props.fullHash) {
this.setState({ thumbLoaded: false });
this.isCached = Boolean(getCachedImage(this.props.fullHash));
this.forceUpdate();
}
}
handleThumbLoad = () => {
this.setState({ thumbLoaded: true });
};
handleThumbError = () => {
this.setState({ thumbLoaded: false });
handleLoad = (e) => {
if (this.props.fullHash && e.target) {
imageCache.set(this.props.fullHash, e.target);
}
};
render() {
const { thumbHash, fullHash } = this.props;
const { thumbLoaded } = this.state;
if (!thumbHash && !fullHash) return null;
const { fullHash } = this.props;
if (!this.thumbUrl && !fullHash) return null;
const isCached = this.isCached || Boolean(getCachedImage(fullHash));
return (
<Box sx={{ position: 'absolute', inset: 0, zIndex: 0, overflow: 'hidden' }}>
{thumbHash && !thumbLoaded ? <ThumbHashImage thumbHash={thumbHash} /> : null}
{fullHash ? (
{!isCached && this.thumbUrl ? (
<Box
component="img"
src={thumb.url(fullHash)}
src={this.thumbUrl}
alt=""
onLoad={this.handleThumbLoad}
onError={this.handleThumbError}
aria-hidden
sx={{
position: 'absolute',
inset: 0,
display: 'block',
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
) : null}
{fullHash ? (
<Box
component="img"
src={thumb.url(fullHash)}
alt=""
onLoad={this.handleLoad}
sx={{
position: 'absolute',
inset: 0,
@@ -232,7 +252,6 @@ class CategoryBackdrop extends PureComponent {
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: thumbLoaded ? 1 : 0,
}}
/>
) : null}