More typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-09 16:22:35 +10:00
parent 45a4443dd1
commit ecfab87aa0
66 changed files with 1350 additions and 1233 deletions

View File

@ -47,7 +47,7 @@
"react-markdown": "4",
"react-media": "^2.0.0-rc.1",
"react-modal": "^3.12.1",
"react-resize-detector": "4.2.3",
"react-resize-detector": "^6.7.4",
"react-router-dom": "^5.1.2",
"react-router-hash-link": "^2.2.2",
"react-scripts": "^4.0.3",
@ -63,7 +63,7 @@
"socket.io-client": "^4.1.2",
"socket.io-msgpack-parser": "^3.0.1",
"source-map-explorer": "^2.5.2",
"theme-ui": "^0.8.4",
"theme-ui": "^0.10.0",
"use-image": "^1.0.7",
"uuid": "^8.3.2",
"webrtc-adapter": "^7.7.1"
@ -107,6 +107,7 @@
"@types/react-router-dom": "^5.1.7",
"@types/shortid": "^0.0.29",
"@types/simple-peer": "^9.6.3",
"@types/uuid": "^8.3.1",
"typescript": "^4.2.4",
"worker-loader": "^3.0.8"
}

View File

@ -1,9 +1,8 @@
import React from "react";
import { Box, Input } from "theme-ui";
import { Box, Input, InputProps } from "theme-ui";
import SearchIcon from "../icons/SearchIcon";
function Search(props) {
function Search(props: InputProps) {
return (
<Box sx={{ position: "relative", flexGrow: 1 }}>
<Input

View File

@ -3,7 +3,21 @@ import { IconButton } from "theme-ui";
import Count from "./DiceButtonCount";
function DiceButton({ title, children, count, onClick, disabled }) {
type DiceButtonProps = {
title: string;
children: React.ReactNode;
count?: number;
onClick: React.MouseEventHandler<HTMLButtonElement>;
disabled: boolean;
};
function DiceButton({
title,
children,
count,
onClick,
disabled,
}: DiceButtonProps) {
return (
<IconButton
title={title}

View File

@ -1,7 +1,7 @@
import React from "react";
import { Box, Text } from "theme-ui";
function DiceButtonCount({ children }) {
function DiceButtonCount({ children }: { children: React.ReactNode }) {
return (
<Box
sx={{

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Flex, IconButton, Box } from "theme-ui";
import SimpleBar from "simplebar-react";
@ -21,6 +21,20 @@ import Divider from "../Divider";
import { dice } from "../../dice";
import useSetting from "../../hooks/useSetting";
import { DefaultDice, DiceRoll, DiceType } from "../../types/Dice";
import Dice from "../../dice/Dice";
type DiceButtonsProps = {
diceRolls: DiceRoll[];
onDiceAdd: (style: typeof Dice, type: DiceType) => void;
onDiceLoad: (dice: DefaultDice) => void;
diceTraySize: "single" | "double";
onDiceTraySizeChange: (newSize: "single" | "double") => void;
shareDice: boolean;
onShareDiceChange: (value: boolean) => void;
loading: boolean;
};
function DiceButtons({
diceRolls,
onDiceAdd,
@ -30,29 +44,32 @@ function DiceButtons({
shareDice,
onShareDiceChange,
loading,
}) {
}: DiceButtonsProps) {
const [currentDiceStyle, setCurrentDiceStyle] = useSetting("dice.style");
const [currentDice, setCurrentDice] = useState(
dice.find((d) => d.key === currentDiceStyle)
const [currentDice, setCurrentDice] = useState<DefaultDice>(
dice.find((d) => d.key === currentDiceStyle) || dice[0]
);
useEffect(() => {
const initialDice = dice.find((d) => d.key === currentDiceStyle);
onDiceLoad(initialDice);
setCurrentDice(initialDice);
if (initialDice) {
onDiceLoad(initialDice);
setCurrentDice(initialDice);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const diceCounts = {};
const diceCounts: Partial<Record<DiceType, number>> = {};
for (let dice of diceRolls) {
if (dice.type in diceCounts) {
diceCounts[dice.type] += 1;
// TODO: Check type
diceCounts[dice.type]! += 1;
} else {
diceCounts[dice.type] = 1;
}
}
async function handleDiceChange(dice) {
async function handleDiceChange(dice: DefaultDice) {
await onDiceLoad(dice);
setCurrentDice(dice);
setCurrentDiceStyle(dice.key);

View File

@ -1,9 +1,10 @@
import React, { useRef, useEffect, useState } from "react";
import { useRef, useEffect, useState } from "react";
import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math";
import { AmmoJSPlugin } from "@babylonjs/core/Physics/Plugins/ammoJSPlugin";
import { TargetCamera } from "@babylonjs/core/Cameras/targetCamera";
//@ts-ignore
import * as AMMO from "ammo.js";
import "@babylonjs/core/Physics/physicsEngineComponent";
@ -19,20 +20,44 @@ import ReactResizeDetector from "react-resize-detector";
import usePreventTouch from "../../hooks/usePreventTouch";
import ErrorBanner from "../banner/ErrorBanner";
import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh";
const diceThrowSpeed = 2;
function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const [error, setError] = useState();
type DiceInteractionProps = {
onSceneMount?: ({
scene,
engine,
canvas,
}: {
scene: Scene;
engine: Engine;
canvas: HTMLCanvasElement | WebGLRenderingContext;
}) => void;
onPointerDown: () => void;
onPointerUp: () => any;
};
const sceneRef = useRef();
const engineRef = useRef();
const canvasRef = useRef();
const containerRef = useRef();
function DiceInteraction({
onSceneMount,
onPointerDown,
onPointerUp,
}: DiceInteractionProps) {
const [error, setError] = useState<Error | undefined>();
const sceneRef = useRef<Scene>();
const engineRef = useRef<Engine>();
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
try {
const canvas = canvasRef.current;
const engine = new Engine(canvas, true, {
preserveDrawingBuffer: true,
stencil: true,
@ -67,13 +92,14 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const delta = newPosition.subtract(currentPosition);
selectedMesh.setAbsolutePosition(newPosition);
const velocity = delta.scale(1000 / scene.deltaTime);
selectedMeshVelocityWindowRef.current = selectedMeshVelocityWindowRef.current.slice(
Math.max(
selectedMeshVelocityWindowRef.current.length -
selectedMeshVelocityWindowSize,
0
)
);
selectedMeshVelocityWindowRef.current =
selectedMeshVelocityWindowRef.current.slice(
Math.max(
selectedMeshVelocityWindowRef.current.length -
selectedMeshVelocityWindowSize,
0
)
);
selectedMeshVelocityWindowRef.current.push(velocity);
}
});
@ -82,21 +108,27 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
}
}, [onSceneMount]);
const selectedMeshRef = useRef();
const selectedMeshVelocityWindowRef = useRef([]);
const selectedMeshRef = useRef<AbstractMesh | null>(null);
const selectedMeshVelocityWindowRef = useRef<Vector3[]>([]);
const selectedMeshVelocityWindowSize = 4;
const selectedMeshMassRef = useRef();
const selectedMeshMassRef = useRef<number>(0);
function handlePointerDown() {
const scene = sceneRef.current;
if (scene) {
const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
if (pickInfo.hit && pickInfo.pickedMesh.name !== "dice_tray") {
pickInfo.pickedMesh.physicsImpostor.setLinearVelocity(Vector3.Zero());
pickInfo.pickedMesh.physicsImpostor.setAngularVelocity(Vector3.Zero());
if (
pickInfo &&
pickInfo.hit &&
pickInfo.pickedMesh &&
pickInfo.pickedMesh.name !== "dice_tray"
) {
pickInfo.pickedMesh.physicsImpostor?.setLinearVelocity(Vector3.Zero());
pickInfo.pickedMesh.physicsImpostor?.setAngularVelocity(Vector3.Zero());
// Save the meshes mass and set it to 0 so we can pick it up
selectedMeshMassRef.current = pickInfo.pickedMesh.physicsImpostor.mass;
pickInfo.pickedMesh.physicsImpostor.setMass(0);
selectedMeshMassRef.current =
pickInfo.pickedMesh.physicsImpostor?.mass || 0;
pickInfo.pickedMesh.physicsImpostor?.setMass(0);
selectedMeshRef.current = pickInfo.pickedMesh;
}
@ -119,27 +151,29 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
}
// Re-apply the meshes mass
selectedMesh.physicsImpostor.setMass(selectedMeshMassRef.current);
selectedMesh.physicsImpostor.forceUpdate();
selectedMesh.physicsImpostor?.setMass(selectedMeshMassRef.current);
selectedMesh.physicsImpostor?.forceUpdate();
selectedMesh.physicsImpostor.applyImpulse(
selectedMesh.physicsImpostor?.applyImpulse(
velocity.scale(diceThrowSpeed * selectedMesh.physicsImpostor.mass),
selectedMesh.physicsImpostor.getObjectCenter()
);
}
selectedMeshRef.current = null;
selectedMeshVelocityWindowRef.current = [];
selectedMeshMassRef.current = null;
selectedMeshMassRef.current = 0;
onPointerUp();
}
function handleResize(width, height) {
const engine = engineRef.current;
if (engine) {
engine.resize();
canvasRef.current.width = width;
canvasRef.current.height = height;
function handleResize(width?: number, height?: number) {
if (width && height) {
const engine = engineRef.current;
if (engine && canvasRef.current) {
engine.resize();
canvasRef.current.width = width;
canvasRef.current.height = height;
}
}
}
@ -165,7 +199,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
style={{ outline: "none" }}
/>
</ReactResizeDetector>
<ErrorBanner error={error} onRequestClose={() => setError()} />
<ErrorBanner error={error} onRequestClose={() => setError(undefined)} />
</div>
);
}

View File

@ -5,17 +5,28 @@ import ClearDiceIcon from "../../icons/ClearDiceIcon";
import RerollDiceIcon from "../../icons/RerollDiceIcon";
import { getDiceRollTotal } from "../../helpers/dice";
import { DiceRoll } from "../../types/Dice";
const maxDiceRollsShown = 6;
function DiceResults({ diceRolls, onDiceClear, onDiceReroll }) {
type DiceResultsProps = {
diceRolls: DiceRoll[];
onDiceClear: () => void;
onDiceReroll: () => void;
};
function DiceResults({
diceRolls,
onDiceClear,
onDiceReroll,
}: DiceResultsProps) {
const [isExpanded, setIsExpanded] = useState(false);
if (diceRolls.length === 0) {
return null;
}
let rolls = [];
let rolls: React.ReactChild[] = [];
if (diceRolls.length > 1) {
rolls = diceRolls
.filter((dice) => dice.roll !== "unknown")

View File

@ -1,9 +1,17 @@
import React from "react";
import { Image } from "theme-ui";
import Tile from "../tile/Tile";
function DiceTile({ dice, isSelected, onDiceSelect, onDone }) {
import { DefaultDice } from "../../types/Dice";
type DiceTileProps = {
dice: DefaultDice;
isSelected: boolean;
onDiceSelect: (dice: DefaultDice) => void;
onDone: (dice: DefaultDice) => void;
};
function DiceTile({ dice, isSelected, onDiceSelect, onDone }: DiceTileProps) {
return (
<div style={{ cursor: "pointer" }}>
<Tile

View File

@ -1,12 +1,24 @@
import React from "react";
import { Grid } from "theme-ui";
import SimpleBar from "simplebar-react";
import DiceTile from "./DiceTile";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import { DefaultDice } from "../../types/Dice";
function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
type DiceTileProps = {
dice: DefaultDice[];
onDiceSelect: (dice: DefaultDice) => void;
selectedDice: DefaultDice;
onDone: (dice: DefaultDice) => void;
};
function DiceTiles({
dice,
onDiceSelect,
selectedDice,
onDone,
}: DiceTileProps) {
const layout = useResponsiveLayout();
return (
@ -29,7 +41,6 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
isSelected={selectedDice && dice.key === selectedDice.key}
onDiceSelect={onDiceSelect}
onDone={onDone}
size={layout.tileSize}
/>
))}
</Grid>

View File

@ -1,10 +1,11 @@
import React, { useRef, useCallback, useEffect, useState } from "react";
import { useRef, useCallback, useEffect, useState } from "react";
import { Vector3 } from "@babylonjs/core/Maths/math";
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
import { Box } from "theme-ui";
// @ts-ignore
import environment from "../../dice/environment.dds";
import DiceInteraction from "./DiceInteraction";
@ -19,6 +20,16 @@ import { useDiceLoading } from "../../contexts/DiceLoadingContext";
import { getDiceRoll } from "../../helpers/dice";
import useSetting from "../../hooks/useSetting";
import { DefaultDice, DiceMesh, DiceRoll, DiceType } from "../../types/Dice";
import { Scene } from "@babylonjs/core";
type DiceTrayOverlayProps = {
isOpen: boolean;
shareDice: boolean;
onShareDiceChange: () => void;
diceRolls: DiceRoll[];
onDiceRollsChange: (newRolls: DiceRoll[]) => void;
};
function DiceTrayOverlay({
isOpen,
@ -26,17 +37,18 @@ function DiceTrayOverlay({
onShareDiceChange,
diceRolls,
onDiceRollsChange,
}) {
const sceneRef = useRef();
const shadowGeneratorRef = useRef();
const diceRefs = useRef([]);
}: DiceTrayOverlayProps) {
const sceneRef = useRef<Scene>();
const shadowGeneratorRef = useRef<ShadowGenerator>();
const diceRefs = useRef<DiceMesh[]>([]);
const sceneVisibleRef = useRef(false);
const sceneInteractionRef = useRef(false);
// Add to the counter to ingore sleep values
const sceneKeepAwakeRef = useRef(0);
const diceTrayRef = useRef();
const diceTrayRef = useRef<DiceTray>();
const [diceTraySize, setDiceTraySize] = useState("single");
const [diceTraySize, setDiceTraySize] =
useState<"single" | "double">("single");
const { assetLoadStart, assetLoadFinish, isLoading } = useDiceLoading();
const [fullScreen] = useSetting("map.fullScreen");
@ -50,7 +62,7 @@ function DiceTrayOverlay({
}
// Forces rendering for 1 second
function forceRender() {
function forceRender(): () => void {
// Force rerender
sceneKeepAwakeRef.current++;
let triggered = false;
@ -97,7 +109,7 @@ function DiceTrayOverlay({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function initializeScene(scene) {
async function initializeScene(scene: Scene) {
handleAssetLoadStart();
let light = new DirectionalLight(
"DirectionalLight",
@ -124,16 +136,14 @@ function DiceTrayOverlay({
handleAssetLoadFinish();
}
function update(scene) {
function getDiceSpeed(dice) {
const diceSpeed = dice.instance.physicsImpostor
.getLinearVelocity()
.length();
function update(scene: Scene) {
function getDiceSpeed(dice: DiceMesh) {
const diceSpeed =
dice.instance.physicsImpostor?.getLinearVelocity()?.length() || 0;
// If the dice is a d100 check the d10 as well
if (dice.type === "d100") {
const d10Speed = dice.d10Instance.physicsImpostor
.getLinearVelocity()
.length();
if (dice.d10Instance) {
const d10Speed =
dice.d10Instance.physicsImpostor?.getLinearVelocity()?.length() || 0;
return Math.max(diceSpeed, d10Speed);
} else {
return diceSpeed;
@ -157,14 +167,14 @@ function DiceTrayOverlay({
const dice = die[i];
const speed = getDiceSpeed(dice);
// If the speed has been below 0.01 for 1s set dice to sleep
if (speed < 0.01 && !dice.sleepTimout) {
dice.sleepTimout = setTimeout(() => {
if (speed < 0.01 && !dice.sleepTimeout) {
dice.sleepTimeout = setTimeout(() => {
dice.asleep = true;
}, 1000);
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimout)) {
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimeout)) {
dice.asleep = false;
clearTimeout(dice.sleepTimout);
dice.sleepTimout = null;
dice.sleepTimeout && clearTimeout(dice.sleepTimeout);
dice.sleepTimeout = undefined;
}
}
@ -173,14 +183,14 @@ function DiceTrayOverlay({
}
}
function handleDiceAdd(style, type) {
function handleDiceAdd(style: typeof Dice, type: DiceType) {
const scene = sceneRef.current;
const shadowGenerator = shadowGeneratorRef.current;
if (scene && shadowGenerator) {
const instance = style.createInstance(type, scene);
shadowGenerator.addShadowCaster(instance);
Dice.roll(instance);
let dice = { type, instance, asleep: false };
style.roll(instance);
let dice: DiceMesh = { type, instance, asleep: false };
// If we have a d100 add a d10 as well
if (type === "d100") {
const d10Instance = style.createInstance("d10", scene);
@ -196,7 +206,7 @@ function DiceTrayOverlay({
const die = diceRefs.current;
for (let dice of die) {
dice.instance.dispose();
if (dice.type === "d100") {
if (dice.d10Instance) {
dice.d10Instance.dispose();
}
}
@ -208,14 +218,14 @@ function DiceTrayOverlay({
const die = diceRefs.current;
for (let dice of die) {
Dice.roll(dice.instance);
if (dice.type === "d100") {
if (dice.d10Instance) {
Dice.roll(dice.d10Instance);
}
dice.asleep = false;
}
}
async function handleDiceLoad(dice) {
async function handleDiceLoad(dice: DefaultDice) {
handleAssetLoadStart();
const scene = sceneRef.current;
if (scene) {
@ -230,10 +240,13 @@ function DiceTrayOverlay({
});
useEffect(() => {
let renderTimeout;
let renderCleanup;
let renderTimeout: NodeJS.Timeout;
let renderCleanup: () => void;
function handleResize() {
const map = document.querySelector(".map");
if (!map) {
return;
}
const mapRect = map.getBoundingClientRect();
const availableWidth = mapRect.width - 108; // Subtract padding
@ -283,7 +296,7 @@ function DiceTrayOverlay({
return;
}
let newRolls = [];
let newRolls: DiceRoll[] = [];
for (let i = 0; i < die.length; i++) {
const dice = die[i];
let roll = getDiceRoll(dice);

View File

@ -1,10 +1,22 @@
import React, { useState } from "react";
import { useState } from "react";
import { IconButton } from "theme-ui";
import SelectDiceIcon from "../../icons/SelectDiceIcon";
import SelectDiceModal from "../../modals/SelectDiceModal";
function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
import { DefaultDice } from "../../types/Dice";
type SelectDiceButtonProps = {
onDiceChange: (dice: DefaultDice) => void;
currentDice: DefaultDice;
disabled: boolean;
};
function SelectDiceButton({
onDiceChange,
currentDice,
disabled,
}: SelectDiceButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() {
@ -14,7 +26,7 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
setIsModalOpen(false);
}
function handleDone(dice) {
function handleDone(dice: DefaultDice) {
onDiceChange(dice);
closeModal();
}
@ -39,4 +51,8 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
);
}
SelectDiceButton.defaultProps = {
disabled: false,
};
export default SelectDiceButton;

View File

@ -1,7 +1,14 @@
import React from "react";
import { useDraggable } from "@dnd-kit/core";
import { Data } from "@dnd-kit/core/dist/store/types";
function Draggable({ id, children, data }) {
type DraggableProps = {
id: string;
children: React.ReactNode;
data: Data;
};
function Draggable({ id, children, data }: DraggableProps) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id,
data,

View File

@ -1,7 +1,12 @@
import React from "react";
import { useDroppable } from "@dnd-kit/core";
function Droppable({ id, children, disabled, ...props }) {
type DroppableProps = React.HTMLAttributes<HTMLDivElement> & {
id: string;
disabled: boolean;
};
function Droppable({ id, children, disabled, ...props }: DroppableProps) {
const { setNodeRef } = useDroppable({ id, disabled });
return (

View File

@ -26,75 +26,7 @@ import {
EditShapeAction,
RemoveShapeAction,
} from "../../actions";
import { Fog, Path, Shape } from "../../helpers/drawing";
import Session from "../../network/Session";
import { Grid } from "../../helpers/grid";
import { ImageFile } from "../../helpers/image";
export type Resolutions = Record<string, ImageFile>;
export type Map = {
id: string;
name: string;
owner: string;
file?: Uint8Array;
quality?: string;
resolutions?: Resolutions;
grid: Grid;
group: string;
width: number;
height: number;
type: string;
lastUsed: number;
lastModified: number;
created: number;
showGrid: boolean;
snapToGrid: boolean;
thumbnail?: ImageFile;
};
export type Note = {
id: string;
color: string;
lastModified: number;
lastModifiedBy: string;
locked: boolean;
size: number;
text: string;
textOnly: boolean;
visible: boolean;
x: number;
y: number;
};
export type TokenState = {
id: string;
tokenId: string;
owner: string;
size: number;
category: string;
label: string;
statuses: string[];
x: number;
y: number;
lastModifiedBy: string;
lastModified: number;
rotation: number;
locked: boolean;
visible: boolean;
type: "default" | "file";
outline: any;
width: number;
height: number;
};
export type MapState = {
tokens: Record<string, TokenState>;
drawShapes: Record<string, Path | Shape>;
fogShapes: Record<string, Fog>;
editFlags: ["drawing", "tokens", "notes", "fog"];
notes: Record<string, Note>;
mapId: string;
};
function Map({
map,

View File

@ -1,11 +1,11 @@
import React from "react";
import { Box } from "theme-ui";
import { Box, ThemeUIStyleObject } from "theme-ui";
import { useInView } from "react-intersection-observer";
function LazyTile({ children }) {
function LazyTile({ children }: { children: React.ReactNode }) {
const [ref, inView] = useInView({ triggerOnce: false });
const sx = inView
const sx: ThemeUIStyleObject = inView
? {}
: { width: "100%", height: "0", paddingTop: "100%", position: "relative" };

View File

@ -6,6 +6,16 @@ import { animated, useSpring } from "react-spring";
import { GROUP_ID_PREFIX } from "../../contexts/TileDragContext";
type SortableTileProps = {
id: string;
disableGrouping: boolean;
disableSorting: boolean;
hidden: boolean;
children: React.ReactNode;
isDragging: boolean;
cursor: string;
};
function SortableTile({
id,
disableGrouping,
@ -14,7 +24,7 @@ function SortableTile({
children,
isDragging,
cursor,
}) {
}: SortableTileProps) {
const {
attributes,
listeners,
@ -35,7 +45,7 @@ function SortableTile({
};
// Sort div left aligned
const sortDropStyle = {
const sortDropStyle: React.CSSProperties = {
position: "absolute",
left: "-5px",
top: 0,
@ -46,7 +56,7 @@ function SortableTile({
};
// Group div center aligned
const groupDropStyle = {
const groupDropStyle: React.CSSProperties = {
position: "absolute",
top: 0,
left: 0,
@ -55,7 +65,7 @@ function SortableTile({
borderWidth: "4px",
borderRadius: "4px",
borderStyle:
over?.id === `${GROUP_ID_PREFIX}${id}` && active.id !== id
over?.id === `${GROUP_ID_PREFIX}${id}` && active?.id !== id
? "solid"
: "none",
};

View File

@ -15,8 +15,14 @@ import {
GROUP_SORTABLE_ID,
} from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext";
import { Group } from "../../types/Group";
function SortableTiles({ renderTile, subgroup }) {
type SortableTilesProps = {
renderTile: (group: Group) => React.ReactNode;
subgroup: boolean;
};
function SortableTiles({ renderTile, subgroup }: SortableTilesProps) {
const dragId = useTileDragId();
const dragCursor = useTileDragCursor();
const overGroupId = useTileOverGroupId();
@ -38,14 +44,14 @@ function SortableTiles({ renderTile, subgroup }) {
const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID;
// Only populate selected groups if needed
let selectedGroupIds = [];
let selectedGroupIds: string[] = [];
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
selectedGroupIds = allSelectedIds;
}
const disableSorting = (openGroupId && !subgroup) || filter;
const disableGrouping = subgroup || disableSorting || filter;
const disableSorting = !!((openGroupId && !subgroup) || filter);
const disableGrouping = !!(subgroup || disableSorting || filter);
function renderSortableGroup(group, selectedGroups) {
function renderSortableGroup(group: Group, selectedGroups: Group[]) {
if (overGroupId === group.id && dragId && group.id !== dragId) {
// If dragging over a group render a preview of that group
const previewGroup = moveGroupsInto(
@ -61,7 +67,7 @@ function SortableTiles({ renderTile, subgroup }) {
function renderTiles() {
const groupsByIds = keyBy(activeGroups, "id");
const selectedGroupIdsSet = new Set(selectedGroupIds);
let selectedGroups = [];
let selectedGroups: Group[] = [];
let hasSelectedContainerGroup = false;
for (let groupId of selectedGroupIds) {
const group = groupsByIds[groupId];
@ -72,8 +78,8 @@ function SortableTiles({ renderTile, subgroup }) {
}
}
}
return activeGroups.map((group) => {
const isDragging = dragId && selectedGroupIdsSet.has(group.id);
return activeGroups.map((group: Group) => {
const isDragging = dragId !== null && selectedGroupIdsSet.has(group.id);
const disableTileGrouping =
disableGrouping || isDragging || hasSelectedContainerGroup;
return (
@ -84,7 +90,7 @@ function SortableTiles({ renderTile, subgroup }) {
disableSorting={disableSorting}
hidden={group.id === openGroupId}
isDragging={isDragging}
cursor={dragCursor}
cursor={dragCursor || ""}
>
{renderSortableGroup(group, selectedGroups)}
</SortableTile>

View File

@ -8,8 +8,17 @@ import Vector2 from "../../helpers/Vector2";
import { useTileDragId } from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext";
import { Group } from "../../types/Group";
function SortableTilesDragOverlay({ renderTile, subgroup }) {
type SortableTilesDragOverlayProps = {
renderTile: (group: Group) => React.ReactNode;
subgroup: boolean;
};
function SortableTilesDragOverlay({
renderTile,
subgroup,
}: SortableTilesDragOverlayProps) {
const dragId = useTileDragId();
const {
groups,
@ -27,7 +36,7 @@ function SortableTilesDragOverlay({ renderTile, subgroup }) {
: groups;
// Only populate selected groups if needed
let selectedGroupIds = [];
let selectedGroupIds: string[] = [];
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
selectedGroupIds = allSelectedIds;
}

View File

@ -3,6 +3,18 @@ import { Flex, IconButton, Box, Text, Badge } from "theme-ui";
import EditTileIcon from "../../icons/EditTileIcon";
type TileProps = {
title: string;
isSelected: boolean;
onSelect: () => void;
onEdit: () => void;
onDoubleClick: () => void;
canEdit: boolean;
badges: React.ReactChild[];
editTitle: string;
children: React.ReactNode;
};
function Tile({
title,
isSelected,
@ -13,7 +25,7 @@ function Tile({
badges,
editTitle,
children,
}) {
}: TileProps) {
return (
<Box
sx={{

View File

@ -1,4 +1,3 @@
import React from "react";
import { Flex, IconButton } from "theme-ui";
import AddIcon from "../../icons/AddIcon";
@ -10,7 +9,12 @@ import RadioIconButton from "../RadioIconButton";
import { useGroup } from "../../contexts/GroupContext";
function TileActionBar({ onAdd, addTitle }) {
type TileActionBarProps = {
onAdd: () => void;
addTitle: string;
};
function TileActionBar({ onAdd, addTitle }: TileActionBarProps) {
const {
selectMode,
onSelectModeChange,
@ -33,7 +37,7 @@ function TileActionBar({ onAdd, addTitle }) {
outlineOffset: "0px",
},
}}
onFocus={() => onGroupSelect()}
onFocus={() => onGroupSelect(undefined)}
>
<Search value={filter} onChange={(e) => onFilterChange(e.target.value)} />
<Flex

View File

@ -9,7 +9,7 @@ import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import Droppable from "../drag/Droppable";
function TilesContainer({ children }) {
function TilesContainer({ children }: { children: React.ReactNode }) {
const { onGroupSelect } = useGroup();
const { theme } = useThemeUI();
@ -21,9 +21,9 @@ function TilesContainer({ children }) {
<SimpleBar
style={{
height: layout.tileContainerHeight,
backgroundColor: theme.colors.muted,
backgroundColor: theme.colors?.muted as string,
}}
onClick={() => onGroupSelect()}
onClick={() => onGroupSelect(undefined)}
>
<Grid
p={3}

View File

@ -16,15 +16,16 @@ import GroupNameModal from "../../modals/GroupNameModal";
import { renameGroup } from "../../helpers/group";
import Droppable from "../drag/Droppable";
import { Group } from "../../types/Group";
function TilesOverlay({ modalSize, children }) {
const {
groups,
openGroupId,
onGroupClose,
onGroupSelect,
onGroupsChange,
} = useGroup();
type TilesOverlayProps = {
modalSize: { width: number; height: number };
children: React.ReactNode;
};
function TilesOverlay({ modalSize, children }: TilesOverlayProps) {
const { groups, openGroupId, onGroupClose, onGroupSelect, onGroupsChange } =
useGroup();
const { theme } = useThemeUI();
@ -37,23 +38,29 @@ function TilesOverlay({ modalSize, children }) {
});
const [containerSize, setContinerSize] = useState({ width: 0, height: 0 });
function handleContainerResize(width, height) {
const size = Math.min(width, height) - 16;
setContinerSize({ width: size, height: size });
function handleContainerResize(width?: number, height?: number) {
if (width && height) {
const size = Math.min(width, height) - 16;
setContinerSize({ width: size, height: size });
}
}
const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false);
function handleGroupNameChange(name) {
onGroupsChange(renameGroup(groups, openGroupId, name));
setIsGroupNameModalOpen(false);
function handleGroupNameChange(name: string) {
if (openGroupId) {
onGroupsChange(renameGroup(groups, openGroupId, name), undefined);
setIsGroupNameModalOpen(false);
}
}
const group = groups.find((group) => group.id === openGroupId);
const group = groups.find((group: Group) => group.id === openGroupId);
if (!openGroupId) {
return null;
}
const groupName = group && group.type === "group" && group.name;
return (
<>
<Box
@ -104,14 +111,14 @@ function TilesOverlay({ modalSize, children }) {
>
<Flex my={1} sx={{ position: "relative" }}>
<Text as="p" my="2px">
{group?.name}
{groupName}
</Text>
<IconButton
sx={{
width: "24px",
height: "24px",
position: group?.name ? "absolute" : "relative",
left: group?.name ? "100%" : 0,
position: groupName ? "absolute" : "relative",
left: groupName ? "100%" : 0,
}}
title="Edit Group"
aria-label="Edit Group"
@ -125,9 +132,9 @@ function TilesOverlay({ modalSize, children }) {
width: containerSize.width - 16,
height: containerSize.height - 48,
marginBottom: "8px",
backgroundColor: theme.colors.muted,
backgroundColor: theme.colors?.muted as string,
}}
onClick={() => onGroupSelect()}
onClick={() => onGroupSelect(undefined)}
>
<Grid
sx={{
@ -179,7 +186,7 @@ function TilesOverlay({ modalSize, children }) {
</ReactResizeDetector>
<GroupNameModal
isOpen={isGroupNameModalOpen}
name={group?.name}
name={groupName || ""}
onSubmit={handleGroupNameChange}
onRequestClose={() => setIsGroupNameModalOpen(false)}
/>

View File

@ -8,49 +8,20 @@ import { useDatabase } from "./DatabaseContext";
import useDebounce from "../hooks/useDebounce";
import { omit } from "../helpers/shared";
import { Asset } from "../types/Asset";
/**
* @typedef Asset
* @property {string} id
* @property {number} width
* @property {number} height
* @property {Uint8Array} file
* @property {string} mime
* @property {string} owner
*/
type AssetsContext = {
getAsset: (assetId: string) => Promise<Asset | undefined>;
addAssets: (assets: Asset[]) => void;
putAsset: (asset: Asset) => void;
};
/**
* @callback getAsset
* @param {string} assetId
* @returns {Promise<Asset|undefined>}
*/
/**
* @callback addAssets
* @param {Asset[]} assets
*/
/**
* @callback putAsset
* @param {Asset} asset
*/
/**
* @typedef AssetsContext
* @property {getAsset} getAsset
* @property {addAssets} addAssets
* @property {putAsset} putAsset
*/
/**
* @type {React.Context<undefined|AssetsContext>}
*/
const AssetsContext = React.createContext();
const AssetsContext = React.createContext<AssetsContext | undefined>(undefined);
// 100 MB max cache size
const maxCacheSize = 1e8;
export function AssetsProvider({ children }) {
export function AssetsProvider({ children }: { children: React.ReactNode }) {
const { worker, database, databaseStatus } = useDatabase();
useEffect(() => {
@ -61,33 +32,39 @@ export function AssetsProvider({ children }) {
const getAsset = useCallback(
async (assetId) => {
return await database.table("assets").get(assetId);
if (database) {
return await database.table("assets").get(assetId);
}
},
[database]
);
const addAssets = useCallback(
async (assets) => {
await database.table("assets").bulkAdd(assets);
if (database) {
await database.table("assets").bulkAdd(assets);
}
},
[database]
);
const putAsset = useCallback(
async (asset) => {
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup
// Safari doesn't support BC so fallback to single thread
if (window.BroadcastChannel) {
const packedAsset = encode(asset);
const success = await worker.putData(
Comlink.transfer(packedAsset, [packedAsset.buffer]),
"assets"
);
if (!success) {
if (database) {
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup
// Safari doesn't support BC so fallback to single thread
if (window.BroadcastChannel) {
const packedAsset = encode(asset);
const success = await worker.putData(
Comlink.transfer(packedAsset, [packedAsset.buffer]),
"assets"
);
if (!success) {
await database.table("assets").put(asset);
}
} else {
await database.table("assets").put(asset);
}
} else {
await database.table("assets").put(asset);
}
},
[database, worker]
@ -119,35 +96,38 @@ export function useAssets() {
* @property {number} references
*/
/**
* @type React.Context<undefined|Object.<string, AssetURL>>
*/
export const AssetURLsStateContext = React.createContext();
type AssetURL = {
url: string | null;
id: string;
references: number;
};
/**
* @type React.Context<undefined|React.Dispatch<React.SetStateAction<{}>>>
*/
export const AssetURLsUpdaterContext = React.createContext();
type AssetURLs = Record<string, AssetURL>;
export const AssetURLsStateContext =
React.createContext<AssetURLs | undefined>(undefined);
export const AssetURLsUpdaterContext =
React.createContext<
React.Dispatch<React.SetStateAction<AssetURLs>> | undefined
>(undefined);
/**
* Helper to manage sharing of custom image sources between uses of useAssetURL
*/
export function AssetURLsProvider({ children }) {
const [assetURLs, setAssetURLs] = useState({});
export function AssetURLsProvider({ children }: { children: React.ReactNode }) {
const [assetURLs, setAssetURLs] = useState<AssetURLs>({});
const { database } = useDatabase();
// Keep track of the assets that need to be loaded
const [assetKeys, setAssetKeys] = useState([]);
const [assetKeys, setAssetKeys] = useState<string[]>([]);
// Load assets after 100ms
const loadingDebouncedAssetURLs = useDebounce(assetURLs, 100);
// Update the asset keys to load when a url is added without an asset attached
useEffect(() => {
if (!loadingDebouncedAssetURLs) {
return;
}
let keysToLoad = [];
let keysToLoad: string[] = [];
for (let url of Object.values(loadingDebouncedAssetURLs)) {
if (url.url === null) {
keysToLoad.push(url.id);
@ -159,8 +139,9 @@ export function AssetURLsProvider({ children }) {
}, [loadingDebouncedAssetURLs]);
// Get the new assets whenever the keys change
const assets = useLiveQuery(
() => database?.table("assets").where("id").anyOf(assetKeys).toArray(),
const assets = useLiveQuery<Asset[]>(
() =>
database?.table("assets").where("id").anyOf(assetKeys).toArray() || [],
[database, assetKeys]
);
@ -197,7 +178,7 @@ export function AssetURLsProvider({ children }) {
let urlsToCleanup = [];
for (let url of Object.values(prevURLs)) {
if (url.references <= 0) {
URL.revokeObjectURL(url.url);
url.url && URL.revokeObjectURL(url.url);
urlsToCleanup.push(url.id);
}
}
@ -220,13 +201,13 @@ export function AssetURLsProvider({ children }) {
/**
* Helper function to load either file or default asset into a URL
* @param {string} assetId
* @param {"file"|"default"} type
* @param {Object.<string, string>} defaultSources
* @param {string|undefined} unknownSource
* @returns {string|undefined}
*/
export function useAssetURL(assetId, type, defaultSources, unknownSource) {
export function useAssetURL(
assetId: string,
type: "file" | "default",
defaultSources: Record<string, string>,
unknownSource?: string
) {
const assetURLs = useContext(AssetURLsStateContext);
if (assetURLs === undefined) {
throw new Error("useAssetURL must be used within a AssetURLsProvider");