feat: replace multiple useState with useReduce in export dialog

This commit is contained in:
Arnošt Pleskot 2023-09-19 17:52:13 +02:00
parent fff2b6ea19
commit 6957398364
No known key found for this signature in database

View File

@ -1,10 +1,4 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import React, { useEffect, useReducer, useRef, useState } from "react";
import clsx from "clsx";
import type { ActionManager } from "../actions/manager";
@ -29,7 +23,10 @@ import {
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { NonDeletedExcalidrawElement } from "../element/types";
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas, getScaleToFit } from "../packages/utils";
@ -52,6 +49,7 @@ import {
} from "../utils";
import { getFancyBackgroundPadding } from "../scene/fancyBackground";
import { Select } from "./Select";
import { Bounds } from "../element/bounds";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
@ -83,6 +81,113 @@ type ImageExportModalProps = {
onExportImage: AppClassProperties["onExportImage"];
};
type State = {
projectName: string;
someElementIsSelected: boolean;
exportSelected: boolean;
exportedElements:
| readonly NonDeletedExcalidrawElement[]
| ExcalidrawElement[];
elementsBounds: Bounds;
exportWithBackground: boolean;
exportBackgroundImage: keyof typeof FANCY_BACKGROUND_IMAGES;
exportDarkMode: boolean;
embedScene: boolean;
exportScale: number;
exportBaseScale: number;
renderError: Error | null;
};
type Action =
| { type: "SET_PROJECT_NAME"; projectName: string }
| {
type: "SET_EXPORT_SELECTED";
exportSelected: boolean;
exportedElements: readonly NonDeletedExcalidrawElement[];
}
| {
type: "SET_EXPORTED_ELEMENTS";
exportedElements: readonly NonDeletedExcalidrawElement[];
}
| { type: "SET_EXPORT_WITH_BACKGROUND"; exportWithBackground: boolean }
| {
type: "SET_EXPORT_BACKGROUND_IMAGE";
exportBackgroundImage: keyof typeof FANCY_BACKGROUND_IMAGES;
}
| { type: "SET_EXPORT_DARK_MODE"; exportDarkMode: boolean }
| { type: "SET_EMBED_SCENE"; embedScene: boolean }
| { type: "SET_EXPORT_SCALE"; exportScale: number }
| { type: "SET_ALL_SCALES"; exportScale: number }
| { type: "SET_RENDER_ERROR"; renderError: Error | null };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_PROJECT_NAME":
return {
...state,
projectName: action.projectName,
};
case "SET_EXPORT_SELECTED":
return {
...state,
exportSelected: action.exportSelected,
exportedElements: action.exportedElements,
};
case "SET_EXPORTED_ELEMENTS":
return {
...state,
exportedElements: action.exportedElements,
elementsBounds: getCommonBounds(action.exportedElements),
};
case "SET_EXPORT_WITH_BACKGROUND":
return {
...state,
exportWithBackground: action.exportWithBackground,
};
case "SET_EXPORT_BACKGROUND_IMAGE":
return {
...state,
exportBackgroundImage: action.exportBackgroundImage,
};
case "SET_EXPORT_DARK_MODE":
return { ...state, exportDarkMode: action.exportDarkMode };
case "SET_EMBED_SCENE":
return { ...state, embedScene: action.embedScene };
case "SET_EXPORT_SCALE":
return { ...state, exportScale: action.exportScale };
case "SET_ALL_SCALES":
return {
...state,
exportScale: action.exportScale,
exportBaseScale: action.exportScale,
};
default:
return state;
}
};
const createInitialState = ({
appState,
elements,
}: Pick<ImageExportModalProps, "appState" | "elements">): State => {
const someElementIsSelected = isSomeElementSelected(elements, appState);
return {
projectName: appState.name,
someElementIsSelected: isSomeElementSelected(elements, appState),
exportedElements: elements,
elementsBounds: getCommonBounds(elements),
exportSelected: someElementIsSelected,
exportWithBackground: appState.exportBackground,
exportBackgroundImage: appState.fancyBackgroundImageKey,
exportDarkMode: appState.exportWithDarkMode,
embedScene: appState.exportEmbedScene,
exportScale: appState.exportScale,
exportBaseScale: appState.exportScale,
renderError: null,
};
};
const ImageExportModal = ({
appState,
elements,
@ -90,61 +195,38 @@ const ImageExportModal = ({
actionManager,
onExportImage,
}: ImageExportModalProps) => {
const appProps = useAppProps();
const [projectName, setProjectName] = useState(appState.name);
const someElementIsSelected = isSomeElementSelected(elements, appState);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const [exportWithBackground, setExportWithBackground] = useState(
appState.exportBackground,
const [state, dispatch] = useReducer(
reducer,
{ appState, elements },
createInitialState,
);
const [exportBackgroundImage, setExportBackgroundImage] = useState<
keyof typeof FANCY_BACKGROUND_IMAGES
>(appState.fancyBackgroundImageKey);
const [exportDarkMode, setExportDarkMode] = useState(
appState.exportWithDarkMode,
);
const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene);
const [exportScale, setExportScale] = useState(appState.exportScale);
const [exportBaseScale, setExportBaseScale] = useState(appState.exportScale);
const previewRef = useRef<HTMLDivElement>(null);
const [renderError, setRenderError] = useState<Error | null>(null);
const exportedElements = useMemo(
() =>
exportSelected
useEffect(() => {
dispatch({
type: "SET_EXPORTED_ELEMENTS",
exportedElements: state.exportSelected
? getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
})
: elements,
[exportSelected, elements],
);
});
}, [elements, appState, state.exportSelected]);
const updateAllScales = useCallback(
(scale: number) => {
actionManager.executeAction(actionChangeExportScale, "ui", scale);
setExportScale(scale);
setExportBaseScale(scale);
},
[actionManager, setExportScale, setExportBaseScale],
);
const appProps = useAppProps();
const previewRef = useRef<HTMLDivElement>(null);
const [renderError, setRenderError] = useState<Error | null>(null);
// Upscale exported image when is smaller than preview
useEffect(() => {
if (
exportedElements.length > 0 &&
exportWithBackground &&
exportBackgroundImage !== "solid"
) {
let scale = defaultExportScale;
if (state.exportWithBackground && state.exportBackgroundImage !== "solid") {
const previewNode = previewRef.current;
if (!previewNode) {
return;
}
const [minX, minY, maxX, maxY] = getCommonBounds(exportedElements);
const [minX, minY, maxX, maxY] = state.elementsBounds;
const maxWidth = previewNode.offsetWidth;
const maxHeight = previewNode.offsetHeight;
@ -153,7 +235,7 @@ const ImageExportModal = ({
true,
);
const scale =
const upscaledScale =
Math.floor(
(getScaleToFit(
{
@ -166,23 +248,25 @@ const ImageExportModal = ({
100,
) / 100;
if (scale > 1) {
if (scale !== exportBaseScale) {
updateAllScales(scale);
}
} else {
updateAllScales(defaultExportScale);
if (upscaledScale > 1) {
scale = upscaledScale;
}
} else if (exportBaseScale !== defaultExportScale) {
updateAllScales(defaultExportScale);
}
if (scale !== state.exportBaseScale) {
dispatch({
type: "SET_ALL_SCALES",
exportScale: scale > 1 ? scale : defaultExportScale,
});
actionManager.executeAction(actionChangeExportScale, "ui", scale);
}
}, [
exportBackgroundImage,
exportWithBackground,
exportBaseScale,
updateAllScales,
exportedElements,
exportSelected,
state.elementsBounds,
state.exportSelected,
state.exportWithBackground,
state.exportBackgroundImage,
state.exportBaseScale,
actionManager,
]);
useEffect(() => {
@ -199,7 +283,7 @@ const ImageExportModal = ({
return;
}
exportToCanvas({
elements: exportedElements,
elements: state.exportedElements,
appState,
files,
exportPadding: DEFAULT_EXPORT_PADDING,
@ -222,7 +306,7 @@ const ImageExportModal = ({
appState.exportBackground,
appState.fancyBackgroundImageKey,
files,
exportedElements,
state.exportedElements,
]);
return (
@ -248,12 +332,15 @@ const ImageExportModal = ({
<input
type="text"
className="TextInput"
value={projectName}
value={state.projectName}
disabled={
typeof appProps.name !== "undefined" || appState.viewModeEnabled
}
onChange={(event) => {
setProjectName(event.target.value);
dispatch({
type: "SET_PROJECT_NAME",
projectName: event.target.value,
});
actionManager.executeAction(
actionChangeProjectName,
"ui",
@ -263,16 +350,25 @@ const ImageExportModal = ({
/>
</div>
)}
{someElementIsSelected && (
{state.someElementIsSelected && (
<ExportSetting
label={t("imageExportDialog.label.onlySelected")}
name="exportOnlySelected"
>
<Switch
name="exportOnlySelected"
checked={exportSelected}
checked={state.exportSelected}
onChange={(checked) => {
setExportSelected(checked);
dispatch({
type: "SET_EXPORT_SELECTED",
exportSelected: checked,
exportedElements: checked
? getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
})
: elements,
});
}}
/>
</ExportSetting>
@ -282,13 +378,16 @@ const ImageExportModal = ({
name="exportBackgroundSwitch"
multipleInputs
>
{exportWithBackground && (
{state.exportWithBackground && (
<Select
value={exportBackgroundImage}
value={state.exportBackgroundImage}
options={fancyBackgroundImageOptions}
onSelect={(key) => {
if (isBackgroundImageKey(key)) {
setExportBackgroundImage(key);
dispatch({
type: "SET_EXPORT_BACKGROUND_IMAGE",
exportBackgroundImage: key,
});
actionManager.executeAction(
actionChangeFancyBackgroundImageUrl,
"ui",
@ -300,9 +399,12 @@ const ImageExportModal = ({
)}
<Switch
name="exportBackgroundSwitch"
checked={exportWithBackground}
checked={state.exportWithBackground}
onChange={(checked) => {
setExportWithBackground(checked);
dispatch({
type: "SET_EXPORT_WITH_BACKGROUND",
exportWithBackground: checked,
});
actionManager.executeAction(
actionChangeExportBackground,
"ui",
@ -318,9 +420,12 @@ const ImageExportModal = ({
>
<Switch
name="exportDarkModeSwitch"
checked={exportDarkMode}
checked={state.exportDarkMode}
onChange={(checked) => {
setExportDarkMode(checked);
dispatch({
type: "SET_EXPORT_DARK_MODE",
exportDarkMode: checked,
});
actionManager.executeAction(
actionExportWithDarkMode,
"ui",
@ -337,9 +442,9 @@ const ImageExportModal = ({
>
<Switch
name="exportEmbedSwitch"
checked={embedScene}
checked={state.embedScene}
onChange={(checked) => {
setEmbedScene(checked);
dispatch({ type: "SET_EMBED_SCENE", embedScene: checked });
actionManager.executeAction(
actionChangeExportEmbedScene,
"ui",
@ -354,13 +459,13 @@ const ImageExportModal = ({
>
<RadioGroup
name="exportScale"
value={exportScale}
value={state.exportScale}
onChange={(scale) => {
setExportScale(scale);
dispatch({ type: "SET_EXPORT_SCALE", exportScale: scale });
actionManager.executeAction(actionChangeExportScale, "ui", scale);
}}
choices={EXPORT_SCALES.map((scale) => ({
value: scale * exportBaseScale,
value: scale * state.exportBaseScale,
label: `${scale}\u00d7`,
}))}
/>
@ -371,7 +476,7 @@ const ImageExportModal = ({
className="ImageExportModal__settings__buttons__button"
label={t("imageExportDialog.title.exportToPng")}
onClick={() =>
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
onExportImage(EXPORT_IMAGE_TYPES.png, state.exportedElements)
}
startIcon={downloadIcon}
>
@ -381,7 +486,7 @@ const ImageExportModal = ({
className="ImageExportModal__settings__buttons__button"
label={t("imageExportDialog.title.exportToSvg")}
onClick={() =>
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
onExportImage(EXPORT_IMAGE_TYPES.svg, state.exportedElements)
}
startIcon={downloadIcon}
>
@ -392,7 +497,10 @@ const ImageExportModal = ({
className="ImageExportModal__settings__buttons__button"
label={t("imageExportDialog.title.copyPngToClipboard")}
onClick={() =>
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
onExportImage(
EXPORT_IMAGE_TYPES.clipboard,
state.exportedElements,
)
}
startIcon={copyIcon}
>