From 83a987d9dff5590118727402e9a0f4f1824c90c4 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 26 Jan 2021 21:49:44 +1100 Subject: [PATCH] Add initial import export functionality --- package.json | 2 + src/components/LoadingBar.js | 41 +++++++ src/components/map/MapLoadingOverlay.js | 40 ++---- src/modals/ImportExportModal.js | 157 ++++++++++++++++++++++++ src/modals/SettingsModal.js | 14 +++ yarn.lock | 15 ++- 6 files changed, 234 insertions(+), 35 deletions(-) create mode 100644 src/components/LoadingBar.js create mode 100644 src/modals/ImportExportModal.js 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"