diff --git a/package.json b/package.json index ada5c835..ba015ef2 100755 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@frontend/kitui": "^1.0.74", - "@frontend/squzanswerer": "^1.0.17", + "@frontend/squzanswerer": "^1.0.19", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", "@mui/x-charts": "^6.19.5", diff --git a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx index 3c1b74c2..314947fa 100644 --- a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx +++ b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx @@ -1,6 +1,7 @@ import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; import { addQuestionVariant, + clearQuestionImages, uploadQuestionImage, } from "@root/questions/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; @@ -125,7 +126,10 @@ export default function OptionsAndPicture({ setCropModalImageBlob={setCropModalImageBlob} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} - questionId={question.id} + onDeleteClick={() => { + if (selectedVariantId) + clearQuestionImages(question.id, selectedVariantId); + }} cropAspectRatio={{ width: 452, height: 300 }} /> { + if (selectedVariantId) + clearQuestionImages(question.id, selectedVariantId); + }} cropAspectRatio={{ width: 452, height: 300 }} /> diff --git a/src/pages/ViewPublicationPage.tsx b/src/pages/ViewPublicationPage.tsx index 635f3aa3..c4432546 100644 --- a/src/pages/ViewPublicationPage.tsx +++ b/src/pages/ViewPublicationPage.tsx @@ -80,6 +80,7 @@ export default function ViewPublicationPage() { }, }} quizId={quizId} + preview /> ); diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index ae0a57d0..9bb34e2c 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -370,6 +370,21 @@ export const setQuestionVariantField = ( }); }; +export const clearQuestionImages = (questionId: string, variantId: string) => { + updateQuestion(questionId, (question) => { + if (!("variants" in question.content)) return; + + const variantIndex = question.content.variants.findIndex( + (variant) => variant.id === variantId, + ); + if (variantIndex === -1) return; + + const variant = question.content.variants[variantIndex]; + variant.extendedText = ""; + variant.originalImageUrl = ""; + }); +}; + export const reorderQuestionVariants = ( questionId: string, sourceIndex: number, diff --git a/src/ui_kit/MediaSelectionAndDisplay.tsx b/src/ui_kit/MediaSelectionAndDisplay.tsx index 7d1abc5c..d7c7ec51 100644 --- a/src/ui_kit/MediaSelectionAndDisplay.tsx +++ b/src/ui_kit/MediaSelectionAndDisplay.tsx @@ -139,7 +139,12 @@ export const MediaSelectionAndDisplay: FC = ({ setCropModalImageBlob={setCropModalImageBlob} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} - questionId={resultData.id} + onDeleteClick={() => { + updateQuestion(resultData.id, (question) => { + question.content.back = null; + question.content.originalBack = null; + }); + }} cropAspectRatio={cropAspectRatio} /> diff --git a/src/ui_kit/Modal/CropModal.tsx b/src/ui_kit/Modal/CropModal.tsx index 85a60a6f..bb7ab8eb 100644 --- a/src/ui_kit/Modal/CropModal.tsx +++ b/src/ui_kit/Modal/CropModal.tsx @@ -1,5 +1,4 @@ import { devlog } from "@frontend/kitui"; -import { CropIcon } from "@icons/CropIcon"; import { ResetIcon } from "@icons/ResetIcon"; import DeleteIcon from "@mui/icons-material/Delete"; import { @@ -14,7 +13,6 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { updateQuestion } from "@root/questions/actions"; import { enqueueSnackbar } from "notistack"; import { FC, useCallback, useMemo, useRef, useState } from "react"; import ReactCrop, { @@ -26,8 +24,7 @@ import ReactCrop, { import "react-image-crop/dist/ReactCrop.css"; import { isImageBlobAGifFile } from "../../utils/isImageBlobAGifFile"; import { - getCroppedImageBlob, - getDarkenedAndResizedImageBlob, + getModifiedImageBlob, getRotatedImageBlob, } from "./utils/imageManipulation"; @@ -62,6 +59,7 @@ interface Props { setCropModalImageBlob: (imageBlob: Blob) => void; onClose: () => void; onSaveImageClick: (imageBlob: Blob) => void; + onDeleteClick?: () => void; questionId?: string; cropAspectRatio?: { width: number; @@ -75,12 +73,15 @@ export const CropModal: FC = ({ originalImageUrl, setCropModalImageBlob, onSaveImageClick, + onDeleteClick, onClose, questionId, cropAspectRatio, }) => { const theme = useTheme(); - const [percentCrop, setPercentCrop] = useState(); + const [percentCrop, setPercentCrop] = useState( + undefined, + ); const [darken, setDarken] = useState(0); const [imageWidth, setImageWidth] = useState(null); const [imageHeight, setImageHeight] = useState(null); @@ -97,22 +98,24 @@ export const CropModal: FC = ({ setDarken(0); } - async function handleCropClick() { + async function handleSaveModifiedImage() { if (!percentCrop || !imageWidth || !imageHeight) return; if (!cropImageElementRef.current) throw new Error("No image"); const width = cropImageElementRef.current.width; const height = cropImageElementRef.current.height; - const pixelCrop = convertToPixelCrop(percentCrop, width, height); + try { - const blob = await getCroppedImageBlob( + const blob = await getModifiedImageBlob( cropImageElementRef.current, pixelCrop, + darken, ); - setCropModalImageBlob(blob); - setPercentCrop(undefined); + onSaveImageClick?.(blob); + resetEditState(); + onClose(); } catch (error) { devlog("getCroppedImageBlob error", error); enqueueSnackbar("Не удалось изменить изображение"); @@ -133,32 +136,15 @@ export const CropModal: FC = ({ } } - async function handleSaveClick() { - if (!cropImageElementRef.current) throw new Error("No image"); - - try { - const blob = await getDarkenedAndResizedImageBlob( - cropImageElementRef.current, - 1, - darken / 100, - ); - onSaveImageClick?.(blob); - resetEditState(); - onClose(); - } catch (error) { - devlog("getDarkenedImageBlob error", error); - enqueueSnackbar("Не удалось сохранить изображение"); - } - } - - async function handleLoadOriginalImage() { + async function handleSaveOriginalImage() { if (!originalImageUrl) return; const response = await fetch(originalImageUrl); const blob = await response.blob(); - setCropModalImageBlob(blob); + onSaveImageClick?.(blob); resetEditState(); + onClose(); } function handleSizeChange(value: number) { @@ -248,6 +234,18 @@ export const CropModal: FC = ({ onLoad={(e) => { setImageWidth(e.currentTarget.naturalWidth); setImageHeight(e.currentTarget.naturalHeight); + + if (cropImageElementRef.current) { + setPercentCrop( + getInitialCrop( + cropImageElementRef.current.width, + cropImageElementRef.current.height, + cropAspectRatio + ? cropAspectRatio.width / cropAspectRatio.height + : 1, + ), + ); + } }} ref={cropImageElementRef} alt="Crop me" @@ -265,23 +263,7 @@ export const CropModal: FC = ({ - {cropAspectRatio && ( - - {`${cropAspectRatio.width} x ${cropAspectRatio.height} px`} - - )} - - = ({ onChange={(_, newValue) => setDarken(newValue as number)} /> - {questionId !== undefined && ( + {onDeleteClick && ( { - updateQuestion(questionId, (question) => { - question.content.back = null; - question.content.originalBack = null; - }); + onDeleteClick?.(); onClose(); }} sx={{ @@ -359,41 +338,22 @@ export const CropModal: FC = ({ }} > - - @@ -451,3 +411,29 @@ export function useCropModalState(initialOpenState = false) { originalImageUrl, } as const; } + +function getInitialCrop( + imageWidth: number, + imageHeight: number, + aspectRatio: number, +): PercentCrop { + const imageAspectRatio = imageWidth / imageHeight; + + return centerCrop( + { + width: + imageAspectRatio < aspectRatio + ? 100 + : (100 * aspectRatio) / imageAspectRatio, + height: + imageAspectRatio < aspectRatio + ? (100 * imageAspectRatio) / aspectRatio + : 100, + unit: "%", + x: 0, + y: 0, + }, + imageWidth, + imageHeight, + ); +} diff --git a/src/ui_kit/Modal/utils/imageManipulation.ts b/src/ui_kit/Modal/utils/imageManipulation.ts index 8601a5f7..7cd5ec89 100644 --- a/src/ui_kit/Modal/utils/imageManipulation.ts +++ b/src/ui_kit/Modal/utils/imageManipulation.ts @@ -21,7 +21,11 @@ export function getRotatedImageBlob(image: HTMLImageElement) { }); } -export function getCroppedImageBlob(image: HTMLImageElement, crop: PixelCrop) { +export function getModifiedImageBlob( + image: HTMLImageElement, + crop: PixelCrop, + darken: number, +) { return new Promise((resolve, reject) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); @@ -60,35 +64,9 @@ export function getCroppedImageBlob(image: HTMLImageElement, crop: PixelCrop) { image.naturalHeight, ); - canvas.toBlob((blob) => { - if (!blob) return reject(new Error("Failed to create blob")); - - resolve(blob); - }); - }); -} - -export function getDarkenedAndResizedImageBlob( - image: HTMLImageElement, - scale: number, - darken: number, -) { - return new Promise((resolve, reject) => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - if (!ctx) return reject(new Error("No 2d context")); - - const width = Math.floor(image.naturalWidth * scale); - const height = Math.floor(image.naturalHeight * scale); - - canvas.width = width; - canvas.height = height; - - ctx.drawImage(image, 0, 0, width, height); - if (darken > 0) { - ctx.fillStyle = `rgba(0, 0, 0, ${darken})`; - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.fillStyle = `rgba(0, 0, 0, ${darken / 100})`; + ctx.fillRect(0, 0, image.naturalWidth, image.naturalHeight); } canvas.toBlob((blob) => { diff --git a/yarn.lock b/yarn.lock index c9b42653..688bd14f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1517,10 +1517,10 @@ immer "^10.0.2" reconnecting-eventsource "^1.6.2" -"@frontend/squzanswerer@^1.0.17": - version "1.0.18" - resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.18.tgz#9a191317ccf7b396af4e85e8c9f1f52cc5243f96" - integrity sha1-mhkTF8z3s5avToXoyfH1LMUkP5Y= +"@frontend/squzanswerer@^1.0.19": + version "1.0.19" + resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.19.tgz#d04b862073bcb9f7b6b0d229684bbceb920614e4" + integrity sha1-0EuGIHO8ufe2sNIpaEu865IGFOQ= dependencies: bowser "1.9.4" country-flag-emoji-polyfill "^0.1.8"