diff --git a/package.json b/package.json
index 76ae3a2..743183a 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"comlink": "^4.3.0",
"deep-diff": "^1.0.2",
"dexie": "^3.0.3",
+ "dexie-export-import": "^1.0.0",
"err-code": "^2.0.3",
"fake-indexeddb": "^3.1.2",
"fuse.js": "^6.4.1",
@@ -48,6 +49,7 @@
"simplify-js": "^1.2.4",
"socket.io-client": "^3.0.3",
"source-map-explorer": "^2.4.2",
+ "streamsaver": "^2.0.5",
"theme-ui": "^0.3.1",
"use-image": "^1.0.5",
"webrtc-adapter": "^7.5.1"
diff --git a/src/components/LoadingBar.js b/src/components/LoadingBar.js
new file mode 100644
index 0000000..68e56dc
--- /dev/null
+++ b/src/components/LoadingBar.js
@@ -0,0 +1,41 @@
+import React, { useEffect, useRef } from "react";
+import { Progress } from "theme-ui";
+
+function LoadingBar({ isLoading, loadingProgressRef }) {
+ const requestRef = useRef();
+ const progressBarRef = useRef();
+
+ // Use an animation frame to update the progress bar
+ // This bypasses react allowing the animation to be smooth
+ useEffect(() => {
+ function animate() {
+ if (!isLoading) {
+ return;
+ }
+ requestRef.current = requestAnimationFrame(animate);
+ if (progressBarRef.current) {
+ progressBarRef.current.value = loadingProgressRef.current;
+ }
+ }
+
+ requestRef.current = requestAnimationFrame(animate);
+
+ return () => {
+ cancelAnimationFrame(requestRef.current);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isLoading]);
+
+ return (
+
+ );
+}
+
+export default LoadingBar;
diff --git a/src/components/map/MapLoadingOverlay.js b/src/components/map/MapLoadingOverlay.js
index 6c63620..da9661a 100644
--- a/src/components/map/MapLoadingOverlay.js
+++ b/src/components/map/MapLoadingOverlay.js
@@ -1,35 +1,13 @@
-import React, { useContext, useEffect, useRef } from "react";
-import { Box, Progress } from "theme-ui";
+import React, { useContext } from "react";
+import { Box } from "theme-ui";
import MapLoadingContext from "../../contexts/MapLoadingContext";
+import LoadingBar from "../LoadingBar";
+
function MapLoadingOverlay() {
const { isLoading, loadingProgressRef } = useContext(MapLoadingContext);
- const requestRef = useRef();
- const progressBarRef = useRef();
-
- // Use an animation frame to update the progress bar
- // This bypasses react allowing the animation to be smooth
- useEffect(() => {
- function animate() {
- if (!isLoading) {
- return;
- }
- requestRef.current = requestAnimationFrame(animate);
- if (progressBarRef.current) {
- progressBarRef.current.value = loadingProgressRef.current;
- }
- }
-
- requestRef.current = requestAnimationFrame(animate);
-
- return () => {
- cancelAnimationFrame(requestRef.current);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isLoading]);
-
return (
isLoading && (
-
)
diff --git a/src/modals/ImportExportModal.js b/src/modals/ImportExportModal.js
new file mode 100644
index 0000000..2f5c793
--- /dev/null
+++ b/src/modals/ImportExportModal.js
@@ -0,0 +1,157 @@
+import React, { useRef, useState, useContext, useEffect } from "react";
+import { Box, Label, Text, Button, Flex } from "theme-ui";
+import { importDB, exportDB } from "dexie-export-import";
+import streamSaver from "streamsaver";
+
+import Modal from "../components/Modal";
+import LoadingOverlay from "../components/LoadingOverlay";
+import LoadingBar from "../components/LoadingBar";
+
+import DatabaseContext from "../contexts/DatabaseContext";
+
+function ImportDatabaseModal({ isOpen, onRequestClose }) {
+ const { database } = useContext(DatabaseContext);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fileInputRef = useRef();
+
+ function openFileDialog() {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ }
+
+ const loadingProgressRef = useRef(0);
+
+ function handleDBProgress({ completedRows, totalRows }) {
+ loadingProgressRef.current = completedRows / totalRows;
+ }
+
+ async function handleImportDatabase(file) {
+ setIsLoading(true);
+ await database.delete();
+ await importDB(file, { progressCallback: handleDBProgress });
+ setIsLoading(false);
+ window.location.reload();
+ }
+
+ const fileStreamRef = useRef();
+
+ async function handleExportDatabase() {
+ setIsLoading(true);
+ const blob = await exportDB(database, {
+ progressCallback: handleDBProgress,
+ });
+ const fileStream = streamSaver.createWriteStream(
+ `${new Date().toISOString()}.db`,
+ {
+ size: blob.size,
+ }
+ );
+ fileStreamRef.current = fileStream;
+
+ const readableStream = blob.stream();
+ if (window.WritableStream && readableStream.pipeTo) {
+ await readableStream.pipeTo(fileStream);
+ setIsLoading(false);
+ } else {
+ const writer = fileStream.getWriter();
+ const reader = readableStream.getReader();
+ async function pump() {
+ const res = await reader.read();
+ if (res.done) {
+ writer.close();
+ } else {
+ writer.write(res.value).then(pump);
+ }
+ }
+ await pump();
+ setIsLoading(false);
+ }
+ fileStreamRef.current = null;
+ }
+
+ function handleClose() {
+ if (isLoading) {
+ return;
+ }
+ onRequestClose();
+ }
+
+ useEffect(() => {
+ function handleBeforeUnload(event) {
+ if (isLoading) {
+ event.returnValue =
+ "Database is still processing, are you sure you want to leave?";
+ }
+ }
+
+ function handleUnload() {
+ if (fileStreamRef.current) {
+ fileStreamRef.current.abort();
+ }
+ }
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ window.addEventListener("unload", handleUnload);
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ window.removeEventListener("unload", handleUnload);
+ };
+ }, [isLoading]);
+
+ return (
+
+
+
+
+
+ Importing a database will overwrite your current data.
+
+ handleImportDatabase(event.target.files[0])}
+ type="file"
+ accept=".db"
+ style={{ display: "none" }}
+ ref={fileInputRef}
+ />
+
+
+
+
+
+ {isLoading && (
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default ImportDatabaseModal;
diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js
index 10af204..0d11144 100644
--- a/src/modals/SettingsModal.js
+++ b/src/modals/SettingsModal.js
@@ -19,6 +19,7 @@ import DatabaseContext from "../contexts/DatabaseContext";
import useSetting from "../helpers/useSetting";
import ConfirmModal from "./ConfirmModal";
+import ImportExportModal from "./ImportExportModal";
function SettingsModal({ isOpen, onRequestClose }) {
const { database } = useContext(DatabaseContext);
@@ -26,6 +27,7 @@ function SettingsModal({ isOpen, onRequestClose }) {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [labelSize, setLabelSize] = useSetting("map.labelSize");
const [storageEstimate, setStorageEstimate] = useState();
+ const [isImportExportModalOpen, setIsImportExportModalOpen] = useState(false);
useEffect(() => {
async function estimateStorage() {
@@ -125,6 +127,14 @@ function SettingsModal({ isOpen, onRequestClose }) {
Erase all content and reset
+
+
+
{storageEstimate && (
@@ -148,6 +158,10 @@ function SettingsModal({ isOpen, onRequestClose }) {
description="This will remove all data including saved maps and tokens."
confirmText="Erase"
/>
+ setIsImportExportModalOpen(false)}
+ />
>
);
}
diff --git a/yarn.lock b/yarn.lock
index 9639718..1c299a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4532,7 +4532,14 @@ detect-port-alt@1.1.6:
address "^1.0.1"
debug "^2.6.0"
-dexie@^3.0.3:
+dexie-export-import@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dexie-export-import/-/dexie-export-import-1.0.0.tgz#2e2cc375a416f540c89d2a39f623d297077221a4"
+ integrity sha512-mqtREaI+/tSEfpwOZeYNyX+FfCxTYt0RB4BIevX4wFGMQL1mQ0cWfIGXBpoRGEnHdiHbf3QNK2+0Ras3dNJfIQ==
+ dependencies:
+ dexie "^3.0.0-alpha.5 || ^2.0.4"
+
+"dexie@^3.0.0-alpha.5 || ^2.0.4", dexie@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.0.3.tgz#ede63849dfe5f07e13e99bb72a040e8ac1d29dab"
integrity sha512-BSFhGpngnCl1DOr+8YNwBDobRMH0ziJs2vts69VilwetHYOtEDcLqo7d/XiIphM0tJZ2rPPyAGd31lgH2Ln3nw==
@@ -10946,7 +10953,6 @@ simple-peer@feross/simple-peer#694/head:
resolved "https://codeload.github.com/feross/simple-peer/tar.gz/0d08d07b83ff3b8c60401688d80642d24dfeffe2"
dependencies:
debug "^4.0.1"
- err-code "^2.0.3"
get-browser-rtc "^1.0.0"
queue-microtask "^1.1.0"
randombytes "^2.0.3"
@@ -11325,6 +11331,11 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
+streamsaver@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/streamsaver/-/streamsaver-2.0.5.tgz#3212f0e908fcece5b3a65591094475cf87850d00"
+ integrity sha512-KIWtBvi8A6FiFZGNSyuIZRZM6C8AvnWTiCx/TYa7so420vC5sQwcBKkdqInuGWoWMfeWy/P+/cRqMtWVf4RW9w==
+
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"