mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-10 20:11:49 +00:00
Write base image uploader logic
This commit is contained in:
1 parent
2081e1fa30
commit
34b8ac81ec
4 files changed
+176
-99
No files matched your search
@@ -1,20 +1,170 @@
|
||||
import { faImage } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { downloadFileFromUrl } from "libs/api/src/lib/LocalApi";
|
||||
import Button from "node_modules/@storyteller/ui-button/src/lib/button";
|
||||
import GalleryModal, { GalleryItem } from "node_modules/@storyteller/ui-gallery-modal/src/lib/gallery-modal";
|
||||
import LoadingSpinner from "node_modules/@storyteller/ui-loading-spinner/src/lib/loading-spinner";
|
||||
import { ReferenceImage } from "node_modules/@storyteller/ui-promptbox/src/lib/PromptBox2D";
|
||||
import { useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
|
||||
import { UploaderStates } from "~/enums";
|
||||
|
||||
export type BaseImageSelectorProps = {
|
||||
onImageSelect: (imageUrl: string) => void;
|
||||
export interface BaseSelectorImage {
|
||||
id: string;
|
||||
url: string;
|
||||
mediaToken: string;
|
||||
}
|
||||
|
||||
const BaseImageSelector = ({
|
||||
export type BaseImageSelectorProps = {
|
||||
onImageSelect: (imageUrl: BaseSelectorImage) => void;
|
||||
}
|
||||
|
||||
const MAX_GALLERY_SELECTIONS = 1;
|
||||
|
||||
export const BaseImageSelector = ({
|
||||
onImageSelect
|
||||
}: BaseImageSelectorProps) => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
[]
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleGalleryClick = () => setIsGalleryModalOpen(true);
|
||||
|
||||
const handleGalleryClose = () => {
|
||||
setIsGalleryModalOpen(false);
|
||||
setSelectedGalleryImages([]);
|
||||
};
|
||||
|
||||
const handleImageSelect = (id: string) => {
|
||||
// If already selected, deselect it
|
||||
// Else if not selected and under max limit, select it
|
||||
if (selectedGalleryImages.includes(id)) {
|
||||
setSelectedGalleryImages([]);
|
||||
} else {
|
||||
setSelectedGalleryImages([id]);
|
||||
}
|
||||
}
|
||||
|
||||
const handleUseGalleryImages = (selectedItems: GalleryItem[]) => {
|
||||
// We only want one file
|
||||
if (selectedItems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = selectedItems[0];
|
||||
if (!item.fullImage) return;
|
||||
const referenceImage: BaseSelectorImage = {
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: item.fullImage,
|
||||
mediaToken: item.id,
|
||||
};
|
||||
sendImageEvent(referenceImage);
|
||||
}
|
||||
|
||||
const handleUploadClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files) {
|
||||
setIsLoading(true);
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
const reader = new FileReader(); reader.onloadend = () => {
|
||||
uploadImage({
|
||||
title: `reference-image-${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}`,
|
||||
assetFile: file,
|
||||
progressCallback: (newState) => {
|
||||
console.debug("Upload progress:", newState.data);
|
||||
if (newState.status === UploaderStates.success && newState.data) {
|
||||
const referenceImage: BaseSelectorImage = {
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: reader.result as string,
|
||||
mediaToken: newState.data || "",
|
||||
};
|
||||
|
||||
toast.success("Image uploaded successfully!");
|
||||
sendImageEvent(referenceImage);
|
||||
} else if (
|
||||
newState.status === UploaderStates.assetError ||
|
||||
newState.status === UploaderStates.imageCreateError
|
||||
) {
|
||||
toast.error("Upload failed. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sendImageEvent = (image: ReferenceImage) => {
|
||||
handleGalleryClose();
|
||||
onImageSelect(image);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center bg-ui-panel border-ui-panel-border border-4">
|
||||
<FontAwesomeIcon icon={faImage} className="text-6xl text-ui-text-secondary" />
|
||||
<span className="text-ui-text-secondary text-lg ml-2">Click to upload or drag and drop an image here to edit</span>
|
||||
<div className="mt-4">
|
||||
<>
|
||||
<div className="h-1/2 w-1/2 flex items-center justify-center bg-ui-background border-ui-panel-border border-8">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<span>Uploading image...</span>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) :
|
||||
<div className="flex flex-col gap-8">
|
||||
<FontAwesomeIcon icon={faImage} className="text-6xl text-ui-controls" />
|
||||
<span className="text-ui-controls text-lg ml-2">Click to upload or drag and drop an image here to edit</span>
|
||||
<div className="mt-4 flex justify-center gap-2">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
multiple
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleUploadClick}
|
||||
>
|
||||
Upload an image
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleGalleryClick}
|
||||
>
|
||||
Select from library
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<GalleryModal
|
||||
isOpen={!!isGalleryModalOpen}
|
||||
onClose={handleGalleryClose}
|
||||
mode="select"
|
||||
selectedItemIds={selectedGalleryImages}
|
||||
onSelectItem={handleImageSelect}
|
||||
maxSelections={MAX_GALLERY_SELECTIONS}
|
||||
onUseSelected={handleUseGalleryImages}
|
||||
onDownloadClicked={downloadFileFromUrl}
|
||||
forceFilter="image"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { AspectRatioType } from "../PageDraw/stores/SceneState";
|
||||
import { ActiveEditTool, useEditStore } from "./stores/EditState";
|
||||
import { EditPaintSurface } from "./EditPaintSurface";
|
||||
import { normalizeCanvas } from "~/Helpers/CanvasHelpers";
|
||||
import { BaseImageSelector, BaseSelectorImage } from "./BaseImageSelector";
|
||||
|
||||
const PageEdit = () => {
|
||||
//useStateSceneLoader();
|
||||
@@ -36,6 +37,7 @@ const PageEdit = () => {
|
||||
const leftPanelRef = useRef<Konva.Layer>({} as Konva.Layer);
|
||||
const rectRef = useRef<Konva.Rect>({} as Konva.Rect);
|
||||
const transformerRefs = useRef<{ [key: string]: Konva.Transformer }>({});
|
||||
const [baseImage, setBaseImage] = useState<BaseSelectorImage | null>(null);
|
||||
|
||||
// Use the Zustand store
|
||||
const store = useEditStore();
|
||||
@@ -223,6 +225,19 @@ const PageEdit = () => {
|
||||
fittedCanvas.toBlob(downloadCallback, "image/png", 1.0);
|
||||
};
|
||||
|
||||
// Display image selector on launch, otherwise hide it
|
||||
if (!baseImage) {
|
||||
return (
|
||||
<div className={"h-screen w-full flex items-center justify-center bg-ui-panel"}>
|
||||
<BaseImageSelector
|
||||
onImageSelect={(image: BaseSelectorImage) => {
|
||||
setBaseImage(image);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -230,99 +245,11 @@ const PageEdit = () => {
|
||||
}`}
|
||||
>
|
||||
<PromptEditor
|
||||
initialPrompt=""
|
||||
onPromptChange={(prompt: string) => {
|
||||
console.log("Prompt changed:", prompt);
|
||||
// Handle prompt changes here
|
||||
}}
|
||||
onRandomize={() => {
|
||||
console.log("Randomize clicked");
|
||||
// Handle randomize action here
|
||||
}}
|
||||
onVary={() => {
|
||||
console.log("Vary clicked");
|
||||
// Handle vary action here
|
||||
}}
|
||||
onModeChange={(mode: string) => { store.setActiveTool(mode as ActiveEditTool) }}
|
||||
selectedMode={store.activeTool}
|
||||
onEnqueuePressed={onEnqueuedPressed}
|
||||
onGenerateClick={downloadLeftPanelBitmap}
|
||||
/>
|
||||
</div>
|
||||
<SideToolbar
|
||||
className="fixed left-0 top-1/2 z-10 -translate-y-1/2 transform"
|
||||
onSelect={(): void => {
|
||||
store.setActiveTool("select");
|
||||
}}
|
||||
onAddShape={(shape: "rectangle" | "circle" | "triangle"): void => {
|
||||
}}
|
||||
onPaintBrush={(hex: string, size: number, opacity: number): void => {
|
||||
// store.setActiveTool("draw");
|
||||
// store.setBrushColor(hex);
|
||||
// store.setBrushSize(size);
|
||||
// store.setBrushOpacity(opacity);
|
||||
}}
|
||||
onEraser={(size: number): void => {
|
||||
// store.setActiveTool("eraser");
|
||||
// store.setBrushSize(size);
|
||||
}}
|
||||
onCanvasBackground={(hex: string): void => {
|
||||
console.log("Canvas background activated", { color: hex });
|
||||
// Add background change logic here
|
||||
// TODO: minor bug needs to update the preview panel
|
||||
// Debounce also causes issues with real time color change.
|
||||
store.setFillColor(hex);
|
||||
}}
|
||||
onGenerateImage={(): void => {
|
||||
console.log("Generate image activated");
|
||||
// Add image generation logic here
|
||||
}}
|
||||
onUploadImage={(): void => {
|
||||
// Create input element dynamically like in PromptEditor
|
||||
console.log("Upload image activated");
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.multiple = true;
|
||||
input.style.display = "none"; // Hide the input
|
||||
|
||||
// Attach to DOM
|
||||
document.body.appendChild(input);
|
||||
|
||||
input.onchange = (e: Event) => {
|
||||
console.log("File change event triggered");
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.files) {
|
||||
const files = Array.from(target.files);
|
||||
console.log("Selected files:", files);
|
||||
const imageFiles = files.filter((file) =>
|
||||
file.type.startsWith("image/"),
|
||||
);
|
||||
console.log("Filtered image files:", imageFiles);
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
console.log("Uploading images:", imageFiles);
|
||||
handleImageUpload(imageFiles);
|
||||
} else {
|
||||
console.log("No valid image files selected");
|
||||
}
|
||||
} else {
|
||||
console.log("No files selected");
|
||||
}
|
||||
// Clean up: remove input from DOM after use
|
||||
document.body.removeChild(input);
|
||||
};
|
||||
|
||||
// Reset value before click (for same file selection)
|
||||
input.value = "";
|
||||
input.click();
|
||||
}}
|
||||
onDelete={(): void => {
|
||||
// This onDelete prop for SideToolbar might still be needed for the button
|
||||
store.deleteSelectedItems();
|
||||
}}
|
||||
activeToolId={store.activeTool}
|
||||
/>
|
||||
<div className="relative z-0">
|
||||
<ContextMenuContainer
|
||||
onAction={(e, action) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ interface TabState {
|
||||
}
|
||||
|
||||
export const useTabStore = create<TabState>((set, get) => ({
|
||||
activeTabId: '2D',
|
||||
activeTabId: 'EDIT',
|
||||
tabData: {},
|
||||
|
||||
setActiveTab: async (newTabId) => {
|
||||
|
||||
@@ -559,8 +559,8 @@ export const PromptBox2D = ({
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
onFocus={() => { }}
|
||||
onBlur={() => { }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
|
||||
Reference in new issue
Block a user