mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 08:28:18 +00:00
Feature/Tabs improvement 2 - Image Panels... Panel (#592)
Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
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";
|
||||
@@ -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,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,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) {
|
||||
|
||||
@@ -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 = () => [
|
||||
|
||||
Reference in new issue
Block a user