mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
Migrating 2d editor to 3d editor resolving a bunch of conflicts
This commit is contained in:
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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,
|
||||
};
|
||||
+47
@@ -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 };
|
||||
},
|
||||
};
|
||||
+68
@@ -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);
|
||||
}
|
||||
+96
@@ -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,
|
||||
};
|
||||
+15
@@ -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
|
||||
};
|
||||
+28
@@ -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));
|
||||
});
|
||||
};
|
||||
+78
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user