import React, { useEffect, useState, useContext, useCallback, useMemo, } from "react"; import { useLiveQuery } from "dexie-react-hooks"; import { useDatabase } from "./DatabaseContext"; import { Map } from "../types/Map"; import { MapState } from "../types/MapState"; import { removeGroupsItems } from "../helpers/group"; import { Group } from "../types/Group"; export type AddMapEventHandler = (map: Map) => Promise; export type RemoveMapsEventHandler = (ids: string[]) => Promise; export type ResetMapEventHandler = (id: string) => Promise; export type UpdateMapEventHanlder = ( id: string, update: Partial ) => Promise; export type UpdateMapStateEventHandler = ( id: string, update: Partial ) => Promise; export type GetMapStateEventHandler = ( id: string ) => Promise; export type GetMapEventHandler = (id: string) => Promise; export type UpdateMapGroupsEventHandler = (groups: Group[]) => Promise; type MapDataContextValue = { maps: Array; mapStates: MapState[]; /** Adds a map to the database, also adds an assosiated state and group for that map */ addMap: AddMapEventHandler; removeMaps: RemoveMapsEventHandler; resetMap: ResetMapEventHandler; updateMap: UpdateMapEventHanlder; updateMapState: UpdateMapStateEventHandler; getMapState: GetMapStateEventHandler; getMap: GetMapEventHandler; mapsLoading: boolean; updateMapGroups: UpdateMapGroupsEventHandler; mapsById: Record; mapGroups: Group[]; }; const MapDataContext = React.createContext(undefined); const defaultMapState: Omit = { tokens: {}, drawings: {}, fogs: {}, editFlags: ["drawing", "tokens", "notes"], notes: {}, }; export function MapDataProvider({ children }: { children: React.ReactNode }) { const { database } = useDatabase(); const mapsQuery = useLiveQuery( () => database?.table("maps").toArray() || [], [database] ); const mapStatesQuery = useLiveQuery( () => database?.table("states").toArray() || [], [database] ); const maps = useMemo(() => mapsQuery || [], [mapsQuery]); const mapStates = useMemo(() => mapStatesQuery || [], [mapStatesQuery]); const mapsLoading = useMemo( () => !mapsQuery || !mapStatesQuery, [mapsQuery, mapStatesQuery] ); const mapGroupQuery = useLiveQuery( () => database?.table("groups").get("maps"), [database] ); const [mapGroups, setMapGroups] = useState([]); useEffect(() => { async function updateMapGroups() { const group = await database?.table("groups").get("maps"); setMapGroups(group.items); } if (database && mapGroupQuery) { updateMapGroups(); } }, [mapGroupQuery, database]); const getMap = useCallback( async (mapId) => { let map = await database?.table("maps").get(mapId); return map; }, [database] ); const getMapState = useCallback( async (mapId) => { let mapState = await database?.table("states").get(mapId); return mapState; }, [database] ); const addMap = useCallback( async (map) => { if (database) { // Just update map database as react state will be updated with an Observable const state = { ...defaultMapState, mapId: map.id }; await database.table("maps").add(map); await database.table("states").add(state); const group = await database.table("groups").get("maps"); await database.table("groups").update("maps", { items: [{ id: map.id, type: "item" }, ...group.items], }); } }, [database] ); const removeMaps = useCallback( async (ids) => { if (database) { const maps = await database.table("maps").bulkGet(ids); // Remove assets linked with maps let assetIds = []; for (let map of maps) { if (map.type === "file") { assetIds.push(map.file); assetIds.push(map.thumbnail); for (let res of Object.values(map.resolutions)) { assetIds.push(res); } } } const group = await database.table("groups").get("maps"); let items = removeGroupsItems(group.items, ids); await database.table("groups").update("maps", { items }); await database.table("maps").bulkDelete(ids); await database.table("states").bulkDelete(ids); await database.table("assets").bulkDelete(assetIds); } }, [database] ); const resetMap = useCallback( async (id) => { const state: MapState = { ...defaultMapState, mapId: id }; await database?.table("states").put(state); return state; }, [database] ); const updateMap = useCallback( async (id, update) => { await database?.table("maps").update(id, update); }, [database] ); const updateMapState = useCallback( async (id, update) => { await database?.table("states").update(id, update); }, [database] ); const updateMapGroups = useCallback( async (groups) => { // Update group state immediately to avoid animation delay setMapGroups(groups); await database?.table("groups").update("maps", { items: groups }); }, [database] ); const [mapsById, setMapsById] = useState>({}); useEffect(() => { setMapsById( maps.reduce((obj: Record, map) => { obj[map.id] = map; return obj; }, {}) ); }, [maps]); const value = { maps, mapStates, mapGroups, addMap, removeMaps, resetMap, updateMap, updateMapState, getMap, mapsLoading, getMapState, updateMapGroups, mapsById, }; return ( {children} ); } export function useMapData() { const context = useContext(MapDataContext); if (context === undefined) { throw new Error("useMapData must be used within a MapDataProvider"); } return context; } export default MapDataContext;