diff --git a/backend/src/index.js b/backend/src/index.js
index 3fab673d..7a577848 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -20,6 +20,7 @@ export const main = async ({ store, schedules, functions, hooks }) => {
await store.createOrUpdateBox("room", { security: "public" });
await store.createOrUpdateBox("session", { security: "public" });
await store.createOrUpdateBox("user", { security: "private" });
+ await store.createOrUpdateBox("files", { security: "private" });
// Add schedules
schedules["daily"] = [deleteOldSession(store)];
diff --git a/package-lock.json b/package-lock.json
index 1c931d6d..49a9d786 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"lodash.debounce": "^4.0.8",
"marked": "^4.0.12",
"memoizee": "^0.4.14",
+ "mime-types": "^2.1.35",
"nanoid": "^3.3.0",
"openvidu-browser": "^2.26.0",
"p-limit": "^4.0.0",
@@ -8011,7 +8012,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -8020,7 +8020,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -16995,14 +16994,12 @@
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"requires": {
"mime-db": "1.52.0"
}
diff --git a/package.json b/package.json
index c3fc99f6..66abec78 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"lodash.debounce": "^4.0.8",
"marked": "^4.0.12",
"memoizee": "^0.4.14",
+ "mime-types": "^2.1.35",
"nanoid": "^3.3.0",
"openvidu-browser": "^2.26.0",
"p-limit": "^4.0.0",
diff --git a/src/gameComponents/Canvas.jsx b/src/gameComponents/Canvas.jsx
index ab4eeeff..0b0bd3d2 100644
--- a/src/gameComponents/Canvas.jsx
+++ b/src/gameComponents/Canvas.jsx
@@ -152,8 +152,6 @@ const NoCanvas = ({ layers, width, height }) => {
}
}, [actions, firstImage.url]);
- console.log(firstImage.url, state.status, state.error);
-
if (state.status === "error") {
return ;
}
diff --git a/src/gameComponents/Image/index.js b/src/gameComponents/Image/index.js
index dbbb91a7..d5ec27e5 100644
--- a/src/gameComponents/Image/index.js
+++ b/src/gameComponents/Image/index.js
@@ -57,6 +57,16 @@ const Template = createItemTemplate({
},
name: i18n.t("Image"),
template: {},
+ async mapMedia(item, fn) {
+ const result = await Promise.all([
+ fn(item.content),
+ fn(item.backContent),
+ fn(item.overlay?.content),
+ ]);
+ item.content = result[0];
+ item.backContent = result[1];
+ item.overlay = { content: result[2] };
+ },
});
export default Template;
diff --git a/src/gameComponents/utils.js b/src/gameComponents/utils.js
index cf545c0c..03d53689 100644
--- a/src/gameComponents/utils.js
+++ b/src/gameComponents/utils.js
@@ -16,7 +16,7 @@ const resize = (prop) => ({ width, actualWidth, prevState }) => {
export const sizeResize = resize("size");
export const radiusResize = resize("radius");
-const defaultTemplate = {
+const defaultTemplate = () => ({
resizeDirections: {
w: true,
h: true,
@@ -25,15 +25,12 @@ const defaultTemplate = {
applyDefault(item) {
return item;
},
-};
+ // eslint-disable-next-line no-unused-vars
+ mapMedia(item, fn) {},
+});
export const createItemTemplate = (template) => {
- return Object.assign(
- {},
- JSON.parse(JSON.stringify(defaultTemplate)),
- template,
- { uid: uid() }
- );
+ return Object.assign({}, defaultTemplate(), template, { uid: uid() });
};
export default createItemTemplate;
diff --git a/src/games/testGame.js b/src/games/testGame.js
index 60eb43f8..80999e7b 100644
--- a/src/games/testGame.js
+++ b/src/games/testGame.js
@@ -292,6 +292,13 @@ const genGame = () => {
color: "#D00022",
size: 80,
},
+ {
+ type: "image",
+ content: "/game_assets/JC.jpg",
+ backContent: "/game_assets/Red_back.jpg",
+ overlay: { content: "/game_assets/overlay.png" },
+ width: 100,
+ },
],
board: {
size: 1000,
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 803359db..7f5d8637 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -324,7 +324,7 @@
"Error while loading the module, please report the error...": "Error while loading the module, please report the error...",
"Drag'n'drop a vassal module here": "Drag'n'drop a vassal module here",
"Load a Vassal module": "Load a Vassal module",
- "Load a Vassal module?": "Load a Vassal module?",
+ "Load a Vassal module?": "Load a Vassal module? (experimental)",
"Load Vassal module": "Load Vassal module",
"Import Vassal module": "Import Vassal module",
"Secondary color": "Secondary color",
@@ -351,5 +351,9 @@
"Don't forget to save your game using the save button located in the middle of the left sidebar.\n\n": "Don't forget to save your game using the save button located in the middle of the left sidebar.\n\n",
"Have fun exploring the possibilities and enjoy the process of game creation!": "Have fun exploring the possibilities and enjoy the process of game creation!",
"Login failed. Please try again.": "Login failed. Please try again.",
- "Home": "Home"
+ "Home": "Home",
+ "Die": "Die",
+ "Image die": "Image die",
+ "Include files": "Include files",
+ "An error occurred. Try again!": "An error occurred. Try again!"
}
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 1c21e653..d75d4639 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -77,7 +77,7 @@
"Game saved": "Sauvegarde effectuée",
"Game studio": "Créer",
"Game": "Jeu",
- "Generating export": "Génération de l'export",
+ "Generating export": "Génération de l'export...",
"Github": "Github",
"Go back to studio": "Retourner au studio",
"Go back": "Retour",
@@ -322,9 +322,9 @@
"Next": "Suivant",
"Previous": "Précédent",
"Error while loading the module, please report the error...": "Une erreur est survenue pendant le chargement de ce module, merci de nous prévenir...",
- "Drag'n'drop a vassal module here": "Glisser&Déposer un module Vassal ici",
+ "Drag'n'drop a vassal module here": "Glisser & Déposer un module Vassal ici",
"Load a Vassal module": "Charger un module Vassal",
- "Load a Vassal module?": "Charger un module Vassal ?",
+ "Load a Vassal module?": "Charger un module Vassal ? (Expérimental)",
"Load Vassal module": "Charger le module Vassal",
"Import Vassal module": "Importer le module Vassal",
"Secondary color": "Couleur secondaire",
@@ -351,5 +351,9 @@
"Don't forget to save your game using the save button located in the middle of the left sidebar.\n\n": "N'oubliez pas de sauvegarder votre jeu en utilisant le bouton de sauvegarde situé au milieu de la barre latérale gauche.\n\n",
"Have fun exploring the possibilities and enjoy the process of game creation!": "Amusez-vous à explorer les possibilités !",
"Login failed. Please try again.": "Échec de l'authentification, veuillez réessayer...",
- "Home": "Accueil"
+ "Home": "Accueil",
+ "Die": "Dé",
+ "Image die": "Dé image",
+ "Include files": "Inclure les fichiers",
+ "An error occurred. Try again!": "Une erreur est survenue. Veuillez réessayer !"
}
diff --git a/src/utils/image.js b/src/utils/image.js
index d955695d..0dc38156 100644
--- a/src/utils/image.js
+++ b/src/utils/image.js
@@ -1,7 +1,9 @@
+import pLimit from "p-limit";
+import { itemTemplates } from "../gameComponents";
+
const imageCache = {};
export const getImageWithRetry = async (url, retry = 0) => {
- console.log("called with", url, retry);
if (!url) {
return null;
}
@@ -10,15 +12,12 @@ export const getImageWithRetry = async (url, retry = 0) => {
img.onload = () => {
resolve(img);
};
- img.onerror = (e) => {
- console.log("onError", url, e);
+ img.onerror = () => {
if (retry < 3) {
- console.log("call retry", retry);
getImageWithRetry(url, retry + 1)
.then(resolve)
.catch(reject);
} else {
- console.log("reject");
reject(new Error(`Failed to load: <${url}>`));
}
};
@@ -35,3 +34,67 @@ export const getImage = async (url) => {
}
return imageCache[url];
};
+
+export class ItemMediaUploader {
+ constructor(onFile) {
+ this.onFile = onFile;
+ this.queue = pLimit(4);
+ this.cache = {};
+ }
+
+ async upload(item) {
+ const template = itemTemplates[item.type];
+ const itemCloned = JSON.parse(JSON.stringify(item));
+
+ await template.mapMedia(itemCloned, async (media) => {
+ if (typeof media === "object" && media?.file) {
+ if (!this.cache[media.file]) {
+ this.cache[media.file] = this.queue(this.onFile, media.file);
+ }
+ return { type: "local", content: await this.cache[media.file] };
+ } else {
+ return media;
+ }
+ });
+
+ return itemCloned;
+ }
+}
+
+export const imageAsBlob = async (image) => {
+ const response = await fetch(image.src);
+ return await response.blob();
+};
+
+export const URLAsBlob = async (url) => {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch file: ${response.statusText}`);
+ }
+ return await response.blob();
+};
+
+export async function hashBlob(blob) {
+ // Read the blob as an ArrayBuffer
+ const blobBuffer = await readBlobAsArrayBuffer(blob);
+
+ // Use the SubtleCrypto API to hash the content
+ const hashBuffer = await crypto.subtle.digest("SHA-256", blobBuffer);
+
+ // Convert the hash to a hex string
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+
+ return hashHex;
+}
+
+function readBlobAsArrayBuffer(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(new Error("Failed to read blob"));
+ reader.readAsArrayBuffer(blob);
+ });
+}
diff --git a/src/utils/index.js b/src/utils/index.js
index 21382cb8..51056afe 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -119,3 +119,14 @@ export const playAudio = (url, volume = 1) => {
console.log("Fail to play audio", e);
}
};
+
+export const triggerFileDownload = (url, filename) => {
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", filename);
+ document.body.appendChild(link);
+ link.click();
+
+ // Clean up and remove the link
+ link.parentNode.removeChild(link);
+};
diff --git a/src/utils/item.js b/src/utils/item.js
index 3d072ea9..e3964cac 100644
--- a/src/utils/item.js
+++ b/src/utils/item.js
@@ -58,3 +58,19 @@ export const getItemElement = (id) => {
}
return elem;
};
+
+export const availableItemVisitor = async (items, callback) => {
+ return await Promise.all(
+ items.map(async (node) => {
+ if (node.items) {
+ return {
+ ...node,
+ items: await availableItemVisitor(node.items, callback),
+ };
+ } else {
+ // It's an element
+ return await callback(node);
+ }
+ })
+ );
+};
diff --git a/src/utils/vassal.js b/src/utils/vassal.js
index 2d0220f0..160898f9 100644
--- a/src/utils/vassal.js
+++ b/src/utils/vassal.js
@@ -1,4 +1,3 @@
-// import { ZipReader, BlobReader, TextWriter, BlobWriter } from "@zip.js/zip.js";
import X2JS from "x2js";
import pLimit from "p-limit";
import JSZip from "jszip";
diff --git a/src/views/BoardView/DownloadLink.jsx b/src/views/BoardView/DownloadLink.jsx
index 59c45c94..af2333ac 100644
--- a/src/views/BoardView/DownloadLink.jsx
+++ b/src/views/BoardView/DownloadLink.jsx
@@ -1,64 +1,118 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { FiDownload } from "react-icons/fi";
+import pLimit from "p-limit";
+import mime from "mime-types";
-const generateDownloadURI = (data) => {
- return (
- "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data))
- );
-};
+import { itemTemplates } from "../../gameComponents";
+import { media2Url } from "../../mediaLibrary";
+import { uid } from "../../utils";
+import { availableItemVisitor } from "../../utils/item";
+import { triggerFileDownload } from "../../utils";
+import { URLAsBlob, hashBlob } from "../../utils/image";
-export const DownloadLink = ({ getData = () => {} }) => {
- const { t } = useTranslation();
+class ZipBuilder {
+ async putFileInZip(url) {
+ if (!this.cache[url]) {
+ this.cache[url] = (async () => {
+ const blob = await URLAsBlob(url);
+ const hash = await hashBlob(blob);
- const [downloadURI, setDownloadURI] = React.useState("");
- const [date, setDate] = React.useState(Date.now());
- const [genOnce, setGenOnce] = React.useState(false);
+ if (this.hashCache[hash]) {
+ return this.hashCache[hash];
+ } else {
+ const extension = mime.extension(blob.type);
+ const filename = `${uid()}.${extension}`;
+ this.zip.file(filename, blob);
+ this.hashCache[hash] = filename;
+ this.incFileCount();
+ return filename;
+ }
+ })();
+ }
+ return this.cache[url];
+ }
- const updateSaveLink = React.useCallback(async () => {
- const data = await getData();
- if (data.items.length) {
- setDownloadURI(generateDownloadURI(data));
- setDate(Date.now());
- setGenOnce(true);
+ async addImageToZipFromItem(item) {
+ const template = itemTemplates[item.type];
+ const itemCloned = JSON.parse(JSON.stringify(item));
+
+ await template.mapMedia(itemCloned, async (media) => {
+ const url = media2Url(media);
+ if (url) {
+ try {
+ const filename = await this.queue(this.putFileInZip.bind(this), url);
+ return { file: filename };
+ } catch (e) {
+ console.warn("Failed to export file", url);
+ return null;
+ }
+ }
+ return media;
+ });
+
+ return itemCloned;
+ }
+
+ async build(data, incFileCount = () => {}, withFile = false) {
+ this.cache = {};
+ this.hashCache = {};
+ const JSZip = (await import("jszip")).default;
+ this.zip = new JSZip();
+ this.queue = pLimit(4);
+ this.incFileCount = incFileCount;
+
+ if (withFile) {
+ data.items = await Promise.all(
+ data.items.map(this.addImageToZipFromItem.bind(this))
+ );
+ data.availableItems = await availableItemVisitor(
+ data.availableItems,
+ this.addImageToZipFromItem.bind(this)
+ );
}
- }, [getData]);
- React.useEffect(() => {
- let mounted = true;
+ this.zip.file("content.json", JSON.stringify(data));
+ const base64 = await this.zip.generateAsync({ type: "base64" });
+ const url = "data:application/zip;base64," + base64;
+
+ return url;
+ }
+}
+
+export const DownloadLink = ({ getData = () => {}, withFile = false }) => {
+ const { t } = useTranslation();
- const cancel = setInterval(() => {
- if (!mounted) return;
- updateSaveLink();
- }, 2000);
+ const [generating, setGenerating] = React.useState(false);
+ const [fileCount, setFileCount] = React.useState(0);
- updateSaveLink();
+ const triggerDownload = React.useCallback(async () => {
+ const data = await getData();
- return () => {
- mounted = false;
- setGenOnce(false);
- clearInterval(cancel);
- };
- }, [updateSaveLink]);
+ if (data.items.length) {
+ setGenerating(true);
+ setFileCount(0);
+ try {
+ const zipBuilder = new ZipBuilder();
+ const url = await zipBuilder.build(
+ JSON.parse(JSON.stringify(data)),
+ () => {
+ setFileCount((prev) => prev + 1);
+ },
+ withFile
+ );
+ triggerFileDownload(url, `airboardgame_${Date.now()}.zip`);
+ } finally {
+ setGenerating(false);
+ }
+ }
+ }, [getData, withFile]);
return (
- <>
- {genOnce && (
-
- {t("Export")}
-
-
- )}
- {!genOnce && (
-
- )}
- >
+
);
};
diff --git a/src/views/BoardView/ExportModal.jsx b/src/views/BoardView/ExportModal.jsx
index 6535477a..ba022d23 100644
--- a/src/views/BoardView/ExportModal.jsx
+++ b/src/views/BoardView/ExportModal.jsx
@@ -9,6 +9,7 @@ import Modal from "../../ui/Modal";
const ExportModal = ({ show, setShow }) => {
const { t } = useTranslation();
const { getSession } = useSession();
+ const [withFile, setWithFile] = React.useState(false);
return (
@@ -21,7 +22,17 @@ const ExportModal = ({ show, setShow }) => {
"You can save the current session on your computer to load it later!"
)}
-
+
+
+
+
);
diff --git a/src/views/BoardView/ItemLibrary.jsx b/src/views/BoardView/ItemLibrary.jsx
index 6cd8c033..0534fd57 100644
--- a/src/views/BoardView/ItemLibrary.jsx
+++ b/src/views/BoardView/ItemLibrary.jsx
@@ -168,6 +168,23 @@ const filterItems = (filter, nodes) =>
return acc;
}, []);
+/**
+ * In the Item library we have a hierarchical structure where nodes are
+ *
+ * {
+ * name, // Name of the group
+ * items: [...] // Items/subgroup in this group
+ * }
+ *
+ * and items (leaf) are
+ *
+ * {
+ * type, // It's an element
+ * ... // Other element properties
+ * }
+ *
+ */
+
const ItemLibrary = ({ items }) => {
const { t } = useTranslation();
const [filter, setFilter] = React.useState("");
diff --git a/src/views/BoardView/LoadData.jsx b/src/views/BoardView/LoadData.jsx
index f50e752e..dfc80cf8 100644
--- a/src/views/BoardView/LoadData.jsx
+++ b/src/views/BoardView/LoadData.jsx
@@ -3,25 +3,76 @@ import { useTranslation } from "react-i18next";
import { useDropzone } from "react-dropzone";
+const loadJSONFile = (file) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onabort = () => reject(new Error("file reading was aborted"));
+ reader.onerror = () => reject(new Error("file reading has failed"));
+ reader.onload = () => {
+ try {
+ const result = JSON.parse(reader.result);
+ resolve({ game: result });
+ } catch (e) {
+ reject(e);
+ }
+ };
+ reader.readAsText(file);
+ });
+};
+
+const loadZIPFile = (file) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onabort = () => reject(new Error("file reading was aborted"));
+ reader.onerror = () => reject(new Error("file reading has failed"));
+ reader.onload = async () => {
+ try {
+ const JSZip = (await import("jszip")).default;
+ const zip = new JSZip();
+ await zip.loadAsync(reader.result);
+ const data = JSON.parse(await zip.file("content.json").async("string"));
+
+ const fileList = [];
+
+ zip.forEach((relativePath, zipEntry) =>
+ fileList.push(
+ (async () => [
+ relativePath,
+ new File([await zipEntry.async("blob")], relativePath),
+ ])()
+ )
+ );
+
+ const files = Object.fromEntries(await Promise.all(fileList));
+
+ resolve({ game: data, files });
+ } catch (e) {
+ reject(e);
+ }
+ };
+ reader.readAsBinaryString(file);
+ });
+};
+
const LoadGame = ({ onLoad = () => {} }) => {
const { t } = useTranslation();
const onDrop = React.useCallback(
(acceptedFiles) => {
- acceptedFiles.forEach((file) => {
- const reader = new FileReader();
-
- reader.onabort = () => console.log("file reading was aborted");
- reader.onerror = () => console.log("file reading has failed");
- reader.onload = () => {
+ acceptedFiles.forEach(async (file) => {
+ try {
+ const result = await loadJSONFile(file);
+ onLoad(result);
+ } catch (e) {
try {
- const result = JSON.parse(reader.result);
+ const result = await loadZIPFile(file);
onLoad(result);
} catch (e) {
console.log("File parsing failed", e);
}
- };
- reader.readAsText(file);
+ }
});
},
[onLoad]
diff --git a/src/views/BoardView/LoadGameModal.jsx b/src/views/BoardView/LoadGameModal.jsx
index 207c06aa..fc924e0f 100644
--- a/src/views/BoardView/LoadGameModal.jsx
+++ b/src/views/BoardView/LoadGameModal.jsx
@@ -5,27 +5,71 @@ import useGame from "../../hooks/useGame";
import Modal from "../../ui/Modal";
+import { uploadResourceImage as uploadMedia } from "../../utils/api";
+import { availableItemVisitor } from "../../utils/item";
+import { ItemMediaUploader } from "../../utils/image";
+
import LoadData from "./LoadData";
const LoadGameModal = ({ show, setShow }) => {
const { t } = useTranslation();
- const { setGame } = useGame();
+ const { setGame, gameId } = useGame();
+ const [loading, setLoading] = React.useState();
+ const [error, setError] = React.useState(false);
+ const [fileCount, setFileCount] = React.useState(0);
const loadGame = React.useCallback(
- (game) => {
- setGame(game);
- setShow(false);
+ async ({ game, files }) => {
+ setLoading(true);
+
+ setFileCount(0);
+
+ const onFile = (file) => {
+ setFileCount((prev) => prev + 1);
+ return uploadMedia("game", gameId, files[file]);
+ };
+
+ const itemMediaUploader = new ItemMediaUploader(onFile);
+
+ try {
+ game.items = await Promise.all(
+ game.items.map((item) => itemMediaUploader.upload(item))
+ );
+ game.availableItems = await availableItemVisitor(
+ game.availableItems,
+ (item) => itemMediaUploader.upload(item)
+ );
+
+ setGame(game);
+ setShow(false);
+ } catch (e) {
+ console.log(e);
+ setError(true);
+ } finally {
+ setLoading(false);
+ }
},
- [setGame, setShow]
+ [gameId, setGame, setShow]
);
+ React.useEffect(() => {
+ if (show) {
+ setError(false);
+ }
+ }, [show]);
+
return (
{t("Load previously exported work?")}
-
+ {loading ? (
+ {`#${fileCount} - ${t("Loading...")}`}
+ ) : (
+
+ )}
+ {error && {t("An error occurred. Try again!")}
}
);
diff --git a/src/views/BoardView/LoadSessionModal.jsx b/src/views/BoardView/LoadSessionModal.jsx
index 819ef4e7..56a7fe09 100644
--- a/src/views/BoardView/LoadSessionModal.jsx
+++ b/src/views/BoardView/LoadSessionModal.jsx
@@ -2,6 +2,9 @@ import React from "react";
import { useTranslation } from "react-i18next";
import useSession from "../../hooks/useSession";
+import { uploadResourceImage as uploadMedia } from "../../utils/api";
+import { availableItemVisitor } from "../../utils/item";
+import { ItemMediaUploader } from "../../utils/image";
import Modal from "../../ui/Modal";
@@ -9,23 +12,63 @@ import LoadData from "./LoadData";
const LoadSessionModal = ({ show, setShow }) => {
const { t } = useTranslation();
- const { setSession } = useSession();
+ const { setSession, sessionId } = useSession();
+ const [loading, setLoading] = React.useState();
+ const [error, setError] = React.useState(false);
+ const [fileCount, setFileCount] = React.useState(0);
const loadSession = React.useCallback(
- (sessionData) => {
- setSession(sessionData);
- setShow(false);
+ async ({ game, files }) => {
+ setError(false);
+ setLoading(true);
+ setFileCount(0);
+
+ const onFile = (file) => {
+ setFileCount((prev) => prev + 1);
+ return uploadMedia("session", sessionId, files[file]);
+ };
+
+ const itemMediaUploader = new ItemMediaUploader(onFile);
+
+ try {
+ game.items = await Promise.all(
+ game.items.map((item) => itemMediaUploader.upload(item))
+ );
+ game.availableItems = await availableItemVisitor(
+ game.availableItems,
+ (item) => itemMediaUploader.upload(item)
+ );
+
+ setSession(game);
+ setShow(false);
+ } catch (e) {
+ console.log(e);
+ setError(true);
+ } finally {
+ setLoading(false);
+ }
},
- [setSession, setShow]
+ [sessionId, setSession, setShow]
);
+ React.useEffect(() => {
+ if (show) {
+ setError(false);
+ }
+ }, [show]);
+
return (
{t("Continue a saved game session?")}
-
+ {loading ? (
+ {`#${fileCount} - ${t("Loading...")}`}
+ ) : (
+
+ )}
+ {error && {t("An error occurred. Try again!")}
}
);
diff --git a/src/views/BoardView/SaveExportModal.jsx b/src/views/BoardView/SaveExportModal.jsx
index dcfc1304..0169968c 100644
--- a/src/views/BoardView/SaveExportModal.jsx
+++ b/src/views/BoardView/SaveExportModal.jsx
@@ -12,6 +12,8 @@ const SaveExportGameModal = ({ show, setShow }) => {
const { saveGame, getGame } = useGame();
+ const [withFile, setWithFile] = React.useState(false);
+
const handleSave = React.useCallback(async () => {
try {
await saveGame();
@@ -64,7 +66,17 @@ const SaveExportGameModal = ({ show, setShow }) => {
);