@etsoo/materialui
Version:
TypeScript Material-UI Implementation
169 lines (168 loc) • 7.83 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React from "react";
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
import RotateRightIcon from "@mui/icons-material/RotateRight";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import ImageIcon from "@mui/icons-material/Image";
import DoneIcon from "@mui/icons-material/Done";
import RemoveIcon from "@mui/icons-material/Remove";
import AddIcon from "@mui/icons-material/Add";
import { Labels } from "./app/Labels";
import { FileUploadButton } from "./FileUploadButton";
import Stack from "@mui/material/Stack";
import Skeleton from "@mui/material/Skeleton";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Slider from "@mui/material/Slider";
const defaultSize = 300;
const defaultState = {
scale: 1,
rotate: 0
};
/**
* User avatar editor
* https://github.com/mosch/react-avatar-editor
* @param props Props
* @returns Component
*/
export function UserAvatarEditor(props) {
// Labels
const labels = Labels.UserAvatarEditor;
// Destruct
const { border = 30, image, maxWidth, onDone, scaledResult = false, width = defaultSize, height = defaultSize, range = [0.1, 2, 0.1], selectFileLabel = labels.selectFile + "..." } = props;
// Container width
const containerWidth = width + 2 * border + 44 + 4;
// Calculated max width
const maxWidthCalculated = maxWidth == null || maxWidth < defaultSize ? 2 * width : maxWidth;
// Ref
const ref = React.createRef();
// Image type
const type = React.useRef("image/jpeg");
// Button ref
const buttonRef = React.createRef();
// Preview image state
const [previewImage, setPreviewImage] = React.useState(image);
React.useEffect(() => setPreviewImage(image), [image]);
// Is ready state
const [ready, setReady] = React.useState(false);
// Editor states
const [editorState, setEditorState] = React.useState(defaultState);
// Height
// noHeight: height is not set and will be updated dynamically
const noHeight = height <= 0;
const [localHeight, setHeight] = React.useState(noHeight ? defaultSize : height);
// Range
const [min, max, step] = range;
const marks = [
{
value: min,
label: min.toString()
},
{
value: max,
label: max.toString()
}
];
if (min < 1) {
marks.splice(1, 0, { value: 1, label: "1" });
}
// Handle zoom
const handleZoom = (_event, value, _activeThumb) => {
const scale = typeof value === "number" ? value : value[0];
setScale(scale);
};
const setScale = (scale) => {
const newState = { ...editorState, scale };
setEditorState(newState);
};
const adjustScale = (isAdd) => {
setScale(editorState.scale + (isAdd ? step : -step));
};
// Handle image load
const handleLoad = (imageInfo) => {
// Ignore too small images
if (imageInfo.resource.width < 10 || imageInfo.resource.height < 10)
return;
if (noHeight) {
setHeight((imageInfo.height * width) / imageInfo.width);
}
setReady(true);
};
// Handle file upload
const handleFileUpload = (files) => {
// Reset all settings
handleReset();
// Set new preview image
const file = files[0];
type.current = file.type;
setPreviewImage(file);
// Set ready state
setReady(false);
// Make the submit button visible
buttonRef.current?.scrollIntoView(false);
};
// Handle reset
const handleReset = () => {
setEditorState({ ...defaultState });
};
const resetUI = () => {
setReady(false);
setPreviewImage(undefined);
handleReset();
};
// Handle rotate
const handleRotate = (r) => {
let rotate = editorState.rotate + r;
if (rotate >= 360 || rotate <= -360)
rotate = 0;
const newState = { ...editorState, rotate };
setEditorState(newState);
};
// Handle done
const handleDone = async () => {
// Data
var data = scaledResult
? ref.current?.getImageScaledToCanvas()
: ref.current?.getImage();
if (data == null)
return;
// pica
const pica = (await import("pica")).default;
const picaInstance = pica();
// toBlob helper
// Convenience method, similar to canvas.toBlob(), but with promise interface & polyfill for old browsers.
const toBlob = (canvas, mimeType = type.current, quality = 1) => {
return picaInstance.toBlob(canvas, mimeType, quality);
};
if (data.width > maxWidthCalculated) {
// Target height
const heightCalculated = (localHeight * maxWidthCalculated) / width;
// Target
const to = document.createElement("canvas");
to.width = maxWidthCalculated;
to.height = heightCalculated;
// Large photo, resize it
// https://github.com/nodeca/pica
const canvas = await picaInstance.resize(data, to, {
unsharpAmount: 160,
unsharpRadius: 0.6,
unsharpThreshold: 1
});
const result = await onDone(canvas, toBlob, type.current);
if (result) {
resetUI();
}
}
else {
const result = await onDone(data, toBlob, type.current);
if (result) {
resetUI();
}
}
};
// Load the component
const AE = React.lazy(() => import("react-avatar-editor"));
return (_jsxs(Stack, { direction: "column", spacing: 0.5, width: containerWidth, children: [_jsx(FileUploadButton, { variant: "outlined", size: "medium", startIcon: _jsx(ImageIcon, {}), fullWidth: true, onUploadFiles: handleFileUpload, inputProps: { accept: "image/png, image/jpeg" }, children: selectFileLabel }), _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(React.Suspense, { fallback: _jsx(Skeleton, { variant: "rounded", width: width, height: localHeight }), children: _jsx(AE, { ref: ref, border: border, width: width, height: localHeight, onLoadSuccess: handleLoad, image: previewImage ??
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", scale: editorState.scale, rotate: editorState.rotate }) }), _jsxs(ButtonGroup, { size: "small", orientation: "vertical", disabled: !ready, children: [_jsx(Button, { onClick: () => handleRotate(90), title: labels.rotateRight, children: _jsx(RotateRightIcon, {}) }), _jsx(Button, { onClick: () => handleRotate(-90), title: labels.rotateLeft, children: _jsx(RotateLeftIcon, {}) }), _jsx(Button, { onClick: handleReset, title: labels.reset, children: _jsx(ClearAllIcon, {}) })] })] }), _jsxs(Stack, { spacing: 0.5, direction: "row", sx: { paddingBottom: 2 }, alignItems: "center", children: [_jsx(IconButton, { size: "small", disabled: !ready || editorState.scale <= min, onClick: () => adjustScale(false), children: _jsx(RemoveIcon, {}) }), _jsx(Slider, { title: labels.zoom, disabled: !ready, min: min, max: max, step: step, value: editorState.scale, valueLabelDisplay: "auto", valueLabelFormat: (value) => `${Math.round(100 * value) / 100}`, marks: marks, onChange: handleZoom }), _jsx(IconButton, { size: "small", disabled: !ready || editorState.scale >= max, onClick: () => adjustScale(true), children: _jsx(AddIcon, {}) })] }), _jsx(Button, { ref: buttonRef, variant: "contained", startIcon: _jsx(DoneIcon, {}), disabled: !ready, onClick: handleDone, children: labels.done })] }));
}