Feature/ Sets Tab; Fixes/ Tabs Improvements (#578)

Tabs Improvements:
- refactor tabs so no more race condition revolving unneccessary signals
- clean up signals
- refactor upload modal movement to be non-signaled and generalized for
more files types
- better naming for files and components, better error handling, eslint
type any fix
- upload file screen can use less state rerenders and better error
messages

Object Uploader
- adapt the old object upload modal with 3d object preview into the
updated best version of upload modal
- preview loader code improvements, better flow and organization

Sets Tab:
- create a set tab, it currently show objects
- hide it behind a FeatureFlag

---------

Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
wilandwil authored and GitHub committed 2024-07-07 21:19:07 -04:00
1 parent c4e937d677
commit a8d0541c31
74 files changed
+1454 -377

No files matched your search

+1
View File
@@ -7,3 +7,4 @@ DEPLOY_PRIME_URL="https://studio-staging.studio.storyteller.ai"
REACT_APP_PUBLIC_POSTHOG_KEY="phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
REACT_APP_PUBLIC_POSTHOG_UI="https://us.i.posthog.com"
CONTEXT="DEVELOPMENT"
# CONTEXT="PRODUCTION"
@@ -1,6 +1,6 @@
import React, { ReactNode, useId, useRef, useState } from "react";
import { AssetType } from "~/enums";
import { UploadModal } from "~/components";
import { UploadModalObject } from "~/components";
import { twMerge } from "tailwind-merge";
interface Props {
@@ -83,7 +83,7 @@ export function FileWrapper({
{...rest}
/>
{render(id)}
<UploadModal
<UploadModalObject
closeModal={closeModal}
file={file!}
isOpen={modalOpen}
@@ -1,4 +1,4 @@
import { FileUploader } from "react-drag-drop-files";
import { FileUploader as DragDropFilesUploader } from "react-drag-drop-files";
import { DragAndDropZone } from "./DragAndDropZone";
interface Props {
@@ -8,11 +8,11 @@ interface Props {
setFile: (file: File | null) => void;
}
export const UploadFile = ({ fileTypes, title, file, setFile }: Props) => {
export const FileUploader = ({ fileTypes, title, file, setFile }: Props) => {
return (
<div className="flex w-full flex-col gap-3">
{title}
<FileUploader
<DragDropFilesUploader
handleChange={(file: File | null) => {
setFile(file);
}}
@@ -21,7 +21,7 @@ export const UploadFile = ({ fileTypes, title, file, setFile }: Props) => {
maxSize={50}
>
<DragAndDropZone file={file} fileTypes={fileTypes} />
</FileUploader>
</DragDropFilesUploader>
</div>
);
};
@@ -1,21 +1,23 @@
import { Button } from "~/components";
import { Button, P, H3 } from "~/components";
import { FilterEngineCategories } from "~/enums";
interface Props {
onCancel: () => void;
onRetry: () => void;
isAssetError: boolean;
type: FilterEngineCategories | string;
errorMessage?: string;
}
export const UploadAssetError = ({
onCancel,
onRetry,
isAssetError,
type,
errorMessage,
}: Props) => {
return (
<>
{isAssetError
? "There was a problem uploading your file."
: "There was a problem uploading the thumbnail for your file."}
<H3>Error in Uploading {type}</H3>
<P>{errorMessage ?? "Unknown Error"}</P>
<div className="mt-6 flex justify-end gap-2">
<Button onClick={onCancel} variant="secondary">
Cancel
@@ -0,0 +1,137 @@
import { Button, H6, Input, ListDropdown } from "~/components";
import { FileUploader } from "./FileUploader";
import { useState } from "react";
import { MediaFileAnimationType } from "~/enums";
interface Props {
title: string;
fileTypes: string[];
onClose: () => void;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
onSubmit: (options: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => void;
}
export const UploadFiles = ({
fileTypes,
onClose,
title,
options,
onSubmit,
}: Props) => {
const fileSubtypes = options?.fileSubtypes;
const hasLength = options?.hasLength;
const hasThumbnailUpload = options?.hasThumbnailUpload;
const [typeOption, setTypeOption] = useState<
MediaFileAnimationType | undefined
>(
fileSubtypes
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
: undefined,
);
const [uploadTitle, setUploadTitle] = useState<{
value: string;
error?: string;
}>({
value: "",
});
const [assetFile, setAssetFile] = useState<{
value: File | null;
error?: string;
}>({ value: null });
const [uploadLength, setUploadLength] = useState<number>();
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const handleSubmit = () => {
if (!uploadTitle.value) {
setUploadTitle((curr) => ({
...curr,
error: "Please enter a title.",
}));
return;
}
if (!assetFile.value) {
setAssetFile((curr) => ({
...curr,
error: "Please select a file to upload.",
}));
return;
}
onSubmit({
title: uploadTitle.value,
assetFile: assetFile.value,
thumbnailFile: thumbnailFile,
length: uploadLength ?? 1000,
typeOption,
});
};
return (
<>
<div className="mb-4 flex flex-col gap-4">
<Input
placeholder="Enter the title here"
errorMessage={uploadTitle.error}
value={uploadTitle.value}
onChange={(event) => setUploadTitle({ value: event.target.value })}
className={uploadTitle.error ? "mb-3" : ""}
/>
{fileSubtypes && fileSubtypes.length > 1 && (
<ListDropdown
list={fileSubtypes}
onSelect={(value) => setTypeOption(value as MediaFileAnimationType)}
/>
)}
{hasLength && (
<Input
type="number"
placeholder="Enter the length in ms (optional)"
value={uploadLength}
onChange={(event) => setUploadLength(parseInt(event.target.value))}
/>
)}
<FileUploader
title={title}
fileTypes={fileTypes}
file={assetFile.value}
setFile={(file: File | null) => {
setAssetFile({
value: file,
});
}}
/>
{assetFile.error && (
<H6 className="z-10 text-red">{assetFile.error}</H6>
)}
{hasThumbnailUpload && (
<FileUploader
title="Upload Thumbnail (optional)"
fileTypes={["PNG", "JGP", "GIF"]}
file={thumbnailFile}
setFile={setThumbnailFile}
/>
)}
<div className="flex justify-end gap-4">
<Button variant="primary" onClick={handleSubmit}>
Upload
</Button>
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</div>
</>
);
};
@@ -7,7 +7,11 @@ import { UploadAssetError } from "./UploadAssetError";
import { UploadSuccess } from "./UploadSuccess";
import { UploadFiles } from "./UploadFiles";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
import {
FilterEngineCategories,
MediaFileAnimationType,
UploaderState,
} from "~/enums";
import { getFileName } from "~/utilities";
interface Props {
@@ -16,34 +20,34 @@ interface Props {
isOpen: boolean;
title: string;
fileTypes: string[];
typeOptions: { [key: string]: string }[];
type: FilterEngineCategories;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
}
enum UploaderState {
ready,
uploadingAsset,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
}
const initialState = {
status: UploaderState.ready,
};
export function UploadModalMovement({
export function UploadModal({
isOpen,
onClose,
onSuccess,
title,
fileTypes,
typeOptions,
type,
options,
}: Props) {
const [objUploadStatus, setObjUploadStatus] = useState(UploaderState.ready);
const [uploaderState, setUploaderState] = useState<{
status: UploaderState;
errorMessage?: string;
}>(initialState);
const resetModalState = () => {
setObjUploadStatus(UploaderState.ready);
setUploaderState(initialState);
};
useEffect(() => {
@@ -60,12 +64,12 @@ export function UploadModalMovement({
thumbnailFile,
}: {
title: string;
typeOption: MediaFileAnimationType;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => {
setObjUploadStatus(UploaderState.uploadingAsset);
setUploaderState({ status: UploaderState.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
file: assetFile,
@@ -78,16 +82,19 @@ export function UploadModalMovement({
});
if (!assetReponse.success || !assetReponse.data) {
setObjUploadStatus(UploaderState.assetError);
setUploaderState({
status: UploaderState.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
const assetToken = assetReponse.data;
if (!thumbnailFile) {
setObjUploadStatus(UploaderState.success);
setUploaderState({ status: UploaderState.success });
return;
}
setObjUploadStatus(UploaderState.uploadingCover);
setUploaderState({ status: UploaderState.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
@@ -95,11 +102,14 @@ export function UploadModalMovement({
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
setObjUploadStatus(UploaderState.coverCreateError);
setUploaderState({
status: UploaderState.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
setObjUploadStatus(UploaderState.settingCover);
setUploaderState({ status: UploaderState.settingCover });
const thumbnailToken = thumbnailResponse.data;
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
@@ -107,21 +117,24 @@ export function UploadModalMovement({
imageToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
setObjUploadStatus(UploaderState.coverSetError);
setUploaderState({
status: UploaderState.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
setObjUploadStatus(UploaderState.success);
setUploaderState({ status: UploaderState.success });
};
const ObjUploaderContent = () => {
switch (objUploadStatus) {
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderState.ready:
return (
<UploadFiles
title={title}
fileTypes={fileTypes}
options={options}
onClose={onClose}
typeOptions={typeOptions}
onSubmit={onSubmit}
/>
);
@@ -145,6 +158,16 @@ export function UploadModalMovement({
/>
);
case UploaderState.assetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={type}
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderState.coverCreateError:
case UploaderState.coverSetError:
return (
@@ -153,7 +176,8 @@ export function UploadModalMovement({
onRetry={() => {
resetModalState();
}}
isAssetError={objUploadStatus === UploaderState.assetError}
type={"Thumbnail"}
errorMessage={uploaderState.errorMessage}
/>
);
}
@@ -161,7 +185,7 @@ export function UploadModalMovement({
return (
<TransitionDialogue isOpen={isOpen} onClose={onClose} title={title}>
<ObjUploaderContent />
<UploaderModalContent />
</TransitionDialogue>
);
}
@@ -0,0 +1,185 @@
import { useCallback, useEffect, useState, useRef } from "react";
import { MediaFileAnimationType } from "~/enums";
import { Button, H6, Input, ListDropdown } from "~/components";
import { FileUploader } from "../UploadModal/FileUploader";
import { loadPreviewOnCanvas } from "./utilities";
interface Props {
title: string;
fileTypes: string[];
onClose: () => void;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
onSubmit: (options: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => void;
}
export const UploadFiles3D = ({
fileTypes,
onClose,
title,
options,
onSubmit,
}: Props) => {
const fileSubtypes = options?.fileSubtypes;
const hasLength = options?.hasLength;
const hasThumbnailUpload = options?.hasThumbnailUpload;
const canvasRef = useRef<HTMLCanvasElement | undefined>(undefined);
const canvasCallbackRef = useCallback((node: HTMLCanvasElement) => {
if (node !== null) {
canvasRef.current = node;
}
}, []);
const [typeOption, setTypeOption] = useState<
MediaFileAnimationType | undefined
>(
fileSubtypes
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
: undefined,
);
const [uploadTitle, setUploadTitle] = useState<{
value: string;
error?: string;
}>({
value: "",
});
const [assetFile, setAssetFile] = useState<{
value: File | null;
error?: string;
}>({ value: null });
const [previewStatus, setPreviewStatus] = useState<{
type: string;
message?: string;
}>({ type: "init" });
const [uploadLength, setUploadLength] = useState<number>();
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const handleSubmit = () => {
if (!uploadTitle.value) {
setUploadTitle((curr) => ({
...curr,
error: "Please enter a title.",
}));
return;
}
if (!assetFile.value) {
setAssetFile((curr) => ({
...curr,
error: "Please select a file to upload.",
}));
return;
}
onSubmit({
title: uploadTitle.value,
assetFile: assetFile.value,
thumbnailFile: thumbnailFile,
length: uploadLength ?? 1000,
typeOption,
});
};
useEffect(() => {
if (canvasRef.current && assetFile.value) {
loadPreviewOnCanvas({
file: assetFile.value,
canvas: canvasRef.current,
statusCallback: (statusObject: { type: string; message?: string }) => {
setPreviewStatus(statusObject);
},
});
}
}, [assetFile.value]);
return (
<>
<div className="mb-4 flex flex-col gap-4">
<Input
placeholder="Enter the title here"
errorMessage={uploadTitle.error}
value={uploadTitle.value}
onChange={(event) => setUploadTitle({ value: event.target.value })}
className={uploadTitle.error ? "mb-3" : ""}
/>
{fileSubtypes && fileSubtypes.length > 1 && (
<ListDropdown
list={fileSubtypes}
onSelect={(value) => setTypeOption(value as MediaFileAnimationType)}
/>
)}
{hasLength && (
<Input
type="number"
placeholder="Enter the length in ms (optional)"
value={uploadLength}
onChange={(event) => setUploadLength(parseInt(event.target.value))}
/>
)}
<FileUploader
title={title}
fileTypes={fileTypes}
file={assetFile.value}
setFile={(file: File | null) => {
setAssetFile({
value: file,
});
}}
/>
{assetFile.error && (
<H6 className="z-10 text-red">{assetFile.error}</H6>
)}
<div className="relative m-auto aspect-square w-full overflow-hidden rounded-lg bg-brand-secondary">
<canvas className="h-full w-full" ref={canvasCallbackRef} />
{!assetFile.value && (
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
File Preivew
</H6>
)}
{previewStatus.type.includes("Error") && (
<>
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
{previewStatus.type}
{previewStatus.message && <br />}
{previewStatus.message}
</H6>
</>
)}
</div>
{hasThumbnailUpload && (
<FileUploader
title="Upload Thumbnail (optional)"
fileTypes={["PNG", "JGP", "GIF"]}
file={thumbnailFile}
setFile={setThumbnailFile}
/>
)}
<div className="flex justify-end gap-4">
<Button variant="primary" onClick={handleSubmit}>
Upload
</Button>
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</div>
</>
);
};
@@ -0,0 +1,193 @@
import { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { TransitionDialogue, LoadingDots } from "~/components";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
import { getFileName } from "~/utilities";
import { uploadAsset } from "./utilities";
interface Props {
onClose: () => void;
onSuccess: () => void;
isOpen: boolean;
title: string;
fileTypes: string[];
type: FilterEngineCategories;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
}
enum UploaderState {
ready,
uploadingAsset,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
}
const initialState = {
status: UploaderState.ready,
};
export function UploadModal3DPreview({
isOpen,
onClose,
onSuccess,
title,
fileTypes,
type,
options,
}: Props) {
const [uploaderState, setUploaderState] = useState<{
status: UploaderState;
errorMessage?: string;
}>(initialState);
const resetModalState = () => {
setUploaderState(initialState);
};
useEffect(() => {
if (isOpen) {
resetModalState();
}
}, [isOpen]);
const onSubmit = async ({
title,
typeOption,
assetFile,
thumbnailFile,
}: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
thumbnailFile: File | null;
}) => {
setUploaderState({ status: UploaderState.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await uploadAsset({
file: assetFile,
title: title,
animationType: typeOption,
});
if (!assetReponse.success || !assetReponse.data) {
setUploaderState({
status: UploaderState.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
const assetToken = assetReponse.data;
if (!thumbnailFile) {
setUploaderState({ status: UploaderState.success });
return;
}
setUploaderState({ status: UploaderState.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
fileName: getFileName(thumbnailFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
setUploaderState({
status: UploaderState.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.settingCover });
const thumbnailToken = thumbnailResponse.data;
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetToken,
imageToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
setUploaderState({
status: UploaderState.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.success });
};
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderState.ready:
return (
<UploadFiles3D
title={title}
fileTypes={fileTypes}
options={options}
onClose={onClose}
onSubmit={onSubmit}
/>
);
case UploaderState.uploadingAsset:
case UploaderState.uploadingCover:
case UploaderState.settingCover:
return (
<>
<LoadingDots className="mb-1 bg-transparent" />
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderState.success:
return (
<UploadSuccess
title={title}
onOk={() => {
onSuccess();
onClose();
}}
/>
);
case UploaderState.assetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={type}
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderState.coverCreateError:
case UploaderState.coverSetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={"Thumbnail"}
errorMessage={uploaderState.errorMessage}
/>
);
}
};
return (
<TransitionDialogue isOpen={isOpen} onClose={onClose} title={title}>
<UploaderModalContent />
</TransitionDialogue>
);
}
@@ -0,0 +1 @@
export * from "./UploadModal3DPreview";
@@ -0,0 +1,2 @@
export * from "./loadPreviewOnCanvas";
export * from "./uploadAsset";
@@ -0,0 +1,226 @@
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
interface LoaderInterface {
file: File;
camera: THREE.PerspectiveCamera;
scene: THREE.Scene;
renderer: THREE.WebGLRenderer;
statusCallback: (statusObject: { type: string; message?: string }) => void;
}
export const loadPreviewOnCanvas = ({
file,
canvas,
statusCallback,
}: {
file: File;
canvas: HTMLCanvasElement;
statusCallback: (error: { type: string; message?: string }) => void;
}) => {
// Setup of scene, camera, and renderer in the canvas
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
camera.position.z = 2;
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
canvas: canvas,
preserveDrawingBuffer: true,
});
renderer.setSize(
canvas.getBoundingClientRect().width || 0,
canvas.getBoundingClientRect().height || 0,
);
const color = 0xfcece7;
const light = new THREE.HemisphereLight(color, 0x8d8d8d, 3.0);
scene.add(light);
// load the file into the preview mini-scene depending of the file's type
if (file.name.includes(".glb")) {
glbLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".fbx")) {
fbxLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".pmd")) {
pmdLoader({ file, scene, camera, renderer, statusCallback });
} else if (
file.name.includes(".png") ||
file.name.includes(".jpg") ||
file.name.includes(".jpeg") ||
file.name.includes(".gif")
) {
imagePlaneLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".vmd")) {
statusCallback({
type: "Preview Error",
message: "Sorry, Preview is not available to VMD files yet",
});
} else {
statusCallback({
type: "Preview Error",
message: "Unknown file type for loader",
});
}
// Render the loaded data
const animate = function () {
renderer.render(scene, camera);
};
renderer.setAnimationLoop(animate);
};
const glbLoader = ({
file,
camera,
scene,
renderer,
statusCallback,
}: LoaderInterface) => {
const loader = new GLTFLoader();
loader.load(
URL.createObjectURL(file),
(data) => {
console.log("preview object loader: onLoaded");
data.scene.children.forEach((child) => {
child.userData["color"] = "#FFFFFF";
scene.add(child);
let maxSize = 2;
if (scene.children.length > 0) {
scene.children.forEach((child) => {
child.traverse((object: THREE.Object3D | THREE.Mesh) => {
// Assuming `object` is your Three.js object and you know it's a Mesh
if (object instanceof THREE.Mesh) {
object.geometry.computeBoundingBox();
const boundingBox = object.geometry.boundingBox;
const center = new THREE.Vector3();
boundingBox.getCenter(center);
const dimensions = new THREE.Vector3();
boundingBox.getSize(dimensions);
const maxDim = Math.max(
dimensions.x,
dimensions.y,
dimensions.z,
);
if (maxSize < maxDim) {
maxSize = maxDim;
camera.position.set(-maxDim, maxDim, maxDim);
camera.lookAt(center);
camera.updateProjectionMatrix();
}
}
});
});
}
renderer.render(scene, camera);
});
},
undefined, // nothing to-do onProgress
(loaderError) => {
/*onError*/
statusCallback({
type: "GLB Loader Error",
message: String(loaderError),
});
},
);
};
const fbxLoader = ({ file, statusCallback }: LoaderInterface) => {
const loader = new FBXLoader();
loader.load(
URL.createObjectURL(file),
(data) => {
console.log(data);
statusCallback({
type: "FBX Loader Error",
message: "Sorry, FBX Loader is still under development",
});
},
undefined, // nothing to-do onProgress
(loaderError) => {
/*onError*/
statusCallback({
type: "FBX Loader Error",
message: String(loaderError),
});
},
);
};
const pmdLoader = ({
camera,
scene,
renderer,
statusCallback,
}: LoaderInterface) => {
camera.position.z = 30;
const loader = new FontLoader();
loader.load(
"https://threejs.org/examples/fonts/helvetiker_regular.typeface.json",
(font) => {
const textGeometry = new TextGeometry("MMD", {
font: font,
size: 100,
depth: 5,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 1,
bevelSize: 1,
bevelOffset: 0,
bevelSegments: 5,
});
textGeometry.computeBoundingBox();
const textMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
});
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
textMesh.scale.set(0.15, 0.15, 0.01);
textMesh.position.set(-22, -5, 0);
scene.add(textMesh);
renderer.render(scene, camera);
},
undefined, // nothing on Progress
(loaderError) => {
/*onError*/
statusCallback({
type: "PMD Loader Error",
message: String(loaderError),
});
},
);
};
const imagePlaneLoader = ({ file, scene, statusCallback }: LoaderInterface) => {
const geometry = new THREE.PlaneGeometry(1, 1);
const loader = new THREE.TextureLoader();
const texture = loader.load(
URL.createObjectURL(file),
undefined, // nothing to-do onLoad
undefined, // nothing to-do onProgress
(loaderError) => {
/*onError*/
statusCallback({
type: "Image Plane Loader Error",
message: String(loaderError),
});
},
);
texture.colorSpace = THREE.SRGBColorSpace;
const image_material = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: texture,
});
const obj = new THREE.Mesh(geometry, image_material);
obj.receiveShadow = true;
obj.castShadow = true;
scene.add(obj);
};
@@ -0,0 +1,53 @@
import { MediaUploadApi } from "~/Classes/ApiManager";
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
import { getFileExtension } from "~/utilities";
import { v4 as uuidv4 } from "uuid";
export const uploadAsset = async ({
file,
title,
animationType,
}: {
file: File;
title: string;
animationType?: MediaFileAnimationType;
}) => {
const mediaUploadApi = new MediaUploadApi();
const fileExtension = getFileExtension(file);
const assetReponse = await (async () => {
switch (fileExtension) {
case ".zip":
return mediaUploadApi.UploadPmx({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.CHARACTER,
maybe_title: title,
maybe_animation_type: animationType,
uuid: uuidv4(),
});
case ".png":
case ".gif":
case ".jpg":
case ".jpeg":
return mediaUploadApi.UploadNewEngineAsset({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.IMAGE_PLANE,
maybe_animation_type: animationType,
maybe_title: title,
uuid: uuidv4(),
});
default:
return mediaUploadApi.UploadNewEngineAsset({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.OBJECT,
maybe_animation_type: animationType,
maybe_title: title,
uuid: uuidv4(),
});
}
})();
return assetReponse;
};
+2
View File
@@ -25,6 +25,8 @@ export * from "./Textarea";
export * from "./Tooltip";
export * from "./TransitionDialogue"; //need to decouple hotkey signal
export * from "./Typography";
export * from "./UploadModal";
export * from "./UploadModal3DPreview";
export * from "./WaveformPlayer";
//TODO: need to rewrite because it's against tailwind
@@ -1,108 +0,0 @@
import { Button, H6, Input, ListDropdown } from "~/components";
import { UploadFile } from "./UploadFile";
import { useState } from "react";
import { MediaFileAnimationType } from "~/enums";
interface Props {
title: string;
fileTypes: string[];
onClose: () => void;
typeOptions: { [key: string]: string }[];
onSubmit: (options: {
title: string;
typeOption: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => void;
}
export const UploadFiles = ({
fileTypes,
onClose,
title,
typeOptions,
onSubmit,
}: Props) => {
const [typeOption, setTypeOption] = useState<MediaFileAnimationType>(
Object.values(typeOptions[0])[0] as MediaFileAnimationType,
);
const [uploadTitle, setUploadTitle] = useState("");
const [uploadLength, setUploadLength] = useState<number>();
const [titleError, setTitleError] = useState("");
const [assetFileError, setAssetFileError] = useState("");
const [assetFile, setAssetFile] = useState<File | null>(null);
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const handleSubmit = () => {
if (!uploadTitle) {
setTitleError("Please enter the title");
} else {
setTitleError("");
}
if (!assetFile) {
setAssetFileError("Please select a file to upload");
} else {
setAssetFileError("");
}
if (!uploadTitle || !assetFile) {
return;
}
onSubmit({
title: uploadTitle,
assetFile: assetFile,
thumbnailFile: thumbnailFile,
length: uploadLength ?? 1000,
typeOption,
});
};
return (
<>
<div className="mb-4 flex flex-col gap-4">
<Input
placeholder="Enter the title here"
errorMessage={titleError}
value={uploadTitle}
onChange={(event) => setUploadTitle(event.target.value)}
className={titleError ? "mb-3" : ""}
/>
{typeOptions.length > 1 && (
<ListDropdown
list={typeOptions}
onSelect={(value) => setTypeOption(value as MediaFileAnimationType)}
/>
)}
<Input
type="number"
placeholder="Enter the length in ms (optional)"
value={uploadLength}
onChange={(event) => setUploadLength(parseInt(event.target.value))}
/>
<UploadFile
title={title}
fileTypes={fileTypes}
file={assetFile}
setFile={setAssetFile}
/>
{assetFileError && <H6 className="z-10 text-red">{assetFileError}</H6>}
<UploadFile
title="Upload Thumbnail (optional)"
fileTypes={["PNG", "JGP", "GIF"]}
file={thumbnailFile}
setFile={setThumbnailFile}
/>
<div className="flex justify-end gap-4">
<Button variant="primary" onClick={handleSubmit}>
Upload
</Button>
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</div>
</>
);
};
@@ -1 +0,0 @@
export * from "./UploadModalMovement";
@@ -47,7 +47,7 @@ enum UploaderState {
loaderError,
}
export function UploadModal({
export function UploadModalObject({
closeModal,
file,
isOpen,
@@ -190,13 +190,13 @@ export function UploadModal({
file.name.includes(".jpeg") ||
file.name.includes(".gif")
) {
const geometry = new THREE.PlaneGeometry( 1, 1 );
const geometry = new THREE.PlaneGeometry(1, 1);
const loader = new THREE.TextureLoader();
const texture = loader.load(URL.createObjectURL(file));
texture.colorSpace = THREE.SRGBColorSpace;
console.log(texture)
console.log(texture);
const image_material = new THREE.MeshBasicMaterial({
color: 0xFFFFFF,
color: 0xffffff,
map: texture,
});
const obj = new THREE.Mesh(geometry, image_material);
@@ -204,10 +204,10 @@ export function UploadModal({
obj.castShadow = true;
scene.add(obj);
}
const animate = function() {
renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );
const animate = function () {
renderer.render(scene, camera);
};
renderer.setAnimationLoop(animate);
}
}
}, [file, targetNode, resetModal, resetModalState]);
@@ -309,7 +309,6 @@ export function UploadModal({
});
}
})();
if (!assetReponse.success || !assetReponse.data) {
objUploadStatusSet(UploaderState.assetError);
@@ -6,7 +6,7 @@ export const fitCameraToCenteredObject = function (
camera: THREE.PerspectiveCamera,
object: THREE.Object3D<THREE.Object3DEventMap>,
offset?: number,
orbitControls?,
orbitControls?: { target: THREE.Vector3; maxDistance: number },
) {
const boundingBox = new THREE.Box3();
boundingBox.setFromObject(object);
@@ -0,0 +1 @@
export * from "./UploadModalObject";
+1 -2
View File
@@ -6,5 +6,4 @@ export * from "./PremiumLock";
// all the Upload stuff will depend on the API Manager Signal
export * from "./UploadAudioButtonDialogue";
export * from "./UploadAudioComponent";
export * from "./UploadModal"; //need to rid of scss
export * from "./UploadModalMovement";
export * from "./UploadModalObject"; //need to rid of scss
-1
View File
@@ -6,5 +6,4 @@ export enum AssetType {
EXPRESSION = "expression",
OBJECT = "object",
SHAPE = "shape",
STYLE = "style", // TODO Remove
}
+1
View File
@@ -1,3 +1,4 @@
export enum FeatureFlags {
SHOW_SEARCH_OBJECTS = "show-search-objects",
SHOW_SETS_TAB = "show-sets-tab",
}
+12
View File
@@ -4,3 +4,15 @@ export enum AUDIO_FILE_TYPE {
FLAC = "FLAC",
OGG = "OGG",
}
export enum IMAGE_FILE_TYPE {
PNG = "PNG",
JPG = "JPG",
JPEG = "JPEG",
GIF = "GIF",
}
export enum OBJECT_FILE_TYPE {
GLB = "GLB",
FBX = "FBX",
}
+9
View File
@@ -0,0 +1,9 @@
export enum TabTitles {
SET_OBJECTS = "Film Sets",
CHARACTERS = "Character",
ANIMATION = "Animation",
EXPRESSIONS = "Facial Expression",
OBJECTS = "Objects",
AUDIO = "Audio",
STYLIZE = "AI Stylize",
}
+10
View File
@@ -0,0 +1,10 @@
export enum UploaderState {
ready,
uploadingAsset,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
}
+2
View File
@@ -10,5 +10,7 @@ export * from "./FileTypes";
export * from "./JobStatus";
export * from "./MediaFileAnimationTypes";
export * from "./QueryFilters";
export * from "./Tabs";
export * from "./toasts";
export * from "./UploaderState";
export * from "./Visibility";
@@ -1,14 +1,25 @@
import { useState } from "react";
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureFlag";
import { FeatureFlags } from "~/enums";
import { useSignals } from "@preact/signals-react/runtime";
import {
dndSidePanelWidth,
sidePanelVisible,
sidePanelWidth,
} from "~/pages/PageEnigma/signals";
import { SidePanelTabs } from "~/pages/PageEnigma/comps/SidePanelTabs";
import { SidePanelMenu } from "~/pages/PageEnigma/comps/SidePanelTabs/SidePanelMenu";
import { useSignals } from "@preact/signals-react/runtime";
import { SidePanelMenu } from "~/pages/PageEnigma/comps/SidePanelMenu";
import { TabItem, tabList } from "./tabList";
export const SidePanel = () => {
useSignals();
const initialTabIdx = usePosthogFeatureFlag(FeatureFlags.SHOW_SETS_TAB)
? 0
: 1;
const [selectedTab, setSelectedTab] = useState<TabItem>(
tabList[initialTabIdx],
);
const displayWidth =
dndSidePanelWidth.value > -1
@@ -31,10 +42,16 @@ export const SidePanel = () => {
}}
>
<div className="relative block h-full w-full bg-ui-panel">
<SidePanelTabs />
<SidePanelTabs tabs={tabList} selectedTab={selectedTab} />
</div>
</div>
<SidePanelMenu />
<SidePanelMenu
tabs={tabList}
selectedTab={selectedTab}
selectTab={(newSelectedTab) => {
setSelectedTab(newSelectedTab);
}}
/>
</>
);
};
@@ -0,0 +1,64 @@
import { ReactNode } from "react";
import { FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
import {
faGlobeSnow,
faPeople,
faRabbitRunning,
faFaceSmileWink,
faCube,
faVolume,
faBrush,
} from "@fortawesome/pro-solid-svg-icons";
import { TabTitles } from "~/enums";
import { ObjectsTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/ObjectsTab";
import { AnimationTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/AnimationTab";
import { AudioTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/AudioTab";
import { StylizeTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/StylizeTab/StylizeTab";
import { ExpressionTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/ExpressionTab";
import { CharactersTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/CharactersTab";
import { SetsTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/SetsTab";
export interface TabItem {
icon: FontAwesomeIconProps["icon"];
title: string;
component: ReactNode;
}
export const tabList = [
{
icon: faGlobeSnow,
title: TabTitles.SET_OBJECTS,
component: <SetsTab />,
},
{
icon: faPeople,
title: TabTitles.CHARACTERS,
component: <CharactersTab />,
},
{
icon: faRabbitRunning,
title: TabTitles.ANIMATION,
component: <AnimationTab />,
},
{
icon: faFaceSmileWink,
title: TabTitles.EXPRESSIONS,
component: <ExpressionTab />,
},
{
icon: faCube,
title: TabTitles.OBJECTS,
component: <ObjectsTab />,
},
{
icon: faVolume,
title: TabTitles.AUDIO,
component: <AudioTab />,
},
{
icon: faBrush,
title: TabTitles.STYLIZE,
component: <StylizeTab />,
},
];
@@ -0,0 +1,86 @@
import { useSignals } from "@preact/signals-react/runtime";
import { twMerge } from "tailwind-merge";
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureFlag";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FeatureFlags, TabTitles } from "~/enums";
import { EditorStates } from "~/pages/PageEnigma/enums";
import { editorState } from "~/pages/PageEnigma/signals/engine";
import { sidePanelVisible } from "~/pages/PageEnigma/signals/sidePanel";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { TabItem } from "../SidePanel/tabList";
import { pageHeight } from "~/signals";
export const SidePanelMenu = ({
tabs,
selectTab,
selectedTab,
}: {
tabs: TabItem[];
selectTab: (newSelectedTab: TabItem) => void;
selectedTab: TabItem;
}) => {
useSignals();
const showSetsTab = usePosthogFeatureFlag(FeatureFlags.SHOW_SETS_TAB);
return (
<div
className={twMerge(
"fixed z-20 bg-assets-background",
"right-0 top-[64px] w-[84px] px-2 py-2",
"overflow-y-auto",
)}
style={{
height: pageHeight.value - 64,
}}
>
<div className="flex w-full flex-col gap-2">
{(tabs ?? []).map((tab) => {
if (tab.title === TabTitles.SET_OBJECTS && !showSetsTab) {
return;
}
return (
<button
key={tab.title}
className={twMerge([
"flex flex-col items-center rounded-lg border-2 border-transparent px-2 py-3 transition-all duration-200 hover:bg-assets-selectedTab/70",
tab.title === selectedTab.title
? "bg-assets-selectedTab opacity-100 hover:bg-assets-selectedTab"
: "opacity-60",
tab.title === TabTitles.STYLIZE &&
"bg-brand-primary font-medium opacity-90 hover:border-white/25 hover:bg-brand-primary hover:opacity-100",
tab.title === selectedTab.title &&
tab.title === TabTitles.STYLIZE
? "border-white/50 opacity-100 hover:border-white/50"
: "",
])}
onClick={() => {
selectTab(tab);
if (!sidePanelVisible.value) {
sidePanelVisible.value = true;
}
if (editorState.value === EditorStates.PREVIEW) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ENTER_EDIT_STATE,
data: null,
});
}
}}
>
<div>
<FontAwesomeIcon icon={tab.icon} size="lg" />
</div>
<div className="-mb-1 mt-1" style={{ fontSize: 11 }}>
{tab.title}
</div>
</button>
);
})}
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SidePanelMenu";
@@ -1,89 +0,0 @@
import { useEffect, useLayoutEffect, useState } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { twMerge } from "tailwind-merge";
import { AssetType } from "~/enums";
import { EditorStates } from "~/pages/PageEnigma/enums";
import { editorState } from "~/pages/PageEnigma/signals/engine";
import {
selectedTab,
sidePanelVisible,
} from "~/pages/PageEnigma/signals/sidePanel";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { TabItem, tabList } from "./tabList";
import { pageHeight } from "~/signals";
export const SidePanelMenu = () => {
useSignals();
const [tabs, setTabs] = useState<TabItem[]>();
useLayoutEffect(() => {
setTabs(tabList);
}, []);
useEffect(() => {
if (tabs) {
selectedTab.value = tabs[0];
}
}, [tabs]);
return (
<div
className={[
"fixed z-20 bg-assets-background",
"px-2 py-2",
"overflow-y-auto",
].join(" ")}
style={{
height: pageHeight.value - 64,
minWidth: 84,
maxWidth: 84,
right: 0,
top: 64,
}}
>
<div className="flex w-full flex-col gap-2">
{(tabs ?? []).map((tab) => (
<button
key={tab.value}
className={twMerge([
"flex flex-col items-center rounded-lg border-2 border-transparent px-2 py-3 transition-all duration-200 hover:bg-assets-selectedTab/70",
tab.value === selectedTab?.value?.value
? "bg-assets-selectedTab opacity-100 hover:bg-assets-selectedTab"
: "opacity-60",
tab.value === AssetType.STYLE &&
"bg-brand-primary font-medium opacity-90 hover:border-white/25 hover:bg-brand-primary hover:opacity-100",
tab.value === selectedTab?.value?.value &&
tab.value === AssetType.STYLE
? "border-white/50 opacity-100 hover:border-white/50"
: "",
])}
onClick={() => {
selectedTab.value = tab;
if (!sidePanelVisible.value) {
sidePanelVisible.value = true;
}
if (editorState.value === EditorStates.PREVIEW) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ENTER_EDIT_STATE,
data: null,
});
}
}}
>
<div>
<img src={tab.icon} alt={tab.title} width={20} height={20} />
</div>
<div className="-mb-1 mt-1 text-nowrap" style={{ fontSize: 11 }}>
{tab.title}
</div>
</button>
))}
</div>
</div>
);
};
@@ -1,20 +1,27 @@
import { useSignals } from "@preact/signals-react/runtime";
import { selectedTab, sidePanelHeight } from "~/pages/PageEnigma/signals";
import { tabList } from "~/pages/PageEnigma/comps/SidePanelTabs/tabList";
import { useMouseEventsSidePanel } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsSidePanel";
import { sidePanelHeight } from "~/pages/PageEnigma/signals";
export const SidePanelTabs = () => {
import { useMouseEventsSidePanel } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsSidePanel";
import { TabItem } from "../SidePanel/tabList";
export const SidePanelTabs = ({
selectedTab,
tabs,
}: {
selectedTab: TabItem;
tabs: TabItem[];
}) => {
useSignals();
const { onPointerDown } = useMouseEventsSidePanel();
return (
<>
<div style={{ height: sidePanelHeight.value, width: "100%" }}>
{tabList.map((tab) => (
{tabs.map((tab, index) => (
<div
key={tab.value}
key={index}
className={
tab.value === selectedTab.value?.value
tab.title === selectedTab.title
? "flex h-full flex-col gap-3.5 overflow-y-auto"
: "hidden"
}
@@ -1,12 +0,0 @@
import { useState } from "react";
import { FetchStatus } from "~/pages/PageEnigma/enums";
export const usePagination = (setStatus: (status: FetchStatus) => void) => {
const [page, setPage] = useState(0);
const pageChange = (page: number) => {
setPage(page);
setStatus(FetchStatus.READY);
};
return { page, pageChange };
};
@@ -1,10 +1,6 @@
import { faChevronLeft, faClose } from "@fortawesome/pro-solid-svg-icons";
import { ButtonIcon } from "~/components";
import {
lastSelectedTab,
selectedTab,
sidePanelVisible,
} from "~/pages/PageEnigma/signals";
import { sidePanelVisible } from "~/pages/PageEnigma/signals";
interface Props {
title: string;
@@ -13,8 +9,6 @@ interface Props {
export function TabTitle({ title, onBack }: Props) {
const onClose = () => {
lastSelectedTab.value = selectedTab.value;
selectedTab.value = null;
sidePanelVisible.value = false;
};
@@ -0,0 +1,3 @@
export * from "./ItemElement";
export * from "./ItemElements";
export * from "./TabTitle";
@@ -14,14 +14,11 @@ import {
import { animationItems } from "~/pages/PageEnigma/signals";
import { addToast } from "~/signals";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button, FilterButtons, Pagination, UploadModal } from "~/components";
import {
Button,
FilterButtons,
Pagination,
UploadModalMovement,
} from "~/components";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
TabTitle,
ItemElements,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import { MediaFilesApi } from "~/Classes/ApiManager";
import { FetchStatus } from "~/pages/PageEnigma/enums";
@@ -201,19 +198,23 @@ export const AnimationTab = () => {
}}
/>
)}
<UploadModalMovement
<UploadModal
onClose={() => setOpen(false)}
onSuccess={fetchUserAnimations}
isOpen={open}
fileTypes={["GLB", "FBX", "VMD"]}
title="Upload Animation"
typeOptions={[
{ Mixamo: MediaFileAnimationType.Mixamo },
{ MikuMikuDance: MediaFileAnimationType.MikuMikuDance },
{ MoveAi: MediaFileAnimationType.MoveAi },
{ Rigify: MediaFileAnimationType.Rigify },
{ Rokoko: MediaFileAnimationType.Rokoko },
]}
options={{
fileSubtypes: [
{ Mixamo: MediaFileAnimationType.Mixamo },
{ MikuMikuDance: MediaFileAnimationType.MikuMikuDance },
{ MoveAi: MediaFileAnimationType.MoveAi },
{ Rigify: MediaFileAnimationType.Rigify },
{ Rokoko: MediaFileAnimationType.Rokoko },
],
hasLength: true,
hasThumbnailUpload: true,
}}
type={FilterEngineCategories.ANIMATION}
/>
</>
@@ -5,7 +5,7 @@ import {
V2VState,
} from "~/pages/PageEnigma/models/voice";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps/TabTitle";
import { PageTTS } from "./pageTTS";
import { PageVoicetoVoice } from "./pageVoiceToVoice";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
@@ -11,7 +11,7 @@ import {
} from "~/components";
import { AudioItemElements } from "./audioItemElements";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps/TabTitle";
import { InferenceElement } from "./audioInferenceElement";
import { AssetFilterOption } from "~/enums";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
@@ -1,7 +1,7 @@
import { useCallback, useState } from "react";
import { Input } from "~/components";
import { Weight } from "~/models";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps/TabTitle";
import { VoiceModelElement } from "./voiceModelElement";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
@@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import { Weight } from "~/models";
import { Input } from "~/components";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps/TabTitle";
import { VoiceModelElement } from "./voiceModelElement";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
@@ -1,10 +1,12 @@
import { cameraFilter, cameraItems } from "~/pages/PageEnigma/signals";
import { useSignals } from "@preact/signals-react/runtime";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button } from "~/components";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import {
ItemElements,
TabTitle,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import { AssetFilterOption } from "~/enums";
export const CameraTab = () => {
@@ -58,10 +60,7 @@ export const CameraTab = () => {
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4">
<ItemElements
items={cameraItems.value}
assetFilter={cameraFilter.value}
/>
<ItemElements items={cameraItems.value} />
</div>
</>
);
@@ -15,9 +15,11 @@ import { addToast } from "~/signals";
import { BucketConfig } from "~/api/BucketConfig";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button, FileWrapper, FilterButtons, Pagination } from "~/components";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import {
ItemElements,
TabTitle,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import { MediaFilesApi } from "~/Classes/ApiManager";
@@ -11,14 +11,16 @@ import {
MediaFileAnimationType,
} from "~/enums";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import {
Button,
FilterButtons,
Pagination,
UploadModalMovement,
UploadModal,
} from "~/components";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import {
ItemElements,
TabTitle,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import { MediaFilesApi } from "~/Classes/ApiManager";
import { addToast } from "~/signals";
@@ -168,13 +170,17 @@ export const ExpressionTab = () => {
}}
/>
)}
<UploadModalMovement
<UploadModal
onClose={() => setOpen(false)}
onSuccess={fetchUserExpressions}
isOpen={open}
fileTypes={["CSV"]}
title="Upload Expression"
typeOptions={[{ ARKit: MediaFileAnimationType.ArKit }]}
options={{
fileSubtypes: [{ ARKit: MediaFileAnimationType.ArKit }],
hasLength: true,
hasThumbnailUpload: true,
}}
type={FilterEngineCategories.EXPRESSION}
/>
</>
@@ -17,7 +17,10 @@ import { shapeItems } from "~/pages/PageEnigma/signals";
import { BucketConfig } from "~/api/BucketConfig";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import {
TabTitle,
ItemElements,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import {
Button,
FileWrapper,
@@ -25,7 +28,6 @@ import {
SearchFilter,
Pagination,
} from "~/components";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { MediaFilesApi } from "~/Classes/ApiManager";
import { addToast } from "~/signals";
@@ -0,0 +1,172 @@
import { useCallback, useEffect, useState } from "react";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import {
AssetFilterOption,
FilterEngineCategories,
IMAGE_FILE_TYPE,
OBJECT_FILE_TYPE,
} from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import {
Button,
FilterButtons,
Pagination,
UploadModal3DPreview,
} from "~/components";
import {
TabTitle,
ItemElements,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import {
fetchFeaturedMediaItems,
fetchUserMediaItems,
FetchMediaItemStates,
isAnyStatusFetching,
} from "../utilities";
export const SetsTab = () => {
const [openUploadModal, setOpenUploadModal] = useState(false);
const [
{ mediaItems: userSetObjects, status: userFetchStatus },
setUserFetch,
] = useState<FetchMediaItemStates>({
mediaItems: undefined,
status: FetchStatus.READY,
});
const [
{ mediaItems: featuredSetObjects, status: feateredFetchStatus },
setFeaturedFetch,
] = useState<FetchMediaItemStates>({
mediaItems: undefined,
status: FetchStatus.READY,
});
const [selectedFilter, setSelectedFilter] = useState(
AssetFilterOption.FEATURED,
);
const diasplayedItems =
selectedFilter === AssetFilterOption.FEATURED
? featuredSetObjects ?? []
: userSetObjects ?? [];
const [currentPage, setCurrentPage] = useState<number>(0);
const pageSize = 21;
const totalPages = Math.ceil(diasplayedItems.length / pageSize);
const isFetching = isAnyStatusFetching([
userFetchStatus,
feateredFetchStatus,
]);
const fetchUserSetObjects = useCallback(
() =>
fetchUserMediaItems({
filterEngineCategories: [
FilterEngineCategories.OBJECT,
FilterEngineCategories.IMAGE_PLANE,
],
setState: (newState: FetchMediaItemStates) => {
setUserFetch((curr) => ({
status: newState.status,
mediaItems: newState.mediaItems
? newState.mediaItems
: curr.mediaItems,
}));
},
defaultErrorMessage: "Unknown Error in Fetching User Set Objects",
}),
[],
);
const fetchFeaturedSetObjects = useCallback(
() =>
fetchFeaturedMediaItems({
filterEngineCategories: [FilterEngineCategories.OBJECT],
setState: (newState: FetchMediaItemStates) => {
setFeaturedFetch((curr) => ({
status: newState.status,
mediaItems: newState.mediaItems
? newState.mediaItems
: curr.mediaItems,
}));
},
defaultErrorMessage: "Unknown Error in Fetching Featured Set Objects",
}),
[],
);
useEffect(() => {
if (!userSetObjects) {
fetchUserSetObjects();
}
if (!featuredSetObjects) {
fetchFeaturedSetObjects();
}
}, [
userSetObjects,
fetchUserSetObjects,
featuredSetObjects,
fetchFeaturedSetObjects,
]);
return (
<>
<TabTitle title="Film Sets" />
<FilterButtons
value={selectedFilter}
onClick={(buttonIdx) => {
setSelectedFilter(Number(buttonIdx));
}}
/>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
onClick={() => setOpenUploadModal(true)}
className="w-full py-3 text-sm font-medium"
>
Upload Set Objects
</Button>
</div>
<div className="w-full grow overflow-y-auto px-4 pb-4">
<ItemElements
busy={isFetching}
debug="animations tab"
currentPage={currentPage}
pageSize={pageSize}
items={diasplayedItems}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
<UploadModal3DPreview
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserSetObjects}
isOpen={openUploadModal}
fileTypes={[
...Object.values(OBJECT_FILE_TYPE),
"PMD",
...Object.values(IMAGE_FILE_TYPE),
]}
title="Upload Set Objects"
type={FilterEngineCategories.OBJECT}
/>
</>
);
};
@@ -4,7 +4,7 @@ import { ArtStyle } from "~/pages/PageEnigma/Editor/api_manager";
import { styleList } from "~/pages/PageEnigma/styleList";
import { ItemPicker } from "./ItemPicker";
import { useSignals } from "@preact/signals-react/runtime";
import { TabTitle } from "../../comps/TabTitle";
import { TabTitle } from "../../sharedComps/TabTitle";
import { StylizeTabPages } from "~/pages/PageEnigma/enums";
interface Props {
@@ -1,4 +1,4 @@
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps/TabTitle";
import { PageStyleSelection } from "./PageStyleSelection";
import { Prompts } from "./Prompts";
import { StyleButtons } from "./StyleButtons";
@@ -1,55 +0,0 @@
import { ObjectsTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ObjectsTab";
import { AnimationTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/AnimationTab";
import { AudioTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/AudioTab";
import { StylizeTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/StylizeTab/StylizeTab";
import { ExpressionTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ExpressionTab";
import { ReactNode } from "react";
import { AssetType } from "~/enums";
import { CharactersTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/CharactersTab";
export interface TabItem {
icon: string;
title: string;
value: AssetType;
component: ReactNode;
}
export const tabList = [
{
icon: "/resources/icons/characters.png",
title: "Characters",
value: AssetType.CHARACTER,
component: <CharactersTab />,
},
{
icon: "/resources/icons/animations.png",
title: "Animation",
value: AssetType.ANIMATION,
component: <AnimationTab />,
},
{
icon: "/resources/icons/expressions.png",
title: "Facial Expression",
value: AssetType.EXPRESSION,
component: <ExpressionTab />,
},
{
icon: "/resources/icons/objects.png",
title: "Objects",
value: AssetType.OBJECT,
component: <ObjectsTab />,
},
{
icon: "/resources/icons/audios.png",
title: "Audio",
value: AssetType.AUDIO,
component: <AudioTab />,
},
{
icon: "/resources/icons/brush.png",
title: "AI Stylize",
value: AssetType.STYLE,
component: <StylizeTab />,
},
];
@@ -0,0 +1,85 @@
import { FilterEngineCategories, ToastTypes } from "~/enums";
import { addToast } from "~/signals";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import { MediaFilesApi } from "~/Classes/ApiManager";
import { MediaItem } from "~/pages/PageEnigma/models";
import { responseMapping } from "./misc";
export interface FetchMediaItemStates {
mediaItems?: MediaItem[];
status: FetchStatus;
}
interface fetchMediaItemsInterface {
setState: ({ mediaItems, status }: FetchMediaItemStates) => void;
filterEngineCategories: FilterEngineCategories[];
defaultErrorMessage?: string;
}
export const fetchUserMediaItems = async ({
setState,
filterEngineCategories,
defaultErrorMessage,
}: fetchMediaItemsInterface) => {
setState({ status: FetchStatus.IN_PROGRESS });
const mediaFilesApi = new MediaFilesApi();
const response = await mediaFilesApi.ListUserMediaFiles({
page_size: 100,
filter_engine_categories: filterEngineCategories,
});
if (response.success && response.data) {
const newSetObjects = responseMapping(
response.data,
filterEngineCategories,
);
setState({
mediaItems: newSetObjects,
status: FetchStatus.SUCCESS,
});
return;
}
addToast(
ToastTypes.ERROR,
response.errorMessage ??
defaultErrorMessage ??
"Unknown Error in Fetching Media Items",
);
setState({ status: FetchStatus.ERROR });
return;
};
export const fetchFeaturedMediaItems = async ({
setState,
filterEngineCategories,
defaultErrorMessage,
}: fetchMediaItemsInterface) => {
setState({ status: FetchStatus.IN_PROGRESS });
const mediaFilesApi = new MediaFilesApi();
const response = await mediaFilesApi.ListFeaturedMediaFiles({
page_size: 100,
filter_engine_categories: filterEngineCategories,
});
if (response.success && response.data) {
const newSetObjects = responseMapping(
response.data,
filterEngineCategories,
);
setState({
mediaItems: newSetObjects,
status: FetchStatus.SUCCESS,
});
return;
}
addToast(
ToastTypes.ERROR,
response.errorMessage ??
defaultErrorMessage ??
"Unknown Error in Fetching Media Items",
);
setState({ status: FetchStatus.ERROR });
return;
};
@@ -0,0 +1,2 @@
export * from "./fetchMediaItems";
export * from "./misc";
@@ -0,0 +1,46 @@
import { AssetType, FilterEngineCategories } from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import { MediaInfo, MediaItem } from "~/pages/PageEnigma/models";
import { BucketConfig } from "~/api/BucketConfig";
export const isAnyStatusFetching = (statuses: FetchStatus[]): boolean => {
return statuses.some((status) => {
return status === FetchStatus.READY || status === FetchStatus.IN_PROGRESS;
});
};
export const responseMapping = (
data: MediaInfo[],
filterEngineCategories: FilterEngineCategories[],
): MediaItem[] => {
//TODO: ASSET TYPES and ENGINE CATEGORIES NEED TO MATCH!!!!
//TODO: GET RID OF ASSET TYPES!!
const assetType =
filterEngineCategories[0] === FilterEngineCategories.IMAGE_PLANE
? AssetType.OBJECT
: filterEngineCategories[0];
return data.map((item) => {
const bucketConfig = new BucketConfig();
const itemThumb = bucketConfig.getCdnUrl(
item.cover_image.maybe_cover_image_public_bucket_path ?? "",
600,
100,
);
return {
colorIndex: item.cover_image.default_cover.color_index,
imageIndex: item.cover_image.default_cover.image_index,
media_id: item.token,
name: item.maybe_title ?? "Unknown",
type: assetType as AssetType,
media_type: item.media_type,
length: ((item.maybe_duration_millis ?? 1000) / 1000) * 60,
version: 1,
...(item.cover_image.maybe_cover_image_public_bucket_path
? {
thumbnail: itemThumb,
}
: {}),
};
});
};
@@ -7,9 +7,6 @@ export const sidePanelVisible = signal(true);
export const dndSidePanelWidth = signal(-1);
export const selectedTab = signal<Tab | null>(null);
export const lastSelectedTab = signal<Tab | null>(null);
export const cameraFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const sidePanelHeight = computed(() => {