diff --git a/assets/icons/3c73ee8caf56fcc08bd11d595dca167d.png b/assets/icons/3c73ee8caf56fcc08bd11d595dca167d.png new file mode 100644 index 0000000..ad6d0c6 Binary files /dev/null and b/assets/icons/3c73ee8caf56fcc08bd11d595dca167d.png differ diff --git a/assets/icons/IdeaDrawnNewLogo_transparent.png b/assets/icons/IdeaDrawnNewLogo_transparent.png new file mode 100644 index 0000000..6d9a791 Binary files /dev/null and b/assets/icons/IdeaDrawnNewLogo_transparent.png differ diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 0000000..ee7888a --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg new file mode 100644 index 0000000..0de353f --- /dev/null +++ b/assets/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/down.svg b/assets/icons/down.svg new file mode 100644 index 0000000..003a983 --- /dev/null +++ b/assets/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/exclamation.png b/assets/icons/exclamation.png new file mode 100644 index 0000000..327868e Binary files /dev/null and b/assets/icons/exclamation.png differ diff --git a/assets/icons/icons_151918.png b/assets/icons/icons_151918.png new file mode 100644 index 0000000..5881f9d Binary files /dev/null and b/assets/icons/icons_151918.png differ diff --git a/assets/icons/icons_19028.png b/assets/icons/icons_19028.png new file mode 100644 index 0000000..f21ed64 Binary files /dev/null and b/assets/icons/icons_19028.png differ diff --git a/assets/icons/icons_417061.png b/assets/icons/icons_417061.png new file mode 100644 index 0000000..7cafcf7 Binary files /dev/null and b/assets/icons/icons_417061.png differ diff --git a/assets/icons/icons_515682.png b/assets/icons/icons_515682.png new file mode 100644 index 0000000..4236cc3 Binary files /dev/null and b/assets/icons/icons_515682.png differ diff --git a/assets/icons/image.png b/assets/icons/image.png new file mode 100644 index 0000000..01c7bf6 Binary files /dev/null and b/assets/icons/image.png differ diff --git a/assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg b/assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg new file mode 100644 index 0000000..4b645a8 --- /dev/null +++ b/assets/icons/search_35dp_E8EAED_FILL0_wght700_GRAD200_opsz40.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/Canvas/Canvas.tsx b/components/Canvas/Canvas.tsx index 45e27ec..88da7d9 100644 --- a/components/Canvas/Canvas.tsx +++ b/components/Canvas/Canvas.tsx @@ -6,12 +6,12 @@ import useIndexed from "../../state/hooks/useIndexed"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; import useLayerReferences from "../../state/hooks/useLayerReferences"; import useStore from "../../state/hooks/useStore"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; import clsx from "clsx"; // Types import type { FC, MouseEvent } from "react"; -import type { Layer, Coordinates } from "../../types"; +import type { Layer, Coordinates, CanvasFile } from "../../types"; // Styles import "./Canvas.styles.css"; @@ -24,6 +24,7 @@ type DBLayer = { image: Blob; name: string; position: number; + id: string; }; const Canvas: FC = ({ isGrabbing }) => { @@ -59,8 +60,8 @@ const Canvas: FC = ({ isGrabbing }) => { })) ); - const { references, add, remove } = useLayerReferences(); - const { get } = useIndexed(); + const { references, add, getActiveLayer } = useLayerReferences(); + const { get, remove } = useIndexed(); const isDrawing = useRef(false); const isMovingElement = useStoreSubscription((state) => state.elementMoving); @@ -238,9 +239,62 @@ const Canvas: FC = ({ isGrabbing }) => { // TODO: Improve this implementation of updating the layers from the storage. useEffect(() => { async function updateLayers() { - const entries = await get<[string, DBLayer][]>("layers"); + const urlParams = new URLSearchParams(window.location.search); + const fileId = urlParams.get("f"); + // The open query parameter is used to determine if the file + // was created from opening a file from the local computer. + const open = urlParams.get("open"); + + if (!fileId) { + // Simply do nothing. We want to redirect if there is no file. + // This is handled in the Page component for the editor route. + return; + } + + const entries = await get("layers", fileId); if (!entries) { + // We're in a new file. We have one layer by default, + // so we'll render the opened file with that layer. + + if (!open) { + return; + } + + const file = await get("files", fileId); + + if (!file) { + console.error( + "Tried to get file from temporary storage, there was no file." + ); + } else { + const ref = getActiveLayer(); + const canvasWidth = Number(ref.style.width.replace("px", "")); + const canvasHeight = Number(ref.style.height.replace("px", "")); + const ctx = ref.getContext("2d"); + const img = new Image(); + + img.width = canvasWidth; + img.height = canvasHeight; + + img.onload = () => { + ctx!.drawImage(img, 0, 0, canvasWidth, canvasHeight); + URL.revokeObjectURL(img.src); + + // A custom event to notify that the image of the layer + // displayed in the layer list needs to be updated. + + const ev = new CustomEvent("imageupdate", { + detail: { + layer: ref + } + }); + + document.dispatchEvent(ev); + }; + + img.src = URL.createObjectURL(file.file); + } return; } @@ -248,18 +302,18 @@ const Canvas: FC = ({ isGrabbing }) => { updateLayerContents(newEntries); } - function updateLayerState(entries: [string, DBLayer][]) { - return new Promise<[string, DBLayer][]>((resolve) => { + function updateLayerState(entries: DBLayer[]) { + return new Promise((resolve) => { const newLayers: Layer[] = []; - const sorted = entries.sort((a, b) => b[1].position - a[1].position); // Sort by position, where the highest position is the top layer. + const sorted = entries.sort((a, b) => b.position - a.position); // Sort by position, where the highest position is the top layer. sorted.forEach((entry, i) => { - const [layerId, layer] = entry; + const { name, id } = entry; newLayers.push({ - name: layer.name, - id: layerId, + name: name, + id: id, active: i === 0, hidden: false }); @@ -275,10 +329,10 @@ const Canvas: FC = ({ isGrabbing }) => { }); } - function updateLayerContents(entries: [string, DBLayer][]) { + function updateLayerContents(entries: DBLayer[]) { entries.forEach((entry) => { - const [, layer] = entry; - const canvas = references.current[layer.position]; + const { position, image } = entry; + const canvas = references.current[position]; if (!canvas) return; @@ -300,12 +354,12 @@ const Canvas: FC = ({ isGrabbing }) => { document.dispatchEvent(ev); }; - img.src = URL.createObjectURL(layer.image); + img.src = URL.createObjectURL(image); }); } updateLayers(); - }, [setLayers, get, references]); + }, [setLayers, get, references, getActiveLayer, remove]); useEffect(() => { const refs = references.current; @@ -350,9 +404,9 @@ const Canvas: FC = ({ isGrabbing }) => { transform }} ref={(element) => { - if (element !== null) { - add(element, i); - } + if (element !== null) { + add(element, i); + } }} id={layer.id} width={width * dpi} diff --git a/components/CanvasPane/CanvasPane.tsx b/components/CanvasPane/CanvasPane.tsx index aeb616e..35af2d8 100644 --- a/components/CanvasPane/CanvasPane.tsx +++ b/components/CanvasPane/CanvasPane.tsx @@ -20,14 +20,17 @@ import type { Coordinates } from "../../types"; // Styles import "./CanvasPane.styles.css"; +import ScaleIndicator from "../ScaleIndicator/ScaleIndicator"; const MemoizedShapeElement = memo(ShapeElement); const MemoizedCanvas = memo(Canvas); const MemoizedDrawingToolbar = memo(DrawingToolbar); +const MemoizedScaleIndicator = memo(ScaleIndicator); const CanvasPane: FC = () => { const { mode, + scale, changeX, changeY, increaseScale, @@ -40,6 +43,7 @@ const CanvasPane: FC = () => { } = useStore( useShallow((state) => ({ mode: state.mode, + scale: state.scale, changeX: state.changeX, changeY: state.changeY, increaseScale: state.increaseScale, @@ -340,6 +344,8 @@ const CanvasPane: FC = () => { > + + ); }; diff --git a/components/CanvasPointerMarker/CanvasPointerMarker.tsx b/components/CanvasPointerMarker/CanvasPointerMarker.tsx index 5885a6f..36b0592 100644 --- a/components/CanvasPointerMarker/CanvasPointerMarker.tsx +++ b/components/CanvasPointerMarker/CanvasPointerMarker.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react"; import useStore from "../../state/hooks/useStore"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; import { useShallow } from "zustand/react/shallow"; -import * as Utils from "../../utils"; +import * as Utils from "../../lib/utils"; // Types import type { FC, RefObject } from "react"; @@ -51,8 +51,7 @@ const CanvasPointerMarker: FC = ({ setIsVisible(false); } - const { x, y, left, top, width, height } = - canvasSpace.getBoundingClientRect(); + const { left, top, width, height } = canvasSpace.getBoundingClientRect(); let newX; let newY; diff --git a/components/CanvasPointerSelection/CanvasPointerSelection.tsx b/components/CanvasPointerSelection/CanvasPointerSelection.tsx index cb96043..afb0daf 100644 --- a/components/CanvasPointerSelection/CanvasPointerSelection.tsx +++ b/components/CanvasPointerSelection/CanvasPointerSelection.tsx @@ -1,6 +1,6 @@ // Lib import { useRef, useEffect, useState } from "react"; -import * as UTILS from "../../utils"; +import * as UTILS from "../../lib/utils"; import useLayerReferences from "../../state/hooks/useLayerReferences"; import useStoreSubscription from "../../state/hooks/useStoreSubscription"; import useStore from "../../state/hooks/useStore"; diff --git a/components/Dropdown/Dropdown.styles.css b/components/Dropdown/Dropdown.styles.css new file mode 100644 index 0000000..79f93cf --- /dev/null +++ b/components/Dropdown/Dropdown.styles.css @@ -0,0 +1,53 @@ +:root { + --description: lightgray; +} + +.desc { + color: var(--description); +} + +.currentFilter { + display: inline-flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 0 10px; +} +.dropdownFilter { + opacity: 0; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + z-index: 888; + width: 10rem; + background: rgba(0, 0, 0, 0.9); + border: solid 1px rgba(255, 255, 255, 0.2); + border-radius: 3px; + padding-block: 10px; + padding-inline: 1rem; + padding-inline-end: 0; + position: absolute; + transition: all 0.3s ease; + pointer-events: none; +} + +.filter-item { + width: fit-content; + background: none; + border: none; + padding-block: 5px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.active { + opacity: 1; + transform: translateY(10px); + pointer-events: all; +} diff --git a/components/Dropdown/Dropdown.tsx b/components/Dropdown/Dropdown.tsx new file mode 100644 index 0000000..1ada09f --- /dev/null +++ b/components/Dropdown/Dropdown.tsx @@ -0,0 +1,84 @@ +// Lib +import check from "../../assets/icons/check.svg"; +import down from "../../assets/icons/down.svg"; +import { useState } from "react"; + +// Types +import type { FC, MouseEvent } from "react"; +import { Option } from "../../types"; + +// Styles +import "./Dropdown.styles.css"; + +type DropdownProps = { + description: string; + options: Option[]; + onSelect?: (option: Option) => void; +}; + +const Dropdown: FC = ({ + description = "Default", + options = [], + onSelect +}) => { + const [selected, setSelected] = useState