diff --git a/web_app/src/App.tsx b/web_app/src/App.tsx index f845013..54cb8e0 100644 --- a/web_app/src/App.tsx +++ b/web_app/src/App.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { nanoid } from "nanoid" -import { useRecoilState, useSetRecoilState } from "recoil" -import { fileState, serverConfigState } from "@/lib/store" +import { useSetRecoilState } from "recoil" +import { serverConfigState } from "@/lib/store" import useInputImage from "@/hooks/useInputImage" import { keepGUIAlive } from "@/lib/utils" import { getServerConfig, isDesktop } from "@/lib/api" @@ -9,6 +9,7 @@ import Header from "@/components/Header" import Workspace from "@/components/Workspace" import FileSelect from "@/components/FileSelect" import { Toaster } from "./components/ui/toaster" +import { useStore } from "./lib/states" const SUPPORTED_FILE_TYPE = [ "image/jpeg", @@ -18,13 +19,15 @@ const SUPPORTED_FILE_TYPE = [ "image/tiff", ] function Home() { - const [file, setFile] = useRecoilState(fileState) + const [file, setFile] = useStore((state) => [state.file, state.setFile]) + const userInputImage = useInputImage() const setServerConfigState = useSetRecoilState(serverConfigState) - // Set Input Image useEffect(() => { - setFile(userInputImage) + if (userInputImage) { + setFile(userInputImage) + } }, [userInputImage, setFile]) // Keeping GUI Window Open diff --git a/web_app/src/components/Editor.tsx b/web_app/src/components/Editor.tsx index 91b2baf..e6b31d8 100644 --- a/web_app/src/components/Editor.tsx +++ b/web_app/src/components/Editor.tsx @@ -1,15 +1,6 @@ -import React, { - SyntheticEvent, - useCallback, - useEffect, - useRef, - useState, -} from "react" -import { - CursorArrowRaysIcon, - ArrowsPointingOutIcon, - ArrowDownTrayIcon, -} from "@heroicons/react/24/outline" +import { SyntheticEvent, useCallback, useEffect, useRef, useState } from "react" +import { CursorArrowRaysIcon } from "@heroicons/react/24/outline" +import { useToast } from "@/components/ui/use-toast" import { ReactZoomPanPinchRef, TransformComponent, @@ -19,7 +10,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil" import { useWindowSize } from "react-use" // import { useWindowSize, useKey, useKeyPressEvent } from "@uidotdev/usehooks" import inpaint, { downloadToOutput, runPlugin } from "@/lib/api" -import { Button, IconButton } from "@/components/ui/button" +import { IconButton } from "@/components/ui/button" import { askWritePermission, copyCanvasImage, @@ -29,30 +20,22 @@ import { loadImage, srcToFile, } from "@/lib/utils" -import { Eraser, Eye, Redo, Undo } from "lucide-react" +import { Eraser, Eye, Redo, Undo, Expand, Download } from "lucide-react" import { - appState, - brushSizeState, croperState, enableFileManagerState, - fileState, - imageHeightState, - imageWidthState, interactiveSegClicksState, isDiffusionModelsState, isEnableAutoSavingState, - isInpaintingState, isInteractiveSegRunningState, isInteractiveSegState, isPix2PixState, isPluginRunningState, isProcessingState, negativePropmtState, - propmtState, runManuallyState, seedState, settingState, - toastState, } from "@/lib/store" // import Croper from "../Croper/Croper" import emitter, { @@ -71,8 +54,9 @@ import { Slider } from "./ui/slider" // import InteractiveSegReplaceModal from "../InteractiveSeg/ReplaceModal" import { PluginName } from "@/lib/types" import { useHotkeys } from "react-hotkeys-hook" +import { useStore } from "@/lib/states" -const TOOLBAR_SIZE = 200 +const TOOLBAR_HEIGHT = 200 const MIN_BRUSH_SIZE = 10 const MAX_BRUSH_SIZE = 200 const BRUSH_COLOR = "#ffcc00bb" @@ -110,15 +94,45 @@ function mouseXY(ev: SyntheticEvent) { return { x: mouseEvent.offsetX, y: mouseEvent.offsetY } } -export default function Editor() { - const [file, setFile] = useRecoilState(fileState) - const promptVal = useRecoilValue(propmtState) +interface EditorProps { + file: File +} + +export default function Editor(props: EditorProps) { + const { file } = props + const { toast } = useToast() + + const [ + isInpainting, + imageWidth, + imageHeight, + baseBrushSize, + brushScale, + promptVal, + setImageSize, + setBrushSize, + setIsInpainting, + ] = useStore((state) => [ + state.isInpainting, + state.imageWidth, + state.imageHeight, + state.brushSize, + state.brushSizeScale, + state.prompt, + state.setImageSize, + state.setBrushSize, + state.setIsInpainting, + ]) + const brushSize = baseBrushSize * brushScale + + // 纯 local state + const [showOriginal, setShowOriginal] = useState(false) + + // const negativePromptVal = useRecoilValue(negativePropmtState) const settings = useRecoilValue(settingState) const [seedVal, setSeed] = useRecoilState(seedState) const croperRect = useRecoilValue(croperState) - const setToastState = useSetRecoilState(toastState) - const [isInpainting, setIsInpainting] = useRecoilState(isInpaintingState) const setIsPluginRunning = useSetRecoilState(isPluginRunningState) const isProcessing = useRecoilValue(isProcessingState) const runMannually = useRecoilValue(runManuallyState) @@ -152,8 +166,6 @@ export default function Editor() { const [clicks, setClicks] = useRecoilState(interactiveSegClicksState) - const [brushSize, setBrushSize] = useRecoilState(brushSizeState) - const [original, isOriginalLoaded] = useImage(file) const [renders, setRenders] = useState([]) const [context, setContext] = useState() @@ -175,7 +187,6 @@ export default function Editor() { brushSize: 20, }) - const [showOriginal, setShowOriginal] = useState(false) const [scale, setScale] = useState(1) const [panned, setPanned] = useState(false) const [minScale, setMinScale] = useState(1.0) @@ -198,10 +209,6 @@ export default function Editor() { const enableFileManager = useRecoilValue(enableFileManagerState) const isEnableAutoSaving = useRecoilValue(isEnableAutoSavingState) - const [imageWidth, setImageWidth] = useRecoilState(imageWidthState) - const [imageHeight, setImageHeight] = useRecoilState(imageHeightState) - const app = useRecoilValue(appState) - const draw = useCallback( (render: HTMLImageElement, lineGroup: LineGroup) => { if (!context) { @@ -422,11 +429,10 @@ export default function Editor() { // clear redo stack resetRedoState() } catch (e: any) { - setToastState({ - open: true, - desc: e.message ? e.message : e.toString(), - state: "error", - duration: 4000, + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: e.message ? e.message : e.toString(), }) drawOnCurrentRender([]) } @@ -464,11 +470,9 @@ export default function Editor() { } else if (isPix2Pix) { runInpainting(false, undefined, null) } else { - setToastState({ - open: true, - desc: "Please draw mask on picture", - state: "error", - duration: 1500, + toast({ + variant: "destructive", + description: "Please draw mask on picture.", }) } emitter.emit(DREAM_BUTTON_MOUSE_LEAVE) @@ -554,11 +558,9 @@ export default function Editor() { // 使用上一次 IS 的 mask 生成 runInpainting(false, undefined, prevInteractiveSegMask, data.image) } else { - setToastState({ - open: true, - desc: "Please draw mask on picture", - state: "error", - duration: 1500, + toast({ + variant: "destructive", + description: "Please draw mask on picture.", }) } }) @@ -577,11 +579,9 @@ export default function Editor() { // 使用上一次 IS 的 mask 生成 runInpainting(false, undefined, prevInteractiveSegMask) } else { - setToastState({ - open: true, - desc: "No mask to reuse", - state: "error", - duration: 1500, + toast({ + variant: "destructive", + description: "No mask to reuse", }) } }) @@ -628,8 +628,7 @@ export default function Editor() { const { blob } = res const newRender = new Image() await loadImage(newRender, blob) - setImageHeight(newRender.height) - setImageWidth(newRender.width) + setImageSize(newRender.height, newRender.width) const newRenders = [...renders, newRender] setRenders(newRenders) const newLineGroups = [...lineGroups, []] @@ -638,15 +637,12 @@ export default function Editor() { const end = new Date() const time = end.getTime() - start.getTime() - setToastState({ - open: true, - desc: `Run ${name} successfully in ${time / 1000}s`, - state: "success", - duration: 3000, + toast({ + description: `Run ${name} successfully in ${time / 1000}s`, }) const rW = windowSize.width / newRender.width - const rH = (windowSize.height - TOOLBAR_SIZE) / newRender.height + const rH = (windowSize.height - TOOLBAR_HEIGHT) / newRender.height let s = 1.0 if (rW < 1 || rH < 1) { s = Math.min(rW, rH) @@ -655,11 +651,9 @@ export default function Editor() { setScale(s) viewportRef.current?.centerView(s, 1) } catch (e: any) { - setToastState({ - open: true, - desc: e.message ? e.message : e.toString(), - state: "error", - duration: 3000, + toast({ + variant: "destructive", + description: e.message ? e.message : e.toString(), }) } finally { setIsPluginRunning(false) @@ -671,8 +665,7 @@ export default function Editor() { getCurrentRender, setIsPluginRunning, isProcessing, - setImageHeight, - setImageWidth, + setImageSize, lineGroups, viewportRef, windowSize, @@ -753,11 +746,10 @@ export default function Editor() { } const [width, height] = getCurrentWidthHeight() - setImageWidth(width) - setImageHeight(height) + setImageSize(width, height) const rW = windowSize.width / width - const rH = (windowSize.height - TOOLBAR_SIZE) / height + const rH = (windowSize.height - TOOLBAR_HEIGHT) / height let s = 1.0 if (rW < 1 || rH < 1) { @@ -950,11 +942,9 @@ export default function Editor() { } img.src = blob } catch (e: any) { - setToastState({ - open: true, - desc: e.message ? e.message : e.toString(), - state: "error", - duration: 4000, + toast({ + variant: "destructive", + description: e.message ? e.message : e.toString(), }) } setIsInteractiveSegRunning(false) @@ -1280,18 +1270,14 @@ export default function Editor() { if ((enableFileManager || isEnableAutoSaving) && renders.length > 0) { try { downloadToOutput(renders[renders.length - 1], file.name, file.type) - setToastState({ - open: true, - desc: `Save image success`, - state: "success", - duration: 2000, + toast({ + description: "Save image success", }) } catch (e: any) { - setToastState({ - open: true, - desc: e.message ? e.message : e.toString(), - state: "error", - duration: 2000, + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: e.message ? e.message : e.toString(), }) } return @@ -1439,6 +1425,15 @@ export default function Editor() { } } + const renderBrush = (style: any) => { + return ( +
+ ) + } + const handleSliderChange = (value: number) => { setBrushSize(value) @@ -1591,24 +1586,16 @@ export default function Editor() { {showBrush && !isInpainting && !isPanning && - (isInteractiveSeg ? ( - renderInteractiveSegCursor() - ) : ( -
- ))} + (isInteractiveSeg + ? renderInteractiveSegCursor() + : renderBrush( + getBrushStyle( + isChangingBrushSizeByMouse ? changeBrushSizeByMouseInit.x : x, + isChangingBrushSizeByMouse ? changeBrushSizeByMouseInit.y : y + ) + ))} - {showRefBrush && ( -
- )} + {showRefBrush && renderBrush(getBrushStyle(windowCenterX, windowCenterY))}
handleSliderChange(vals[0])} onClick={() => setShowRefBrush(false)} /> @@ -1627,7 +1614,7 @@ export default function Editor() { disabled={scale === minScale && panned === false} onClick={resetZoom} > - + @@ -1662,24 +1649,22 @@ export default function Editor() { disabled={!renders.length} onClick={download} > - + - {settings.runInpaintingManually && !isDiffusionModels && ( - { - // ensured by disabled - runInpainting(false, undefined, interactiveSegMask) - }} - > - - - )} + { + // ensured by disabled + runInpainting(false, undefined, interactiveSegMask) + }} + > + +
{/* [ + state.fileManagerState, + state.setFileManagerLayout, + state.setFileManagerSortBy, + state.setFileManagerSortOrder, + state.setFileManagerSearchText, + ]) + useHotkeys("f", () => { toggleOpen() }) @@ -91,25 +96,11 @@ export default function FileManager(props: Props) { const [scrollTop, setScrollTop] = useState(0) const [closeScrollTop, setCloseScrollTop] = useState(0) - const [sortBy, setSortBy] = useRecoilState(fileManagerSortBy) - const [sortOrder, setSortOrder] = useRecoilState(fileManagerSortOrder) - const [layout, setLayout] = useRecoilState(fileManagerLayout) - const [debouncedSearchText, setDebouncedSearchText] = useRecoilState( - fileManagerSearchText - ) const ref = useRef(null) - const [searchText, setSearchText] = useState(debouncedSearchText) + const debouncedSearchText = useDebounce(fileManagerState.searchText, 300) const [tab, setTab] = useState(IMAGE_TAB) const [photos, setPhotos] = useState([]) - const [, cancel] = useDebounce( - () => { - setDebouncedSearchText(searchText) - }, - 300, - [searchText] - ) - useEffect(() => { if (!open) { setCloseScrollTop(scrollTop) @@ -153,7 +144,11 @@ export default function FileManager(props: Props) { ) } - filteredFilenames = _.orderBy(filteredFilenames, sortBy, sortOrder) + filteredFilenames = _.orderBy( + filteredFilenames, + fileManagerState.sortBy, + fileManagerState.sortOrder + ) const newPhotos = filteredFilenames.map((filename: Filename) => { const width = photoWidth @@ -171,7 +166,7 @@ export default function FileManager(props: Props) { } } fetchData() - }, [tab, debouncedSearchText, sortBy, sortOrder, photoWidth, open]) + }, [tab, debouncedSearchText, fileManagerState, photoWidth, open]) const onScroll = (event: SyntheticEvent) => { setScrollTop(event.currentTarget.scrollTop) @@ -190,19 +185,21 @@ export default function FileManager(props: Props) { { - setLayout("rows") + setFileManagerLayout("rows") }} > { - setLayout("masonry") + setFileManagerLayout("masonry") }} - className={layout !== "masonry" ? "opacity-50" : ""} + className={ + fileManagerState.layout !== "masonry" ? "opacity-50" : "" + } > @@ -213,9 +210,9 @@ export default function FileManager(props: Props) { return ( - + - + @@ -225,14 +222,14 @@ export default function FileManager(props: Props) { ) => { evt.preventDefault() evt.stopPropagation() const target = evt.target as HTMLInputElement - setSearchText(target.value) + setFileManagerSearchText(target.value) }} placeholder="Search by file name" /> @@ -248,17 +245,17 @@ export default function FileManager(props: Props) {
- {sortOrder === SortOrder.DESCENDING ? ( + {fileManagerState.sortOrder === SortOrder.DESCENDING ? ( { - setSortOrder(SortOrder.ASCENDING) + setFileManagerSortOrder(SortOrder.ASCENDING) }} > @@ -292,7 +289,7 @@ export default function FileManager(props: Props) { { - setSortOrder(SortOrder.DESCENDING) + setFileManagerSortOrder(SortOrder.DESCENDING) }} > @@ -308,7 +305,7 @@ export default function FileManager(props: Props) { ref={onRefChange} > +