mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 08:28:18 +00:00
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:
1 parent
c4e937d677
commit
a8d0541c31
74 files changed
+1454
-377
No files matched your search
@@ -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}
|
||||
|
||||
File renamed without changes.
+4
-4
@@ -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>
|
||||
);
|
||||
};
|
||||
+8
-6
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+54
-30
@@ -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>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
};
|
||||
@@ -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";
|
||||
File renamed without changes.
+8
-9
@@ -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);
|
||||
+1
-1
@@ -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";
|
||||
@@ -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
|
||||
@@ -6,5 +6,4 @@ export enum AssetType {
|
||||
EXPRESSION = "expression",
|
||||
OBJECT = "object",
|
||||
SHAPE = "shape",
|
||||
STYLE = "style", // TODO Remove
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum FeatureFlags {
|
||||
SHOW_SEARCH_OBJECTS = "show-search-objects",
|
||||
SHOW_SETS_TAB = "show-sets-tab",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export enum UploaderState {
|
||||
ready,
|
||||
uploadingAsset,
|
||||
uploadingCover,
|
||||
settingCover,
|
||||
success,
|
||||
assetError,
|
||||
coverCreateError,
|
||||
coverSetError,
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
+1
-7
@@ -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";
|
||||
+16
-15
@@ -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}
|
||||
/>
|
||||
</>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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";
|
||||
+1
-1
@@ -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
-1
@@ -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";
|
||||
|
||||
+1
-1
@@ -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";
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5
-6
@@ -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>
|
||||
</>
|
||||
);
|
||||
+4
-2
@@ -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
-5
@@ -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}
|
||||
/>
|
||||
</>
|
||||
+4
-2
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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 {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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(() => {
|
||||
|
||||
Reference in new issue
Block a user