fixed characters upload and gallery dnd + added 3d meme models (#827)

- added 6 memes models and custom thumbnails
- upload modal now has checkbox to allow character uploads
- gallery drag and drop now works properly for characters
This commit is contained in:
BFlat authored and GitHub committed 2025-09-17 12:26:35 +07:00
1 parent 5093361f2c
commit 0163504aad
15 files changed
+154 -26

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -82,6 +82,18 @@ export const UploadFiles3D = ({
});
};
useEffect(() => {
// Reset subtype when fileSubtypes set changes (e.g., toggling character)
if (!fileSubtypes || fileSubtypes.length === 0) {
setSubtype(undefined);
return;
}
const nextDefault = Object.values(fileSubtypes[0])[0] as
| MediaFileAnimationType
| undefined;
setSubtype(nextDefault);
}, [fileSubtypes]);
useEffect(() => {
if (canvasRef.current && assetFile.value) {
loadPreviewOnCanvas({
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { LoadingDots } from "@storyteller/ui-loading";
import { Modal } from "@storyteller/ui-modal";
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
@@ -8,6 +8,7 @@ import {
FilterEngineCategories,
UploaderStates,
OBJECT_FILE_TYPE,
MediaFileAnimationType,
} from "../../../enums";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import {
@@ -34,9 +35,26 @@ export function UploadModal3D(props: Props) {
const { isOpen, onClose, onSuccess, title, titleIcon, options } = props;
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const [isCharacter, setIsCharacter] = useState(false);
// Category fixed to OBJECT
const selectedCategory = FilterEngineCategories.OBJECT;
const selectedCategory = isCharacter
? FilterEngineCategories.CHARACTER
: FilterEngineCategories.OBJECT;
const characterAnimationOptions = useMemo(() => {
if (!isCharacter) return undefined;
const values = Object.values(MediaFileAnimationType);
const sorted = values.sort((a, b) =>
a === MediaFileAnimationType.MixamoArKit
? -1
: b === MediaFileAnimationType.MixamoArKit
? 1
: a.localeCompare(b),
);
const toLabel = (v: string) =>
v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
return sorted.map((v) => ({ [toLabel(v)]: v }));
}, [isCharacter]);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
@@ -49,6 +67,7 @@ export function UploadModal3D(props: Props) {
useEffect(() => {
if (isOpen) {
resetModalState();
setIsCharacter(false);
}
}, [isOpen]);
@@ -69,11 +88,23 @@ export function UploadModal3D(props: Props) {
case UploaderStates.ready:
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<input
id="upload-as-character"
type="checkbox"
checked={isCharacter}
onChange={(e) => setIsCharacter(e.target.checked)}
/>
<label htmlFor="upload-as-character">Upload as Character</label>
</div>
<UploadFiles3D
title={title}
engineCategory={selectedCategory}
fileTypes={objectFileTypes}
options={options}
options={{
...(options ?? {}),
fileSubtypes: characterAnimationOptions,
}}
onClose={onClose}
onUploadProgress={updateUploaderState}
/>
@@ -33,6 +33,7 @@ export const uploadAsset = async ({
fileName: file.name,
engine_category: engineCategory,
maybe_title: title,
maybe_animation_type: animationType,
uuid: uuidv4(),
});
}
@@ -55,6 +55,7 @@ import {
} from "~/components/signaled/TopBar/TopBar";
import { uploadPlaneFromMediaToken } from "~/components/reusable/UploadModalMedia/uploadPlane";
import { addObject } from "./signals/objectGroup/addObject";
import { addCharacter } from "./signals/characterGroups/addCharacter";
import { AssetType, AUTH_STATUS } from "~/enums";
import { v4 as uuidv4 } from "uuid";
import { MediaItem } from "~/pages/PageEnigma/models";
@@ -77,7 +78,7 @@ import { useTabStore } from "../Stores/TabState";
import PageEdit from "../PageEdit/PageEdit";
import { ImageModel } from "@storyteller/model-list";
const PAGE_ID : ModelPage = ModelPage.Stage3D;
const PAGE_ID: ModelPage = ModelPage.Stage3D;
export const PageEditor = () => {
useSignals();
@@ -103,7 +104,8 @@ export const PageEditor = () => {
};
}, []);
const selectedImageModel : ImageModel | undefined = getSelectedImageModel(PAGE_ID);
const selectedImageModel: ImageModel | undefined =
getSelectedImageModel(PAGE_ID);
const height =
dndTimelineHeight.value > -1
@@ -305,11 +307,12 @@ export const PageEditor = () => {
try {
if (item.mediaClass === "dimensional") {
// Handle 3D models
const isCharacter = item.assetType === "character";
const mediaItem: MediaItem = {
version: 1,
type: AssetType.OBJECT,
type: isCharacter ? AssetType.CHARACTER : AssetType.OBJECT,
media_id: item.id || uuidv4(),
name: item.label || "3D Object",
name: item.label || (isCharacter ? "Character" : "3D Object"),
...(worldPosition && {
position: {
x: worldPosition.x,
@@ -318,7 +321,11 @@ export const PageEditor = () => {
},
}),
};
addObject(mediaItem);
if (isCharacter) {
addCharacter(mediaItem);
} else {
addObject(mediaItem);
}
} else {
// Handle images (create image planes)
const mediaItem: MediaItem = {
@@ -10,6 +10,7 @@ import {
faChevronRight,
faMountainCity,
faDog,
faFaceGrinStars,
faUpFromLine,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -29,6 +30,7 @@ import {
demoSkyboxItems,
demoShapeItems,
demoCharacterItems,
demoMemeItems,
assetModalVisibleDuringDrag,
reopenAfterDragSignal,
assetModalVisible,
@@ -246,10 +248,10 @@ export const AssetModal = () => {
featuredImagePlanesFetchStatus,
]);
const libraryTabs = [
{ id: "library", label: "Library" },
{ id: "mine", label: "Mine" },
];
// const libraryTabs = [
// { id: "library", label: "Library" },
// { id: "mine", label: "Mine" },
// ];
const assetTabs = useMemo<AssetTab[]>(
() => [
@@ -276,6 +278,14 @@ export const AssetModal = () => {
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
: (userObjects ?? []),
},
{
id: "memes",
label: "Memes",
labelSingle: "Meme",
icon: faFaceGrinStars,
engineCategory: FilterEngineCategories.CHARACTER,
items: activeLibraryTab === "library" ? demoMemeItems.value : [],
},
{
id: "sets",
label: "Sets",
@@ -323,6 +333,7 @@ export const AssetModal = () => {
demoCharacterItems.value,
demoShapeItems.value,
demoSkyboxItems.value,
demoMemeItems.value,
featuredCharacters,
featuredCreatures,
featuredObjects,
@@ -426,19 +437,19 @@ export const AssetModal = () => {
);
};
const handleAddAsset = () => {
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(null);
setIsSelectVisible(true);
};
// const handleAddAsset = () => {
// handleClose();
// setIsUploadModalOpen(true);
// setSelectedCategory(null);
// setIsSelectVisible(true);
// };
const handleAddSpecificAsset = (category: FilterEngineCategories) => {
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(category);
setIsSelectVisible(false);
};
// const handleAddSpecificAsset = (category: FilterEngineCategories) => {
// handleClose();
// setIsUploadModalOpen(true);
// setSelectedCategory(category);
// setIsSelectVisible(false);
// };
const handleUploadSuccess = (category: FilterEngineCategories) => {
if (reopenAfterDragSignal.value) {
@@ -0,0 +1,54 @@
import { signal } from "@preact/signals-core";
import { MediaItem } from "~/pages/PageEnigma/models";
import { AssetType } from "~/enums";
export const demoMemeItems = signal<MediaItem[]>([
{
version: 1,
media_id: "m_yb02j1b0dn6ashn14ap2e2vnvy1jhz",
media_type: "glb",
type: AssetType.CHARACTER,
name: "Toothless",
thumbnail: "/resources/memes/toothless.jpg",
},
{
version: 1,
media_id: "m_jgrzn7f66dvvw90qqttjn3m0dk8gef",
media_type: "glb",
type: AssetType.CHARACTER,
name: "Doge",
thumbnail: "/resources/memes/doge.jpg",
},
{
version: 1,
media_id: "m_h8gk5m59252nbza07p08dyn1fnhnsf",
media_type: "glb",
type: AssetType.CHARACTER,
name: "Grimace",
thumbnail: "/resources/memes/grimace.jpg",
},
{
version: 1,
media_id: "m_pzjn9583k1hqcetcfv1a2y04hkaspq",
media_type: "glb",
type: AssetType.CHARACTER,
name: "Roblox",
thumbnail: "/resources/memes/roblox.jpg",
},
{
version: 1,
media_id: "m_xh0aj0wmdhvyf0pv5jj1wj3ap7p1rf",
media_type: "glb",
type: AssetType.CHARACTER,
name: "JD Vance",
thumbnail: "/resources/memes/jd_vance.jpg",
},
{
version: 1,
media_id: "m_v8mkb44wy4r1ed7g5z8bq8n6bynerg",
media_type: "glb",
type: AssetType.CHARACTER,
name: "Lil JD",
thumbnail: "/resources/memes/lil_jd.jpg",
},
]);
@@ -8,7 +8,7 @@ export const demoSkyboxItems = signal<MediaItem[]>([
media_id: "SKY::m_1",
type: AssetType.SKYBOX,
name: "Empty Skybox",
thumbnail: "/resources/skybox/empty.jpg",
thumbnail: "/resources/placeholders/placeholder.png",
},
{
version: 1,
@@ -3,3 +3,4 @@ export * from "./demoAssets";
export * from "./demoAudioItems";
export * from "./demoCharacterItems";
export * from "./demoSkyboxItems";
export * from "./demoMemeItems";
@@ -50,6 +50,7 @@ export interface GalleryItem {
fullImage?: string | null;
createdAt: string;
mediaClass?: string;
assetType?: string;
}
interface GroupedItems {
@@ -256,6 +257,16 @@ export const GalleryModal = React.memo(
(item.filter_media_classes
? item.filter_media_classes[0]
: "image"),
// assetType for 3D assets so drop handlers can decide between character vs object
assetType:
item.media_class === "dimensional"
? item.maybe_animation_type ||
item.origin_product_category === "character" ||
(item.origin &&
item.origin.product_category === "character")
? "character"
: "object"
: undefined,
}));
setAllItems((prev) => (reset ? newItems : [...prev, ...newItems]));
// Pagination logic