import { devlog } from "@frontend/kitui"; import { CropIcon } from "@icons/CropIcon"; import { ResetIcon } from "@icons/ResetIcon"; import { Box, Button, IconButton, Modal, Slider, SxProps, Theme, Typography, useMediaQuery, useTheme, } from "@mui/material"; import { enqueueSnackbar } from "notistack"; import { FC, useEffect, useMemo, useRef, useState } from "react"; import ReactCrop, { PercentCrop, PixelCrop } from "react-image-crop"; import "react-image-crop/dist/ReactCrop.css"; import { getCroppedImageBlob, getDarkenedAndResizedImageBlob, getRotatedImageBlob } from "./utils/imageManipulation"; const styleSlider: SxProps = { color: "#7E2AEA", height: "12px", "& .MuiSlider-track": { border: "none", }, "& .MuiSlider-rail": { backgroundColor: "#F2F3F7", border: `1px solid #9A9AAF`, }, "& .MuiSlider-thumb": { height: 26, width: 26, border: `6px solid #7E2AEA`, backgroundColor: "white", boxShadow: `0px 0px 0px 3px white, 0px 4px 4px 3px #C3C8DD`, "&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": { boxShadow: `0px 0px 0px 3px white, 0px 4px 4px 3px #C3C8DD`, }, }, }; interface Props { isOpen: boolean; imageBlob: Blob | null; originalImageUrl: string | null; setCropModalImageBlob: (imageBlob: Blob) => void; onClose: () => void; onSaveImageClick: (imageBlob: Blob) => void; } export const CropModal: FC = ({ isOpen, imageBlob, originalImageUrl, setCropModalImageBlob, onSaveImageClick, onClose }) => { const theme = useTheme(); const [percentCrop, setPercentCrop] = useState(); const [completedCrop, setCompletedCrop] = useState(); const [darken, setDarken] = useState(0); const [scale, setScale] = useState(1); const [imageWidth, setImageWidth] = useState(null); const [imageHeight, setImageHeight] = useState(null); const cropImageElementRef = useRef(null); const isMobile = useMediaQuery(theme.breakpoints.down(786)); const imageUrl = useMemo(() => imageBlob && URL.createObjectURL(imageBlob), [imageBlob]); function resetEditState() { setPercentCrop(undefined); setCompletedCrop(undefined); setDarken(0); setScale(1); } async function handleCropClick() { if (!cropImageElementRef.current) throw new Error("No image"); if (!completedCrop) return; try { const blob = await getCroppedImageBlob(cropImageElementRef.current, completedCrop); setCropModalImageBlob(blob); setPercentCrop(undefined); setCompletedCrop(undefined); } catch (error) { devlog("getCroppedImageBlob error", error); enqueueSnackbar("Не удалось изменить изображение"); } }; async function handleRotateClick() { if (!cropImageElementRef.current) throw new Error("No image"); try { const blob = await getRotatedImageBlob(cropImageElementRef.current); setCropModalImageBlob(blob); setPercentCrop(undefined); setCompletedCrop(undefined); } catch (error) { devlog("getRotatedImageBlob error", error); enqueueSnackbar("Не удалось изменить изображение"); } } async function handleSaveClick() { if (!cropImageElementRef.current) throw new Error("No image"); try { const blob = await getDarkenedAndResizedImageBlob(cropImageElementRef.current, scale, darken / 100); onSaveImageClick?.(blob); resetEditState(); onClose(); } catch (error) { devlog("getDarkenedImageBlob error", error); enqueueSnackbar("Не удалось сохранить изображение"); } } async function handleLoadOriginalImage() { if (!originalImageUrl) return; const response = await fetch(originalImageUrl); const blob = await response.blob(); setCropModalImageBlob(blob); resetEditState(); } return ( { resetEditState(); onClose(); }} aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description" > {imageUrl && ( setPercentCrop(percentCrop)} onComplete={(c) => setCompletedCrop(c)} minWidth={50} minHeight={50} > { setImageWidth(e.currentTarget.naturalWidth); setImageHeight(e.currentTarget.naturalHeight); }} ref={cropImageElementRef} alt="Crop me" src={imageUrl} style={{ filter: `brightness(${100 - darken}%)`, width: imageWidth ? imageWidth * scale : undefined, height: "100%", maxWidth: "100%", maxHeight: "320px", display: "block", objectFit: "contain", }} /> )} {imageWidth && imageHeight && ((percentCrop?.height && percentCrop?.width) ? {`${Math.round(percentCrop.width / 100 * imageWidth * scale)} x ${Math.round(percentCrop.height / 100 * imageHeight * scale)} px`} : {`${Math.round(imageWidth * scale)} x ${Math.round(imageHeight * scale)} px`} )} Размер { setScale((newValue as number) * 0.01); }} /> Затемнение setDarken(newValue as number)} /> ); }; export function useCropModalState(initialOpenState = false) { const [isCropModalOpen, setOpened] = useState(initialOpenState); const [imageBlob, setCropModalImageBlob] = useState(null); const [originalImageUrl, setOriginalImageUrl] = useState(null); const closeCropModal = () => { setOpened(false); setCropModalImageBlob(null); setOriginalImageUrl(null); }; async function openCropModal(image: Blob | string, originalImageUrl: string | null | undefined = null) { if (typeof image === "string") { const response = await fetch(image); image = await response.blob(); } setCropModalImageBlob(image); setOriginalImageUrl(originalImageUrl); setOpened(true); } return { isCropModalOpen, openCropModal, closeCropModal, imageBlob, setCropModalImageBlob, originalImageUrl, } as const; }