From 05fbd2ff03c7f18ba71401af0dc124a7dfaba00f Mon Sep 17 00:00:00 2001 From: Michael Chung Date: Tue, 6 May 2025 17:18:52 -0400 Subject: [PATCH] Migrating 2d editor to 3d editor resolving a bunch of conflicts --- .../pages/Page2d/ContextualToolbarNode.tsx | 97 +++++++++ .../src/pages/Page2d/KonvaRootComponent.tsx | 18 +- .../app/src/pages/Page2d/SignaledDialogs.tsx | 59 ++++++ .../features/ToolbarNode/ToolbarNode.tsx | 0 .../components/features/ToolbarNode/data.ts | 0 .../components/features/ToolbarNode/enums.ts | 0 .../components/features/ToolbarNode/index.tsx | 0 .../components/reusable/UndoRedo/UndoRedo.tsx | 0 .../app/src/pages/Page2d/components/styles.ts | 32 +++ .../signals/authentication/auth_signals.ts | 52 +++++ .../Page2d/signals/authentication/enums.ts | 15 ++ .../Page2d/signals/authentication/fetchers.ts | 127 ++++++++++++ .../Page2d/signals/authentication/index.ts | 8 + .../signals/authentication/utilities.ts | 39 ++++ .../signals/generation/generationSignals.ts | 50 +++++ .../pages/Page2d/signals/generation/index.ts | 1 + .../app/src/pages/Page2d/signals/index.ts | 4 + .../Page2d/signals/uiAccess/buttonRetry.ts | 50 +++++ .../Page2d/signals/uiAccess/buttonTest.ts | 29 +++ .../signals/uiAccess/dialogChromakey.ts | 28 +++ .../Page2d/signals/uiAccess/dialogError.ts | 24 +++ .../pages/Page2d/signals/uiAccess/index.ts | 29 +++ .../Page2d/signals/uiAccess/loadingBar.ts | 86 ++++++++ .../pages/Page2d/signals/uiAccess/magicBox.ts | 32 +++ .../signals/uiAccess/toolbarMain/index.ts | 7 + .../uiAccess/toolbarMain/loadingBar.ts | 47 +++++ .../uiAccess/toolbarMain/toolbarMain.ts | 68 +++++++ .../Page2d/signals/uiAccess/toolbarNode.ts | 188 ++++++++++++++++++ .../uiAccess/toolbarVideoExtraction.ts | 96 +++++++++ .../src/pages/Page2d/signals/uiAccess/type.ts | 36 ++++ .../pages/Page2d/signals/uiEvents/addMedia.ts | 78 ++++++++ .../Page2d/signals/uiEvents/aiStylize.ts | 59 ++++++ .../pages/Page2d/signals/uiEvents/appMode.ts | 38 ++++ .../Page2d/signals/uiEvents/buttonRetry.ts | 29 +++ .../Page2d/signals/uiEvents/buttonTest.ts | 26 +++ .../pages/Page2d/signals/uiEvents/index.ts | 70 +++++++ .../signals/uiEvents/loadingIndicator.ts | 55 +++++ .../Page2d/signals/uiEvents/modelSelection.ts | 100 ++++++++++ .../Page2d/signals/uiEvents/promptSettings.ts | 44 ++++ .../uiEvents/toolbarMain/backgroundMenu.ts | 15 ++ .../signals/uiEvents/toolbarMain/eraseMode.ts | 14 ++ .../signals/uiEvents/toolbarMain/index.ts | 23 +++ .../uiEvents/toolbarMain/loadingBar.ts | 28 +++ .../signals/uiEvents/toolbarMain/paintMode.ts | 32 +++ .../uiEvents/toolbarMain/toolbarMain.ts | 78 ++++++++ .../Page2d/signals/uiEvents/toolbarNode.ts | 153 ++++++++++++++ .../app/src/pages/Page2d/useRenderCounter.ts | 14 ++ .../app/src/pages/PageEnigma/PageEditor.tsx | 9 +- 48 files changed, 2076 insertions(+), 11 deletions(-) create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/ContextualToolbarNode.tsx create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/SignaledDialogs.tsx rename frontend/apps/{editor2d/src => editor3d/app/src/pages/Page2d}/components/features/ToolbarNode/ToolbarNode.tsx (100%) rename frontend/apps/{editor2d/src => editor3d/app/src/pages/Page2d}/components/features/ToolbarNode/data.ts (100%) rename frontend/apps/{editor2d/src => editor3d/app/src/pages/Page2d}/components/features/ToolbarNode/enums.ts (100%) rename frontend/apps/{editor2d/src => editor3d/app/src/pages/Page2d}/components/features/ToolbarNode/index.tsx (100%) rename frontend/apps/{editor2d/src => editor3d/app/src/pages/Page2d}/components/reusable/UndoRedo/UndoRedo.tsx (100%) create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/components/styles.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/auth_signals.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/enums.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/fetchers.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/utilities.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/generationSignals.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonRetry.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonTest.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogChromakey.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogError.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/loadingBar.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/magicBox.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/loadingBar.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/toolbarMain.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarNode.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarVideoExtraction.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/type.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/addMedia.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/aiStylize.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/appMode.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonRetry.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonTest.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/loadingIndicator.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/modelSelection.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/promptSettings.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/backgroundMenu.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/eraseMode.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/index.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/loadingBar.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/paintMode.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/toolbarMain.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarNode.ts create mode 100644 frontend/apps/editor3d/app/src/pages/Page2d/useRenderCounter.ts diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/ContextualToolbarNode.tsx b/frontend/apps/editor3d/app/src/pages/Page2d/ContextualToolbarNode.tsx new file mode 100644 index 0000000000..20766adfd1 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/ContextualToolbarNode.tsx @@ -0,0 +1,97 @@ +import { MouseEventHandler, useState, useEffect } from "react"; +import { Transition } from "@headlessui/react"; +import { twMerge } from "tailwind-merge"; + +import { uiAccess } from "./signals/uiAccess"; +import { dispatchUiEvents } from "./signals/uiEvents"; + +import { ToolbarNode } from "./components/features/ToolbarNode"; +import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums"; +import { transitionTimingStyles } from "./components/styles"; +import { useSignals } from "@preact/signals-react/runtime"; + +export const ContextualToolbarNode = () => { + const [winSize, setWinSize] = useState<{ width: number; height: number }>({ + width: window.innerWidth, + height: window.innerHeight, + }); + const { isShowing, position, ...rest } = uiAccess.toolbarNode.signal.value; + const buttonsProps = Object.values(ToolbarNodeButtonNames).reduce( + (acc, buttonName) => { + acc[buttonName] = { + onClick: dispatchUiEvents.toolbarNode[buttonName], + disabled: + uiAccess.toolbarNode.signal.value.buttonStates[buttonName].disabled, + hidden: + uiAccess.toolbarNode.signal.value.buttonStates[buttonName].hidden, + active: + uiAccess.toolbarNode.signal.value.buttonStates[buttonName].active, + }; + return acc; + }, + {} as { + [key in ToolbarNodeButtonNames]: { + onClick: MouseEventHandler; + disabled: boolean; + hidden: boolean; + active: boolean; + }; + }, + ); + useEffect(() => { + function resizeHandler() { + setWinSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + window.addEventListener("resize", resizeHandler); + return () => { + window.removeEventListener("resize", resizeHandler); + }; + }, []); + + const handleColorChange = (color: string) => { + // Don't do anything if no node is known to be bound + if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) { + return; + } + + // Update the UI component + uiAccess.toolbarNode.setColor(color); + + // Signal the engine with the node ID (just grab the first -and expected only- node) + const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0]; + dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color }); + } + + useSignals(); + const color = uiAccess.toolbarNode.signal.value.color; + + const boundedTopPosition = Math.min(position.y, winSize.height - 164); + return ( + + { + dispatchUiEvents.toolbarNode.lock(e); + }} + color={color} + onColorChange={handleColorChange} + /> + + ); +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/KonvaRootComponent.tsx b/frontend/apps/editor3d/app/src/pages/Page2d/KonvaRootComponent.tsx index 56c5b50500..f20fb2a59b 100644 --- a/frontend/apps/editor3d/app/src/pages/Page2d/KonvaRootComponent.tsx +++ b/frontend/apps/editor3d/app/src/pages/Page2d/KonvaRootComponent.tsx @@ -3,25 +3,26 @@ import { useCallback, useRef, useState } from "react"; // Components import { KonvaCanvasContainer } from "./KonvaCanvasContainer"; import { ContextualToolbarNode } from "./ContextualToolbarNode"; -import { SignaledCanvasDragDropFiles } from "./SignaledCanvasDragDropFiles"; +// import { SignaledCanvasDragDropFiles } from "./SignaledCanvasDragDropFiles"; import { SignaledDialogs } from "./SignaledDialogs"; import { SignaledToolbarMain } from "./SignaledToolbarMain"; -import { PromptBox } from "~/components/PromptBox"; +// import { PromptBox } from "~/components/PromptBox"; // The KonvaApp is the root of the Konva stage // and only entry point for anything in Konva JS import { EngineType } from "~/KonvaApp"; import { KonvaApp } from "~/KonvaApp"; // all the signal-contexts are wrapped in hooks -import { useAppUiContext } from "./contextSignals/appUi"; +import { useAppUiContext } from "./contextSignals/appUi"; not needed for 2d. import { useLayoutContext } from "./contextSignals/layout"; // common hooks import { useRenderCounter } from "~/hooks/useRenderCounter"; import { useNavigate } from "react-router-dom"; -import { UndoRedo } from "~/components/reusable/UndoRedo/UndoRedo"; +// TODO REFACTOR THIS INTO THE COMPONENT LIBRARY its used in 3d and 2d +import { UndoRedo } from "./components/reusable/UndoRedo/UndoRedo"; export const KonvaRootComponent = ({ className, @@ -34,7 +35,7 @@ export const KonvaRootComponent = ({ // Let's make sure we only log once useRenderCounter("KonvaRootComponent"); const navigate = useNavigate(); - const appUiContext = useAppUiContext(); + // const appUiContext = useAppUiContext(); const layoutContext = useLayoutContext(); // Add state to track engine initialization @@ -80,11 +81,12 @@ export const KonvaRootComponent = ({ {/* Conditionally render the UI components that need the engine */} {isEngineReady && engineRef.current && ( - - + /> */} + {/* / */} ; + resetAll: () => void; +}) => { + return ( + <> + {/* Dialogs that are opened from within Reactland*/} + { + dispatchUiEvents.addImageToEngine(file); + }} + /> + { + dispatchUiEvents.addTextToEngine(data); + }} + closeCallback={resetAll} + /> + {/* Dialogs that are opened from the Engine*/} + + + ); +}; + +const SignaledDialogError = () => { + const props = dialogError.signal.value; + const { isShowing, title, message } = props; + const onClose = useCallback(() => { + dialogError.hide(); + }, []); + return ( + + ); +}; diff --git a/frontend/apps/editor2d/src/components/features/ToolbarNode/ToolbarNode.tsx b/frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/ToolbarNode.tsx similarity index 100% rename from frontend/apps/editor2d/src/components/features/ToolbarNode/ToolbarNode.tsx rename to frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/ToolbarNode.tsx diff --git a/frontend/apps/editor2d/src/components/features/ToolbarNode/data.ts b/frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/data.ts similarity index 100% rename from frontend/apps/editor2d/src/components/features/ToolbarNode/data.ts rename to frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/data.ts diff --git a/frontend/apps/editor2d/src/components/features/ToolbarNode/enums.ts b/frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/enums.ts similarity index 100% rename from frontend/apps/editor2d/src/components/features/ToolbarNode/enums.ts rename to frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/enums.ts diff --git a/frontend/apps/editor2d/src/components/features/ToolbarNode/index.tsx b/frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/index.tsx similarity index 100% rename from frontend/apps/editor2d/src/components/features/ToolbarNode/index.tsx rename to frontend/apps/editor3d/app/src/pages/Page2d/components/features/ToolbarNode/index.tsx diff --git a/frontend/apps/editor2d/src/components/reusable/UndoRedo/UndoRedo.tsx b/frontend/apps/editor3d/app/src/pages/Page2d/components/reusable/UndoRedo/UndoRedo.tsx similarity index 100% rename from frontend/apps/editor2d/src/components/reusable/UndoRedo/UndoRedo.tsx rename to frontend/apps/editor3d/app/src/pages/Page2d/components/reusable/UndoRedo/UndoRedo.tsx diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/components/styles.ts b/frontend/apps/editor3d/app/src/pages/Page2d/components/styles.ts new file mode 100644 index 0000000000..c5a4a9d8b7 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/components/styles.ts @@ -0,0 +1,32 @@ +import { twMerge } from "tailwind-merge"; + +export const paperWrapperStyles = + "rounded-xl border border-ui-border p-1.5 shadow-lg bg-ui-panel"; + +export const transitionTimingStyles = "transition duration-150 ease-in-out"; + +export const toolTipStyles = { + base: twMerge( + "relative", + "after:absolute after:z-40", + "after:content-[attr(data-tooltip)] after:text-black after:text-nowrap after:text-sm after:font-medium after:text-white/80 after:glass", + "after:rounded-lg after:border after:border-ui-border after:bg-ui-panel after:px-2.5 after:py-1 after:shadow-xl", + "after:hidden hover:after:block", + ), + top: twMerge( + "after:left-1/2 after:bottom-full after:-translate-x-1/2 after:mb-3.5", + ), + bottom: twMerge( + "after:left-1/2 after:top-full after:-translate-x-1/2 after:mt-3.5", + ), + left: twMerge( + "after:top-1/2 after:right-full after:-translate-y-1/2 after:mr-3.5", + ), + right: twMerge( + "after:top-1/2 after:left-full after:-translate-y-1/2 after:ml-3.5", + ), +}; + +export const dialogPanelStyles = twMerge( + "flex max-w-5xl flex-col justify-between gap-4 p-6 bg-[#1C1C20]", +); diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/auth_signals.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/auth_signals.ts new file mode 100644 index 0000000000..5c0a1f4472 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/auth_signals.ts @@ -0,0 +1,52 @@ +import { computed, signal } from "@preact/signals-react"; +import { UserInfo } from "@ApiManager/models/Users"; +import { ActiveSubscriptions } from "@ApiManager/models/Billing"; +import { LoyaltyProgram } from "@ApiManager/enums/Billing"; +import { USER_FEATURE_FLAGS } from "@ApiManager/enums/UserFeatures"; + +import { AUTH_STATUS } from "./enums"; + +const status = signal(AUTH_STATUS.INIT); +const userInfo = signal(undefined); +const activeSubs = signal(undefined); + +const canUpload3D = computed(() => { + if (!userInfo.value || !userInfo.value.maybe_feature_flags) { + return undefined; + } + return userInfo.value.maybe_feature_flags.includes( + USER_FEATURE_FLAGS.UPLOAD_3D, + ); +}); + +const hasAccess = computed(() => { + if ( + userInfo.value === undefined || + userInfo.value.can_access_studio === undefined + ) { + return undefined; + } + return userInfo.value.can_access_studio; +}); + +const hasPremium = computed(() => { + if (activeSubs.value === undefined) { + return undefined; + } + if ( + activeSubs.value.active_subscriptions.length > 0 || + activeSubs.value.maybe_loyalty_program === LoyaltyProgram.CONTRIBUTOR + ) { + return true; + } + return false; +}); + +export const signals = { + status, + userInfo, + activeSubs, + canUpload3D, + hasAccess, + hasPremium, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/enums.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/enums.ts new file mode 100644 index 0000000000..ce32954d6a --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/enums.ts @@ -0,0 +1,15 @@ +export enum AUTH_STATUS { + INIT = "INIT", + NO_ACCESS = "no_access", + LOGGED_IN = "logged_in", + LOGGING = "logging", + GET_USER_INFO = "get_user_info", + LOGGED_OUT = "logged_out", +} + +export enum AUTH_ERROR_FALLBACKS { + CreateSessionError = "Unknown Error during Create Session", + DestorySessionError = "Unknown Error during Destroy Session", + GetSessionError = "Unknown Error During Get Session", + Unauthorized = "User Unauthorized", +} diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/fetchers.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/fetchers.ts new file mode 100644 index 0000000000..0cc6afce9a --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/fetchers.ts @@ -0,0 +1,127 @@ +import { signals } from "./auth_signals"; +import { UsersApi } from "@storyteller/api"; +import { BillingApi } from "@storyteller/api"; + +import { + updateActiveSubscriptions, + updateAuthStatus, + updateUserInfo, + setLogoutStates, +} from "./utilities"; +import { AUTH_STATUS } from "./enums"; + +export const logout = async ( + failureCallback?: (errorMessage: string) => void, +) => { + const usersApi = new UsersApi(); + const logoutResponse = await usersApi.Logout(); + if (!logoutResponse.success && failureCallback) { + failureCallback( + logoutResponse.errorMessage || "Unknown Error during Destroy Session", + ); + } + // if success, nothing + // regarldess of success/fail, clear the state and localstorage + setLogoutStates(); +}; + +export const login = async ({ + usernameOrEmail, + password, +}: { + usernameOrEmail: string; + password: string; + failureCallback?: () => void; +}) => { + updateAuthStatus(AUTH_STATUS.LOGGING); + + const usersApi = new UsersApi(); + const loginResponse = await usersApi.Login({ usernameOrEmail, password }); + if (!loginResponse.success || !loginResponse.data) { + setLogoutStates(); + return; + } + + // technically user is login with the system now, HOWEVER, + // in storyteller studio, only having a sesison is not enough, + // we need session info and active subscription info as well + getUserInfoAndSubcriptions(); +}; + +export const signUp = async ({ + username, + email, + password, + passwordConfirmation, +}: { + username: string; + email: string; + password: string; + passwordConfirmation: string; +}) => { + updateAuthStatus(AUTH_STATUS.LOGGING); + + const usersApi = new UsersApi(); + const response = await usersApi.Signup({ + email, + password, + passwordConfirmation, + username, + }); + console.log(response); + if (!response.success || !response.data) { + setLogoutStates(); + return response.errorMessage ?? "Unknown error"; + } + + // technically user is login with the system now, HOWEVER, + // in storyteller studio, only having a sesison is not enough, + // we need session info and active subscription info as well + getUserInfoAndSubcriptions(); + return ""; +}; + +export const persistLogin = async () => { + //Only run First Load, return if not + if (signals.status.value !== AUTH_STATUS.INIT) { + return; + } + getUserInfoAndSubcriptions(); +}; + +async function getUserInfoAndSubcriptions() { + updateAuthStatus(AUTH_STATUS.GET_USER_INFO); + const usersApi = new UsersApi(); + const sessionResponse = await usersApi.GetSession(); + if ( + !sessionResponse.success || + !sessionResponse.data || + !sessionResponse.data.user + ) { + setLogoutStates(); + return; + } + + if (sessionResponse.data && !sessionResponse.data.user.can_access_studio) { + updateAuthStatus(AUTH_STATUS.NO_ACCESS); + return; + } + + const billingApi = new BillingApi(); + const subscriptionsResponse = await billingApi.ListActiveSubscriptions(); + if ( + !subscriptionsResponse.success || + !subscriptionsResponse.data || + !subscriptionsResponse.data.active_subscriptions + ) { + setLogoutStates(); + return; + } + + updateUserInfo(sessionResponse.data.user); + updateActiveSubscriptions({ + maybe_loyalty_program: subscriptionsResponse.data.maybe_loyalty_program, + active_subscriptions: subscriptionsResponse.data.active_subscriptions || [], + }); + updateAuthStatus(AUTH_STATUS.LOGGED_IN); +} diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/index.ts new file mode 100644 index 0000000000..a7a13b1aff --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/index.ts @@ -0,0 +1,8 @@ +import { signals } from "./auth_signals"; +import * as enums from "./enums"; +import * as fetchers from "./fetchers"; +export const authentication = { + signals, + fetchers, + enums, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/utilities.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/utilities.ts new file mode 100644 index 0000000000..e4f9a70825 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/authentication/utilities.ts @@ -0,0 +1,39 @@ +import { AUTH_STATUS } from "./enums"; +import { UserInfo } from "~/Classes/ApiManager/models/Users"; +import { ActiveSubscriptions } from "@ApiManager/models/Billing"; + +import { signals } from "./auth_signals"; + +export const updateAuthStatus = (newStatus: AUTH_STATUS) => { + signals.status.value = newStatus; +}; + +export const updateUserInfo = ( + newInfo: UserInfo | undefined, + flush?: boolean, +) => { + if (newInfo && !flush) { + //case of updating UserInfo partly + signals.userInfo.value = { + ...signals.userInfo.value, + ...newInfo, + }; + } else { + //case of setting a new User + //case of deleting userInfo + signals.userInfo.value = newInfo; + } +}; +export const updateActiveSubscriptions = ( + activeSubs: ActiveSubscriptions | undefined, +) => { + signals.activeSubs.value = activeSubs; +}; + +// this function is not exposed, only the logout function is +export const setLogoutStates = () => { + updateAuthStatus(AUTH_STATUS.LOGGED_OUT); + updateUserInfo(undefined); + updateActiveSubscriptions(undefined); + // flushAllBackgroundLoadedMedia(); +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/generationSignals.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/generationSignals.ts new file mode 100644 index 0000000000..7a04de74a6 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/generationSignals.ts @@ -0,0 +1,50 @@ +import { effect, signal } from "@preact/signals-react"; + +export enum GenerationLoadingState { + INIT, + GENERATING, + GENERATED, +} + +const initValue = { loadingState: GenerationLoadingState.INIT, prompt: "" }; + +export const generationSignal = signal<{ + loadingState: GenerationLoadingState; + prompt: string; + imageB64?: string; +}>(initValue); + +export const startGeneration = (prompt: string) => { + generationSignal.value = { + loadingState: GenerationLoadingState.GENERATING, + prompt, + imageB64: undefined, + }; +}; + +export const onStartGeneration = (callback: (prompt: string) => void) => { + effect(() => { + // If value just changed to GENERATING, notify the listeners + if ( + generationSignal.value.loadingState === GenerationLoadingState.GENERATING + ) { + callback(generationSignal.value.prompt); + } + }); +}; + +export const finishGeneration = (imageB64: string) => { + generationSignal.value = { + loadingState: GenerationLoadingState.GENERATED, + prompt: generationSignal.value.prompt, + imageB64, + }; +}; + +export const cancelGeneration = () => { + if ( + generationSignal.value.loadingState === GenerationLoadingState.GENERATING + ) { + generationSignal.value = initValue; + } +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/index.ts new file mode 100644 index 0000000000..f0b5f004b8 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/generation/index.ts @@ -0,0 +1 @@ +export * from "./generationSignals"; \ No newline at end of file diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/index.ts new file mode 100644 index 0000000000..157d93dd58 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/index.ts @@ -0,0 +1,4 @@ +export * from "./authentication"; +export * from "./uiAccess"; +export * from "./uiEvents"; +export * from "./generation"; \ No newline at end of file diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonRetry.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonRetry.ts new file mode 100644 index 0000000000..d9d4620248 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonRetry.ts @@ -0,0 +1,50 @@ +import { signal } from "@preact/signals-react"; +import { ContextualButtonRetryProps } from "./type"; +const buttonRetrySignal = signal({ + position: { + x: 0, + y: 0, + }, + isShowing: false, + disabled: false, +}); + +export const buttonRetry = { + signal: buttonRetrySignal, + disable: () => { + buttonRetrySignal.value = { ...buttonRetrySignal.value, disabled: true }; + }, + updatePosition(position: { x: number; y: number }) { + buttonRetrySignal.value = { + ...buttonRetrySignal.value, + position, + }; + }, + enable: () => { + buttonRetrySignal.value = { + ...buttonRetrySignal.value, + disabled: false, + }; + }, + show(props: Omit) { + if (buttonRetrySignal.value.isShowing) { + if (import.meta.env.DEV) { + console.warn( + "Loading bar is already showing", + "use the `updatePosition` methods instead", + ); + } + return; + } + buttonRetrySignal.value = { + ...props, + isShowing: true, + }; + }, + hide() { + buttonRetrySignal.value = { + ...buttonRetrySignal.value, + isShowing: false, + }; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonTest.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonTest.ts new file mode 100644 index 0000000000..23023f48a9 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/buttonTest.ts @@ -0,0 +1,29 @@ +import { signal } from "@preact/signals-react"; +const buttonTestSignal = signal<{ disabled: boolean; active: boolean }>({ + disabled: false, + active: false, +}); +export const buttonTest = { + signal: buttonTestSignal, + disable: () => { + buttonTestSignal.value = { ...buttonTestSignal.value, disabled: true }; + }, + enable: () => { + buttonTestSignal.value = { + ...buttonTestSignal.value, + disabled: false, + }; + }, + setActive: () => { + buttonTestSignal.value = { + ...buttonTestSignal.value, + active: true, + }; + }, + setInactive: () => { + buttonTestSignal.value = { + ...buttonTestSignal.value, + active: false, + }; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogChromakey.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogChromakey.ts new file mode 100644 index 0000000000..7f5542ddba --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogChromakey.ts @@ -0,0 +1,28 @@ +import { signal } from "@preact/signals-react"; +import { ChromakeyProps } from "~/components/features/DialogChromakey/type"; + +const initialState = { + isShowing: false, + chromakeyProps: { + isChromakeyEnabled: false, + }, +}; +const dialogChromaSignal = signal<{ + isShowing: boolean; + chromakeyProps: ChromakeyProps; +}>(initialState); + +export const dialogChromakey = { + signal: dialogChromaSignal, + + show(chromakeyProps: ChromakeyProps) { + dialogChromaSignal.value = { + ...dialogChromaSignal.value, + isShowing: true, + chromakeyProps, + }; + }, + hide() { + dialogChromaSignal.value = initialState; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogError.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogError.ts new file mode 100644 index 0000000000..7a6e670b2f --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/dialogError.ts @@ -0,0 +1,24 @@ +import { signal } from "@preact/signals-react"; + +const initialState = { + isShowing: false, + title: "", + message: "", +}; +const dialogErrorSignal = signal(initialState); + +export const dialogError = { + signal: dialogErrorSignal, + + show({ title, message }: { title?: string; message?: string }) { + dialogErrorSignal.value = { + ...dialogErrorSignal.value, + title: title ?? "Error", + message: message ?? "An unknownerror occurred", + isShowing: true, + }; + }, + hide() { + dialogErrorSignal.value = initialState; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/index.ts new file mode 100644 index 0000000000..1ff4db2098 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/index.ts @@ -0,0 +1,29 @@ +import { ContextualLoadingBarProps } from "./type"; + +type uiAccessType = { + loadingBar: ContextualLoadingBarProps; +}; + +export type { uiAccessType }; + +import { buttonRetry } from "./buttonRetry"; +import { buttonTest } from "./buttonTest"; +import { dialogChromakey } from "./dialogChromakey"; +import { dialogError } from "./dialogError"; +import { loadingBar } from "./loadingBar"; +import { magicBox } from "./magicBox"; +import { toolbarMain } from "./toolbarMain"; +import { toolbarNode } from "./toolbarNode"; +import { toolbarVideoExtraction } from "./toolbarVideoExtraction"; + +export const uiAccess = { + buttonRetry, + buttonTest, + dialogChromakey, + dialogError, + loadingBar, + magicBox, + toolbarMain, + toolbarNode, + toolbarVideoExtraction, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/loadingBar.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/loadingBar.ts new file mode 100644 index 0000000000..712d11e65c --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/loadingBar.ts @@ -0,0 +1,86 @@ +import { signal } from "@preact/signals-react"; +import { ContextualLoadingBarProps } from "./type"; +import { LoadingBarStatus } from "~/components/ui/LoadingBar"; + +const loadingBarSignal = signal({ + position: { + x: 0, + y: 0, + }, + width: 300, + isShowing: false, + progress: 0, + status: LoadingBarStatus.IDLE, + message: undefined, + onRetry: undefined, +}); + +export const loadingBar = { + signal: loadingBarSignal, + isShowing() { + return loadingBarSignal.value.isShowing; + }, + update(props: Partial>) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + // isShowing: true, + ...props, + }; + }, + updateMessage(message: string) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + message, + }; + }, + updateWidth(width: number) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + width, + }; + }, + updatePosition(position: { x: number; y: number }) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + position, + }; + }, + updateProgress(progress: number) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + progress, + }; + }, + updateStatus(status: LoadingBarStatus) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + status, + }; + }, + show(props?: Partial>) { + if (loadingBarSignal.value.isShowing) { + if (import.meta.env.DEV) { + console.warn( + "Loading bar is already showing", + "use the `update` methods instead", + ); + } + return; + } + const mergedProps = props + ? { ...props, ...loadingBarSignal.value } + : loadingBarSignal.value; + loadingBarSignal.value = { + ...mergedProps, + isShowing: true, + }; + }, + hide() { + if (loadingBarSignal.value.isShowing !== false) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + isShowing: false, + }; + } + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/magicBox.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/magicBox.ts new file mode 100644 index 0000000000..e7ea65ee5f --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/magicBox.ts @@ -0,0 +1,32 @@ +import { signal } from "@preact/signals-react"; +type MagicBoxSignalType = { + isShowing: boolean; + orientation: "vertical" | "horizontal"; + scale: number; +}; +const magicBoxSignal = signal({ + isShowing: false, + orientation: "vertical", + scale: 1, +}); +export const magicBox = { + signal: magicBoxSignal, + isShowing: () => { + return magicBoxSignal.value.isShowing; + }, + hide: () => { + magicBoxSignal.value = { ...magicBoxSignal.value, isShowing: false }; + }, + show: () => { + magicBoxSignal.value = { ...magicBoxSignal.value, isShowing: true }; + }, + setOrientation: (orientation: "vertical" | "horizontal") => { + magicBoxSignal.value = { ...magicBoxSignal.value, orientation }; + }, + setScale: (scale: number) => { + magicBoxSignal.value = { ...magicBoxSignal.value, scale }; + }, + update: (updatedVals: Partial) => { + magicBoxSignal.value = { ...magicBoxSignal.value, ...updatedVals }; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/index.ts new file mode 100644 index 0000000000..0012cb5ea5 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/index.ts @@ -0,0 +1,7 @@ +import { toolbarMain as toolbarMainOnly } from "./toolbarMain"; +import { loadingBar } from "./loadingBar"; + +export const toolbarMain = { + ...toolbarMainOnly, + loadingBar, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/loadingBar.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/loadingBar.ts new file mode 100644 index 0000000000..85c2e09842 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/loadingBar.ts @@ -0,0 +1,47 @@ +import { signal } from "@preact/signals-react"; +import { LoadingBarStatus } from "~/components/ui"; +import { LoadingBarInterface } from "../type"; + +const loadingBarSignal = signal({ + isShowing: false, + progress: 0, + status: LoadingBarStatus.IDLE, + message: undefined, +}); + +export const loadingBar = { + signal: loadingBarSignal, + update: (props: Partial) => { + loadingBarSignal.value = { ...loadingBarSignal.value, ...props }; + }, + updateMessage(message: string | undefined) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + message, + }; + }, + updateProgress(progress: number) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + progress, + }; + }, + updateStatus(status: LoadingBarStatus) { + loadingBarSignal.value = { + ...loadingBarSignal.value, + status, + }; + }, + show: (props?: Omit) => { + const mergedProps = props + ? { ...props, ...loadingBarSignal.value } + : loadingBarSignal.value; + loadingBarSignal.value = { + ...mergedProps, + isShowing: true, + }; + }, + hide: () => { + loadingBarSignal.value = { ...loadingBarSignal.value, isShowing: false }; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/toolbarMain.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/toolbarMain.ts new file mode 100644 index 0000000000..0a6c0911ee --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarMain/toolbarMain.ts @@ -0,0 +1,68 @@ +import { signal } from "@preact/signals-react"; +import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum"; +import { isToolbarMode } from "~/signals/uiEvents/toolbarMain/toolbarMain"; + +interface ToolbarMainSignalInterface { + disabled: boolean; + buttonStates: { + [key in ToolbarMainButtonNames]: { disabled: boolean; active: boolean }; + }; +} +const toolbarMainSignal = signal({ + disabled: false, + buttonStates: initButtonStates(), +}); + +export const toolbarMain = { + signal: toolbarMainSignal, + enable() { + toolbarMainSignal.value = { + ...toolbarMainSignal.value, + disabled: false, + }; + }, + disable() { + toolbarMainSignal.value = { + ...toolbarMainSignal.value, + disabled: true, + }; + }, + + changeButtonState( + buttonName: ToolbarMainButtonNames, + { disabled, active }: { disabled?: boolean; active?: boolean }, + ) { + toolbarMainSignal.value = { + ...toolbarMainSignal.value, + buttonStates: { + ...toolbarMainSignal.value.buttonStates, + [buttonName]: { + disabled: active ? false : (disabled ?? false), + active: active ?? false, + }, + }, + }; + }, + + setMode(mode: ToolbarMainButtonNames) { + if (!isToolbarMode(mode)) { + return; + } + + this.changeButtonState(ToolbarMainButtonNames.SELECT, { active: false }); + this.changeButtonState(ToolbarMainButtonNames.ERASER, { active: false }); + this.changeButtonState(ToolbarMainButtonNames.PAINT, { active: false }); + this.changeButtonState(mode, { active: true }); + } +}; + +function initButtonStates() { + const ret: { [key: string]: { disabled: boolean; active: boolean } } = {}; + Object.values(ToolbarMainButtonNames).forEach((buttonName) => { + ret[buttonName] = { + disabled: false, + active: false, + }; + }); + return ret as ToolbarMainSignalInterface["buttonStates"]; +} diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarNode.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarNode.ts new file mode 100644 index 0000000000..b977385019 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarNode.ts @@ -0,0 +1,188 @@ +import { signal } from "@preact/signals-react"; +import { ContextualUi } from "./type"; +import { ToolbarNodeButtonNames as ButtonNames } from "~/components/features/ToolbarNode/enums"; +import { DEFAULT_PAINT_COLOR } from "../uiEvents/toolbarMain/paintMode"; + +type ButtonStates = { + [key in ButtonNames]: { + disabled: boolean; + active: boolean; + hidden: boolean; + }; +}; +export interface ContextualToolbarProps extends ContextualUi { + knodeIds: string[]; + disabled: boolean; + downloadUrl?: string; + locked: boolean | "unknown"; + lockDisabled: boolean; + buttonStates: ButtonStates; + color: string; + position: { x: number, y: number }; +} +interface PartialContextualToolbarProps + extends Partial> { + buttonStates?: Partial; +} +const toolbarNodeSignal = signal({ + knodeIds: [], + position: { + x: 0, + y: 0, + }, + isShowing: false, + disabled: false, + locked: false, + lockDisabled: false, + buttonStates: initButtonStates(), + color: DEFAULT_PAINT_COLOR, +}); + +export const toolbarNode = { + signal: toolbarNodeSignal, + isShowing() { + return toolbarNodeSignal.value.isShowing; + }, + setup(props: ContextualToolbarProps) { + toolbarNodeSignal.value = props; + }, + update(props: Partial) { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + ...props, + }; + }, + setPosition(position: ContextualToolbarProps["position"]) { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + position, + }; + }, + isLocked() { + return toolbarNodeSignal.value.locked; + }, + isLockDisabled() { + return toolbarNodeSignal.value.lockDisabled; + }, + disableLock() { + toolbarNodeSignal.value.lockDisabled = true; + }, + enableLock() { + toolbarNodeSignal.value.lockDisabled = false; + }, + setLocked(locked: boolean) { + const { buttonStates } = toolbarNodeSignal.value; + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + buttonStates: { + ...buttonStates, + [ButtonNames.TRANSFORM]: { + disabled: locked, + hidden: false, + active: buttonStates[ButtonNames.TRANSFORM].active, + }, + [ButtonNames.COLOR]: { + disabled: locked, + hidden: false, + active: buttonStates[ButtonNames.COLOR].active, + }, + }, + locked: locked, + }; + }, + show( + values: PartialContextualToolbarProps = { + locked: false, + buttonStates: {}, + }, + ) { + const { locked, buttonStates, ...rest } = values; + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + ...rest, + buttonStates: { + ...initButtonStates({ + locked, + }), + ...buttonStates, + }, + isShowing: true, + locked: locked ?? false, + }; + }, + hide() { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + isShowing: false, + }; + }, + enable() { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + disabled: false, + }; + }, + disable() { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + disabled: true, + }; + }, + changeButtonState( + buttonName: ButtonNames, + { disabled, active }: { disabled?: boolean; active?: boolean }, + ) { + const prevButtonState = toolbarNodeSignal.value.buttonStates[buttonName]; + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + buttonStates: { + ...toolbarNodeSignal.value.buttonStates, + [buttonName]: { + disabled: disabled ?? prevButtonState.disabled, + active: active ?? prevButtonState.active, + }, + }, + }; + }, + setColor(color: string) { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + color, + } + }, + batchButtonStates({ buttonStates }: { buttonStates: Partial }) { + const prevButtonStates = toolbarNodeSignal.value.buttonStates; + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + buttonStates: { + ...prevButtonStates, + ...buttonStates, + }, + }; + }, + resetAllButtonStates() { + toolbarNodeSignal.value = { + ...toolbarNodeSignal.value, + buttonStates: initButtonStates(), + }; + }, +}; + +function initButtonStates(props: { locked?: boolean | "unknown" } = {}) { + return Object.values(ButtonNames).reduce((buttonStates, buttonName) => { + if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { + buttonStates[buttonName] = { + disabled: props.locked === "unknown" || props.locked === true, + hidden: false, + active: true, + }; + } else { + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + } + return buttonStates; + }, {} as ButtonStates); +} diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarVideoExtraction.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarVideoExtraction.ts new file mode 100644 index 0000000000..5fe21e33db --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/toolbarVideoExtraction.ts @@ -0,0 +1,96 @@ +import { signal } from "@preact/signals-react"; +import { ButtonState, LoadingBarState } from "./type"; +import { ToolbarVideoExtractionButtonNames as ButtonNames } from "~/components/features/ToolbarVideoExtraction/enums"; +import { VideoExtractionEvents } from "~/KonvaApp/types/events"; + +type ButtonStates = { + [key in ButtonNames]: ButtonState; +}; + +export interface ToolbarVideoExtractionProps { + isShowing: boolean; + disabled: boolean; + mode: "inclusion" | "exclusion"; + ready: boolean; + buttonStates: ButtonStates; + loadingBarState: LoadingBarState; +} + +const toolbarVideoExtractionSignal = signal({ + ready: false, + mode: "inclusion", + isShowing: false, + disabled: false, + buttonStates: initButtonStates(), + loadingBarState: { + progress: 0, + status: VideoExtractionEvents.SESSION_CLOSED, + message: undefined, + }, +}); + +export const toolbarVideoExtraction = { + signal: toolbarVideoExtractionSignal, + update(updateProps: Partial) { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + ...updateProps, + }; + }, + setReady(ready: boolean) { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + ready, + }; + }, + setMode(mode: "inclusion" | "exclusion") { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + mode, + }; + }, + updateProgress(newLoadingBarState: LoadingBarState) { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + loadingBarState: { + ...toolbarVideoExtractionSignal.value.loadingBarState, + ...newLoadingBarState, + }, + }; + }, + show() { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + isShowing: true, + }; + }, + hide() { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + isShowing: true, + }; + }, + enable() { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + disabled: false, + }; + }, + disable() { + toolbarVideoExtractionSignal.value = { + ...toolbarVideoExtractionSignal.value, + disabled: true, + }; + }, +}; + +function initButtonStates() { + return Object.values(ButtonNames).reduce((buttonStates, buttonName) => { + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + return buttonStates; + }, {} as ButtonStates); +} diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/type.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/type.ts new file mode 100644 index 0000000000..08815421cc --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiAccess/type.ts @@ -0,0 +1,36 @@ +import { LoadingBarProps, LoadingBarStatus } from "~/components/ui"; + +export type LoadingBarState = { + progress: number; + status: string; + message: string | undefined; +}; +export type ButtonState = { + disabled: boolean; + active: boolean; + hidden: boolean; +}; +export interface ContextualUi { + position: { + x: number; + y: number; + }; + isShowing: boolean; +} + +export interface ContextualLoadingBarProps + extends ContextualUi, + Omit { + width: number; +} + +export interface ContextualButtonRetryProps extends ContextualUi { + disabled: boolean; +} + +export interface LoadingBarInterface { + isShowing: boolean; + progress: number; + status: LoadingBarStatus; + message?: string; +} diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/addMedia.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/addMedia.ts new file mode 100644 index 0000000000..25954e9fcd --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/addMedia.ts @@ -0,0 +1,78 @@ +import { signal, effect } from "@preact/signals-react"; +import { ShapeNodeData, TextNodeData } from "~/KonvaApp/types"; + +// ADDING IMAGES +const stagedImage = signal(null); +const addImageToEngine = (image: File) => { + stagedImage.value = image; +}; + +const onGetStagedImage = (callback: (file: File) => void) => { + effect(() => { + if (stagedImage.value) { + callback(stagedImage.value); + } + }); +}; + +// ADDING VIDEOS +type VideoSignal = { + mediaFileToken: string; + mediaFileUrl: string; + videoWidth: number; + videoHeight: number; +}; +const stagedVideo = signal(null); + +const addVideoToEngine = (videoData: VideoSignal) => { + stagedVideo.value = videoData; +}; + +const onGetStagedVideo = (callback: (videoData: VideoSignal) => void) => { + effect(() => { + if (stagedVideo.value) { + callback(stagedVideo.value); + } + }); +}; + +// ADD and EDIT TEXT +const stagedText = signal(null); +const addTextToEngine = (data: TextNodeData) => { + stagedText.value = data; +}; +const onAddTextToEngine = (callback: (data: TextNodeData) => void) => { + effect(() => { + if (stagedText.value) { + callback(stagedText.value); + } + }); +}; + +// Add shapes +const stagedShape = signal(null); +const addShapeToEngine = (data: ShapeNodeData) => { + stagedShape.value = data; +} +const onAddShapeToEngine = (callback: (data: ShapeNodeData) => void) => { + effect(() => { + if (stagedShape.value) { + callback(stagedShape.value); + } + }); +}; + +//EXPORTS +export const dispatchers = { + addImageToEngine, + addVideoToEngine, + addTextToEngine, + addShapeToEngine, +}; + +export const events = { + onGetStagedImage, + onGetStagedVideo, + onAddTextToEngine, + onAddShapeToEngine, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/aiStylize.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/aiStylize.ts new file mode 100644 index 0000000000..6666ae9095 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/aiStylize.ts @@ -0,0 +1,59 @@ +import { signal, effect } from "@preact/signals-react"; +import { + RandomTextsNegative, + RandomTextsPositive, +} from "~/components/features/DialogAiStylize/data/randomTextList"; + +import { ArtStyleNames } from "~/components/features/DialogAiStylize/enums"; + +interface AiStylizeSingalType { + artstyle: ArtStyleNames; + positivePrompt: string; + negativePrompt: string; + cinematic: boolean; + enginePreProcessing: boolean; + faceDetail: boolean; + lipSync: boolean; + upscale: boolean; + styleStrength: number; +} +const initialValues = { + artstyle: ArtStyleNames.Anime2DFlat, + positivePrompt: RandomTextsPositive[ArtStyleNames.Anime2DFlat][0], + negativePrompt: RandomTextsNegative[ArtStyleNames.Anime2DFlat][0], + cinematic: false, + enginePreProcessing: false, + faceDetail: false, + lipSync: false, + upscale: false, + styleStrength: 0.8, +}; +const stagedAiStylizeRequest = signal(); + +const dispatchRequest = (data: AiStylizeSingalType) => { + stagedAiStylizeRequest.value = data; +}; +let onRequestEffectCleanup: (() => void) | undefined; +const onRequest = (callback: (data: AiStylizeSingalType) => void) => { + if (onRequestEffectCleanup) { + onRequestEffectCleanup(); + } + onRequestEffectCleanup = effect(() => { + if (stagedAiStylizeRequest.value) { + callback(stagedAiStylizeRequest.value); + stagedAiStylizeRequest.value = undefined; + } + }); +}; + +const getCurrentValues = stagedAiStylizeRequest.value; +const getInitialValues = initialValues; + +export const aiStylizeDispatchers = { + dispatchRequest, +}; +export const aiStylizeEvents = { + onRequest, + getCurrentValues, + getInitialValues, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/appMode.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/appMode.ts new file mode 100644 index 0000000000..58450d8450 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/appMode.ts @@ -0,0 +1,38 @@ +import { signal, effect } from "@preact/signals-core"; + +// this is to control the app mode - BFlat + +// Define the app mode types +export type AppModeType = "realtime" | "edit" | "generate" | "gallery"; + +// Create a signal for the current app mode +export const appMode = signal("realtime"); + +// Create a function to change the app mode +export const changeAppMode = (mode: AppModeType) => { + appMode.value = mode; +}; + +// Store callbacks for mode changes +const callbacks: ((mode: AppModeType) => void)[] = []; + +// Set up an effect to notify callbacks when the mode changes +effect(() => { + const currentMode = appMode.value; + callbacks.forEach((callback) => callback(currentMode)); +}); + +// Export an event handler for components to use +export const appModeEvents = { + onChange: (callback: (mode: AppModeType) => void) => { + callbacks.push(callback); + + // Return a function to unsubscribe + return () => { + const index = callbacks.indexOf(callback); + if (index !== -1) { + callbacks.splice(index, 1); + } + }; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonRetry.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonRetry.ts new file mode 100644 index 0000000000..d4e346e9fa --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonRetry.ts @@ -0,0 +1,29 @@ +import { MouseEventHandler } from "react"; +import { signal, effect } from "@preact/signals-react"; + +const event = signal< + React.MouseEvent | undefined +>(undefined); + +let effectCleanup: (() => void) | undefined = undefined; + +export const eventsHandler = { + onClick: (callback: MouseEventHandler) => { + if (effectCleanup) { + effectCleanup(); + } + effectCleanup = effect(() => { + if (event.value) { + callback(event.value); + event.value = undefined; + } + }); + }, +}; + +export const dispatcher = { + onClick: (e: React.MouseEvent) => { + console.log("Button Retry dispatcher"); + event.value = e; + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonTest.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonTest.ts new file mode 100644 index 0000000000..2da9b0f72f --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/buttonTest.ts @@ -0,0 +1,26 @@ +// import { MouseEventHandler } from "react"; +import { signal, effect } from "@preact/signals-react"; + +const event = signal(); + +export const eventsHandler = { + onClick: (callback: () => void) => { + effect(() => { + if (event.value) { + console.log("Button Test effect event handler", event.value); + callback(); + return () => { + console.log("Button Test effect event handler cleanup"); + event.value = undefined; + }; + } + }); + }, +}; + +export const dispatcher = { + onClick: () => { + console.log("Button Test dispatcher"); + event.value = new Date().getTime(); + }, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/index.ts new file mode 100644 index 0000000000..f54c5ff6f5 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/index.ts @@ -0,0 +1,70 @@ +import { + dispatchers as addMediaDispatchers, + events as addMediaEvents, +} from "./addMedia"; +import { aiStylizeDispatchers, aiStylizeEvents } from "./aiStylize"; +import { + dispatcher as buttonTestDispatcher, + eventsHandler as buttonTestEvent, +} from "./buttonTest"; +import { + dispatcher as buttonRetryDispatcher, + eventsHandler as buttonRetryEvent, +} from "./buttonRetry"; +import { dispatchChromakeyRequest, onChromakeyRequest } from "./chromakey"; +import { + dispatchers as toolbarMainDispatchers, + eventsHandlers as toolbarMainEvents, +} from "./toolbarMain"; +import { + dispatchers as toolbarNodeDispatchers, + eventsHandlers as toolbarNodeEvents, +} from "./toolbarNode"; +import { + dispatchers as toolbarVideoExtractionDispatchers, + eventsHandlers as toolbarVideoExtractionEvents, +} from "./toolbarVideoExtraction"; +import { + dispatchers as promptDispatchers, + events as promptEvents, +} from "./promptSettings"; +import { + dispatchers as loadingIndicatorDispatchers, + events as loadingIndicatorEvents, +} from "./loadingIndicator"; +import { + dispatchers as modelSelectionDispatchers, + events as modelSelectionEvents, +} from "./modelSelection"; +import { appMode, changeAppMode, appModeEvents } from "./appMode"; + +export const uiEvents = { + ...addMediaEvents, + onChromakeyRequest, + aiStylize: aiStylizeEvents, + buttonRetry: buttonRetryEvent, + buttonTest: buttonTestEvent, + toolbarMain: toolbarMainEvents, + toolbarNode: toolbarNodeEvents, + toolbarVideoExtraction: toolbarVideoExtractionEvents, + promptEvents: promptEvents, + loadingIndicator: loadingIndicatorEvents, + modelSelection: modelSelectionEvents, + appMode: appModeEvents, +}; +export const dispatchUiEvents = { + ...addMediaDispatchers, + dispatchChromakeyRequest, + aiStylize: aiStylizeDispatchers, + buttonRetry: buttonRetryDispatcher, + buttonTest: buttonTestDispatcher, + toolbarMain: toolbarMainDispatchers, + toolbarNode: toolbarNodeDispatchers, + toolbarVideoExtraction: toolbarVideoExtractionDispatchers, + promptSettings: promptDispatchers, + loadingIndicator: loadingIndicatorDispatchers, + modelSelection: modelSelectionDispatchers, + changeAppMode, +}; + +export { appMode }; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/loadingIndicator.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/loadingIndicator.ts new file mode 100644 index 0000000000..23b49d59c1 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/loadingIndicator.ts @@ -0,0 +1,55 @@ +import { effect, signal } from "@preact/signals-react"; + +// Model loading settings +// - Is loading visible +// - Loading progress percentage +export const DEFAULT_IS_LOADING_VISIBLE = false; +export const isLoadingVisible = signal(DEFAULT_IS_LOADING_VISIBLE); +const setIsLoadingVisible = (visible: boolean) => { + isLoadingVisible.value = visible; +}; + +const onIsLoadingVisibleChanged = (callback: (visible: boolean) => void) => { + effect(() => { + callback(isLoadingVisible.value); + }); +}; + +export const DEFAULT_LOADING_PROGRESS = 0; +export const loadingProgress = signal(DEFAULT_LOADING_PROGRESS); +const setLoadingProgress = (progress: number) => { + // Round to integer, then clamp between 0 and 100 + loadingProgress.value = Math.max(0, Math.min(100, Math.round(progress))); +}; + +const onLoadingProgressChanged = (callback: (progress: number) => void) => { + effect(() => { + callback(loadingProgress.value); + }); +}; + +// Loading text signal +export const DEFAULT_LOADING_TEXT = "Downloading"; +export const loadingText = signal(DEFAULT_LOADING_TEXT); +const setLoadingText = (text: string) => { + loadingText.value = text; +}; + +const onLoadingTextChanged = (callback: (text: string) => void) => { + effect(() => { + callback(loadingText.value); + }); +}; + +// EXPORTS +export const dispatchers = { + setIsLoadingVisible, + setLoadingProgress, + setLoadingText, +}; + +export const events = { + onIsLoadingVisibleChanged, + onLoadingProgressChanged, + onLoadingTextChanged, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/modelSelection.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/modelSelection.ts new file mode 100644 index 0000000000..966e93d673 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/modelSelection.ts @@ -0,0 +1,100 @@ +import { effect, signal } from "@preact/signals-react"; +import { models } from "~/data/models"; + +// Define the signals for selected models and download states +export const selectedModel = signal( + models.length > 0 ? models[0].id : null, +); +export const selectedLora = signal(null); + +// Replace single download tracking with a map to track multiple downloads +export interface DownloadInfo { + progress: number; + type: "model" | "lora"; +} + +// Map of modelId/loraId to download progress +export const downloadingItems = signal>({}); + +// Dispatchers +const setSelectedModel = (modelId: string | null) => { + selectedModel.value = modelId; +}; + +const setSelectedLora = (loraId: string | null) => { + selectedLora.value = loraId; +}; + +// Start downloading a model or lora +const startDownload = (id: string, type: "model" | "lora") => { + const newDownloads = { ...downloadingItems.value }; + newDownloads[id] = { progress: 0, type }; + downloadingItems.value = newDownloads; +}; + +// Update the progress of a downloading item +const updateDownloadProgress = (id: string, progress: number) => { + if (!downloadingItems.value[id]) return; + + const newDownloads = { ...downloadingItems.value }; + newDownloads[id] = { + ...newDownloads[id], + progress: Math.max(0, Math.min(100, Math.round(progress))), + }; + downloadingItems.value = newDownloads; +}; + +// Complete or cancel a download +const finishDownload = (id: string) => { + const newDownloads = { ...downloadingItems.value }; + delete newDownloads[id]; + downloadingItems.value = newDownloads; +}; + +// Check if an item is currently downloading +const isDownloading = (id: string): boolean => { + return !!downloadingItems.value[id]; +}; + +// Get the progress of a downloading item +const getDownloadProgress = (id: string): number => { + return downloadingItems.value[id]?.progress || 0; +}; + +// Event listeners +const onSelectedModelChanged = (callback: (modelId: string | null) => void) => { + effect(() => { + callback(selectedModel.value); + }); +}; + +const onSelectedLoraChanged = (callback: (loraId: string | null) => void) => { + effect(() => { + callback(selectedLora.value); + }); +}; + +const onDownloadsChanged = ( + callback: (downloads: Record) => void, +) => { + effect(() => { + callback(downloadingItems.value); + }); +}; + +// EXPORTS +export const dispatchers = { + setSelectedModel, + setSelectedLora, + startDownload, + updateDownloadProgress, + finishDownload, + isDownloading, + getDownloadProgress, +}; + +export const events = { + onSelectedModelChanged, + onSelectedLoraChanged, + onDownloadsChanged, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/promptSettings.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/promptSettings.ts new file mode 100644 index 0000000000..104add5d3b --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/promptSettings.ts @@ -0,0 +1,44 @@ +import { effect, signal } from "@preact/signals-react"; + +// Prompt settings +// - Prompt text +// - Prompt strength +export const DEFAULT_PROMPT_TEXT: string = ""; +export const promptText = signal(DEFAULT_PROMPT_TEXT); +const setPrompt = (data: string) => { + promptText.value = data; +}; + +const onPromptTextChanged = (callback: (data: string) => void) => { + effect(() => { + if (promptText.value) { + callback(promptText.value); + } + }); +}; + +export const DEFAULT_PROMPT_STRENGTH = 100; +export const promptStrength = signal(DEFAULT_PROMPT_STRENGTH); +const setPromptStrength = (data: number) => { + // Round to integer, then clamp between 0 and 100 + promptStrength.value = Math.max(0, Math.min(100, Math.round(data))); +}; + +const onPromptStrengthChanged = (callback: (data: number) => void) => { + effect(() => { + if (promptStrength.value) { + callback(promptStrength.value); + } + }); +}; + +//EXPORTS +export const dispatchers = { + setPrompt, + setPromptStrength, +}; + +export const events = { + onPromptTextChanged, + onPromptStrengthChanged, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/backgroundMenu.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/backgroundMenu.ts new file mode 100644 index 0000000000..3a08141e2a --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/backgroundMenu.ts @@ -0,0 +1,15 @@ +import { signal, effect } from "@preact/signals-react"; + +export const DEFAULT_BG_COLOR: string = "#ffffff"; +export const bgColor = signal(DEFAULT_BG_COLOR); +export const setBgColor = (data: string) => { + bgColor.value = data; +}; + +export const onBgColorChanged = (callback: (data: string) => void) => { + effect(() => { + if (bgColor.value) { + callback(bgColor.value); + } + }); +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/eraseMode.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/eraseMode.ts new file mode 100644 index 0000000000..df32951f7c --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/eraseMode.ts @@ -0,0 +1,14 @@ +import { effect, signal } from "@preact/signals-react"; +import { clamp, normalize } from "~/utilities"; +import { BRUSH_MAX_SIZE, BRUSH_MIN_SIZE, DEFAULT_PAINT_BRUSH_SIZE, paintBrushSize } from "./paintMode"; + +export const eraseBrushSize = signal(DEFAULT_PAINT_BRUSH_SIZE); +export const setEraseBrushSize = (brushSize: number) => { + eraseBrushSize.value = clamp(brushSize, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE); +} + +export const onEraseBrushSizeChanged = (callback: (normalizedSize: number) => void) => { + effect(() => { + callback(normalize(paintBrushSize.value, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE)); + }); +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/index.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/index.ts new file mode 100644 index 0000000000..24a39e5107 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/index.ts @@ -0,0 +1,23 @@ +import { buttonDispatchers, buttonEventsHandlers } from "./toolbarMain"; +import { + loadingBarRetryDispatch, + loadingBarRetryEventHandler, +} from "./loadingBar"; +import { onEraseBrushSizeChanged, setEraseBrushSize } from "./eraseMode"; +import { onPaintColorChanged, setPaintBrushSize, setPaintColor } from "./paintMode"; +import { onBgColorChanged, setBgColor } from "./backgroundMenu"; +export const dispatchers = { + ...buttonDispatchers, + loadingBarRetry: loadingBarRetryDispatch, + setPaintColor, + setPaintBrushSize, + setEraseBrushSize, + setBgColor +}; +export const eventsHandlers = { + ...buttonEventsHandlers, + loadingBarRetry: loadingBarRetryEventHandler, + onPaintColorChanged, + onEraseBrushSizeChanged, + onBgColorChanged +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/loadingBar.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/loadingBar.ts new file mode 100644 index 0000000000..e6d382d515 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/loadingBar.ts @@ -0,0 +1,28 @@ +import { MouseEventHandler } from "react"; +import { signal, effect } from "@preact/signals-react"; + +const loadingBarRetryEvent = signal< + React.MouseEvent | undefined +>(); + +let loadingBarRetryEffectCleanup: (() => void) | undefined = undefined; +export const loadingBarRetryEventHandler = { + onClick: (callback: MouseEventHandler) => { + if (loadingBarRetryEffectCleanup) { + loadingBarRetryEffectCleanup(); + } + loadingBarRetryEffectCleanup = effect(() => { + if (loadingBarRetryEvent.value) { + callback(loadingBarRetryEvent.value); + loadingBarRetryEvent.value = undefined; + } + }); + }, +}; + +export const loadingBarRetryDispatch = ( + e: React.MouseEvent, +) => { + console.log("toolbarMain > loadingBar > retry : onClick dispatched", e); + loadingBarRetryEvent.value = e; +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/paintMode.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/paintMode.ts new file mode 100644 index 0000000000..d06f4cad76 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/paintMode.ts @@ -0,0 +1,32 @@ +import { effect, signal } from "@preact/signals-react"; +import { clamp, normalize } from "~/utilities"; + +export const DEFAULT_PAINT_COLOR: string = "#ff0000"; +export const paintColor = signal(DEFAULT_PAINT_COLOR); +export const setPaintColor = (data: string) => { + paintColor.value = data; +}; + +export const BRUSH_MIN_SIZE: number = 0; +export const BRUSH_MAX_SIZE: number = 100; +// NOTE: Must be between MIN and MAX sizes - there's no runtime checks +export const DEFAULT_PAINT_BRUSH_SIZE: number = 50; +export const paintBrushSize = signal(DEFAULT_PAINT_BRUSH_SIZE); +export const setPaintBrushSize = (brushSize: number) => { + paintBrushSize.value = clamp(brushSize, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE); + console.debug(`Paint brush size set to ${paintBrushSize.value}`); +} + +export const onPaintColorChanged = (callback: (data: string) => void) => { + effect(() => { + if (paintColor.value) { + callback(paintColor.value); + } + }); +}; + +export const onPaintBrushSizeChanged = (callback: (normalizedSize: number) => void) => { + effect(() => { + callback(normalize(paintBrushSize.value, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE)); + }); +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/toolbarMain.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/toolbarMain.ts new file mode 100644 index 0000000000..60092475e9 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarMain/toolbarMain.ts @@ -0,0 +1,78 @@ +import { MouseEventHandler } from "react"; +import { signal, effect, Signal } from "@preact/signals-react"; + +import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum"; +import { AppModes } from "~/KonvaApp/constants"; + +const events = Object.values(ToolbarMainButtonNames).reduce( + (acc, buttonName) => { + acc[buttonName] = signal< + React.MouseEvent | undefined + >(); + return acc; + }, + {} as { + [key in ToolbarMainButtonNames]: Signal< + (React.MouseEvent | undefined) | undefined + >; + }, +); +const effectsCleanups = Object.values(ToolbarMainButtonNames).reduce( + (acc, buttonName) => { + acc[buttonName] = undefined; + return acc; + }, + {} as { + [key in ToolbarMainButtonNames]: (() => void) | undefined; + }, +); + +export const isToolbarMode = (buttonName: ToolbarMainButtonNames) => { + return appModeMap[buttonName] !== undefined; +} + +export const appModeMap: Record = { + [ToolbarMainButtonNames.SELECT]: AppModes.SELECT, + [ToolbarMainButtonNames.ERASER]: AppModes.ERASER, + [ToolbarMainButtonNames.PAINT]: AppModes.PAINT, +} + +const defaultHandler = (buttonName: ToolbarMainButtonNames, callback: MouseEventHandler) => { + if (effectsCleanups[buttonName]) { + effectsCleanups[buttonName](); + } + effectsCleanups[buttonName] = effect(() => { + if (events[buttonName].value) { + callback(events[buttonName].value); + events[buttonName].value = undefined; + } + }); +} + +export const buttonEventsHandlers = Object.values( + ToolbarMainButtonNames, +).reduce( + (acc, buttonName) => { + acc[buttonName] = { + onClick: (callback: MouseEventHandler) => { defaultHandler(buttonName, callback) }, + }; + return acc; + }, + {} as { + [key in ToolbarMainButtonNames]: { + onClick: (callback: MouseEventHandler) => void; + }; + }, +); + +export const buttonDispatchers = Object.values(ToolbarMainButtonNames).reduce( + (acc, buttonName) => { + acc[buttonName] = (e: React.MouseEvent) => { + events[buttonName].value = e; + }; + return acc; + }, + {} as { + [key in ToolbarMainButtonNames]: MouseEventHandler; + }, +); diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarNode.ts b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarNode.ts new file mode 100644 index 0000000000..001d3edd79 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/signals/uiEvents/toolbarNode.ts @@ -0,0 +1,153 @@ +import { MouseEventHandler } from "react"; +import { signal, effect, Signal } from "@preact/signals-react"; + +import { ToolbarNodeButtonNames as ButtonNames } from "./components/features/ToolbarNode/enums"; +const lockEvent = signal | undefined>(); +let lockEffectCleanup: (() => void) | undefined; +const lockEventHandler = (callback: MouseEventHandler) => { + if (lockEffectCleanup) { + lockEffectCleanup(); + } + lockEffectCleanup = effect(() => { + if (lockEvent.value) { + callback(lockEvent.value); + lockEvent.value = undefined; + } + }); +}; +const lockDispatcher = (e: React.MouseEvent) => { + lockEvent.value = e; +}; + +const buttonEventsRecords = Object.values(ButtonNames).reduce( + (acc, buttonName) => { + acc[buttonName] = { + eventSignal: signal< + React.MouseEvent | undefined + >(), + effectCleanup: undefined, + lastEventTimestamp: undefined, + }; + return acc; + }, + {} as { + [key in ButtonNames]: { + eventSignal: Signal< + | (React.MouseEvent | undefined) + | undefined + >; + effectCleanup: (() => void) | undefined; + lastEventTimestamp: number | undefined; + }; + }, +); + +const buttonEventsHandlers = Object.values(ButtonNames).reduce( + (acc, buttonName) => { + // We define custom events/handlers for color changes + if (buttonName === ButtonNames.COLOR) { + return acc; + } + + acc[buttonName] = { + onClick: (callback: MouseEventHandler) => { + const { eventSignal, effectCleanup, lastEventTimestamp } = + buttonEventsRecords[buttonName]; + if (effectCleanup !== undefined) { + // this clears the effect listener to rebind a new one for the onClick + effectCleanup(); + } + buttonEventsRecords[buttonName].effectCleanup = effect(() => { + if ( + eventSignal.value && + lastEventTimestamp !== eventSignal.value.timeStamp + ) { + buttonEventsRecords[buttonName].lastEventTimestamp = + eventSignal.value.timeStamp; + // console.log(buttonName, "EFFECT is triggered"); + callback(eventSignal.value); + buttonEventsRecords[buttonName].eventSignal.value = undefined; + } + }); + }, + }; + return acc; + }, + {} as { + [key in ButtonNames]: { + onClick: (callback: MouseEventHandler) => void; + }; + }, +); + +const buttonDispatchers = Object.values(ButtonNames).reduce( + (acc, buttonName) => { + acc[buttonName] = (e: React.MouseEvent) => { + // console.log(buttonName + " DISPATCHED", e); + buttonEventsRecords[buttonName].eventSignal.value = e; + }; + return acc; + }, + {} as { + [key in ButtonNames]: MouseEventHandler; + }, +); + +const retryEvent = signal | undefined>(); +let lastRetryEventTimeStamp: number | undefined = undefined; +let retryEffectCleanup: (() => void) | undefined; +const retryEventHandler = (callback: MouseEventHandler) => { + if (retryEffectCleanup) { + retryEffectCleanup(); + } + retryEffectCleanup = effect(() => { + if (retryEvent.value) { + if ( + lastRetryEventTimeStamp === undefined || + lastRetryEventTimeStamp !== retryEvent.value.timeStamp + ) { + lastRetryEventTimeStamp = retryEvent.value.timeStamp; + callback(retryEvent.value); + } + } + }); +}; + +const retryDispatcher = (e: React.MouseEvent) => { + retryEvent.value = e; +}; + +export type NodeColor = { + kNodeId: string; + color: string; +} +const selectedColorEvent = signal(); +const setSelectedColor = (nodeColor: NodeColor) => { + selectedColorEvent.value = nodeColor; +} +const onSelectedColorChanged = (callback: (color: NodeColor) => void) => { + effect(() => { + if (selectedColorEvent.value) { + callback(selectedColorEvent.value); + } + }); +}; + +export const dispatchers = { + lock: lockDispatcher, + retry: retryDispatcher, + setSelectedColor, + ...buttonDispatchers, +}; +export const eventsHandlers = { + lock: { + onClick: lockEventHandler, + }, + retry: { + onClick: retryEventHandler, + }, + color: { + onConfirmChanged: onSelectedColorChanged, + }, + ...buttonEventsHandlers, +}; diff --git a/frontend/apps/editor3d/app/src/pages/Page2d/useRenderCounter.ts b/frontend/apps/editor3d/app/src/pages/Page2d/useRenderCounter.ts new file mode 100644 index 0000000000..6b01c8c82a --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/Page2d/useRenderCounter.ts @@ -0,0 +1,14 @@ +import { useRef } from "react"; + +export const useRenderCounter = (componentName: string) => { + const renderCount = useRef(0); + renderCount.current++; + if (renderCount.current === 1) { + if (import.meta.env.DEV) { + console.log(`${componentName} rerendered ${renderCount.current} times`); + } + } else { + console.warn(`${componentName} rerendered ${renderCount.current} times`); + } + return renderCount.current; +}; diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx index 3ffc7d5b38..7d0362b66f 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx @@ -27,11 +27,12 @@ import { IsDesktopApp } from "@storyteller/tauri-utils"; import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay"; import { DemoModal } from "@storyteller/ui-demo-modal"; import { appTabId, setAppTabId } from "~/signals/appTab"; +import { KonvaCanvasContainer } from "../Page2d/KonvaCanvasContainer"; +import { KonvaRootComponent } from "../Page2d/KonvaRootComponent"; export const PageEditor = () => { useSignals(); - //console.log(api()); - //To prevent the click event from propagating to the canvas: TODO: HANDLE THIS BETTER? + const handleOverlayClick = (event: React.MouseEvent) => { event.stopPropagation(); }; @@ -159,7 +160,9 @@ export const PageEditor = () => { } const ContentFor2D = () => { - return
hello world
; + return + + ; }; return (