From f419029d569b9a1b9824986930a5cbb0da0cf059 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 1 May 2021 12:54:00 +1000 Subject: [PATCH] Add default maps and tokens to db and split db upgrade into new file --- src/actions/index.js | 37 -- src/contexts/AuthContext.js | 5 - src/contexts/MapDataContext.js | 31 +- src/contexts/TokenDataContext.js | 18 +- src/database.js | 668 +--------------------------- src/maps/index.js | 54 ++- src/modals/SettingsModal.js | 1 + src/tokens/index.js | 39 +- src/upgrade.js | 719 +++++++++++++++++++++++++++++++ src/workers/DatabaseWorker.js | 3 +- 10 files changed, 815 insertions(+), 760 deletions(-) create mode 100644 src/upgrade.js diff --git a/src/actions/index.js b/src/actions/index.js index 7f3489d..7822cc1 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -4,43 +4,6 @@ import EditShapeAction from "./EditShapeAction"; import RemoveShapeAction from "./RemoveShapeAction"; import SubtractShapeAction from "./SubtractShapeAction"; -/** - * Convert from the previous representation of actions (1.7.0) to the new representation (1.8.0) - * and combine into shapes - * @param {Array} actions - * @param {number} actionIndex - */ -export function convertOldActionsToShapes(actions, actionIndex) { - let newShapes = {}; - for (let i = 0; i <= actionIndex; i++) { - const action = actions[i]; - if (!action) { - continue; - } - let newAction; - if (action.shapes) { - if (action.type === "add") { - newAction = new AddShapeAction(action.shapes); - } else if (action.type === "edit") { - newAction = new EditShapeAction(action.shapes); - } else if (action.type === "remove") { - newAction = new RemoveShapeAction(action.shapes); - } else if (action.type === "subtract") { - newAction = new SubtractShapeAction(action.shapes); - } else if (action.type === "cut") { - newAction = new CutShapeAction(action.shapes); - } - } else if (action.type === "remove" && action.shapeIds) { - newAction = new RemoveShapeAction(action.shapeIds); - } - - if (newAction) { - newShapes = newAction.execute(newShapes); - } - } - return newShapes; -} - export { AddShapeAction, CutShapeAction, diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js index 868fe7e..f08971c 100644 --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.js @@ -1,5 +1,4 @@ import React, { useState, useEffect, useContext } from "react"; -import shortid from "shortid"; import { useDatabase } from "./DatabaseContext"; @@ -35,10 +34,6 @@ export function AuthProvider({ children }) { const storedUserId = await database.table("user").get("userId"); if (storedUserId) { setUserId(storedUserId.value); - } else { - const id = shortid.generate(); - setUserId(id); - database.table("user").add({ key: "userId", value: id }); } } diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index 4ed8379..7a3fc03 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -4,8 +4,6 @@ import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; -import { maps as defaultMaps } from "../maps"; - const MapDataContext = React.createContext(); const defaultMapState = { @@ -30,30 +28,6 @@ export function MapDataProvider({ children }) { if (!userId || !database || databaseStatus === "loading") { return; } - async function getDefaultMaps() { - const defaultMapsWithIds = []; - for (let i = 0; i < defaultMaps.length; i++) { - const defaultMap = defaultMaps[i]; - const id = `__default-${defaultMap.name}`; - defaultMapsWithIds.push({ - ...defaultMap, - id, - owner: userId, - // Emulate the time increasing to avoid sort errors - created: Date.now() + i, - lastModified: Date.now() + i, - showGrid: false, - snapToGrid: true, - group: "default", - }); - // Add a state for the map if there isn't one already - const state = await database.table("states").get(id); - if (!state) { - await database.table("states").add({ ...defaultMapState, mapId: id }); - } - } - return defaultMapsWithIds; - } async function loadMaps() { let storedMaps = []; @@ -68,10 +42,9 @@ export function MapDataProvider({ children }) { storedMaps.push(map); }); } + // TODO: remove sort when groups are added const sortedMaps = storedMaps.sort((a, b) => b.created - a.created); - const defaultMapsWithIds = await getDefaultMaps(); - const allMaps = [...sortedMaps, ...defaultMapsWithIds]; - setMaps(allMaps); + setMaps(sortedMaps); const storedStates = await database.table("states").toArray(); setMapStates(storedStates); setMapsLoading(false); diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index 1e5d458..21d8c43 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -4,8 +4,6 @@ import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; -import { tokens as defaultTokens } from "../tokens"; - const TokenDataContext = React.createContext(); export function TokenDataProvider({ children }) { @@ -19,18 +17,6 @@ export function TokenDataProvider({ children }) { if (!userId || !database || databaseStatus === "loading") { return; } - function getDefaultTokens() { - const defaultTokensWithIds = []; - for (let defaultToken of defaultTokens) { - defaultTokensWithIds.push({ - ...defaultToken, - id: `__default-${defaultToken.name}`, - owner: userId, - group: "default", - }); - } - return defaultTokensWithIds; - } async function loadTokens() { let storedTokens = []; @@ -45,9 +31,7 @@ export function TokenDataProvider({ children }) { }); } const sortedTokens = storedTokens.sort((a, b) => b.created - a.created); - const defaultTokensWithIds = getDefaultTokens(); - const allTokens = [...sortedTokens, ...defaultTokensWithIds]; - setTokens(allTokens); + setTokens(sortedTokens); setTokensLoading(false); } diff --git a/src/database.js b/src/database.js index 0d862b2..8d3e3b1 100644 --- a/src/database.js +++ b/src/database.js @@ -1,655 +1,26 @@ // eslint-disable-next-line no-unused-vars -import Dexie, { Version, DexieOptions } from "dexie"; -import "dexie-observable"; -import shortid from "shortid"; +import Dexie, { DexieOptions } from "dexie"; import { v4 as uuid } from "uuid"; -import Case from "case"; +import "dexie-observable"; -import blobToBuffer from "./helpers/blobToBuffer"; -import { getGridDefaultInset } from "./helpers/grid"; -import { convertOldActionsToShapes } from "./actions"; -import { createThumbnail } from "./helpers/image"; - -// Helper to create a thumbnail for a file in a db -async function createDataThumbnail(data) { - let url; - if (data?.resolutions?.low?.file) { - url = URL.createObjectURL(new Blob([data.resolutions.low.file])); - } else { - url = URL.createObjectURL(new Blob([data.file])); - } - return await Dexie.waitFor( - new Promise((resolve) => { - let image = new Image(); - image.onload = async () => { - const thumbnail = await createThumbnail(image); - resolve(thumbnail); - }; - image.src = url; - }), - 60000 * 10 // 10 minute timeout - ); -} +import { loadVersions, latestVersion } from "./upgrade"; +import { getDefaultMaps } from "./maps"; +import { getDefaultTokens } from "./tokens"; /** - * @callback VersionCallback - * @param {Version} version - */ - -/** - * Mapping of version number to their upgrade function - * @type {Object.} - */ -const versions = { - // v1.2.0 - 1(v) { - v.stores({ - maps: "id, owner", - states: "mapId", - tokens: "id, owner", - user: "key", - }); - }, - // v1.2.1 - Move from blob files to array buffers - 2(v) { - v.stores({}).upgrade(async (tx) => { - const maps = await Dexie.waitFor(tx.table("maps").toArray()); - let mapBuffers = {}; - for (let map of maps) { - mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file)); - } - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.file = mapBuffers[map.id]; - }); - }); - }, - // v1.3.0 - Added new default tokens - 3(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - function mapTokenId(id) { - switch (id) { - case "__default-Axes": - return "__default-Barbarian"; - case "__default-Bird": - return "__default-Druid"; - case "__default-Book": - return "__default-Wizard"; - case "__default-Crown": - return "__default-Humanoid"; - case "__default-Dragon": - return "__default-Dragon"; - case "__default-Eye": - return "__default-Warlock"; - case "__default-Fist": - return "__default-Monk"; - case "__default-Horse": - return "__default-Fey"; - case "__default-Leaf": - return "__default-Druid"; - case "__default-Lion": - return "__default-Monstrosity"; - case "__default-Money": - return "__default-Humanoid"; - case "__default-Moon": - return "__default-Cleric"; - case "__default-Potion": - return "__default-Sorcerer"; - case "__default-Shield": - return "__default-Paladin"; - case "__default-Skull": - return "__default-Undead"; - case "__default-Snake": - return "__default-Beast"; - case "__default-Sun": - return "__default-Cleric"; - case "__default-Swords": - return "__default-Fighter"; - case "__default-Tree": - return "__default-Plant"; - case "__default-Triangle": - return "__default-Sorcerer"; - default: - return "__default-Fighter"; - } - } - for (let stateId in state.tokens) { - state.tokens[stateId].tokenId = mapTokenId( - state.tokens[stateId].tokenId - ); - state.tokens[stateId].lastEditedBy = ""; - state.tokens[stateId].rotation = 0; - } - }); - }); - }, - // v1.3.1 - Added show grid option - 4(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.showGrid = false; - }); - }); - }, - // v1.4.0 - Added fog subtraction - 5(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - for (let fogAction of state.fogDrawActions) { - if (fogAction.type === "add" || fogAction.type === "edit") { - for (let shape of fogAction.shapes) { - shape.data.holes = []; - } - } - } - }); - }); - }, - // v1.4.2 - Added map resolutions - 6(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.resolutions = {}; - map.quality = "original"; - }); - }); - }, - // v1.5.0 - Fixed default token rogue spelling - 7(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - for (let id in state.tokens) { - if (state.tokens[id].tokenId === "__default-Rouge") { - state.tokens[id].tokenId = "__default-Rogue"; - } - } - }); - }); - }, - // v1.5.0 - Added map snap to grid option - 8(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.snapToGrid = true; - }); - }); - }, - // v1.5.1 - Added lock, visibility and modified to tokens - 9(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - for (let id in state.tokens) { - state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy; - delete state.tokens[id].lastEditedBy; - state.tokens[id].lastModified = Date.now(); - state.tokens[id].locked = false; - state.tokens[id].visible = true; - } - }); - }); - }, - // v1.5.1 - Added token prop category and remove isVehicle bool - 10(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("tokens") - .toCollection() - .modify((token) => { - token.category = token.isVehicle ? "vehicle" : "character"; - delete token.isVehicle; - }); - }); - }, - // v1.5.2 - Added automatic cache invalidation to maps - 11(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.lastUsed = map.lastModified; - }); - }); - }, - // v1.5.2 - Added automatic cache invalidation to tokens - 12(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("tokens") - .toCollection() - .modify((token) => { - token.lastUsed = token.lastModified; - }); - }); - }, - // v1.6.0 - Added map grouping and grid scale and offset - 13(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.group = ""; - map.grid = { - size: { x: map.gridX, y: map.gridY }, - inset: getGridDefaultInset( - { size: { x: map.gridX, y: map.gridY }, type: "square" }, - map.width, - map.height - ), - type: "square", - }; - delete map.gridX; - delete map.gridY; - delete map.gridType; - }); - }); - }, - // v1.6.0 - Added token grouping - 14(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("tokens") - .toCollection() - .modify((token) => { - token.group = ""; - }); - }); - }, - // v1.6.1 - Added width and height to tokens - 15(v) { - v.stores({}).upgrade(async (tx) => { - const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); - let tokenSizes = {}; - for (let token of tokens) { - const url = URL.createObjectURL(new Blob([token.file])); - let image = new Image(); - tokenSizes[token.id] = await Dexie.waitFor( - new Promise((resolve) => { - image.onload = () => { - resolve({ width: image.width, height: image.height }); - }; - image.src = url; - }) - ); - } - return tx - .table("tokens") - .toCollection() - .modify((token) => { - token.width = tokenSizes[token.id].width; - token.height = tokenSizes[token.id].height; - }); - }); - }, - // v1.7.0 - Added note tool - 16(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - state.notes = {}; - state.editFlags = [...state.editFlags, "notes"]; - }); - }); - }, - // 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data - 17(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - for (let i = 0; i < state.fogDrawActions.length; i++) { - const action = state.fogDrawActions[i]; - if (action && action.type === "edit") { - for (let j = 0; j < action.shapes.length; j++) { - const shape = action.shapes[j]; - const temp = { ...shape }; - state.fogDrawActions[i].shapes[j] = { - id: temp.id, - visible: temp.visible, - }; - } - } - } - }); - }); - }, - // 1.8.0 - Added note text only mode, converted draw and fog representations - 18(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - for (let id in state.notes) { - state.notes[id].textOnly = false; - } - - state.drawShapes = convertOldActionsToShapes( - state.mapDrawActions, - state.mapDrawActionIndex - ); - state.fogShapes = convertOldActionsToShapes( - state.fogDrawActions, - state.fogDrawActionIndex - ); - - delete state.mapDrawActions; - delete state.mapDrawActionIndex; - delete state.fogDrawActions; - delete state.fogDrawActionIndex; - }); - }); - }, - // 1.8.0 - Add thumbnail to maps and add measurement to grid - 19(v) { - v.stores({}).upgrade(async (tx) => { - const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) - .value; - const maps = await Dexie.waitFor(tx.table("maps").toArray()); - const thumbnails = {}; - for (let map of maps) { - try { - if (map.owner === userId) { - thumbnails[map.id] = await createDataThumbnail(map); - } - } catch {} - } - return tx - .table("maps") - .toCollection() - .modify((map) => { - map.thumbnail = thumbnails[map.id]; - map.grid.measurement = { type: "chebyshev", scale: "5ft" }; - }); - }); - }, - // 1.8.0 - Add thumbnail to tokens - 20(v) { - v.stores({}).upgrade(async (tx) => { - const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) - .value; - const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); - const thumbnails = {}; - for (let token of tokens) { - try { - if (token.owner === userId) { - thumbnails[token.id] = await createDataThumbnail(token); - } - } catch {} - } - return tx - .table("tokens") - .toCollection() - .modify((token) => { - token.thumbnail = thumbnails[token.id]; - }); - }); - }, - // 1.8.0 - Upgrade for Dexie.Observable - 21(v) { - v.stores({}); - }, - // v1.8.1 - Shorten fog shape ids - 22(v) { - v.stores({}).upgrade((tx) => { - return tx - .table("states") - .toCollection() - .modify((state) => { - for (let id of Object.keys(state.fogShapes)) { - const newId = shortid.generate(); - state.fogShapes[newId] = state.fogShapes[id]; - state.fogShapes[newId].id = newId; - delete state.fogShapes[id]; - } - }); - }); - }, - // v1.9.0 - Move map assets into new table - 23(v) { - v.stores({ assets: "id, owner" }).upgrade((tx) => { - tx.table("maps").each((map) => { - let assets = []; - assets.push({ - id: uuid(), - file: map.file, - width: map.width, - height: map.height, - mime: "", - prevId: map.id, - prevType: "map", - }); - - for (let resolution in map.resolutions) { - const mapRes = map.resolutions[resolution]; - assets.push({ - id: uuid(), - file: mapRes.file, - width: mapRes.width, - height: mapRes.height, - mime: "", - prevId: map.id, - prevType: "mapResolution", - resolution, - }); - } - - assets.push({ - id: uuid(), - file: map.thumbnail.file, - width: map.thumbnail.width, - height: map.thumbnail.height, - mime: "", - prevId: map.id, - prevType: "mapThumbnail", - }); - - tx.table("assets").bulkAdd(assets); - }); - }); - }, - // v1.9.0 - Move token assets into new table - 24(v) { - v.stores({}).upgrade((tx) => { - tx.table("tokens").each((token) => { - let assets = []; - assets.push({ - id: uuid(), - file: token.file, - width: token.width, - height: token.height, - mime: "", - prevId: token.id, - prevType: "token", - }); - assets.push({ - id: uuid(), - file: token.thumbnail.file, - width: token.thumbnail.width, - height: token.thumbnail.height, - mime: "", - prevId: token.id, - prevType: "tokenThumbnail", - }); - tx.table("assets").bulkAdd(assets); - }); - }); - }, - // v1.9.0 - Create foreign keys for assets - 25(v) { - v.stores({}).upgrade((tx) => { - tx.table("assets").each((asset) => { - if (asset.prevType === "map") { - tx.table("maps").update(asset.prevId, { - file: asset.id, - }); - } else if (asset.prevType === "token") { - tx.table("tokens").update(asset.prevId, { - file: asset.id, - }); - } else if (asset.prevType === "mapThumbnail") { - tx.table("maps").update(asset.prevId, { thumbnail: asset.id }); - } else if (asset.prevType === "tokenThumbnail") { - tx.table("tokens").update(asset.prevId, { thumbnail: asset.id }); - } else if (asset.prevType === "mapResolution") { - tx.table("maps").update(asset.prevId, { - resolutions: undefined, - [asset.resolution]: asset.id, - }); - } - }); - }); - }, - // v1.9.0 - Remove asset migration helpers - 26(v) { - v.stores({}).upgrade((tx) => { - tx.table("assets") - .toCollection() - .modify((asset) => { - delete asset.prevId; - if (asset.prevType === "mapResolution") { - delete asset.resolution; - } - delete asset.prevType; - }); - }); - }, - // v1.9.0 - Remap map resolution assets - 27(v) { - v.stores({}).upgrade((tx) => { - tx.table("maps") - .toCollection() - .modify((map) => { - const resolutions = ["low", "medium", "high", "ultra"]; - map.resolutions = {}; - for (let res of resolutions) { - if (res in map) { - map.resolutions[res] = map[res]; - delete map[res]; - } - } - delete map.lastUsed; - }); - }); - }, - // v1.9.0 - Move tokens to use more defaults and add token outline to tokens - 28(v) { - v.stores({}).upgrade((tx) => { - tx.table("tokens") - .toCollection() - .modify(async (token) => { - token.defaultCategory = token.category; - delete token.category; - token.defaultLabel = ""; - if (token.width === token.height) { - token.outline = "circle"; - } else { - token.outline = "rect"; - } - delete token.lastUsed; - }); - }); - }, - // v1.9.0 - Move tokens to use more defaults and add token outline to token states - 29(v) { - v.stores({}).upgrade(async (tx) => { - const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); - tx.table("states") - .toCollection() - .modify((state) => { - for (let id in state.tokens) { - if (!state.tokens[id].tokenId.startsWith("__default")) { - const token = tokens.find( - (token) => token.id === state.tokens[id].tokenId - ); - if (token) { - state.tokens[id].category = token.defaultCategory; - state.tokens[id].file = token.file; - state.tokens[id].type = "file"; - state.tokens[id].outline = token.outline; - state.tokens[id].width = token.width; - state.tokens[id].height = token.height; - } else { - state.tokens[id].category = "character"; - state.tokens[id].type = "file"; - state.tokens[id].file = ""; - state.tokens[id].outline = "rect"; - state.tokens[id].width = 256; - state.tokens[id].height = 256; - } - } else { - state.tokens[id].category = "character"; - state.tokens[id].type = "default"; - state.tokens[id].key = Case.camel( - state.tokens[id].tokenId.slice(10) - ); - state.tokens[id].outline = "circle"; - state.tokens[id].width = 256; - state.tokens[id].height = 256; - } - } - }); - }); - }, - // v1.9.0 - Remove maps not owned by user as cache is now done on the asset level - 30(v) { - v.stores({}).upgrade(async (tx) => { - const userId = await tx.table("user").get("userId"); - if (userId) { - tx.table("maps").where("owner").notEqual(userId.value).delete(); - } - }); - }, - // v1.9.0 - Remove tokens not owned by user as cache is now done on the asset level - 31(v) { - v.stores({}).upgrade(async (tx) => { - const userId = await tx.table("user").get("userId"); - if (userId) { - tx.table("tokens").where("owner").notEqual(userId.value).delete(); - } - }); - }, -}; - -const latestVersion = 31; - -/** - * Load versions onto a database up to a specific version number + * Populate DB with initial data * @param {Dexie} db - * @param {number=} upTo version number to load up to, latest version if undefined */ -export function loadVersions(db, upTo = latestVersion) { - for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) { - versions[versionNumber](db.version(versionNumber)); - } +function populate(db) { + db.on("populate", () => { + const userId = uuid(); + db.table("user").add({ key: "userId", value: userId }); + const { maps, mapStates } = getDefaultMaps(userId); + db.table("maps").bulkAdd(maps); + db.table("states").bulkAdd(mapStates); + const tokens = getDefaultTokens(userId); + db.table("tokens").bulkAdd(tokens); + }); } /** @@ -657,14 +28,19 @@ export function loadVersions(db, upTo = latestVersion) { * @param {DexieOptions} options * @param {string=} name * @param {number=} versionNumber + * @param {boolean=} populateData * @returns {Dexie} */ export function getDatabase( options, name = "OwlbearRodeoDB", - versionNumber = latestVersion + versionNumber = latestVersion, + populateData = true ) { let db = new Dexie(name, options); loadVersions(db, versionNumber); + if (populateData) { + populate(db); + } return db; } diff --git a/src/maps/index.js b/src/maps/index.js index 2b55878..aca8daf 100644 --- a/src/maps/index.js +++ b/src/maps/index.js @@ -18,18 +18,46 @@ export const mapSources = { wood: woodImage, }; -export const maps = Object.keys(mapSources).map((key) => ({ - key, - name: Case.capital(key), - grid: { - size: { x: 22, y: 22 }, - inset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } }, - type: "square", - measurement: { type: "chebyshev", scale: "5ft" }, - }, - width: 1024, - height: 1024, - type: "default", -})); +export function getDefaultMaps(userId) { + const mapKeys = Object.keys(mapSources); + let maps = []; + let mapStates = []; + for (let i = 0; i < mapKeys.length; i++) { + const key = mapKeys[i]; + const name = Case.capital(key); + const id = `__default-${name}`; + const map = { + id, + key, + name, + owner: userId, + grid: { + size: { x: 22, y: 22 }, + inset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } }, + type: "square", + measurement: { type: "chebyshev", scale: "5ft" }, + }, + width: 1024, + height: 1024, + type: "default", + created: mapKeys.length - i, + lastModified: Date.now(), + showGrid: false, + snapToGrid: true, + group: "", + }; + maps.push(map); + const state = { + mapId: id, + tokens: {}, + drawShapes: {}, + fogShapes: {}, + editFlags: ["drawing", "tokens", "notes"], + notes: {}, + }; + mapStates.push(state); + } + return { maps, mapStates }; +} export const unknownSource = unknownImage; diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js index b819b58..b475f1a 100644 --- a/src/modals/SettingsModal.js +++ b/src/modals/SettingsModal.js @@ -58,6 +58,7 @@ function SettingsModal({ isOpen, onRequestClose }) { async function handleEraseAllData() { setIsLoading(true); localStorage.clear(); + database.close(); await database.delete(); window.location.reload(); } diff --git a/src/tokens/index.js b/src/tokens/index.js index 07f5108..d2b9438 100644 --- a/src/tokens/index.js +++ b/src/tokens/index.js @@ -80,17 +80,32 @@ function getDefaultTokenSize(key) { } } -export const tokens = Object.keys(tokenSources).map((key) => ({ - key, - name: Case.capital(key), - type: "default", - defaultSize: getDefaultTokenSize(key), - defaultLabel: "", - defaultCategory: "character", - hideInSidebar: false, - width: 256, - height: 256, - outline: "circle", -})); +export function getDefaultTokens(userId) { + const tokenKeys = Object.keys(tokenSources); + let tokens = []; + for (let i = 0; i < tokenKeys.length; i++) { + const key = tokenKeys[i]; + const name = Case.capital(key); + const token = { + key, + name, + id: `__default-${name}`, + type: "default", + defaultSize: getDefaultTokenSize(key), + defaultLabel: "", + defaultCategory: "character", + hideInSidebar: false, + width: 256, + height: 256, + outline: "circle", + owner: userId, + group: "default", + created: tokenKeys.length - i, + lastModified: Date.now(), + }; + tokens.push(token); + } + return tokens; +} export const unknownSource = unknown; diff --git a/src/upgrade.js b/src/upgrade.js new file mode 100644 index 0000000..53ae31b --- /dev/null +++ b/src/upgrade.js @@ -0,0 +1,719 @@ +// eslint-disable-next-line no-unused-vars +import Dexie, { Version } from "dexie"; +import shortid from "shortid"; +import { v4 as uuid } from "uuid"; +import Case from "case"; + +import blobToBuffer from "./helpers/blobToBuffer"; +import { getGridDefaultInset } from "./helpers/grid"; +import { createThumbnail } from "./helpers/image"; +import { + AddShapeAction, + EditShapeAction, + RemoveShapeAction, + SubtractShapeAction, + CutShapeAction, +} from "./actions"; +import { getDefaultMaps } from "./maps"; +import { getDefaultTokens } from "./tokens"; + +/** + * @callback VersionCallback + * @param {Version} version + */ + +/** + * Mapping of version number to their upgrade function + * @type {Object.} + */ +export const versions = { + // v1.2.0 + 1(v) { + v.stores({ + maps: "id, owner", + states: "mapId", + tokens: "id, owner", + user: "key", + }); + }, + // v1.2.1 - Move from blob files to array buffers + 2(v) { + v.stores({}).upgrade(async (tx) => { + const maps = await Dexie.waitFor(tx.table("maps").toArray()); + let mapBuffers = {}; + for (let map of maps) { + mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file)); + } + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.file = mapBuffers[map.id]; + }); + }); + }, + // v1.3.0 - Added new default tokens + 3(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + function mapTokenId(id) { + switch (id) { + case "__default-Axes": + return "__default-Barbarian"; + case "__default-Bird": + return "__default-Druid"; + case "__default-Book": + return "__default-Wizard"; + case "__default-Crown": + return "__default-Humanoid"; + case "__default-Dragon": + return "__default-Dragon"; + case "__default-Eye": + return "__default-Warlock"; + case "__default-Fist": + return "__default-Monk"; + case "__default-Horse": + return "__default-Fey"; + case "__default-Leaf": + return "__default-Druid"; + case "__default-Lion": + return "__default-Monstrosity"; + case "__default-Money": + return "__default-Humanoid"; + case "__default-Moon": + return "__default-Cleric"; + case "__default-Potion": + return "__default-Sorcerer"; + case "__default-Shield": + return "__default-Paladin"; + case "__default-Skull": + return "__default-Undead"; + case "__default-Snake": + return "__default-Beast"; + case "__default-Sun": + return "__default-Cleric"; + case "__default-Swords": + return "__default-Fighter"; + case "__default-Tree": + return "__default-Plant"; + case "__default-Triangle": + return "__default-Sorcerer"; + default: + return "__default-Fighter"; + } + } + for (let stateId in state.tokens) { + state.tokens[stateId].tokenId = mapTokenId( + state.tokens[stateId].tokenId + ); + state.tokens[stateId].lastEditedBy = ""; + state.tokens[stateId].rotation = 0; + } + }); + }); + }, + // v1.3.1 - Added show grid option + 4(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.showGrid = false; + }); + }); + }, + // v1.4.0 - Added fog subtraction + 5(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let fogAction of state.fogDrawActions) { + if (fogAction.type === "add" || fogAction.type === "edit") { + for (let shape of fogAction.shapes) { + shape.data.holes = []; + } + } + } + }); + }); + }, + // v1.4.2 - Added map resolutions + 6(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.resolutions = {}; + map.quality = "original"; + }); + }); + }, + // v1.5.0 - Fixed default token rogue spelling + 7(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let id in state.tokens) { + if (state.tokens[id].tokenId === "__default-Rouge") { + state.tokens[id].tokenId = "__default-Rogue"; + } + } + }); + }); + }, + // v1.5.0 - Added map snap to grid option + 8(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.snapToGrid = true; + }); + }); + }, + // v1.5.1 - Added lock, visibility and modified to tokens + 9(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let id in state.tokens) { + state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy; + delete state.tokens[id].lastEditedBy; + state.tokens[id].lastModified = Date.now(); + state.tokens[id].locked = false; + state.tokens[id].visible = true; + } + }); + }); + }, + // v1.5.1 - Added token prop category and remove isVehicle bool + 10(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("tokens") + .toCollection() + .modify((token) => { + token.category = token.isVehicle ? "vehicle" : "character"; + delete token.isVehicle; + }); + }); + }, + // v1.5.2 - Added automatic cache invalidation to maps + 11(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.lastUsed = map.lastModified; + }); + }); + }, + // v1.5.2 - Added automatic cache invalidation to tokens + 12(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("tokens") + .toCollection() + .modify((token) => { + token.lastUsed = token.lastModified; + }); + }); + }, + // v1.6.0 - Added map grouping and grid scale and offset + 13(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.group = ""; + map.grid = { + size: { x: map.gridX, y: map.gridY }, + inset: getGridDefaultInset( + { size: { x: map.gridX, y: map.gridY }, type: "square" }, + map.width, + map.height + ), + type: "square", + }; + delete map.gridX; + delete map.gridY; + delete map.gridType; + }); + }); + }, + // v1.6.0 - Added token grouping + 14(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("tokens") + .toCollection() + .modify((token) => { + token.group = ""; + }); + }); + }, + // v1.6.1 - Added width and height to tokens + 15(v) { + v.stores({}).upgrade(async (tx) => { + const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); + let tokenSizes = {}; + for (let token of tokens) { + const url = URL.createObjectURL(new Blob([token.file])); + let image = new Image(); + tokenSizes[token.id] = await Dexie.waitFor( + new Promise((resolve) => { + image.onload = () => { + resolve({ width: image.width, height: image.height }); + }; + image.src = url; + }) + ); + } + return tx + .table("tokens") + .toCollection() + .modify((token) => { + token.width = tokenSizes[token.id].width; + token.height = tokenSizes[token.id].height; + }); + }); + }, + // v1.7.0 - Added note tool + 16(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + state.notes = {}; + state.editFlags = [...state.editFlags, "notes"]; + }); + }); + }, + // 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data + 17(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let i = 0; i < state.fogDrawActions.length; i++) { + const action = state.fogDrawActions[i]; + if (action && action.type === "edit") { + for (let j = 0; j < action.shapes.length; j++) { + const shape = action.shapes[j]; + const temp = { ...shape }; + state.fogDrawActions[i].shapes[j] = { + id: temp.id, + visible: temp.visible, + }; + } + } + } + }); + }); + }, + // 1.8.0 - Added note text only mode, converted draw and fog representations + 18(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let id in state.notes) { + state.notes[id].textOnly = false; + } + + state.drawShapes = convertOldActionsToShapes( + state.mapDrawActions, + state.mapDrawActionIndex + ); + state.fogShapes = convertOldActionsToShapes( + state.fogDrawActions, + state.fogDrawActionIndex + ); + + delete state.mapDrawActions; + delete state.mapDrawActionIndex; + delete state.fogDrawActions; + delete state.fogDrawActionIndex; + }); + }); + }, + // 1.8.0 - Add thumbnail to maps and add measurement to grid + 19(v) { + v.stores({}).upgrade(async (tx) => { + const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) + .value; + const maps = await Dexie.waitFor(tx.table("maps").toArray()); + const thumbnails = {}; + for (let map of maps) { + try { + if (map.owner === userId) { + thumbnails[map.id] = await createDataThumbnail(map); + } + } catch {} + } + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.thumbnail = thumbnails[map.id]; + map.grid.measurement = { type: "chebyshev", scale: "5ft" }; + }); + }); + }, + // 1.8.0 - Add thumbnail to tokens + 20(v) { + v.stores({}).upgrade(async (tx) => { + const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) + .value; + const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); + const thumbnails = {}; + for (let token of tokens) { + try { + if (token.owner === userId) { + thumbnails[token.id] = await createDataThumbnail(token); + } + } catch {} + } + return tx + .table("tokens") + .toCollection() + .modify((token) => { + token.thumbnail = thumbnails[token.id]; + }); + }); + }, + // 1.8.0 - Upgrade for Dexie.Observable + 21(v) { + v.stores({}); + }, + // v1.8.1 - Shorten fog shape ids + 22(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let id of Object.keys(state.fogShapes)) { + const newId = shortid.generate(); + state.fogShapes[newId] = state.fogShapes[id]; + state.fogShapes[newId].id = newId; + delete state.fogShapes[id]; + } + }); + }); + }, + // v1.9.0 - Move map assets into new table + 23(v) { + v.stores({ assets: "id, owner" }).upgrade((tx) => { + tx.table("maps").each((map) => { + let assets = []; + assets.push({ + id: uuid(), + file: map.file, + width: map.width, + height: map.height, + mime: "", + prevId: map.id, + prevType: "map", + }); + + for (let resolution in map.resolutions) { + const mapRes = map.resolutions[resolution]; + assets.push({ + id: uuid(), + file: mapRes.file, + width: mapRes.width, + height: mapRes.height, + mime: "", + prevId: map.id, + prevType: "mapResolution", + resolution, + }); + } + + assets.push({ + id: uuid(), + file: map.thumbnail.file, + width: map.thumbnail.width, + height: map.thumbnail.height, + mime: "", + prevId: map.id, + prevType: "mapThumbnail", + }); + + tx.table("assets").bulkAdd(assets); + }); + }); + }, + // v1.9.0 - Move token assets into new table + 24(v) { + v.stores({}).upgrade((tx) => { + tx.table("tokens").each((token) => { + let assets = []; + assets.push({ + id: uuid(), + file: token.file, + width: token.width, + height: token.height, + mime: "", + prevId: token.id, + prevType: "token", + }); + assets.push({ + id: uuid(), + file: token.thumbnail.file, + width: token.thumbnail.width, + height: token.thumbnail.height, + mime: "", + prevId: token.id, + prevType: "tokenThumbnail", + }); + tx.table("assets").bulkAdd(assets); + }); + }); + }, + // v1.9.0 - Create foreign keys for assets + 25(v) { + v.stores({}).upgrade((tx) => { + tx.table("assets").each((asset) => { + if (asset.prevType === "map") { + tx.table("maps").update(asset.prevId, { + file: asset.id, + }); + } else if (asset.prevType === "token") { + tx.table("tokens").update(asset.prevId, { + file: asset.id, + }); + } else if (asset.prevType === "mapThumbnail") { + tx.table("maps").update(asset.prevId, { thumbnail: asset.id }); + } else if (asset.prevType === "tokenThumbnail") { + tx.table("tokens").update(asset.prevId, { thumbnail: asset.id }); + } else if (asset.prevType === "mapResolution") { + tx.table("maps").update(asset.prevId, { + resolutions: undefined, + [asset.resolution]: asset.id, + }); + } + }); + }); + }, + // v1.9.0 - Remove asset migration helpers + 26(v) { + v.stores({}).upgrade((tx) => { + tx.table("assets") + .toCollection() + .modify((asset) => { + delete asset.prevId; + if (asset.prevType === "mapResolution") { + delete asset.resolution; + } + delete asset.prevType; + }); + }); + }, + // v1.9.0 - Remap map resolution assets + 27(v) { + v.stores({}).upgrade((tx) => { + tx.table("maps") + .toCollection() + .modify((map) => { + const resolutions = ["low", "medium", "high", "ultra"]; + map.resolutions = {}; + for (let res of resolutions) { + if (res in map) { + map.resolutions[res] = map[res]; + delete map[res]; + } + } + delete map.lastUsed; + }); + }); + }, + // v1.9.0 - Move tokens to use more defaults and add token outline to tokens + 28(v) { + v.stores({}).upgrade((tx) => { + tx.table("tokens") + .toCollection() + .modify(async (token) => { + token.defaultCategory = token.category; + delete token.category; + token.defaultLabel = ""; + if (token.width === token.height) { + token.outline = "circle"; + } else { + token.outline = "rect"; + } + delete token.lastUsed; + }); + }); + }, + // v1.9.0 - Move tokens to use more defaults and add token outline to token states + 29(v) { + v.stores({}).upgrade(async (tx) => { + const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); + tx.table("states") + .toCollection() + .modify((state) => { + for (let id in state.tokens) { + if (!state.tokens[id].tokenId.startsWith("__default")) { + const token = tokens.find( + (token) => token.id === state.tokens[id].tokenId + ); + if (token) { + state.tokens[id].category = token.defaultCategory; + state.tokens[id].file = token.file; + state.tokens[id].type = "file"; + state.tokens[id].outline = token.outline; + state.tokens[id].width = token.width; + state.tokens[id].height = token.height; + } else { + state.tokens[id].category = "character"; + state.tokens[id].type = "file"; + state.tokens[id].file = ""; + state.tokens[id].outline = "rect"; + state.tokens[id].width = 256; + state.tokens[id].height = 256; + } + } else { + state.tokens[id].category = "character"; + state.tokens[id].type = "default"; + state.tokens[id].key = Case.camel( + state.tokens[id].tokenId.slice(10) + ); + state.tokens[id].outline = "circle"; + state.tokens[id].width = 256; + state.tokens[id].height = 256; + } + } + }); + }); + }, + // v1.9.0 - Remove maps not owned by user as cache is now done on the asset level + 30(v) { + v.stores({}).upgrade(async (tx) => { + const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) + ?.value; + if (userId) { + tx.table("maps").where("owner").notEqual(userId).delete(); + } + }); + }, + // v1.9.0 - Remove tokens not owned by user as cache is now done on the asset level + 31(v) { + v.stores({}).upgrade(async (tx) => { + const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) + ?.value; + if (userId) { + tx.table("tokens").where("owner").notEqual(userId).delete(); + } + }); + }, + // v1.9.0 - Store default maps and tokens in db + 32(v) { + v.stores({}).upgrade(async (tx) => { + const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) + ?.value; + if (!userId) { + return; + } + const { maps } = getDefaultMaps(userId); + tx.table("maps").bulkAdd(maps); + const tokens = getDefaultTokens(userId); + tx.table("tokens").bulkAdd(tokens); + }); + }, +}; + +export const latestVersion = 32; + +/** + * Load versions onto a database up to a specific version number + * @param {Dexie} db + * @param {number=} upTo version number to load up to, latest version if undefined + */ +export function loadVersions(db, upTo = latestVersion) { + for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) { + versions[versionNumber](db.version(versionNumber)); + } +} + +/** + * Convert from the previous representation of actions (1.7.0) to the new representation (1.8.0) + * and combine into shapes + * @param {Array} actions + * @param {number} actionIndex + */ +function convertOldActionsToShapes(actions, actionIndex) { + let newShapes = {}; + for (let i = 0; i <= actionIndex; i++) { + const action = actions[i]; + if (!action) { + continue; + } + let newAction; + if (action.shapes) { + if (action.type === "add") { + newAction = new AddShapeAction(action.shapes); + } else if (action.type === "edit") { + newAction = new EditShapeAction(action.shapes); + } else if (action.type === "remove") { + newAction = new RemoveShapeAction(action.shapes); + } else if (action.type === "subtract") { + newAction = new SubtractShapeAction(action.shapes); + } else if (action.type === "cut") { + newAction = new CutShapeAction(action.shapes); + } + } else if (action.type === "remove" && action.shapeIds) { + newAction = new RemoveShapeAction(action.shapeIds); + } + + if (newAction) { + newShapes = newAction.execute(newShapes); + } + } + return newShapes; +} + +// Helper to create a thumbnail for a file in a db +async function createDataThumbnail(data) { + let url; + if (data?.resolutions?.low?.file) { + url = URL.createObjectURL(new Blob([data.resolutions.low.file])); + } else { + url = URL.createObjectURL(new Blob([data.file])); + } + return await Dexie.waitFor( + new Promise((resolve) => { + let image = new Image(); + image.onload = async () => { + const thumbnail = await createThumbnail(image); + resolve({ + file: thumbnail.file, + width: thumbnail.width, + height: thumbnail.height, + type: "file", + id: "thumbnail", + }); + }; + image.src = url; + }), + 60000 * 10 // 10 minute timeout + ); +} diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index b196fee..d49384f 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -144,7 +144,8 @@ let service = { importDB = getDatabase( { addons: [] }, databaseName, - importMeta.data.databaseVersion + importMeta.data.databaseVersion, + false ); await importInto(importDB, data, { progressCallback,