Feature/upload modal fixes (#276)

Co-authored-by: Victor <victor@storyteller.ai>
This commit is contained in:
Victor OandVictor authored and GitHub committed 2024-04-30 14:47:22 -04:00
1 parent 5380b367f9
commit fe55fd0e0f
5 files changed
+302 -103

No files matched your search

+6 -6
View File
@@ -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);
}
};
+16 -6
View File
@@ -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<File | undefined>();
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<HTMLInputElement>) => {
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<HTMLDivElement>) => {
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({
<UploadModal
{...{
closeModal,
onClose: clearFile,
file,
isOpen: modalOpen,
onClose: clearFile,
onSuccess,
resetModal,
resetModalSet,
}}
/>
</div>
@@ -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;
+267 -90
View File
@@ -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 (
<>
<canvas
{...{
className: "object-preview-canvas",
ref: objPreviewRef,
}}></canvas>
<Input
{...{
label: "Name",
onChange: ({ target }: React.ChangeEvent<HTMLInputElement>) =>
titleSet(target.value),
placeholder: "Enter a name",
value: title,
}}
/>
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "secondary",
}}>
Cancel
</Button>
<Button
{...{
onClick: () => uploadAsset(targetNode),
}}>
Upload
</Button>
</div>
</>
);
case UploaderState.uploadingAsset:
case UploaderState.uploadingCover:
case UploaderState.settingCover:
return (
<div {...{ className: "obj-uploader-modal-load-view" }}>
<LoadingDots {...{ className: "uploader-dots" }} />
<div {...{ className: "uploader-message" }}>Uploading...</div>
</div>
);
case UploaderState.success:
return (
<div {...{ className: "obj-uploader-modal-success-view" }}>
<div
{...{
className: "uploader-message",
}}>{`Added ${title} to objects`}</div>
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "primary",
}}>
Ok
</Button>
</div>
</div>
);
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"}
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "secondary",
}}>
Cancel
</Button>
<Button
{...{
onClick: () => {
switch (objUploadStatus) {
case UploaderState.assetError: {
uploadAsset();
break;
}
case UploaderState.coverCreateError: {
createCoverImage(assetToken);
break;
}
case UploaderState.coverSetError: {
setCoverImage(assetToken, coverToken);
break;
}
}
},
variant: "primary",
}}>
Try again
</Button>
</div>
</>
);
case UploaderState.loaderError:
return (
<>
There was a problem loading your object. Try another.
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "secondary",
}}>
Cancel
</Button>
<Button
{...{
onClick: () => {
objUploadStatusSet(UploaderState.ready);
resetModalState();
},
variant: "primary",
}}>
Cancel
</Button>
</div>
</>
);
}
};
return (
@@ -127,43 +340,7 @@ export default function UploadModal({
onClose,
title: "Upload",
}}>
<canvas
{...{
className: "object-preview-canvas",
ref: objPreviewRef,
}}></canvas>
<Input
{...{
label: "Name",
onChange: ({ target }: React.ChangeEvent) => nameSet(target.value),
value: name,
}}
/>
<label {...{ htmlFor: visiblityId }}>Visibility</label>
<select {...{ id: visiblityId }}>
{visiblityOpts.map(({ label, value }, key) => (
<option key={key} {...{ value }}>
{label}
</option>
))}
</select>
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
},
variant: "secondary",
}}>
Cancel
</Button>
<Button
{...{
onClick: submitUpload,
}}>
Upload
</Button>
</div>
{objUploaderContent()}
</TransitionDialogue>
);
}
@@ -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",