diff --git a/app/components/reusable/UploadModalImages/UploadFilesImages.tsx b/app/components/reusable/UploadModalImages/UploadFilesImages.tsx
new file mode 100644
index 0000000000..ce9e2c2b1b
--- /dev/null
+++ b/app/components/reusable/UploadModalImages/UploadFilesImages.tsx
@@ -0,0 +1,134 @@
+import { useState } from "react";
+
+import { MediaFileAnimationType } from "~/enums";
+
+import { Button, H6, Input, ListDropdown } from "~/components";
+import { FileUploader } from "../UploadModal/FileUploader";
+
+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 UploadFilesImages = ({
+ fileTypes,
+ onClose,
+ title,
+ options,
+ onSubmit,
+}: Props) => {
+ const fileSubtypes = options?.fileSubtypes;
+
+ const [typeOption, setTypeOption] = useState<
+ MediaFileAnimationType | undefined
+ >(
+ fileSubtypes
+ ? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
+ : undefined,
+ );
+ const [uploadTitle, setUploadTitle] = useState<{
+ value: string;
+ error?: string;
+ }>({
+ value: "",
+ });
+
+ const [assetFile, setAssetFile] = useState<{
+ value: File | null;
+ error?: string;
+ }>({ value: 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,
+ typeOption,
+ });
+ };
+
+ return (
+ <>
+
+
setUploadTitle({ value: event.target.value })}
+ className={uploadTitle.error ? "mb-3" : ""}
+ />
+ {fileSubtypes && fileSubtypes.length > 1 && (
+
setTypeOption(value as MediaFileAnimationType)}
+ />
+ )}
+ {
+ setAssetFile({
+ value: file,
+ });
+ }}
+ />
+ {assetFile.error && (
+ {assetFile.error}
+ )}
+
+
+ {!assetFile.value && (
+
+ File Preivew
+
+ )}
+ {assetFile.value && (
+
})
+ )}
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/app/components/reusable/UploadModalImages/UploadModalImages.tsx b/app/components/reusable/UploadModalImages/UploadModalImages.tsx
new file mode 100644
index 0000000000..ac691b3706
--- /dev/null
+++ b/app/components/reusable/UploadModalImages/UploadModalImages.tsx
@@ -0,0 +1,186 @@
+import { useEffect, useState } from "react";
+import { v4 as uuidv4 } from "uuid";
+
+import { TransitionDialogue, LoadingDots } from "~/components";
+
+import { UploadAssetError } from "../UploadModal/UploadAssetError";
+import { UploadSuccess } from "../UploadModal/UploadSuccess";
+import { UploadFilesImages } from "./UploadFilesImages";
+import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
+import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
+
+import { getFileName } from "~/utilities";
+
+interface Props {
+ onClose: () => void;
+ onSuccess: () => void;
+ isOpen: boolean;
+ title: string;
+ fileTypes: string[];
+ type: FilterEngineCategories;
+ options?: {
+ fileSubtypes?: { [key: string]: string }[];
+ };
+}
+
+enum UploaderState {
+ ready,
+ uploadingAsset,
+ uploadingCover,
+ settingCover,
+ success,
+ assetError,
+ coverCreateError,
+ coverSetError,
+}
+
+const initialState = {
+ status: UploaderState.ready,
+};
+
+export function UploadModalImages({
+ isOpen,
+ onClose,
+ onSuccess,
+ title,
+ fileTypes,
+ type,
+ options,
+}: Props) {
+ const [uploaderState, setUploaderState] = useState<{
+ status: UploaderState;
+ errorMessage?: string;
+ }>(initialState);
+
+ const resetModalState = () => {
+ setUploaderState(initialState);
+ };
+
+ useEffect(() => {
+ if (isOpen) {
+ resetModalState();
+ }
+ }, [isOpen]);
+
+ const onSubmit = async ({
+ title,
+ assetFile,
+ }: {
+ title: string;
+ typeOption?: MediaFileAnimationType;
+ assetFile: File;
+ thumbnailFile: File | null;
+ }) => {
+ setUploaderState({ status: UploaderState.uploadingAsset });
+ const mediaUploadApi = new MediaUploadApi();
+ const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
+ file: assetFile,
+ fileName: assetFile.name,
+ engine_category: FilterEngineCategories.IMAGE_PLANE,
+ maybe_title: title,
+ uuid: uuidv4(),
+ });
+
+ if (!assetReponse.success || !assetReponse.data) {
+ setUploaderState({
+ status: UploaderState.assetError,
+ errorMessage: assetReponse.errorMessage,
+ });
+ return;
+ }
+
+ setUploaderState({ status: UploaderState.uploadingCover });
+ const thumbnailResponse = await mediaUploadApi.UploadImage({
+ uuid: uuidv4(),
+ blob: assetFile,
+ fileName: getFileName(assetFile),
+ maybe_title: "thumbnail_" + title,
+ });
+ if (!thumbnailResponse.success || !thumbnailResponse.data) {
+ setUploaderState({
+ status: UploaderState.coverCreateError,
+ errorMessage: thumbnailResponse.errorMessage,
+ });
+ return;
+ }
+
+ setUploaderState({ status: UploaderState.settingCover });
+
+ const mediaFilesApi = new MediaFilesApi();
+ const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
+ mediaFileToken: assetReponse.data,
+ imageToken: thumbnailResponse.data,
+ });
+ if (!setThumbnailResponse.success) {
+ setUploaderState({
+ status: UploaderState.coverSetError,
+ errorMessage: setThumbnailResponse.errorMessage,
+ });
+ return;
+ }
+ setUploaderState({ status: UploaderState.success });
+ };
+
+ const UploaderModalContent = () => {
+ switch (uploaderState.status) {
+ case UploaderState.ready:
+ return (
+
+ );
+ case UploaderState.uploadingAsset:
+ case UploaderState.uploadingCover:
+ case UploaderState.settingCover:
+ return (
+ <>
+
+ Uploading...
+ >
+ );
+ case UploaderState.success:
+ return (
+ {
+ onSuccess();
+ onClose();
+ }}
+ />
+ );
+ case UploaderState.assetError:
+ return (
+ {
+ resetModalState();
+ }}
+ type={type}
+ errorMessage={uploaderState.errorMessage}
+ />
+ );
+ case UploaderState.coverCreateError:
+ case UploaderState.coverSetError:
+ return (
+ {
+ resetModalState();
+ }}
+ type={"Thumbnail"}
+ errorMessage={uploaderState.errorMessage}
+ />
+ );
+ }
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/app/components/reusable/UploadModalImages/index.ts b/app/components/reusable/UploadModalImages/index.ts
new file mode 100644
index 0000000000..8a683d28e9
--- /dev/null
+++ b/app/components/reusable/UploadModalImages/index.ts
@@ -0,0 +1 @@
+export * from "./UploadModalImages";
diff --git a/app/components/reusable/index.ts b/app/components/reusable/index.ts
index f31055a664..b27527c247 100644
--- a/app/components/reusable/index.ts
+++ b/app/components/reusable/index.ts
@@ -27,6 +27,7 @@ export * from "./TransitionDialogue"; //need to decouple hotkey signal
export * from "./Typography";
export * from "./UploadModal";
export * from "./UploadModal3DPreview";
+export * from "./UploadModalImages";
export * from "./WaveformPlayer";
//TODO: need to rewrite because it's against tailwind
diff --git a/app/enums/FeatureFlags.ts b/app/enums/FeatureFlags.ts
index 74a01ccfb3..e828292c96 100644
--- a/app/enums/FeatureFlags.ts
+++ b/app/enums/FeatureFlags.ts
@@ -1,4 +1,5 @@
export enum FeatureFlags {
SHOW_SEARCH_OBJECTS = "show-search-objects",
+ SHOW_IMAGEPLANE_TAB = "show-imageplane-tab",
SHOW_SETS_TAB = "show-sets-tab",
}
diff --git a/app/enums/Tabs.ts b/app/enums/Tabs.ts
index bfe6477c0c..ae553389ab 100644
--- a/app/enums/Tabs.ts
+++ b/app/enums/Tabs.ts
@@ -1,5 +1,6 @@
export enum TabTitles {
SET_OBJECTS = "Film Sets",
+ IMAGE_PLANE = "Image Panels",
CHARACTERS = "Character",
ANIMATION = "Animation",
EXPRESSIONS = "Facial Expression",
diff --git a/app/pages/PageEnigma/comps/SidePanel/tabList.tsx b/app/pages/PageEnigma/comps/SidePanel/tabList.tsx
index b5ca5ec329..9f53dc199a 100644
--- a/app/pages/PageEnigma/comps/SidePanel/tabList.tsx
+++ b/app/pages/PageEnigma/comps/SidePanel/tabList.tsx
@@ -8,6 +8,7 @@ import {
faCube,
faVolume,
faBrush,
+ faPresentationScreen,
} from "@fortawesome/pro-solid-svg-icons";
import { TabTitles } from "~/enums";
@@ -18,6 +19,7 @@ import { StylizeTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/Styl
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";
+import { ImagePlanesTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/ImagePlanesTab";
export interface TabItem {
icon: FontAwesomeIconProps["icon"];
@@ -51,6 +53,11 @@ export const tabList = [
title: TabTitles.OBJECTS,
component: ,
},
+ {
+ icon: faPresentationScreen,
+ title: TabTitles.IMAGE_PLANE,
+ component: ,
+ },
{
icon: faVolume,
title: TabTitles.AUDIO,
diff --git a/app/pages/PageEnigma/comps/SidePanelMenu/SidePanelMenu.tsx b/app/pages/PageEnigma/comps/SidePanelMenu/SidePanelMenu.tsx
index 3462765fbf..9c8dc1b0d4 100644
--- a/app/pages/PageEnigma/comps/SidePanelMenu/SidePanelMenu.tsx
+++ b/app/pages/PageEnigma/comps/SidePanelMenu/SidePanelMenu.tsx
@@ -26,6 +26,9 @@ export const SidePanelMenu = ({
}) => {
useSignals();
const showSetsTab = usePosthogFeatureFlag(FeatureFlags.SHOW_SETS_TAB);
+ const showImagePlaneTab = usePosthogFeatureFlag(
+ FeatureFlags.SHOW_IMAGEPLANE_TAB,
+ );
return (
{
+ const [openUploadModal, setOpenUploadModal] = useState(false);
+
+ const [{ mediaItems: userImages, status: userFetchStatus }, setUserFetch] =
+ useState
({
+ mediaItems: undefined,
+ status: FetchStatus.READY,
+ });
+ const [
+ { mediaItems: featuredImages, status: feateredFetchStatus },
+ setFeaturedFetch,
+ ] = useState({
+ mediaItems: undefined,
+ status: FetchStatus.READY,
+ });
+
+ const [selectedFilter, setSelectedFilter] = useState(AssetFilterOption.MINE);
+ const diasplayedItems =
+ selectedFilter === AssetFilterOption.FEATURED
+ ? featuredImages ?? []
+ : userImages ?? [];
+
+ const [currentPage, setCurrentPage] = useState(0);
+
+ const pageSize = 21;
+ const totalPages = Math.ceil(diasplayedItems.length / pageSize);
+
+ const isFetching = isAnyStatusFetching([
+ userFetchStatus,
+ feateredFetchStatus,
+ ]);
+
+ const fetchUserImages = useCallback(
+ () =>
+ fetchUserMediaItems({
+ filterEngineCategories: [FilterEngineCategories.IMAGE_PLANE],
+ setState: (newState: FetchMediaItemStates) => {
+ setUserFetch((curr) => ({
+ status: newState.status,
+ mediaItems: newState.mediaItems
+ ? newState.mediaItems
+ : curr.mediaItems,
+ }));
+ },
+ defaultErrorMessage: "Unknown Error in Fetching User Images",
+ }),
+ [],
+ );
+
+ const fetchFeaturedImages = useCallback(
+ () =>
+ fetchFeaturedMediaItems({
+ filterEngineCategories: [FilterEngineCategories.IMAGE_PLANE],
+ setState: (newState: FetchMediaItemStates) => {
+ setFeaturedFetch((curr) => ({
+ status: newState.status,
+ mediaItems: newState.mediaItems
+ ? newState.mediaItems
+ : curr.mediaItems,
+ }));
+ },
+ defaultErrorMessage: "Unknown Error in Fetching Featured Images",
+ }),
+ [],
+ );
+
+ useEffect(() => {
+ if (!userImages) {
+ fetchUserImages();
+ }
+ if (!featuredImages) {
+ fetchFeaturedImages();
+ }
+ }, [userImages, fetchUserImages, featuredImages, fetchFeaturedImages]);
+
+ return (
+ <>
+
+
+ {
+ setSelectedFilter(Number(buttonIdx));
+ }}
+ />
+
+
+
+
+
+
+
+ {totalPages > 1 && (
+ {
+ setCurrentPage(newPage);
+ }}
+ />
+ )}
+ setOpenUploadModal(false)}
+ onSuccess={fetchUserImages}
+ isOpen={openUploadModal}
+ fileTypes={Object.values(IMAGE_FILE_TYPE)}
+ title="Upload Image Panels"
+ type={FilterEngineCategories.IMAGE_PLANE}
+ />
+ >
+ );
+};
diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/ObjectsTab.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/ObjectsTab.tsx
index e271e65158..4e017a4f57 100644
--- a/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/ObjectsTab.tsx
+++ b/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/ObjectsTab.tsx
@@ -120,7 +120,7 @@ export const ObjectsTab = () => {
const mediaFilesApi = new MediaFilesApi();
const response = await mediaFilesApi.SearchFeaturedMediaFiles({
search_term: searchTerm,
- filter_engine_categories: [FilterEngineCategories.OBJECT],
+ filter_engine_categories: [FilterEngineCategories.IMAGE_PLANE],
});
if (response.success && response.data) {
diff --git a/app/root.tsx b/app/root.tsx
index f874ff7957..12b6800d89 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -23,10 +23,8 @@ import { config } from "@fortawesome/fontawesome-svg-core";
import EnvironmentVariables from "~/Classes/EnvironmentVariables";
import { pageHeight, pageWidth } from "~/signals";
-import { Toaster } from "~/components";
import { showWizard } from "~/pages/PageEnigma/Wizard/signals/wizard";
-
config.autoAddCss = false; /* eslint-disable import/first */
export const links: LinksFunction = () => [