Converted /modals to typescript

This commit is contained in:
Nicola Thouliss 2021-06-02 17:49:31 +10:00
parent 32f6e1fb23
commit ecc4f67f37
33 changed files with 571 additions and 311 deletions

View File

@ -1,9 +1,8 @@
import React from "react";
import { Box } from "theme-ui"; import { Box } from "theme-ui";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
function LoadingOverlay({ bg }) { function LoadingOverlay({ bg }: any ) {
return ( return (
<Box <Box
sx={{ sx={{

View File

@ -1,7 +1,11 @@
import React from "react"; import React, { ReactChild } from "react";
import Modal from "react-modal"; import Modal, { Props } from "react-modal";
import { useThemeUI, Close } from "theme-ui"; import { useThemeUI, Close } from "theme-ui";
type ModalProps = Props & {
children: ReactChild | ReactChild[],
allowClose: boolean
}
function StyledModal({ function StyledModal({
isOpen, isOpen,
onRequestClose, onRequestClose,
@ -9,7 +13,7 @@ function StyledModal({
allowClose, allowClose,
style, style,
...props ...props
}) { }: ModalProps ) {
const { theme } = useThemeUI(); const { theme } = useThemeUI();
return ( return (
@ -19,7 +23,7 @@ function StyledModal({
style={{ style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.73)", zIndex: 100 }, overlay: { backgroundColor: "rgba(0, 0, 0, 0.73)", zIndex: 100 },
content: { content: {
backgroundColor: theme.colors.background, backgroundColor: theme.colors?.background,
top: "50%", top: "50%",
left: "50%", left: "50%",
right: "auto", right: "auto",
@ -28,7 +32,7 @@ function StyledModal({
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
maxHeight: "100%", maxHeight: "100%",
...style, ...style,
}, } as React.CSSProperties,
}} }}
{...props} {...props}
> >
@ -46,7 +50,7 @@ function StyledModal({
StyledModal.defaultProps = { StyledModal.defaultProps = {
allowClose: true, allowClose: true,
style: {}, style: {}
}; };
export default StyledModal; export default StyledModal;

View File

@ -1,10 +1,19 @@
import React, { useState } from "react"; import { useState } from "react";
import { Box, Slider as ThemeSlider } from "theme-ui"; import { Box, Slider as ThemeSlider, SliderProps } from "theme-ui";
function Slider({ min, max, value, ml, mr, labelFunc, ...rest }) { type SliderModalProps = SliderProps & {
min: number,
max: number,
value: number,
ml: any,
mr: any,
labelFunc: any
}
function Slider({ min, max, value, ml, mr, labelFunc, ...rest }: SliderModalProps ) {
const percentValue = ((value - min) * 100) / (max - min); const percentValue = ((value - min) * 100) / (max - min);
const [labelVisible, setLabelVisible] = useState(false); const [labelVisible, setLabelVisible] = useState<boolean>(false);
return ( return (
<Box sx={{ position: "relative" }} ml={ml} mr={mr}> <Box sx={{ position: "relative" }} ml={ml} mr={mr}>
@ -63,7 +72,7 @@ Slider.defaultProps = {
value: 0, value: 0,
ml: 0, ml: 0,
mr: 0, mr: 0,
labelFunc: (value) => value, labelFunc: (value: any) => value,
}; };
export default Slider; export default Slider;

View File

@ -24,6 +24,78 @@ import {
EditShapeAction, EditShapeAction,
RemoveShapeAction, RemoveShapeAction,
} from "../../actions"; } from "../../actions";
import { Fog, Path, Shape } from "../../helpers/drawing";
import Session from "../../network/Session";
import { Grid } from "../../helpers/grid";
import { ImageFile } from "../../helpers/image";
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,
label: string,
status: string[],
x: number,
y: number,
lastModifiedBy: string,
lastModified: number,
rotation: number,
locked: boolean,
visible: boolean
}
interface PathId extends Path {
id: string
}
interface ShapeId extends Shape {
id: string
}
export type MapState = {
tokens: Record<string, TokenState>,
drawShapes: PathId | ShapeId,
fogShapes: Fog[],
editFlags: string[],
notes: Note[],
mapId: string,
}
function Map({ function Map({
map, map,
@ -47,14 +119,36 @@ function Map({
allowNoteEditing, allowNoteEditing,
disabledTokens, disabledTokens,
session, session,
}: {
map: any
mapState: MapState
mapActions: any,
onMapTokenStateChange: any,
onMapTokenStateRemove: any,
onMapChange: any,
onMapReset: any,
onMapDraw: any,
onMapDrawUndo: any,
onMapDrawRedo: any,
onFogDraw: any,
onFogDrawUndo: any,
onFogDrawRedo: any,
onMapNoteChange: any,
onMapNoteRemove: any,
allowMapDrawing: boolean,
allowFogDrawing: boolean,
allowMapChange: boolean,
allowNoteEditing: boolean,
disabledTokens: any,
session: Session
}) { }) {
const { tokensById } = useTokenData(); const { tokensById } = useTokenData();
const [selectedToolId, setSelectedToolId] = useState("move"); const [selectedToolId, setSelectedToolId] = useState("move");
const { settings, setSettings } = useSettings(); const { settings, setSettings }: { settings: any, setSettings: any} = useSettings();
function handleToolSettingChange(tool, change) { function handleToolSettingChange(tool: any, change: any) {
setSettings((prevSettings) => ({ setSettings((prevSettings: any) => ({
...prevSettings, ...prevSettings,
[tool]: { [tool]: {
...prevSettings[tool], ...prevSettings[tool],
@ -66,7 +160,7 @@ function Map({
const drawShapes = Object.values(mapState?.drawShapes || {}); const drawShapes = Object.values(mapState?.drawShapes || {});
const fogShapes = Object.values(mapState?.fogShapes || {}); const fogShapes = Object.values(mapState?.fogShapes || {});
function handleToolAction(action) { function handleToolAction(action: string) {
if (action === "eraseAll") { if (action === "eraseAll") {
onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id))); onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id)));
} }
@ -84,27 +178,27 @@ function Map({
} }
} }
function handleMapShapeAdd(shape) { function handleMapShapeAdd(shape: Shape) {
onMapDraw(new AddShapeAction([shape])); onMapDraw(new AddShapeAction([shape]));
} }
function handleMapShapesRemove(shapeIds) { function handleMapShapesRemove(shapeIds: string[]) {
onMapDraw(new RemoveShapeAction(shapeIds)); onMapDraw(new RemoveShapeAction(shapeIds));
} }
function handleFogShapesAdd(shapes) { function handleFogShapesAdd(shapes: Shape[]) {
onFogDraw(new AddShapeAction(shapes)); onFogDraw(new AddShapeAction(shapes));
} }
function handleFogShapesCut(shapes) { function handleFogShapesCut(shapes: Shape[]) {
onFogDraw(new CutShapeAction(shapes)); onFogDraw(new CutShapeAction(shapes));
} }
function handleFogShapesRemove(shapeIds) { function handleFogShapesRemove(shapeIds: string[]) {
onFogDraw(new RemoveShapeAction(shapeIds)); onFogDraw(new RemoveShapeAction(shapeIds));
} }
function handleFogShapesEdit(shapes) { function handleFogShapesEdit(shapes: Shape[]) {
onFogDraw(new EditShapeAction(shapes)); onFogDraw(new EditShapeAction(shapes));
} }
@ -127,7 +221,7 @@ function Map({
disabledControls.push("note"); disabledControls.push("note");
} }
const disabledSettings = { fog: [], drawing: [] }; const disabledSettings: { fog: any[], drawing: any[]} = { fog: [], drawing: [] };
if (drawShapes.length === 0) { if (drawShapes.length === 0) {
disabledSettings.drawing.push("erase"); disabledSettings.drawing.push("erase");
} }
@ -166,10 +260,10 @@ function Map({
/> />
); );
const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); const [isTokenMenuOpen, setIsTokenMenuOpen]: [ isTokenMenuOpen: boolean, setIsTokenMenuOpen: React.Dispatch<React.SetStateAction<boolean>>] = useState<boolean>(false);
const [tokenMenuOptions, setTokenMenuOptions] = useState({}); const [tokenMenuOptions, setTokenMenuOptions]: [ tokenMenuOptions: any, setTokenMenuOptions: any ] = useState({});
const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); const [tokenDraggingOptions, setTokenDraggingOptions]: [ tokenDraggingOptions: any, setTokenDragginOptions: any ] = useState();
function handleTokenMenuOpen(tokenStateId, tokenImage) { function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) {
setTokenMenuOptions({ tokenStateId, tokenImage }); setTokenMenuOptions({ tokenStateId, tokenImage });
setIsTokenMenuOpen(true); setIsTokenMenuOpen(true);
} }
@ -200,7 +294,7 @@ function Map({
const tokenDragOverlay = tokenDraggingOptions && ( const tokenDragOverlay = tokenDraggingOptions && (
<TokenDragOverlay <TokenDragOverlay
onTokenStateRemove={(state) => { onTokenStateRemove={(state: any) => {
onMapTokenStateRemove(state); onMapTokenStateRemove(state);
setTokenDraggingOptions(null); setTokenDraggingOptions(null);
}} }}
@ -243,7 +337,6 @@ function Map({
<MapMeasure <MapMeasure
map={map} map={map}
active={selectedToolId === "measure"} active={selectedToolId === "measure"}
selectedToolSettings={settings[selectedToolId]}
/> />
); );
@ -254,15 +347,15 @@ function Map({
/> />
); );
const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false);
const [noteMenuOptions, setNoteMenuOptions] = useState({}); const [noteMenuOptions, setNoteMenuOptions] = useState<any>({});
const [noteDraggingOptions, setNoteDraggingOptions] = useState(); const [noteDraggingOptions, setNoteDraggingOptions]= useState<any>();
function handleNoteMenuOpen(noteId, noteNode) { function handleNoteMenuOpen(noteId: string, noteNode: any) {
setNoteMenuOptions({ noteId, noteNode }); setNoteMenuOptions({ noteId, noteNode });
setIsNoteMenuOpen(true); setIsNoteMenuOpen(true);
} }
function sortNotes(a, b, noteDraggingOptions) { function sortNotes(a: any, b: any, noteDraggingOptions: any) {
if ( if (
noteDraggingOptions && noteDraggingOptions &&
noteDraggingOptions.dragging && noteDraggingOptions.dragging &&
@ -287,7 +380,6 @@ function Map({
<MapNotes <MapNotes
map={map} map={map}
active={selectedToolId === "note"} active={selectedToolId === "note"}
selectedToolSettings={settings[selectedToolId]}
onNoteAdd={onMapNoteChange} onNoteAdd={onMapNoteChange}
onNoteChange={onMapNoteChange} onNoteChange={onMapNoteChange}
notes={ notes={
@ -302,7 +394,7 @@ function Map({
allowNoteEditing && allowNoteEditing &&
(selectedToolId === "note" || selectedToolId === "move") (selectedToolId === "note" || selectedToolId === "move")
} }
onNoteDragStart={(e, noteId) => onNoteDragStart={(e: any, noteId: any) =>
setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target })
} }
onNoteDragEnd={() => onNoteDragEnd={() =>
@ -328,7 +420,7 @@ function Map({
dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)} dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)}
noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup} noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup}
noteId={noteDraggingOptions && noteDraggingOptions.noteId} noteId={noteDraggingOptions && noteDraggingOptions.noteId}
onNoteRemove={(noteId) => { onNoteRemove={(noteId: any) => {
onMapNoteRemove(noteId); onMapNoteRemove(noteId);
setNoteDraggingOptions(null); setNoteDraggingOptions(null);
}} }}

View File

@ -3,10 +3,10 @@ import { EventEmitter } from "events";
const KeyboardContext = React.createContext({ keyEmitter: new EventEmitter() }); const KeyboardContext = React.createContext({ keyEmitter: new EventEmitter() });
export function KeyboardProvider({ children }) { export function KeyboardProvider({ children }: { children: any}) {
const [keyEmitter] = useState(new EventEmitter()); const [keyEmitter] = useState(new EventEmitter());
useEffect(() => { useEffect(() => {
function handleKeyDown(event) { function handleKeyDown(event: Event) {
// Ignore text input // Ignore text input
if ( if (
event.target instanceof HTMLInputElement || event.target instanceof HTMLInputElement ||
@ -17,7 +17,7 @@ export function KeyboardProvider({ children }) {
keyEmitter.emit("keyDown", event); keyEmitter.emit("keyDown", event);
} }
function handleKeyUp(event) { function handleKeyUp(event: Event) {
// Ignore text input // Ignore text input
if ( if (
event.target instanceof HTMLInputElement || event.target instanceof HTMLInputElement ||
@ -49,7 +49,7 @@ export function KeyboardProvider({ children }) {
* @param {KeyboardEvent} onKeyDown * @param {KeyboardEvent} onKeyDown
* @param {KeyboardEvent} onKeyUp * @param {KeyboardEvent} onKeyUp
*/ */
export function useKeyboard(onKeyDown, onKeyUp) { export function useKeyboard(onKeyDown: (...args: any[]) => void, onKeyUp: (...args: any[]) => void) {
const context = useContext(KeyboardContext); const context = useContext(KeyboardContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useKeyboard must be used within a KeyboardProvider"); throw new Error("useKeyboard must be used within a KeyboardProvider");
@ -78,7 +78,7 @@ export function useKeyboard(onKeyDown, onKeyUp) {
* Handler to handle a blur event. Useful when using a shortcut that uses the Alt or Cmd * Handler to handle a blur event. Useful when using a shortcut that uses the Alt or Cmd
* @param {FocusEvent} onBlur * @param {FocusEvent} onBlur
*/ */
export function useBlur(onBlur) { export function useBlur(onBlur: EventListenerOrEventListenerObject) {
useEffect(() => { useEffect(() => {
if (onBlur) { if (onBlur) {
window.addEventListener("blur", onBlur); window.addEventListener("blur", onBlur);

View File

@ -10,13 +10,29 @@ import { decode } from "@msgpack/msgpack";
import { useAuth } from "./AuthContext"; import { useAuth } from "./AuthContext";
import { useDatabase } from "./DatabaseContext"; import { useDatabase } from "./DatabaseContext";
import { tokens as defaultTokens } from "../tokens"; import { DefaultToken, FileToken, Token, tokens as defaultTokens } from "../tokens";
const TokenDataContext = React.createContext(); type TokenDataContext = {
tokens: Token[];
ownedTokens: Token[];
addToken: (token: Token) => Promise<void>;
removeToken: (id: string) => Promise<void>;
removeTokens: (ids: string[]) => Promise<void>;
updateToken: (id: string, update: Partial<Token>) => Promise<void>;
updateTokens: (ids: string[], update: Partial<Token>) => Promise<void>;
putToken: (token: Token) => Promise<void>;
getToken: (tokenId: string) => Token | undefined
tokensById: { [key: string]: Token; };
tokensLoading: boolean;
getTokenFromDB: (tokenId: string) => Promise<Token>;
loadTokens: (tokenIds: string[]) => Promise<void>;
}
const TokenDataContext = React.createContext<TokenDataContext | undefined>(undefined);
const cachedTokenMax = 100; const cachedTokenMax = 100;
export function TokenDataProvider({ children }) { export function TokenDataProvider({ children }: { children: any }) {
const { database, databaseStatus, worker } = useDatabase(); const { database, databaseStatus, worker } = useDatabase();
const { userId } = useAuth(); const { userId } = useAuth();
@ -24,7 +40,7 @@ export function TokenDataProvider({ children }) {
* Contains all tokens without any file data, * Contains all tokens without any file data,
* to ensure file data is present call loadTokens * to ensure file data is present call loadTokens
*/ */
const [tokens, setTokens] = useState([]); const [tokens, setTokens] = useState<Token[]>([]);
const [tokensLoading, setTokensLoading] = useState(true); const [tokensLoading, setTokensLoading] = useState(true);
useEffect(() => { useEffect(() => {
@ -32,13 +48,12 @@ export function TokenDataProvider({ children }) {
return; return;
} }
function getDefaultTokens() { function getDefaultTokens() {
const defaultTokensWithIds = []; const defaultTokensWithIds: Required<DefaultToken[]> = [];
for (let defaultToken of defaultTokens) { for (let defaultToken of defaultTokens) {
defaultTokensWithIds.push({ defaultTokensWithIds.push({
...defaultToken, ...defaultToken,
id: `__default-${defaultToken.name}`, id: `__default-${defaultToken.name}`,
owner: userId, owner: userId,
group: "default",
}); });
} }
return defaultTokensWithIds; return defaultTokensWithIds;
@ -46,19 +61,19 @@ export function TokenDataProvider({ children }) {
// Loads tokens without the file data to save memory // Loads tokens without the file data to save memory
async function loadTokens() { async function loadTokens() {
let storedTokens = []; let storedTokens: any = [];
// Try to load tokens with worker, fallback to database if failed // Try to load tokens with worker, fallback to database if failed
const packedTokens = await worker.loadData("tokens"); const packedTokens: ArrayLike<number> | BufferSource = await worker.loadData("tokens");
if (packedTokens) { if (packedTokens) {
storedTokens = decode(packedTokens); storedTokens = decode(packedTokens);
} else { } else {
console.warn("Unable to load tokens with worker, loading may be slow"); console.warn("Unable to load tokens with worker, loading may be slow");
await database.table("tokens").each((token) => { await database?.table("tokens").each((token: FileToken) => {
const { file, resolutions, ...rest } = token; const { file, ...rest } = token;
storedTokens.push(rest); storedTokens.push(rest);
}); });
} }
const sortedTokens = storedTokens.sort((a, b) => b.created - a.created); const sortedTokens = storedTokens.sort((a: any, b: any) => b.created - a.created);
const defaultTokensWithIds = getDefaultTokens(); const defaultTokensWithIds = getDefaultTokens();
const allTokens = [...sortedTokens, ...defaultTokensWithIds]; const allTokens = [...sortedTokens, ...defaultTokensWithIds];
setTokens(allTokens); setTokens(allTokens);
@ -79,7 +94,7 @@ export function TokenDataProvider({ children }) {
const getTokenFromDB = useCallback( const getTokenFromDB = useCallback(
async (tokenId) => { async (tokenId) => {
let token = await database.table("tokens").get(tokenId); let token = await database?.table("tokens").get(tokenId);
return token; return token;
}, },
[database] [database]
@ -90,23 +105,23 @@ export function TokenDataProvider({ children }) {
* Sorted by when they we're last used * Sorted by when they we're last used
*/ */
const updateCache = useCallback(async () => { const updateCache = useCallback(async () => {
const cachedTokens = await database const cachedTokens: Token[] | undefined = await database?.table("tokens").where("owner").notEqual(userId).sortBy("lastUsed");
.table("tokens") // TODO: handle undefined cachedTokens
.where("owner") if (!cachedTokens) {
.notEqual(userId) return;
.sortBy("lastUsed"); }
if (cachedTokens.length > cachedTokenMax) { if (cachedTokens?.length > cachedTokenMax) {
const cacheDeleteCount = cachedTokens.length - cachedTokenMax; const cacheDeleteCount = cachedTokens.length - cachedTokenMax
const idsToDelete = cachedTokens const idsToDelete = cachedTokens
.slice(0, cacheDeleteCount) .slice(0, cacheDeleteCount)
.map((token) => token.id); .map((token) => token.id);
database.table("tokens").where("id").anyOf(idsToDelete).delete(); database?.table("tokens").where("id").anyOf(idsToDelete).delete();
} }
}, [database, userId]); }, [database, userId]);
const addToken = useCallback( const addToken = useCallback(
async (token) => { async (token) => {
await database.table("tokens").add(token); await database?.table("tokens").add(token);
if (token.owner !== userId) { if (token.owner !== userId) {
await updateCache(); await updateCache();
} }
@ -115,23 +130,23 @@ export function TokenDataProvider({ children }) {
); );
const removeToken = useCallback( const removeToken = useCallback(
async (id) => { async (id: string) => {
await database.table("tokens").delete(id); await database?.table("tokens").delete(id);
}, },
[database] [database]
); );
const removeTokens = useCallback( const removeTokens = useCallback(
async (ids) => { async (ids: string[]) => {
await database.table("tokens").bulkDelete(ids); await database?.table("tokens").bulkDelete(ids);
}, },
[database] [database]
); );
const updateToken = useCallback( const updateToken = useCallback(
async (id, update) => { async (id: string, update: any) => {
const change = { lastModified: Date.now(), ...update }; const change = { lastModified: Date.now(), ...update };
await database.table("tokens").update(id, change); await database?.table("tokens").update(id, change);
}, },
[database] [database]
); );
@ -140,7 +155,7 @@ export function TokenDataProvider({ children }) {
async (ids, update) => { async (ids, update) => {
const change = { lastModified: Date.now(), ...update }; const change = { lastModified: Date.now(), ...update };
await Promise.all( await Promise.all(
ids.map((id) => database.table("tokens").update(id, change)) ids.map((id: string) => database?.table("tokens").update(id, change))
); );
}, },
[database] [database]
@ -148,7 +163,7 @@ export function TokenDataProvider({ children }) {
const putToken = useCallback( const putToken = useCallback(
async (token) => { async (token) => {
await database.table("tokens").put(token); await database?.table("tokens").put(token);
if (token.owner !== userId) { if (token.owner !== userId) {
await updateCache(); await updateCache();
} }
@ -157,13 +172,17 @@ export function TokenDataProvider({ children }) {
); );
const loadTokens = useCallback( const loadTokens = useCallback(
async (tokenIds) => { async (tokenIds: string[]) => {
const loadedTokens = await database.table("tokens").bulkGet(tokenIds); const loadedTokens: FileToken[] | undefined = await database?.table("tokens").bulkGet(tokenIds);
const loadedTokensById = loadedTokens.reduce((obj, token) => { const loadedTokensById = loadedTokens?.reduce((obj: { [key: string]: FileToken }, token: FileToken) => {
obj[token.id] = token; obj[token.id] = token;
return obj; return obj;
}, {}); }, {});
setTokens((prevTokens) => { if (!loadedTokensById) {
// TODO: whatever
return;
}
setTokens((prevTokens: Token[]) => {
return prevTokens.map((prevToken) => { return prevTokens.map((prevToken) => {
if (prevToken.id in loadedTokensById) { if (prevToken.id in loadedTokensById) {
return loadedTokensById[prevToken.id]; return loadedTokensById[prevToken.id];
@ -176,22 +195,13 @@ export function TokenDataProvider({ children }) {
[database] [database]
); );
const unloadTokens = useCallback(async () => {
setTokens((prevTokens) => {
return prevTokens.map((prevToken) => {
const { file, ...rest } = prevToken;
return rest;
});
});
}, []);
// Create DB observable to sync creating and deleting // Create DB observable to sync creating and deleting
useEffect(() => { useEffect(() => {
if (!database || databaseStatus === "loading") { if (!database || databaseStatus === "loading") {
return; return;
} }
function handleTokenChanges(changes) { function handleTokenChanges(changes: any) {
for (let change of changes) { for (let change of changes) {
if (change.table === "tokens") { if (change.table === "tokens") {
if (change.type === 1) { if (change.type === 1) {
@ -230,12 +240,12 @@ export function TokenDataProvider({ children }) {
const ownedTokens = tokens.filter((token) => token.owner === userId); const ownedTokens = tokens.filter((token) => token.owner === userId);
const tokensById = tokens.reduce((obj, token) => { const tokensById: { [key: string]: Token; } = tokens.reduce((obj: { [key: string]: Token }, token) => {
obj[token.id] = token; obj[token.id] = token;
return obj; return obj;
}, {}); }, {});
const value = { const value: TokenDataContext = {
tokens, tokens,
ownedTokens, ownedTokens,
addToken, addToken,
@ -249,7 +259,6 @@ export function TokenDataProvider({ children }) {
tokensLoading, tokensLoading,
getTokenFromDB, getTokenFromDB,
loadTokens, loadTokens,
unloadTokens,
}; };
return ( return (
@ -259,7 +268,7 @@ export function TokenDataProvider({ children }) {
); );
} }
export function useTokenData() { export function useTokenData(): TokenDataContext {
const context = useContext(TokenDataContext); const context = useContext(TokenDataContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useTokenData must be used within a TokenDataProvider"); throw new Error("useTokenData must be used within a TokenDataProvider");

1
src/global.d.ts vendored
View File

@ -5,3 +5,4 @@ declare module 'fake-indexeddb/lib/FDBKeyRange';
declare module '*.glb'; declare module '*.glb';
declare module '*.png'; declare module '*.png';
declare module '*.mp4'; declare module '*.mp4';
declare module '*.bin';

View File

@ -70,7 +70,7 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle"
* @typedef {("fill"|"stroke")} PathType * @typedef {("fill"|"stroke")} PathType
*/ */
// type PathType = "fill" | "stroke" type PathType = "fill" | "stroke"
/** /**
* @typedef Path * @typedef Path
@ -83,15 +83,15 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle"
* @property {"path"} type * @property {"path"} type
*/ */
// type Path = { export type Path = {
// blend: boolean, blend: boolean,
// color: string, color: string,
// data: PointsData, data: PointsData,
// id: string, id: string,
// pathType: PathType, pathType: PathType,
// strokeWidth: number, strokeWidth: number,
// type: "path" type: "path"
// } }
/** /**
* @typedef Shape * @typedef Shape
@ -104,15 +104,15 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle"
* @property {"shape"} type * @property {"shape"} type
*/ */
// type Shape = { export type Shape = {
// blend: boolean, blend: boolean,
// color: string, color: string,
// data: ShapeData, data: ShapeData,
// id: string, id: string,
// shapeType: ShapeType, shapeType: ShapeType,
// strokeWidth: number, strokeWidth: number,
// type: "shape" type: "shape"
// } }
/** /**
* @typedef Fog * @typedef Fog
@ -124,7 +124,7 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle"
* @property {boolean} visible * @property {boolean} visible
*/ */
type Fog = { export type Fog = {
color: string, color: string,
data: FogData, data: FogData,
id: string, id: string,

View File

@ -38,10 +38,10 @@ type GridMeasurement ={
* @property {GridMeasurement} measurement * @property {GridMeasurement} measurement
*/ */
export type Grid = { export type Grid = {
inset: GridInset, inset?: GridInset,
size: Vector2, size: Vector2,
type: ("square"|"hexVertical"|"hexHorizontal"), type: ("square"|"hexVertical"|"hexHorizontal"),
measurement: GridMeasurement measurement?: GridMeasurement
} }
/** /**
@ -51,7 +51,7 @@ export type Grid = {
* @param {number} baseHeight Height of the grid in pixels before inset * @param {number} baseHeight Height of the grid in pixels before inset
* @returns {Size} * @returns {Size}
*/ */
export function getGridPixelSize(grid: Grid, baseWidth: number, baseHeight: number): Size { export function getGridPixelSize(grid: Required<Grid>, baseWidth: number, baseHeight: number): Size {
const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth; const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth;
const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight; const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight;
return new Size(width, height); return new Size(width, height);
@ -225,7 +225,7 @@ export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: num
* @param {number} mapHeight Height of the map in pixels before inset * @param {number} mapHeight Height of the map in pixels before inset
* @returns {GridInset} * @returns {GridInset}
*/ */
export function getGridUpdatedInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset { export function getGridUpdatedInset(grid: Required<Grid>, mapWidth: number, mapHeight: number): GridInset {
let inset = grid.inset; let inset = grid.inset;
// Take current inset width and use it to calculate the new height // Take current inset width and use it to calculate the new height
if (grid.size.x > 0 && grid.size.x > 0) { if (grid.size.x > 0 && grid.size.x > 0) {
@ -295,7 +295,7 @@ export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizo
* @param {Vector2} b * @param {Vector2} b
* @param {Size} cellSize * @param {Size} cellSize
*/ */
export function gridDistance(grid: Grid, a: Vector2, b: Vector2, cellSize: Size) { export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellSize: Size) {
// Get grid coordinates // Get grid coordinates
const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize); const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize);
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);

View File

@ -109,7 +109,7 @@ export async function resizeImage(image: HTMLImageElement, size: number, type: s
* @property {string} id * @property {string} id
*/ */
type ImageFile = { export type ImageFile = {
file: Uint8Array | null, file: Uint8Array | null,
width: number, width: number,
height: number, height: number,

View File

@ -8,7 +8,7 @@ import { groupBy } from "./shared";
*/ */
// Helper for generating search results for items // Helper for generating search results for items
export function useSearch(items: [], search: string) { export function useSearch(items: any[], search: string) {
// TODO: add types to search items -> don't like the never type // TODO: add types to search items -> don't like the never type
const [filteredItems, setFilteredItems]: [ const [filteredItems, setFilteredItems]: [
filteredItems: any, filteredItems: any,
@ -18,10 +18,7 @@ export function useSearch(items: [], search: string) {
filteredItemScores: {}, filteredItemScores: {},
setFilteredItemScores: React.Dispatch<React.SetStateAction<{}>> setFilteredItemScores: React.Dispatch<React.SetStateAction<{}>>
] = useState({}); ] = useState({});
const [fuse, setFuse]: [ const [fuse, setFuse] = useState<any>();
fuse: Fuse<never> | undefined,
setFuse: React.Dispatch<Fuse<never> | undefined>
] = useState();
// Update search index when items change // Update search index when items change
useEffect(() => { useEffect(() => {
@ -85,7 +82,7 @@ export function useGroup(
export function handleItemSelect( export function handleItemSelect(
item: any, item: any,
selectMode: any, selectMode: any,
selectedIds: number[], selectedIds: string[],
setSelectedIds: any, setSelectedIds: any,
itemsByGroup: any, itemsByGroup: any,
itemGroups: any itemGroups: any
@ -120,15 +117,15 @@ export function handleItemSelect(
const lastIndex = items.findIndex( const lastIndex = items.findIndex(
(m: any) => m.id === selectedIds[selectedIds.length - 1] (m: any) => m.id === selectedIds[selectedIds.length - 1]
); );
let idsToAdd: number[] = []; let idsToAdd: string[] = [];
let idsToRemove: number[] = []; let idsToRemove: string[] = [];
const direction = mapIndex > lastIndex ? 1 : -1; const direction = mapIndex > lastIndex ? 1 : -1;
for ( for (
let i = lastIndex + direction; let i = lastIndex + direction;
direction < 0 ? i >= mapIndex : i <= mapIndex; direction < 0 ? i >= mapIndex : i <= mapIndex;
i += direction i += direction
) { ) {
const itemId: number = items[i].id; const itemId: string = items[i].id;
if (selectedIds.includes(itemId)) { if (selectedIds.includes(itemId)) {
idsToRemove.push(itemId); idsToRemove.push(itemId);
} else { } else {

View File

@ -1,4 +1,4 @@
import { ModelJSON, WeightsManifestConfig } from "@tensorflow/tfjs-core/dist/io/types"; import { ModelJSON } from "@tensorflow/tfjs-core/dist/io/types";
import blobToBuffer from "../helpers/blobToBuffer"; import blobToBuffer from "../helpers/blobToBuffer";
class Model { class Model {

View File

@ -1,9 +1,16 @@
import React from "react";
import { Box, Label, Text } from "theme-ui"; import { Box, Label, Text } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
function AddPartyMemberModal({ isOpen, onRequestClose, gameId }) { function AddPartyMemberModal({
isOpen,
onRequestClose,
gameId,
}: {
isOpen: boolean;
onRequestClose: any;
gameId: string;
}) {
return ( return (
<Modal isOpen={isOpen} onRequestClose={onRequestClose}> <Modal isOpen={isOpen} onRequestClose={onRequestClose}>
<Box> <Box>

View File

@ -1,27 +1,27 @@
import React, { useState, useRef } from "react"; import { useState, useRef, ChangeEvent, FormEvent } from "react";
import { Box, Input, Button, Label, Flex } from "theme-ui"; import { Box, Input, Button, Label, Flex } from "theme-ui";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
function AuthModal({ isOpen, onSubmit }) { function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassword: string) => void}) {
const { password, setPassword } = useAuth(); const { password, setPassword } = useAuth();
const [tmpPassword, setTempPassword] = useState(password); const [tmpPassword, setTempPassword] = useState<string>(password);
function handleChange(event) { function handleChange(event: ChangeEvent<HTMLInputElement>): void {
setTempPassword(event.target.value); setTempPassword(event.target?.value);
} }
function handleSubmit(event) { function handleSubmit(event: FormEvent<HTMLElement>): void {
event.preventDefault(); event.preventDefault();
setPassword(tmpPassword); setPassword(tmpPassword);
onSubmit(tmpPassword); onSubmit(tmpPassword);
} }
const inputRef = useRef(); const inputRef = useRef<any>();
function focusInput() { function focusInput(): void {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current?.focus();
} }
return ( return (

View File

@ -1,4 +1,4 @@
import React, { useRef } from "react"; import { useRef } from "react";
import { Box, Input, Button, Label, Flex } from "theme-ui"; import { Box, Input, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
@ -9,10 +9,16 @@ function ChangeNicknameModal({
onChangeSubmit, onChangeSubmit,
nickname, nickname,
onChange, onChange,
}: {
isOpen: boolean,
onRequestClose: () => void,
onChangeSubmit: any,
nickname: string,
onChange: any,
}) { }) {
const inputRef = useRef(); const inputRef = useRef<HTMLInputElement | null>(null);
function focusInput() { function focusInput() {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current?.focus();
} }
return ( return (

View File

@ -1,8 +1,16 @@
import React from "react";
import { Box, Label, Flex, Button, Text } from "theme-ui"; import { Box, Label, Flex, Button, Text } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
type ConfirmModalProps = {
isOpen: boolean,
onRequestClose: () => void,
onConfirm: () => void,
confirmText: string,
label: string,
description: string,
}
function ConfirmModal({ function ConfirmModal({
isOpen, isOpen,
onRequestClose, onRequestClose,
@ -10,12 +18,12 @@ function ConfirmModal({
confirmText, confirmText,
label, label,
description, description,
}) { }: ConfirmModalProps ) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ maxWidth: "300px" }} style={{ content: { maxWidth: "300px" } }}
> >
<Box> <Box>
<Label py={2}>{label}</Label> <Label py={2}>{label}</Label>

View File

@ -1,24 +1,32 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Box, Button, Label, Flex } from "theme-ui"; import { Box, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
import Select from "../components/Select"; import Select from "../components/Select";
type EditGroupProps = {
isOpen: boolean,
onRequestClose: () => void,
onChange: any,
groups: string[],
defaultGroup: string | undefined | false,
}
function EditGroupModal({ function EditGroupModal({
isOpen, isOpen,
onRequestClose, onRequestClose,
onChange, onChange,
groups, groups,
defaultGroup, defaultGroup,
}) { }: EditGroupProps) {
const [value, setValue] = useState(); const [value, setValue] = useState<{ value: string; label: string; } | undefined>();
const [options, setOptions] = useState([]); const [options, setOptions] = useState<{ value: string; label: string; }[]>([]);
useEffect(() => { useEffect(() => {
if (defaultGroup) { if (defaultGroup) {
setValue({ value: defaultGroup, label: defaultGroup }); setValue({ value: defaultGroup, label: defaultGroup });
} else { } else {
setValue(); setValue(undefined);
} }
}, [defaultGroup]); }, [defaultGroup]);
@ -26,7 +34,7 @@ function EditGroupModal({
setOptions(groups.map((group) => ({ value: group, label: group }))); setOptions(groups.map((group) => ({ value: group, label: group })));
}, [groups]); }, [groups]);
function handleCreate(group) { function handleCreate(group: string) {
const newOption = { value: group, label: group }; const newOption = { value: group, label: group };
setValue(newOption); setValue(newOption);
setOptions((prev) => [...prev, newOption]); setOptions((prev) => [...prev, newOption]);
@ -40,7 +48,7 @@ function EditGroupModal({
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ overflow: "visible" }} style={{ content: { overflow: "visible" } }}
> >
<Box onSubmit={handleChange} sx={{ width: "300px" }}> <Box onSubmit={handleChange} sx={{ width: "300px" }}>
<Label py={2}>Select or add a group</Label> <Label py={2}>Select or add a group</Label>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Flex, Label } from "theme-ui"; import { Button, Flex, Label } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
@ -12,8 +12,15 @@ import { isEmpty } from "../helpers/shared";
import { getGridDefaultInset } from "../helpers/grid"; import { getGridDefaultInset } from "../helpers/grid";
import useResponsiveLayout from "../hooks/useResponsiveLayout"; import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { MapState } from "../components/map/Map";
function EditMapModal({ isOpen, onDone, mapId }) { type EditMapProps = {
isOpen: boolean,
onDone: any,
mapId: string
}
function EditMapModal({ isOpen, onDone, mapId }: EditMapProps) {
const { const {
updateMap, updateMap,
updateMapState, updateMapState,
@ -23,8 +30,8 @@ function EditMapModal({ isOpen, onDone, mapId }) {
} = useMapData(); } = useMapData();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [map, setMap] = useState(); const [map, setMap] = useState<any>();
const [mapState, setMapState] = useState(); const [mapState, setMapState] = useState<MapState>();
// Load full map when modal is opened // Load full map when modal is opened
useEffect(() => { useEffect(() => {
async function loadMap() { async function loadMap() {
@ -43,8 +50,8 @@ function EditMapModal({ isOpen, onDone, mapId }) {
if (isOpen && mapId) { if (isOpen && mapId) {
loadMap(); loadMap();
} else { } else {
setMap(); setMap(undefined);
setMapState(); setMapState(undefined);
} }
}, [isOpen, mapId, getMapFromDB, getMapStateFromDB, getMap]); }, [isOpen, mapId, getMapFromDB, getMapStateFromDB, getMap]);
@ -64,19 +71,19 @@ function EditMapModal({ isOpen, onDone, mapId }) {
*/ */
// Local cache of map setting changes // Local cache of map setting changes
// Applied when done is clicked or map selection is changed // Applied when done is clicked or map selection is changed
const [mapSettingChanges, setMapSettingChanges] = useState({}); const [mapSettingChanges, setMapSettingChanges] = useState<any>({});
const [mapStateSettingChanges, setMapStateSettingChanges] = useState({}); const [mapStateSettingChanges, setMapStateSettingChanges] = useState<any>({});
function handleMapSettingsChange(key, value) { function handleMapSettingsChange(key: string, value: string) {
setMapSettingChanges((prevChanges) => ({ setMapSettingChanges((prevChanges: any) => ({
...prevChanges, ...prevChanges,
[key]: value, [key]: value,
lastModified: Date.now(), lastModified: Date.now(),
})); }));
} }
function handleMapStateSettingsChange(key, value) { function handleMapStateSettingsChange(key: string, value: string) {
setMapStateSettingChanges((prevChanges) => ({ setMapStateSettingChanges((prevChanges: any) => ({
...prevChanges, ...prevChanges,
[key]: value, [key]: value,
})); }));
@ -137,7 +144,7 @@ function EditMapModal({ isOpen, onDone, mapId }) {
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={handleClose} onRequestClose={handleClose}
style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} style={{ content: {maxWidth: layout.modalSize, width: "calc(100% - 16px)"} }}
> >
<Flex <Flex
sx={{ sx={{

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button, Flex, Label } from "theme-ui"; import { Button, Flex, Label } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
@ -11,12 +11,19 @@ import { useTokenData } from "../contexts/TokenDataContext";
import { isEmpty } from "../helpers/shared"; import { isEmpty } from "../helpers/shared";
import useResponsiveLayout from "../hooks/useResponsiveLayout"; import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { Token } from "../tokens";
function EditTokenModal({ isOpen, onDone, tokenId }) { type EditModalProps = {
isOpen: boolean,
onDone: () => void,
tokenId: string,
};
function EditTokenModal({ isOpen, onDone, tokenId }: EditModalProps) {
const { updateToken, getTokenFromDB } = useTokenData(); const { updateToken, getTokenFromDB } = useTokenData();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [token, setToken] = useState(); const [token, setToken] = useState<Token>();
useEffect(() => { useEffect(() => {
async function loadToken() { async function loadToken() {
setIsLoading(true); setIsLoading(true);
@ -27,7 +34,7 @@ function EditTokenModal({ isOpen, onDone, tokenId }) {
if (isOpen && tokenId) { if (isOpen && tokenId) {
loadToken(); loadToken();
} else { } else {
setToken(); setToken(undefined);
} }
}, [isOpen, tokenId, getTokenFromDB]); }, [isOpen, tokenId, getTokenFromDB]);
@ -41,10 +48,13 @@ function EditTokenModal({ isOpen, onDone, tokenId }) {
onDone(); onDone();
} }
const [tokenSettingChanges, setTokenSettingChanges] = useState({}); const [tokenSettingChanges, setTokenSettingChanges] = useState<any>({});
function handleTokenSettingsChange(key, value) { function handleTokenSettingsChange(key: any, value: any) {
setTokenSettingChanges((prevChanges) => ({ ...prevChanges, [key]: value })); setTokenSettingChanges((prevChanges: any) => ({
...prevChanges,
[key]: value,
}));
} }
async function applyTokenChanges() { async function applyTokenChanges() {
@ -72,8 +82,7 @@ function EditTokenModal({ isOpen, onDone, tokenId }) {
isOpen={isOpen} isOpen={isOpen}
onRequestClose={handleClose} onRequestClose={handleClose}
style={{ style={{
maxWidth: layout.modalSize, content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" },
width: "calc(100% - 16px)",
}} }}
> >
<Flex <Flex

View File

@ -1,14 +1,13 @@
import React from "react";
import { Box, Label, Text } from "theme-ui"; import { Box, Label, Text } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
function ForceUpdateModal({ isOpen }) { function ForceUpdateModal({ isOpen }: { isOpen: boolean }) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
allowClose={false} allowClose={false}
style={{ maxWidth: "450px" }} style={{ content: { maxWidth: "450px" } }}
> >
<Box> <Box>
<Label py={2}>New Update Available</Label> <Label py={2}>New Update Available</Label>

View File

@ -1,14 +1,13 @@
import React from "react";
import { Box, Label, Flex, Button, Text } from "theme-ui"; import { Box, Label, Flex, Button, Text } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
function GameExpiredModal({ isOpen, onRequestClose }) { function GameExpiredModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void }) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ maxWidth: "450px" }} style={{ content: { maxWidth: "450px" } }}
> >
<Box> <Box>
<Label py={2}>Game Timed Out</Label> <Label py={2}>Game Timed Out</Label>

View File

@ -1,4 +1,3 @@
import React from "react";
import { Box, Label, Text } from "theme-ui"; import { Box, Label, Text } from "theme-ui";
import raw from "raw.macro"; import raw from "raw.macro";
@ -8,12 +7,12 @@ import Link from "../components/Link";
const gettingStarted = raw("../docs/howTo/gettingStarted.md"); const gettingStarted = raw("../docs/howTo/gettingStarted.md");
function GettingStartedModal({ isOpen, onRequestClose }) { function GettingStartedModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void } ) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ maxWidth: "450px" }} style={{ content: { maxWidth: "450px" } }}
> >
<Box> <Box>
<Label py={2}>Getting Started</Label> <Label py={2}>Getting Started</Label>

View File

@ -1,4 +1,4 @@
import React, { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import { Box, Label, Text, Button, Flex } from "theme-ui"; import { Box, Label, Text, Button, Flex } from "theme-ui";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import * as Comlink from "comlink"; import * as Comlink from "comlink";
@ -16,24 +16,25 @@ import { useDatabase } from "../contexts/DatabaseContext";
import SelectDataModal from "./SelectDataModal"; import SelectDataModal from "./SelectDataModal";
import { getDatabase } from "../database"; import { getDatabase } from "../database";
import { Map, MapState, TokenState } from "../components/map/Map";
const importDBName = "OwlbearRodeoImportDB"; const importDBName = "OwlbearRodeoImportDB";
function ImportExportModal({ isOpen, onRequestClose }) { function ImportExportModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void}) {
const { worker } = useDatabase(); const { worker } = useDatabase();
const { userId } = useAuth(); const { userId } = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState<Error>();
const backgroundTaskRunningRef = useRef(false); const backgroundTaskRunningRef = useRef(false);
const fileInputRef = useRef(); const fileInputRef = useRef<any>();
const [showImportSelector, setShowImportSelector] = useState(false); const [showImportSelector, setShowImportSelector] = useState(false);
const [showExportSelector, setShowExportSelector] = useState(false); const [showExportSelector, setShowExportSelector] = useState(false);
const { addToast } = useToasts(); const { addToast } = useToasts();
function addSuccessToast(message, maps, tokens) { function addSuccessToast(message: string, maps: any, tokens: TokenState[]) {
const mapText = `${maps.length} map${maps.length > 1 ? "s" : ""}`; const mapText = `${maps.length} map${maps.length > 1 ? "s" : ""}`;
const tokenText = `${tokens.length} token${tokens.length > 1 ? "s" : ""}`; const tokenText = `${tokens.length} token${tokens.length > 1 ? "s" : ""}`;
if (maps.length > 0 && tokens.length > 0) { if (maps.length > 0 && tokens.length > 0) {
@ -53,11 +54,11 @@ function ImportExportModal({ isOpen, onRequestClose }) {
const loadingProgressRef = useRef(0); const loadingProgressRef = useRef(0);
function handleDBProgress({ completedRows, totalRows }) { function handleDBProgress({ completedRows, totalRows }: { completedRows: number, totalRows: number }) {
loadingProgressRef.current = completedRows / totalRows; loadingProgressRef.current = completedRows / totalRows;
} }
async function handleImportDatabase(file) { async function handleImportDatabase(file: File) {
setIsLoading(true); setIsLoading(true);
backgroundTaskRunningRef.current = true; backgroundTaskRunningRef.current = true;
try { try {
@ -94,7 +95,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
} }
useEffect(() => { useEffect(() => {
function handleBeforeUnload(event) { function handleBeforeUnload(event: any) {
if (backgroundTaskRunningRef.current) { if (backgroundTaskRunningRef.current) {
event.returnValue = event.returnValue =
"Database is still processing, are you sure you want to leave?"; "Database is still processing, are you sure you want to leave?";
@ -121,7 +122,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
setShowImportSelector(false); setShowImportSelector(false);
} }
async function handleImportSelectorConfirm(checkedMaps, checkedTokens) { async function handleImportSelectorConfirm(checkedMaps: any, checkedTokens: TokenState[]) {
setIsLoading(true); setIsLoading(true);
backgroundTaskRunningRef.current = true; backgroundTaskRunningRef.current = true;
setShowImportSelector(false); setShowImportSelector(false);
@ -131,11 +132,11 @@ function ImportExportModal({ isOpen, onRequestClose }) {
const db = getDatabase({}); const db = getDatabase({});
try { try {
// Keep track of a mapping of old token ids to new ones to apply them to the map states // Keep track of a mapping of old token ids to new ones to apply them to the map states
let newTokenIds = {}; let newTokenIds: {[id: string]: string} = {};
if (checkedTokens.length > 0) { if (checkedTokens.length > 0) {
const tokenIds = checkedTokens.map((token) => token.id); const tokenIds = checkedTokens.map((token) => token.id);
const tokensToAdd = await importDB.table("tokens").bulkGet(tokenIds); const tokensToAdd: TokenState[] = await importDB.table("tokens").bulkGet(tokenIds);
let newTokens = []; let newTokens: TokenState[] = [];
for (let token of tokensToAdd) { for (let token of tokensToAdd) {
const newId = shortid.generate(); const newId = shortid.generate();
newTokenIds[token.id] = newId; newTokenIds[token.id] = newId;
@ -146,12 +147,12 @@ function ImportExportModal({ isOpen, onRequestClose }) {
} }
if (checkedMaps.length > 0) { if (checkedMaps.length > 0) {
const mapIds = checkedMaps.map((map) => map.id); const mapIds = checkedMaps.map((map: any) => map.id);
const mapsToAdd = await importDB.table("maps").bulkGet(mapIds); const mapsToAdd = await importDB.table("maps").bulkGet(mapIds);
let newMaps = []; let newMaps = [];
let newStates = []; let newStates = [];
for (let map of mapsToAdd) { for (let map of mapsToAdd) {
let state = await importDB.table("states").get(map.id); let state: MapState = await importDB.table("states").get(map.id);
// Apply new token ids to imported state // Apply new token ids to imported state
for (let tokenState of Object.values(state.tokens)) { for (let tokenState of Object.values(state.tokens)) {
if (tokenState.tokenId in newTokenIds) { if (tokenState.tokenId in newTokenIds) {
@ -179,7 +180,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
backgroundTaskRunningRef.current = false; backgroundTaskRunningRef.current = false;
} }
function exportSelectorFilter(table, value) { function exportSelectorFilter(table: any, value: Map | TokenState) {
// Only show owned maps and tokens // Only show owned maps and tokens
if (table === "maps" || table === "tokens") { if (table === "maps" || table === "tokens") {
if (value.owner === userId) { if (value.owner === userId) {
@ -197,7 +198,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
setShowExportSelector(false); setShowExportSelector(false);
} }
async function handleExportSelectorConfirm(checkedMaps, checkedTokens) { async function handleExportSelectorConfirm(checkedMaps: Map[], checkedTokens: TokenState[]) {
setShowExportSelector(false); setShowExportSelector(false);
setIsLoading(true); setIsLoading(true);
backgroundTaskRunningRef.current = true; backgroundTaskRunningRef.current = true;
@ -238,7 +239,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
Select import or export then select the data you wish to use Select import or export then select the data you wish to use
</Text> </Text>
<input <input
onChange={(event) => handleImportDatabase(event.target.files[0])} onChange={(event) => event.target.files && handleImportDatabase(event.target.files[0])}
type="file" type="file"
accept=".owlbear" accept=".owlbear"
style={{ display: "none" }} style={{ display: "none" }}
@ -272,7 +273,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
</Box> </Box>
</Box> </Box>
)} )}
<ErrorBanner error={error} onRequestClose={() => setError()} /> <ErrorBanner error={error} onRequestClose={() => setError(undefined)} />
<SelectDataModal <SelectDataModal
isOpen={showImportSelector} isOpen={showImportSelector}
onRequestClose={handleImportSelectorClose} onRequestClose={handleImportSelectorClose}

View File

@ -1,23 +1,23 @@
import React, { useState, useRef } from "react"; import { useState, useRef, FormEvent, ChangeEvent } from "react";
import { Box, Label, Input, Button, Flex } from "theme-ui"; import { Box, Label, Input, Button, Flex } from "theme-ui";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
function JoinModal({ isOpen, onRequestClose }) { function JoinModal({ isOpen, onRequestClose }: any) {
let history = useHistory(); let history = useHistory();
const [gameId, setGameId] = useState(""); const [gameId, setGameId] = useState("");
function handleChange(event) { function handleChange(event: ChangeEvent<HTMLInputElement>) {
setGameId(event.target.value); setGameId(event.target?.value);
} }
function handleSubmit(event) { function handleSubmit(event: FormEvent<HTMLDivElement>) {
event.preventDefault(); event.preventDefault();
history.push(`/game/${gameId}`); history.push(`/game/${gameId}`);
} }
const inputRef = useRef(); const inputRef = useRef<any>();
function focusInput() { function focusInput() {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current.focus();
} }
@ -27,6 +27,7 @@ function JoinModal({ isOpen, onRequestClose }) {
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
onAfterOpen={focusInput} onAfterOpen={focusInput}
> >
<Flex <Flex
sx={{ sx={{

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { ChangeEvent, useEffect, useState } from "react";
import { Box, Label, Flex, Button, Text, Checkbox, Divider } from "theme-ui"; import { Box, Label, Flex, Button, Text, Checkbox, Divider } from "theme-ui";
import SimpleBar from "simplebar-react"; import SimpleBar from "simplebar-react";
@ -6,6 +6,15 @@ import Modal from "../components/Modal";
import LoadingOverlay from "../components/LoadingOverlay"; import LoadingOverlay from "../components/LoadingOverlay";
import { getDatabase } from "../database"; import { getDatabase } from "../database";
import { Props } from "react-modal";
type SelectDataProps = Props & {
onConfirm: any,
confirmText: string,
label: string,
databaseName: string,
filter: any,
}
function SelectDataModal({ function SelectDataModal({
isOpen, isOpen,
@ -15,10 +24,10 @@ function SelectDataModal({
label, label,
databaseName, databaseName,
filter, filter,
}) { }: SelectDataProps) {
const [maps, setMaps] = useState({}); const [maps, setMaps] = useState<any>({});
const [tokensByMap, setTokensByMap] = useState({}); const [tokensByMap, setTokensByMap] = useState<any>({});
const [tokens, setTokens] = useState({}); const [tokens, setTokens] = useState<any>({});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const hasMaps = Object.values(maps).length > 0; const hasMaps = Object.values(maps).length > 0;
@ -29,9 +38,9 @@ function SelectDataModal({
if (isOpen && databaseName) { if (isOpen && databaseName) {
setIsLoading(true); setIsLoading(true);
const db = getDatabase({ addons: [] }, databaseName); const db = getDatabase({ addons: [] }, databaseName);
let loadedMaps = {}; let loadedMaps: any = [];
let loadedTokensByMap = {}; let loadedTokensByMap: any = {};
let loadedTokens = {}; let loadedTokens: any = [];
await db await db
.table("maps") .table("maps")
.filter((map) => filter("maps", map, map.id)) .filter((map) => filter("maps", map, map.id))
@ -44,7 +53,7 @@ function SelectDataModal({
.each((state) => { .each((state) => {
loadedTokensByMap[state.mapId] = new Set( loadedTokensByMap[state.mapId] = new Set(
Object.values(state.tokens).map( Object.values(state.tokens).map(
(tokenState) => tokenState.tokenId (tokenState: any) => tokenState.tokenId
) )
); );
}); });
@ -73,9 +82,9 @@ function SelectDataModal({
}, [isOpen, databaseName, filter]); }, [isOpen, databaseName, filter]);
// An object mapping a tokenId to how many checked maps it is currently used in // An object mapping a tokenId to how many checked maps it is currently used in
const [tokenUsedCount, setTokenUsedCount] = useState({}); const [tokenUsedCount, setTokenUsedCount] = useState<any>({});
useEffect(() => { useEffect(() => {
let tokensUsed = {}; let tokensUsed: any = {};
for (let mapId in maps) { for (let mapId in maps) {
if (maps[mapId].checked && mapId in tokensByMap) { if (maps[mapId].checked && mapId in tokensByMap) {
for (let tokenId of tokensByMap[mapId]) { for (let tokenId of tokensByMap[mapId]) {
@ -89,7 +98,7 @@ function SelectDataModal({
} }
setTokenUsedCount(tokensUsed); setTokenUsedCount(tokensUsed);
// Update tokens to ensure used tokens are checked // Update tokens to ensure used tokens are checked
setTokens((prevTokens) => { setTokens((prevTokens: any) => {
let newTokens = { ...prevTokens }; let newTokens = { ...prevTokens };
for (let id in newTokens) { for (let id in newTokens) {
if (id in tokensUsed) { if (id in tokensUsed) {
@ -101,13 +110,13 @@ function SelectDataModal({
}, [maps, tokensByMap]); }, [maps, tokensByMap]);
function handleConfirm() { function handleConfirm() {
let checkedMaps = Object.values(maps).filter((map) => map.checked); let checkedMaps = Object.values(maps).filter((map: any) => map.checked);
let checkedTokens = Object.values(tokens).filter((token) => token.checked); let checkedTokens = Object.values(tokens).filter((token: any) => token.checked);
onConfirm(checkedMaps, checkedTokens); onConfirm(checkedMaps, checkedTokens);
} }
function handleSelectMapsChanged(event) { function handleSelectMapsChanged(event: ChangeEvent<HTMLInputElement>) {
setMaps((prevMaps) => { setMaps((prevMaps: any) => {
let newMaps = { ...prevMaps }; let newMaps = { ...prevMaps };
for (let id in newMaps) { for (let id in newMaps) {
newMaps[id].checked = event.target.checked; newMaps[id].checked = event.target.checked;
@ -116,7 +125,7 @@ function SelectDataModal({
}); });
// If all token select is unchecked then ensure all tokens are unchecked // If all token select is unchecked then ensure all tokens are unchecked
if (!event.target.checked && !tokensSelectChecked) { if (!event.target.checked && !tokensSelectChecked) {
setTokens((prevTokens) => { setTokens((prevTokens: any) => {
let newTokens = { ...prevTokens }; let newTokens = { ...prevTokens };
for (let id in newTokens) { for (let id in newTokens) {
newTokens[id].checked = false; newTokens[id].checked = false;
@ -126,14 +135,14 @@ function SelectDataModal({
} }
} }
function handleMapChange(event, map) { function handleMapChange(event: ChangeEvent<HTMLInputElement>, map: any) {
setMaps((prevMaps) => ({ setMaps((prevMaps: any) => ({
...prevMaps, ...prevMaps,
[map.id]: { ...map, checked: event.target.checked }, [map.id]: { ...map, checked: event.target.checked },
})); }));
// If all token select is unchecked then ensure tokens assosiated to this map are unchecked // If all token select is unchecked then ensure tokens assosiated to this map are unchecked
if (!event.target.checked && !tokensSelectChecked) { if (!event.target.checked && !tokensSelectChecked) {
setTokens((prevTokens) => { setTokens((prevTokens: any) => {
let newTokens = { ...prevTokens }; let newTokens = { ...prevTokens };
for (let id in newTokens) { for (let id in newTokens) {
if (tokensByMap[map.id].has(id) && tokenUsedCount[id] === 1) { if (tokensByMap[map.id].has(id) && tokenUsedCount[id] === 1) {
@ -145,8 +154,8 @@ function SelectDataModal({
} }
} }
function handleSelectTokensChange(event) { function handleSelectTokensChange(event: ChangeEvent<HTMLInputElement>) {
setTokens((prevTokens) => { setTokens((prevTokens: any) => {
let newTokens = { ...prevTokens }; let newTokens = { ...prevTokens };
for (let id in newTokens) { for (let id in newTokens) {
if (!(id in tokenUsedCount)) { if (!(id in tokenUsedCount)) {
@ -157,8 +166,8 @@ function SelectDataModal({
}); });
} }
function handleTokenChange(event, token) { function handleTokenChange(event: ChangeEvent<HTMLInputElement>, token: any) {
setTokens((prevTokens) => ({ setTokens((prevTokens: any) => ({
...prevTokens, ...prevTokens,
[token.id]: { ...token, checked: event.target.checked }, [token.id]: { ...token, checked: event.target.checked },
})); }));
@ -167,14 +176,14 @@ function SelectDataModal({
// Some tokens are checked not by maps or all tokens are checked by maps // Some tokens are checked not by maps or all tokens are checked by maps
const tokensSelectChecked = const tokensSelectChecked =
Object.values(tokens).some( Object.values(tokens).some(
(token) => !(token.id in tokenUsedCount) && token.checked (token: any) => !(token.id in tokenUsedCount) && token.checked
) || Object.values(tokens).every((token) => token.id in tokenUsedCount); ) || Object.values(tokens).every((token: any) => token.id in tokenUsedCount);
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ maxWidth: "450px", width: "100%" }} style={{ content: {maxWidth: "450px", width: "100%"} }}
> >
<Box <Box
sx={{ sx={{
@ -205,13 +214,13 @@ function SelectDataModal({
<Flex> <Flex>
<Label> <Label>
<Checkbox <Checkbox
checked={Object.values(maps).some((map) => map.checked)} checked={Object.values(maps).some((map: any) => map.checked)}
onChange={handleSelectMapsChanged} onChange={handleSelectMapsChanged}
/> />
Maps Maps
</Label> </Label>
</Flex> </Flex>
{Object.values(maps).map((map) => ( {Object.values(maps).map((map: any) => (
<Label <Label
key={map.id} key={map.id}
my={1} my={1}
@ -237,7 +246,7 @@ function SelectDataModal({
/> />
Tokens Tokens
</Label> </Label>
{Object.values(tokens).map((token) => ( {Object.values(tokens).map((token: any) => (
<Box pl={4} my={1} key={token.id}> <Box pl={4} my={1} key={token.id}>
<Label sx={{ fontFamily: "body2" }}> <Label sx={{ fontFamily: "body2" }}>
<Checkbox <Checkbox
@ -265,8 +274,8 @@ function SelectDataModal({
</Button> </Button>
<Button <Button
disabled={ disabled={
!Object.values(maps).some((map) => map.checked) && !Object.values(maps).some((map: any) => map.checked) &&
!Object.values(tokens).some((token) => token.checked) !Object.values(tokens).some((token: any) => token.checked)
} }
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
m={1} m={1}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import { Flex, Label, Button } from "theme-ui"; import { Flex, Label, Button } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
@ -7,8 +7,16 @@ import DiceTiles from "../components/dice/DiceTiles";
import { dice } from "../dice"; import { dice } from "../dice";
import useResponsiveLayout from "../hooks/useResponsiveLayout"; import useResponsiveLayout from "../hooks/useResponsiveLayout";
import Dice from "../dice/Dice";
function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }) { type SelectDiceProps = {
isOpen: boolean,
onRequestClose: () => void,
onDone: any,
defaultDice: Dice
}
function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }: SelectDiceProps) {
const [selectedDice, setSelectedDice] = useState(defaultDice); const [selectedDice, setSelectedDice] = useState(defaultDice);
const layout = useResponsiveLayout(); const layout = useResponsiveLayout();
@ -16,7 +24,7 @@ function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }) {
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} style={{ content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" } }}
> >
<Flex <Flex
sx={{ sx={{

View File

@ -1,4 +1,4 @@
import React, { useRef, useState } from "react"; import { ChangeEvent, useRef, useState } from "react";
import { Button, Flex, Label } from "theme-ui"; import { Button, Flex, Label } from "theme-ui";
import shortid from "shortid"; import shortid from "shortid";
import Case from "case"; import Case from "case";
@ -30,6 +30,15 @@ import { useAuth } from "../contexts/AuthContext";
import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext";
import shortcuts from "../shortcuts"; import shortcuts from "../shortcuts";
import { MapState } from "../components/map/Map";
type SelectMapProps = {
isOpen: boolean,
onDone: any,
onMapChange: any,
onMapReset: any,
currentMap: any
}
const defaultMapProps = { const defaultMapProps = {
showGrid: false, showGrid: false,
@ -56,7 +65,7 @@ function SelectMapModal({
onMapReset, onMapReset,
// The map currently being view in the map screen // The map currently being view in the map screen
currentMap, currentMap,
}) { }: SelectMapProps ) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { userId } = useAuth(); const { userId } = useAuth();
@ -79,7 +88,7 @@ function SelectMapModal({
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [filteredMaps, filteredMapScores] = useSearch(ownedMaps, search); const [filteredMaps, filteredMapScores] = useSearch(ownedMaps, search);
function handleSearchChange(event) { function handleSearchChange(event: ChangeEvent<HTMLInputElement>) {
setSearch(event.target.value); setSearch(event.target.value);
} }
@ -88,7 +97,7 @@ function SelectMapModal({
*/ */
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
async function handleMapsGroup(group) { async function handleMapsGroup(group: any) {
setIsLoading(true); setIsLoading(true);
setIsGroupModalOpen(false); setIsGroupModalOpen(false);
await updateMaps(selectedMapIds, { group }); await updateMaps(selectedMapIds, { group });
@ -106,15 +115,15 @@ function SelectMapModal({
* Image Upload * Image Upload
*/ */
const fileInputRef = useRef(); const fileInputRef = useRef<any>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState( const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState(
false false
); );
const largeImageWarningFiles = useRef(); const largeImageWarningFiles = useRef<any>();
async function handleImagesUpload(files) { async function handleImagesUpload(files: any) {
if (navigator.storage) { if (navigator.storage) {
// Attempt to enable persistant storage // Attempt to enable persistant storage
await navigator.storage.persist(); await navigator.storage.persist();
@ -166,7 +175,7 @@ function SelectMapModal({
clearFileInput(); clearFileInput();
} }
async function handleImageUpload(file) { async function handleImageUpload(file: any) {
if (!file) { if (!file) {
return Promise.reject(); return Promise.reject();
} }
@ -222,9 +231,9 @@ function SelectMapModal({
} }
// Create resolutions // Create resolutions
const resolutions = {}; const resolutions: any = {};
for (let resolution of mapResolutions) { for (let resolution of mapResolutions) {
const resolutionPixelSize = Vector2.multiply( const resolutionPixelSize: Vector2 = Vector2.multiply(
gridSize, gridSize,
resolution.size resolution.size
); );
@ -234,7 +243,7 @@ function SelectMapModal({
) { ) {
const resized = await resizeImage( const resized = await resizeImage(
image, image,
Vector2.max(resolutionPixelSize), Vector2.max(resolutionPixelSize, undefined) as number,
file.type, file.type,
resolution.quality resolution.quality
); );
@ -284,7 +293,7 @@ function SelectMapModal({
}); });
setIsLoading(false); setIsLoading(false);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
resolve(); resolve(undefined);
}; };
image.onerror = reject; image.onerror = reject;
image.src = url; image.src = url;
@ -293,7 +302,7 @@ function SelectMapModal({
function openImageDialog() { function openImageDialog() {
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.click(); fileInputRef.current?.click();
} }
} }
@ -302,16 +311,16 @@ function SelectMapModal({
*/ */
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
// The map selected in the modal // The map selected in the modal
const [selectedMapIds, setSelectedMapIds] = useState([]); const [selectedMapIds, setSelectedMapIds] = useState<string[]>([]);
const selectedMaps = ownedMaps.filter((map) => const selectedMaps = ownedMaps.filter((map: any) =>
selectedMapIds.includes(map.id) selectedMapIds.includes(map.id)
); );
const selectedMapStates = mapStates.filter((state) => const selectedMapStates = mapStates.filter((state: MapState) =>
selectedMapIds.includes(state.mapId) selectedMapIds.includes(state.mapId)
); );
async function handleMapAdd(map) { async function handleMapAdd(map: any) {
await addMap(map); await addMap(map);
setSelectedMapIds([map.id]); setSelectedMapIds([map.id]);
} }
@ -346,7 +355,7 @@ function SelectMapModal({
// Either single, multiple or range // Either single, multiple or range
const [selectMode, setSelectMode] = useState("single"); const [selectMode, setSelectMode] = useState("single");
function handleMapSelect(map) { function handleMapSelect(map: any) {
handleItemSelect( handleItemSelect(
map, map,
selectMode, selectMode,
@ -392,7 +401,7 @@ function SelectMapModal({
/** /**
* Shortcuts * Shortcuts
*/ */
function handleKeyDown(event) { function handleKeyDown(event: KeyboardEvent): KeyboardEvent | void {
if (!isOpen) { if (!isOpen) {
return; return;
} }
@ -406,7 +415,7 @@ function SelectMapModal({
// Selected maps and none are default // Selected maps and none are default
if ( if (
selectedMapIds.length > 0 && selectedMapIds.length > 0 &&
!selectedMaps.some((map) => map.type === "default") !selectedMaps.some((map: any) => map.type === "default")
) { ) {
// Ensure all other modals are closed // Ensure all other modals are closed
setIsGroupModalOpen(false); setIsGroupModalOpen(false);
@ -417,7 +426,7 @@ function SelectMapModal({
} }
} }
function handleKeyUp(event) { function handleKeyUp(event: KeyboardEvent) {
if (!isOpen) { if (!isOpen) {
return; return;
} }
@ -444,7 +453,7 @@ function SelectMapModal({
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={handleClose} onRequestClose={handleClose}
style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} style={{ content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" } }}
> >
<ImageDrop onDrop={handleImagesUpload} dropText="Drop map to upload"> <ImageDrop onDrop={handleImagesUpload} dropText="Drop map to upload">
<input <input
@ -500,15 +509,15 @@ function SelectMapModal({
isOpen={isGroupModalOpen} isOpen={isGroupModalOpen}
onChange={handleMapsGroup} onChange={handleMapsGroup}
groups={mapGroups.filter( groups={mapGroups.filter(
(group) => group !== "" && group !== "default" (group: any) => group !== "" && group !== "default"
)} )}
onRequestClose={() => setIsGroupModalOpen(false)} onRequestClose={() => setIsGroupModalOpen(false)}
// Select the default group by testing whether all selected maps are the same // Select the default group by testing whether all selected maps are the same
defaultGroup={ defaultGroup={
selectedMaps.length > 0 && selectedMaps.length > 0 &&
selectedMaps selectedMaps
.map((map) => map.group) .map((map: any) => map.group)
.reduce((prev, curr) => (prev === curr ? curr : undefined)) .reduce((prev: any, curr: any) => (prev === curr ? curr : undefined))
} }
/> />
<ConfirmModal <ConfirmModal

View File

@ -1,4 +1,4 @@
import React, { useRef, useState } from "react"; import { ChangeEvent, useRef, useState } from "react";
import { Flex, Label, Button } from "theme-ui"; import { Flex, Label, Button } from "theme-ui";
import shortid from "shortid"; import shortid from "shortid";
import Case from "case"; import Case from "case";
@ -24,8 +24,9 @@ import { useAuth } from "../contexts/AuthContext";
import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext";
import shortcuts from "../shortcuts"; import shortcuts from "../shortcuts";
import { FileToken, Token } from "../tokens";
function SelectTokensModal({ isOpen, onRequestClose }) { function SelectTokensModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: any }) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { userId } = useAuth(); const { userId } = useAuth();
@ -43,7 +44,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [filteredTokens, filteredTokenScores] = useSearch(ownedTokens, search); const [filteredTokens, filteredTokenScores] = useSearch(ownedTokens, search);
function handleSearchChange(event) { function handleSearchChange(event: ChangeEvent<HTMLInputElement>) {
setSearch(event.target.value); setSearch(event.target.value);
} }
@ -52,7 +53,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
*/ */
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
async function handleTokensGroup(group) { async function handleTokensGroup(group: string) {
setIsLoading(true); setIsLoading(true);
setIsGroupModalOpen(false); setIsGroupModalOpen(false);
await updateTokens(selectedTokenIds, { group }); await updateTokens(selectedTokenIds, { group });
@ -70,13 +71,13 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
* Image Upload * Image Upload
*/ */
const fileInputRef = useRef(); const fileInputRef = useRef<any>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState( const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState(
false false
); );
const largeImageWarningFiles = useRef(); const largeImageWarningFiles = useRef<File[]>();
function openImageDialog() { function openImageDialog() {
if (fileInputRef.current) { if (fileInputRef.current) {
@ -84,12 +85,17 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
} }
} }
async function handleImagesUpload(files) { async function handleImagesUpload(files: FileList | null) {
if (navigator.storage) { if (navigator.storage) {
// Attempt to enable persistant storage // Attempt to enable persistant storage
await navigator.storage.persist(); await navigator.storage.persist();
} }
// TODO: handle null files
if (files === null) {
return;
}
let tokenFiles = []; let tokenFiles = [];
for (let file of files) { for (let file of files) {
if (file.size > 5e7) { if (file.size > 5e7) {
@ -129,6 +135,9 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
async function handleLargeImageWarningConfirm() { async function handleLargeImageWarningConfirm() {
setShowLargeImageWarning(false); setShowLargeImageWarning(false);
const files = largeImageWarningFiles.current; const files = largeImageWarningFiles.current;
if (!files) {
return;
}
for (let file of files) { for (let file of files) {
await handleImageUpload(file); await handleImageUpload(file);
} }
@ -136,7 +145,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
clearFileInput(); clearFileInput();
} }
async function handleImageUpload(file) { async function handleImageUpload(file: File) {
let name = "Unknown Token"; let name = "Unknown Token";
if (file.name) { if (file.name) {
// Remove file extension // Remove file extension
@ -180,7 +189,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
height: image.height, height: image.height,
}); });
setIsLoading(false); setIsLoading(false);
resolve(); resolve(undefined);
}; };
image.onerror = reject; image.onerror = reject;
image.src = url; image.src = url;
@ -190,13 +199,13 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
/** /**
* Token controls * Token controls
*/ */
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState<boolean>(false);
const [selectedTokenIds, setSelectedTokenIds] = useState([]); const [selectedTokenIds, setSelectedTokenIds] = useState<string[]>([]);
const selectedTokens = ownedTokens.filter((token) => const selectedTokens = ownedTokens.filter((token) =>
selectedTokenIds.includes(token.id) selectedTokenIds.includes(token.id)
); );
function handleTokenAdd(token) { function handleTokenAdd(token: FileToken) {
addToken(token); addToken(token);
setSelectedTokenIds([token.id]); setSelectedTokenIds([token.id]);
} }
@ -210,7 +219,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
setIsLoading(false); setIsLoading(false);
} }
async function handleTokensHide(hideInSidebar) { async function handleTokensHide(hideInSidebar: boolean) {
setIsLoading(true); setIsLoading(true);
await updateTokens(selectedTokenIds, { hideInSidebar }); await updateTokens(selectedTokenIds, { hideInSidebar });
setIsLoading(false); setIsLoading(false);
@ -219,7 +228,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
// Either single, multiple or range // Either single, multiple or range
const [selectMode, setSelectMode] = useState("single"); const [selectMode, setSelectMode] = useState("single");
async function handleTokenSelect(token) { async function handleTokenSelect(token: Token) {
handleItemSelect( handleItemSelect(
token, token,
selectMode, selectMode,
@ -233,7 +242,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
/** /**
* Shortcuts * Shortcuts
*/ */
function handleKeyDown(event) { function handleKeyDown(event: KeyboardEvent) {
if (!isOpen) { if (!isOpen) {
return; return;
} }
@ -257,7 +266,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
} }
} }
function handleKeyUp(event) { function handleKeyUp(event: KeyboardEvent) {
if (!isOpen) { if (!isOpen) {
return; return;
} }
@ -280,11 +289,19 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
const layout = useResponsiveLayout(); const layout = useResponsiveLayout();
let tokenId;
if (selectedTokens.length === 1 && selectedTokens[0].id) {
tokenId = selectedTokens[0].id
} else {
// TODO: handle tokenId not found
tokenId = ""
}
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} style={{ content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" } }}
> >
<ImageDrop onDrop={handleImagesUpload} dropText="Drop token to upload"> <ImageDrop onDrop={handleImagesUpload} dropText="Drop token to upload">
<input <input
@ -328,17 +345,19 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
</Button> </Button>
</Flex> </Flex>
</ImageDrop> </ImageDrop>
<>
{(isLoading || tokensLoading) && <LoadingOverlay bg="overlay" />} {(isLoading || tokensLoading) && <LoadingOverlay bg="overlay" />}
</>
<EditTokenModal <EditTokenModal
isOpen={isEditModalOpen} isOpen={isEditModalOpen}
onDone={() => setIsEditModalOpen(false)} onDone={() => setIsEditModalOpen(false)}
tokenId={selectedTokens.length === 1 && selectedTokens[0].id} tokenId={tokenId}
/> />
<EditGroupModal <EditGroupModal
isOpen={isGroupModalOpen} isOpen={isGroupModalOpen}
onChange={handleTokensGroup} onChange={handleTokensGroup}
groups={tokenGroups.filter( groups={tokenGroups.filter(
(group) => group !== "" && group !== "default" (group: string) => group !== "" && group !== "default"
)} )}
onRequestClose={() => setIsGroupModalOpen(false)} onRequestClose={() => setIsGroupModalOpen(false)}
// Select the default group by testing whether all selected tokens are the same // Select the default group by testing whether all selected tokens are the same

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { import {
Label, Label,
Flex, Flex,
@ -21,8 +21,9 @@ import useSetting from "../hooks/useSetting";
import ConfirmModal from "./ConfirmModal"; import ConfirmModal from "./ConfirmModal";
import ImportExportModal from "./ImportExportModal"; import ImportExportModal from "./ImportExportModal";
import { MapState } from "../components/map/Map";
function SettingsModal({ isOpen, onRequestClose }) { function SettingsModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void }) {
const { database, databaseStatus } = useDatabase(); const { database, databaseStatus } = useDatabase();
const { userId } = useAuth(); const { userId } = useAuth();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -32,7 +33,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
); );
const [showFogGuides, setShowFogGuides] = useSetting("fog.showGuides"); const [showFogGuides, setShowFogGuides] = useSetting("fog.showGuides");
const [fogEditOpacity, setFogEditOpacity] = useSetting("fog.editOpacity"); const [fogEditOpacity, setFogEditOpacity] = useSetting("fog.editOpacity");
const [storageEstimate, setStorageEstimate] = useState(); const [storageEstimate, setStorageEstimate] = useState<StorageEstimate>();
const [isImportExportModalOpen, setIsImportExportModalOpen] = useState(false); const [isImportExportModalOpen, setIsImportExportModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -58,7 +59,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
async function handleEraseAllData() { async function handleEraseAllData() {
setIsLoading(true); setIsLoading(true);
localStorage.clear(); localStorage.clear();
await database.delete(); await database?.delete();
window.location.reload(); window.location.reload();
} }
@ -66,6 +67,11 @@ function SettingsModal({ isOpen, onRequestClose }) {
setIsLoading(true); setIsLoading(true);
// Clear saved settings // Clear saved settings
localStorage.clear(); localStorage.clear();
//TODO: handle id database is undefined
if (!database) {
return;
}
// Clear map cache // Clear map cache
await database.table("maps").where("owner").notEqual(userId).delete(); await database.table("maps").where("owner").notEqual(userId).delete();
// Find all other peoples tokens who aren't benig used in a map state and delete them // Find all other peoples tokens who aren't benig used in a map state and delete them
@ -74,7 +80,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
.where("owner") .where("owner")
.notEqual(userId) .notEqual(userId)
.toArray(); .toArray();
const states = await database.table("states").toArray(); const states: MapState[] = await database?.table("states").toArray();
for (let token of tokens) { for (let token of tokens) {
let inUse = false; let inUse = false;
for (let state of states) { for (let state of states) {
@ -126,7 +132,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
sx={{ width: "initial" }} sx={{ width: "initial" }}
value={fogEditOpacity} value={fogEditOpacity}
onChange={(e) => setFogEditOpacity(parseFloat(e.target.value))} onChange={(e) => setFogEditOpacity(parseFloat(e.target.value))}
labelFunc={(value) => `${Math.round(value * 100)}%`} labelFunc={(value: number) => `${Math.round(value * 100)}%`}
/> />
</Label> </Label>
<Label py={2}> <Label py={2}>
@ -139,7 +145,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
sx={{ width: "initial" }} sx={{ width: "initial" }}
value={labelSize} value={labelSize}
onChange={(e) => setLabelSize(parseFloat(e.target.value))} onChange={(e) => setLabelSize(parseFloat(e.target.value))}
labelFunc={(value) => `${value}x`} labelFunc={(value: number) => `${value}x`}
/> />
</Label> </Label>
<Label py={2}> <Label py={2}>
@ -154,7 +160,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
onChange={(e) => onChange={(e) =>
setGridSnappingSensitivity(parseFloat(e.target.value)) setGridSnappingSensitivity(parseFloat(e.target.value))
} }
labelFunc={(value) => `${value * 2}`} labelFunc={(value: number) => `${value * 2}`}
/> />
</Label> </Label>
<Divider bg="text" /> <Divider bg="text" />
@ -185,13 +191,13 @@ function SettingsModal({ isOpen, onRequestClose }) {
Import / Export Data Import / Export Data
</Button> </Button>
</Flex> </Flex>
{storageEstimate && ( {storageEstimate !&& (
<Flex sx={{ justifyContent: "center" }}> <Flex sx={{ justifyContent: "center" }}>
<Text variant="caption"> <Text variant="caption">
Storage Used: {prettyBytes(storageEstimate.usage)} of{" "} Storage Used: {prettyBytes(storageEstimate.usage as number)} of{" "}
{prettyBytes(storageEstimate.quota)} ( {prettyBytes(storageEstimate.quota as number)} (
{Math.round( {Math.round(
(storageEstimate.usage / Math.max(storageEstimate.quota, 1)) * (storageEstimate.usage as number / Math.max(storageEstimate.quota as number, 1)) *
100 100
)} )}
%) %)

View File

@ -1,4 +1,4 @@
import React, { useRef } from "react"; import { ChangeEvent, useRef } from "react";
import { Box, Label, Input, Button, Flex, Checkbox } from "theme-ui"; import { Box, Label, Input, Button, Flex, Checkbox } from "theme-ui";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import shortid from "shortid"; import shortid from "shortid";
@ -9,20 +9,20 @@ import useSetting from "../hooks/useSetting";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
function StartModal({ isOpen, onRequestClose }) { function StartModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void}) {
let history = useHistory(); let history = useHistory();
const { password, setPassword } = useAuth(); const { password, setPassword } = useAuth();
function handlePasswordChange(event) { function handlePasswordChange(event: ChangeEvent<HTMLInputElement>) {
setPassword(event.target.value); setPassword(event.target.value);
} }
const [usePassword, setUsePassword] = useSetting("game.usePassword"); const [usePassword, setUsePassword] = useSetting("game.usePassword");
function handleUsePasswordChange(event) { function handleUsePasswordChange(event: ChangeEvent<HTMLInputElement>) {
setUsePassword(event.target.checked); setUsePassword(event.target.checked);
} }
function handleSubmit(event) { function handleSubmit(event: ChangeEvent<HTMLInputElement>) {
event.preventDefault(); event.preventDefault();
if (!usePassword) { if (!usePassword) {
setPassword(""); setPassword("");
@ -30,7 +30,7 @@ function StartModal({ isOpen, onRequestClose }) {
history.push(`/game/${shortid.generate()}`); history.push(`/game/${shortid.generate()}`);
} }
const inputRef = useRef(); const inputRef = useRef<any>();
function focusInput() { function focusInput() {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current.focus();
} }

View File

@ -1,8 +1,19 @@
import React from "react";
import { Box, Text, Button, Label, Flex } from "theme-ui"; import { Box, Text, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
type StartStreamProps = {
isOpen: boolean,
onRequestClose: () => void,
isSupported: boolean,
unavailableMessage: string,
stream: MediaStream,
noAudioTrack: boolean,
noAudioMessage: string,
onStreamStart: any,
onStreamEnd: any,
}
function StartStreamModal({ function StartStreamModal({
isOpen, isOpen,
onRequestClose, onRequestClose,
@ -13,7 +24,7 @@ function StartStreamModal({
noAudioMessage, noAudioMessage,
onStreamStart, onStreamStart,
onStreamEnd, onStreamEnd,
}) { }: StartStreamProps) {
return ( return (
<Modal isOpen={isOpen} onRequestClose={onRequestClose}> <Modal isOpen={isOpen} onRequestClose={onRequestClose}>
<Box> <Box>

View File

@ -1,4 +1,4 @@
import React, { useRef } from "react"; import { ChangeEvent, useRef } from "react";
import { Box, Label, Input, Button, Flex, Text } from "theme-ui"; import { Box, Label, Input, Button, Flex, Text } from "theme-ui";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
@ -7,14 +7,22 @@ import { getHMSDuration, getDurationHMS } from "../helpers/timer";
import useSetting from "../hooks/useSetting"; import useSetting from "../hooks/useSetting";
type StartTimerProps = {
isOpen: boolean,
onRequestClose: () => void,
onTimerStart: any,
onTimerStop: any,
timer: any,
}
function StartTimerModal({ function StartTimerModal({
isOpen, isOpen,
onRequestClose, onRequestClose,
onTimerStart, onTimerStart,
onTimerStop, onTimerStop,
timer, timer,
}) { }: StartTimerProps) {
const inputRef = useRef(); const inputRef = useRef<any>();
function focusInput() { function focusInput() {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current.focus();
} }
@ -23,7 +31,7 @@ function StartTimerModal({
const [minute, setMinute] = useSetting("timer.minute"); const [minute, setMinute] = useSetting("timer.minute");
const [second, setSecond] = useSetting("timer.second"); const [second, setSecond] = useSetting("timer.second");
function handleSubmit(event) { function handleSubmit(event: ChangeEvent<HTMLInputElement>) {
event.preventDefault(); event.preventDefault();
if (timer) { if (timer) {
onTimerStop(); onTimerStop();
@ -44,7 +52,7 @@ function StartTimerModal({
paddingLeft: 0, paddingLeft: 0,
}; };
function parseValue(value, max) { function parseValue(value: string, max: number) {
const num = parseInt(value); const num = parseInt(value);
if (isNaN(num)) { if (isNaN(num)) {
return 0; return 0;

View File

@ -32,6 +32,7 @@ import undead from "./Undead.png";
import warlock from "./Warlock.png"; import warlock from "./Warlock.png";
import wizard from "./Wizard.png"; import wizard from "./Wizard.png";
import unknown from "./Unknown.png"; import unknown from "./Unknown.png";
import { ImageFile } from "../helpers/image";
export const tokenSources = { export const tokenSources = {
barbarian, barbarian,
@ -80,7 +81,40 @@ function getDefaultTokenSize(key: string) {
} }
} }
export const tokens = Object.keys(tokenSources).map((key) => ({ type TokenCategory = "character" | "vehicle" | "prop"
export type Token = {
id: string,
name: string,
defaultSize: number,
category: TokenCategory,
hideInSidebar: boolean,
width: number,
height: number,
owner: string,
type: string,
group: string | undefined,
created: number,
lastModified: number,
lastUsed: number,
}
export interface DefaultToken extends Omit<Token, "id" | "owner" | "created" | "lastModified" | "lastUsed"> {
id?: string,
owner?: string,
created?: number,
lastModified?: number,
lastUsed?: number,
key: string,
type: "default",
group: "default",
}
export interface FileToken extends Token {
file: Uint8Array,
thumbnail: ImageFile,
type: "file",
}
export const tokens: DefaultToken[] = Object.keys(tokenSources).map((key) => ({
key, key,
name: Case.capital(key), name: Case.capital(key),
type: "default", type: "default",
@ -89,6 +123,7 @@ export const tokens = Object.keys(tokenSources).map((key) => ({
hideInSidebar: false, hideInSidebar: false,
width: 256, width: 256,
height: 256, height: 256,
group: "default",
})); }));
export const unknownSource = unknown; export const unknownSource = unknown;