Skip to content

Commit

Permalink
Add file to upload
Browse files Browse the repository at this point in the history
  • Loading branch information
jrmi committed Aug 6, 2023
1 parent 0c4e8f5 commit c6fb6d7
Show file tree
Hide file tree
Showing 20 changed files with 315 additions and 92 deletions.
1 change: 1 addition & 0 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
Expand Down
7 changes: 2 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions src/gameComponents/Canvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Error />;
}
Expand Down
10 changes: 10 additions & 0 deletions src/gameComponents/Image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
12 changes: 4 additions & 8 deletions src/gameComponents/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,15 +25,11 @@ const defaultTemplate = {
applyDefault(item) {
return item;
},
};
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;
7 changes: 7 additions & 0 deletions src/games/testGame.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
6 changes: 5 additions & 1 deletion src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
"Image die": "Dé image",
"Include files": "Inclure les fichiers",
"An error occurred. Try again!": "Une erreur est survenue. Veuillez réessayer !"
}
24 changes: 19 additions & 5 deletions src/utils/image.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { itemTemplates } from "../gameComponents";

const imageCache = {};

export const getImageWithRetry = async (url, retry = 0) => {
console.log("called with", url, retry);
if (!url) {
return null;
}
Expand All @@ -10,15 +11,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}>`));
}
};
Expand All @@ -35,3 +33,19 @@ export const getImage = async (url) => {
}
return imageCache[url];
};

export const uploadItemMedia = (onFile) => async (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) {
const imageURL = await onFile(media.file);
return { type: "local", content: imageURL };
} else {
return media;
}
});

return itemCloned;
};
11 changes: 11 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
16 changes: 16 additions & 0 deletions src/utils/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
})
);
};
1 change: 0 additions & 1 deletion src/utils/vassal.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
119 changes: 73 additions & 46 deletions src/views/BoardView/DownloadLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,90 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { FiDownload } from "react-icons/fi";

const generateDownloadURI = (data) => {
return (
"data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data))
);
import { itemTemplates } from "../../gameComponents";
import { media2Url } from "../../mediaLibrary";
import { getImage } from "../../utils/image";
import { uid } from "../../utils";
import mime from "mime-types";
import { availableItemVisitor } from "../../utils/item";
import { triggerFileDownload } from "../../utils";

const imageAsBlob = async (image) => {
const response = await fetch(image.src);
return await response.blob();
};

export const DownloadLink = ({ getData = () => {} }) => {
const { t } = useTranslation();
const addImageToZipFromItem = (zip) => async (item) => {
const template = itemTemplates[item.type];
const itemCloned = JSON.parse(JSON.stringify(item));

const [downloadURI, setDownloadURI] = React.useState("");
const [date, setDate] = React.useState(Date.now());
const [genOnce, setGenOnce] = React.useState(false);
await template.mapMedia(itemCloned, async (media) => {
const url = media2Url(media);
if (url) {
const image = await getImage(url);
const blob = await imageAsBlob(image);
const extension = mime.extension(blob.type);
const filename = `${uid()}.${extension}`;

const updateSaveLink = React.useCallback(async () => {
const data = await getData();
if (data.items.length) {
setDownloadURI(generateDownloadURI(data));
setDate(Date.now());
setGenOnce(true);
zip.file(filename, blob);

return { file: filename };
}
}, [getData]);
return media;
});

return itemCloned;
};

React.useEffect(() => {
let mounted = true;
const buildZipFile = async (data, withFile = false) => {
const JSZip = (await import("jszip")).default;
const zip = new JSZip();
const addImage = addImageToZipFromItem(zip);

const cancel = setInterval(() => {
if (!mounted) return;
updateSaveLink();
}, 2000);
if (withFile) {
data.items = await Promise.all(
data.items.map(async (item) => await addImage(item))
);
data.availableItems = await availableItemVisitor(
data.availableItems,
addImage
);
}

updateSaveLink();
zip.file("content.json", JSON.stringify(data));
const base64 = await zip.generateAsync({ type: "base64" });
const url = "data:application/zip;base64," + base64;

return () => {
mounted = false;
setGenOnce(false);
clearInterval(cancel);
};
}, [updateSaveLink]);
return url;
};

export const DownloadLink = ({ getData = () => {}, withFile = false }) => {
const { t } = useTranslation();

const [generating, setGenerating] = React.useState(false);

const triggerDownload = React.useCallback(async () => {
const data = await getData();

if (data.items.length) {
setGenerating(true);
try {
const url = await buildZipFile(
JSON.parse(JSON.stringify(data)),
withFile
);
triggerFileDownload(url, `airboardgame_${Date.now()}.zip`);
} finally {
setGenerating(false);
}
}
}, [getData, withFile]);

return (
<>
{genOnce && (
<a
className="button success icon"
href={downloadURI}
download={`airboardgame_${date}.json`}
>
{t("Export")}
<FiDownload size="20" color="#f9fbfa" alt="Download" />
</a>
)}
{!genOnce && (
<button className="button" disabled>
{t("Generating export")}...
</button>
)}
</>
<button className="button success icon" onClick={triggerDownload}>
{!generating ? t("Export") : t("Generating export")}
<FiDownload size="20" color="#f9fbfa" alt="Download" />
</button>
);
};

Expand Down
13 changes: 12 additions & 1 deletion src/views/BoardView/ExportModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Modal title={t("Save game")} setShow={setShow} show={show}>
Expand All @@ -21,7 +22,17 @@ const ExportModal = ({ show, setShow }) => {
"You can save the current session on your computer to load it later!"
)}
</p>
<DownloadLink getData={getSession} />
<DownloadLink getData={getSession} withFile={withFile} />
<div style={{ paddingTop: "1em" }}>
<label>
<input
type="checkbox"
checked={withFile}
onChange={() => setWithFile((prev) => !prev)}
/>
{t("Include files")}
</label>
</div>
</section>
</Modal>
);
Expand Down
Loading

0 comments on commit c6fb6d7

Please sign in to comment.