Add default maps and tokens to db and split db upgrade into new file

This commit is contained in:
Mitchell McCaffrey 2021-05-01 12:54:00 +10:00
parent 2b342480a8
commit f419029d56
10 changed files with 815 additions and 760 deletions

View File

@ -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,

View File

@ -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 });
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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.<number, VersionCallback>}
*/
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;
}

View File

@ -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;

View File

@ -58,6 +58,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
async function handleEraseAllData() {
setIsLoading(true);
localStorage.clear();
database.close();
await database.delete();
window.location.reload();
}

View File

@ -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;

719
src/upgrade.js Normal file
View File

@ -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.<number, VersionCallback>}
*/
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
);
}

View File

@ -144,7 +144,8 @@ let service = {
importDB = getDatabase(
{ addons: [] },
databaseName,
importMeta.data.databaseVersion
importMeta.data.databaseVersion,
false
);
await importInto(importDB, data, {
progressCallback,