More typescript
This commit is contained in:
parent
45a4443dd1
commit
ecfab87aa0
@ -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"
|
||||
}
|
||||
|
@ -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
|
@ -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}
|
@ -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={{
|
@ -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);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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")
|
@ -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
|
@ -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>
|
@ -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);
|
@ -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;
|
@ -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,
|
@ -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 (
|
@ -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,
|
||||
|
@ -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" };
|
||||
|
@ -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",
|
||||
};
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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={{
|
@ -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
|
@ -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}
|
@ -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)}
|
||||
/>
|
@ -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");
|
||||