Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-17 18:18:57 +10:00
parent 2a053f4854
commit a4363e542c
30 changed files with 247 additions and 163 deletions

View File

@ -99,6 +99,7 @@
"@types/deep-diff": "^1.0.0", "@types/deep-diff": "^1.0.0",
"@types/file-saver": "^2.0.2", "@types/file-saver": "^2.0.2",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/lodash.chunk": "^4.2.6",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6", "@types/lodash.set": "^4.3.6",

View File

@ -18,11 +18,13 @@ import SelectDiceButton from "./SelectDiceButton";
import Divider from "../Divider"; import Divider from "../Divider";
import Dice from "../../dice/Dice";
import { dice } from "../../dice"; import { dice } from "../../dice";
import useSetting from "../../hooks/useSetting"; import useSetting from "../../hooks/useSetting";
import { DefaultDice, DiceRoll, DiceType } from "../../types/Dice"; import { DefaultDice, DiceRoll, DiceType } from "../../types/Dice";
import Dice from "../../dice/Dice"; import { DiceShareChangeEventHandler } from "../../types/Events";
type DiceButtonsProps = { type DiceButtonsProps = {
diceRolls: DiceRoll[]; diceRolls: DiceRoll[];
@ -31,7 +33,7 @@ type DiceButtonsProps = {
diceTraySize: "single" | "double"; diceTraySize: "single" | "double";
onDiceTraySizeChange: (newSize: "single" | "double") => void; onDiceTraySizeChange: (newSize: "single" | "double") => void;
shareDice: boolean; shareDice: boolean;
onShareDiceChange: (value: boolean) => void; onShareDiceChange: DiceShareChangeEventHandler;
loading: boolean; loading: boolean;
}; };

View File

@ -3,6 +3,7 @@ import { Vector3 } from "@babylonjs/core/Maths/math";
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight"; import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator"; import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture"; import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
import { Scene } from "@babylonjs/core";
import { Box } from "theme-ui"; import { Box } from "theme-ui";
// @ts-ignore // @ts-ignore
@ -19,16 +20,21 @@ import DiceTray from "../../dice/diceTray/DiceTray";
import { useDiceLoading } from "../../contexts/DiceLoadingContext"; import { useDiceLoading } from "../../contexts/DiceLoadingContext";
import { getDiceRoll } from "../../helpers/dice"; import { getDiceRoll } from "../../helpers/dice";
import useSetting from "../../hooks/useSetting"; import useSetting from "../../hooks/useSetting";
import { DefaultDice, DiceMesh, DiceRoll, DiceType } from "../../types/Dice"; import { DefaultDice, DiceMesh, DiceRoll, DiceType } from "../../types/Dice";
import { Scene } from "@babylonjs/core"; import {
DiceRollsChangeEventHandler,
DiceShareChangeEventHandler,
} from "../../types/Events";
type DiceTrayOverlayProps = { type DiceTrayOverlayProps = {
isOpen: boolean; isOpen: boolean;
shareDice: boolean; shareDice: boolean;
onShareDiceChange: () => void; onShareDiceChange: DiceShareChangeEventHandler;
diceRolls: DiceRoll[]; diceRolls: DiceRoll[];
onDiceRollsChange: (newRolls: DiceRoll[]) => void; onDiceRollsChange: DiceRollsChangeEventHandler;
}; };
function DiceTrayOverlay({ function DiceTrayOverlay({

View File

@ -12,7 +12,6 @@ import MapMeasure from "./MapMeasure";
import NetworkedMapPointer from "../../network/NetworkedMapPointer"; import NetworkedMapPointer from "../../network/NetworkedMapPointer";
import MapNotes from "./MapNotes"; import MapNotes from "./MapNotes";
import { useTokenData } from "../../contexts/TokenDataContext";
import { useSettings } from "../../contexts/SettingsContext"; import { useSettings } from "../../contexts/SettingsContext";
import TokenMenu from "../token/TokenMenu"; import TokenMenu from "../token/TokenMenu";
@ -29,13 +28,12 @@ import {
import Session from "../../network/Session"; import Session from "../../network/Session";
import { Drawing, DrawingState } from "../../types/Drawing"; import { Drawing, DrawingState } from "../../types/Drawing";
import { Fog, FogState } from "../../types/Fog"; import { Fog, FogState } from "../../types/Fog";
import { Map, MapActions, MapToolId } from "../../types/Map"; import { Map as MapType, MapActions, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState"; import { MapState } from "../../types/MapState";
import { Settings } from "../../types/Settings"; import { Settings } from "../../types/Settings";
import { import {
MapChangeEventHandler, MapChangeEventHandler,
MapResetEventHandler, MapResetEventHandler,
MapTokensStateCreateHandler,
MapTokenStateRemoveHandler, MapTokenStateRemoveHandler,
NoteChangeEventHandler, NoteChangeEventHandler,
NoteRemoveEventHander, NoteRemoveEventHander,
@ -47,7 +45,7 @@ import { TokenDraggingOptions, TokenMenuOptions } from "../../types/Token";
import { Note, NoteDraggingOptions, NoteMenuOptions } from "../../types/Note"; import { Note, NoteDraggingOptions, NoteMenuOptions } from "../../types/Note";
type MapProps = { type MapProps = {
map: Map | null; map: MapType | null;
mapState: MapState | null; mapState: MapState | null;
mapActions: MapActions; mapActions: MapActions;
onMapTokenStateChange: TokenStateChangeEventHandler; onMapTokenStateChange: TokenStateChangeEventHandler;
@ -95,8 +93,6 @@ function Map({
}: MapProps) { }: MapProps) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { tokensById } = useTokenData();
const [selectedToolId, setSelectedToolId] = useState<MapToolId>("move"); const [selectedToolId, setSelectedToolId] = useState<MapToolId>("move");
const { settings, setSettings } = useSettings(); const { settings, setSettings } = useSettings();

View File

@ -193,7 +193,11 @@ function MapInteraction({
return ( return (
<MapInteractionProvider value={mapInteraction}> <MapInteractionProvider value={mapInteraction}>
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}> <GridProvider
grid={map?.grid || null}
width={mapWidth}
height={mapHeight}
>
<Box <Box
sx={{ sx={{
position: "relative", position: "relative",

View File

@ -52,7 +52,7 @@ function MapMeasure({ map, active }: MapMeasureProps) {
useState<MeasureData | null>(null); useState<MeasureData | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const gridScale = parseGridScale(active && grid.measurement.scale); const gridScale = parseGridScale(active ? grid.measurement.scale : null);
const snapPositionToGrid = useGridSnapping( const snapPositionToGrid = useGridSnapping(
grid.measurement.type === "euclidean" ? 0 : 1, grid.measurement.type === "euclidean" ? 0 : 1,

View File

@ -1,4 +1,3 @@
import React from "react";
import { Grid } from "theme-ui"; import { Grid } from "theme-ui";
import Tile from "../tile/Tile"; import Tile from "../tile/Tile";

View File

@ -9,15 +9,14 @@ type MapTileImageProps = {
} & ImageProps; } & ImageProps;
function MapTileImage({ map, ...props }: MapTileImageProps) { function MapTileImage({ map, ...props }: MapTileImageProps) {
const mapURL = useDataURL( const mapURL = useDataURL(
map, map,
defaultMapSources, defaultMapSources,
undefined, undefined,
map.type === "file" map.type === "file"
); );
return <Image src={mapURL} {...props} />; return <Image src={mapURL} {...props} />;
} }
);
export default MapTileImage; export default MapTileImage;

View File

@ -26,11 +26,14 @@ import { useKeyboard } from "../../../contexts/KeyboardContext";
import shortcuts from "../../../shortcuts"; import shortcuts from "../../../shortcuts";
import { DrawingToolSettings, DrawingToolType } from "../../../types/Drawing"; import {
DrawingToolSettings as DrawingToolSettingsType,
DrawingToolType,
} from "../../../types/Drawing";
type DrawingToolSettingsProps = { type DrawingToolSettingsProps = {
settings: DrawingToolSettings; settings: DrawingToolSettingsType;
onSettingChange: (change: Partial<DrawingToolSettings>) => void; onSettingChange: (change: Partial<DrawingToolSettingsType>) => void;
onToolAction: (action: string) => void; onToolAction: (action: string) => void;
disabledActions: string[]; disabledActions: string[];
}; };

View File

@ -23,11 +23,14 @@ import { useKeyboard } from "../../../contexts/KeyboardContext";
import shortcuts from "../../../shortcuts"; import shortcuts from "../../../shortcuts";
import { FogToolSettings, FogToolType } from "../../../types/Fog"; import {
FogToolSettings as FogToolSettingsType,
FogToolType,
} from "../../../types/Fog";
type FogToolSettingsProps = { type FogToolSettingsProps = {
settings: FogToolSettings; settings: FogToolSettingsType;
onSettingChange: (change: Partial<FogToolSettings>) => void; onSettingChange: (change: Partial<FogToolSettingsType>) => void;
onToolAction: (action: string) => void; onToolAction: (action: string) => void;
disabledActions: string[]; disabledActions: string[];
}; };

View File

@ -2,11 +2,11 @@ import { Flex } from "theme-ui";
import ColorControl from "./ColorControl"; import ColorControl from "./ColorControl";
import { PointerToolSettings } from "../../../types/Pointer"; import { PointerToolSettings as PointerToolSettingsType } from "../../../types/Pointer";
type PointerToolSettingsProps = { type PointerToolSettingsProps = {
settings: PointerToolSettings; settings: PointerToolSettingsType;
onSettingChange: (change: Partial<PointerToolSettings>) => void; onSettingChange: (change: Partial<PointerToolSettingsType>) => void;
}; };
function PointerToolSettings({ function PointerToolSettings({

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import { IconButton } from "theme-ui"; import { IconButton } from "theme-ui";
import AddPartyMemberModal from "../../modals/AddPartyMemberModal"; import AddPartyMemberModal from "../../modals/AddPartyMemberModal";

View File

@ -1,16 +1,18 @@
import React, { useState, useEffect, ChangeEvent } from "react"; import { useState, useEffect, ChangeEvent, FormEvent } from "react";
import { IconButton } from "theme-ui"; import { IconButton } from "theme-ui";
import ChangeNicknameModal from "../../modals/ChangeNicknameModal"; import ChangeNicknameModal from "../../modals/ChangeNicknameModal";
import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon"; import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon";
type ChangeNicknameButtonProps = {
nickname: string;
onChange: (nickname: string) => void;
};
function ChangeNicknameButton({ function ChangeNicknameButton({
nickname, nickname,
onChange, onChange,
}: { }: ChangeNicknameButtonProps) {
nickname: string;
onChange;
}) {
const [isChangeModalOpen, setIsChangeModalOpen] = useState(false); const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
function openModal() { function openModal() {
setIsChangeModalOpen(true); setIsChangeModalOpen(true);
@ -25,7 +27,7 @@ function ChangeNicknameButton({
setChangedNickname(nickname); setChangedNickname(nickname);
}, [nickname]); }, [nickname]);
function handleChangeSubmit(event: Event) { function handleChangeSubmit(event: FormEvent) {
event.preventDefault(); event.preventDefault();
onChange(changedNickname); onChange(changedNickname);
closeModal(); closeModal();

View File

@ -1,14 +1,13 @@
import { Flex, Box, Text } from "theme-ui"; import { Flex, Box, Text } from "theme-ui";
import { DiceRoll as DiceRollType, DiceType } from "../../types/Dice";
function DiceRoll({ type DiceRollProps = {
rolls, rolls: DiceRollType[];
type, type: DiceType;
children, children: React.ReactNode;
}: { };
rolls;
type: string; function DiceRoll({ rolls, type, children }: DiceRollProps) {
children;
}) {
return ( return (
<Flex sx={{ flexWrap: "wrap" }}> <Flex sx={{ flexWrap: "wrap" }}>
<Box sx={{ transform: "scale(0.8)" }}>{children}</Box> <Box sx={{ transform: "scale(0.8)" }}>{children}</Box>

View File

@ -13,8 +13,9 @@ import D100Icon from "../../icons/D100Icon";
import DiceRoll from "./DiceRoll"; import DiceRoll from "./DiceRoll";
import { getDiceRollTotal } from "../../helpers/dice"; import { getDiceRollTotal } from "../../helpers/dice";
import { DiceRoll as DiceRollType, DiceType } from "../../types/Dice";
const diceIcons = [ const diceIcons: { type: DiceType; Icon: React.ElementType }[] = [
{ type: "d20", Icon: D20Icon }, { type: "d20", Icon: D20Icon },
{ type: "d12", Icon: D12Icon }, { type: "d12", Icon: D12Icon },
{ type: "d10", Icon: D10Icon }, { type: "d10", Icon: D10Icon },
@ -24,7 +25,11 @@ const diceIcons = [
{ type: "d100", Icon: D100Icon }, { type: "d100", Icon: D100Icon },
]; ];
function DiceRolls({ rolls }: { rolls }) { type DiceRollsProps = {
rolls: DiceRollType[];
};
function DiceRolls({ rolls }: DiceRollsProps) {
const total = getDiceRollTotal(rolls); const total = getDiceRollTotal(rolls);
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);

View File

@ -9,19 +9,27 @@ import useSetting from "../../hooks/useSetting";
import LoadingOverlay from "../LoadingOverlay"; import LoadingOverlay from "../LoadingOverlay";
import {
DiceShareChangeEventHandler,
DiceRollsChangeEventHandler,
} from "../../types/Events";
import { DiceRoll } from "../../types/Dice";
const DiceTrayOverlay = React.lazy(() => import("../dice/DiceTrayOverlay")); const DiceTrayOverlay = React.lazy(() => import("../dice/DiceTrayOverlay"));
type DiceTrayButtonProps = {
shareDice: boolean;
onShareDiceChange: DiceShareChangeEventHandler;
diceRolls: DiceRoll[];
onDiceRollsChange: DiceRollsChangeEventHandler;
};
function DiceTrayButton({ function DiceTrayButton({
shareDice, shareDice,
onShareDiceChange, onShareDiceChange,
diceRolls, diceRolls,
onDiceRollsChange, onDiceRollsChange,
}: { }: DiceTrayButtonProps) {
shareDice: boolean;
onShareDiceChange;
diceRolls: [];
onDiceRollsChange;
}) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [fullScreen] = useSetting("map.fullScreen"); const [fullScreen] = useSetting("map.fullScreen");

View File

@ -2,17 +2,15 @@ import { Text, Flex } from "theme-ui";
import Stream from "./Stream"; import Stream from "./Stream";
import DiceRolls from "./DiceRolls"; import DiceRolls from "./DiceRolls";
import { DiceRoll } from "../../types/Dice";
// TODO: check if stream is a required or optional param type NicknameProps = {
function Nickname({
nickname,
stream,
diceRolls,
}: {
nickname: string; nickname: string;
stream?; stream?: MediaStream;
diceRolls; diceRolls?: DiceRoll[];
}) { };
function Nickname({ nickname, stream, diceRolls }: NicknameProps) {
return ( return (
<Flex sx={{ flexDirection: "column" }}> <Flex sx={{ flexDirection: "column" }}>
<Text <Text

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import { Flex, Box, Text } from "theme-ui"; import { Flex, Box, Text } from "theme-ui";
import SimpleBar from "simplebar-react"; import SimpleBar from "simplebar-react";
@ -15,6 +15,20 @@ import useSetting from "../../hooks/useSetting";
import { useParty } from "../../contexts/PartyContext"; import { useParty } from "../../contexts/PartyContext";
import { usePlayerState, usePlayerUpdater } from "../../contexts/PlayerContext"; import { usePlayerState, usePlayerUpdater } from "../../contexts/PlayerContext";
import { DiceRoll } from "../../types/Dice";
import {
StreamEndEventHandler,
StreamStartEventHandler,
} from "../../types/Events";
import { Timer as TimerType } from "../../types/Timer";
type PartyProps = {
gameId: string;
stream: MediaStream | null;
partyStreams: Record<string, MediaStream>;
onStreamStart: StreamStartEventHandler;
onStreamEnd: StreamEndEventHandler;
};
function Party({ function Party({
gameId, gameId,
@ -22,33 +36,27 @@ function Party({
partyStreams, partyStreams,
onStreamStart, onStreamStart,
onStreamEnd, onStreamEnd,
}: { }: PartyProps) {
gameId: string;
stream;
partyStreams;
onStreamStart;
onStreamEnd;
}) {
const setPlayerState = usePlayerUpdater(); const setPlayerState = usePlayerUpdater();
const playerState = usePlayerState(); const playerState = usePlayerState();
const partyState = useParty(); const partyState = useParty();
const [fullScreen] = useSetting<boolean>("map.fullScreen"); const [fullScreen] = useSetting<boolean>("map.fullScreen");
const [shareDice, setShareDice] = useSetting("dice.shareDice"); const [shareDice, setShareDice] = useSetting<boolean>("dice.shareDice");
function handleTimerStart(newTimer: PartyTimer) { function handleTimerStart(newTimer: TimerType) {
setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); setPlayerState((prevState) => ({ ...prevState, timer: newTimer }));
} }
function handleTimerStop() { function handleTimerStop() {
setPlayerState((prevState) => ({ ...prevState, timer: null })); setPlayerState((prevState) => ({ ...prevState, timer: undefined }));
} }
useEffect(() => { useEffect(() => {
let prevTime = performance.now(); let prevTime = performance.now();
let request = requestAnimationFrame(update); let request = requestAnimationFrame(update);
let counter = 0; let counter = 0;
function update(time) { function update(time: number) {
request = requestAnimationFrame(update); request = requestAnimationFrame(update);
const deltaTime = time - prevTime; const deltaTime = time - prevTime;
prevTime = time; prevTime = time;
@ -57,12 +65,12 @@ function Party({
counter += deltaTime; counter += deltaTime;
// Update timer every second // Update timer every second
if (counter > 1000) { if (counter > 1000) {
const newTimer: PartyTimer = { const newTimer: TimerType = {
...playerState.timer, ...playerState.timer,
current: playerState.timer.current - counter, current: playerState.timer.current - counter,
}; };
if (newTimer.current < 0) { if (newTimer.current < 0) {
setPlayerState((prevState) => ({ ...prevState, timer: null })); setPlayerState((prevState) => ({ ...prevState, timer: undefined }));
} else { } else {
setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); setPlayerState((prevState) => ({ ...prevState, timer: newTimer }));
} }
@ -79,9 +87,9 @@ function Party({
setPlayerState((prevState) => ({ ...prevState, nickname: newNickname })); setPlayerState((prevState) => ({ ...prevState, nickname: newNickname }));
} }
function handleDiceRollsChange(newDiceRolls: number[]) { function handleDiceRollsChange(newDiceRolls: DiceRoll[]) {
setPlayerState( setPlayerState(
(prevState: PlayerDice) => ({ (prevState) => ({
...prevState, ...prevState,
dice: { share: shareDice, rolls: newDiceRolls }, dice: { share: shareDice, rolls: newDiceRolls },
}), }),
@ -91,7 +99,7 @@ function Party({
function handleShareDiceChange(newShareDice: boolean) { function handleShareDiceChange(newShareDice: boolean) {
setShareDice(newShareDice); setShareDice(newShareDice);
setPlayerState((prevState: PlayerInfo) => ({ setPlayerState((prevState) => ({
...prevState, ...prevState,
dice: { ...prevState.dice, share: newShareDice }, dice: { ...prevState.dice, share: newShareDice },
})); }));
@ -134,17 +142,16 @@ function Party({
height: "calc(100% - 232px)", height: "calc(100% - 232px)",
}} }}
> >
{/* TODO: check if stream is required here */}
<Nickname <Nickname
nickname={`${playerState.nickname} (you)`} nickname={`${playerState.nickname} (you)`}
diceRolls={shareDice && playerState.dice.rolls} diceRolls={shareDice ? playerState.dice.rolls : undefined}
/> />
{Object.entries(partyState).map(([id, { nickname, dice }]) => ( {Object.entries(partyState).map(([id, { nickname, dice }]) => (
<Nickname <Nickname
nickname={nickname} nickname={nickname}
key={id} key={id}
stream={partyStreams[id]} stream={partyStreams[id]}
diceRolls={dice.share && dice.rolls} diceRolls={dice.share ? dice.rolls : undefined}
/> />
))} ))}
{playerState.timer && <Timer timer={playerState.timer} index={0} />} {playerState.timer && <Timer timer={playerState.timer} index={0} />}

View File

@ -1,20 +1,26 @@
import React, { useState } from "react"; import { useState } from "react";
import { IconButton, Box, Text } from "theme-ui"; import { IconButton, Box, Text } from "theme-ui";
import adapter from "webrtc-adapter"; import adapter from "webrtc-adapter";
import Link from "../Link"; import Link from "../Link";
import StartStreamModal from "../../modals/StartStreamModal"; import StartStreamModal from "../../modals/StartStreamModal";
import {
StreamEndEventHandler,
StreamStartEventHandler,
} from "../../types/Events";
type StartStreamProps = {
onStreamStart: StreamStartEventHandler;
onStreamEnd: StreamEndEventHandler;
stream: MediaStream | null;
};
function StartStreamButton({ function StartStreamButton({
onStreamStart, onStreamStart,
onStreamEnd, onStreamEnd,
stream, stream,
}: { }: StartStreamProps) {
onStreamStart;
onStreamEnd;
stream;
}) {
const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false); const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false);
function openModal() { function openModal() {
setIsStreamModalOpen(true); setIsStreamModalOpen(true);
@ -53,7 +59,7 @@ function StartStreamButton({
function handleStreamStart() { function handleStreamStart() {
// Must be defined this way in typescript due to open issue - https://github.com/microsoft/TypeScript/issues/33232 // Must be defined this way in typescript due to open issue - https://github.com/microsoft/TypeScript/issues/33232
const mediaDevices = navigator.mediaDevices; const mediaDevices: any = navigator.mediaDevices;
mediaDevices mediaDevices
.getDisplayMedia({ .getDisplayMedia({
video: true, video: true,
@ -63,7 +69,7 @@ function StartStreamButton({
echoCancellation: false, echoCancellation: false,
}, },
}) })
.then((localStream: { getTracks }) => { .then((localStream: MediaStream) => {
const tracks = localStream.getTracks(); const tracks = localStream.getTracks();
const hasAudio = tracks.some( const hasAudio = tracks.some(

View File

@ -1,18 +1,25 @@
import React, { useState } from "react"; import { useState } from "react";
import { IconButton } from "theme-ui"; import { IconButton } from "theme-ui";
import StartTimerModal from "../../modals/StartTimerModal"; import StartTimerModal from "../../modals/StartTimerModal";
import StartTimerIcon from "../../icons/StartTimerIcon"; import StartTimerIcon from "../../icons/StartTimerIcon";
import {
TimerStartEventHandler,
TimerStopEventHandler,
} from "../../types/Events";
import { Timer } from "../../types/Timer";
type StartTimerButtonProps = {
onTimerStart: TimerStartEventHandler;
onTimerStop: TimerStopEventHandler;
timer?: Timer;
};
function StartTimerButton({ function StartTimerButton({
onTimerStart, onTimerStart,
onTimerStop, onTimerStop,
timer, timer,
}: { }: StartTimerButtonProps) {
onTimerStart;
onTimerStop;
timer;
}) {
const [isTimerModalOpen, setIsTimerModalOpen] = useState(false); const [isTimerModalOpen, setIsTimerModalOpen] = useState(false);
function openModal() { function openModal() {

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, ChangeEvent } from "react"; import { useState, useRef, useEffect, ChangeEvent } from "react";
import { Text, IconButton, Box, Flex } from "theme-ui"; import { Text, IconButton, Box, Flex } from "theme-ui";
import StreamMuteIcon from "../../icons/StreamMuteIcon"; import StreamMuteIcon from "../../icons/StreamMuteIcon";
@ -6,18 +6,17 @@ import StreamMuteIcon from "../../icons/StreamMuteIcon";
import Banner from "../banner/Banner"; import Banner from "../banner/Banner";
import Slider from "../Slider"; import Slider from "../Slider";
function Stream({ type StreamProps = {
stream,
nickname,
}: {
stream: MediaStream; stream: MediaStream;
nickname: string; nickname: string;
}) { };
function Stream({ stream, nickname }: StreamProps) {
const [streamVolume, setStreamVolume] = useState(1); const [streamVolume, setStreamVolume] = useState(1);
const [showStreamInteractBanner, setShowStreamInteractBanner] = const [showStreamInteractBanner, setShowStreamInteractBanner] =
useState(false); useState(false);
const [streamMuted, setStreamMuted] = useState(false); const [streamMuted, setStreamMuted] = useState(false);
const audioRef = useRef(); const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => { useEffect(() => {
if (audioRef.current) { if (audioRef.current) {
@ -59,17 +58,21 @@ function Stream({
const [isVolumeControlAvailable, setIsVolumeControlAvailable] = const [isVolumeControlAvailable, setIsVolumeControlAvailable] =
useState(true); useState(true);
useEffect(() => { useEffect(() => {
let audio = audioRef.current; const audio = audioRef.current;
function checkVolumeControlAvailable() { if (!audio) {
return;
}
const checkVolumeControlAvailable = () => {
const prevVolume = audio.volume; const prevVolume = audio.volume;
// Set volume to 0.5, then check if the value actually stuck 100ms later // Set volume to 0.5, then check if the value actually stuck 100ms later
audio.volume = 0.5; audio.volume = 0.5;
setTimeout(() => { setTimeout(() => {
setIsVolumeControlAvailable(audio.volume === 0.5); if (audio) {
audio.volume = prevVolume; setIsVolumeControlAvailable(audio.volume === 0.5);
// TODO: check if this supposed to be a number or number[] audio.volume = prevVolume;
}
}, 100); }, 100);
} };
audio.addEventListener("playing", checkVolumeControlAvailable); audio.addEventListener("playing", checkVolumeControlAvailable);
@ -79,7 +82,7 @@ function Stream({
}, []); }, []);
// Use an audio context gain node to control volume to go past 100% // Use an audio context gain node to control volume to go past 100%
const audioGainRef = useRef(); const audioGainRef = useRef<GainNode>();
useEffect(() => { useEffect(() => {
let audioContext: AudioContext; let audioContext: AudioContext;
if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) { if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) {

View File

@ -1,11 +1,17 @@
import React, { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Box, Progress } from "theme-ui"; import { Box, Progress } from "theme-ui";
import usePortal from "../../hooks/usePortal"; import usePortal from "../../hooks/usePortal";
import { Timer as TimerType } from "../../types/Timer";
function Timer({ timer, index }: { timer; index: number }) { type TimerProps = {
const progressBarRef = useRef(); timer?: TimerType;
index: number;
};
function Timer({ timer, index }: TimerProps) {
const progressBarRef = useRef<HTMLProgressElement>(null);
useEffect(() => { useEffect(() => {
if (progressBarRef.current && timer) { if (progressBarRef.current && timer) {
@ -16,7 +22,7 @@ function Timer({ timer, index }: { timer; index: number }) {
useEffect(() => { useEffect(() => {
let request = requestAnimationFrame(animate); let request = requestAnimationFrame(animate);
let previousTime = performance.now(); let previousTime = performance.now();
function animate(time) { function animate(time: number) {
request = requestAnimationFrame(animate); request = requestAnimationFrame(animate);
const deltaTime = time - previousTime; const deltaTime = time - previousTime;
previousTime = time; previousTime = time;

View File

@ -23,7 +23,6 @@ export type CustomDragEndEvent = DragEndWithOverlayEvent & DragEndEvent;
type CustomDragProps = { type CustomDragProps = {
onDragEnd?: (event: CustomDragEndEvent) => void; onDragEnd?: (event: CustomDragEndEvent) => void;
;
}; };
function DragPositionMonitor({ onDragEnd }: CustomDragProps) { function DragPositionMonitor({ onDragEnd }: CustomDragProps) {

View File

@ -66,21 +66,25 @@ export const GridCellPixelOffsetContext = React.createContext(
const defaultStrokeWidth = 1 / 10; const defaultStrokeWidth = 1 / 10;
type GridProviderProps = {
grid: Grid | null;
width: number;
height: number;
children: React.ReactNode;
};
export function GridProvider({ export function GridProvider({
grid: inputGrid, grid: inputGrid,
width, width,
height, height,
children, children,
}: { }: GridProviderProps) {
grid: Grid; let grid: Grid;
width: number;
height: number;
children: React.ReactNode;
}) {
let grid = inputGrid;
if (!grid.size.x || !grid.size.y) { if (!inputGrid || !inputGrid.size.x || !inputGrid.size.y) {
grid = defaultValue.grid; grid = defaultValue.grid;
} else {
grid = inputGrid;
} }
const [gridPixelSize, setGridPixelSize] = useState( const [gridPixelSize, setGridPixelSize] = useState(

View File

@ -359,24 +359,22 @@ export function gridDistance(
* @returns {GridScale} * @returns {GridScale}
*/ */
export function parseGridScale(scale: string): GridScale { export function parseGridScale(scale: string | null): GridScale {
if (typeof scale === "string") { if (typeof scale === "string") {
const match = scale.match(/(\d*)(\.\d*)?([a-zA-Z]*)/); const match = scale.match(/(\d*)(\.\d*)?([a-zA-Z]*)/);
// TODO: handle case where match is not found if (match) {
if (!match) { const integer = parseFloat(match[1]);
throw Error; const fractional = parseFloat(match[2]);
} const unit = match[3] || "";
const integer = parseFloat(match[1]); if (!isNaN(integer) && !isNaN(fractional)) {
const fractional = parseFloat(match[2]); return {
const unit = match[3] || ""; multiplier: integer + fractional,
if (!isNaN(integer) && !isNaN(fractional)) { unit: unit,
return { digits: match[2].length - 1,
multiplier: integer + fractional, };
unit: unit, } else if (!isNaN(integer) && isNaN(fractional)) {
digits: match[2].length - 1, return { multiplier: integer, unit: unit, digits: 0 };
}; }
} else if (!isNaN(integer) && isNaN(fractional)) {
return { multiplier: integer, unit: unit, digits: 0 };
} }
} }
return { multiplier: 1, unit: "", digits: 0 }; return { multiplier: 1, unit: "", digits: 0 };

View File

@ -2,20 +2,22 @@ import { Box, Text, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
import { RequestCloseEventHandler } from "../types/Events"; import {
RequestCloseEventHandler,
StreamEndEventHandler,
} from "../types/Events";
export type StreamStartEventHandler = () => void; export type StreamOpenAndStartEventHandler = () => void;
export type StreamEndEventHandler = (stream: MediaStream) => void;
type StartStreamProps = { type StartStreamProps = {
isOpen: boolean; isOpen: boolean;
onRequestClose: RequestCloseEventHandler; onRequestClose: RequestCloseEventHandler;
isSupported: boolean; isSupported: boolean;
unavailableMessage: JSX.Element; unavailableMessage: JSX.Element;
stream: MediaStream; stream: MediaStream | null;
noAudioTrack: boolean; noAudioTrack: boolean;
noAudioMessage: JSX.Element; noAudioMessage: JSX.Element;
onStreamStart: StreamStartEventHandler; onStreamStart: StreamOpenAndStartEventHandler;
onStreamEnd: StreamEndEventHandler; onStreamEnd: StreamEndEventHandler;
}; };

View File

@ -7,12 +7,13 @@ import { getHMSDuration, getDurationHMS } from "../helpers/timer";
import useSetting from "../hooks/useSetting"; import useSetting from "../hooks/useSetting";
import { RequestCloseEventHandler } from "../types/Events"; import {
RequestCloseEventHandler,
TimerStartEventHandler,
TimerStopEventHandler,
} from "../types/Events";
import { Timer } from "../types/Timer"; import { Timer } from "../types/Timer";
export type TimerStartEventHandler = (event: Timer) => void;
export type TimerStopEventHandler = () => void;
type StartTimerProps = { type StartTimerProps = {
isOpen: boolean; isOpen: boolean;
onRequestClose: RequestCloseEventHandler; onRequestClose: RequestCloseEventHandler;

View File

@ -1,8 +1,9 @@
import Konva from "konva"; import Konva from "konva";
import { DefaultDice } from "./Dice"; import { DefaultDice, DiceRoll } from "./Dice";
import { Map } from "./Map"; import { Map } from "./Map";
import { MapState } from "./MapState"; import { MapState } from "./MapState";
import { Note } from "./Note"; import { Note } from "./Note";
import { Timer } from "./Timer";
import { Token } from "./Token"; import { Token } from "./Token";
import { TokenState } from "./TokenState"; import { TokenState } from "./TokenState";
@ -43,3 +44,12 @@ export type NoteDragEventHandler = (
event: Konva.KonvaEventObject<DragEvent>, event: Konva.KonvaEventObject<DragEvent>,
noteId: string noteId: string
) => void; ) => void;
export type DiceShareChangeEventHandler = (share: boolean) => void;
export type DiceRollsChangeEventHandler = (newRolls: DiceRoll[]) => void;
export type StreamStartEventHandler = (stream: MediaStream) => void;
export type StreamEndEventHandler = (stream: MediaStream) => void;
export type TimerStartEventHandler = (event: Timer) => void;
export type TimerStopEventHandler = () => void;

View File

@ -22,7 +22,10 @@ import { Group, GroupContainer } from "./types/Group";
export type UpgradeEventHandler = (versionNumber: number) => void; export type UpgradeEventHandler = (versionNumber: number) => void;
type VersionCallback = (version: Version, onUpgrade?: UpgradeEventHandler); type VersionCallback = (
version: Version,
onUpgrade?: UpgradeEventHandler
) => void;
/** /**
* Mapping of version number to their upgrade function * Mapping of version number to their upgrade function
@ -285,7 +288,7 @@ export const versions: Record<number, VersionCallback> = {
v.stores({}).upgrade(async (tx) => { v.stores({}).upgrade(async (tx) => {
onUpgrade?.(15); onUpgrade?.(15);
const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
let tokenSizes: Record<string, {width: number, height: number}> = {}; let tokenSizes: Record<string, { width: number; height: number }> = {};
for (let token of tokens) { for (let token of tokens) {
const url = URL.createObjectURL(new Blob([token.file])); const url = URL.createObjectURL(new Blob([token.file]));
let image = new Image(); let image = new Image();
@ -775,7 +778,7 @@ export const versions: Record<number, VersionCallback> = {
34(v, onUpgrade) { 34(v, onUpgrade) {
v.stores({ groups: "id" }).upgrade(async (tx) => { v.stores({ groups: "id" }).upgrade(async (tx) => {
onUpgrade?.(34); onUpgrade?.(34);
function groupItems(items: {id: string, group: string}[]) { function groupItems(items: { id: string; group: string }[]) {
let groups: Group[] = []; let groups: Group[] = [];
let subGroups: Record<string, GroupContainer> = {}; let subGroups: Record<string, GroupContainer> = {};
for (let item of items) { for (let item of items) {
@ -841,7 +844,11 @@ export const latestVersion = 36;
* @param {number=} upTo version number to load up to, latest version if undefined * @param {number=} upTo version number to load up to, latest version if undefined
* @param {UpgradeEventHandler=} onUpgrade * @param {UpgradeEventHandler=} onUpgrade
*/ */
export function loadVersions(db: Dexie, upTo: number | undefined = latestVersion, onUpgrade: UpgradeEventHandler | undefined) { export function loadVersions(
db: Dexie,
upTo: number | undefined = latestVersion,
onUpgrade: UpgradeEventHandler | undefined
) {
for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) { for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) {
versions[versionNumber](db.version(versionNumber), onUpgrade); versions[versionNumber](db.version(versionNumber), onUpgrade);
} }
@ -883,12 +890,12 @@ function convertOldActionsToShapes(actions: any[], actionIndex: number) {
} }
// Helper to create a thumbnail for a file in a db // Helper to create a thumbnail for a file in a db
async function createDataThumbnail(data: any) : Promise<{ async function createDataThumbnail(data: any): Promise<{
file: Uint8Array, file: Uint8Array;
width: number, width: number;
height: number, height: number;
type: "file", type: "file";
id: "thumbnail", id: "thumbnail";
}> { }> {
let url: string; let url: string;
if (data?.resolutions?.low?.file) { if (data?.resolutions?.low?.file) {
@ -917,7 +924,9 @@ async function createDataThumbnail(data: any) : Promise<{
); );
} }
async function createDataOutline(data: any) : Promise<{id: string, outline: Outline}> { async function createDataOutline(
data: any
): Promise<{ id: string; outline: Outline }> {
const url = URL.createObjectURL(new Blob([data.file])); const url = URL.createObjectURL(new Blob([data.file]));
return await Dexie.waitFor( return await Dexie.waitFor(
new Promise((resolve) => { new Promise((resolve) => {

View File

@ -3069,6 +3069,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash.chunk@^4.2.6":
version "4.2.6"
resolved "https://registry.yarnpkg.com/@types/lodash.chunk/-/lodash.chunk-4.2.6.tgz#9d35f05360b0298715d7f3d9efb34dd4f77e5d2a"
integrity sha512-SPlusB7jxXyGcTXYcUdWr7WmhArO/rmTq54VN88iKMxGUhyg79I4Q8n4riGn3kjaTjOJrVlHhxgX/d7woak5BQ==
dependencies:
"@types/lodash" "*"
"@types/lodash.clonedeep@^4.5.6": "@types/lodash.clonedeep@^4.5.6":
version "4.5.6" version "4.5.6"
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b" resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b"