Compare commits

..
Author SHA1 Message Date
Brandon Thomas dde547ad1c remove lots of old code from repo 2025-08-24 15:24:54 -04:00
Brandon Thomas f0e8bec162 cleanup titlebar setup 2025-08-24 15:01:31 -04:00
10 changed files with 71 additions and 277 deletions
@@ -22,7 +22,6 @@ import {
GalleryModal,
galleryModalVisibleViewMode,
galleryModalVisibleDuringDrag,
galleryModalLightboxVisible,
} from "@storyteller/ui-gallery-modal";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { Tooltip } from "@storyteller/ui-tooltip";
@@ -49,8 +48,6 @@ import {
useTauriWindowControls,
useTauriPlatform,
} from "@storyteller/tauri-utils";
import { useEditStore } from "../../../pages/PageEdit/stores/EditState";
import { BaseSelectorImage } from "../../../pages/PageEdit/BaseImageSelector";
interface Props {
pageName: string;
@@ -140,30 +137,6 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
}
};
const handleEditFromGallery = async (url: string, mediaId?: string) => {
try {
// Reset editor state
useEditStore.getState().RESET();
// Switch to EDIT tab
useTabStore.getState().setActiveTab("EDIT");
// Select the image for editing
const baseImage: BaseSelectorImage = {
url,
mediaToken: mediaId || "",
};
useEditStore.getState().setBaseImageInfo(baseImage);
// Close gallery modal and lightbox if open
galleryModalVisibleViewMode.value = false;
galleryModalVisibleDuringDrag.value = false;
galleryModalLightboxVisible.value = false;
} catch (e) {
// no-op
}
};
const getPageTitle = (): string => {
switch (tabStore.activeTabId) {
case "2D":
@@ -198,9 +171,7 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
>
<div className="flex items-center gap-3" data-tauri-drag-region>
<div className="mr-2" data-tauri-drag-region>
<span className="sr-only" data-tauri-drag-region>
ArtCraft
</span>
<span className="sr-only" data-tauri-drag-region>ArtCraft</span>
<img
className="h-[24px] w-auto"
src="/resources/images/artcraft-logo-3.png"
@@ -324,11 +295,7 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
globalAccountLogoutCallback={() => setLogoutStates()}
/>
<GalleryModal
mode="view"
onDownloadClicked={downloadFile}
onEditClicked={handleEditFromGallery}
/>
<GalleryModal mode="view" onDownloadClicked={downloadFile} />
</>
);
};
@@ -1,8 +1,7 @@
import { Button } from "@storyteller/ui-button";
import { useImageEditCompleteEvent } from "@storyteller/tauri-events";
import { EditedImage, useImageEditCompleteEvent } from "@storyteller/tauri-events";
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { BaseSelectorImage } from "./BaseImageSelector";
export interface ImageBundle {
@@ -17,74 +16,43 @@ interface HistoryStackProps {
export const HistoryStack = ({
onClear,
onImageSelect = () => {},
onImageSelect = () => { },
startingBundles = [],
}: HistoryStackProps) => {
const [imageBundles, setImageBundles] =
useState<ImageBundle[]>(startingBundles);
const [selectedImageToken, setSelectedImageToken] = useState<string | null>(
startingBundles.length > 0 && startingBundles[0].images.length > 0
? startingBundles[0].images[0].mediaToken
: null,
);
const [imageBundles, setImageBundles] = useState<ImageBundle[]>(startingBundles);
useImageEditCompleteEvent(async (event) => {
const newBundle: ImageBundle = {
images: event.edited_images.map(
(editedImage) =>
({
url: editedImage.cdn_url,
mediaToken: editedImage.media_token,
}) as BaseSelectorImage,
),
images: event.edited_images.map((editedImage) => ({
url: editedImage.cdn_url,
mediaToken: editedImage.media_token,
} as BaseSelectorImage)),
};
setImageBundles([...imageBundles, newBundle]);
const newlySelected = newBundle.images[0];
if (newlySelected) {
setSelectedImageToken(newlySelected.mediaToken);
onImageSelect(newlySelected);
}
imageBundles.push(newBundle);
setImageBundles(imageBundles);
});
const handleClear = () => {
setImageBundles(startingBundles);
setSelectedImageToken(null);
onClear();
};
}
return (
<div className="max-h-1/2 glass h-auto w-16 overflow-y-auto rounded-lg p-1">
<div className="flex flex-col-reverse items-center justify-center gap-2">
<Button
className="h-7 w-full rounded-md opacity-80"
iconClassName="h-3 w-3"
icon={faTrash}
type="reset"
onClick={handleClear}
variant="destructive"
/>
<div className="w-16 h-auto max-h-1/2 overflow-y-auto rounded-lg bg-white">
<div className="flex flex-col-reverse items-center justify-center">
<Button icon={faTrash} type="reset" onClick={handleClear} />
{imageBundles.map((bundle) => (
<>
<hr className="h-0.5 w-3/4 rounded-md border-none bg-white/10" />
<hr className="border-none bg-red w-full rounded-md h-2" />
{bundle.images.map((image, imgIndex) => (
<Button
key={imgIndex}
className={twMerge(
"aspect-square h-full w-full border-2 border-white bg-transparent p-0 hover:bg-transparent hover:opacity-80",
selectedImageToken === image.mediaToken &&
"border-primary hover:opacity-100",
)}
onClick={() => {
setSelectedImageToken(image.mediaToken);
onImageSelect(image);
}}
>
<Button key={imgIndex} className="w-full h-24" onClick={() => { onImageSelect(image) }}>
{/* TODO: Fix CORS issue here */}
<img
src={image.url + "?historystack+" + Math.random()}
alt=""
crossOrigin="anonymous"
className="h-full w-full rounded-lg object-cover"
className="w-full h-full rounded-lg"
/>
</Button>
))}
@@ -93,4 +61,4 @@ export const HistoryStack = ({
</div>
</div>
);
};
}
@@ -1,61 +0,0 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark, faTrashXmark } from "@fortawesome/pro-solid-svg-icons";
import { Tooltip } from "@storyteller/ui-tooltip";
import {
showActionReminder,
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
interface LeftResetStackProps {
onReset: () => void;
className?: string;
}
const LeftResetStack: React.FC<LeftResetStackProps> = ({
onReset,
className = "",
}) => {
return (
<div
className={`glass rounded-xl border-2 border-red/50 shadow-lg hover:border-red/80 ${className}`}
>
<div className="relative h-full">
<Tooltip
content="Reset All"
position="right"
closeOnClick={true}
className="ms-1 rounded-md bg-red px-3 py-1"
delay={100}
>
<button
className="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-transparent text-white transition-colors hover:bg-red/50"
onClick={() =>
showActionReminder({
reminderType: "default",
title: "Reset All",
primaryActionIcon: faTrashXmark,
primaryActionBtnClassName: "bg-red hover:bg-red/80",
message: (
<p className="text-sm text-white/70">
Are you sure you want to reset all? This will clear all your
work and cannot be undone.
</p>
),
primaryActionText: "Reset all",
onPrimaryAction: () => {
onReset();
isActionReminderOpen.value = false;
},
})
}
>
<FontAwesomeIcon icon={faXmark} className="h-5 w-5 text-xl" />
</button>
</Tooltip>
</div>
</div>
);
};
export default LeftResetStack;
@@ -20,11 +20,9 @@ import {
ModelSelector,
useModelSelectorStore,
} from "@storyteller/ui-model-selector";
import { lookupModelByTauriId, ModelInfo } from "@storyteller/model-list";
import { ModelInfo } from "@storyteller/model-list";
import { useImageEditCompleteEvent } from "@storyteller/tauri-events";
import { HistoryStack, ImageBundle } from "./HistoryStack";
import { ModelTag } from "libs/model-list/src/lib/ModelTag";
import LeftResetStack from "./LeftResetStack";
const PAGE_ID: ModelPage = ModelPage.ImageEditor;
@@ -32,15 +30,13 @@ const PageEdit = () => {
//useStateSceneLoader();
const { selectedModels } = useModelSelectorStore();
const selectedModelLabel =
const selectedModel =
selectedModels[PAGE_ID] || IMAGE_EDITOR_PAGE_MODEL_LIST[0]?.label;
const selectedModel = IMAGE_EDITOR_PAGE_MODEL_LIST.find(
(m) => m.label === selectedModelLabel,
);
const selectedModelInfo: ModelInfo | undefined =
selectedModel?.modelInfo;
IMAGE_EDITOR_PAGE_MODEL_LIST.find(
(m) => m.label === selectedModel,
)?.modelInfo;
// State for canvas dimensions
const canvasWidth = useRef<number>(1024);
@@ -144,6 +140,7 @@ const PageEdit = () => {
};
}, [store]);
const onFitPressed = useCallback(async () => {
// Get the stage and its container dimensions
const stage = stageRef.current;
@@ -191,6 +188,7 @@ const PageEdit = () => {
// When baseImageBitmap changes from null to an actual image, call onFitPressed
// Use a slight delay to ensure the image dimensions are properly loaded
onFitPressed();
}, [onFitPressed, store.baseImageBitmap]);
// Create a function to use the left layer ref and download the bitmap from it
@@ -230,63 +228,37 @@ const PageEdit = () => {
return new Uint8Array(arrayBuffer);
};
const handleGenerate = useCallback(
async (prompt: string) => {
if (isEnqueuing) {
console.warn("Already enqueuing an image, please wait.");
return;
}
const handleGenerate = useCallback(async (prompt: string) => {
if (isEnqueuing) {
console.warn("Already enqueuing an image, please wait.");
return;
}
setIsEnqueuing(true);
setIsEnqueuing(true);
const editedImageToken = store.baseImageInfo?.mediaToken;
const editedImageToken = store.baseImageInfo?.mediaToken;
if (!editedImageToken) {
console.error("Base image is not available");
return;
}
if (!editedImageToken) {
console.error("Base image is not available");
return;
}
// TODO: Call inference API here
const arrayBuffer = await getMaskArrayBuffer();
// TODO: Call inference API here
const arrayBuffer = await getMaskArrayBuffer();
try {
await EnqueueImageInpaint({
model: selectedModelInfo,
image_media_token: editedImageToken,
mask_image_raw_bytes: arrayBuffer,
prompt: prompt,
image_count: generationCount,
frontend_caller: "image_editor",
});
} finally {
setIsEnqueuing(false);
}
},
[
generationCount,
isEnqueuing,
selectedModelInfo,
store.baseImageInfo?.mediaToken,
],
);
const modelConfig = lookupModelByTauriId(selectedModelInfo!.tauri_id);
const supportsMaskedInpainting = modelConfig?.tags?.includes(ModelTag.MaskedInpainting) ?? false;
if (!supportsMaskedInpainting && (store.activeTool !== "select" || store.lineNodes.length > 0)) {
// TODO: Implement a new mode for unsupported masking and hide the nodes layer instead of clearing
store.setActiveTool("select");
store.clearLineNodes();
}
/*
*
* Only component logic below this
*
*
*
*
*/
try {
await EnqueueImageInpaint({
model: selectedModelInfo,
image_media_token: editedImageToken,
mask_image_raw_bytes: arrayBuffer,
prompt: prompt,
image_count: generationCount,
frontend_caller: "image_editor",
});
} finally {
setIsEnqueuing(false);
}
}, [generationCount, isEnqueuing, selectedModelInfo, store.baseImageInfo?.mediaToken]);
// Display image selector on launch, otherwise hide it
// Also show loading state if info is set but image is loading
@@ -312,9 +284,8 @@ const PageEdit = () => {
return (
<>
<div
className={`preserve-aspect-ratio fixed left-1/2 top-0 z-10 -translate-x-1/2 transform ${
isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
className={`preserve-aspect-ratio fixed left-1/2 top-0 z-10 -translate-x-1/2 transform ${isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
style={{ display: store.activeTool === "edit" ? "block" : "none" }}
>
<DrawToolControlBar
@@ -325,32 +296,21 @@ const PageEdit = () => {
/>
</div>
<div
className={`preserve-aspect-ratio fixed left-4 top-1/2 z-10 -translate-y-1/2 transform ${
isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
>
<LeftResetStack onReset={() => store.RESET()} />
</div>
<div
className={`preserve-aspect-ratio fixed right-4 top-1/2 z-10 -translate-y-1/2 transform ${
isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
className={`preserve-aspect-ratio fixed right-4 top-1/2 z-10 -translate-y-1/2 transform ${isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
>
<HistoryStack
onClear={() => {
store.RESET();
}}
onClear={() => { store.RESET(); }}
startingBundles={[{ images: [store.baseImageInfo] } as ImageBundle]}
onImageSelect={(baseImage) => {
store.clearLineNodes();
store.setBaseImageInfo(baseImage);
store.setBaseImageInfo(baseImage)
}}
/>
</div>
<div
className={`preserve-aspect-ratio fixed bottom-0 left-1/2 z-10 -translate-x-1/2 transform ${
isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
className={`preserve-aspect-ratio fixed bottom-0 left-1/2 z-10 -translate-x-1/2 transform ${isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
>
<PromptEditor
modelInfo={selectedModelInfo}
@@ -363,7 +323,6 @@ const PageEdit = () => {
isDisabled={isEnqueuing}
generationCount={generationCount}
onGenerationCountChange={setGenerationCount}
supportsMaskedInpainting={supportsMaskedInpainting}
/>
</div>
<div className="relative z-0">
@@ -10,7 +10,6 @@ const PromptEditor: React.FC<PromptBoxEditProps> = ({
modelInfo,
generationCount,
onGenerationCountChange,
supportsMaskedInpainting
}) => {
return (
<div className="mx-auto flex w-full max-w-3xl flex-col space-y-2">
@@ -23,7 +22,6 @@ const PromptEditor: React.FC<PromptBoxEditProps> = ({
modelInfo={modelInfo}
generationCount={generationCount}
onGenerationCountChange={onGenerationCountChange}
supportsMaskedInpainting={supportsMaskedInpainting}
/>
</div>
);
@@ -810,14 +810,7 @@ export const useEditStore = create<EditState>((set, get, store) => ({
},
// Toolbar actions
setActiveTool: (tool: ActiveEditTool) => {
// Avoid triggering state change if the tool is same
if (tool === get().activeTool) {
return;
}
set({ activeTool: tool })
},
setActiveTool: (tool: ActiveEditTool) => set({ activeTool: tool }),
setEditOperation: (mode: EditOperation) => set({ editOperation: mode }),
setBrushColor: (color: string) => set({ brushColor: color }),
setBrushSize: (size: number) => set({ brushSize: size }),
@@ -72,7 +72,6 @@ interface GalleryModalProps {
) => Promise<void>;
isOpen?: boolean;
forceFilter?: string;
onEditClicked?: (url: string, media_id?: string) => Promise<void> | void;
}
export const GalleryModal = React.memo(
@@ -87,7 +86,6 @@ export const GalleryModal = React.memo(
onAddToSceneClicked,
isOpen,
forceFilter,
onEditClicked,
}: GalleryModalProps) => {
const [loading, setLoading] = useState(false);
// Lightbox state is now handled via signals
@@ -660,7 +658,6 @@ export const GalleryModal = React.memo(
onDownloadClicked={onDownloadClicked}
onAddToSceneClicked={onAddToSceneClicked}
mediaClass={lightboxImageSignal.value?.mediaClass}
onEditClicked={onEditClicked}
/>
)}
</>
@@ -1,7 +1,7 @@
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import dayjs from "dayjs";
import { faDownToLine, faPencil } from "@fortawesome/pro-solid-svg-icons";
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
import {
EnqueueImageTo3dObject,
EnqueueImageTo3dObjectModel,
@@ -40,7 +40,6 @@ interface LightboxModalProps {
) => Promise<void>;
mediaClass?: string;
onPromptCopy?: (prompt: string) => void;
onEditClicked?: (url: string, media_id?: string) => Promise<void> | void;
}
export function LightboxModal({
@@ -58,7 +57,6 @@ export function LightboxModal({
onDownloadClicked,
onAddToSceneClicked,
mediaClass,
onEditClicked,
}: LightboxModalProps) {
// NB(bt,2025-06-14): We add ?cors=1 to the image url to prevent caching "sec-fetch-mode: no-cors" from
// the <image> tag request from being cached. If we then drag it into the canvas after it's been cached,
@@ -404,19 +402,6 @@ export function LightboxModal({
3D
</Button>
{onEditClicked && downloadUrl && (
<Button
icon={faPencil}
onClick={async (e) => {
e.stopPropagation();
gtagEvent("edit_image_clicked");
await onEditClicked(downloadUrl, mediaId);
}}
>
Edit
</Button>
)}
{onAddToSceneClicked && downloadUrl && (
<Button
onClick={async (e) => {
@@ -1,6 +1,6 @@
import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover";
import { useModelSelectorStore } from "./model-selector-store";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { ModelPage } from "./model-pages";
interface ModelSelectorProps {
@@ -22,14 +22,6 @@ export function ModelSelector({
const { selectedModels, setSelectedModel } = useModelSelectorStore();
const selectedModel = selectedModels[page] || items[0]?.label;
// For the first mount, make sure the selected model is set for other components to listen
useEffect(() => {
// Initialize selected model if not set
if (!selectedModels[page] && items[0]) {
setSelectedModel(page, items[0].label);
}
}, []);
const handleModelSelect = (item: PopoverItem) => {
setSelectedModel(page, item.label);
};
@@ -26,7 +26,6 @@ export interface PromptBoxEditProps {
modelInfo?: ModelInfo;
generationCount?: number;
onGenerationCountChange?: (count: number) => void;
supportsMaskedInpainting?: boolean;
}
export const PromptBoxEdit = ({
@@ -38,7 +37,6 @@ export const PromptBoxEdit = ({
modelInfo,
generationCount: generationCountProp,
onGenerationCountChange,
supportsMaskedInpainting = false,
}: PromptBoxEditProps) => {
const [prompt, setPrompt] = useState("");
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
@@ -182,19 +180,17 @@ export const PromptBoxEdit = ({
onChange={handleChange}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onFocus={() => { }}
onBlur={() => { }}
onFocus={() => {}}
onBlur={() => {}}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
{supportsMaskedInpainting && (
<ButtonIconSelect
options={modes}
onOptionChange={onModeSelectionChange}
selectedOption={selectedMode}
/>
)}
<ButtonIconSelect
options={modes}
onOptionChange={onModeSelectionChange}
selectedOption={selectedMode}
/>
<Tooltip
content={