From fe55fd0e0faae3287dce7b4295ee5c11979cb0ae Mon Sep 17 00:00:00 2001 From: Victor O Date: Tue, 30 Apr 2024 11:47:22 -0700 Subject: [PATCH] Feature/upload modal fixes (#276) Co-authored-by: Victor --- app/api/media_files/UploadEngineAsset.ts | 12 +- app/components/FileWrapper/FileWrapper.tsx | 22 +- app/components/UploadModal/UploadModal.scss | 12 + app/components/UploadModal/UploadModal.tsx | 357 +++++++++++++----- .../SidePanelTabs/itemTabs/ObjectsTab.tsx | 2 +- 5 files changed, 302 insertions(+), 103 deletions(-) diff --git a/app/api/media_files/UploadEngineAsset.ts b/app/api/media_files/UploadEngineAsset.ts index e2e850155a..05d4c40ccf 100644 --- a/app/api/media_files/UploadEngineAsset.ts +++ b/app/api/media_files/UploadEngineAsset.ts @@ -1,4 +1,3 @@ -import { MediaFileSubtype } from "../enums/MediaFileSubtype"; import MakeMultipartRequest from "../MakeMultipartRequest"; export enum MediaFileSubtype { @@ -28,16 +27,17 @@ export enum MediaFileSubtype { } export interface UploadEngineAssetRequest { - uuid_idempotency_token?: string, - file: any, + file: File; media_file_subtype?: MediaFileSubtype; + title?: string; + uuid_idempotency_token?: string; } export interface UploadEngineAssetResponse { - media_file_token: string, - success: boolean + media_file_token: string; + success: boolean; } export const UploadEngineAsset = (request: UploadEngineAssetRequest) => { return MakeMultipartRequest("/v1/media_files/upload/engine_asset", request); -} +}; diff --git a/app/components/FileWrapper/FileWrapper.tsx b/app/components/FileWrapper/FileWrapper.tsx index a7c6bcc7b2..3328dc29e5 100644 --- a/app/components/FileWrapper/FileWrapper.tsx +++ b/app/components/FileWrapper/FileWrapper.tsx @@ -1,4 +1,4 @@ -import React, { useId, useState } from "react"; +import React, { useId, useRef, useState } from "react"; import { UploadModal } from "~/components"; // import { useId } from 'hooks'; // replace with react v18 import "./FileWrapper.scss"; @@ -17,19 +17,26 @@ export default function FileWrapper({ onSuccess, ...rest }: Props) { - const [file, fileSet] = useState(); + const fileRef = useRef(null); + const [file, fileSet] = useState(); const [modalOpen, modalOpenSet] = useState(false); + const [resetModal, resetModalSet] = useState(false); const [dragging, draggingSet] = useState(false); const id = "file-input-" + useId(); const accept = fileTypes.map((type) => `.${type.toLowerCase()}`).join(","); - const clearFile = () => fileSet(undefined); + const clearFile = () => { + if (fileRef.current !== null) { + fileRef.current.value = ""; + fileSet(undefined); + } + }; const closeModal = () => modalOpenSet(false); - const fileChange = (e: React.DragEvent) => { + const fileChange = (e: React.ChangeEvent) => { + resetModalSet(true); e.preventDefault(); fileSet(e.target.files[0]); modalOpenSet(true); - // onChange({ target: { name: e.target?.name || "file-input", value: e.target.files[0] }}); }; const onDragDrop = (e: React.DragEvent) => { e.preventDefault(); @@ -66,6 +73,7 @@ export default function FileWrapper({ onChange: fileChange, type: "file", id, + ref: fileRef, ...rest, }} /> @@ -73,10 +81,12 @@ export default function FileWrapper({ diff --git a/app/components/UploadModal/UploadModal.scss b/app/components/UploadModal/UploadModal.scss index 847a0a4c5b..e6e3471016 100644 --- a/app/components/UploadModal/UploadModal.scss +++ b/app/components/UploadModal/UploadModal.scss @@ -1,3 +1,15 @@ +.obj-uploader-modal-load-view { + .uploader-dots { + background: transparent; + margin-bottom: 1rem; + } + .uploader-message { + width: 100%; + opacity: 0.5; + text-align: center; + } +} + .object-preview-canvas { width: 100%; overflow: hidden; diff --git a/app/components/UploadModal/UploadModal.tsx b/app/components/UploadModal/UploadModal.tsx index cfb14850db..ec649f3f08 100644 --- a/app/components/UploadModal/UploadModal.tsx +++ b/app/components/UploadModal/UploadModal.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useId, useState } from "react"; -import { Button, Input, TransitionDialogue } from "~/components"; +import { Button, Input, TransitionDialogue, LoadingDots } from "~/components"; import { UploadEngineAsset, UploadEngineAssetResponse, @@ -14,6 +14,7 @@ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import "./UploadModal.scss"; import { fitCameraToCenteredObject } from "./fitCameraToCenteredObject"; import { EditCoverImage } from "~/api/media_files/EditCoverImage"; +import { Visibility } from "~/pages/PageEnigma/js/api_manager"; interface Props { closeModal: () => void; @@ -21,6 +22,20 @@ interface Props { onClose: () => void; onSuccess: () => void; isOpen: boolean; + resetModal: boolean; + resetModalSet: (b: boolean) => void; +} + +enum UploaderState { + ready, + uploadingAsset, + uploadingCover, + settingCover, + success, + assetError, + coverCreateError, + coverSetError, + loaderError, } export default function UploadModal({ @@ -29,7 +44,14 @@ export default function UploadModal({ isOpen, onClose, onSuccess, + resetModal, + resetModalSet, }: Props) { + const [title, titleSet] = useState(""); + const [visibility, visibilitySet] = useState("public"); + const [objUploadStatus, objUploadStatusSet] = useState(UploaderState.ready); + const [assetToken, assetTokenSet] = useState(""); + const [coverToken, coverTokenSet] = useState(""); const [targetNode, targetNodeSet] = useState(null); const objPreviewRef = useCallback((node) => { if (node !== null) { @@ -37,15 +59,24 @@ export default function UploadModal({ } }, []); + const resetModalState = useCallback(() => { + resetModalSet(false); + titleSet(""); + visibilitySet(""); + objUploadStatusSet(UploaderState.ready); + assetTokenSet(""); + coverTokenSet(""); + }, [resetModalSet]); + useEffect(() => { - if (!!targetNode) { + // modal reset needed because we don't unmount components for some reason + if (resetModal) { + resetModalState(); + } + + if (targetNode !== null) { const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 0.1, - 1000, - ); + const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ alpha: true, @@ -64,60 +95,242 @@ export default function UploadModal({ scene.add(light); - loader.load(URL.createObjectURL(file), (glb) => { - glb.scene.children.forEach((child) => { - child.userData["color"] = "#FFFFFF"; - scene.add(child); + if (file) { + loader.load( + URL.createObjectURL(file), + (glb) => { + glb.scene.children.forEach((child) => { + child.userData["color"] = "#FFFFFF"; + scene.add(child); - if (scene.children[1]) { - fitCameraToCenteredObject(camera, scene.children[1]); + if (scene.children[1]) { + fitCameraToCenteredObject(camera, scene.children[1]); - renderer.render(scene, camera); - } - }); - }); + renderer.render(scene, camera); + } + }); + }, + () => {}, + (loaderError) => { + console.log("loader error:", loaderError); + objUploadStatusSet(UploaderState.loaderError); + }, + ); + } } - }, [file, targetNode]); + }, [file, targetNode, resetModal, resetModalState]); - const [name, nameSet] = useState(""); - const visiblityOpts = [ - { - label: "public", - value: "public", - }, - { - label: "private", - value: "private", - }, - ]; - const visiblityId = `fy-uploader-modal-visibility-${useId()}`; + // for visibility later - const submitUpload = () => { + // const visiblityOpts = [ + // { + // label: "public", + // value: "public", + // }, + // { + // label: "private", + // value: "private", + // }, + // ]; + // const visiblityId = `fy-uploader-modal-visibility-${useId()}`; + + const setCoverImage = (asset: string, cover: string) => { + objUploadStatusSet(UploaderState.settingCover); + EditCoverImage(asset, { + cover_image_media_file_token: cover, + }) + .then(() => { + objUploadStatusSet(UploaderState.success); + onSuccess(); + }) + .catch(() => { + objUploadStatusSet(UploaderState.coverSetError); + }); + }; + + const createCoverImage = (asset: string) => { + targetNode.toBlob((blob) => { + // TODO failure modal + const newCap = new File([blob], "storyteller-cap.png"); + objUploadStatusSet(UploaderState.uploadingCover); + UploadMedia({ + uuid_idempotency_token: uuidv4(), + file: newCap, + source: "file", + title: `Cover image${title ? " for " + title : ""}`, + }) + .then((captureRes: UploadMediaResponse) => { + coverTokenSet(captureRes.media_file_token); + setCoverImage(asset, captureRes.media_file_token); + }) + .catch(() => { + objUploadStatusSet(UploaderState.coverCreateError); + }); + }); + }; + + const uploadAsset = () => { + objUploadStatusSet(UploaderState.uploadingAsset); UploadEngineAsset({ - uuid_idempotency_token: uuidv4(), file, media_file_subtype: "mixamo", - }).then((assetRes: UploadEngineAssetResponse) => { - if ("media_file_token" in assetRes) { - targetNode.toBlob((blob) => { - // TODO failure modal - const newCap = new File([blob], "storyteller-cap.png"); + title, + visibility, + uuid_idempotency_token: uuidv4(), + }) + .then((assetRes: UploadEngineAssetResponse) => { + if ("media_file_token" in assetRes) { + assetTokenSet(assetRes.media_file_token); + createCoverImage(assetRes.media_file_token); + } + }) + .catch(() => { + objUploadStatusSet(UploaderState.assetError); + }); + }; - UploadMedia({ - uuid_idempotency_token: uuidv4(), - file: newCap, - source: "file", - }).then((captureRes: UploadMediaResponse) => { - EditCoverImage(assetRes.media_file_token, { - cover_image_media_file_token: captureRes.media_file_token, - }).then(() => { - closeModal(); - onSuccess(); - }); - }); - }); - } - }); + const objUploaderContent = () => { + switch (objUploadStatus) { + case UploaderState.ready: + return ( + <> + + ) => + titleSet(target.value), + placeholder: "Enter a name", + value: title, + }} + /> +
+ + +
+ + ); + case UploaderState.uploadingAsset: + case UploaderState.uploadingCover: + case UploaderState.settingCover: + return ( +
+ +
Uploading...
+
+ ); + case UploaderState.success: + return ( +
+
{`Added ${title} to objects`}
+
+ +
+
+ ); + case UploaderState.assetError: + case UploaderState.coverCreateError: + case UploaderState.coverSetError: + return ( + <> + {objUploadStatus === UploaderState.assetError + ? "There was a problem uploading your object." + : "There was a problem creating art for your object"} +
+ + +
+ + ); + case UploaderState.loaderError: + return ( + <> + There was a problem loading your object. Try another. +
+ + +
+ + ); + } }; return ( @@ -127,43 +340,7 @@ export default function UploadModal({ onClose, title: "Upload", }}> - - nameSet(target.value), - value: name, - }} - /> - - -
- - -
+ {objUploaderContent()} ); } diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ObjectsTab.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ObjectsTab.tsx index 337c8e4f2d..232bfca1f5 100644 --- a/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ObjectsTab.tsx +++ b/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ObjectsTab.tsx @@ -63,7 +63,7 @@ export const ObjectsTab = ({ type }: Props) => { if (status === FetchStatus.ready && type !== AssetType.CHARACTER) { statusSet(FetchStatus.in_progress); GetMediaByUser( - authState.userInfo.username, + authState?.userInfo?.username || "", {}, { filter_media_type: "glb",