u
This commit is contained in:
@@ -2,19 +2,34 @@ import { useEffect, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { thumbHashToRGBA } from 'thumbhash';
|
||||
|
||||
function decodeThumbHash(thumbHash) {
|
||||
const bytes = Uint8Array.from(atob(thumbHash), (c) => c.charCodeAt(0));
|
||||
return thumbHashToRGBA(bytes);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function paintThumbHash(canvas, thumbHash) {
|
||||
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;
|
||||
if (!parent) return;
|
||||
const rect = parent ? parent.getBoundingClientRect() : null;
|
||||
const width = explicitWidth || rect?.width;
|
||||
const height = explicitHeight || rect?.height;
|
||||
if (!width || !height) return;
|
||||
|
||||
const { width, height } = parent.getBoundingClientRect();
|
||||
if (width === 0 || height === 0) return;
|
||||
const decoded = decodeThumbHash(thumbHash);
|
||||
if (!decoded) return;
|
||||
const { w, h, rgba } = decoded;
|
||||
|
||||
const { w, h, rgba } = decodeThumbHash(thumbHash);
|
||||
const src = document.createElement('canvas');
|
||||
src.width = w;
|
||||
src.height = h;
|
||||
@@ -34,24 +49,24 @@ function paintThumbHash(canvas, thumbHash) {
|
||||
ctx.drawImage(src, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
export default function ThumbHashImage({ thumbHash, sx }) {
|
||||
export default function ThumbHashImage({ thumbHash, width, height, sx }) {
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas || !thumbHash) return;
|
||||
|
||||
paintThumbHash(canvas, thumbHash);
|
||||
paintThumbHash(canvas, thumbHash, width, height);
|
||||
|
||||
const parent = canvas.parentElement;
|
||||
if (!parent || typeof ResizeObserver === 'undefined') return undefined;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
paintThumbHash(canvas, thumbHash);
|
||||
paintThumbHash(canvas, thumbHash, width, height);
|
||||
});
|
||||
observer.observe(parent);
|
||||
return () => observer.disconnect();
|
||||
}, [thumbHash]);
|
||||
}, [thumbHash, width, height]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -7,6 +7,9 @@ import { EditablePanelTile } from 'dashgui/panels';
|
||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import { BrandLabel, BrandThumb } from '../../brand';
|
||||
|
||||
class PanelValue extends Component {
|
||||
render() {
|
||||
const { label, children } = this.props;
|
||||
@@ -27,7 +30,12 @@ class PanelValue extends Component {
|
||||
}
|
||||
|
||||
function brandDraftValue(article) {
|
||||
return { name: article?.brand?.name ?? '' };
|
||||
return {
|
||||
name: article?.brand?.name ?? '',
|
||||
thumbHash: article?.brand?.thumbHash ?? null,
|
||||
thumbW: article?.brand?.thumbW ?? null,
|
||||
thumbH: article?.brand?.thumbH ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function brandDraftDirty(draft, saved) {
|
||||
@@ -43,6 +51,11 @@ const SECOND_ROW_MIN_HEIGHT = 220;
|
||||
class BrandEdit extends Component {
|
||||
render() {
|
||||
const { draft, onChange, inputRef, brands } = this.props;
|
||||
const trimmed = String(draft.name ?? '').trim().toLowerCase();
|
||||
const matchingBrand = brands.find(
|
||||
(b) => String(b.name ?? '').toLowerCase() === trimmed,
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
@@ -57,6 +70,21 @@ class BrandEdit extends Component {
|
||||
onChange={(_event, option) => {
|
||||
onChange({ name: option ? String(option.name ?? '') : '' });
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
const name = typeof option === 'string' ? option : String(option.name ?? '');
|
||||
return (
|
||||
<Box
|
||||
component="li"
|
||||
key={key}
|
||||
{...optionProps}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
||||
>
|
||||
<BrandThumb brand={option} size={20} />
|
||||
<span>{name}</span>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
@@ -65,6 +93,16 @@ class BrandEdit extends Component {
|
||||
size="small"
|
||||
slotProps={{
|
||||
...params.slotProps,
|
||||
input: {
|
||||
...params.slotProps?.input,
|
||||
startAdornment: matchingBrand ? (
|
||||
<InputAdornment position="start" sx={{ mr: 0.5 }}>
|
||||
<BrandThumb brand={matchingBrand} size={20} />
|
||||
</InputAdornment>
|
||||
) : (
|
||||
params.slotProps?.input?.startAdornment
|
||||
),
|
||||
},
|
||||
inputLabel: {
|
||||
...params.slotProps?.inputLabel,
|
||||
shrink: true,
|
||||
@@ -152,8 +190,19 @@ export default class ArticleViewBrand extends PureComponent {
|
||||
onSave={this.handleSave}
|
||||
onError={notifyError}
|
||||
isDirty={brandDraftDirty}
|
||||
display={
|
||||
<PanelValue>
|
||||
{value.name ? (
|
||||
<BrandLabel
|
||||
brand={articleData?.brand || value}
|
||||
name={value.name}
|
||||
size={22}
|
||||
typographyProps={{ variant: 'body2', fontWeight: 600 }}
|
||||
/>
|
||||
) : null}
|
||||
</PanelValue>
|
||||
}
|
||||
canSave={brandDraftValid}
|
||||
display={<PanelValue>{value.name || null}</PanelValue>}
|
||||
edit={({ draft, onChange, inputRef }) => (
|
||||
<BrandEdit
|
||||
brands={brands}
|
||||
|
||||
Reference in New Issue
Block a user