UNPKG

@etsoo/materialui

Version:

TypeScript Material-UI Implementation

169 lines (168 loc) 7.83 kB
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 })] })); }