diff --git a/package.json b/package.json
index c71c97a..e700bdd 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"dependencies": {
"@babylonjs/core": "^4.2.0",
"@babylonjs/loaders": "^4.2.0",
- "@mitchemmc/dexie-export-import": "^1.0.0-rc.2",
+ "@mitchemmc/dexie-export-import": "^1.0.0",
"@msgpack/msgpack": "^2.3.0",
"@sentry/react": "^5.27.1",
"@stripe/stripe-js": "^1.3.2",
diff --git a/src/database.js b/src/database.js
index 6eef6ad..51983f0 100644
--- a/src/database.js
+++ b/src/database.js
@@ -385,8 +385,8 @@ function loadVersions(db) {
}
// Get the dexie database used in DatabaseContext
-export function getDatabase(options) {
- let db = new Dexie("OwlbearRodeoDB", options);
+export function getDatabase(options, name="OwlbearRodeoDB") {
+ let db = new Dexie(name, options);
loadVersions(db);
return db;
}
diff --git a/src/modals/DataSelectorModal.js b/src/modals/DataSelectorModal.js
new file mode 100644
index 0000000..d82a488
--- /dev/null
+++ b/src/modals/DataSelectorModal.js
@@ -0,0 +1,210 @@
+import React, { useEffect, useState } from "react";
+import { Box, Label, Flex, Button, Text, Checkbox, Divider } from "theme-ui";
+import SimpleBar from "simplebar-react";
+
+import Modal from "../components/Modal";
+import LoadingOverlay from "../components/LoadingOverlay";
+
+import { getDatabase } from "../database";
+
+function DataSelectorModal({
+ isOpen,
+ onRequestClose,
+ onConfirm,
+ confirmText,
+ label,
+ databaseName,
+ filter,
+}) {
+ const [maps, setMaps] = useState({});
+ const [tokens, setTokens] = useState({});
+
+ const [isLoading, setIsLoading] = useState(false);
+ const hasMaps = Object.values(maps).length > 0;
+ const hasTokens = Object.values(tokens).length > 0;
+
+ useEffect(() => {
+ async function loadData() {
+ if (isOpen && databaseName) {
+ setIsLoading(true);
+ const db = getDatabase({}, databaseName);
+ let loadedMaps = {};
+ let loadedTokens = {};
+ await db
+ .table("maps")
+ .filter((map) => filter("maps", map, map.id))
+ .each((map) => {
+ loadedMaps[map.id] = { name: map.name, id: map.id, checked: true };
+ });
+ await db
+ .table("tokens")
+ .filter((token) => filter("tokens", token, token.id))
+ .each((token) => {
+ loadedTokens[token.id] = {
+ name: token.name,
+ id: token.id,
+ checked: true,
+ };
+ });
+ db.close();
+ setMaps(loadedMaps);
+ setTokens(loadedTokens);
+ setIsLoading(false);
+ } else {
+ setMaps({});
+ setTokens({});
+ }
+ }
+ loadData();
+ }, [isOpen, databaseName, filter]);
+
+ function handleConfirm() {
+ let checkedMaps = Object.values(maps).filter((map) => map.checked);
+ let checkedTokens = Object.values(tokens).filter((token) => token.checked);
+ onConfirm(checkedMaps, checkedTokens);
+ }
+
+ return (
+
+
+
+ {!hasMaps && !hasTokens && (
+
+ No custom maps or tokens found.
+
+ )}
+
+
+ {hasMaps && (
+ <>
+
+
+
+ {Object.values(maps).map((map) => (
+
+ ))}
+ >
+ )}
+ {hasMaps && hasTokens && }
+ {hasTokens && (
+ <>
+
+ {Object.values(tokens).map((token) => (
+
+ ))}
+ >
+ )}
+
+
+
+
+
+
+ {isLoading && }
+
+
+ );
+}
+
+DataSelectorModal.defaultProps = {
+ label: "Select data",
+ confirmText: "Yes",
+ filter: () => true,
+ databaseName: "OwlbearRodeoDB",
+};
+
+export default DataSelectorModal;
diff --git a/src/modals/ImportExportModal.js b/src/modals/ImportExportModal.js
index b917779..ad87311 100644
--- a/src/modals/ImportExportModal.js
+++ b/src/modals/ImportExportModal.js
@@ -2,23 +2,36 @@ import React, { useRef, useState, useEffect } from "react";
import { Box, Label, Text, Button, Flex } from "theme-ui";
import { saveAs } from "file-saver";
import * as Comlink from "comlink";
+import shortid from "shortid";
import Modal from "../components/Modal";
import LoadingOverlay from "../components/LoadingOverlay";
import LoadingBar from "../components/LoadingBar";
import Banner from "../components/Banner";
+import { useAuth } from "../contexts/AuthContext";
+
+import DataSelectorModal from "./DataSelectorModal";
+
+import { getDatabase } from "../database";
+
import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax
const worker = Comlink.wrap(new DatabaseWorker());
+const importDBName = "OwlbearRodeoImportDB";
+
function ImportExportModal({ isOpen, onRequestClose }) {
+ const { userId } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const backgroundTaskRunningRef = useRef(false);
const fileInputRef = useRef();
+ const [showImportSelector, setShowImportSelector] = useState(false);
+ const [showExportSelector, setShowExportSelector] = useState(false);
+
function openFileDialog() {
if (fileInputRef.current) {
fileInputRef.current.click();
@@ -35,10 +48,14 @@ function ImportExportModal({ isOpen, onRequestClose }) {
setIsLoading(true);
backgroundTaskRunningRef.current = true;
try {
- await worker.importData(file, Comlink.proxy(handleDBProgress));
+ await worker.importData(
+ file,
+ importDBName,
+ Comlink.proxy(handleDBProgress)
+ );
setIsLoading(false);
+ setShowImportSelector(true);
backgroundTaskRunningRef.current = false;
- window.location.reload();
} catch (e) {
setIsLoading(false);
backgroundTaskRunningRef.current = false;
@@ -52,19 +69,14 @@ function ImportExportModal({ isOpen, onRequestClose }) {
setError(e);
}
}
+ // Set file input to null to allow adding the same data 2 times in a row
+ if (fileInputRef.current) {
+ fileInputRef.current.value = null;
+ }
}
- async function handleExportDatabase() {
- setIsLoading(true);
- backgroundTaskRunningRef.current = true;
- try {
- const blob = await worker.exportData(Comlink.proxy(handleDBProgress));
- saveAs(blob, `${new Date().toISOString()}.owlbear`);
- } catch (e) {
- setError(e);
- }
- setIsLoading(false);
- backgroundTaskRunningRef.current = false;
+ function handleExportDatabase() {
+ setShowExportSelector(true);
}
useEffect(() => {
@@ -88,6 +100,91 @@ function ImportExportModal({ isOpen, onRequestClose }) {
onRequestClose();
}
+ async function handleImportSelectorClose() {
+ const importDB = getDatabase({}, importDBName);
+ await importDB.delete();
+ setShowImportSelector(false);
+ }
+
+ async function handleImportSelectorConfirm(checkedMaps, checkedTokens) {
+ setIsLoading(true);
+ backgroundTaskRunningRef.current = true;
+ setShowImportSelector(false);
+ loadingProgressRef.current = 0;
+
+ const importDB = getDatabase({}, importDBName);
+ const db = getDatabase({});
+
+ if (checkedMaps.length > 0) {
+ const mapIds = checkedMaps.map((map) => map.id);
+ const mapsToAdd = await importDB.table("maps").bulkGet(mapIds);
+ let newMaps = [];
+ let newStates = [];
+ for (let map of mapsToAdd) {
+ const state = await importDB.table("states").get(map.id);
+ const newId = shortid.generate();
+ // Generate new id and change owner
+ newMaps.push({ ...map, id: newId, owner: userId });
+ newStates.push({ ...state, mapId: newId });
+ }
+ await db.table("maps").bulkAdd(newMaps);
+ await db.table("states").bulkAdd(newStates);
+ }
+ if (checkedTokens.length > 0) {
+ const tokenIds = checkedTokens.map((token) => token.id);
+ const tokensToAdd = await importDB.table("tokens").bulkGet(tokenIds);
+ console.log(tokensToAdd);
+ let newTokens = [];
+ for (let token of tokensToAdd) {
+ const newId = shortid.generate();
+ // Generate new id and change owner
+ newTokens.push({ ...token, id: newId, owner: userId });
+ }
+ await db.table("tokens").bulkAdd(newTokens);
+ }
+
+ await importDB.delete();
+ setIsLoading(false);
+ backgroundTaskRunningRef.current = false;
+ window.location.reload();
+ }
+
+ function exportSelectorFilter(table, value) {
+ // Only show owned maps and tokens
+ if (table === "maps" || table === "tokens") {
+ if (value.owner === userId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ async function handleExportSelectorClose() {
+ setShowExportSelector(false);
+ }
+
+ async function handleExportSelectorConfirm(checkedMaps, checkedTokens) {
+ setShowExportSelector(false);
+ setIsLoading(true);
+ backgroundTaskRunningRef.current = true;
+
+ const mapIds = checkedMaps.map((map) => map.id);
+ const tokenIds = checkedTokens.map((token) => token.id);
+
+ try {
+ const blob = await worker.exportData(
+ Comlink.proxy(handleDBProgress),
+ mapIds,
+ tokenIds
+ );
+ saveAs(blob, `${new Date().toISOString()}.owlbear`);
+ } catch (e) {
+ setError(e);
+ }
+ setIsLoading(false);
+ backgroundTaskRunningRef.current = false;
+ }
+
return (
-
+
- Importing a database will overwrite your current data.
+ Select import or export then select the data you wish to use
handleImportDatabase(event.target.files[0])}
@@ -144,6 +241,22 @@ function ImportExportModal({ isOpen, onRequestClose }) {
+
+
);
diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js
index 5c9467e..45dadf7 100644
--- a/src/modals/SettingsModal.js
+++ b/src/modals/SettingsModal.js
@@ -151,7 +151,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
onClick={() => setIsImportExportModalOpen(true)}
disabled={databaseStatus !== "loaded"}
>
- Import / Export Database
+ Import / Export Data
{storageEstimate && (
diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js
index 1c6882e..935a928 100644
--- a/src/workers/DatabaseWorker.js
+++ b/src/workers/DatabaseWorker.js
@@ -1,5 +1,9 @@
import * as Comlink from "comlink";
-import { importInto, exportDB } from "@mitchemmc/dexie-export-import";
+import {
+ importInto,
+ exportDB,
+ peakImportFile,
+} from "@mitchemmc/dexie-export-import";
import { encode } from "@msgpack/msgpack";
import { getDatabase } from "../database";
@@ -17,7 +21,9 @@ let service = {
let db = getDatabase({});
if (key) {
// Load specific item
- return await db.table(table).get(key);
+ const data = await db.table(table).get(key);
+ db.close();
+ return data;
} else {
// Load entire table
let items = [];
@@ -31,6 +37,8 @@ let service = {
}
});
+ db.close();
+
// Pack data with msgpack so we can use transfer to avoid memory issues
const packed = encode(items);
return Comlink.transfer(packed, [packed.buffer]);
@@ -41,23 +49,67 @@ let service = {
/**
* Export current database
* @param {function} progressCallback
+ * @param {string[]} maps An array of map ids to export
+ * @param {string[]} tokens An array of token ids to export
*/
- async exportData(progressCallback) {
+ async exportData(progressCallback, maps, tokens) {
let db = getDatabase({});
- return await exportDB(db, {
+
+ const filter = (table, value) => {
+ if (table === "maps") {
+ return maps.includes(value.id);
+ }
+ if (table === "states") {
+ return maps.includes(value.mapId);
+ }
+ if (table === "tokens") {
+ return tokens.includes(value.id);
+ }
+ return false;
+ };
+
+ const data = await exportDB(db, {
progressCallback,
+ filter,
numRowsPerChunk: 1,
});
+ db.close();
+ return data;
},
/**
* Import into current database
* @param {Blob} data
+ * @param {string} databaseName The name of the database to import into
* @param {function} progressCallback
*/
- async importData(data, progressCallback) {
+ async importData(data, databaseName, progressCallback) {
+ const importMeta = await peakImportFile(data);
let db = getDatabase({});
- await importInto(db, data, { progressCallback, overwriteValues: true });
+
+ if (importMeta.data.databaseName !== db.name) {
+ throw new Error("Unable to import database, name mismatch");
+ }
+
+ let importDB = getDatabase({}, databaseName);
+ await importInto(importDB, data, {
+ progressCallback,
+ acceptNameDiff: true,
+ overwriteValues: true,
+ filter: (table, value) => {
+ // Ensure values are of the correct form
+ if (table === "maps" || table === "tokens") {
+ console.log("id" in value && "owner" in value);
+ return "id" in value && "owner" in value;
+ }
+ if (table === "states") {
+ return "mapId" in value;
+ }
+ return true;
+ },
+ });
+ db.close();
+ importDB.close();
},
};
diff --git a/yarn.lock b/yarn.lock
index 8bc129b..d39a9ac 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1443,10 +1443,10 @@
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.16.tgz#538eb14473194d0b3c54020cb230e426174315cd"
integrity sha512-+FhuSVOPo7+4fZaRwWuCSRUcZkJOkZu0rfAbBKvoCg1LWb1Td8Vzi0DTLORdSvgWNbU6+EL40HIgwTOs00x2Jw==
-"@mitchemmc/dexie-export-import@^1.0.0-rc.2":
- version "1.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/@mitchemmc/dexie-export-import/-/dexie-export-import-1.0.0-rc.2.tgz#80c3e9b3331c9ad50cfe3c9378aedf7640a467ae"
- integrity sha512-iCkiUrGTYlIy6sWfp1DeeUMv2NlilG70e+3FV54Av5nsGsFuUCrTh7sCR6pcQChmq1fdYXu6Jg//SkocP5rcNg==
+"@mitchemmc/dexie-export-import@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@mitchemmc/dexie-export-import/-/dexie-export-import-1.0.0.tgz#035ed78cd39940662e66bcccd8152e552d0b5a45"
+ integrity sha512-mDiOD24gYXylrToc0t6nzeNfgnhCDqiLC8AdQ698/Hu229qZgj7ECFDzj+HmIXLlriPniWhIZxv02j7EN7zjEQ==
dependencies:
dexie "^3.0.0-alpha.5 || ^2.0.4"
rollup-plugin-sourcemaps "^0.6.3"