refactor: simplify ImageExportDialog (#6578)

This commit is contained in:
David Luzar 2023-05-13 22:58:35 +02:00 committed by GitHub
parent b1b325b9a7
commit f6f9ed0396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 85 additions and 91 deletions

View File

@ -59,6 +59,7 @@ import {
ELEMENT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT,
ENV, ENV,
EVENT, EVENT,
EXPORT_IMAGE_TYPES,
GRID_SIZE, GRID_SIZE,
IMAGE_MIME_TYPES, IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
@ -82,7 +83,7 @@ import {
VERTICAL_ALIGN, VERTICAL_ALIGN,
ZOOM_STEP, ZOOM_STEP,
} from "../constants"; } from "../constants";
import { loadFromBlob } from "../data"; import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restore, restoreElements } from "../data/restore"; import { restore, restoreElements } from "../data/restore";
import { import {
@ -237,6 +238,7 @@ import {
getShortcutKey, getShortcutKey,
isTransparent, isTransparent,
easeToValuesRAF, easeToValuesRAF,
muteFSAbortError,
} from "../utils"; } from "../utils";
import { import {
ContextMenu, ContextMenu,
@ -251,6 +253,7 @@ import {
generateIdFromFile, generateIdFromFile,
getDataURL, getDataURL,
getFileFromEvent, getFileFromEvent,
isImageFileHandle,
isSupportedImageFile, isSupportedImageFile,
loadSceneOrLibraryFromBlob, loadSceneOrLibraryFromBlob,
normalizeFile, normalizeFile,
@ -618,6 +621,7 @@ class App extends React.Component<AppProps, AppState> {
} }
UIOptions={this.props.UIOptions} UIOptions={this.props.UIOptions}
onImageAction={this.onImageAction} onImageAction={this.onImageAction}
onExportImage={this.onExportImage}
renderWelcomeScreen={ renderWelcomeScreen={
!this.state.isLoading && !this.state.isLoading &&
this.state.showWelcomeScreen && this.state.showWelcomeScreen &&
@ -688,6 +692,37 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
public onExportImage = async (
type: keyof typeof EXPORT_IMAGE_TYPES,
elements: readonly NonDeletedExcalidrawElement[],
) => {
trackEvent("export", type, "ui");
const fileHandle = await exportCanvas(
type,
elements,
this.state,
this.files,
{
exportBackground: this.state.exportBackground,
name: this.state.name,
viewBackgroundColor: this.state.viewBackgroundColor,
},
)
.catch(muteFSAbortError)
.catch((error) => {
console.error(error);
this.setState({ errorMessage: error.message });
});
if (
this.state.exportEmbedScene &&
fileHandle &&
isImageFileHandle(fileHandle)
) {
this.setState({ fileHandle });
}
};
private syncActionResult = withBatchedUpdates( private syncActionResult = withBatchedUpdates(
(actionResult: ActionResult) => { (actionResult: ActionResult) => {
if (this.unmounted || actionResult === false) { if (this.unmounted || actionResult === false) {

View File

@ -4,13 +4,17 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { BinaryFiles, UIAppState } from "../types"; import { AppClassProperties, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { clipboard } from "./icons"; import { clipboard } from "./icons";
import Stack from "./Stack"; import Stack from "./Stack";
import OpenColor from "open-color"; import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; import {
DEFAULT_EXPORT_PADDING,
EXPORT_IMAGE_TYPES,
isFirefox,
} from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { exportToCanvas } from "../packages/utils"; import { exportToCanvas } from "../packages/utils";
@ -65,21 +69,14 @@ const ImageExportModal = ({
elements, elements,
appState, appState,
files, files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager, actionManager,
onExportToPng, onExportImage,
onExportToSvg,
onExportToClipboard,
}: { }: {
appState: UIAppState; appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles; files: BinaryFiles;
exportPadding?: number;
actionManager: ActionManager; actionManager: ActionManager;
onExportToPng: ExportCB; onExportImage: AppClassProperties["onExportImage"];
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onCloseRequest: () => void;
}) => { }) => {
const someElementIsSelected = isSomeElementSelected(elements, appState); const someElementIsSelected = isSomeElementSelected(elements, appState);
const [exportSelected, setExportSelected] = useState(someElementIsSelected); const [exportSelected, setExportSelected] = useState(someElementIsSelected);
@ -90,10 +87,6 @@ const ImageExportModal = ({
? getSelectedElements(elements, appState, true) ? getSelectedElements(elements, appState, true)
: elements; : elements;
useEffect(() => {
setExportSelected(someElementIsSelected);
}, [someElementIsSelected]);
useEffect(() => { useEffect(() => {
const previewNode = previewRef.current; const previewNode = previewRef.current;
if (!previewNode) { if (!previewNode) {
@ -107,7 +100,7 @@ const ImageExportModal = ({
elements: exportedElements, elements: exportedElements,
appState, appState,
files, files,
exportPadding, exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight: maxWidth, maxWidthOrHeight: maxWidth,
}) })
.then((canvas) => { .then((canvas) => {
@ -122,7 +115,7 @@ const ImageExportModal = ({
console.error(error); console.error(error);
setRenderError(error); setRenderError(error);
}); });
}, [appState, files, exportedElements, exportPadding]); }, [appState, files, exportedElements]);
return ( return (
<div className="ExportDialog"> <div className="ExportDialog">
@ -177,7 +170,9 @@ const ImageExportModal = ({
color="indigo" color="indigo"
title={t("buttons.exportToPng")} title={t("buttons.exportToPng")}
aria-label={t("buttons.exportToPng")} aria-label={t("buttons.exportToPng")}
onClick={() => onExportToPng(exportedElements)} onClick={() =>
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
}
> >
PNG PNG
</ExportButton> </ExportButton>
@ -185,7 +180,9 @@ const ImageExportModal = ({
color="red" color="red"
title={t("buttons.exportToSvg")} title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")} aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements)} onClick={() =>
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
}
> >
SVG SVG
</ExportButton> </ExportButton>
@ -194,7 +191,9 @@ const ImageExportModal = ({
{(probablySupportsClipboardBlob || isFirefox) && ( {(probablySupportsClipboardBlob || isFirefox) && (
<ExportButton <ExportButton
title={t("buttons.copyPngToClipboard")} title={t("buttons.copyPngToClipboard")}
onClick={() => onExportToClipboard(exportedElements)} onClick={() =>
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
}
color="gray" color="gray"
shade={7} shade={7}
> >
@ -209,45 +208,31 @@ const ImageExportModal = ({
export const ImageExportDialog = ({ export const ImageExportDialog = ({
elements, elements,
appState, appState,
setAppState,
files, files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager, actionManager,
onExportToPng, onExportImage,
onExportToSvg, onCloseRequest,
onExportToClipboard,
}: { }: {
appState: UIAppState; appState: UIAppState;
setAppState: React.Component<any, UIAppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles; files: BinaryFiles;
exportPadding?: number;
actionManager: ActionManager; actionManager: ActionManager;
onExportToPng: ExportCB; onExportImage: AppClassProperties["onExportImage"];
onExportToSvg: ExportCB; onCloseRequest: () => void;
onExportToClipboard: ExportCB;
}) => { }) => {
const handleClose = React.useCallback(() => { if (appState.openDialog !== "imageExport") {
setAppState({ openDialog: null }); return null;
}, [setAppState]); }
return ( return (
<> <Dialog onCloseRequest={onCloseRequest} title={t("buttons.exportImage")}>
{appState.openDialog === "imageExport" && ( <ImageExportModal
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}> elements={elements}
<ImageExportModal appState={appState}
elements={elements} files={files}
appState={appState} actionManager={actionManager}
files={files} onExportImage={onExportImage}
exportPadding={exportPadding} />
actionManager={actionManager} </Dialog>
onExportToPng={onExportToPng}
onExportToSvg={onExportToSvg}
onExportToClipboard={onExportToClipboard}
onCloseRequest={handleClose}
/>
</Dialog>
)}
</>
); );
}; };

View File

@ -2,23 +2,22 @@ import clsx from "clsx";
import React from "react"; import React from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants"; import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
import { exportCanvas } from "../data";
import { isTextElement, showSelectedShapeActions } from "../element"; import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n"; import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { ExportType } from "../scene/types";
import { import {
AppProps, AppProps,
AppState, AppState,
ExcalidrawProps, ExcalidrawProps,
BinaryFiles, BinaryFiles,
UIAppState, UIAppState,
AppClassProperties,
} from "../types"; } from "../types";
import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils"; import { capitalizeString, isShallowEqual } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { ErrorDialog } from "./ErrorDialog"; import { ErrorDialog } from "./ErrorDialog";
import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; import { ImageExportDialog } from "./ImageExportDialog";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
import { HintViewer } from "./HintViewer"; import { HintViewer } from "./HintViewer";
import { Island } from "./Island"; import { Island } from "./Island";
@ -31,7 +30,6 @@ import { HelpDialog } from "./HelpDialog";
import Stack from "./Stack"; import Stack from "./Stack";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import { JSONExportDialog } from "./JSONExportDialog"; import { JSONExportDialog } from "./JSONExportDialog";
import { isImageFileHandle } from "../data/blob";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
@ -69,6 +67,7 @@ interface LayerUIProps {
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
children?: React.ReactNode; children?: React.ReactNode;
} }
@ -114,6 +113,7 @@ const LayerUI = ({
renderCustomStats, renderCustomStats,
UIOptions, UIOptions,
onImageAction, onImageAction,
onExportImage,
renderWelcomeScreen, renderWelcomeScreen,
children, children,
}: LayerUIProps) => { }: LayerUIProps) => {
@ -143,47 +143,14 @@ const LayerUI = ({
return null; return null;
} }
const createExporter =
(type: ExportType): ExportCB =>
async (exportedElements) => {
trackEvent("export", type, "ui");
const fileHandle = await exportCanvas(
type,
exportedElements,
// FIXME once we split UI canvas from element canvas
appState as AppState,
files,
{
exportBackground: appState.exportBackground,
name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor,
},
)
.catch(muteFSAbortError)
.catch((error) => {
console.error(error);
setAppState({ errorMessage: error.message });
});
if (
appState.exportEmbedScene &&
fileHandle &&
isImageFileHandle(fileHandle)
) {
setAppState({ fileHandle });
}
};
return ( return (
<ImageExportDialog <ImageExportDialog
elements={elements} elements={elements}
appState={appState} appState={appState}
setAppState={setAppState}
files={files} files={files}
actionManager={actionManager} actionManager={actionManager}
onExportToPng={createExporter("png")} onExportImage={onExportImage}
onExportToSvg={createExporter("svg")} onCloseRequest={() => setAppState({ openDialog: null })}
onExportToClipboard={createExporter("clipboard")}
/> />
); );
}; };

View File

@ -131,6 +131,12 @@ export const MIME_TYPES = {
...IMAGE_MIME_TYPES, ...IMAGE_MIME_TYPES,
} as const; } as const;
export const EXPORT_IMAGE_TYPES = {
png: "png",
svg: "svg",
clipboard: "clipboard",
} as const;
export const EXPORT_DATA_TYPES = { export const EXPORT_DATA_TYPES = {
excalidraw: "excalidraw", excalidraw: "excalidraw",
excalidrawClipboard: "excalidraw/clipboard", excalidrawClipboard: "excalidraw/clipboard",

View File

@ -442,6 +442,7 @@ export type AppClassProperties = {
pasteFromClipboard: App["pasteFromClipboard"]; pasteFromClipboard: App["pasteFromClipboard"];
id: App["id"]; id: App["id"];
onInsertElements: App["onInsertElements"]; onInsertElements: App["onInsertElements"];
onExportImage: App["onExportImage"];
}; };
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{