mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 17:08:13 +00:00
Feature/ Tabs improvement 2.3 (#605)
- Rebased to take the filter object from bombay - New Object Upload Tab available in Film Set Tab and Object Tab - Character Tab now uses the generic uploader because previewing and taking a snapshot of a character is too much of a code challenge atm. - added the dev-only feature tag, expressions uploads is now dev-only - went over acceptable file types and took out refreshed all the upload tabs - realign the code so all upload tabs uses the exact same logic, interfaces, and code structure, for better legibility this PR eliminates: https://github.com/storytold/GottaGoFast/pull/600 https://github.com/storytold/GottaGoFast/pull/598 --------- Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
1 parent
d09837387f
commit
dcec252f47
38 files changed
+736
-1497
No files matched your search
@@ -1,48 +0,0 @@
|
||||
.fy-file-wrapper {
|
||||
// flex-grow: 1;
|
||||
// height: calc(100% - 4px);
|
||||
|
||||
.file-picker-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fy-file-wrapper-input {
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
width: 0;
|
||||
// overflow: hidden;
|
||||
position: absolute;
|
||||
|
||||
+ label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.fy-uploader-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.fy-uploader-layout-icon {
|
||||
font-size: 2.625rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fy-file-dragging {
|
||||
background: rgba(#676781, 0.25);
|
||||
border-color: rgba(#fff, 0.33);
|
||||
}
|
||||
|
||||
.fy-uploader {
|
||||
[type="file"] {
|
||||
+ label {
|
||||
border: dashed 2px rgba(255, 255, 255, 0.12) !important;
|
||||
border-radius: 0.5rem;
|
||||
transition: 0.1s all;
|
||||
&:hover {
|
||||
// background-color: lighten($panel, 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { ReactNode, useId, useRef, useState } from "react";
|
||||
import { AssetType } from "~/enums";
|
||||
import { UploadModalObject } from "~/components";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface Props {
|
||||
render: (parentId: string) => ReactNode;
|
||||
fileTypes?: string[];
|
||||
onChange?: (file?: React.ChangeEvent) => void;
|
||||
onSuccess: () => void;
|
||||
panelClass?: string;
|
||||
type: AssetType;
|
||||
}
|
||||
|
||||
export function FileWrapper({
|
||||
render,
|
||||
fileTypes = [],
|
||||
onSuccess,
|
||||
type,
|
||||
...rest
|
||||
}: Props) {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [file, setFile] = useState<File | undefined>();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [resetModal, setResetModal] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const id = "file-input-" + useId();
|
||||
const accept = fileTypes
|
||||
.map((fileType) => `.${fileType.toLowerCase()}`)
|
||||
.join(",");
|
||||
|
||||
const clearFile = () => {
|
||||
if (fileRef.current !== null) {
|
||||
fileRef.current.value = "";
|
||||
setFile(undefined);
|
||||
}
|
||||
};
|
||||
const closeModal = () => setModalOpen(false);
|
||||
|
||||
const fileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setResetModal(true);
|
||||
event.preventDefault();
|
||||
setFile(event.target?.files?.[0]);
|
||||
setModalOpen(true);
|
||||
};
|
||||
const onDragDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const onDragEvent =
|
||||
(onOff: boolean) =>
|
||||
(event: React.DragEvent<HTMLDivElement>): void => {
|
||||
setDragging(onOff);
|
||||
onDragDrop(event);
|
||||
};
|
||||
const onDrop = (event: React.DragEvent<HTMLDivElement>): void => {
|
||||
setDragging(false);
|
||||
onDragDrop(event);
|
||||
if (event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
setResetModal(true);
|
||||
setFile(event.dataTransfer.files[0]);
|
||||
setModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-full flex-col gap-3.5 overflow-y-auto",
|
||||
dragging ? "border-white/33 border bg-ui-controls-button" : "",
|
||||
)}
|
||||
onDragLeave={onDragEvent(false)}
|
||||
onDragOver={onDragEvent(true)}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<input
|
||||
accept={accept}
|
||||
className="absolute hidden h-0 w-0"
|
||||
onChange={fileChange}
|
||||
type="file"
|
||||
id={id}
|
||||
ref={fileRef}
|
||||
{...rest}
|
||||
/>
|
||||
{render(id)}
|
||||
<UploadModalObject
|
||||
closeModal={closeModal}
|
||||
file={file!}
|
||||
isOpen={modalOpen}
|
||||
onClose={clearFile}
|
||||
onSuccess={onSuccess}
|
||||
resetModal={resetModal}
|
||||
setResetModal={setResetModal}
|
||||
type={type}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function LoadingIcon() {
|
||||
return <>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm ms-3"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</>;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./FileWrapper";
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Button, H6, Input, ListDropdown } from "~/components";
|
||||
import { FileUploader } from "./FileUploader";
|
||||
import { useState } from "react";
|
||||
import { MediaFileAnimationType } from "~/enums";
|
||||
import {
|
||||
THUMBNAILS_FILE_TYPE,
|
||||
MediaFileAnimationType,
|
||||
FilterEngineCategories,
|
||||
} from "~/enums";
|
||||
import { uploadAssets } from "./uploadAssets";
|
||||
import { UploaderState } from "~/models";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
engineCategory: FilterEngineCategories;
|
||||
fileTypes: string[];
|
||||
onClose: () => void;
|
||||
options?: {
|
||||
@@ -12,21 +19,16 @@ interface Props {
|
||||
hasLength?: boolean;
|
||||
hasThumbnailUpload?: boolean;
|
||||
};
|
||||
onSubmit: (options: {
|
||||
title: string;
|
||||
typeOption?: MediaFileAnimationType;
|
||||
assetFile: File;
|
||||
length: number;
|
||||
thumbnailFile: File | null;
|
||||
}) => void;
|
||||
onUploadProgress: (newState: UploaderState) => void;
|
||||
}
|
||||
|
||||
export const UploadFiles = ({
|
||||
fileTypes,
|
||||
engineCategory,
|
||||
onClose,
|
||||
title,
|
||||
options,
|
||||
onSubmit,
|
||||
onUploadProgress,
|
||||
}: Props) => {
|
||||
const fileSubtypes = options?.fileSubtypes;
|
||||
const hasLength = options?.hasLength;
|
||||
@@ -69,12 +71,15 @@ export const UploadFiles = ({
|
||||
}));
|
||||
return;
|
||||
}
|
||||
onSubmit({
|
||||
|
||||
uploadAssets({
|
||||
title: uploadTitle.value,
|
||||
assetFile: assetFile.value,
|
||||
thumbnailFile: thumbnailFile,
|
||||
length: uploadLength ?? 1000,
|
||||
typeOption,
|
||||
engineCategory: engineCategory,
|
||||
animationType: typeOption,
|
||||
thumbnailFile: thumbnailFile || undefined,
|
||||
length: uploadLength,
|
||||
progressCallback: onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -118,7 +123,7 @@ export const UploadFiles = ({
|
||||
{hasThumbnailUpload && (
|
||||
<FileUploader
|
||||
title="Upload Thumbnail (optional)"
|
||||
fileTypes={["PNG", "JGP", "GIF"]}
|
||||
fileTypes={Object.values(THUMBNAILS_FILE_TYPE)}
|
||||
file={thumbnailFile}
|
||||
setFile={setThumbnailFile}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { TransitionDialogue, LoadingDots } from "~/components";
|
||||
|
||||
import { UploadAssetError } from "./UploadAssetError";
|
||||
import { UploadSuccess } from "./UploadSuccess";
|
||||
import { UploadFiles } from "./UploadFiles";
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
MediaFileAnimationType,
|
||||
UploaderState,
|
||||
} from "~/enums";
|
||||
import { getFileName } from "~/utilities";
|
||||
|
||||
import { FilterEngineCategories, UploaderStates } from "~/enums";
|
||||
import { UploaderState, initialUploaderState } from "~/models";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -28,10 +23,6 @@ interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
status: UploaderState.ready,
|
||||
};
|
||||
|
||||
export function UploadModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -41,13 +32,15 @@ export function UploadModal({
|
||||
type,
|
||||
options,
|
||||
}: Props) {
|
||||
const [uploaderState, setUploaderState] = useState<{
|
||||
status: UploaderState;
|
||||
errorMessage?: string;
|
||||
}>(initialState);
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setUploaderState(initialState);
|
||||
setUploaderState(initialUploaderState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -56,98 +49,29 @@ export function UploadModal({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const onSubmit = async ({
|
||||
title,
|
||||
typeOption,
|
||||
assetFile,
|
||||
length,
|
||||
thumbnailFile,
|
||||
}: {
|
||||
title: string;
|
||||
typeOption?: MediaFileAnimationType;
|
||||
assetFile: File;
|
||||
length: number;
|
||||
thumbnailFile: File | null;
|
||||
}) => {
|
||||
setUploaderState({ status: UploaderState.uploadingAsset });
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
|
||||
file: assetFile,
|
||||
fileName: assetFile.name,
|
||||
engine_category: type,
|
||||
maybe_animation_type: typeOption,
|
||||
maybe_duration_millis: length,
|
||||
maybe_title: title,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
|
||||
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:
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<UploadFiles
|
||||
title={title}
|
||||
engineCategory={type}
|
||||
fileTypes={fileTypes}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
);
|
||||
case UploaderState.uploadingAsset:
|
||||
case UploaderState.uploadingCover:
|
||||
case UploaderState.settingCover:
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
case UploaderStates.settingCover:
|
||||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderState.success:
|
||||
case UploaderStates.success:
|
||||
return (
|
||||
<UploadSuccess
|
||||
title={title}
|
||||
@@ -157,7 +81,7 @@ export function UploadModal({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case UploaderState.assetError:
|
||||
case UploaderStates.assetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
@@ -168,8 +92,8 @@ export function UploadModal({
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
case UploaderState.coverCreateError:
|
||||
case UploaderState.coverSetError:
|
||||
case UploaderStates.coverCreateError:
|
||||
case UploaderStates.coverSetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
MediaFileAnimationType,
|
||||
UploaderStates,
|
||||
} from "~/enums";
|
||||
|
||||
import { getFileName } from "~/utilities";
|
||||
|
||||
import { MediaUploadApi, MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { UploaderState } from "~/models";
|
||||
|
||||
export const uploadAssets = async ({
|
||||
title,
|
||||
assetFile,
|
||||
thumbnailFile,
|
||||
engineCategory,
|
||||
animationType,
|
||||
length,
|
||||
progressCallback,
|
||||
}: {
|
||||
title: string;
|
||||
assetFile: File;
|
||||
engineCategory: FilterEngineCategories;
|
||||
thumbnailFile?: File;
|
||||
animationType?: MediaFileAnimationType;
|
||||
length?: number;
|
||||
progressCallback: (newState: UploaderState) => void;
|
||||
}) => {
|
||||
progressCallback({ status: UploaderStates.uploadingAsset });
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
|
||||
file: assetFile,
|
||||
fileName: assetFile.name,
|
||||
engine_category: engineCategory,
|
||||
maybe_animation_type: animationType,
|
||||
maybe_duration_millis: length,
|
||||
maybe_title: title,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
|
||||
if (!assetReponse.success || !assetReponse.data) {
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: assetReponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const assetToken = assetReponse.data;
|
||||
if (!thumbnailFile) {
|
||||
progressCallback({ status: UploaderStates.success });
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.uploadingCover });
|
||||
const thumbnailResponse = await mediaUploadApi.UploadImage({
|
||||
uuid: uuidv4(),
|
||||
blob: thumbnailFile,
|
||||
fileName: getFileName(thumbnailFile),
|
||||
maybe_title: "thumbnail_" + title,
|
||||
});
|
||||
if (!thumbnailResponse.success || !thumbnailResponse.data) {
|
||||
progressCallback({
|
||||
status: UploaderStates.coverCreateError,
|
||||
errorMessage: thumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.settingCover });
|
||||
const thumbnailToken = thumbnailResponse.data;
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
|
||||
mediaFileToken: assetToken,
|
||||
imageToken: thumbnailToken,
|
||||
});
|
||||
if (!setThumbnailResponse.success) {
|
||||
progressCallback({
|
||||
status: UploaderStates.coverSetError,
|
||||
errorMessage: setThumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
progressCallback({ status: UploaderStates.success });
|
||||
};
|
||||
+42
-49
@@ -1,40 +1,31 @@
|
||||
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";
|
||||
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
|
||||
import { upload3DObjects } from "./utilities/upload3DObjects";
|
||||
import { UploaderState } from "~/models";
|
||||
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;
|
||||
onClose: () => void;
|
||||
onUploadProgress: (newState: UploaderState) => void;
|
||||
}
|
||||
|
||||
export const UploadFiles3D = ({
|
||||
fileTypes,
|
||||
onClose,
|
||||
title,
|
||||
fileTypes,
|
||||
options,
|
||||
onSubmit,
|
||||
onClose,
|
||||
onUploadProgress,
|
||||
}: 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) {
|
||||
@@ -42,13 +33,16 @@ export const UploadFiles3D = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [typeOption, setTypeOption] = useState<
|
||||
MediaFileAnimationType | undefined
|
||||
>(
|
||||
const fileSubtypes = options?.fileSubtypes;
|
||||
// const hasLength = options?.hasLength;
|
||||
// const hasThumbnailUpload = options?.hasThumbnailUpload;
|
||||
|
||||
const [subtype, setSubtype] = useState<MediaFileAnimationType | undefined>(
|
||||
fileSubtypes
|
||||
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const [uploadTitle, setUploadTitle] = useState<{
|
||||
value: string;
|
||||
error?: string;
|
||||
@@ -66,8 +60,9 @@ export const UploadFiles3D = ({
|
||||
message?: string;
|
||||
}>({ type: "init" });
|
||||
|
||||
const [uploadLength, setUploadLength] = useState<number>();
|
||||
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||
const [thumbnailFile, setThumbnailFile] = useState<Blob | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!uploadTitle.value) {
|
||||
@@ -86,12 +81,12 @@ export const UploadFiles3D = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
upload3DObjects({
|
||||
title: uploadTitle.value,
|
||||
assetFile: assetFile.value,
|
||||
thumbnailFile: thumbnailFile,
|
||||
length: uploadLength ?? 1000,
|
||||
typeOption,
|
||||
thumbnailSnapshot: thumbnailFile,
|
||||
animationType: subtype,
|
||||
progressCallback: onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,6 +102,19 @@ export const UploadFiles3D = ({
|
||||
}
|
||||
}, [assetFile.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewStatus.type === "OK" && canvasRef.current) {
|
||||
snapshotCanvasAsThumbnail({
|
||||
targetNode: canvasRef.current!,
|
||||
resultCallback: (snapshotBlob) => {
|
||||
if (snapshotBlob) {
|
||||
setThumbnailFile(snapshotBlob);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [previewStatus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
@@ -117,20 +125,7 @@ export const UploadFiles3D = ({
|
||||
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}
|
||||
@@ -141,6 +136,12 @@ export const UploadFiles3D = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{fileSubtypes && fileSubtypes.length > 1 && (
|
||||
<ListDropdown
|
||||
list={fileSubtypes}
|
||||
onSelect={(value) => setSubtype(value as MediaFileAnimationType)}
|
||||
/>
|
||||
)}
|
||||
{assetFile.error && (
|
||||
<H6 className="z-10 text-red">{assetFile.error}</H6>
|
||||
)}
|
||||
@@ -163,14 +164,6 @@ export const UploadFiles3D = ({
|
||||
)}
|
||||
</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
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TransitionDialogue, LoadingDots } from "~/components";
|
||||
|
||||
import { UploadAssetError } from "../UploadModal/UploadAssetError";
|
||||
import { UploadSuccess } from "../UploadModal/UploadSuccess";
|
||||
import { UploadFiles3D } from "./UploadFiles3D";
|
||||
import { FilterEngineCategories, UploaderStates } from "~/enums";
|
||||
import { initialUploaderState, UploaderState } from "~/models";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
options?: {
|
||||
fileSubtypes?: { [key: string]: string }[];
|
||||
hasLength?: boolean;
|
||||
hasThumbnailUpload?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadModal3D({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
fileTypes,
|
||||
options,
|
||||
}: Props) {
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setUploaderState(initialUploaderState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
resetModalState();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const UploaderModalContent = () => {
|
||||
switch (uploaderState.status) {
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<UploadFiles3D
|
||||
title={title}
|
||||
fileTypes={fileTypes}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
);
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
case UploaderStates.settingCover:
|
||||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderStates.success:
|
||||
return (
|
||||
<UploadSuccess
|
||||
title={title}
|
||||
onOk={() => {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case UploaderStates.assetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={FilterEngineCategories.OBJECT}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
case UploaderStates.coverCreateError:
|
||||
case UploaderStates.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 "./UploadModal3D";
|
||||
+1
@@ -1,2 +1,3 @@
|
||||
export * from "./loadPreviewOnCanvas";
|
||||
export * from "./thumbnailHelpers";
|
||||
export * from "./uploadAsset";
|
||||
+12
-26
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
|
||||
@@ -46,8 +45,6 @@ export const loadPreviewOnCanvas = ({
|
||||
// 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 (
|
||||
@@ -87,7 +84,6 @@ const glbLoader = ({
|
||||
loader.load(
|
||||
URL.createObjectURL(file),
|
||||
(data) => {
|
||||
console.log("preview object loader: onLoaded");
|
||||
data.scene.children.forEach((child) => {
|
||||
child.userData["color"] = "#FFFFFF";
|
||||
scene.add(child);
|
||||
@@ -120,6 +116,10 @@ const glbLoader = ({
|
||||
});
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
statusCallback({
|
||||
type: "OK",
|
||||
message: "Preview should be available",
|
||||
});
|
||||
});
|
||||
},
|
||||
undefined, // nothing to-do onProgress
|
||||
@@ -133,28 +133,6 @@ const glbLoader = ({
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -186,6 +164,10 @@ const pmdLoader = ({
|
||||
textMesh.position.set(-22, -5, 0);
|
||||
scene.add(textMesh);
|
||||
renderer.render(scene, camera);
|
||||
statusCallback({
|
||||
type: "OK",
|
||||
message: "Preview should be available",
|
||||
});
|
||||
},
|
||||
undefined, // nothing on Progress
|
||||
(loaderError) => {
|
||||
@@ -223,4 +205,8 @@ const imagePlaneLoader = ({ file, scene, statusCallback }: LoaderInterface) => {
|
||||
obj.receiveShadow = true;
|
||||
obj.castShadow = true;
|
||||
scene.add(obj);
|
||||
statusCallback({
|
||||
type: "OK",
|
||||
message: "Preview should be available",
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { getFileName } from "~/utilities";
|
||||
|
||||
export const snapshotCanvasAsThumbnail = async ({
|
||||
targetNode,
|
||||
resultCallback,
|
||||
}: {
|
||||
targetNode: HTMLCanvasElement | undefined;
|
||||
resultCallback: (blob: Blob | undefined) => void;
|
||||
}) => {
|
||||
if (!targetNode) {
|
||||
//TODO: Verbose Error
|
||||
return undefined;
|
||||
}
|
||||
targetNode.toBlob((blob: Blob | null) => {
|
||||
resultCallback(blob || undefined);
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadSnapshotAsThumbnail = async ({
|
||||
assetTitle = "unknown_asset",
|
||||
blob,
|
||||
}: {
|
||||
assetTitle?: string;
|
||||
blob: Blob;
|
||||
}) => {
|
||||
const thumbnailFile = new File([blob], "storyteller-cap.png");
|
||||
|
||||
const thumbnailTitle = "thumbnail_" + assetTitle;
|
||||
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
return await mediaUploadApi.UploadImage({
|
||||
uuid: uuidv4(),
|
||||
blob: thumbnailFile,
|
||||
fileName: getFileName(thumbnailFile),
|
||||
maybe_title: thumbnailTitle,
|
||||
});
|
||||
};
|
||||
|
||||
export const setThumbnail = async ({
|
||||
assetToken,
|
||||
thumbnailToken,
|
||||
}: {
|
||||
assetToken: string;
|
||||
thumbnailToken: string;
|
||||
}) => {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
return await mediaFilesApi.UpdateCoverImage({
|
||||
mediaFileToken: assetToken,
|
||||
imageToken: thumbnailToken,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { MediaFileAnimationType, UploaderStates } from "~/enums";
|
||||
import { setThumbnail, uploadSnapshotAsThumbnail } from "./thumbnailHelpers";
|
||||
|
||||
import { uploadAsset } from "./uploadAsset";
|
||||
import { UploaderState } from "~/models";
|
||||
|
||||
export const upload3DObjects = async ({
|
||||
title,
|
||||
assetFile,
|
||||
thumbnailSnapshot,
|
||||
animationType,
|
||||
progressCallback,
|
||||
}: {
|
||||
title: string;
|
||||
assetFile: File;
|
||||
thumbnailSnapshot: Blob | undefined;
|
||||
animationType?: MediaFileAnimationType;
|
||||
progressCallback: (newState: UploaderState) => void;
|
||||
}) => {
|
||||
progressCallback({ status: UploaderStates.uploadingAsset });
|
||||
|
||||
const assetReponse = await uploadAsset({
|
||||
file: assetFile,
|
||||
title: title,
|
||||
animationType: animationType,
|
||||
});
|
||||
|
||||
if (!assetReponse.success || !assetReponse.data) {
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: assetReponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const assetToken = assetReponse.data;
|
||||
|
||||
if (!thumbnailSnapshot) {
|
||||
progressCallback({ status: UploaderStates.success });
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.uploadingCover });
|
||||
const thumbnailResponse = await uploadSnapshotAsThumbnail({
|
||||
assetTitle: title,
|
||||
blob: thumbnailSnapshot,
|
||||
});
|
||||
if (!thumbnailResponse.success || !thumbnailResponse.data) {
|
||||
progressCallback({
|
||||
status: UploaderStates.coverCreateError,
|
||||
errorMessage: thumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.settingCover });
|
||||
const thumbnailToken = thumbnailResponse.data;
|
||||
const setThumbnailResponse = await setThumbnail({
|
||||
assetToken: assetToken,
|
||||
thumbnailToken: thumbnailToken,
|
||||
});
|
||||
if (!setThumbnailResponse.success) {
|
||||
progressCallback({
|
||||
status: UploaderStates.coverSetError,
|
||||
errorMessage: setThumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
progressCallback({ status: UploaderStates.success });
|
||||
};
|
||||
+12
-13
@@ -25,24 +25,23 @@ export const uploadAsset = async ({
|
||||
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(),
|
||||
});
|
||||
// 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(),
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./UploadModal3DPreview";
|
||||
@@ -1,44 +1,23 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { MediaFileAnimationType } from "~/enums";
|
||||
|
||||
import { Button, H6, Input, ListDropdown } from "~/components";
|
||||
import { Button, H6, Input } from "~/components";
|
||||
import { FileUploader } from "../UploadModal/FileUploader";
|
||||
import { uploadImagePlane } from "./uploadImagePlane";
|
||||
import { UploaderState } from "~/models";
|
||||
|
||||
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;
|
||||
onUploadProgress: (newState: UploaderState) => void;
|
||||
}
|
||||
|
||||
export const UploadFilesImages = ({
|
||||
fileTypes,
|
||||
onClose,
|
||||
title,
|
||||
options,
|
||||
onSubmit,
|
||||
onUploadProgress,
|
||||
}: Props) => {
|
||||
const fileSubtypes = options?.fileSubtypes;
|
||||
|
||||
const [typeOption, setTypeOption] = useState<
|
||||
MediaFileAnimationType | undefined
|
||||
>(
|
||||
fileSubtypes
|
||||
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
|
||||
: undefined,
|
||||
);
|
||||
const [uploadTitle, setUploadTitle] = useState<{
|
||||
value: string;
|
||||
error?: string;
|
||||
@@ -68,10 +47,10 @@ export const UploadFilesImages = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
uploadImagePlane({
|
||||
title: uploadTitle.value,
|
||||
assetFile: assetFile.value,
|
||||
typeOption,
|
||||
progressCallback: onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -85,12 +64,6 @@ export const UploadFilesImages = ({
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
<FileUploader
|
||||
title={title}
|
||||
fileTypes={fileTypes}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
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 { UploadFilesImages } from "./UploadFilesImages";
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
|
||||
|
||||
import { getFileName } from "~/utilities";
|
||||
import { FilterEngineCategories, UploaderStates } from "~/enums";
|
||||
import { initialUploaderState, UploaderState } from "~/models";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -17,43 +14,26 @@ interface Props {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
type: FilterEngineCategories;
|
||||
options?: {
|
||||
fileSubtypes?: { [key: string]: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
enum UploaderState {
|
||||
ready,
|
||||
uploadingAsset,
|
||||
uploadingCover,
|
||||
settingCover,
|
||||
success,
|
||||
assetError,
|
||||
coverCreateError,
|
||||
coverSetError,
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
status: UploaderState.ready,
|
||||
};
|
||||
|
||||
export function UploadModalImages({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
fileTypes,
|
||||
type,
|
||||
options,
|
||||
}: Props) {
|
||||
const [uploaderState, setUploaderState] = useState<{
|
||||
status: UploaderState;
|
||||
errorMessage?: string;
|
||||
}>(initialState);
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
};
|
||||
const resetModalState = () => {
|
||||
setUploaderState(initialState);
|
||||
setUploaderState(initialUploaderState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,87 +42,27 @@ export function UploadModalImages({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const onSubmit = async ({
|
||||
title,
|
||||
assetFile,
|
||||
}: {
|
||||
title: string;
|
||||
typeOption?: MediaFileAnimationType;
|
||||
assetFile: File;
|
||||
thumbnailFile: File | null;
|
||||
}) => {
|
||||
setUploaderState({ status: UploaderState.uploadingAsset });
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
|
||||
file: assetFile,
|
||||
fileName: assetFile.name,
|
||||
engine_category: FilterEngineCategories.IMAGE_PLANE,
|
||||
maybe_title: title,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
|
||||
if (!assetReponse.success || !assetReponse.data) {
|
||||
setUploaderState({
|
||||
status: UploaderState.assetError,
|
||||
errorMessage: assetReponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setUploaderState({ status: UploaderState.uploadingCover });
|
||||
const thumbnailResponse = await mediaUploadApi.UploadImage({
|
||||
uuid: uuidv4(),
|
||||
blob: assetFile,
|
||||
fileName: getFileName(assetFile),
|
||||
maybe_title: "thumbnail_" + title,
|
||||
});
|
||||
if (!thumbnailResponse.success || !thumbnailResponse.data) {
|
||||
setUploaderState({
|
||||
status: UploaderState.coverCreateError,
|
||||
errorMessage: thumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setUploaderState({ status: UploaderState.settingCover });
|
||||
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
|
||||
mediaFileToken: assetReponse.data,
|
||||
imageToken: thumbnailResponse.data,
|
||||
});
|
||||
if (!setThumbnailResponse.success) {
|
||||
setUploaderState({
|
||||
status: UploaderState.coverSetError,
|
||||
errorMessage: setThumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setUploaderState({ status: UploaderState.success });
|
||||
};
|
||||
|
||||
const UploaderModalContent = () => {
|
||||
switch (uploaderState.status) {
|
||||
case UploaderState.ready:
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<UploadFilesImages
|
||||
title={title}
|
||||
fileTypes={fileTypes}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
);
|
||||
case UploaderState.uploadingAsset:
|
||||
case UploaderState.uploadingCover:
|
||||
case UploaderState.settingCover:
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
case UploaderStates.settingCover:
|
||||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderState.success:
|
||||
case UploaderStates.success:
|
||||
return (
|
||||
<UploadSuccess
|
||||
title={title}
|
||||
@@ -152,19 +72,19 @@ export function UploadModalImages({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case UploaderState.assetError:
|
||||
case UploaderStates.assetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={type}
|
||||
type={FilterEngineCategories.IMAGE_PLANE}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
case UploaderState.coverCreateError:
|
||||
case UploaderState.coverSetError:
|
||||
case UploaderStates.coverCreateError:
|
||||
case UploaderStates.coverSetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { UploaderStates, FilterEngineCategories } from "~/enums";
|
||||
import { MediaUploadApi, MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { getFileName } from "~/utilities";
|
||||
import { UploaderState } from "~/models";
|
||||
|
||||
export const uploadImagePlane = async ({
|
||||
title,
|
||||
assetFile,
|
||||
progressCallback,
|
||||
}: {
|
||||
title: string;
|
||||
assetFile: File;
|
||||
progressCallback: (newState: UploaderState) => void;
|
||||
}) => {
|
||||
progressCallback({ status: UploaderStates.uploadingAsset });
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
|
||||
file: assetFile,
|
||||
fileName: assetFile.name,
|
||||
engine_category: FilterEngineCategories.IMAGE_PLANE,
|
||||
maybe_title: title,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
|
||||
if (!assetReponse.success || !assetReponse.data) {
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: assetReponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.uploadingCover });
|
||||
const thumbnailResponse = await mediaUploadApi.UploadImage({
|
||||
uuid: uuidv4(),
|
||||
blob: assetFile,
|
||||
fileName: getFileName(assetFile),
|
||||
maybe_title: "thumbnail_" + title,
|
||||
});
|
||||
if (!thumbnailResponse.success || !thumbnailResponse.data) {
|
||||
progressCallback({
|
||||
status: UploaderStates.coverCreateError,
|
||||
errorMessage: thumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.settingCover });
|
||||
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
|
||||
mediaFileToken: assetReponse.data,
|
||||
imageToken: thumbnailResponse.data,
|
||||
});
|
||||
if (!setThumbnailResponse.success) {
|
||||
progressCallback({
|
||||
status: UploaderStates.coverSetError,
|
||||
errorMessage: setThumbnailResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
progressCallback({ status: UploaderStates.success });
|
||||
};
|
||||
@@ -7,7 +7,6 @@ export * from "./ButtonIconSelect";
|
||||
export * from "./ButtonLink";
|
||||
export * from "./ConfirmationModal";
|
||||
export * from "./FileUploader";
|
||||
export * from "./FileWrapper"; //need to rid of scss
|
||||
export * from "./FilterButtons"; //this relies on a list of filters locally, could be more flexible
|
||||
export * from "./Gravatar";
|
||||
export * from "./Input"; //need to decouple hotkey signal
|
||||
@@ -26,7 +25,7 @@ export * from "./Tooltip";
|
||||
export * from "./TransitionDialogue"; //need to decouple hotkey signal
|
||||
export * from "./Typography";
|
||||
export * from "./UploadModal";
|
||||
export * from "./UploadModal3DPreview";
|
||||
export * from "./UploadModal3D";
|
||||
export * from "./UploadModalImages";
|
||||
export * from "./WaveformPlayer";
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
.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,542 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
TransitionDialogue,
|
||||
LoadingDots,
|
||||
} from "~/components";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
// import { MMDLoader } from "three/addons/loaders/MMDLoader.js";
|
||||
import { FontLoader } from "three/addons/loaders/FontLoader.js";
|
||||
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
FilterEngineCategories,
|
||||
MediaFileAnimationType,
|
||||
} from "~/enums";
|
||||
|
||||
import "./UploadModal.scss";
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { getFileExtension, getFileName } from "~/utilities";
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
file: File;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
isOpen: boolean;
|
||||
resetModal: boolean;
|
||||
setResetModal: (b: boolean) => void;
|
||||
type: AssetType;
|
||||
}
|
||||
|
||||
enum UploaderState {
|
||||
ready,
|
||||
uploadingAsset,
|
||||
uploadingCover,
|
||||
settingCover,
|
||||
success,
|
||||
assetError,
|
||||
coverCreateError,
|
||||
coverSetError,
|
||||
loaderError,
|
||||
}
|
||||
|
||||
export function UploadModalObject({
|
||||
closeModal,
|
||||
file,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
resetModal,
|
||||
setResetModal,
|
||||
type,
|
||||
}: 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<HTMLCanvasElement | null>(null);
|
||||
const [animationType, animationTypeSet] = useState<
|
||||
MediaFileAnimationType | undefined
|
||||
>(undefined);
|
||||
|
||||
const isCharacter = type === AssetType.CHARACTER;
|
||||
|
||||
const objPreviewRef = useCallback((node: HTMLCanvasElement) => {
|
||||
if (node !== null) {
|
||||
targetNodeSet(node);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetModalState = useCallback(() => {
|
||||
setResetModal(false);
|
||||
titleSet("");
|
||||
// visibilitySet("");
|
||||
objUploadStatusSet(UploaderState.ready);
|
||||
assetTokenSet("");
|
||||
coverTokenSet("");
|
||||
animationTypeSet(undefined);
|
||||
}, [setResetModal]);
|
||||
|
||||
useEffect(() => {
|
||||
// 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, 1, 0.1, 1000);
|
||||
camera.position.z = 2;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
canvas: targetNode,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setSize(300 || 0, 300 || 0);
|
||||
|
||||
const color = 0xfcece7;
|
||||
const light = new THREE.HemisphereLight(color, 0x8d8d8d, 3.0);
|
||||
|
||||
scene.add(light);
|
||||
|
||||
if (file) {
|
||||
if (file.name.includes(".glb")) {
|
||||
const loader = new GLTFLoader();
|
||||
loader.load(
|
||||
URL.createObjectURL(file),
|
||||
(glb) => {
|
||||
glb.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);
|
||||
});
|
||||
},
|
||||
() => {},
|
||||
(loaderError) => {
|
||||
console.log("loader error:", loaderError);
|
||||
objUploadStatusSet(UploaderState.loaderError);
|
||||
},
|
||||
);
|
||||
} else if (file.name.includes(".pmd")) {
|
||||
camera.position.z = 30;
|
||||
const loader = new FontLoader();
|
||||
loader.load(
|
||||
"https://threejs.org/examples/fonts/helvetiker_regular.typeface.json",
|
||||
function (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);
|
||||
},
|
||||
);
|
||||
} else if (
|
||||
file.name.includes(".png") ||
|
||||
file.name.includes(".jpg") ||
|
||||
file.name.includes(".jpeg") ||
|
||||
file.name.includes(".gif")
|
||||
) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
const animate = function () {
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
renderer.setAnimationLoop(animate);
|
||||
}
|
||||
}
|
||||
}, [file, targetNode, resetModal, resetModalState]);
|
||||
|
||||
// for visibility later
|
||||
|
||||
// const visiblityOpts = [
|
||||
// {
|
||||
// label: "public",
|
||||
// value: "public",
|
||||
// },
|
||||
// {
|
||||
// label: "private",
|
||||
// value: "private",
|
||||
// },
|
||||
// ];
|
||||
// const visiblityId = `fy-uploader-modal-visibility-${useId()}`;
|
||||
|
||||
const setCoverImage = async (assetToken: string, thumbnailToken: string) => {
|
||||
objUploadStatusSet(UploaderState.settingCover);
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
|
||||
mediaFileToken: assetToken,
|
||||
imageToken: thumbnailToken,
|
||||
});
|
||||
if (!setThumbnailResponse.success) {
|
||||
objUploadStatusSet(UploaderState.coverSetError);
|
||||
return;
|
||||
}
|
||||
objUploadStatusSet(UploaderState.success);
|
||||
};
|
||||
|
||||
const createCoverImage = async (assetToken: string) => {
|
||||
if (!targetNode) {
|
||||
//TODO: Verbose Error
|
||||
return;
|
||||
}
|
||||
await targetNode.toBlob(async (blob: Blob | null) => {
|
||||
if (!blob) {
|
||||
//TODO: Verbose Error
|
||||
return;
|
||||
}
|
||||
const thumbnailFile = new File([blob], "storyteller-cap.png");
|
||||
objUploadStatusSet(UploaderState.uploadingCover);
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const thumbnailResponse = await mediaUploadApi.UploadImage({
|
||||
uuid: uuidv4(),
|
||||
blob: thumbnailFile,
|
||||
fileName: getFileName(thumbnailFile),
|
||||
maybe_title: "thumbnail_" + title,
|
||||
});
|
||||
if (!thumbnailResponse.success || !thumbnailResponse.data) {
|
||||
objUploadStatusSet(UploaderState.coverCreateError);
|
||||
return;
|
||||
}
|
||||
coverTokenSet(thumbnailResponse.data);
|
||||
setCoverImage(assetToken, thumbnailResponse.data);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadAsset = async () => {
|
||||
objUploadStatusSet(UploaderState.uploadingAsset);
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const fileExtension = getFileExtension(file);
|
||||
console.log(fileExtension);
|
||||
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: isCharacter
|
||||
? FilterEngineCategories.CHARACTER
|
||||
: FilterEngineCategories.OBJECT,
|
||||
maybe_animation_type: animationType,
|
||||
maybe_title: title,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
if (!assetReponse.success || !assetReponse.data) {
|
||||
objUploadStatusSet(UploaderState.assetError);
|
||||
return;
|
||||
}
|
||||
|
||||
assetTokenSet(assetReponse.data);
|
||||
createCoverImage(assetReponse.data);
|
||||
};
|
||||
|
||||
const animationOptions = [
|
||||
{ label: "ARKit", value: MediaFileAnimationType.ArKit },
|
||||
{
|
||||
label: "MikuMikuDance",
|
||||
value: MediaFileAnimationType.MikuMikuDance,
|
||||
},
|
||||
{
|
||||
label: "MikuMikuDance ARKit",
|
||||
value: MediaFileAnimationType.MikuMikuDanceArKit,
|
||||
},
|
||||
{ label: "Mixamo", value: MediaFileAnimationType.Mixamo },
|
||||
{
|
||||
label: "Mixamo ARKit",
|
||||
value: MediaFileAnimationType.MixamoArKit,
|
||||
},
|
||||
{ label: "MocapNet", value: MediaFileAnimationType.MocapNet },
|
||||
{
|
||||
label: "MocapNet ARKit",
|
||||
value: MediaFileAnimationType.MocapNetArKit,
|
||||
},
|
||||
{ label: "Move AI", value: MediaFileAnimationType.MoveAi },
|
||||
{
|
||||
label: "Move AI ARKit",
|
||||
value: MediaFileAnimationType.MoveAiArKit,
|
||||
},
|
||||
{ label: "Rigify", value: MediaFileAnimationType.Rigify },
|
||||
{
|
||||
label: "Rigify ARKit",
|
||||
value: MediaFileAnimationType.MoveAiArKit,
|
||||
},
|
||||
];
|
||||
|
||||
const objUploaderContent = () => {
|
||||
switch (objUploadStatus) {
|
||||
case UploaderState.ready:
|
||||
return (
|
||||
<>
|
||||
<canvas
|
||||
{...{
|
||||
className: "object-preview-canvas m-auto",
|
||||
ref: objPreviewRef,
|
||||
}}
|
||||
></canvas>
|
||||
<Input
|
||||
{...{
|
||||
label: "Name",
|
||||
onChange: ({ target }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
titleSet(target.value),
|
||||
placeholder: "Enter a name",
|
||||
value: title,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isCharacter ? (
|
||||
<>
|
||||
<label
|
||||
className="mb-2 mt-3 block"
|
||||
htmlFor="upload-modal-animation-type-select"
|
||||
>
|
||||
Animation type
|
||||
</label>
|
||||
<Select
|
||||
options={animationOptions}
|
||||
onChange={(value: MediaFileAnimationType) => {
|
||||
animationTypeSet(value);
|
||||
}}
|
||||
placeholder="Select an animation type (optional)"
|
||||
value={animationType}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
{...{
|
||||
onClick: () => {
|
||||
closeModal();
|
||||
onClose();
|
||||
},
|
||||
variant: "secondary",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
{...{
|
||||
onClick: () => uploadAsset(),
|
||||
}}
|
||||
>
|
||||
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 your library`}</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
{...{
|
||||
onClick: () => {
|
||||
onSuccess();
|
||||
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",
|
||||
}}
|
||||
>
|
||||
Try another
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
{...{
|
||||
isOpen,
|
||||
onClose: () => {
|
||||
closeModal();
|
||||
onClose();
|
||||
resetModalState();
|
||||
},
|
||||
title: `Upload ${isCharacter ? "Character" : "Object"}`,
|
||||
}}
|
||||
>
|
||||
{objUploaderContent()}
|
||||
</TransitionDialogue>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
// from here https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
|
||||
|
||||
export const fitCameraToCenteredObject = function (
|
||||
camera: THREE.PerspectiveCamera,
|
||||
object: THREE.Object3D<THREE.Object3DEventMap>,
|
||||
offset?: number,
|
||||
orbitControls?: { target: THREE.Vector3; maxDistance: number },
|
||||
) {
|
||||
const boundingBox = new THREE.Box3();
|
||||
boundingBox.setFromObject(object);
|
||||
|
||||
const size = new THREE.Vector3();
|
||||
boundingBox.getSize(size);
|
||||
|
||||
// figure out how to fit the box in the view:
|
||||
// 1. figure out horizontal FOV (on non-1.0 aspects)
|
||||
// 2. figure out distance from the object in X and Y planes
|
||||
// 3. select the max distance (to fit both sides in)
|
||||
//
|
||||
// The reason is as follows:
|
||||
//
|
||||
// Imagine a bounding box (BB) is centered at (0,0,0).
|
||||
// Camera has vertical FOV (camera.fov) and horizontal FOV
|
||||
// (camera.fov scaled by aspect, see fovh below)
|
||||
//
|
||||
// Therefore if you want to put the entire object into the field of view,
|
||||
// you have to compute the distance as: z/2 (half of Z size of the BB
|
||||
// protruding towards us) plus for both X and Y size of BB you have to
|
||||
// figure out the distance created by the appropriate FOV.
|
||||
//
|
||||
// The FOV is always a triangle:
|
||||
//
|
||||
// (size/2)
|
||||
// +--------+
|
||||
// | /
|
||||
// | /
|
||||
// | /
|
||||
// | F° /
|
||||
// | /
|
||||
// | /
|
||||
// | /
|
||||
// |/
|
||||
//
|
||||
// F° is half of respective FOV, so to compute the distance (the length
|
||||
// of the straight line) one has to: `size/2 / Math.tan(F)`.
|
||||
//
|
||||
// FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
|
||||
// the camera.fov is the vertical FOV.
|
||||
|
||||
const fov = camera.fov * (Math.PI / 180);
|
||||
const fovh = 2 * Math.atan(Math.tan(fov / 2) * camera.aspect);
|
||||
const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2));
|
||||
const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2));
|
||||
let cameraZ = Math.max(dx, dy);
|
||||
|
||||
// offset the camera, if desired (to avoid filling the whole canvas)
|
||||
if (offset !== undefined && offset !== 0) cameraZ *= offset;
|
||||
|
||||
camera.position.set(
|
||||
0,
|
||||
0.75, // this centers it vertically enough while testing, but probably needs to be dynamic
|
||||
cameraZ,
|
||||
);
|
||||
|
||||
// set the far plane of the camera so that it easily encompasses the whole object
|
||||
const minZ = boundingBox.min.z;
|
||||
const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ;
|
||||
|
||||
camera.far = cameraToFarEdge * 3;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
if (orbitControls !== undefined) {
|
||||
// set camera to rotate around the center
|
||||
orbitControls.target = new THREE.Vector3(0, 0, 0);
|
||||
|
||||
// prevent camera from zooming out far enough to create far plane cutoff
|
||||
orbitControls.maxDistance = cameraToFarEdge * 2;
|
||||
}
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./UploadModalObject";
|
||||
@@ -6,4 +6,3 @@ export * from "./PremiumLock";
|
||||
// all the Upload stuff will depend on the API Manager Signal
|
||||
export * from "./UploadAudioButtonDialogue";
|
||||
export * from "./UploadAudioComponent";
|
||||
export * from "./UploadModalObject"; //need to rid of scss
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum FeatureFlags {
|
||||
DEV_ONLY = "dev-only",
|
||||
SHOW_SEARCH_OBJECTS = "show-search-objects",
|
||||
SHOW_IMAGEPLANE_TAB = "show-imageplane-tab",
|
||||
SHOW_SETS_TAB = "show-sets-tab",
|
||||
|
||||
+17
-2
@@ -1,3 +1,8 @@
|
||||
export enum ANIMATION_FILE_TYPE {
|
||||
GLB = "GLB",
|
||||
VMD = "VMD",
|
||||
}
|
||||
|
||||
export enum AUDIO_FILE_TYPE {
|
||||
MP3 = "MP3",
|
||||
WAV = "WAV",
|
||||
@@ -5,7 +10,13 @@ export enum AUDIO_FILE_TYPE {
|
||||
OGG = "OGG",
|
||||
}
|
||||
|
||||
export enum IMAGE_FILE_TYPE {
|
||||
export enum IMAGEPLANE_FILE_TYPE {
|
||||
PNG = "PNG",
|
||||
JPG = "JPG",
|
||||
JPEG = "JPEG",
|
||||
}
|
||||
|
||||
export enum THUMBNAILS_FILE_TYPE {
|
||||
PNG = "PNG",
|
||||
JPG = "JPG",
|
||||
JPEG = "JPEG",
|
||||
@@ -14,5 +25,9 @@ export enum IMAGE_FILE_TYPE {
|
||||
|
||||
export enum OBJECT_FILE_TYPE {
|
||||
GLB = "GLB",
|
||||
FBX = "FBX",
|
||||
}
|
||||
|
||||
export enum CHARACTER_FILE_TYPE {
|
||||
PMD = "PMD",
|
||||
ZIP = "ZIP",
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum UploaderState {
|
||||
export enum UploaderStates {
|
||||
ready,
|
||||
uploadingAsset,
|
||||
uploadingCover,
|
||||
+1
-1
@@ -12,5 +12,5 @@ export * from "./MediaFileAnimationTypes";
|
||||
export * from "./QueryFilters";
|
||||
export * from "./Tabs";
|
||||
export * from "./toasts";
|
||||
export * from "./UploaderState";
|
||||
export * from "./UploaderStates";
|
||||
export * from "./Visibility";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { UploaderStates } from "~/enums";
|
||||
|
||||
export interface UploaderState {
|
||||
status: UploaderStates;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const initialUploaderState = {
|
||||
status: UploaderStates.ready,
|
||||
};
|
||||
@@ -2,5 +2,6 @@ export * from "./Billing";
|
||||
export * from "./BetaKeys";
|
||||
export * from "./Job";
|
||||
export * from "./JobState";
|
||||
export * from "./UploaderState";
|
||||
export * from "./Users";
|
||||
export * from "./Weight";
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
ANIMATION_FILE_TYPE,
|
||||
AssetFilterOption,
|
||||
FeatureFlags,
|
||||
FilterEngineCategories,
|
||||
@@ -267,7 +268,7 @@ export const AnimationTab = () => {
|
||||
onClose={() => setOpen(false)}
|
||||
onSuccess={fetchUserAnimations}
|
||||
isOpen={open}
|
||||
fileTypes={["GLB", "FBX", "VMD"]}
|
||||
fileTypes={Object.values(ANIMATION_FILE_TYPE)}
|
||||
title="Upload Animation"
|
||||
options={{
|
||||
fileSubtypes: [
|
||||
|
||||
@@ -3,18 +3,19 @@ import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
AssetFilterOption,
|
||||
AssetType,
|
||||
CHARACTER_FILE_TYPE,
|
||||
FeatureFlags,
|
||||
FilterEngineCategories,
|
||||
MediaFileAnimationType,
|
||||
} from "~/enums";
|
||||
import { FetchStatus } from "~/pages/PageEnigma/enums";
|
||||
import { characterItems as demoCharacterItems } from "~/pages/PageEnigma/signals";
|
||||
import {
|
||||
Button,
|
||||
FileWrapper,
|
||||
FilterButtons,
|
||||
Pagination,
|
||||
SearchFilter,
|
||||
UploadModal,
|
||||
} from "~/components";
|
||||
import {
|
||||
ItemElements,
|
||||
@@ -37,6 +38,8 @@ export const CharactersTab = () => {
|
||||
FeatureFlags.SHOW_SEARCH_OBJECTS,
|
||||
);
|
||||
|
||||
const [openUploadModal, setOpenUploadModal] = useState(false);
|
||||
|
||||
const [searchTermFeatured, setSearchTermFeatured] = useState("");
|
||||
const [searchTermUser, setSearchTermUser] = useState("");
|
||||
|
||||
@@ -199,80 +202,90 @@ export const CharactersTab = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileWrapper
|
||||
onSuccess={fetchUserCharacters}
|
||||
type={AssetType.CHARACTER}
|
||||
render={(parentId) => (
|
||||
<>
|
||||
<TabTitle title="Characters" />
|
||||
<div>
|
||||
<FilterButtons
|
||||
value={selectedFilter}
|
||||
onClick={(button) => {
|
||||
setSelectedFilter(Number(button));
|
||||
setCurrentPage(0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3 px-4">
|
||||
<Button
|
||||
className="file-picker-button py-3"
|
||||
htmlFor={parentId}
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
>
|
||||
Upload Character
|
||||
</Button>
|
||||
{showSearchObjectComponent && (
|
||||
<SearchFilter
|
||||
searchTerm={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
: searchTermUser
|
||||
}
|
||||
onSearchChange={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? setSearchTermFeatured
|
||||
: setSearchTermUser
|
||||
}
|
||||
key={selectedFilter}
|
||||
placeholder={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? "Search featured characters"
|
||||
: "Search my characters"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
|
||||
<ItemElements
|
||||
busy={isFetching}
|
||||
debug="characters tab"
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
items={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
? featuredSearchResults ?? []
|
||||
: displayedItems
|
||||
: searchTermUser
|
||||
? userSearchResults ?? []
|
||||
: displayedItems
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
className="-mt-4 px-4"
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={(newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<TabTitle title="Characters" />
|
||||
<div>
|
||||
<FilterButtons
|
||||
value={selectedFilter}
|
||||
onClick={(button) => {
|
||||
setSelectedFilter(Number(button));
|
||||
setCurrentPage(0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3 px-4">
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
onClick={() => setOpenUploadModal(true)}
|
||||
className="w-full py-3 text-sm font-medium"
|
||||
>
|
||||
Upload Characters
|
||||
</Button>
|
||||
{showSearchObjectComponent && (
|
||||
<SearchFilter
|
||||
searchTerm={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
: searchTermUser
|
||||
}
|
||||
onSearchChange={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? setSearchTermFeatured
|
||||
: setSearchTermUser
|
||||
}
|
||||
key={selectedFilter}
|
||||
placeholder={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? "Search featured characters"
|
||||
: "Search my characters"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
|
||||
<ItemElements
|
||||
busy={isFetching}
|
||||
debug="characters tab"
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
items={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
? featuredSearchResults ?? []
|
||||
: displayedItems
|
||||
: searchTermUser
|
||||
? userSearchResults ?? []
|
||||
: displayedItems
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
className="-mt-4 px-4"
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={(newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<UploadModal
|
||||
onClose={() => setOpenUploadModal(false)}
|
||||
onSuccess={fetchUserCharacters}
|
||||
isOpen={openUploadModal}
|
||||
type={FilterEngineCategories.CHARACTER}
|
||||
options={{
|
||||
hasThumbnailUpload: true,
|
||||
fileSubtypes: [
|
||||
{ Mixamo: MediaFileAnimationType.Mixamo },
|
||||
{ MikuMikuDance: MediaFileAnimationType.MikuMikuDance },
|
||||
{ MoveAi: MediaFileAnimationType.MoveAi },
|
||||
{ Rigify: MediaFileAnimationType.Rigify },
|
||||
{ Rokoko: MediaFileAnimationType.Rokoko },
|
||||
],
|
||||
}}
|
||||
fileTypes={Object.values(CHARACTER_FILE_TYPE)}
|
||||
title="Upload Characters"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,8 @@ export const ExpressionTab = () => {
|
||||
FeatureFlags.SHOW_SEARCH_OBJECTS,
|
||||
);
|
||||
|
||||
const showUploadButton = usePosthogFeatureFlag(FeatureFlags.DEV_ONLY);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchTermFeatured, setSearchTermFeatured] = useState("");
|
||||
const [searchTermUser, setSearchTermUser] = useState("");
|
||||
@@ -197,14 +199,16 @@ export const ExpressionTab = () => {
|
||||
}}
|
||||
/>
|
||||
<div className="flex w-full flex-col gap-3 px-4">
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
onClick={() => setOpen(true)}
|
||||
className="w-full py-3 text-sm font-medium"
|
||||
>
|
||||
Upload Expression (Dev Only)
|
||||
</Button>
|
||||
{showUploadButton && (
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
onClick={() => setOpen(true)}
|
||||
className="w-full py-3 text-sm font-medium"
|
||||
>
|
||||
Upload Expression (Dev Only)
|
||||
</Button>
|
||||
)}
|
||||
{showSearchObjectComponent && (
|
||||
<SearchFilter
|
||||
searchTerm={
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AssetFilterOption,
|
||||
FeatureFlags,
|
||||
FilterEngineCategories,
|
||||
IMAGE_FILE_TYPE,
|
||||
IMAGEPLANE_FILE_TYPE,
|
||||
} from "~/enums";
|
||||
import { FetchStatus } from "~/pages/PageEnigma/enums";
|
||||
import {
|
||||
@@ -246,9 +246,8 @@ export const ImagePlanesTab = () => {
|
||||
onClose={() => setOpenUploadModal(false)}
|
||||
onSuccess={fetchUserImages}
|
||||
isOpen={openUploadModal}
|
||||
fileTypes={Object.values(IMAGE_FILE_TYPE)}
|
||||
fileTypes={Object.values(IMAGEPLANE_FILE_TYPE)}
|
||||
title="Upload Image Panels"
|
||||
type={FilterEngineCategories.IMAGE_PLANE}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
AssetFilterOption,
|
||||
AssetType,
|
||||
FeatureFlags,
|
||||
FilterEngineCategories,
|
||||
OBJECT_FILE_TYPE,
|
||||
} from "~/enums";
|
||||
import { FetchStatus } from "~/pages/PageEnigma/enums";
|
||||
import { shapeItems } from "~/pages/PageEnigma/signals";
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
|
||||
import {
|
||||
Button,
|
||||
FileWrapper,
|
||||
FilterButtons,
|
||||
SearchFilter,
|
||||
Pagination,
|
||||
UploadModal3D,
|
||||
} from "~/components";
|
||||
import {
|
||||
fetchFeaturedMediaItems,
|
||||
@@ -38,6 +38,8 @@ export const ObjectsTab = () => {
|
||||
FeatureFlags.SHOW_SEARCH_OBJECTS,
|
||||
);
|
||||
|
||||
const [openUploadModal, setOpenUploadModal] = useState(false);
|
||||
|
||||
const [searchTermFeatured, setSearchTermFeatured] = useState("");
|
||||
const [searchTermUser, setSearchTermUser] = useState("");
|
||||
|
||||
@@ -188,78 +190,79 @@ export const ObjectsTab = () => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<FileWrapper
|
||||
onSuccess={fetchUserObjects}
|
||||
type={AssetType.OBJECT}
|
||||
render={(parentId) => (
|
||||
<>
|
||||
<TabTitle title="Objects" />
|
||||
<FilterButtons
|
||||
value={selectedFilter}
|
||||
onClick={(button) => {
|
||||
setSelectedFilter(Number(button));
|
||||
setCurrentPage(0);
|
||||
}}
|
||||
<>
|
||||
<TabTitle title="Objects" />
|
||||
<FilterButtons
|
||||
value={selectedFilter}
|
||||
onClick={(button) => {
|
||||
setSelectedFilter(Number(button));
|
||||
setCurrentPage(0);
|
||||
}}
|
||||
/>
|
||||
<div className="flex w-full flex-col gap-3 px-4">
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
onClick={() => setOpenUploadModal(true)}
|
||||
className="w-full py-3 text-sm font-medium"
|
||||
>
|
||||
Upload Objects
|
||||
</Button>
|
||||
{showSearchObjectComponent && (
|
||||
<SearchFilter
|
||||
searchTerm={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
: searchTermUser
|
||||
}
|
||||
onSearchChange={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? setSearchTermFeatured
|
||||
: setSearchTermUser
|
||||
}
|
||||
key={selectedFilter}
|
||||
placeholder={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? "Search featured objects"
|
||||
: "Search my objects"
|
||||
}
|
||||
/>
|
||||
<div className="flex w-full flex-col gap-3 px-4">
|
||||
<Button
|
||||
className="file-picker-button py-3"
|
||||
htmlFor={parentId}
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
>
|
||||
Upload Object
|
||||
</Button>
|
||||
{showSearchObjectComponent && (
|
||||
<SearchFilter
|
||||
searchTerm={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
: searchTermUser
|
||||
}
|
||||
onSearchChange={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? setSearchTermFeatured
|
||||
: setSearchTermUser
|
||||
}
|
||||
key={selectedFilter}
|
||||
placeholder={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? "Search featured objects"
|
||||
: "Search my objects"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
|
||||
<ItemElements
|
||||
busy={isFetching}
|
||||
debug="objects tab"
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
items={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
? featuredSearchResults ?? []
|
||||
: displayedItems
|
||||
: searchTermUser
|
||||
? userSearchResults ?? []
|
||||
: displayedItems
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
className="-mt-4 px-4"
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={(newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
|
||||
<ItemElements
|
||||
busy={isFetching}
|
||||
debug="objects tab"
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
items={
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermFeatured
|
||||
? featuredSearchResults ?? []
|
||||
: displayedItems
|
||||
: searchTermUser
|
||||
? userSearchResults ?? []
|
||||
: displayedItems
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
className="-mt-4 px-4"
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={(newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<UploadModal3D
|
||||
onClose={() => setOpenUploadModal(false)}
|
||||
onSuccess={fetchUserObjects}
|
||||
isOpen={openUploadModal}
|
||||
fileTypes={Object.values(OBJECT_FILE_TYPE)}
|
||||
title="Upload Objects"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -3,9 +3,9 @@ import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import {
|
||||
AssetFilterOption,
|
||||
CHARACTER_FILE_TYPE,
|
||||
FeatureFlags,
|
||||
FilterEngineCategories,
|
||||
IMAGE_FILE_TYPE,
|
||||
OBJECT_FILE_TYPE,
|
||||
} from "~/enums";
|
||||
import { FetchStatus } from "~/pages/PageEnigma/enums";
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
FilterButtons,
|
||||
Pagination,
|
||||
SearchFilter,
|
||||
UploadModal3DPreview,
|
||||
UploadModal3D,
|
||||
} from "~/components";
|
||||
|
||||
import {
|
||||
@@ -111,7 +111,10 @@ export const SetsTab = () => {
|
||||
const fetchFeaturedSetObjects = useCallback(
|
||||
() =>
|
||||
fetchFeaturedMediaItems({
|
||||
filterEngineCategories: [FilterEngineCategories.OBJECT],
|
||||
filterEngineCategories: [
|
||||
FilterEngineCategories.OBJECT,
|
||||
FilterEngineCategories.IMAGE_PLANE,
|
||||
],
|
||||
setState: (newState: FetchMediaItemStates) => {
|
||||
setFeaturedFetch((curr) => ({
|
||||
status: newState.status,
|
||||
@@ -257,17 +260,15 @@ export const SetsTab = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<UploadModal3DPreview
|
||||
<UploadModal3D
|
||||
onClose={() => setOpenUploadModal(false)}
|
||||
onSuccess={fetchUserSetObjects}
|
||||
isOpen={openUploadModal}
|
||||
fileTypes={[
|
||||
...Object.values(OBJECT_FILE_TYPE),
|
||||
"PMD",
|
||||
...Object.values(IMAGE_FILE_TYPE),
|
||||
...Object.values(CHARACTER_FILE_TYPE),
|
||||
]}
|
||||
title="Upload Set Objects"
|
||||
type={FilterEngineCategories.OBJECT}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user