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"