mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
Feature/upload modal fixes (#276)
Co-authored-by: Victor <victor@storyteller.ai>
This commit is contained in:
1 parent
5380b367f9
commit
fe55fd0e0f
5 files changed
+302
-103
No files matched your search
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user