Compare commits
3 Commits
master
...
dwelle/sup
Author | SHA1 | Date | |
---|---|---|---|
|
ba951b9a03 | ||
|
6fb5c2acda | ||
|
df9af0052d |
@ -14,7 +14,7 @@ import {
|
|||||||
hasText,
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppState, Zoom } from "../types";
|
import { AppProps, AppState, CanvasActions, Zoom } from "../types";
|
||||||
import {
|
import {
|
||||||
capitalizeString,
|
capitalizeString,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
@ -213,15 +213,25 @@ export const ShapesSwitcher = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
appState,
|
appState,
|
||||||
|
UIOptions,
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
activeTool: AppState["activeTool"];
|
activeTool: AppState["activeTool"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
|
UIOptions: AppProps["UIOptions"];
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
|
if (
|
||||||
|
UIOptions.canvasActions.tools[
|
||||||
|
value as Extract<typeof value, keyof CanvasActions["tools"]>
|
||||||
|
] === false
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const letter =
|
const letter =
|
||||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||||
|
@ -205,6 +205,7 @@ import {
|
|||||||
PointerDownState,
|
PointerDownState,
|
||||||
SceneData,
|
SceneData,
|
||||||
Device,
|
Device,
|
||||||
|
CanvasActions,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
@ -292,6 +293,7 @@ import {
|
|||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
||||||
|
import { ImageSceneDataError } from "../errors";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
@ -1547,6 +1549,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||||
|
if (!this.isToolSupported("image")) {
|
||||||
|
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
{ clientX: cursorX, clientY: cursorY },
|
{ clientX: cursorX, clientY: cursorY },
|
||||||
this.state,
|
this.state,
|
||||||
@ -2343,11 +2349,32 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// we pruposefuly widen tyhe `tool` type so this helper can be called with
|
||||||
|
// any tool without having to type check it
|
||||||
|
private isToolSupported = <
|
||||||
|
T extends typeof SHAPES[number]["value"] | "eraser" | "hand" | "custom",
|
||||||
|
>(
|
||||||
|
tool: T,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
this.props.UIOptions.canvasActions.tools[
|
||||||
|
tool as Extract<T, keyof CanvasActions["tools"]>
|
||||||
|
] !== false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private setActiveTool = (
|
private setActiveTool = (
|
||||||
tool:
|
tool:
|
||||||
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
|
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
|
||||||
| { type: "custom"; customType: string },
|
| { type: "custom"; customType: string },
|
||||||
) => {
|
) => {
|
||||||
|
if (!this.isToolSupported(tool.type)) {
|
||||||
|
console.warn(
|
||||||
|
`"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||||
if (nextActiveTool.type === "hand") {
|
if (nextActiveTool.type === "hand") {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||||
@ -5994,7 +6021,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const { file, fileHandle } = await getFileFromEvent(event);
|
const { file, fileHandle } = await getFileFromEvent(event);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isSupportedImageFile(file)) {
|
// if image tool not supported, don't show an error here and let it fall
|
||||||
|
// through so we still support importing scene data from images. If no
|
||||||
|
// scene data encoded, we'll show an error then
|
||||||
|
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
|
||||||
// first attempt to decode scene from the image if it's embedded
|
// first attempt to decode scene from the image if it's embedded
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
@ -6103,6 +6133,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error instanceof ImageSceneDataError &&
|
||||||
|
error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
|
||||||
|
!this.isToolSupported("image")
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: t("errors.imageToolNotSupported"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({ isLoading: false, errorMessage: error.message });
|
this.setState({ isLoading: false, errorMessage: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -305,6 +305,7 @@ const LayerUI = ({
|
|||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
UIOptions={UIOptions}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
@ -408,6 +409,7 @@ const LayerUI = ({
|
|||||||
renderSidebars={renderSidebars}
|
renderSidebars={renderSidebars}
|
||||||
device={device}
|
device={device}
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
|
UIOptions={UIOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState, Device, ExcalidrawProps } from "../types";
|
import { AppProps, AppState, Device, ExcalidrawProps } from "../types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
@ -42,6 +42,7 @@ type MobileMenuProps = {
|
|||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
|
UIOptions: AppProps["UIOptions"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@ -59,6 +60,7 @@ export const MobileMenu = ({
|
|||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
|
UIOptions,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
@ -82,6 +84,7 @@ export const MobileMenu = ({
|
|||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
UIOptions={UIOptions}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
|
@ -164,6 +164,9 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
saveToActiveFile: true,
|
saveToActiveFile: true,
|
||||||
toggleTheme: null,
|
toggleTheme: null,
|
||||||
saveAsImage: true,
|
saveAsImage: true,
|
||||||
|
tools: {
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState";
|
|||||||
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement, FileId } from "../element/types";
|
import { ExcalidrawElement, FileId } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError, ImageSceneDataError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState, DataURL, LibraryItem } from "../types";
|
import { AppState, DataURL, LibraryItem } from "../types";
|
||||||
@ -24,15 +24,12 @@ const parseFileContents = async (blob: Blob | File) => {
|
|||||||
).decodePngMetadata(blob);
|
).decodePngMetadata(blob);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message === "INVALID") {
|
if (error.message === "INVALID") {
|
||||||
throw new DOMException(
|
throw new ImageSceneDataError(
|
||||||
t("alerts.imageDoesNotContainScene"),
|
t("alerts.imageDoesNotContainScene"),
|
||||||
"EncodingError",
|
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new DOMException(
|
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
|
||||||
t("alerts.cannotRestoreFromImage"),
|
|
||||||
"EncodingError",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -58,15 +55,12 @@ const parseFileContents = async (blob: Blob | File) => {
|
|||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message === "INVALID") {
|
if (error.message === "INVALID") {
|
||||||
throw new DOMException(
|
throw new ImageSceneDataError(
|
||||||
t("alerts.imageDoesNotContainScene"),
|
t("alerts.imageDoesNotContainScene"),
|
||||||
"EncodingError",
|
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new DOMException(
|
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
|
||||||
t("alerts.cannotRestoreFromImage"),
|
|
||||||
"EncodingError",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,19 @@ export class AbortError extends DOMException {
|
|||||||
super(message, "AbortError");
|
super(message, "AbortError");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageSceneDataErrorCode =
|
||||||
|
| "IMAGE_NOT_CONTAINS_SCENE_DATA"
|
||||||
|
| "IMAGE_SCENE_DATA_ERROR";
|
||||||
|
|
||||||
|
export class ImageSceneDataError extends Error {
|
||||||
|
public code;
|
||||||
|
constructor(
|
||||||
|
message = "Image Scene Data Error",
|
||||||
|
code: ImageSceneDataErrorCode = "IMAGE_SCENE_DATA_ERROR",
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "EncodingError";
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -618,6 +618,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
UIOptions={{
|
UIOptions={{
|
||||||
canvasActions: {
|
canvasActions: {
|
||||||
|
tools: { image: false },
|
||||||
toggleTheme: true,
|
toggleTheme: true,
|
||||||
export: {
|
export: {
|
||||||
onExportToBackend,
|
onExportToBackend,
|
||||||
|
@ -206,6 +206,7 @@
|
|||||||
"importLibraryError": "Couldn't load library",
|
"importLibraryError": "Couldn't load library",
|
||||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
||||||
|
"imageToolNotSupported": "Images are disabled.",
|
||||||
"brave_measure_text_error": {
|
"brave_measure_text_error": {
|
||||||
"start": "Looks like you are using Brave browser with the",
|
"start": "Looks like you are using Brave browser with the",
|
||||||
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
|
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
|
||||||
|
@ -52,6 +52,10 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
canvasActions: {
|
canvasActions: {
|
||||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||||
...canvasActions,
|
...canvasActions,
|
||||||
|
tools: {
|
||||||
|
...DEFAULT_UI_OPTIONS.canvasActions.tools,
|
||||||
|
...props.UIOptions?.canvasActions?.tools,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -375,7 +375,7 @@ export type ExportOpts = {
|
|||||||
// truthiness value will determine whether the action is rendered or not
|
// truthiness value will determine whether the action is rendered or not
|
||||||
// (see manager renderAction). We also override canvasAction values in
|
// (see manager renderAction). We also override canvasAction values in
|
||||||
// excalidraw package index.tsx.
|
// excalidraw package index.tsx.
|
||||||
type CanvasActions = Partial<{
|
export type CanvasActions = Partial<{
|
||||||
changeViewBackgroundColor: boolean;
|
changeViewBackgroundColor: boolean;
|
||||||
clearCanvas: boolean;
|
clearCanvas: boolean;
|
||||||
export: false | ExportOpts;
|
export: false | ExportOpts;
|
||||||
@ -383,6 +383,9 @@ type CanvasActions = Partial<{
|
|||||||
saveToActiveFile: boolean;
|
saveToActiveFile: boolean;
|
||||||
toggleTheme: boolean | null;
|
toggleTheme: boolean | null;
|
||||||
saveAsImage: boolean;
|
saveAsImage: boolean;
|
||||||
|
tools: {
|
||||||
|
image: boolean;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type UIOptions = Partial<{
|
type UIOptions = Partial<{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user