Feature/upload movement (#330)

Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com>
This commit is contained in:
FromTheLOLAshesandChristopher Oliphant authored and GitHub committed 2024-05-07 12:05:17 -07:00
1 parent a47e5767b7
commit fa2fb38d0d
22 files changed
+787 -110

No files matched your search

+2 -1
View File
@@ -19,7 +19,8 @@ export interface GetMediaParams {
| "character"
| "animation"
| "object"
| "skybox";
| "skybox"
| "expression";
}
export const GetMediaByUser = MakeRequest<
+20 -3
View File
@@ -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;
}
+8 -7
View File
@@ -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;
}
+21 -27
View File
@@ -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;
}
+1
View File
@@ -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;
+1 -20
View File
@@ -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[]>([
{
-1
View File
@@ -100,7 +100,6 @@ export default function App() {
useEffect(() => {
environmentVariables.value = data.ENV;
console.log(data.ENV);
}, [data]);
return (