Revert "feat: rewrite preview to use React.Suspense"

This reverts commit cd021716f1.
This commit is contained in:
Arnošt Pleskot 2023-09-25 12:50:03 +02:00
parent cd021716f1
commit 7e7d3e0514
No known key found for this signature in database
3 changed files with 97 additions and 179 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useReducer, useRef } from "react";
import React, { useEffect, useReducer, useRef, useState } from "react";
import clsx from "clsx";
import type { ActionManager } from "../actions/manager";
@ -21,6 +21,7 @@ import {
FANCY_BACKGROUND_IMAGES,
} from "../constants";
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import {
ExcalidrawElement,
@ -28,7 +29,7 @@ import {
} from "../element/types";
import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getScaleToFit } from "../packages/utils";
import { exportToCanvas, getScaleToFit } from "../packages/utils";
import { copyIcon, downloadIcon, helpIcon } from "./icons";
import { Dialog } from "./Dialog";
@ -49,7 +50,6 @@ import {
import { getFancyBackgroundPadding } from "../scene/fancyBackground";
import { Select } from "./Select";
import { Bounds } from "../element/bounds";
import { CanvasPreview } from "./ImageExportPreview";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
@ -61,6 +61,18 @@ const fancyBackgroundImageOptions = Object.entries(FANCY_BACKGROUND_IMAGES).map(
}),
);
export const ErrorCanvasPreview = () => {
return (
<div>
<h3>{t("canvasError.cannotShowPreview")}</h3>
<p>
<span>{t("canvasError.canvasTooBig")}</span>
</p>
<em>({t("canvasError.canvasTooBigTip")})</em>
</div>
);
};
type ImageExportModalProps = {
appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
@ -216,6 +228,7 @@ const ImageExportModal = ({
const appProps = useAppProps();
const previewRef = useRef<HTMLDivElement>(null);
const [renderError, setRenderError] = useState<Error | null>(null);
// Upscale exported image when is smaller than preview
useEffect(() => {
@ -268,17 +281,91 @@ const ImageExportModal = ({
actionManager,
]);
useEffect(() => {
const previewNode = previewRef.current;
if (!previewNode) {
return;
}
const maxWidth = previewNode.offsetWidth;
const maxHeight = previewNode.offsetHeight;
const maxWidthOrHeight = Math.min(maxWidth, maxHeight);
if (!maxWidth) {
return;
}
// when switching between solid/no background and image background, we clear the canvas to prevent flickering
const isExportWithFancyBackground =
appState.exportBackground && appState.fancyBackgroundImageKey !== "solid";
if (state.isExportWithFancyBackground !== isExportWithFancyBackground) {
const existingCanvas = previewNode.querySelector("canvas");
if (existingCanvas) {
const context = existingCanvas.getContext("2d");
context!.clearRect(0, 0, existingCanvas.width, existingCanvas.height);
}
dispatch({
type: "SET_IS_EXPORT_WITH_FANCY_BACKGROUND",
isExportWithFancyBackground,
});
}
exportToCanvas({
elements: state.exportedElements,
appState,
files,
exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight,
})
.then((canvas) => {
setRenderError(null);
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
return canvasToBlob(canvas).then(() => {
const existingCanvas = previewNode.querySelector("canvas");
if (!existingCanvas) {
previewNode.appendChild(canvas);
return;
}
existingCanvas.width = canvas.width;
existingCanvas.height = canvas.height;
const context = existingCanvas.getContext("2d");
context!.drawImage(canvas, 0, 0);
});
// Get the 2D rendering context of the existing canvas
})
.catch((error) => {
console.error(error);
setRenderError(error);
});
}, [
appState,
appState.exportBackground,
appState.fancyBackgroundImageKey,
files,
state.exportedElements,
state.isExportWithFancyBackground,
]);
return (
<div className="ImageExportModal">
<h3>{t("imageExportDialog.header")}</h3>
<div className="ImageExportModal__preview">
<React.Suspense fallback={<div>Loading...</div>}>
<CanvasPreview
appState={appState}
files={files}
elements={state.exportedElements}
/>
</React.Suspense>
<div
className={clsx("ImageExportModal__preview__canvas", {
"ImageExportModal__preview__canvas--img-bcg":
appState.exportBackground &&
appState.fancyBackgroundImageKey !== "solid",
})}
ref={previewRef}
>
{renderError && <ErrorCanvasPreview />}
</div>
</div>
<div className="ImageExportModal__settings">
<h3>{t("imageExportDialog.header")}</h3>

View File

@ -1,106 +0,0 @@
import clsx from "clsx";
import { useEffect, useRef } from "react";
import { DEFAULT_EXPORT_PADDING } from "../constants";
import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { useSuspendable } from "../hooks/useSuspendable";
import { t } from "../i18n";
import { exportToCanvas } from "../packages/utils";
import { BinaryFiles, UIAppState } from "../types";
const ErrorCanvasPreview = () => {
return (
<div>
<h3>{t("canvasError.cannotShowPreview")}</h3>
<p>
<span>{t("canvasError.canvasTooBig")}</span>
</p>
<em>({t("canvasError.canvasTooBigTip")})</em>
</div>
);
};
type CanvasPreviewProps = {
appState: UIAppState;
files: BinaryFiles;
elements: readonly NonDeletedExcalidrawElement[];
};
export const CanvasPreview = ({
appState,
files,
elements,
}: CanvasPreviewProps) => {
const [canvasData, canvasError, canvasStatus, suspendCanvas, pendingPromise] =
useSuspendable<HTMLCanvasElement>();
const previewRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const previewNode = previewRef.current;
if (!previewNode) {
return;
}
const maxWidth = previewNode.offsetWidth;
const maxHeight = previewNode.offsetHeight;
const maxWidthOrHeight = Math.min(maxWidth, maxHeight);
if (!maxWidth) {
return;
}
const promise = exportToCanvas({
elements,
appState,
files,
exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight,
}).then((canvas) => {
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
return canvasToBlob(canvas).then(() => {
return canvas;
});
});
suspendCanvas(promise);
}, [appState, files, elements, suspendCanvas]);
useEffect(() => {
if (!canvasData || !canvasRef.current) {
return;
}
const canvas = canvasRef.current;
canvas.width = canvasData.width;
canvas.height = canvasData.height;
const context = canvas.getContext("2d");
context!.drawImage(canvasData, 0, 0);
}, [canvasData]);
if (canvasStatus === "pending" && pendingPromise) {
throw pendingPromise;
}
if (canvasStatus === "rejected") {
console.error(canvasError);
return <ErrorCanvasPreview />;
}
return (
<div
className={clsx("ImageExportModal__preview__canvas", {
"ImageExportModal__preview__canvas--img-bcg":
appState.exportBackground &&
appState.fancyBackgroundImageKey !== "solid",
})}
ref={previewRef}
>
<canvas ref={canvasRef} />
</div>
);
};

View File

@ -1,63 +0,0 @@
import { useReducer, useCallback, useRef } from "react";
type Status = "idle" | "pending" | "resolved" | "rejected";
type Action<T> =
| { type: "start" }
| { type: "resolve"; payload: T }
| { type: "reject"; payload: Error };
type State<T> = {
status: Status;
result: T | null;
error: Error | null;
};
function reducer<T>(state: State<T>, action: Action<T>): State<T> {
switch (action.type) {
case "start":
return { ...state, status: "pending" };
case "resolve":
return { status: "resolved", result: action.payload, error: null };
case "reject":
return { status: "rejected", result: null, error: action.payload };
default:
throw new Error("Unhandled action type");
}
}
export function useSuspendable<T>(): [
T | null,
Error | null,
Status,
(promise: Promise<T>) => Promise<void>,
Promise<T> | null,
] {
const [state, dispatch] = useReducer(reducer, {
status: "idle",
result: null,
error: null,
});
const pendingPromise = useRef<Promise<T> | null>(null);
const suspend = useCallback((promise: Promise<T>) => {
pendingPromise.current = promise;
dispatch({ type: "start" });
return promise
.then((data) => {
dispatch({ type: "resolve", payload: data });
})
.catch((error) => {
dispatch({ type: "reject", payload: error as Error });
});
}, []);
return [
state.result as T | null,
state.error,
state.status,
suspend,
pendingPromise.current,
];
}