Compare commits

...
156 changed files with 10720 additions and 3440 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main"],"permissions":["core:default","log:default","upload:allow-download","upload:default",{"identifier":"http:default","allow":[{"url":"https://*.storyteller.ai"},{"url":"https://*.fakeyou.com"}],"deny":[{"url":"https://private.tauri.app"}]}]}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -45,10 +45,6 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const handleClick = () => {
window.location.href = "https://storyteller-2d.netlify.app/";
};
return (
<>
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
@@ -76,16 +72,6 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
disabled={false}
onTabChange={(tabId) => setAppTabId(tabId)}
/>
{/* <Button
variant="secondary"
icon={faChevronRight}
iconClassName="text-xs"
iconFlip={true}
onClick={handleClick}
className="bg-transparent p-0 text-sm text-white/80 hover:bg-transparent hover:text-white hover:underline hover:underline-offset-2"
>
Go to 2D Canvas
</Button> */}
</div>
<div className="flex items-center justify-center gap-2 font-medium">
@@ -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>
);
};
@@ -0,0 +1,14 @@
import { forwardRef, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import { useRenderCounter } from "~/hooks/useRenderCounter";
export const KonvaCanvasContainer = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
useRenderCounter("KonvaCanvasContainer");
const classes = twMerge("pegboard -z-10", className);
return <div ref={ref} className={classes} {...props} />;
});
@@ -0,0 +1,103 @@
import { useCallback, useRef, useState } from "react";
// Components
import { KonvaCanvasContainer } from "./KonvaCanvasContainer";
import { ContextualToolbarNode } from "./ContextualToolbarNode";
// import { SignaledCanvasDragDropFiles } from "./SignaledCanvasDragDropFiles";
import { SignaledDialogs } from "./SignaledDialogs";
import { SignaledToolbarMain } from "./SignaledToolbarMain";
// 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"; not needed for 2d.
import { useLayoutContext } from "./contextSignals/layout";
// common hooks
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { useNavigate } from "react-router-dom";
// TODO REFACTOR THIS INTO THE COMPONENT LIBRARY its used in 3d and 2d
import { UndoRedo } from "./components/reusable/UndoRedo/UndoRedo";
export const KonvaRootComponent = ({
className,
sceneToken,
}: {
className: string;
sceneToken?: string;
}) => {
// This is a hook that will log the number of times the component has rerendered
// Let's make sure we only log once
useRenderCounter("KonvaRootComponent");
const navigate = useNavigate();
// const appUiContext = useAppUiContext();
const layoutContext = useLayoutContext();
// Add state to track engine initialization
const [isEngineReady, setIsEngineReady] = useState(false);
const engineRef = useRef<EngineType | null>(null);
const konvaContainerCallbackRef = useCallback((node: HTMLDivElement) => {
// Only initialize if we have a node and haven't initialized yet
if (node !== null && !isEngineReady && engineRef.current === null) {
try {
const options = {
navigate: navigate,
sceneToken: sceneToken,
};
engineRef.current = KonvaApp(node, options);
// Only set ready if initialization succeeded
if (engineRef.current) {
setIsEngineReady(true);
}
} catch (error) {
console.error("Failed to initialize KonvaApp:", error);
engineRef.current = null;
setIsEngineReady(false);
}
}
}, [navigate, sceneToken, isEngineReady]);
return (
<>
{/* Always render the canvas container */}
<KonvaCanvasContainer
ref={konvaContainerCallbackRef}
className={className}
/>
{/* Conditionally render the loading overlay */}
{!isEngineReady && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-gray-500">Initializing canvas...</div>
</div>
)}
{/* Conditionally render the UI components that need the engine */}
{isEngineReady && engineRef.current && (
<UndoRedo engine={engineRef.current}>
{/* <SignaledCanvasDragDropFiles
openAddImage={appUiContext.openAddImage}
openAddVideo={appUiContext.openAddVideo}
/> */}
{/* /<PromptBox /> */}
<SignaledToolbarMain
layoutSignal={layoutContext.signal}
appUiContext={appUiContext}
/>
<SignaledDialogs
appUiSignal={appUiContext.signal}
resetAll={appUiContext.resetAll}
/>
<ContextualToolbarNode />
</UndoRedo>
)}
</>
);
};
@@ -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;
};
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useContext, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useSignals } from "@preact/signals-react/runtime";
import { LoadingDots, TopBar } from "~/components";
@@ -16,22 +16,35 @@ import {
editorLoader,
cameraAspectRatio,
outlinerIsShowing,
disableHotkeyInput,
enableHotkeyInput,
gridVisibility,
setGridVisibility,
} from "~/pages/PageEnigma/signals";
import { EditorCanvas } from "./comps/EngineCanvases";
import { SceneContainer } from "./comps/SceneContainer";
import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { PromptBox3D } from "@storyteller/ui-promptbox";
import { PopoverItem } from "@storyteller/ui-popover";
import { OnboardingHelper } from "./comps/OnboardingHelper";
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";
import { addCamera, cameras, deleteCamera, focalLengthDragging, selectedCameraId, updateCamera } from "./signals/camera";
import { isPromptBoxFocused } from "./signals/promptBox";
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
import { EngineContext } from "./contexts/EngineContext";
import Queue, { QueueNames } from "./Queue";
import { toEngineActions } from "./Queue/toEngineActions";
import { UnionedDataTypes } from "./Queue/Queue";
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();
};
@@ -74,14 +87,164 @@ export const PageEditor = () => {
return scaleHeight;
};
return (
<div className="w-screen">
<TopBar
pageName="Edit Scene"
appTabIdSignal={appTabId}
setAppTabId={setAppTabId}
/>
<OnboardingHelper />
// These are callbacks required by promptbox
const editorEngine = useContext(EngineContext);
const handleCameraSelect = (selectedItem: PopoverItem) => {
const selectedCamera = cameras.value.find(
(cam) => cam.label === selectedItem.label,
);
if (selectedCamera && editorEngine) {
selectedCameraId.value = selectedCamera.id;
// Show focal length display temporarily
// TODO: Rename dragging to visible - BFlat
focalLengthDragging.value = {
isDragging: true,
focalLength: selectedCamera.focalLength,
};
setTimeout(() => {
focalLengthDragging.value = {
isDragging: false,
focalLength: selectedCamera.focalLength,
};
}, 1500);
// Update the main camera to match the selected camera's properties
if (editorEngine.camera) {
// First update position and lookAt
editorEngine.camera.position.set(
selectedCamera.position.x,
selectedCamera.position.y,
selectedCamera.position.z,
);
editorEngine.camera.lookAt(
selectedCamera.lookAt.x,
selectedCamera.lookAt.y,
selectedCamera.lookAt.z,
);
// Update FOV
editorEngine.camera.fov = editorEngine.focalLengthToFov(
selectedCamera.focalLength,
);
editorEngine.camera.updateProjectionMatrix();
// Reset and update camera controls
if (editorEngine.cameraViewControls) {
editorEngine.cameraViewControls.reset();
editorEngine.cameraViewControls.update(0);
}
// Force a render to update the view
editorEngine.renderScene();
// Queue an update to ensure the engine processes the camera change
// Queue.publish({
// queueName: QueueNames.TO_ENGINE,
// action: toEngineActions.CAMERA_CHANGED,
// data: selectedCamera,
// });
}
// Force update camera properties in the state
updateCamera(selectedCamera.id, {
focalLength: selectedCamera.focalLength,
position: selectedCamera.position,
rotation: selectedCamera.rotation,
lookAt: selectedCamera.lookAt,
});
}
};
const handleAddCamera = () => {
// Check if we've reached the maximum number of cameras
if (cameras.value.length >= 6) {
console.warn("Maximum number of cameras (6) reached");
return;
}
const newIndex = cameras.value.length + 1;
const newId = `cam${newIndex}`;
// This is for generating random orbital position for the new camera using spherical coordinates
const radius = Math.random() * 5 + 7; // Distance from center: 7 to 12 units
const theta = Math.random() * Math.PI * 2; // Azimuthal angle: 0 to 2π
const phi = Math.PI / 3 + (Math.random() * Math.PI) / 6; // Polar angle: π/3 to π/2 (60° to 90° from horizontal)
// Convert spherical coordinates to Cartesian coordinates
const randomX = radius * Math.sin(phi) * Math.cos(theta);
const randomY = Math.abs(radius * Math.cos(phi)) + 2; // Ensure Y is positive and at least 2 units up
const randomZ = radius * Math.sin(phi) * Math.sin(theta);
addCamera({
id: newId,
label: `Camera ${newIndex}`,
focalLength: 24,
position: {
x: randomX,
y: randomY,
z: randomZ,
},
rotation: { x: 0, y: 0, z: 0 },
lookAt: { x: 0, y: 0, z: 0 },
});
// Switch to the newly created camera
selectedCameraId.value = newId;
// Update the engine camera to match the new camera's properties
if (editorEngine && editorEngine.camera) {
editorEngine.camera.position.set(randomX, randomY, randomZ);
editorEngine.camera.lookAt(0, 0, 0);
editorEngine.camera.fov = editorEngine.focalLengthToFov(24);
editorEngine.camera.updateProjectionMatrix();
// Reset and update camera controls
if (editorEngine.cameraViewControls) {
editorEngine.cameraViewControls.reset();
editorEngine.cameraViewControls.update(0);
}
// Force a render to update the view
editorEngine.renderScene();
// Queue.publish({
// queueName: QueueNames.TO_ENGINE,
// action: toEngineActions.CAMERA_CHANGED,
// data: {
// id: newId,
// label: `Camera ${newIndex}`,
// focalLength: 24,
// position: { x: randomX, y: randomY, z: randomZ },
// rotation: { x: 0, y: 0, z: 0 },
// lookAt: { x: 0, y: 0, z: 0 },
// },
// });
}
};
const handleCameraNameChange = (id: string, newName: string) => {
updateCamera(id, { label: newName });
};
const handleCameraFocalLengthChange = (id: string, value: number) => {
const camera = cameras.value.find((cam) => cam.id === id);
if (camera) {
updateCamera(id, { focalLength: value });
}
};
const onAspectRatioSelect = (newRatio: UnionedDataTypes) => {
// Publish the change to the engine
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.CHANGE_CAMERA_ASPECT_RATIO,
data: newRatio,
});
}
const ContentFor3D = () => {
return <> <OnboardingHelper />
<div
className="relative flex w-screen"
style={{ height: "calc(100vh - 68px)" }}
@@ -136,7 +299,34 @@ export const PageEditor = () => {
<ControlPanelSceneObject />
</div>
<PromptBox />
<PromptBox3D
cameras={cameras}
cameraAspectRatio={cameraAspectRatio}
disableHotkeyInput={disableHotkeyInput}
enableHotkeyInput={enableHotkeyInput}
gridVisibility={gridVisibility}
setGridVisibility={setGridVisibility}
selectedCameraId={selectedCameraId}
deleteCamera={deleteCamera}
focalLengthDragging={focalLengthDragging}
isPromptBoxFocused={isPromptBoxFocused}
uploadImage={uploadImage}
handleCameraSelect={handleCameraSelect}
handleAddCamera={handleAddCamera}
handleCameraNameChange={handleCameraNameChange}
handleCameraFocalLengthChange={handleCameraFocalLengthChange}
onAspectRatioSelect={onAspectRatioSelect}
setEnginePrompt={(prompt) => {
if (!editorEngine) {
return;
}
editorEngine!.positive_prompt = prompt
}}
snapshotCurrentFrame={
editorEngine?.snapShotOfCurrentFrame
}
/>
<LoadingDots
className="absolute left-0 top-0 z-50"
@@ -146,18 +336,7 @@ export const PageEditor = () => {
/>
</div>
</div>
{/* Side panel */}
{/* <div onClick={handleOverlayClick}>
<SidePanel />
</div> */}
</div>
{/* Timeline */}
{/* <div onClick={handleOverlayClick}>
<Timeline />
</div> */}
<DemoModal
title="Welcome to ArtCraft 3D"
subTitle="Your 3D editor for digital art and design"
@@ -172,6 +351,23 @@ export const PageEditor = () => {
}
}}
/>
</>
}
const ContentFor2D = () => {
return <KonvaCanvasContainer>
<KonvaRootComponent className="w-full h-full" />
</KonvaCanvasContainer>;
};
return (
<div className="w-screen">
<TopBar
pageName="Edit Scene"
appTabIdSignal={appTabId}
setAppTabId={setAppTabId}
/>
{appTabId.value === "3D" ? <ContentFor3D /> : <ContentFor2D />}
</div>
);
};
File diff suppressed because it is too large Load Diff
-30
View File
@@ -1,30 +0,0 @@
{
"name": "proxy",
"version": "1.0.0",
"description": "",
"main": "src/proxy.ts",
"type": "module",
"types": "./types",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node --import=tsx src/proxy.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"canvas": "^2.11.2",
"common": "file:../common",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.3",
"http-proxy-middleware": "^3.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/ffmpeg-static": "^3.0.3",
"@types/fluent-ffmpeg": "^2.1.24",
"tsx": "^4.11.2"
}
}
@@ -1,12 +0,0 @@
#!/bin/bash
ffmpeg -i ./$1/videos/step1.mp4 \
-f lavfi \
-i anullsrc \
-max_muxing_queue_size 999999 \
-vf select='gte(n\,1)',scale=1024:576 \
-c:v libx264 \
-c:a aac \
-shortest \
-pix_fmt yuv420p \
-f mp4 ./$1/videos/tmp0.mp4;
-75
View File
@@ -1,75 +0,0 @@
import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import cors from "cors";
const app = express();
app.use(cors());
app.use(
"/v1",
createProxyMiddleware({
target: "https://api.storyteller.ai/v1",
changeOrigin: true,
secure: true,
logger: console,
}),
);
app.use(
"/google",
createProxyMiddleware({
target: "https://storage.googleapis.com",
changeOrigin: true,
secure: true,
// logger: console,
}),
);
app.use(
"/funnel",
createProxyMiddleware({
// target: "https://funnel.tailce84f.ts.net",
target: "https://style.storyteller.ai",
changeOrigin: true,
secure: true,
// logger: console,
}),
);
app.use(
"/cdn",
createProxyMiddleware({
target: "https://cdn.storyteller.ai",
changeOrigin: true,
secure: true,
// logger: console,
}),
);
app.use(
"/vocodes",
createProxyMiddleware({
target: "https://storage.googleapis.com/vocodes-public",
changeOrigin: true,
secure: true,
// logger: console,
}),
);
app.use(
"/gravatar",
createProxyMiddleware({
target: "https://www.gravatar.com",
changeOrigin: true,
secure: true,
logger: console,
}),
);
// app.use(express.json({ limit: "150mb" }));
// app.post("/upload-video", uploadVideo);
app.listen(3000, () => {
console.log("Listening on port 3000");
});
@@ -1,24 +0,0 @@
{
"include": [
"env.d.ts",
"**/*.ts",
"**/*.tsx",
"netlify-edge-plugin.ts",
"src/proxy.ts"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"isolatedModules": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": "./src",
"noEmit": true
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# @storyteller/enums
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/enums` to execute the unit tests via [Vitest](https://vitest.dev/).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@storyteller/enums",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
export * from "./lib/EditorState";
export * from "./lib/Queue";
export * from "./lib/UploaderStates";
export * from "./lib/Hotkeys";
export * from "./lib/Toaster";
@@ -0,0 +1,13 @@
export enum EditorStates {
EDIT,
CAMERA_VIEW,
PREVIEW,
}
export enum CameraAspectRatio {
HORIZONTAL_16_9 = "horizontal_16_9",
VERTICAL_9_16 = "vertical_9_16",
SQUARE_1_1 = "SQUARE_1_1",
HORIZONTAL_3_2 = "horizontal_3_2",
VERTICAL_2_3 = "vertical_2_3",
}
@@ -0,0 +1,6 @@
export enum DomLevels {
NONE,
INPUT,
PANEL,
DIALOGUE,
}
@@ -0,0 +1,10 @@
export enum AssetType {
ANIMATION = "animation",
AUDIO = "audio",
CAMERA = "camera",
CHARACTER = "character",
EXPRESSION = "expression",
OBJECT = "object",
SKYBOX = "skybox",
SHAPE = "shape",
}
@@ -0,0 +1,7 @@
export enum ClipGroup {
CHARACTER = "character",
CAMERA = "camera",
GLOBAL_AUDIO = "global_audio",
OBJECT = "object",
PROMPT_TRAVEL = "prompt",
}
@@ -0,0 +1,8 @@
export enum ClipType {
TRANSFORM = "transform",
AUDIO = "audio",
ANIMATION = "animation",
EXPRESSION = "expression",
FAKE = "fake",
PROMPT_TRAVEL = "prompt",
}
@@ -0,0 +1,14 @@
export enum fromEngineActions {
ADD_OBJECT = "add_object",
CAMERA_ASPECT_RATIO_CHANGED = "camera_aspect_ratio_changed",
// data = CameraAspectRatio
DELETE_OBJECT = "delete_object",
DESELECT_OBJECT = "deselect_object",
RESET_TIMELINE = "reset_timeline",
SELECT_OBJECT = "select_object",
UPDATE_CHARACTER_ID = "update_character_id",
UPDATE_TIME = "update_time",
UPDATE_TIME_LINE = "update_timeline",
POP_A_TOAST = "pop_a_toast",
// data={currentTime: number}
}
@@ -0,0 +1,5 @@
export enum QueueNames {
FROM_ENGINE = "FROM_ENGINE",
TO_ENGINE = "TO_ENGINE",
TO_TIMELINE = "TO_TIMELINE",
}
@@ -0,0 +1,47 @@
export enum toEngineActions {
ADD_CLIP = "add_clip",
// data=QueueClip
ADD_KEYFRAME = "add_keyframe",
ADD_CHARACTER = "add_character",
ADD_OBJECT = "add_object",
ADD_SHAPE = "add_shape",
// data= QueueKeyframe
DELETE_CLIP = "delete_clip",
// data=QueueClip
DELETE_KEYFRAME = "delete_keyframe",
// data=QueueKeyframe
ENTER_EDIT_STATE = "enter_edit",
// data=null
ENTER_PREVIEW_STATE = "enter_preview",
// data=null
GENERATE_VIDEO = "generate_video",
// data=null
MUTE = "mute",
// data={version, type, group, object_uuid} if lipsync
// data={version, type, group} if global_audio
REFRESH_PREVIEW = "refresh_preview",
// data=QueueClip
TOGGLE_CAMERA_STATE = "toggle_camera",
// data=null
TOGGLE_REPEATING = "toggle_repeating",
// data=null
CHANGE_CAMERA_ASPECT_RATIO = "change_camera_aspect_ratio",
// data=CamperaAspectRatio
CAMERA_CHANGED = "camera_changed",
UNMUTE = "unmute",
// data={version, type, group, object_uuid} if lipsync
// data={version, type, group} if global_audio
UPDATE_CLIP = "update_clip",
// data=QueueClip
UPDATE_KEYFRAME = "update_keyframe",
// data=QueueKeyframe
UPDATE_TIME = "update_time",
// data={currentTime: number}
}
@@ -0,0 +1,4 @@
export enum toTimelineActions {
ADD_KEYFRAME = "add_keyframe",
GENERATE_PROGRESS = "generate_progress",
}
@@ -0,0 +1,7 @@
export * from "./AssetType";
export * from "./QueueNames";
export * from "./FromEngineActions";
export * from "./ToEngineActions";
export * from "./ToTimelineActions";
export * from "./ClipType";
export * from "./ClipGroup";
@@ -0,0 +1,5 @@
export enum ToastTypes {
SUCCESS,
WARNING,
ERROR,
}
@@ -0,0 +1 @@
export * from "./ToastTypes";
@@ -0,0 +1,12 @@
export enum UploaderStates {
ready,
uploadingAsset,
uploadingImage,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
imageCreateError,
}
+13
View File
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+49
View File
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/common/enums',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/enums',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/interfaces
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/interfaces` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/interfaces",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,8 @@
export * from "./lib/Camera";
export * from "./lib/UploadImage";
export * from "./lib/IGenerationOptions";
export * from "./lib/UpdateTime";
export * from "./lib/Queue";
export * from "./lib/MediaItem";
export * from "./lib/UploadImage";
export * from "./lib/UploadedState";
@@ -0,0 +1,13 @@
export interface Camera {
id: string;
label: string;
focalLength: number;
position: { x: number; y: number; z: number };
rotation: { x: number; y: number; z: number };
lookAt: { x: number; y: number; z: number };
}
export interface FocalLengthDragging {
isDragging: boolean;
focalLength: number;
}
@@ -0,0 +1,9 @@
export interface IGenerationOptions {
upscale: boolean;
faceDetail: boolean;
styleStrength: number;
lipSync: boolean;
cinematic: boolean;
globalIpAdapterImageMediaToken: string | null;
enginePreProcessing: boolean;
}
@@ -0,0 +1,18 @@
import { AssetType } from "@storyteller/enums";
export interface MediaItem {
version: number;
type: AssetType;
maybe_animation_type?: string;
media_type?: string;
media_id: string;
object_uuid?: string;
name: string;
description?: string;
publicBucketPath?: string;
length?: number;
thumbnail?: string;
isMine?: boolean;
isBookmarked?: boolean;
imageIndex?: number;
}
@@ -0,0 +1,28 @@
import { ClipGroup, ClipType } from "@storyteller/enums";
import { XYZ } from "@storyteller/types";
export interface QueueClip {
version: number;
type: ClipType;
group: ClipGroup;
object_uuid?: string;
clip_uuid?: string;
media_id?: string;
name?: string;
offset?: number;
length?: number;
selected?: boolean;
}
export interface QueueKeyframe {
version: number;
keyframe_uuid?: string;
group: ClipGroup;
object_uuid: string;
object_name?: string;
offset?: number;
position: XYZ;
rotation: XYZ;
scale: XYZ;
selected?: boolean;
}
@@ -0,0 +1,3 @@
export interface UpdateTime {
currentTime: number;
}
@@ -0,0 +1,7 @@
import { UploaderState } from "./UploadedState";
export interface UploadImageArgs {
title: string;
assetFile: File;
progressCallback: (newState: UploaderState) => void;
}
@@ -0,0 +1,7 @@
import { UploaderStates } from "@storyteller/enums";
export interface UploaderState {
status: UploaderStates;
errorMessage?: string;
data?: string;
}
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/common/interfaces',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/interfaces',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# @storyteller/types
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/types` to execute the unit tests via [Vitest](https://vitest.dev/).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@storyteller/types",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib';
@@ -0,0 +1,3 @@
export * from './queue';
export * from './toast';
export * from './xyz';

Some files were not shown because too many files have changed in this diff Show More