Migrating 2d editor to 3d editor resolving a bunch of conflicts

This commit is contained in:
Michael Chung committed 2025-05-06 17:18:52 -04:00
1 parent 10caf36d2f
commit 05fbd2ff03
48 files changed
+2076 -11

No files matched your search

@@ -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<HTMLButtonElement>;
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 (
<Transition
as="div"
show={isShowing}
className={twMerge(
transitionTimingStyles,
"fixed -translate-x-1/2 translate-y-5 data-[closed]:opacity-0",
)}
style={{
top: boundedTopPosition,
left: position.x,
}}
>
<ToolbarNode
{...rest}
buttonsProps={buttonsProps}
locked={uiAccess.toolbarNode.signal.value.locked}
onLockClicked={(e) => {
dispatchUiEvents.toolbarNode.lock(e);
}}
color={color}
onColorChange={handleColorChange}
/>
</Transition>
);
};
@@ -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 && (
<UndoRedo engine={engineRef.current}>
<SignaledCanvasDragDropFiles
{/* <SignaledCanvasDragDropFiles
openAddImage={appUiContext.openAddImage}
openAddVideo={appUiContext.openAddVideo}
/>
<PromptBox />
/> */}
{/* /<PromptBox /> */}
<SignaledToolbarMain
layoutSignal={layoutContext.signal}
appUiContext={appUiContext}
@@ -0,0 +1,59 @@
import { useCallback } from "react";
import { Signal } from "@preact/signals-react";
import { dispatchUiEvents } from "./signals/uiEvents";
import {
DialogAddImage,
DialogEditText,
DialogError,
} from "~/components/features";
import { AppUiSignalType } from "./contextSignals/appUi";
import { dialogError } from "~/signals/uiAccess/dialogError";
export const SignaledDialogs = ({
appUiSignal,
resetAll,
}: {
appUiSignal: Signal<AppUiSignalType>;
resetAll: () => void;
}) => {
return (
<>
{/* Dialogs that are opened from within Reactland*/}
<DialogAddImage
isOpen={appUiSignal.value.isAddImageOpen ?? false}
stagedImage={appUiSignal.value.stagedImage}
closeCallback={resetAll}
onAddImage={(file) => {
dispatchUiEvents.addImageToEngine(file);
}}
/>
<DialogEditText
isOpen={appUiSignal.value.isEditTextOpen ?? false}
onDoneEditText={(data) => {
dispatchUiEvents.addTextToEngine(data);
}}
closeCallback={resetAll}
/>
{/* Dialogs that are opened from the Engine*/}
<SignaledDialogError />
</>
);
};
const SignaledDialogError = () => {
const props = dialogError.signal.value;
const { isShowing, title, message } = props;
const onClose = useCallback(() => {
dialogError.hide();
}, []);
return (
<DialogError
isShowing={isShowing}
title={title}
message={message}
onClose={onClose}
/>
);
};
@@ -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]",
);
@@ -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>(AUTH_STATUS.INIT);
const userInfo = signal<UserInfo | undefined>(undefined);
const activeSubs = signal<ActiveSubscriptions | undefined>(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,
};
@@ -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",
}
@@ -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);
}
@@ -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,
};
@@ -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();
};
@@ -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;
}
};
@@ -0,0 +1 @@
export * from "./generationSignals";
@@ -0,0 +1,4 @@
export * from "./authentication";
export * from "./uiAccess";
export * from "./uiEvents";
export * from "./generation";
@@ -0,0 +1,50 @@
import { signal } from "@preact/signals-react";
import { ContextualButtonRetryProps } from "./type";
const buttonRetrySignal = signal<ContextualButtonRetryProps>({
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<ContextualButtonRetryProps, "isShowing">) {
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,
};
},
};
@@ -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,
};
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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,
};
@@ -0,0 +1,86 @@
import { signal } from "@preact/signals-react";
import { ContextualLoadingBarProps } from "./type";
import { LoadingBarStatus } from "~/components/ui/LoadingBar";
const loadingBarSignal = signal<ContextualLoadingBarProps>({
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<Omit<ContextualLoadingBarProps, "isShowing">>) {
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<Omit<ContextualLoadingBarProps, "isShowing">>) {
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,
};
}
},
};
@@ -0,0 +1,32 @@
import { signal } from "@preact/signals-react";
type MagicBoxSignalType = {
isShowing: boolean;
orientation: "vertical" | "horizontal";
scale: number;
};
const magicBoxSignal = signal<MagicBoxSignalType>({
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<MagicBoxSignalType>) => {
magicBoxSignal.value = { ...magicBoxSignal.value, ...updatedVals };
},
};
@@ -0,0 +1,7 @@
import { toolbarMain as toolbarMainOnly } from "./toolbarMain";
import { loadingBar } from "./loadingBar";
export const toolbarMain = {
...toolbarMainOnly,
loadingBar,
};
@@ -0,0 +1,47 @@
import { signal } from "@preact/signals-react";
import { LoadingBarStatus } from "~/components/ui";
import { LoadingBarInterface } from "../type";
const loadingBarSignal = signal<LoadingBarInterface>({
isShowing: false,
progress: 0,
status: LoadingBarStatus.IDLE,
message: undefined,
});
export const loadingBar = {
signal: loadingBarSignal,
update: (props: Partial<LoadingBarInterface>) => {
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<LoadingBarInterface, "isShowing">) => {
const mergedProps = props
? { ...props, ...loadingBarSignal.value }
: loadingBarSignal.value;
loadingBarSignal.value = {
...mergedProps,
isShowing: true,
};
},
hide: () => {
loadingBarSignal.value = { ...loadingBarSignal.value, isShowing: false };
},
};
@@ -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<ToolbarMainSignalInterface>({
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"];
}
@@ -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<Omit<ContextualToolbarProps, "buttonStates">> {
buttonStates?: Partial<ButtonStates>;
}
const toolbarNodeSignal = signal<ContextualToolbarProps>({
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<ContextualToolbarProps>) {
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<ButtonStates> }) {
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);
}
@@ -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<ToolbarVideoExtractionProps>({
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<ToolbarVideoExtractionProps>) {
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);
}
@@ -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<LoadingBarProps, "position"> {
width: number;
}
export interface ContextualButtonRetryProps extends ContextualUi {
disabled: boolean;
}
export interface LoadingBarInterface {
isShowing: boolean;
progress: number;
status: LoadingBarStatus;
message?: string;
}
@@ -0,0 +1,78 @@
import { signal, effect } from "@preact/signals-react";
import { ShapeNodeData, TextNodeData } from "~/KonvaApp/types";
// ADDING IMAGES
const stagedImage = signal<File | null>(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<VideoSignal | null>(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<TextNodeData | null>(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<ShapeNodeData | null>(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,
};
@@ -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<AiStylizeSingalType>();
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,
};
@@ -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<AppModeType>("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);
}
};
},
};
@@ -0,0 +1,29 @@
import { MouseEventHandler } from "react";
import { signal, effect } from "@preact/signals-react";
const event = signal<
React.MouseEvent<HTMLButtonElement, MouseEvent> | undefined
>(undefined);
let effectCleanup: (() => void) | undefined = undefined;
export const eventsHandler = {
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => {
if (effectCleanup) {
effectCleanup();
}
effectCleanup = effect(() => {
if (event.value) {
callback(event.value);
event.value = undefined;
}
});
},
};
export const dispatcher = {
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
console.log("Button Retry dispatcher");
event.value = e;
},
};
@@ -0,0 +1,26 @@
// import { MouseEventHandler } from "react";
import { signal, effect } from "@preact/signals-react";
const event = signal<number | undefined>();
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();
},
};
@@ -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 };
@@ -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<boolean>(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<number>(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<string>(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,
};
@@ -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<string | null>(
models.length > 0 ? models[0].id : null,
);
export const selectedLora = signal<string | null>(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<Record<string, DownloadInfo>>({});
// 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<string, DownloadInfo>) => void,
) => {
effect(() => {
callback(downloadingItems.value);
});
};
// EXPORTS
export const dispatchers = {
setSelectedModel,
setSelectedLora,
startDownload,
updateDownloadProgress,
finishDownload,
isDownloading,
getDownloadProgress,
};
export const events = {
onSelectedModelChanged,
onSelectedLoraChanged,
onDownloadsChanged,
};
@@ -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<string>(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<number>(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,
};
@@ -0,0 +1,15 @@
import { signal, effect } from "@preact/signals-react";
export const DEFAULT_BG_COLOR: string = "#ffffff";
export const bgColor = signal<string>(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);
}
});
};
@@ -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<number>(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));
});
};
@@ -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
};
@@ -0,0 +1,28 @@
import { MouseEventHandler } from "react";
import { signal, effect } from "@preact/signals-react";
const loadingBarRetryEvent = signal<
React.MouseEvent<HTMLButtonElement> | undefined
>();
let loadingBarRetryEffectCleanup: (() => void) | undefined = undefined;
export const loadingBarRetryEventHandler = {
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => {
if (loadingBarRetryEffectCleanup) {
loadingBarRetryEffectCleanup();
}
loadingBarRetryEffectCleanup = effect(() => {
if (loadingBarRetryEvent.value) {
callback(loadingBarRetryEvent.value);
loadingBarRetryEvent.value = undefined;
}
});
},
};
export const loadingBarRetryDispatch = (
e: React.MouseEvent<HTMLButtonElement>,
) => {
console.log("toolbarMain > loadingBar > retry : onClick dispatched", e);
loadingBarRetryEvent.value = e;
};
@@ -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<string>(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<number>(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));
});
};
@@ -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<HTMLButtonElement, MouseEvent> | undefined
>();
return acc;
},
{} as {
[key in ToolbarMainButtonNames]: Signal<
(React.MouseEvent<HTMLButtonElement, 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<string, AppModes> = {
[ToolbarMainButtonNames.SELECT]: AppModes.SELECT,
[ToolbarMainButtonNames.ERASER]: AppModes.ERASER,
[ToolbarMainButtonNames.PAINT]: AppModes.PAINT,
}
const defaultHandler = (buttonName: ToolbarMainButtonNames, callback: MouseEventHandler<HTMLButtonElement>) => {
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<HTMLButtonElement>) => { defaultHandler(buttonName, callback) },
};
return acc;
},
{} as {
[key in ToolbarMainButtonNames]: {
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => void;
};
},
);
export const buttonDispatchers = Object.values(ToolbarMainButtonNames).reduce(
(acc, buttonName) => {
acc[buttonName] = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
events[buttonName].value = e;
};
return acc;
},
{} as {
[key in ToolbarMainButtonNames]: MouseEventHandler<HTMLButtonElement>;
},
);
@@ -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<React.MouseEvent<HTMLButtonElement> | undefined>();
let lockEffectCleanup: (() => void) | undefined;
const lockEventHandler = (callback: MouseEventHandler<HTMLButtonElement>) => {
if (lockEffectCleanup) {
lockEffectCleanup();
}
lockEffectCleanup = effect(() => {
if (lockEvent.value) {
callback(lockEvent.value);
lockEvent.value = undefined;
}
});
};
const lockDispatcher = (e: React.MouseEvent<HTMLButtonElement>) => {
lockEvent.value = e;
};
const buttonEventsRecords = Object.values(ButtonNames).reduce(
(acc, buttonName) => {
acc[buttonName] = {
eventSignal: signal<
React.MouseEvent<HTMLButtonElement, MouseEvent> | undefined
>(),
effectCleanup: undefined,
lastEventTimestamp: undefined,
};
return acc;
},
{} as {
[key in ButtonNames]: {
eventSignal: Signal<
| (React.MouseEvent<HTMLButtonElement, 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<HTMLButtonElement>) => {
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<HTMLButtonElement>) => void;
};
},
);
const buttonDispatchers = Object.values(ButtonNames).reduce(
(acc, buttonName) => {
acc[buttonName] = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// console.log(buttonName + " DISPATCHED", e);
buttonEventsRecords[buttonName].eventSignal.value = e;
};
return acc;
},
{} as {
[key in ButtonNames]: MouseEventHandler<HTMLButtonElement>;
},
);
const retryEvent = signal<React.MouseEvent<HTMLButtonElement> | undefined>();
let lastRetryEventTimeStamp: number | undefined = undefined;
let retryEffectCleanup: (() => void) | undefined;
const retryEventHandler = (callback: MouseEventHandler<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {
retryEvent.value = e;
};
export type NodeColor = {
kNodeId: string;
color: string;
}
const selectedColorEvent = signal<NodeColor | null>();
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,
};
@@ -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;
};
@@ -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<HTMLDivElement>) => {
event.stopPropagation();
};
@@ -159,7 +160,9 @@ export const PageEditor = () => {
}
const ContentFor2D = () => {
return <div>hello world</div>;
return <KonvaCanvasContainer>
<KonvaRootComponent className="w-full h-full"/>
</KonvaCanvasContainer>;
};
return (