Feature/Tabs improvement 2 - Image Panels... Panel (#592)

Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
wilandwil authored and GitHub committed 2024-07-08 20:46:23 -04:00
1 parent e4ac69af64
commit 3d707b9911
11 files changed
+492 -3

No files matched your search

@@ -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 (
<>
<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)}
/>
)}
<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">
{!assetFile.value && (
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
File Preivew
</H6>
)}
{assetFile.value && (
<img
alt="file upload preview"
className="m-auto max-h-full max-w-full"
src={URL.createObjectURL(assetFile.value)}
/>
)}
</div>
<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,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 (
<UploadFilesImages
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 "./UploadModalImages";
+1
View File
@@ -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
+1
View File
@@ -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",
}
+1
View File
@@ -1,5 +1,6 @@
export enum TabTitles {
SET_OBJECTS = "Film Sets",
IMAGE_PLANE = "Image Panels",
CHARACTERS = "Character",
ANIMATION = "Animation",
EXPRESSIONS = "Facial Expression",
@@ -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: <ObjectsTab />,
},
{
icon: faPresentationScreen,
title: TabTitles.IMAGE_PLANE,
component: <ImagePlanesTab />,
},
{
icon: faVolume,
title: TabTitles.AUDIO,
@@ -26,6 +26,9 @@ export const SidePanelMenu = ({
}) => {
useSignals();
const showSetsTab = usePosthogFeatureFlag(FeatureFlags.SHOW_SETS_TAB);
const showImagePlaneTab = usePosthogFeatureFlag(
FeatureFlags.SHOW_IMAGEPLANE_TAB,
);
return (
<div
className={twMerge(
@@ -42,6 +45,9 @@ export const SidePanelMenu = ({
if (tab.title === TabTitles.SET_OBJECTS && !showSetsTab) {
return;
}
if (tab.title === TabTitles.IMAGE_PLANE && !showImagePlaneTab) {
return;
}
return (
<button
key={tab.title}
@@ -0,0 +1,154 @@
import { useCallback, useEffect, useState } from "react";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import {
AssetFilterOption,
FilterEngineCategories,
IMAGE_FILE_TYPE,
} from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import {
Button,
FilterButtons,
Pagination,
UploadModalImages,
} from "~/components";
import {
TabTitle,
ItemElements,
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import {
fetchFeaturedMediaItems,
fetchUserMediaItems,
FetchMediaItemStates,
isAnyStatusFetching,
} from "../utilities";
export const ImagePlanesTab = () => {
const [openUploadModal, setOpenUploadModal] = useState(false);
const [{ mediaItems: userImages, status: userFetchStatus }, setUserFetch] =
useState<FetchMediaItemStates>({
mediaItems: undefined,
status: FetchStatus.READY,
});
const [
{ mediaItems: featuredImages, status: feateredFetchStatus },
setFeaturedFetch,
] = useState<FetchMediaItemStates>({
mediaItems: undefined,
status: FetchStatus.READY,
});
const [selectedFilter, setSelectedFilter] = useState(AssetFilterOption.MINE);
const diasplayedItems =
selectedFilter === AssetFilterOption.FEATURED
? featuredImages ?? []
: userImages ?? [];
const [currentPage, setCurrentPage] = useState<number>(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 (
<>
<TabTitle title="Image Panels" />
<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 Image Panels
</Button>
</div>
<div className="w-full grow overflow-y-auto px-4 pb-4">
<ItemElements
busy={isFetching}
currentPage={currentPage}
pageSize={pageSize}
items={diasplayedItems}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
<UploadModalImages
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserImages}
isOpen={openUploadModal}
fileTypes={Object.values(IMAGE_FILE_TYPE)}
title="Upload Image Panels"
type={FilterEngineCategories.IMAGE_PLANE}
/>
</>
);
};
@@ -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) {
-2
View File
@@ -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 = () => [