From 5afaecd282d71e1e9725e20b88491848fc765b87 Mon Sep 17 00:00:00 2001 From: BFlat Date: Mon, 25 Aug 2025 23:14:46 +0700 Subject: [PATCH] inpainting history styling (#782) - history stack styling - selected image highlighted - left reset all button with confirm modal before action --- .../src/components/signaled/TopBar/TopBar.tsx | 37 +++++++- .../app/src/pages/PageEdit/HistoryStack.tsx | 68 ++++++++++---- .../app/src/pages/PageEdit/LeftResetStack.tsx | 61 +++++++++++++ .../app/src/pages/PageEdit/PageEdit.tsx | 91 +++++++++++-------- .../gallery-modal/src/lib/gallery-modal.tsx | 3 + .../lightbox-modal/src/lib/lightbox-modal.tsx | 17 +++- 6 files changed, 220 insertions(+), 57 deletions(-) create mode 100644 frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx index 2ab2ca2ba6..45d8e79eba 100644 --- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx @@ -22,6 +22,7 @@ import { GalleryModal, galleryModalVisibleViewMode, galleryModalVisibleDuringDrag, + galleryModalLightboxVisible, } from "@storyteller/ui-gallery-modal"; import { SettingsModal } from "@storyteller/ui-settings-modal"; import { Tooltip } from "@storyteller/ui-tooltip"; @@ -48,6 +49,8 @@ import { useTauriWindowControls, useTauriPlatform, } from "@storyteller/tauri-utils"; +import { useEditStore } from "../../../pages/PageEdit/stores/EditState"; +import { BaseSelectorImage } from "../../../pages/PageEdit/BaseImageSelector"; interface Props { pageName: string; @@ -137,6 +140,30 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => { } }; + const handleEditFromGallery = async (url: string, mediaId?: string) => { + try { + // Reset editor state + useEditStore.getState().RESET(); + + // Switch to EDIT tab + useTabStore.getState().setActiveTab("EDIT"); + + // Select the image for editing + const baseImage: BaseSelectorImage = { + url, + mediaToken: mediaId || "", + }; + useEditStore.getState().setBaseImageInfo(baseImage); + + // Close gallery modal and lightbox if open + galleryModalVisibleViewMode.value = false; + galleryModalVisibleDuringDrag.value = false; + galleryModalLightboxVisible.value = false; + } catch (e) { + // no-op + } + }; + const getPageTitle = (): string => { switch (tabStore.activeTabId) { case "2D": @@ -171,7 +198,9 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => { >
- ArtCraft + + ArtCraft + { globalAccountLogoutCallback={() => setLogoutStates()} /> - + ); }; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx index 14445f795c..993f74c578 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx @@ -1,7 +1,8 @@ import { Button } from "@storyteller/ui-button"; -import { EditedImage, useImageEditCompleteEvent } from "@storyteller/tauri-events"; +import { useImageEditCompleteEvent } from "@storyteller/tauri-events"; import { faTrash } from "@fortawesome/pro-solid-svg-icons"; import { useState } from "react"; +import { twMerge } from "tailwind-merge"; import { BaseSelectorImage } from "./BaseImageSelector"; export interface ImageBundle { @@ -16,43 +17,74 @@ interface HistoryStackProps { export const HistoryStack = ({ onClear, - onImageSelect = () => { }, + onImageSelect = () => {}, startingBundles = [], }: HistoryStackProps) => { - - const [imageBundles, setImageBundles] = useState(startingBundles); + const [imageBundles, setImageBundles] = + useState(startingBundles); + const [selectedImageToken, setSelectedImageToken] = useState( + startingBundles.length > 0 && startingBundles[0].images.length > 0 + ? startingBundles[0].images[0].mediaToken + : null, + ); useImageEditCompleteEvent(async (event) => { const newBundle: ImageBundle = { - images: event.edited_images.map((editedImage) => ({ - url: editedImage.cdn_url, - mediaToken: editedImage.media_token, - } as BaseSelectorImage)), + images: event.edited_images.map( + (editedImage) => + ({ + url: editedImage.cdn_url, + mediaToken: editedImage.media_token, + }) as BaseSelectorImage, + ), }; - imageBundles.push(newBundle); - setImageBundles(imageBundles); + setImageBundles([...imageBundles, newBundle]); + const newlySelected = newBundle.images[0]; + if (newlySelected) { + setSelectedImageToken(newlySelected.mediaToken); + onImageSelect(newlySelected); + } }); const handleClear = () => { setImageBundles(startingBundles); + setSelectedImageToken(null); onClear(); - } + }; return ( -
-
- ))} @@ -61,4 +93,4 @@ export const HistoryStack = ({
); -} +}; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx new file mode 100644 index 0000000000..cbca4328fb --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faXmark, faTrashXmark } from "@fortawesome/pro-solid-svg-icons"; +import { Tooltip } from "@storyteller/ui-tooltip"; +import { + showActionReminder, + isActionReminderOpen, +} from "@storyteller/ui-action-reminder-modal"; + +interface LeftResetStackProps { + onReset: () => void; + className?: string; +} + +const LeftResetStack: React.FC = ({ + onReset, + className = "", +}) => { + return ( +
+
+ + + +
+
+ ); +}; + +export default LeftResetStack; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx index 1ca9f7b3b9..bcb39f9b70 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx @@ -23,6 +23,7 @@ import { import { ModelInfo } from "@storyteller/model-list"; import { useImageEditCompleteEvent } from "@storyteller/tauri-events"; import { HistoryStack, ImageBundle } from "./HistoryStack"; +import LeftResetStack from "./LeftResetStack"; const PAGE_ID: ModelPage = ModelPage.ImageEditor; @@ -140,7 +141,6 @@ const PageEdit = () => { }; }, [store]); - const onFitPressed = useCallback(async () => { // Get the stage and its container dimensions const stage = stageRef.current; @@ -188,7 +188,6 @@ const PageEdit = () => { // When baseImageBitmap changes from null to an actual image, call onFitPressed // Use a slight delay to ensure the image dimensions are properly loaded onFitPressed(); - }, [onFitPressed, store.baseImageBitmap]); // Create a function to use the left layer ref and download the bitmap from it @@ -228,37 +227,45 @@ const PageEdit = () => { return new Uint8Array(arrayBuffer); }; - const handleGenerate = useCallback(async (prompt: string) => { - if (isEnqueuing) { - console.warn("Already enqueuing an image, please wait."); - return; - } + const handleGenerate = useCallback( + async (prompt: string) => { + if (isEnqueuing) { + console.warn("Already enqueuing an image, please wait."); + return; + } - setIsEnqueuing(true); + setIsEnqueuing(true); - const editedImageToken = store.baseImageInfo?.mediaToken; + const editedImageToken = store.baseImageInfo?.mediaToken; - if (!editedImageToken) { - console.error("Base image is not available"); - return; - } + if (!editedImageToken) { + console.error("Base image is not available"); + return; + } - // TODO: Call inference API here - const arrayBuffer = await getMaskArrayBuffer(); + // TODO: Call inference API here + const arrayBuffer = await getMaskArrayBuffer(); - try { - await EnqueueImageInpaint({ - model: selectedModelInfo, - image_media_token: editedImageToken, - mask_image_raw_bytes: arrayBuffer, - prompt: prompt, - image_count: generationCount, - frontend_caller: "image_editor", - }); - } finally { - setIsEnqueuing(false); - } - }, [generationCount, isEnqueuing, selectedModelInfo, store.baseImageInfo?.mediaToken]); + try { + await EnqueueImageInpaint({ + model: selectedModelInfo, + image_media_token: editedImageToken, + mask_image_raw_bytes: arrayBuffer, + prompt: prompt, + image_count: generationCount, + frontend_caller: "image_editor", + }); + } finally { + setIsEnqueuing(false); + } + }, + [ + generationCount, + isEnqueuing, + selectedModelInfo, + store.baseImageInfo?.mediaToken, + ], + ); // Display image selector on launch, otherwise hide it // Also show loading state if info is set but image is loading @@ -284,8 +291,9 @@ const PageEdit = () => { return ( <>
{ />
+ store.RESET()} /> +
+
{ store.RESET(); }} + onClear={() => { + store.RESET(); + }} startingBundles={[{ images: [store.baseImageInfo] } as ImageBundle]} onImageSelect={(baseImage) => { store.clearLineNodes(); - store.setBaseImageInfo(baseImage) + store.setBaseImageInfo(baseImage); }} />
Promise; isOpen?: boolean; forceFilter?: string; + onEditClicked?: (url: string, media_id?: string) => Promise | void; } export const GalleryModal = React.memo( @@ -86,6 +87,7 @@ export const GalleryModal = React.memo( onAddToSceneClicked, isOpen, forceFilter, + onEditClicked, }: GalleryModalProps) => { const [loading, setLoading] = useState(false); // Lightbox state is now handled via signals @@ -658,6 +660,7 @@ export const GalleryModal = React.memo( onDownloadClicked={onDownloadClicked} onAddToSceneClicked={onAddToSceneClicked} mediaClass={lightboxImageSignal.value?.mediaClass} + onEditClicked={onEditClicked} /> )} diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx index acd1a97b61..7365c75144 100644 --- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx +++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx @@ -1,7 +1,7 @@ import { Modal } from "@storyteller/ui-modal"; import { Button } from "@storyteller/ui-button"; import dayjs from "dayjs"; -import { faDownToLine } from "@fortawesome/pro-solid-svg-icons"; +import { faDownToLine, faPencil } from "@fortawesome/pro-solid-svg-icons"; import { EnqueueImageTo3dObject, EnqueueImageTo3dObjectModel, @@ -40,6 +40,7 @@ interface LightboxModalProps { ) => Promise; mediaClass?: string; onPromptCopy?: (prompt: string) => void; + onEditClicked?: (url: string, media_id?: string) => Promise | void; } export function LightboxModal({ @@ -57,6 +58,7 @@ export function LightboxModal({ onDownloadClicked, onAddToSceneClicked, mediaClass, + onEditClicked, }: LightboxModalProps) { // NB(bt,2025-06-14): We add ?cors=1 to the image url to prevent caching "sec-fetch-mode: no-cors" from // the tag request from being cached. If we then drag it into the canvas after it's been cached, @@ -402,6 +404,19 @@ export function LightboxModal({ 3D + {onEditClicked && downloadUrl && ( + + )} + {onAddToSceneClicked && downloadUrl && (