mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
Feature/upload movement (#330)
Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com>
This commit is contained in:
1 parent
a47e5767b7
commit
fa2fb38d0d
22 files changed
+787
-110
No files matched your search
@@ -19,7 +19,8 @@ export interface GetMediaParams {
|
||||
| "character"
|
||||
| "animation"
|
||||
| "object"
|
||||
| "skybox";
|
||||
| "skybox"
|
||||
| "expression";
|
||||
}
|
||||
|
||||
export const GetMediaByUser = MakeRequest<
|
||||
|
||||
@@ -6,6 +6,21 @@ export enum MediaFileEngineCategory {
|
||||
Animation = "animation",
|
||||
Object = "object",
|
||||
Skybox = "skybox",
|
||||
Expression = "expression",
|
||||
}
|
||||
|
||||
export enum MediaFileAnimationType {
|
||||
ar_kit = "ar_kit",
|
||||
miku_miku_dance = "miku_miku_dance",
|
||||
miku_miku_dance_ar_kit = "miku_miku_dance_ar_kit",
|
||||
mixamo = "mixamo",
|
||||
mixamo_ar_kit = "mixamo_ar_kit",
|
||||
mocap_net = "mocap_net",
|
||||
mocap_net_ar_kit = "mocap_net_ar_kit",
|
||||
move_ai = "move_ai",
|
||||
move_ai_ar_kit = "move_ai_ar_kit",
|
||||
rigify = "rigify",
|
||||
rigify_ar_kit = "rigify_ar_kit",
|
||||
}
|
||||
|
||||
export enum MediaFileSubtype {
|
||||
@@ -35,15 +50,17 @@ export enum MediaFileSubtype {
|
||||
}
|
||||
|
||||
export interface UploadEngineAssetRequest {
|
||||
file: File;
|
||||
media_file_subtype?: MediaFileSubtype;
|
||||
title?: string;
|
||||
engine_category: MediaFileEngineCategory;
|
||||
file: File;
|
||||
maybe_animation_type: MediaFileAnimationType | null;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: string;
|
||||
uuid_idempotency_token?: string;
|
||||
}
|
||||
|
||||
export interface UploadEngineAssetResponse {
|
||||
media_file_token: string;
|
||||
engine_category: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import MakeMultipartRequest from "../MakeMultipartRequest";
|
||||
|
||||
export interface UploadMediaRequest {
|
||||
uuid_idempotency_token?: string,
|
||||
file: any,
|
||||
source: string
|
||||
uuid_idempotency_token?: string;
|
||||
file: File;
|
||||
source: string;
|
||||
title?: string;
|
||||
// dataset_token: string,
|
||||
}
|
||||
|
||||
export interface UploadMediaResponse {
|
||||
media_file_token: string,
|
||||
success: boolean
|
||||
media_file_token: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export const UploadMedia = (request: UploadMediaRequest) => {
|
||||
return MakeMultipartRequest("/v1/media_files/upload",request);
|
||||
}
|
||||
return MakeMultipartRequest("/v1/media_files/upload", request);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ export enum MediaFileAnimationType {
|
||||
MoveAiArKit = "move_ai_ar_kit",
|
||||
Rigify = "rigify",
|
||||
RigifyArKit = "rigify_ar_kit",
|
||||
Rokoko = "rokoko",
|
||||
}
|
||||
|
||||
export interface UploadNewEngineAssetRequest {
|
||||
@@ -29,6 +30,7 @@ export interface UploadNewEngineAssetRequest {
|
||||
maybe_animation_type: MediaFileAnimationType;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: "public" | "private";
|
||||
maybe_duration_millis?: number;
|
||||
uuid_idempotency_token?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,54 @@
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { Listbox, Transition } from '@headlessui/react'
|
||||
import { faCheck, faChevronDown } from '@fortawesome/pro-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { faCheck, faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface ListDropdownProps {
|
||||
list: {[key: string] : string}[];
|
||||
onSelect: (val:string)=>void;
|
||||
list: { [key: string]: string }[];
|
||||
onSelect: (val: string) => void;
|
||||
}
|
||||
export const ListDropdown = ({
|
||||
list, onSelect
|
||||
}:ListDropdownProps) => {
|
||||
export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
|
||||
const [selected, setSelected] = useState(list[0]);
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(Object.values(selected)[0]);
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<Listbox value={selected} onChange={setSelected}>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative w-full h-10 cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
|
||||
<span className="block truncate">{selected.name}</span>
|
||||
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
|
||||
<span className="block truncate">{Object.keys(selected)[0]}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true"/>
|
||||
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm z-10">
|
||||
leaveTo="opacity-0">
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm">
|
||||
{list.map((item, itemIdx) => (
|
||||
<Listbox.Option
|
||||
key={itemIdx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${
|
||||
active ? 'text-white' : 'text-gray-400'
|
||||
active ? "text-white" : "text-gray-400"
|
||||
}`
|
||||
}
|
||||
value={item}
|
||||
>
|
||||
value={item}>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? 'font-medium' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}>
|
||||
{Object.values(item)[0]}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<FontAwesomeIcon icon={faCheck} aria-hidden="true"/>
|
||||
<FontAwesomeIcon icon={faCheck} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
@@ -65,5 +59,5 @@ export const ListDropdown = ({
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -3,10 +3,10 @@ import * as THREE from "three";
|
||||
// from here https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
|
||||
|
||||
export const fitCameraToCenteredObject = function (
|
||||
camera,
|
||||
object,
|
||||
offset,
|
||||
orbitControls,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
object: THREE.Object3D<THREE.Object3DEventMap>,
|
||||
offset?: number,
|
||||
orbitControls?,
|
||||
) {
|
||||
const boundingBox = new THREE.Box3();
|
||||
boundingBox.setFromObject(object);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFileArrowUp, faFileAudio } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { P } from "~/components";
|
||||
import { getFileName } from "./utils/getFileName";
|
||||
|
||||
interface Props {
|
||||
file: File | null;
|
||||
fileTypes: string[];
|
||||
}
|
||||
|
||||
export const DragAndDropZone = ({ file, fileTypes }: Props) => {
|
||||
const fileSize =
|
||||
file && file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: file
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
|
||||
const fileName = file && getFileName(file).toUpperCase();
|
||||
|
||||
if (!file) {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border-2 border-dashed border-ui-controls-button/50 bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="text-4xl" />
|
||||
<div className="flex flex-col gap-0">
|
||||
<P className="font-medium">
|
||||
<u>Upload a file</u> or drop it here
|
||||
</P>
|
||||
<P className="flex items-center gap-2 text-sm font-normal opacity-50">
|
||||
{fileTypes.join(", ").toString()} supported
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-lg border-2 border-dashed border-ui-controls-button/50 bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileAudio} className="text-4xl" />
|
||||
<div className="flex grow flex-col gap-0">
|
||||
<P className="font-medium">
|
||||
{file.name.slice(0, file.name.lastIndexOf("."))}
|
||||
</P>
|
||||
<P className="flex items-center gap-2 text-sm font-normal">
|
||||
<span className="opacity-50">
|
||||
{`${fileName} file size: ${fileSize} `}
|
||||
</span>
|
||||
<u className="transition-all hover:text-white/80">Change File</u>
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button } from "~/components";
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
onRetry: () => void;
|
||||
isAssetError: boolean;
|
||||
}
|
||||
|
||||
export const UploadAssetError = ({
|
||||
onCancel,
|
||||
onRetry,
|
||||
isAssetError,
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
{isAssetError
|
||||
? "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: onCancel,
|
||||
variant: "secondary",
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
{...{
|
||||
onClick: onRetry,
|
||||
variant: "primary",
|
||||
}}>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { FileUploader } from "react-drag-drop-files";
|
||||
import { Button } from "~/components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { DragAndDropZone } from "./DragAndDropZone";
|
||||
import { UploadMediaResponse } from "~/components/UploadAudioButtonDialogue/utilities";
|
||||
import { useState } from "react";
|
||||
|
||||
enum UploadState {
|
||||
"init",
|
||||
"staged",
|
||||
"uploading",
|
||||
"uploaded",
|
||||
"error",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
setToken?: (token: string) => void;
|
||||
uploadFile?: (file: File) => Promise<UploadMediaResponse>;
|
||||
file: File | null;
|
||||
setFile: (file: File | null) => void;
|
||||
}
|
||||
|
||||
export const UploadFile = ({
|
||||
fileTypes,
|
||||
title,
|
||||
setToken,
|
||||
uploadFile,
|
||||
file,
|
||||
setFile,
|
||||
}: Props) => {
|
||||
const [uploadState, setUploadState] = useState<UploadState>(UploadState.init);
|
||||
|
||||
const submitUpload = (file: File | null) => {
|
||||
if (file && uploadFile) {
|
||||
setUploadState(UploadState.uploading);
|
||||
uploadFile(file).then((res: UploadMediaResponse) => {
|
||||
if ("media_file_token" in res) {
|
||||
setUploadState(UploadState.uploaded);
|
||||
setToken && setToken(res.media_file_token);
|
||||
} else {
|
||||
setUploadState(UploadState.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
{title}
|
||||
<FileUploader
|
||||
handleChange={(file: File | null) => {
|
||||
setFile(file);
|
||||
setUploadState(file ? UploadState.staged : UploadState.init);
|
||||
}}
|
||||
name="file"
|
||||
types={fileTypes}
|
||||
maxSize={50}>
|
||||
<DragAndDropZone file={file} fileTypes={fileTypes} />
|
||||
</FileUploader>
|
||||
{!!uploadFile && (
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
onClick={() => submitUpload(file)}
|
||||
disabled={uploadState !== UploadState.staged}>
|
||||
Upload
|
||||
{uploadState === UploadState.uploading && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Button, H6, Input, ListDropdown } from "~/components";
|
||||
import { UploadFile } from "~/components/UploadModalMovement/UploadFile";
|
||||
import { useState } from "react";
|
||||
import { MediaFileAnimationType } from "~/api/media_files/UploadNewEngineAsset";
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from "~/components";
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
export const UploadLoaderError = ({ onCancel, onRetry }: Props) => {
|
||||
return (
|
||||
<>
|
||||
There was a problem loading your object. Try another.
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
{...{
|
||||
onClick: onCancel,
|
||||
variant: "secondary",
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
{...{
|
||||
onClick: onRetry,
|
||||
variant: "primary",
|
||||
}}>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { TransitionDialogue, LoadingDots } from "~/components";
|
||||
import "./UploadModal.scss";
|
||||
import { UploadLoaderError } from "~/components/UploadModalMovement/UploadLoaderError";
|
||||
import { UploadAssetError } from "~/components/UploadModalMovement/UploadAssetError";
|
||||
import { UploadSuccess } from "~/components/UploadModalMovement/UploadSuccess";
|
||||
import { UploadFiles } from "~/components/UploadModalMovement/UploadFiles";
|
||||
import {
|
||||
MediaFileAnimationType,
|
||||
MediaFileEngineCategory,
|
||||
UploadNewEngineAsset,
|
||||
} from "~/api/media_files/UploadNewEngineAsset";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { UploadMedia } from "~/api/media_files/UploadMedia";
|
||||
import { EditCoverImage } from "~/api/media_files/EditCoverImage";
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
typeOptions: { [key: string]: string }[];
|
||||
type: MediaFileEngineCategory;
|
||||
}
|
||||
|
||||
enum UploaderState {
|
||||
ready,
|
||||
uploadingAsset,
|
||||
uploadingCover,
|
||||
settingCover,
|
||||
success,
|
||||
assetError,
|
||||
coverCreateError,
|
||||
coverSetError,
|
||||
loaderError,
|
||||
}
|
||||
|
||||
export default function UploadModalMovement({
|
||||
closeModal,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
fileTypes,
|
||||
typeOptions,
|
||||
type,
|
||||
}: Props) {
|
||||
const [objUploadStatus, setObjUploadStatus] = useState(UploaderState.ready);
|
||||
|
||||
const resetModalState = useCallback(() => {
|
||||
setObjUploadStatus(UploaderState.ready);
|
||||
}, []);
|
||||
|
||||
const onSubmit = async ({
|
||||
title,
|
||||
typeOption,
|
||||
assetFile,
|
||||
length,
|
||||
thumbnailFile,
|
||||
}: {
|
||||
title: string;
|
||||
typeOption: MediaFileAnimationType;
|
||||
assetFile: File;
|
||||
length: number;
|
||||
thumbnailFile: File | null;
|
||||
}) => {
|
||||
const res = await UploadNewEngineAsset({
|
||||
engine_category: type,
|
||||
file: assetFile,
|
||||
maybe_animation_type: typeOption as MediaFileAnimationType,
|
||||
maybe_duration_millis: length,
|
||||
maybe_title: title,
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
});
|
||||
if (!("media_file_token" in res)) {
|
||||
setObjUploadStatus(UploaderState.assetError);
|
||||
return;
|
||||
}
|
||||
const assetToken = res.media_file_token;
|
||||
if (!thumbnailFile) {
|
||||
setObjUploadStatus(UploaderState.success);
|
||||
return;
|
||||
}
|
||||
const resp = await UploadMedia({
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
file: thumbnailFile,
|
||||
source: title,
|
||||
});
|
||||
if (!("media_file_token" in resp)) {
|
||||
setObjUploadStatus(UploaderState.uploadingCover);
|
||||
return;
|
||||
}
|
||||
const thumbnailToken = resp.media_file_token;
|
||||
const editRes = await EditCoverImage(assetToken, {
|
||||
cover_image_media_file_token: thumbnailToken,
|
||||
});
|
||||
if (!editRes.success) {
|
||||
setObjUploadStatus(UploaderState.uploadingCover);
|
||||
return;
|
||||
}
|
||||
setObjUploadStatus(UploaderState.success);
|
||||
};
|
||||
|
||||
const objUploaderContent = () => {
|
||||
switch (objUploadStatus) {
|
||||
case UploaderState.ready:
|
||||
return (
|
||||
<UploadFiles
|
||||
title={title}
|
||||
fileTypes={fileTypes}
|
||||
onClose={() => {
|
||||
closeModal();
|
||||
onClose();
|
||||
}}
|
||||
typeOptions={typeOptions}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<UploadSuccess
|
||||
title={title}
|
||||
onOk={() => {
|
||||
closeModal();
|
||||
onClose();
|
||||
onSuccess();
|
||||
setTimeout(() => setObjUploadStatus(UploaderState.ready), 0);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case UploaderState.assetError:
|
||||
case UploaderState.coverCreateError:
|
||||
case UploaderState.coverSetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
onClose();
|
||||
setTimeout(() => setObjUploadStatus(UploaderState.ready), 0);
|
||||
}}
|
||||
onRetry={() => {
|
||||
switch (objUploadStatus) {
|
||||
case UploaderState.assetError: {
|
||||
break;
|
||||
}
|
||||
case UploaderState.coverCreateError: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
isAssetError={objUploadStatus === UploaderState.assetError}
|
||||
/>
|
||||
);
|
||||
case UploaderState.loaderError:
|
||||
return (
|
||||
<UploadLoaderError
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
onClose();
|
||||
setTimeout(() => setObjUploadStatus(UploaderState.ready), 0);
|
||||
}}
|
||||
onRetry={() => {
|
||||
setObjUploadStatus(UploaderState.ready);
|
||||
resetModalState();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
{...{
|
||||
isOpen,
|
||||
onClose,
|
||||
title: title,
|
||||
}}>
|
||||
{objUploaderContent()}
|
||||
</TransitionDialogue>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Button } from "~/components";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
onOk: () => void;
|
||||
}
|
||||
|
||||
export const UploadSuccess = ({ title, onOk }: Props) => {
|
||||
return (
|
||||
<div {...{ className: "obj-uploader-modal-success-view" }}>
|
||||
<div
|
||||
{...{
|
||||
className: "uploader-message",
|
||||
}}>{`Added ${title} to objects`}</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
{...{
|
||||
onClick: onOk,
|
||||
variant: "primary",
|
||||
}}>
|
||||
Ok
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./UploadModalMovement";
|
||||
@@ -0,0 +1,3 @@
|
||||
export const getFileName = (file: File) => {
|
||||
return file.name.split(".")[0] || "";
|
||||
};
|
||||
@@ -1,14 +1,73 @@
|
||||
import { animationFilter, animationItems } from "~/pages/PageEnigma/store";
|
||||
import { AssetFilterOption } from "~/pages/PageEnigma/models";
|
||||
import {
|
||||
AssetFilterOption,
|
||||
AssetType,
|
||||
MediaItem,
|
||||
} from "~/pages/PageEnigma/models";
|
||||
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 UploadModalMovement from "~/components/UploadModalMovement";
|
||||
import { MediaFileAnimationType } from "~/api/media_files/UploadNewEngineAsset";
|
||||
import { MediaFileEngineCategory } from "~/api/media_files/UploadEngineAsset";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
GetMediaByUser,
|
||||
GetMediaListResponse,
|
||||
} from "~/api/media_files/GetMediaByUser";
|
||||
import { AuthenticationContext } from "~/contexts/Authentication";
|
||||
|
||||
export const AnimationTab = () => {
|
||||
useSignals();
|
||||
const { authState } = useContext(AuthenticationContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userAnimations, setUserAnimations] = useState<MediaItem[]>([]);
|
||||
|
||||
const refetchAnimations = useCallback(async () => {
|
||||
if (!authState?.userInfo) {
|
||||
return;
|
||||
}
|
||||
return GetMediaByUser(
|
||||
authState?.userInfo?.username || "",
|
||||
{},
|
||||
{
|
||||
filter_engine_categories: MediaFileEngineCategory.Animation,
|
||||
// page_size: 5,
|
||||
},
|
||||
).then((res: GetMediaListResponse) => {
|
||||
if (res.success && res.results) {
|
||||
setUserAnimations(
|
||||
res.results.map((item, index: number) => {
|
||||
return {
|
||||
version: 1,
|
||||
type: AssetType.ANIMATION,
|
||||
media_id: item.token,
|
||||
name: item.maybe_title,
|
||||
publicBucketPath: item.public_bucket_path,
|
||||
length: ((item.maybe_duration_millis ?? 1000) / 1000) * 60,
|
||||
thumbnail: item.cover_image?.maybe_cover_image_public_bucket_path
|
||||
? "https://cdn.fakeyou.com/cdn-cgi/image/width=600,quality=100" +
|
||||
item.cover_image?.maybe_cover_image_public_bucket_path
|
||||
: undefined,
|
||||
isMine:
|
||||
item.maybe_creator_user?.user_token ===
|
||||
authState?.userInfo?.user_token,
|
||||
imageIndex: index,
|
||||
} as MediaItem;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [authState?.userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authState?.userInfo && !userAnimations.length) {
|
||||
refetchAnimations();
|
||||
}
|
||||
}, [authState?.userInfo, refetchAnimations, userAnimations]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -53,16 +112,33 @@ export const AnimationTab = () => {
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
onClick={() => setOpen(true)}
|
||||
className="w-full py-3 text-sm font-medium">
|
||||
Upload Animation
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full grow overflow-y-auto px-4 pb-4">
|
||||
<ItemElements
|
||||
items={animationItems.value}
|
||||
items={[...userAnimations, ...animationItems.value]}
|
||||
assetFilter={animationFilter.value}
|
||||
/>
|
||||
</div>
|
||||
<UploadModalMovement
|
||||
closeModal={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
onSuccess={refetchAnimations}
|
||||
isOpen={open}
|
||||
fileTypes={["GLB"]}
|
||||
title="Upload Animation"
|
||||
typeOptions={[
|
||||
{ Mixamo: MediaFileAnimationType.Mixamo },
|
||||
{ MikuMikuDance: MediaFileAnimationType.MikuMikuDance },
|
||||
{ MoveAi: MediaFileAnimationType.MoveAi },
|
||||
{ Rigify: MediaFileAnimationType.Rigify },
|
||||
{ Rokoko: MediaFileAnimationType.Rokoko },
|
||||
]}
|
||||
type={MediaFileEngineCategory.Animation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,75 +1,136 @@
|
||||
import { expressionFilter, expressionItems } from "~/pages/PageEnigma/store";
|
||||
import { AssetFilterOption } from "~/pages/PageEnigma/models";
|
||||
import {
|
||||
AssetFilterOption,
|
||||
AssetType,
|
||||
MediaItem,
|
||||
} from "~/pages/PageEnigma/models";
|
||||
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 "../comps/TabTitle";
|
||||
import UploadModalMovement from "~/components/UploadModalMovement";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { MediaFileEngineCategory } from "~/api/media_files/UploadEngineAsset";
|
||||
import {
|
||||
GetMediaByUser,
|
||||
GetMediaListResponse,
|
||||
} from "~/api/media_files/GetMediaByUser";
|
||||
import { AuthenticationContext } from "~/contexts/Authentication";
|
||||
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
|
||||
import { MediaFileAnimationType } from "~/api/media_files/UploadNewEngineAsset";
|
||||
|
||||
export const ExpressionTab = () => {
|
||||
useSignals();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { authState } = useContext(AuthenticationContext);
|
||||
|
||||
const refetchExpressions = useCallback(async () => {
|
||||
if (!authState?.userInfo) {
|
||||
return;
|
||||
}
|
||||
return GetMediaByUser(
|
||||
authState?.userInfo?.username || "",
|
||||
{},
|
||||
{
|
||||
filter_engine_categories: MediaFileEngineCategory.Expression,
|
||||
// page_size: 5,
|
||||
},
|
||||
).then((res: GetMediaListResponse) => {
|
||||
if (res.success && res.results) {
|
||||
expressionItems.value = res.results.map((item, index: number) => {
|
||||
return {
|
||||
version: 1,
|
||||
type: AssetType.EXPRESSION,
|
||||
media_id: item.token,
|
||||
name: item.maybe_title,
|
||||
publicBucketPath: item.public_bucket_path,
|
||||
length: ((item.maybe_duration_millis ?? 1000) / 1000) * 60,
|
||||
thumbnail: item.cover_image?.maybe_cover_image_public_bucket_path
|
||||
? "https://cdn.fakeyou.com/cdn-cgi/image/width=600,quality=100" +
|
||||
item.cover_image?.maybe_cover_image_public_bucket_path
|
||||
: undefined,
|
||||
isMine:
|
||||
item.maybe_creator_user?.user_token ===
|
||||
authState?.userInfo?.user_token,
|
||||
imageIndex: index,
|
||||
} as MediaItem;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [authState?.userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authState?.userInfo && !expressionItems.value.length) {
|
||||
refetchExpressions();
|
||||
}
|
||||
}, [authState?.userInfo, refetchExpressions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabTitle title="Expressions" />
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div className="flex gap-2 overflow-x-auto overflow-y-hidden px-4">
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
expressionFilter.value === AssetFilterOption.ALL
|
||||
? "active"
|
||||
: "",
|
||||
)}
|
||||
onClick={() => (expressionFilter.value = AssetFilterOption.ALL)}>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
expressionFilter.value === AssetFilterOption.MINE
|
||||
? "active"
|
||||
: "",
|
||||
)}
|
||||
onClick={() => (expressionFilter.value = AssetFilterOption.MINE)}
|
||||
disabled={!expressionItems.value.some((item) => item.isMine)}>
|
||||
My Expressions
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
expressionFilter.value === AssetFilterOption.BOOKMARKED
|
||||
? "active"
|
||||
: "",
|
||||
)}
|
||||
onClick={() =>
|
||||
(expressionFilter.value = AssetFilterOption.BOOKMARKED)
|
||||
}
|
||||
disabled={
|
||||
!expressionItems.value.some((item) => item.isBookmarked)
|
||||
}>
|
||||
Bookmarked
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full overflow-x-auto overflow-y-hidden">
|
||||
<div className="mb-4 flex justify-start gap-2 px-4">
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
expressionFilter.value === AssetFilterOption.ALL ? "active" : "",
|
||||
"disabled",
|
||||
)}
|
||||
onClick={() => (expressionFilter.value = AssetFilterOption.ALL)}>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
expressionFilter.value === AssetFilterOption.MINE ? "active" : "",
|
||||
"disabled",
|
||||
)}
|
||||
onClick={() => (expressionFilter.value = AssetFilterOption.MINE)}
|
||||
disabled={!expressionItems.value.some((item) => item.isMine)}>
|
||||
My Expressions
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
expressionFilter.value === AssetFilterOption.BOOKMARKED
|
||||
? "active"
|
||||
: "",
|
||||
"disabled",
|
||||
)}
|
||||
onClick={() =>
|
||||
(expressionFilter.value = AssetFilterOption.BOOKMARKED)
|
||||
}
|
||||
disabled={!expressionItems.value.some((item) => item.isBookmarked)}>
|
||||
Bookmarked
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full px-4">
|
||||
<div className="w-full px-4 pb-4">
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
onClick={() => setOpen(true)}
|
||||
className="w-full py-3 text-sm font-medium">
|
||||
Upload Expression
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full grow overflow-y-auto px-4">
|
||||
<div className="h-full w-full overflow-y-auto px-4">
|
||||
<ItemElements
|
||||
items={expressionItems.value}
|
||||
assetFilter={expressionFilter.value}
|
||||
/>
|
||||
</div>
|
||||
<UploadModalMovement
|
||||
closeModal={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
onSuccess={refetchExpressions}
|
||||
isOpen={open}
|
||||
fileTypes={["GLB"]}
|
||||
title="Upload Expression"
|
||||
typeOptions={[{ ARKit: MediaFileAnimationType.ArKit }]}
|
||||
type={MediaFileEngineCategory.Expression}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -119,6 +119,7 @@ export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
|
||||
);
|
||||
|
||||
if (clipOffset + (dragItem.value!.length ?? 0) > filmLength.value * 60) {
|
||||
DndAsset.notDropText = "Item is longer than the film length";
|
||||
canDrop.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface MediaInfo {
|
||||
origin_product_category: string;
|
||||
maybe_origin_model_type: null | string;
|
||||
maybe_origin_model_token: null | string;
|
||||
maybe_duration_millis: number | null;
|
||||
public_bucket_path: string;
|
||||
cover_image: {
|
||||
maybe_cover_image_public_bucket_path: null | string;
|
||||
|
||||
@@ -484,26 +484,7 @@ export const audioItems = signal<AudioMediaItem[]>([
|
||||
},
|
||||
]);
|
||||
|
||||
export const expressionItems = signal<MediaItem[]>([
|
||||
{
|
||||
version: 1,
|
||||
media_id: "m_yzreg1494d08aepezj74607d2ascep",
|
||||
type: AssetType.EXPRESSION,
|
||||
length: 100,
|
||||
name: "Smile",
|
||||
thumbnail: "/resources/characters/img04.png",
|
||||
isMine: true,
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
media_id: "m_qepzjytd4ahd5askdt2ngrwzj2aa3f",
|
||||
type: AssetType.EXPRESSION,
|
||||
length: 100,
|
||||
name: "Frown",
|
||||
thumbnail: "/resources/characters/img07.png",
|
||||
isBookmarked: true,
|
||||
},
|
||||
]);
|
||||
export const expressionItems = signal<MediaItem[]>([]);
|
||||
|
||||
export const cameraItems = signal<MediaItem[]>([
|
||||
{
|
||||
|
||||
@@ -100,7 +100,6 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
environmentVariables.value = data.ENV;
|
||||
console.log(data.ENV);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user