wip
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { nanoid } from "nanoid"
|
||||
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"
|
||||
@@ -19,10 +18,13 @@ const SUPPORTED_FILE_TYPE = [
|
||||
"image/tiff",
|
||||
]
|
||||
function Home() {
|
||||
const [file, setFile] = useStore((state) => [state.file, state.setFile])
|
||||
const [file, setServerConfig, setFile] = useStore((state) => [
|
||||
state.file,
|
||||
state.setServerConfig,
|
||||
state.setFile,
|
||||
])
|
||||
|
||||
const userInputImage = useInputImage()
|
||||
const setServerConfigState = useSetRecoilState(serverConfigState)
|
||||
|
||||
useEffect(() => {
|
||||
if (userInputImage) {
|
||||
@@ -44,8 +46,7 @@ function Home() {
|
||||
useEffect(() => {
|
||||
const fetchServerConfig = async () => {
|
||||
const serverConfig = await getServerConfig().then((res) => res.json())
|
||||
console.log(serverConfig)
|
||||
setServerConfigState(serverConfig)
|
||||
setServerConfig(serverConfig)
|
||||
}
|
||||
fetchServerConfig()
|
||||
}, [])
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useStore } from "@/lib/states"
|
||||
import { cn } from "@/lib/utils"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
const DOC_MOVE_OPTS = { capture: true, passive: false }
|
||||
|
||||
@@ -75,11 +77,6 @@ const Cropper = (props: Props) => {
|
||||
state.setCropperWidth,
|
||||
state.setCropperHeight,
|
||||
])
|
||||
// const [x, setX] = useRecoilState(croperX)
|
||||
// const [y, setY] = useRecoilState(croperY)
|
||||
// const [height, setHeight] = useRecoilState(croperHeight)
|
||||
// const [width, setWidth] = useRecoilState(croperWidth)
|
||||
// const isInpainting = useRecoilValue(isInpaintingState)
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [isMoving, setIsMoving] = useState(false)
|
||||
@@ -100,7 +97,7 @@ const Cropper = (props: Props) => {
|
||||
})
|
||||
|
||||
const onDragFocus = () => {
|
||||
console.log("focus")
|
||||
// console.log("focus")
|
||||
}
|
||||
|
||||
const clampLeftRight = (newX: number, newWidth: number) => {
|
||||
@@ -254,102 +251,64 @@ const Cropper = (props: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createCropSelection = () => {
|
||||
const createDragHandle = (cursor: string, side1: string, side2: string) => {
|
||||
const sideLength = 12
|
||||
const draghandleCls = `w-[${sideLength}px] h-[${sideLength}px] z-4 absolute block border-2 border-primary borde pointer-events-auto hover:bg-primary`
|
||||
|
||||
let side2Cls = `${side2}-[-${sideLength / 2}px]`
|
||||
if (side2 === "") {
|
||||
if (side1 === "top" || side1 === "bottom") {
|
||||
side2Cls = `left-[calc(50%-${sideLength / 2}px)]`
|
||||
} else if (side1 === "left" || side1 === "right") {
|
||||
side2Cls = `top-[calc(50%-${sideLength / 2}px)]`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="drag-elements"
|
||||
onFocus={onDragFocus}
|
||||
onPointerDown={onCropPointerDown}
|
||||
>
|
||||
className={cn(
|
||||
draghandleCls,
|
||||
`${cursor}`,
|
||||
side1 ? `${side1}-[-${sideLength / 2}px]` : "",
|
||||
side2Cls
|
||||
)}
|
||||
data-ord={side1 + side2}
|
||||
aria-label={side1 + side2}
|
||||
tabIndex={-1}
|
||||
role="button"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const createCropSelection = () => {
|
||||
return (
|
||||
<div onFocus={onDragFocus} onPointerDown={onCropPointerDown}>
|
||||
<div
|
||||
className="drag-bar ord-top"
|
||||
className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]"
|
||||
data-ord="top"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-bar ord-right"
|
||||
className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
|
||||
data-ord="right"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-bar ord-bottom"
|
||||
className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]"
|
||||
data-ord="bottom"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-bar ord-left"
|
||||
className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]"
|
||||
data-ord="left"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-topleft"
|
||||
data-ord="topleft"
|
||||
aria-label="topleft"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
{createDragHandle("cursor-nw-resize", "top", "left")}
|
||||
{createDragHandle("cursor-ne-resize", "top", "right")}
|
||||
{createDragHandle("cursor-se-resize", "bottom", "left")}
|
||||
{createDragHandle("cursor-sw-resize", "bottom", "right")}
|
||||
|
||||
<div
|
||||
className="drag-handle ord-topright"
|
||||
data-ord="topright"
|
||||
aria-label="topright"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-bottomleft"
|
||||
data-ord="bottomleft"
|
||||
aria-label="bottomleft"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-bottomright"
|
||||
data-ord="bottomright"
|
||||
aria-label="bottomright"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-top"
|
||||
data-ord="top"
|
||||
aria-label="top"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-right"
|
||||
data-ord="right"
|
||||
aria-label="right"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-bottom"
|
||||
data-ord="bottom"
|
||||
aria-label="bottom"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-left"
|
||||
data-ord="left"
|
||||
aria-label="left"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
{createDragHandle("cursor-ns-resize", "top", "")}
|
||||
{createDragHandle("cursor-ns-resize", "bottom", "")}
|
||||
{createDragHandle("cursor-ew-resize", "left", "")}
|
||||
{createDragHandle("cursor-ew-resize", "right", "")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -370,17 +329,17 @@ const Cropper = (props: Props) => {
|
||||
const createInfoBar = () => {
|
||||
return (
|
||||
<div
|
||||
className="border absolute pointer-events-auto text-[1rem] px-[0.8rem] py-[0.2rem] flex items-center justify-center gap-[12px] rounded-full hover:cursor-move"
|
||||
onPointerDown={onInfoBarPointerDown}
|
||||
className={twMerge(
|
||||
"border absolute pointer-events-auto px-2 py-1 rounded-full hover:cursor-move bg-background",
|
||||
"origin-top-left top-0 left-0"
|
||||
)}
|
||||
style={{
|
||||
transform: `scale(${1 / scale})`,
|
||||
top: `${10 / scale}px`,
|
||||
left: `${10 / scale}px`,
|
||||
transform: `scale(${(1 / scale) * 0.8})`,
|
||||
}}
|
||||
onPointerDown={onInfoBarPointerDown}
|
||||
>
|
||||
<div>
|
||||
{width} x {height}
|
||||
</div>
|
||||
{/* TODO: 移动的时候会显示 brush */}
|
||||
{width} x {height}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,10 @@ import { CursorArrowRaysIcon } from "@heroicons/react/24/outline"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
ReactZoomPanPinchContentRef,
|
||||
ReactZoomPanPinchRef,
|
||||
TransformComponent,
|
||||
TransformWrapper,
|
||||
} from "react-zoom-pan-pinch"
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"
|
||||
import { useWindowSize } from "react-use"
|
||||
// import { useWindowSize, useKey, useKeyPressEvent } from "@uidotdev/usehooks"
|
||||
import { useKeyPressEvent, useWindowSize } from "react-use"
|
||||
import inpaint, { downloadToOutput, runPlugin } from "@/lib/api"
|
||||
import { IconButton } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -22,23 +19,6 @@ import {
|
||||
srcToFile,
|
||||
} from "@/lib/utils"
|
||||
import { Eraser, Eye, Redo, Undo, Expand, Download } from "lucide-react"
|
||||
import {
|
||||
croperState,
|
||||
enableFileManagerState,
|
||||
interactiveSegClicksState,
|
||||
isDiffusionModelsState,
|
||||
isEnableAutoSavingState,
|
||||
isInteractiveSegRunningState,
|
||||
isInteractiveSegState,
|
||||
isPix2PixState,
|
||||
isPluginRunningState,
|
||||
isProcessingState,
|
||||
negativePropmtState,
|
||||
runManuallyState,
|
||||
seedState,
|
||||
settingState,
|
||||
} from "@/lib/store"
|
||||
// import Croper from "../Croper/Croper"
|
||||
import emitter, {
|
||||
EVENT_PROMPT,
|
||||
EVENT_CUSTOM_MASK,
|
||||
@@ -49,19 +29,15 @@ import emitter, {
|
||||
} from "@/lib/event"
|
||||
import { useImage } from "@/hooks/useImage"
|
||||
import { Slider } from "./ui/slider"
|
||||
// import FileSelect from "../FileSelect/FileSelect"
|
||||
// import InteractiveSeg from "../InteractiveSeg/InteractiveSeg"
|
||||
// import InteractiveSegConfirmActions from "../InteractiveSeg/ConfirmActions"
|
||||
// import InteractiveSegReplaceModal from "../InteractiveSeg/ReplaceModal"
|
||||
import { PluginName } from "@/lib/types"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { useStore } from "@/lib/states"
|
||||
import Cropper from "./Cropper"
|
||||
import { HotkeysEvent } from "react-hotkeys-hook/dist/types"
|
||||
|
||||
const TOOLBAR_HEIGHT = 200
|
||||
const MIN_BRUSH_SIZE = 10
|
||||
const MAX_BRUSH_SIZE = 200
|
||||
const COMPARE_SLIDER_DURATION_MS = 300
|
||||
const BRUSH_COLOR = "#ffcc00bb"
|
||||
|
||||
interface Line {
|
||||
@@ -110,48 +86,55 @@ export default function Editor(props: EditorProps) {
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
baseBrushSize,
|
||||
brushScale,
|
||||
promptVal,
|
||||
brushSizeScale,
|
||||
settings,
|
||||
enableAutoSaving,
|
||||
cropperRect,
|
||||
enableManualInpainting,
|
||||
setImageSize,
|
||||
setBrushSize,
|
||||
setIsInpainting,
|
||||
setSeed,
|
||||
interactiveSegState,
|
||||
updateInteractiveSegState,
|
||||
resetInteractiveSegState,
|
||||
isPluginRunning,
|
||||
setIsPluginRunning,
|
||||
] = useStore((state) => [
|
||||
state.isInpainting,
|
||||
state.imageWidth,
|
||||
state.imageHeight,
|
||||
state.brushSize,
|
||||
state.brushSizeScale,
|
||||
state.prompt,
|
||||
state.settings,
|
||||
state.serverConfig.enableAutoSaving,
|
||||
state.cropperState,
|
||||
state.settings.enableManualInpainting,
|
||||
state.setImageSize,
|
||||
state.setBrushSize,
|
||||
state.setIsInpainting,
|
||||
state.setSeed,
|
||||
state.interactiveSegState,
|
||||
state.updateInteractiveSegState,
|
||||
state.resetInteractiveSegState,
|
||||
state.isPluginRunning,
|
||||
state.setIsPluginRunning,
|
||||
])
|
||||
const brushSize = baseBrushSize * brushScale
|
||||
const brushSize = baseBrushSize * brushSizeScale
|
||||
|
||||
// 纯 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 setIsPluginRunning = useSetRecoilState(isPluginRunningState)
|
||||
const isProcessing = useRecoilValue(isProcessingState)
|
||||
const runMannually = useRecoilValue(runManuallyState)
|
||||
const isDiffusionModels = useRecoilValue(isDiffusionModelsState)
|
||||
const isPix2Pix = useRecoilValue(isPix2PixState)
|
||||
const [isInteractiveSeg, setIsInteractiveSeg] = useRecoilState(
|
||||
isInteractiveSegState
|
||||
)
|
||||
const setIsInteractiveSegRunning = useSetRecoilState(
|
||||
isInteractiveSegRunningState
|
||||
)
|
||||
const isProcessing = isInpainting
|
||||
const isDiffusionModels = false
|
||||
const isPix2Pix = false
|
||||
|
||||
const [showInteractiveSegModal, setShowInteractiveSegModal] = useState(false)
|
||||
const [interactiveSegMask, setInteractiveSegMask] = useState<
|
||||
HTMLImageElement | null | undefined
|
||||
>(null)
|
||||
|
||||
// only used while interactive segmentation is on
|
||||
const [tmpInteractiveSegMask, setTmpInteractiveSegMask] = useState<
|
||||
HTMLImageElement | null | undefined
|
||||
@@ -167,8 +150,6 @@ export default function Editor(props: EditorProps) {
|
||||
const [dreamButtonHoverLineGroup, setDreamButtonHoverLineGroup] =
|
||||
useState<LineGroup>([])
|
||||
|
||||
const [clicks, setClicks] = useRecoilState(interactiveSegClicksState)
|
||||
|
||||
const [original, isOriginalLoaded] = useImage(file)
|
||||
const [renders, setRenders] = useState<HTMLImageElement[]>([])
|
||||
const [context, setContext] = useState<CanvasRenderingContext2D>()
|
||||
@@ -201,7 +182,6 @@ export default function Editor(props: EditorProps) {
|
||||
const [initialCentered, setInitialCentered] = useState(false)
|
||||
|
||||
const [isDraging, setIsDraging] = useState(false)
|
||||
const [isMultiStrokeKeyPressed, setIsMultiStrokeKeyPressed] = useState(false)
|
||||
|
||||
const [sliderPos, setSliderPos] = useState<number>(0)
|
||||
|
||||
@@ -209,8 +189,6 @@ export default function Editor(props: EditorProps) {
|
||||
const [redoRenders, setRedoRenders] = useState<HTMLImageElement[]>([])
|
||||
const [redoCurLines, setRedoCurLines] = useState<Line[]>([])
|
||||
const [redoLineGroups, setRedoLineGroups] = useState<LineGroup[]>([])
|
||||
const enableFileManager = useRecoilValue(enableFileManagerState)
|
||||
const isEnableAutoSaving = useRecoilValue(isEnableAutoSavingState)
|
||||
|
||||
const draw = useCallback(
|
||||
(render: HTMLImageElement, lineGroup: LineGroup) => {
|
||||
@@ -223,10 +201,10 @@ export default function Editor(props: EditorProps) {
|
||||
|
||||
context.clearRect(0, 0, context.canvas.width, context.canvas.height)
|
||||
context.drawImage(render, 0, 0, imageWidth, imageHeight)
|
||||
if (isInteractiveSeg && tmpInteractiveSegMask) {
|
||||
if (interactiveSegState.isInteractiveSeg && tmpInteractiveSegMask) {
|
||||
context.drawImage(tmpInteractiveSegMask, 0, 0, imageWidth, imageHeight)
|
||||
}
|
||||
if (!isInteractiveSeg && interactiveSegMask) {
|
||||
if (!interactiveSegState.isInteractiveSeg && interactiveSegMask) {
|
||||
context.drawImage(interactiveSegMask, 0, 0, imageWidth, imageHeight)
|
||||
}
|
||||
if (dreamButtonHoverSegMask) {
|
||||
@@ -243,7 +221,7 @@ export default function Editor(props: EditorProps) {
|
||||
},
|
||||
[
|
||||
context,
|
||||
isInteractiveSeg,
|
||||
interactiveSegState,
|
||||
tmpInteractiveSegMask,
|
||||
dreamButtonHoverSegMask,
|
||||
interactiveSegMask,
|
||||
@@ -363,34 +341,31 @@ export default function Editor(props: EditorProps) {
|
||||
setCurLineGroup([])
|
||||
setIsDraging(false)
|
||||
setIsInpainting(true)
|
||||
if (settings.graduallyInpainting) {
|
||||
drawLinesOnMask([maskLineGroup], maskImage)
|
||||
} else {
|
||||
drawLinesOnMask(newLineGroups)
|
||||
}
|
||||
drawLinesOnMask([maskLineGroup], maskImage)
|
||||
|
||||
let targetFile = file
|
||||
if (settings.graduallyInpainting === true) {
|
||||
if (useLastLineGroup === true) {
|
||||
// renders.length == 1 还是用原来的
|
||||
if (renders.length > 1) {
|
||||
const lastRender = renders[renders.length - 2]
|
||||
targetFile = await srcToFile(
|
||||
lastRender.currentSrc,
|
||||
file.name,
|
||||
file.type
|
||||
)
|
||||
}
|
||||
} else if (renders.length > 0) {
|
||||
console.info("gradually inpainting on last result")
|
||||
|
||||
const lastRender = renders[renders.length - 1]
|
||||
console.log(
|
||||
`randers.length ${renders.length} useLastLineGroup: ${useLastLineGroup}`
|
||||
)
|
||||
if (useLastLineGroup === true) {
|
||||
// renders.length == 1 还是用原来的
|
||||
if (renders.length > 1) {
|
||||
const lastRender = renders[renders.length - 2]
|
||||
targetFile = await srcToFile(
|
||||
lastRender.currentSrc,
|
||||
file.name,
|
||||
file.type
|
||||
)
|
||||
}
|
||||
} else if (renders.length > 0) {
|
||||
console.info("gradually inpainting on last result")
|
||||
|
||||
const lastRender = renders[renders.length - 1]
|
||||
targetFile = await srcToFile(
|
||||
lastRender.currentSrc,
|
||||
file.name,
|
||||
file.type
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -398,10 +373,7 @@ export default function Editor(props: EditorProps) {
|
||||
const res = await inpaint(
|
||||
targetFile,
|
||||
settings,
|
||||
croperRect,
|
||||
promptVal,
|
||||
negativePromptVal,
|
||||
seedVal,
|
||||
cropperRect,
|
||||
useCustomMask ? undefined : maskCanvas.toDataURL(),
|
||||
useCustomMask ? customMask : undefined,
|
||||
paintByExampleImage
|
||||
@@ -445,18 +417,15 @@ export default function Editor(props: EditorProps) {
|
||||
setInteractiveSegMask(null)
|
||||
},
|
||||
[
|
||||
renders,
|
||||
lineGroups,
|
||||
curLineGroup,
|
||||
maskCanvas,
|
||||
settings.graduallyInpainting,
|
||||
settings,
|
||||
croperRect,
|
||||
promptVal,
|
||||
negativePromptVal,
|
||||
cropperRect,
|
||||
drawOnCurrentRender,
|
||||
hadDrawSomething,
|
||||
drawLinesOnMask,
|
||||
seedVal,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -487,7 +456,6 @@ export default function Editor(props: EditorProps) {
|
||||
}, [
|
||||
hadDrawSomething,
|
||||
runInpainting,
|
||||
promptVal,
|
||||
interactiveSegMask,
|
||||
prevInteractiveSegMask,
|
||||
])
|
||||
@@ -604,7 +572,7 @@ export default function Editor(props: EditorProps) {
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on(PluginName.InteractiveSeg, () => {
|
||||
setIsInteractiveSeg(true)
|
||||
// setIsInteractiveSeg(true)
|
||||
if (interactiveSegMask !== null) {
|
||||
setShowInteractiveSegModal(true)
|
||||
}
|
||||
@@ -807,8 +775,8 @@ export default function Editor(props: EditorProps) {
|
||||
const offsetX = (windowSize.width - imageWidth * minScale) / 2
|
||||
const offsetY = (windowSize.height - imageHeight * minScale) / 2
|
||||
viewport.setTransform(offsetX, offsetY, minScale, 200, "easeOutQuad")
|
||||
if (viewport.state) {
|
||||
viewport.state.scale = minScale
|
||||
if (viewport.instance.transformState.scale) {
|
||||
viewport.instance.transformState.scale = minScale
|
||||
}
|
||||
|
||||
setScale(minScale)
|
||||
@@ -850,24 +818,12 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onInteractiveCancel = useCallback(() => {
|
||||
setIsInteractiveSeg(false)
|
||||
setIsInteractiveSegRunning(false)
|
||||
setClicks([])
|
||||
setTmpInteractiveSegMask(null)
|
||||
}, [])
|
||||
|
||||
const handleEscPressed = () => {
|
||||
if (isProcessing) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isInteractiveSeg) {
|
||||
onInteractiveCancel()
|
||||
return
|
||||
}
|
||||
|
||||
if (isDraging || isMultiStrokeKeyPressed) {
|
||||
if (isDraging) {
|
||||
setIsDraging(false)
|
||||
setCurLineGroup([])
|
||||
drawOnCurrentRender([])
|
||||
@@ -879,9 +835,6 @@ export default function Editor(props: EditorProps) {
|
||||
useHotkeys("Escape", handleEscPressed, [
|
||||
isDraging,
|
||||
isInpainting,
|
||||
isMultiStrokeKeyPressed,
|
||||
isInteractiveSeg,
|
||||
onInteractiveCancel,
|
||||
resetZoom,
|
||||
drawOnCurrentRender,
|
||||
])
|
||||
@@ -901,7 +854,7 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isInteractiveSeg) {
|
||||
if (interactiveSegState.isInteractiveSeg) {
|
||||
return
|
||||
}
|
||||
if (isPanning) {
|
||||
@@ -924,7 +877,7 @@ export default function Editor(props: EditorProps) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsInteractiveSegRunning(true)
|
||||
// setIsInteractiveSegRunning(true)
|
||||
const targetFile = await getCurrentRender()
|
||||
const prevMask = null
|
||||
try {
|
||||
@@ -950,14 +903,14 @@ export default function Editor(props: EditorProps) {
|
||||
description: e.message ? e.message : e.toString(),
|
||||
})
|
||||
}
|
||||
setIsInteractiveSegRunning(false)
|
||||
// setIsInteractiveSegRunning(false)
|
||||
}
|
||||
|
||||
const onPointerUp = (ev: SyntheticEvent) => {
|
||||
if (isMidClick(ev)) {
|
||||
setIsPanning(false)
|
||||
}
|
||||
if (isInteractiveSeg) {
|
||||
if (interactiveSegState.isInteractiveSeg) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -978,12 +931,7 @@ export default function Editor(props: EditorProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMultiStrokeKeyPressed) {
|
||||
setIsDraging(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (runMannually) {
|
||||
if (enableManualInpainting) {
|
||||
setIsDraging(false)
|
||||
} else {
|
||||
runInpainting()
|
||||
@@ -991,34 +939,34 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
|
||||
const isOutsideCroper = (clickPnt: { x: number; y: number }) => {
|
||||
if (clickPnt.x < croperRect.x) {
|
||||
if (clickPnt.x < cropperRect.x) {
|
||||
return true
|
||||
}
|
||||
if (clickPnt.y < croperRect.y) {
|
||||
if (clickPnt.y < cropperRect.y) {
|
||||
return true
|
||||
}
|
||||
if (clickPnt.x > croperRect.x + croperRect.width) {
|
||||
if (clickPnt.x > cropperRect.x + cropperRect.width) {
|
||||
return true
|
||||
}
|
||||
if (clickPnt.y > croperRect.y + croperRect.height) {
|
||||
if (clickPnt.y > cropperRect.y + cropperRect.height) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const onCanvasMouseUp = (ev: SyntheticEvent) => {
|
||||
if (isInteractiveSeg) {
|
||||
if (interactiveSegState.isInteractiveSeg) {
|
||||
const xy = mouseXY(ev)
|
||||
const isX = xy.x
|
||||
const isY = xy.y
|
||||
const newClicks: number[][] = [...clicks]
|
||||
const newClicks: number[][] = [...interactiveSegState.clicks]
|
||||
if (isRightClick(ev)) {
|
||||
newClicks.push([isX, isY, 0, newClicks.length])
|
||||
} else {
|
||||
newClicks.push([isX, isY, 1, newClicks.length])
|
||||
}
|
||||
// runInteractiveSeg(newClicks)
|
||||
setClicks(newClicks)
|
||||
updateInteractiveSegState({ clicks: newClicks })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,7 +974,7 @@ export default function Editor(props: EditorProps) {
|
||||
if (isProcessing) {
|
||||
return
|
||||
}
|
||||
if (isInteractiveSeg) {
|
||||
if (interactiveSegState.isInteractiveSeg) {
|
||||
return
|
||||
}
|
||||
if (isChangingBrushSizeByMouse) {
|
||||
@@ -1063,7 +1011,7 @@ export default function Editor(props: EditorProps) {
|
||||
setIsDraging(true)
|
||||
|
||||
let lineGroup: LineGroup = []
|
||||
if (isMultiStrokeKeyPressed || runMannually) {
|
||||
if (enableManualInpainting) {
|
||||
lineGroup = [...curLineGroup]
|
||||
}
|
||||
lineGroup.push({ size: brushSize, pts: [mouseXY(ev)] })
|
||||
@@ -1122,9 +1070,9 @@ export default function Editor(props: EditorProps) {
|
||||
context,
|
||||
])
|
||||
|
||||
const undo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
||||
const undo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
|
||||
keyboardEvent.preventDefault()
|
||||
if (runMannually && curLineGroup.length !== 0) {
|
||||
if (enableManualInpainting && curLineGroup.length !== 0) {
|
||||
undoStroke()
|
||||
} else {
|
||||
undoRender()
|
||||
@@ -1134,7 +1082,7 @@ export default function Editor(props: EditorProps) {
|
||||
useHotkeys("meta+z,ctrl+z", undo, undefined, [
|
||||
undoStroke,
|
||||
undoRender,
|
||||
runMannually,
|
||||
enableManualInpainting,
|
||||
curLineGroup,
|
||||
context?.canvas,
|
||||
renders,
|
||||
@@ -1148,7 +1096,7 @@ export default function Editor(props: EditorProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (runMannually) {
|
||||
if (enableManualInpainting) {
|
||||
if (curLineGroup.length === 0) {
|
||||
return true
|
||||
}
|
||||
@@ -1188,9 +1136,9 @@ export default function Editor(props: EditorProps) {
|
||||
// draw(newRenders[newRenders.length - 1], [])
|
||||
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
|
||||
|
||||
const redo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
||||
const redo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
|
||||
keyboardEvent.preventDefault()
|
||||
if (runMannually && redoCurLines.length !== 0) {
|
||||
if (enableManualInpainting && redoCurLines.length !== 0) {
|
||||
redoStroke()
|
||||
} else {
|
||||
redoRender()
|
||||
@@ -1200,7 +1148,7 @@ export default function Editor(props: EditorProps) {
|
||||
useHotkeys("shift+ctrl+z,shift+meta+z", redo, undefined, [
|
||||
redoStroke,
|
||||
redoRender,
|
||||
runMannually,
|
||||
enableManualInpainting,
|
||||
redoCurLines,
|
||||
])
|
||||
|
||||
@@ -1212,7 +1160,7 @@ export default function Editor(props: EditorProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (runMannually) {
|
||||
if (enableManualInpainting) {
|
||||
if (redoCurLines.length === 0) {
|
||||
return true
|
||||
}
|
||||
@@ -1223,37 +1171,39 @@ export default function Editor(props: EditorProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
// useKeyPressEvent(
|
||||
// "Tab",
|
||||
// (ev) => {
|
||||
// ev?.preventDefault()
|
||||
// ev?.stopPropagation()
|
||||
// if (hadRunInpainting()) {
|
||||
// setShowOriginal(() => {
|
||||
// window.setTimeout(() => {
|
||||
// setSliderPos(100)
|
||||
// }, 10)
|
||||
// return true
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// (ev) => {
|
||||
// ev?.preventDefault()
|
||||
// ev?.stopPropagation()
|
||||
// if (hadRunInpainting()) {
|
||||
// setSliderPos(0)
|
||||
// window.setTimeout(() => {
|
||||
// setShowOriginal(false)
|
||||
// }, 350)
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
useKeyPressEvent(
|
||||
"Tab",
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
if (hadRunInpainting()) {
|
||||
setShowOriginal(() => {
|
||||
window.setTimeout(() => {
|
||||
setSliderPos(100)
|
||||
}, 10)
|
||||
return true
|
||||
})
|
||||
}
|
||||
},
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
if (hadRunInpainting()) {
|
||||
window.setTimeout(() => {
|
||||
setSliderPos(0)
|
||||
}, 10)
|
||||
window.setTimeout(() => {
|
||||
setShowOriginal(false)
|
||||
}, COMPARE_SLIDER_DURATION_MS)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function download() {
|
||||
if (file === undefined) {
|
||||
return
|
||||
}
|
||||
if ((enableFileManager || isEnableAutoSaving) && renders.length > 0) {
|
||||
if (enableAutoSaving && renders.length > 0) {
|
||||
try {
|
||||
downloadToOutput(renders[renders.length - 1], file.name, file.type)
|
||||
toast({
|
||||
@@ -1273,7 +1223,7 @@ export default function Editor(props: EditorProps) {
|
||||
const name = file.name.replace(/(\.[\w\d_-]+)$/i, "_cleanup$1")
|
||||
const curRender = renders[renders.length - 1]
|
||||
downloadImage(curRender.currentSrc, name)
|
||||
if (settings.downloadMask) {
|
||||
if (settings.enableDownloadMask) {
|
||||
let maskFileName = file.name.replace(/(\.[\w\d_-]+)$/i, "_mask$1")
|
||||
maskFileName = maskFileName.replace(/\.[^/.]+$/, ".jpg")
|
||||
|
||||
@@ -1305,104 +1255,98 @@ export default function Editor(props: EditorProps) {
|
||||
return undefined
|
||||
}, [showBrush, isPanning])
|
||||
|
||||
// Standard Hotkeys for Brush Size
|
||||
// useHotKey("[", () => {
|
||||
// setBrushSize((currentBrushSize: number) => {
|
||||
// if (currentBrushSize > 10) {
|
||||
// return currentBrushSize - 10
|
||||
// }
|
||||
// if (currentBrushSize <= 10 && currentBrushSize > 0) {
|
||||
// return currentBrushSize - 5
|
||||
// }
|
||||
// return currentBrushSize
|
||||
// })
|
||||
// })
|
||||
useHotkeys(
|
||||
"[",
|
||||
() => {
|
||||
let newBrushSize = baseBrushSize
|
||||
if (baseBrushSize > 10) {
|
||||
newBrushSize = baseBrushSize - 10
|
||||
}
|
||||
if (baseBrushSize <= 10 && baseBrushSize > 0) {
|
||||
newBrushSize = baseBrushSize - 5
|
||||
}
|
||||
setBrushSize(newBrushSize)
|
||||
},
|
||||
[baseBrushSize]
|
||||
)
|
||||
|
||||
// useHotKey("]", () => {
|
||||
// setBrushSize((currentBrushSize: number) => {
|
||||
// return currentBrushSize + 10
|
||||
// })
|
||||
// })
|
||||
useHotkeys(
|
||||
"]",
|
||||
() => {
|
||||
setBrushSize(baseBrushSize + 10)
|
||||
},
|
||||
[baseBrushSize]
|
||||
)
|
||||
|
||||
// // Manual Inpainting Hotkey
|
||||
// useHotKey(
|
||||
// "shift+r",
|
||||
// () => {
|
||||
// if (runMannually && hadDrawSomething()) {
|
||||
// runInpainting()
|
||||
// }
|
||||
// },
|
||||
// {},
|
||||
// [runMannually, runInpainting, hadDrawSomething]
|
||||
// )
|
||||
// Manual Inpainting Hotkey
|
||||
useHotkeys(
|
||||
"shift+r",
|
||||
() => {
|
||||
if (enableManualInpainting && hadDrawSomething()) {
|
||||
runInpainting()
|
||||
}
|
||||
},
|
||||
[enableManualInpainting, runInpainting, hadDrawSomething]
|
||||
)
|
||||
|
||||
// useHotKey(
|
||||
// "ctrl+c, cmd+c",
|
||||
// async () => {
|
||||
// const hasPermission = await askWritePermission()
|
||||
// if (hasPermission && renders.length > 0) {
|
||||
// if (context?.canvas) {
|
||||
// await copyCanvasImage(context?.canvas)
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: "Copy inpainting result to clipboard",
|
||||
// state: "success",
|
||||
// duration: 3000,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {},
|
||||
// [renders, context]
|
||||
// )
|
||||
useHotkeys(
|
||||
"ctrl+c, cmd+c",
|
||||
async () => {
|
||||
const hasPermission = await askWritePermission()
|
||||
if (hasPermission && renders.length > 0) {
|
||||
if (context?.canvas) {
|
||||
await copyCanvasImage(context?.canvas)
|
||||
toast({
|
||||
title: "Copy inpainting result to clipboard",
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[renders, context]
|
||||
)
|
||||
|
||||
// Toggle clean/zoom tool on spacebar.
|
||||
// useKeyPressEvent(
|
||||
// " ",
|
||||
// (ev) => {
|
||||
// if (!app.disableShortCuts) {
|
||||
// ev?.preventDefault()
|
||||
// ev?.stopPropagation()
|
||||
// setShowBrush(false)
|
||||
// setIsPanning(true)
|
||||
// }
|
||||
// },
|
||||
// (ev) => {
|
||||
// if (!app.disableShortCuts) {
|
||||
// ev?.preventDefault()
|
||||
// ev?.stopPropagation()
|
||||
// setShowBrush(true)
|
||||
// setIsPanning(false)
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
useKeyPressEvent(
|
||||
" ",
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setShowBrush(false)
|
||||
setIsPanning(true)
|
||||
},
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setShowBrush(true)
|
||||
setIsPanning(false)
|
||||
}
|
||||
)
|
||||
|
||||
// useKeyPressEvent(
|
||||
// "Alt",
|
||||
// (ev) => {
|
||||
// ev?.preventDefault()
|
||||
// ev?.stopPropagation()
|
||||
// setIsChangingBrushSizeByMouse(true)
|
||||
// setChangeBrushSizeByMouseInit({ x, y, brushSize })
|
||||
// },
|
||||
// (ev) => {
|
||||
// ev?.preventDefault()
|
||||
// ev?.stopPropagation()
|
||||
// setIsChangingBrushSizeByMouse(false)
|
||||
// }
|
||||
// )
|
||||
useKeyPressEvent(
|
||||
"Alt",
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setIsChangingBrushSizeByMouse(true)
|
||||
setChangeBrushSizeByMouseInit({ x, y, brushSize })
|
||||
},
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setIsChangingBrushSizeByMouse(false)
|
||||
}
|
||||
)
|
||||
|
||||
const getCurScale = (): number => {
|
||||
let s = minScale
|
||||
if (viewportRef.current?.state?.scale !== undefined) {
|
||||
s = viewportRef.current?.state.scale
|
||||
console.log("!!!!!!")
|
||||
if (viewportRef.current?.instance?.transformState.scale !== undefined) {
|
||||
s = viewportRef.current?.instance?.transformState.scale
|
||||
}
|
||||
return s!
|
||||
}
|
||||
|
||||
const getBrushStyle = (_x: number, _y: number) => {
|
||||
const curScale = scale
|
||||
const curScale = getCurScale()
|
||||
return {
|
||||
width: `${brushSize * curScale}px`,
|
||||
height: `${brushSize * curScale}px`,
|
||||
@@ -1435,7 +1379,7 @@ export default function Editor(props: EditorProps) {
|
||||
const renderInteractiveSegCursor = () => {
|
||||
return (
|
||||
<div
|
||||
className="interactive-seg-cursor"
|
||||
className="absolute h-[20px] w-[20px] pointer-events-none rounded-[50%] bg-[rgba(21,_215,_121,_0.936)] [box-shadow:0_0_0_0_rgba(21,_215,_121,_0.936)] animate-pulse"
|
||||
style={{
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
@@ -1475,7 +1419,9 @@ export default function Editor(props: EditorProps) {
|
||||
}}
|
||||
>
|
||||
<TransformComponent
|
||||
contentClass={isProcessing ? "pointer-events-none" : ""}
|
||||
contentClass={
|
||||
isProcessing ? "pointer-events-none animate-pulse duration-700" : ""
|
||||
}
|
||||
contentStyle={{
|
||||
visibility: initialCentered ? "visible" : "hidden",
|
||||
}}
|
||||
@@ -1486,7 +1432,7 @@ export default function Editor(props: EditorProps) {
|
||||
style={{
|
||||
cursor: getCursor(),
|
||||
clipPath: `inset(0 ${sliderPos}% 0 0)`,
|
||||
transition: "clip-path 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
transition: `clip-path ${COMPARE_SLIDER_DURATION_MS}ms`,
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -1519,9 +1465,10 @@ export default function Editor(props: EditorProps) {
|
||||
{showOriginal && (
|
||||
<>
|
||||
<div
|
||||
className="[grid-area:original-image-content] h-full w-[6px] justify-self-end [transition:all_300ms_cubic-bezier(0.4,_0,_0.2,_1)]"
|
||||
className="[grid-area:original-image-content] z-10 bg-primary h-full w-[6px] justify-self-end"
|
||||
style={{
|
||||
marginRight: `${sliderPos}%`,
|
||||
transition: `margin-right ${COMPARE_SLIDER_DURATION_MS}ms`,
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
@@ -1543,12 +1490,12 @@ export default function Editor(props: EditorProps) {
|
||||
maxWidth={imageWidth}
|
||||
minHeight={Math.min(256, imageHeight)}
|
||||
minWidth={Math.min(256, imageWidth)}
|
||||
scale={scale}
|
||||
scale={getCurScale()}
|
||||
// show={settings.showCroper}
|
||||
show={true}
|
||||
// show={isDiffusionModels && settings.showCroper}
|
||||
/>
|
||||
|
||||
{/* {isInteractiveSeg ? <InteractiveSeg /> : <></>} */}
|
||||
{/* {interactiveSegState.isInteractiveSeg ? <InteractiveSeg /> : <></>} */}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
)
|
||||
@@ -1558,7 +1505,7 @@ export default function Editor(props: EditorProps) {
|
||||
setInteractiveSegMask(tmpInteractiveSegMask)
|
||||
setTmpInteractiveSegMask(null)
|
||||
|
||||
if (!runMannually && tmpInteractiveSegMask) {
|
||||
if (!enableManualInpainting && tmpInteractiveSegMask) {
|
||||
runInpainting(false, undefined, tmpInteractiveSegMask)
|
||||
}
|
||||
}
|
||||
@@ -1570,16 +1517,12 @@ export default function Editor(props: EditorProps) {
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onPointerUp}
|
||||
>
|
||||
{/* <InteractiveSegConfirmActions
|
||||
onAcceptClick={onInteractiveAccept}
|
||||
onCancelClick={onInteractiveCancel}
|
||||
/> */}
|
||||
{renderCanvas()}
|
||||
|
||||
{showBrush &&
|
||||
!isInpainting &&
|
||||
!isPanning &&
|
||||
(isInteractiveSeg
|
||||
(interactiveSegState.isInteractiveSeg
|
||||
? renderInteractiveSegCursor()
|
||||
: renderBrush(
|
||||
getBrushStyle(
|
||||
@@ -1590,20 +1533,21 @@ export default function Editor(props: EditorProps) {
|
||||
|
||||
{showRefBrush && renderBrush(getBrushStyle(windowCenterX, windowCenterY))}
|
||||
|
||||
<div className="fixed flex bottom-10 border px-4 py-2 rounded-[3rem] gap-8 items-center justify-center backdrop-filter backdrop-blur-md">
|
||||
<div className="fixed flex bottom-5 border px-4 py-2 rounded-[3rem] gap-8 items-center justify-center backdrop-filter backdrop-blur-md bg-background/50">
|
||||
<Slider
|
||||
className="w-48"
|
||||
defaultValue={[50]}
|
||||
min={MIN_BRUSH_SIZE}
|
||||
max={MAX_BRUSH_SIZE}
|
||||
step={1}
|
||||
tabIndex={-1}
|
||||
value={[baseBrushSize]}
|
||||
onValueChange={(vals) => handleSliderChange(vals[0])}
|
||||
onClick={() => setShowRefBrush(false)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<IconButton
|
||||
tooltip="Reset Zoom & Pan"
|
||||
tooltip="Reset zoom & pan"
|
||||
disabled={scale === minScale && panned === false}
|
||||
onClick={resetZoom}
|
||||
>
|
||||
@@ -1616,23 +1560,26 @@ export default function Editor(props: EditorProps) {
|
||||
<Redo />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
tooltip="Show Original"
|
||||
className={showOriginal ? "eyeicon-active" : ""}
|
||||
// onDown={(ev) => {
|
||||
// ev.preventDefault()
|
||||
// setShowOriginal(() => {
|
||||
// window.setTimeout(() => {
|
||||
// setSliderPos(100)
|
||||
// }, 10)
|
||||
// return true
|
||||
// })
|
||||
// }}
|
||||
// onUp={() => {
|
||||
// setSliderPos(0)
|
||||
// window.setTimeout(() => {
|
||||
// setShowOriginal(false)
|
||||
// }, 300)
|
||||
// }}
|
||||
tooltip="Show original image"
|
||||
onPointerDown={(ev) => {
|
||||
ev.preventDefault()
|
||||
setShowOriginal(() => {
|
||||
window.setTimeout(() => {
|
||||
setSliderPos(100)
|
||||
}, 10)
|
||||
return true
|
||||
})
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
window.setTimeout(() => {
|
||||
// 防止快速点击 show original image 按钮时图片消失
|
||||
setSliderPos(0)
|
||||
}, 10)
|
||||
|
||||
window.setTimeout(() => {
|
||||
setShowOriginal(false)
|
||||
}, COMPARE_SLIDER_DURATION_MS)
|
||||
}}
|
||||
disabled={renders.length === 0}
|
||||
>
|
||||
<Eye />
|
||||
@@ -1645,36 +1592,25 @@ export default function Editor(props: EditorProps) {
|
||||
<Download />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
tooltip="Run Inpainting"
|
||||
disabled={
|
||||
isProcessing ||
|
||||
(!hadDrawSomething() && interactiveSegMask === null)
|
||||
}
|
||||
onClick={() => {
|
||||
// ensured by disabled
|
||||
runInpainting(false, undefined, interactiveSegMask)
|
||||
}}
|
||||
>
|
||||
<Eraser />
|
||||
</IconButton>
|
||||
{settings.enableManualInpainting ? (
|
||||
<IconButton
|
||||
tooltip="Run Inpainting"
|
||||
disabled={
|
||||
isProcessing ||
|
||||
(!hadDrawSomething() && interactiveSegMask === null)
|
||||
}
|
||||
onClick={() => {
|
||||
// ensured by disabled
|
||||
runInpainting(false, undefined, interactiveSegMask)
|
||||
}}
|
||||
>
|
||||
<Eraser />
|
||||
</IconButton>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <InteractiveSegReplaceModal
|
||||
show={showInteractiveSegModal}
|
||||
onClose={() => {
|
||||
onInteractiveCancel()
|
||||
setShowInteractiveSegModal(false)
|
||||
}}
|
||||
onCleanClick={() => {
|
||||
onInteractiveCancel()
|
||||
setInteractiveSegMask(null)
|
||||
}}
|
||||
onReplaceClick={() => {
|
||||
setShowInteractiveSegModal(false)
|
||||
setIsInteractiveSeg(true)
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,18 +74,9 @@ export default function FileManager(props: Props) {
|
||||
const { onPhotoClick, photoWidth } = props
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
|
||||
const [
|
||||
fileManagerState,
|
||||
setFileManagerLayout,
|
||||
setFileManagerSortBy,
|
||||
setFileManagerSortOrder,
|
||||
setFileManagerSearchText,
|
||||
] = useStore((state) => [
|
||||
const [fileManagerState, updateFileManagerState] = useStore((state) => [
|
||||
state.fileManagerState,
|
||||
state.setFileManagerLayout,
|
||||
state.setFileManagerSortBy,
|
||||
state.setFileManagerSortOrder,
|
||||
state.setFileManagerSearchText,
|
||||
state.updateFileManagerState,
|
||||
])
|
||||
|
||||
useHotkeys("f", () => {
|
||||
@@ -185,7 +176,7 @@ export default function FileManager(props: Props) {
|
||||
<IconButton
|
||||
tooltip="Rows layout"
|
||||
onClick={() => {
|
||||
setFileManagerLayout("rows")
|
||||
updateFileManagerState({ layout: "rows" })
|
||||
}}
|
||||
>
|
||||
<ViewHorizontalIcon
|
||||
@@ -195,7 +186,7 @@ export default function FileManager(props: Props) {
|
||||
<IconButton
|
||||
tooltip="Grid layout"
|
||||
onClick={() => {
|
||||
setFileManagerLayout("masonry")
|
||||
updateFileManagerState({ layout: "masonry" })
|
||||
}}
|
||||
>
|
||||
<ViewGridIcon
|
||||
@@ -230,7 +221,7 @@ export default function FileManager(props: Props) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
const target = evt.target as HTMLInputElement
|
||||
setFileManagerSearchText(target.value)
|
||||
updateFileManagerState({ searchText: target.value })
|
||||
}}
|
||||
placeholder="Search by file name"
|
||||
/>
|
||||
@@ -250,13 +241,13 @@ export default function FileManager(props: Props) {
|
||||
onValueChange={(val) => {
|
||||
switch (val) {
|
||||
case SORT_BY_NAME:
|
||||
setFileManagerSortBy(SortBy.NAME)
|
||||
updateFileManagerState({ sortBy: SortBy.NAME })
|
||||
break
|
||||
case SORT_BY_CREATED_TIME:
|
||||
setFileManagerSortBy(SortBy.CTIME)
|
||||
updateFileManagerState({ sortBy: SortBy.CTIME })
|
||||
break
|
||||
case SORT_BY_MODIFIED_TIME:
|
||||
setFileManagerSortBy(SortBy.MTIME)
|
||||
updateFileManagerState({ sortBy: SortBy.MTIME })
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -281,7 +272,7 @@ export default function FileManager(props: Props) {
|
||||
<IconButton
|
||||
tooltip="Descending Order"
|
||||
onClick={() => {
|
||||
setFileManagerSortOrder(SortOrder.ASCENDING)
|
||||
updateFileManagerState({ sortOrder: SortOrder.ASCENDING })
|
||||
}}
|
||||
>
|
||||
<BarsArrowDownIcon />
|
||||
@@ -290,7 +281,7 @@ export default function FileManager(props: Props) {
|
||||
<IconButton
|
||||
tooltip="Ascending Order"
|
||||
onClick={() => {
|
||||
setFileManagerSortOrder(SortOrder.DESCENDING)
|
||||
updateFileManagerState({ sortOrder: SortOrder.DESCENDING })
|
||||
}}
|
||||
>
|
||||
<BarsArrowUpIcon />
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import { PlayIcon } from "@radix-ui/react-icons"
|
||||
import React, { useCallback, useState } from "react"
|
||||
import { useRecoilState, useRecoilValue } from "recoil"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import {
|
||||
enableFileManagerState,
|
||||
isPix2PixState,
|
||||
isSDState,
|
||||
maskState,
|
||||
runManuallyState,
|
||||
} from "@/lib/store"
|
||||
import { IconButton, ImageUploadButton } from "@/components/ui/button"
|
||||
import Shortcuts from "@/components/Shortcuts"
|
||||
// import SettingIcon from "../Settings/SettingIcon"
|
||||
// import PromptInput from "./PromptInput"
|
||||
// import CoffeeIcon from '../CoffeeIcon/CoffeeIcon'
|
||||
import emitter, {
|
||||
DREAM_BUTTON_MOUSE_ENTER,
|
||||
DREAM_BUTTON_MOUSE_LEAVE,
|
||||
@@ -24,24 +13,37 @@ import { useImage } from "@/hooks/useImage"
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
||||
import PromptInput from "./PromptInput"
|
||||
import { RotateCw, Image } from "lucide-react"
|
||||
import { RotateCw, Image, Upload } from "lucide-react"
|
||||
import FileManager from "./FileManager"
|
||||
import { getMediaFile } from "@/lib/api"
|
||||
import { useStore } from "@/lib/states"
|
||||
import SettingsDialog from "./Settings"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Header = () => {
|
||||
const [file, isInpainting, setFile] = useStore((state) => [
|
||||
const [
|
||||
file,
|
||||
customMask,
|
||||
isInpainting,
|
||||
enableFileManager,
|
||||
enableManualInpainting,
|
||||
enableUploadMask,
|
||||
shouldShowPromptInput,
|
||||
setFile,
|
||||
setCustomFile,
|
||||
] = useStore((state) => [
|
||||
state.file,
|
||||
state.customMask,
|
||||
state.isInpainting,
|
||||
state.serverConfig.enableFileManager,
|
||||
state.settings.enableManualInpainting,
|
||||
state.settings.enableUploadMask,
|
||||
state.shouldShowPromptInput(),
|
||||
state.setFile,
|
||||
state.setCustomFile,
|
||||
])
|
||||
const [mask, setMask] = useRecoilState(maskState)
|
||||
// const [maskImage, maskImageLoaded] = useImage(mask)
|
||||
const isSD = useRecoilValue(isSDState)
|
||||
const isPix2Pix = useRecoilValue(isPix2PixState)
|
||||
const runManually = useRecoilValue(runManuallyState)
|
||||
const [maskImage, maskImageLoaded] = useImage(customMask)
|
||||
const [openMaskPopover, setOpenMaskPopover] = useState(false)
|
||||
const enableFileManager = useRecoilValue(enableFileManagerState)
|
||||
|
||||
const handleRerunLastMask = useCallback(() => {
|
||||
emitter.emit(RERUN_LAST_MASK)
|
||||
@@ -68,7 +70,7 @@ const Header = () => {
|
||||
|
||||
return (
|
||||
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 backdrop-filter backdrop-blur-md border-b">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
{enableFileManager ? (
|
||||
<FileManager
|
||||
photoWidth={512}
|
||||
@@ -92,38 +94,37 @@ const Header = () => {
|
||||
</ImageUploadButton>
|
||||
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
visibility: file ? "visible" : "hidden",
|
||||
}}
|
||||
className={cn([
|
||||
"flex items-center gap-1",
|
||||
file && enableUploadMask ? "visible" : "hidden",
|
||||
])}
|
||||
>
|
||||
<ImageUploadButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Upload custom mask"
|
||||
onFileUpload={(file) => {
|
||||
setMask(file)
|
||||
console.info("Send custom mask")
|
||||
if (!runManually) {
|
||||
setCustomFile(file)
|
||||
if (!enableManualInpainting) {
|
||||
emitter.emit(EVENT_CUSTOM_MASK, { mask: file })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>M</div>
|
||||
<Upload />
|
||||
</ImageUploadButton>
|
||||
|
||||
{mask ? (
|
||||
{customMask ? (
|
||||
<Popover open={openMaskPopover}>
|
||||
<PopoverTrigger
|
||||
className="btn-primary side-panel-trigger"
|
||||
onMouseEnter={() => setOpenMaskPopover(true)}
|
||||
onMouseLeave={() => setOpenMaskPopover(false)}
|
||||
style={{
|
||||
visibility: mask ? "visible" : "hidden",
|
||||
visibility: customMask ? "visible" : "hidden",
|
||||
outline: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (mask) {
|
||||
emitter.emit(EVENT_CUSTOM_MASK, { mask })
|
||||
if (customMask) {
|
||||
emitter.emit(EVENT_CUSTOM_MASK, { mask: customMask })
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -131,36 +132,36 @@ const Header = () => {
|
||||
<PlayIcon />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
{/* <PopoverContent>
|
||||
<PopoverContent>
|
||||
{maskImageLoaded ? (
|
||||
<img src={maskImage.src} alt="Custom mask" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</PopoverContent> */}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Rerun last mask"
|
||||
onClick={handleRerunLastMask}
|
||||
onMouseEnter={onRerunMouseEnter}
|
||||
onMouseLeave={onRerunMouseLeave}
|
||||
>
|
||||
<RotateCw />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Rerun last mask"
|
||||
onClick={handleRerunLastMask}
|
||||
onMouseEnter={onRerunMouseEnter}
|
||||
onMouseLeave={onRerunMouseLeave}
|
||||
>
|
||||
<RotateCw className={file ? "visible" : "hidden"} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{isSD ? <PromptInput /> : <></>}
|
||||
{shouldShowPromptInput ? <PromptInput /> : <></>}
|
||||
|
||||
{/* <CoffeeIcon /> */}
|
||||
<div>
|
||||
<div className="flex gap-1">
|
||||
{/* <CoffeeIcon /> */}
|
||||
<Shortcuts />
|
||||
{/* <SettingIcon /> */}
|
||||
<SettingsDialog />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ const ImageSize = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg px-2 py-[6px] z-10">
|
||||
<div className="border rounded-lg px-2 py-[6px] z-10 bg-background">
|
||||
{imageWidth}x{imageHeight}
|
||||
</div>
|
||||
)
|
||||
|
||||
136
web_app/src/components/InteractiveSeg.tsx
Normal file
136
web_app/src/components/InteractiveSeg.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useStore } from "@/lib/states"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
|
||||
import { MousePointerClick } from "lucide-react"
|
||||
import { DropdownMenuItem } from "./ui/dropdown-menu"
|
||||
|
||||
interface InteractiveSegReplaceModal {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
onCleanClick: () => void
|
||||
onReplaceClick: () => void
|
||||
}
|
||||
|
||||
const InteractiveSegReplaceModal = (props: InteractiveSegReplaceModal) => {
|
||||
const { show, onClose, onCleanClick, onReplaceClick } = props
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={show} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Do you want to remove it or create a new one?</DialogTitle>
|
||||
<div className="flex gap-[12px] w-full justify-end items-center">
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose()
|
||||
onCleanClick()
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button onClick={onReplaceClick}>Create new</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const InteractiveSegConfirmActions = () => {
|
||||
const [interactiveSegState, resetInteractiveSegState] = useStore((state) => [
|
||||
state.interactiveSegState,
|
||||
state.resetInteractiveSegState,
|
||||
])
|
||||
|
||||
if (!interactiveSegState.isInteractiveSeg) {
|
||||
return null
|
||||
}
|
||||
|
||||
const onAcceptClick = () => {
|
||||
resetInteractiveSegState()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-10 absolute top-[68px] rounded-xl border-solid border p-[8px] left-1/2 translate-x-[-50%] flex justify-center items-center gap-[8px] bg-background">
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetInteractiveSegState()
|
||||
}}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onAcceptClick()
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
x: number
|
||||
y: number
|
||||
positive: boolean
|
||||
}
|
||||
|
||||
const Item = (props: ItemProps) => {
|
||||
const { x, y, positive } = props
|
||||
const name = positive
|
||||
? "bg-[rgba(21,_215,_121,_0.936)] outline-[6px_solid_rgba(98,_255,_179,_0.31)]"
|
||||
: "bg-[rgba(237,_49,_55,_0.942)] outline-[6px_solid_rgba(255,_89,_95,_0.31)]"
|
||||
return (
|
||||
<div
|
||||
className={`absolute h-[8px] w-[8px] rounded-[50%] ${name}`}
|
||||
style={{
|
||||
left: x,
|
||||
top: y,
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const InteractiveSegPoints = () => {
|
||||
const clicks = useStore((state) => state.interactiveSegState.clicks)
|
||||
|
||||
return (
|
||||
<div className="absolute h-full w-full overflow-hidden pointer-events-none">
|
||||
{clicks.map((click) => {
|
||||
return (
|
||||
<Item
|
||||
key={click[3]}
|
||||
x={click[0]}
|
||||
y={click[1]}
|
||||
positive={click[2] === 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const InteractiveSeg = () => {
|
||||
const [interactiveSegState, updateInteractiveSegState] = useStore((state) => [
|
||||
state.interactiveSegState,
|
||||
state.updateInteractiveSegState,
|
||||
])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InteractiveSegConfirmActions />
|
||||
{/* <InteractiveSegReplaceModal /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { InteractiveSeg, InteractiveSegPoints }
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import { Button } from "./ui/button"
|
||||
import { Fullscreen, MousePointerClick, Slice, Smile } from "lucide-react"
|
||||
import { MixIcon } from "@radix-ui/react-icons"
|
||||
import { useStore } from "@/lib/states"
|
||||
import { InteractiveSeg } from "./InteractiveSeg"
|
||||
|
||||
export enum PluginName {
|
||||
RemoveBG = "RemoveBG",
|
||||
@@ -48,17 +50,10 @@ const pluginMap = {
|
||||
}
|
||||
|
||||
const Plugins = () => {
|
||||
// const [open, toggleOpen] = useToggle(true)
|
||||
// const serverConfig = useRecoilValue(serverConfigState)
|
||||
// const isProcessing = useRecoilValue(isProcessingState)
|
||||
const plugins = [
|
||||
PluginName.RemoveBG,
|
||||
PluginName.AnimeSeg,
|
||||
PluginName.RealESRGAN,
|
||||
PluginName.GFPGAN,
|
||||
PluginName.RestoreFormer,
|
||||
PluginName.InteractiveSeg,
|
||||
]
|
||||
const [plugins, updateInteractiveSegState] = useStore((state) => [
|
||||
state.serverConfig.plugins,
|
||||
state.updateInteractiveSegState,
|
||||
])
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return null
|
||||
@@ -68,6 +63,9 @@ const Plugins = () => {
|
||||
// if (!disabled) {
|
||||
// emitter.emit(pluginName)
|
||||
// }
|
||||
if (pluginName === PluginName.InteractiveSeg) {
|
||||
updateInteractiveSegState({ isInteractiveSeg: true })
|
||||
}
|
||||
}
|
||||
|
||||
const onRealESRGANClick = (upscale: number) => {
|
||||
@@ -98,8 +96,8 @@ const Plugins = () => {
|
||||
}
|
||||
|
||||
const renderPlugins = () => {
|
||||
return plugins.map((plugin: PluginName) => {
|
||||
const { IconClass, showName } = pluginMap[plugin]
|
||||
return plugins.map((plugin: string) => {
|
||||
const { IconClass, showName } = pluginMap[plugin as PluginName]
|
||||
if (plugin === PluginName.RealESRGAN) {
|
||||
return renderRealESRGANPlugin()
|
||||
}
|
||||
@@ -116,7 +114,10 @@ const Plugins = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger className="border rounded-lg z-10">
|
||||
<DropdownMenuTrigger
|
||||
className="border rounded-lg z-10 bg-background"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<MixIcon className="p-2" />
|
||||
</Button>
|
||||
|
||||
@@ -9,17 +9,17 @@ import { Input } from "./ui/input"
|
||||
import { useStore } from "@/lib/states"
|
||||
|
||||
const PromptInput = () => {
|
||||
const [isInpainting, prompt, setPrompt] = useStore((state) => [
|
||||
const [isInpainting, prompt, updateSettings] = useStore((state) => [
|
||||
state.isInpainting,
|
||||
state.prompt,
|
||||
state.setPrompt,
|
||||
state.settings.prompt,
|
||||
state.updateSettings,
|
||||
])
|
||||
|
||||
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
const target = evt.target as HTMLInputElement
|
||||
setPrompt(target.value)
|
||||
updateSettings({ prompt: target.value })
|
||||
}
|
||||
|
||||
const handleRepaintClick = () => {
|
||||
|
||||
435
web_app/src/components/Settings.tsx
Normal file
435
web_app/src/components/Settings.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
import { IconButton } from "@/components/ui/button"
|
||||
import { useToggle } from "@uidotdev/usehooks"
|
||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { Info, Settings } from "lucide-react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
|
||||
import { useState } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { fetchModelInfos, switchModel } from "@/lib/api"
|
||||
import { ModelInfo } from "@/lib/types"
|
||||
import { useStore } from "@/lib/states"
|
||||
import { ScrollArea } from "./ui/scroll-area"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogHeader,
|
||||
} from "./ui/alert-dialog"
|
||||
|
||||
const formSchema = z.object({
|
||||
enableFileManager: z.boolean(),
|
||||
inputDirectory: z.string().refine(async (id) => {
|
||||
// verify that ID exists in database
|
||||
return true
|
||||
}),
|
||||
outputDirectory: z.string().refine(async (id) => {
|
||||
// verify that ID exists in database
|
||||
return true
|
||||
}),
|
||||
enableDownloadMask: z.boolean(),
|
||||
enableManualInpainting: z.boolean(),
|
||||
enableUploadMask: z.boolean(),
|
||||
})
|
||||
|
||||
const TAB_GENERAL = "General"
|
||||
const TAB_MODEL = "Model"
|
||||
const TAB_FILE_MANAGER = "File Manager"
|
||||
|
||||
const TAB_NAMES = [TAB_MODEL, TAB_GENERAL]
|
||||
|
||||
export function SettingsDialog() {
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
const [openModelSwitching, toggleOpenModelSwitching] = useToggle(false)
|
||||
const [tab, setTab] = useState(TAB_GENERAL)
|
||||
const [settings, updateSettings, fileManagerState, updateFileManagerState] =
|
||||
useStore((state) => [
|
||||
state.settings,
|
||||
state.updateSettings,
|
||||
state.fileManagerState,
|
||||
state.updateFileManagerState,
|
||||
])
|
||||
const { toast } = useToast()
|
||||
const [model, setModel] = useState<ModelInfo>(settings.model)
|
||||
|
||||
const { data: modelInfos, isSuccess } = useQuery({
|
||||
queryKey: ["modelInfos"],
|
||||
queryFn: fetchModelInfos,
|
||||
})
|
||||
|
||||
// 1. Define your form.
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
enableDownloadMask: settings.enableDownloadMask,
|
||||
enableManualInpainting: settings.enableManualInpainting,
|
||||
enableUploadMask: settings.enableUploadMask,
|
||||
enableFileManager: fileManagerState.enabled,
|
||||
inputDirectory: fileManagerState.inputDirectory,
|
||||
outputDirectory: fileManagerState.outputDirectory,
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// Do something with the form values. ✅ This will be type-safe and validated.
|
||||
updateSettings({
|
||||
enableDownloadMask: values.enableDownloadMask,
|
||||
enableManualInpainting: values.enableManualInpainting,
|
||||
enableUploadMask: values.enableUploadMask,
|
||||
})
|
||||
|
||||
// TODO: validate input/output Directory
|
||||
updateFileManagerState({
|
||||
enabled: values.enableFileManager,
|
||||
inputDirectory: values.inputDirectory,
|
||||
outputDirectory: values.outputDirectory,
|
||||
})
|
||||
|
||||
if (model.name !== settings.model.name) {
|
||||
toggleOpenModelSwitching()
|
||||
switchModel(model.name)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
toast({
|
||||
title: `Switch to ${model.name} success`,
|
||||
})
|
||||
updateSettings({ model: model })
|
||||
} else {
|
||||
throw new Error("Server error")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: `Switch to ${model.name} failed`,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
toggleOpenModelSwitching()
|
||||
})
|
||||
}
|
||||
}
|
||||
useHotkeys("s", () => {
|
||||
toggleOpen()
|
||||
form.handleSubmit(onSubmit)()
|
||||
})
|
||||
|
||||
function onOpenChange(value: boolean) {
|
||||
toggleOpen()
|
||||
if (!value) {
|
||||
form.handleSubmit(onSubmit)()
|
||||
}
|
||||
}
|
||||
|
||||
function onModelSelect(info: ModelInfo) {
|
||||
setModel(info)
|
||||
}
|
||||
|
||||
function renderModelList(model_types: string[]) {
|
||||
if (!modelInfos) {
|
||||
return <div>Please download model first</div>
|
||||
}
|
||||
return modelInfos
|
||||
.filter((info) => model_types.includes(info.model_type))
|
||||
.map((info: ModelInfo) => {
|
||||
return (
|
||||
<div key={info.name} onClick={() => onModelSelect(info)}>
|
||||
<div
|
||||
className={cn([
|
||||
info.name === model.name ? "bg-muted " : "hover:bg-muted",
|
||||
"rounded-md px-2 py-1 my-1",
|
||||
"cursor-default",
|
||||
])}
|
||||
>
|
||||
<div className="text-base max-w-sm">{info.name}</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function renderModelSettings() {
|
||||
if (!isSuccess) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
let defaultTab = "inpaint"
|
||||
for (let info of modelInfos) {
|
||||
if (model.name === info.name) {
|
||||
defaultTab = info.model_type
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 rounded-md">
|
||||
<div>Current Model</div>
|
||||
<div>{model.name}</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4 rounded-md">
|
||||
<div className="flex gap-4 items-center justify-start">
|
||||
<div>Available models</div>
|
||||
<IconButton tooltip="How to download new model" asChild>
|
||||
<Info />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Tabs defaultValue={defaultTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="inpaint">Inpaint</TabsTrigger>
|
||||
<TabsTrigger value="diffusers_sd">Diffusion</TabsTrigger>
|
||||
<TabsTrigger value="diffusers_sd_inpaint">
|
||||
Diffusion inpaint
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="diffusers_other">Diffusion other</TabsTrigger>
|
||||
</TabsList>
|
||||
<ScrollArea className="h-[240px] w-full mt-2">
|
||||
<TabsContent value="inpaint">
|
||||
{renderModelList(["inpaint"])}
|
||||
</TabsContent>
|
||||
<TabsContent value="diffusers_sd">
|
||||
{renderModelList(["diffusers_sd", "diffusers_sdxl"])}
|
||||
</TabsContent>
|
||||
<TabsContent value="diffusers_sd_inpaint">
|
||||
{renderModelList([
|
||||
"diffusers_sd_inpaint",
|
||||
"diffusers_sdxl_inpaint",
|
||||
])}
|
||||
</TabsContent>
|
||||
<TabsContent value="diffusers_other">
|
||||
{renderModelList(["diffusers_other"])}
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderGeneralSettings() {
|
||||
return (
|
||||
<div className="space-y-4 w-[400px]">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableManualInpainting"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Enable manual inpainting</FormLabel>
|
||||
<FormDescription>
|
||||
Click a button to trigger inpainting after draw mask.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableDownloadMask"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Enable download mask</FormLabel>
|
||||
<FormDescription>
|
||||
Also download the mask after save the inpainting result.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableUploadMask"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex tems-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Enable upload mask</FormLabel>
|
||||
<FormDescription>
|
||||
Enable upload custom mask to perform inpainting.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderFileManagerSettings() {
|
||||
return (
|
||||
<div className="flex flex-col justify-between rounded-lg gap-4 w-[400px]">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableFileManager"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between gap-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Enable file manger</FormLabel>
|
||||
<FormDescription className="max-w-sm">
|
||||
Browser images
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inputDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Input directory</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Browser images from this directory.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="outputDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Save directory</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Result images will be saved to this directory.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertDialog open={openModelSwitching}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
TODO: 添加加载动画 Switching to {model.name}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<IconButton tooltip="Settings">
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="max-w-3xl h-[600px]"
|
||||
// onEscapeKeyDown={(event) => event.preventDefault()}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
// onPointerDownOutside={(event) => event.preventDefault()}
|
||||
>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-row space-x-8 h-full">
|
||||
<div className="flex flex-col space-y-1">
|
||||
{TAB_NAMES.map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
variant="ghost"
|
||||
onClick={() => setTab(item)}
|
||||
className={cn(
|
||||
tab === item ? "bg-muted " : "hover:bg-muted",
|
||||
"justify-start"
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Separator orientation="vertical" />
|
||||
<Form {...form}>
|
||||
<div className="flex w-full justify-center">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{tab === TAB_MODEL ? renderModelSettings() : <></>}
|
||||
{tab === TAB_GENERAL ? renderGeneralSettings() : <></>}
|
||||
{/* {tab === TAB_FILE_MANAGER ? (
|
||||
renderFileManagerSettings()
|
||||
) : (
|
||||
<></>
|
||||
)} */}
|
||||
|
||||
{/* <div className=" absolute right-">
|
||||
<Button type="submit">Ok</Button>
|
||||
</div> */}
|
||||
</form>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsDialog
|
||||
@@ -1,18 +1,16 @@
|
||||
import { useEffect } from "react"
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"
|
||||
import Editor from "./Editor"
|
||||
// import SettingModal from "./Settings/SettingsModal"
|
||||
import {
|
||||
AIModel,
|
||||
isPaintByExampleState,
|
||||
isPix2PixState,
|
||||
isSDState,
|
||||
settingState,
|
||||
} from "@/lib/store"
|
||||
import { currentModel, modelDownloaded, switchModel } from "@/lib/api"
|
||||
import { currentModel } from "@/lib/api"
|
||||
import { useStore } from "@/lib/states"
|
||||
import ImageSize from "./ImageSize"
|
||||
import Plugins from "./Plugins"
|
||||
import { InteractiveSeg } from "./InteractiveSeg"
|
||||
// import SidePanel from "./SidePanel/SidePanel"
|
||||
// import PESidePanel from "./SidePanel/PESidePanel"
|
||||
// import P2PSidePanel from "./SidePanel/P2PSidePanel"
|
||||
@@ -21,73 +19,18 @@ import Plugins from "./Plugins"
|
||||
// import ImageSize from "./ImageSize/ImageSize"
|
||||
|
||||
const Workspace = () => {
|
||||
const file = useStore((state) => state.file)
|
||||
const [settings, setSettingState] = useRecoilState(settingState)
|
||||
const isSD = useRecoilValue(isSDState)
|
||||
const isPaintByExample = useRecoilValue(isPaintByExampleState)
|
||||
const isPix2Pix = useRecoilValue(isPix2PixState)
|
||||
|
||||
const onSettingClose = async () => {
|
||||
const curModel = await currentModel().then((res) => res.text())
|
||||
if (curModel === settings.model) {
|
||||
return
|
||||
}
|
||||
const downloaded = await modelDownloaded(settings.model).then((res) =>
|
||||
res.text()
|
||||
)
|
||||
|
||||
const { model } = settings
|
||||
|
||||
let loadingMessage = `Switching to ${model} model`
|
||||
let loadingDuration = 3000
|
||||
if (downloaded === "False") {
|
||||
loadingMessage = `Downloading ${model} model, this may take a while`
|
||||
loadingDuration = 9999999999
|
||||
}
|
||||
|
||||
// TODO 修改成 Modal
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: loadingMessage,
|
||||
// state: "loading",
|
||||
// duration: loadingDuration,
|
||||
// })
|
||||
|
||||
switchModel(model)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: `Switch to ${model} model success`,
|
||||
// state: "success",
|
||||
// duration: 3000,
|
||||
// })
|
||||
} else {
|
||||
throw new Error("Server error")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: `Switch to ${model} model failed`,
|
||||
// state: "error",
|
||||
// duration: 3000,
|
||||
// })
|
||||
setSettingState((old) => {
|
||||
return { ...old, model: curModel as AIModel }
|
||||
})
|
||||
})
|
||||
}
|
||||
const [file, updateSettings] = useStore((state) => [
|
||||
state.file,
|
||||
state.updateSettings,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
currentModel()
|
||||
.then((res) => res.text())
|
||||
.then((res) => res.json())
|
||||
.then((model) => {
|
||||
setSettingState((old) => {
|
||||
return { ...old, model: model as AIModel }
|
||||
})
|
||||
updateSettings({ model })
|
||||
})
|
||||
}, [setSettingState])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -99,6 +42,7 @@ const Workspace = () => {
|
||||
<Plugins />
|
||||
<ImageSize />
|
||||
</div>
|
||||
<InteractiveSeg />
|
||||
{file ? <Editor file={file} /> : <></>}
|
||||
</>
|
||||
)
|
||||
|
||||
139
web_app/src/components/ui/alert-dialog.tsx
Normal file
139
web_app/src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@@ -78,7 +78,7 @@ const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
{...rest}
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
className="cursor-default"
|
||||
className="cursor-default bg-background"
|
||||
>
|
||||
<div className="icon-button-icon-wrapper">{children}</div>
|
||||
</Button>
|
||||
|
||||
@@ -87,7 +87,7 @@ const DialogTitle = React.forwardRef<
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
176
web_app/src/components/ui/form.tsx
Normal file
176
web_app/src/components/ui/form.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", "text-sm", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
29
web_app/src/components/ui/separator.tsx
Normal file
29
web_app/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -18,7 +18,10 @@ const Slider = React.forwardRef<
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb
|
||||
tabIndex={-1}
|
||||
className="block h-4 w-4 rounded-full border border-primary/60 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
|
||||
--primary: 220.9 39.3% 11%;
|
||||
--primary: 48 100.0% 50.0%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
@@ -74,7 +74,7 @@
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
|
||||
--primary: 210 20% 98%;
|
||||
--primary: 48 100.0% 50.0%;
|
||||
--primary-foreground: 220.9 39.3% 11%;
|
||||
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { PluginName } from "@/lib/types"
|
||||
import { ControlNetMethodMap, Rect, Settings } from "@/lib/store"
|
||||
import { dataURItoBlob, loadImage, srcToFile } from "@/lib/utils"
|
||||
import { ModelInfo, Rect } from "@/lib/types"
|
||||
import { Settings } from "@/lib/states"
|
||||
import { dataURItoBlob, srcToFile } from "@/lib/utils"
|
||||
import axios from "axios"
|
||||
|
||||
export const API_ENDPOINT = import.meta.env.VITE_BACKEND
|
||||
? import.meta.env.VITE_BACKEND
|
||||
: ""
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_ENDPOINT,
|
||||
})
|
||||
|
||||
export default async function inpaint(
|
||||
imageFile: File,
|
||||
settings: Settings,
|
||||
croperRect: Rect,
|
||||
prompt?: string,
|
||||
negativePrompt?: string,
|
||||
seed?: number,
|
||||
maskBase64?: string,
|
||||
customMask?: File,
|
||||
paintByExampleImage?: File
|
||||
@@ -26,38 +28,29 @@ export default async function inpaint(
|
||||
fd.append("mask", customMask)
|
||||
}
|
||||
|
||||
const hdSettings = settings.hdSettings[settings.model]
|
||||
fd.append("ldmSteps", settings.ldmSteps.toString())
|
||||
fd.append("ldmSampler", settings.ldmSampler.toString())
|
||||
fd.append("zitsWireframe", settings.zitsWireframe.toString())
|
||||
fd.append("hdStrategy", hdSettings.hdStrategy)
|
||||
fd.append("hdStrategyCropMargin", hdSettings.hdStrategyCropMargin.toString())
|
||||
fd.append(
|
||||
"hdStrategyCropTrigerSize",
|
||||
hdSettings.hdStrategyCropTrigerSize.toString()
|
||||
)
|
||||
fd.append(
|
||||
"hdStrategyResizeLimit",
|
||||
hdSettings.hdStrategyResizeLimit.toString()
|
||||
)
|
||||
fd.append("hdStrategy", "Crop")
|
||||
fd.append("hdStrategyCropMargin", "128")
|
||||
fd.append("hdStrategyCropTrigerSize", "640")
|
||||
fd.append("hdStrategyResizeLimit", "2048")
|
||||
|
||||
fd.append("prompt", prompt === undefined ? "" : prompt)
|
||||
fd.append(
|
||||
"negativePrompt",
|
||||
negativePrompt === undefined ? "" : negativePrompt
|
||||
)
|
||||
fd.append("prompt", settings.prompt)
|
||||
fd.append("negativePrompt", settings.negativePrompt)
|
||||
fd.append("croperX", croperRect.x.toString())
|
||||
fd.append("croperY", croperRect.y.toString())
|
||||
fd.append("croperHeight", croperRect.height.toString())
|
||||
fd.append("croperWidth", croperRect.width.toString())
|
||||
fd.append("useCroper", settings.showCroper ? "true" : "false")
|
||||
// fd.append("useCroper", settings.showCroper ? "true" : "false")
|
||||
fd.append("useCroper", "false")
|
||||
|
||||
fd.append("sdMaskBlur", settings.sdMaskBlur.toString())
|
||||
fd.append("sdStrength", settings.sdStrength.toString())
|
||||
fd.append("sdSteps", settings.sdSteps.toString())
|
||||
fd.append("sdGuidanceScale", settings.sdGuidanceScale.toString())
|
||||
fd.append("sdSampler", settings.sdSampler.toString())
|
||||
fd.append("sdSeed", seed ? seed.toString() : "-1")
|
||||
fd.append("sdSeed", settings.seed.toString())
|
||||
fd.append("sdMatchHistograms", settings.sdMatchHistograms ? "true" : "false")
|
||||
fd.append("sdScale", (settings.sdScale / 100).toString())
|
||||
|
||||
@@ -69,7 +62,7 @@ export default async function inpaint(
|
||||
"paintByExampleGuidanceScale",
|
||||
settings.paintByExampleGuidanceScale.toString()
|
||||
)
|
||||
fd.append("paintByExampleSeed", seed ? seed.toString() : "-1")
|
||||
fd.append("paintByExampleSeed", settings.seed.toString())
|
||||
fd.append(
|
||||
"paintByExampleMaskBlur",
|
||||
settings.paintByExampleMaskBlur.toString()
|
||||
@@ -94,10 +87,7 @@ export default async function inpaint(
|
||||
"controlnet_conditioning_scale",
|
||||
settings.controlnetConditioningScale.toString()
|
||||
)
|
||||
fd.append(
|
||||
"controlnet_method",
|
||||
ControlNetMethodMap[settings.controlnetMethod.toString()]
|
||||
)
|
||||
fd.append("controlnet_method", settings.controlnetMethod.toString())
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/inpaint`, {
|
||||
@@ -137,6 +127,10 @@ export function currentModel() {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchModelInfos(): Promise<ModelInfo[]> {
|
||||
return api.get("/models").then((response) => response.data)
|
||||
}
|
||||
|
||||
export function isDesktop() {
|
||||
return fetch(`${API_ENDPOINT}/is_desktop`, {
|
||||
method: "GET",
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { create, StoreApi, UseBoundStore } from "zustand"
|
||||
import { create } from "zustand"
|
||||
import { persist } from "zustand/middleware"
|
||||
import { immer } from "zustand/middleware/immer"
|
||||
import { SortBy, SortOrder } from "./types"
|
||||
import { CV2Flag, LDMSampler, ModelInfo, SortBy, SortOrder } from "./types"
|
||||
import { DEFAULT_BRUSH_SIZE } from "./const"
|
||||
import { SDSampler } from "./store"
|
||||
|
||||
type FileManagerState = {
|
||||
sortBy: SortBy
|
||||
sortOrder: SortOrder
|
||||
layout: "rows" | "masonry"
|
||||
searchText: string
|
||||
inputDirectory: string
|
||||
outputDirectory: string
|
||||
}
|
||||
|
||||
type CropperState = {
|
||||
@@ -18,81 +21,256 @@ type CropperState = {
|
||||
height: number
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
model: ModelInfo
|
||||
enableDownloadMask: boolean
|
||||
enableManualInpainting: boolean
|
||||
enableUploadMask: boolean
|
||||
showCroper: boolean
|
||||
|
||||
// For LDM
|
||||
ldmSteps: number
|
||||
ldmSampler: LDMSampler
|
||||
|
||||
// For ZITS
|
||||
zitsWireframe: boolean
|
||||
|
||||
// For OpenCV2
|
||||
cv2Radius: number
|
||||
cv2Flag: CV2Flag
|
||||
|
||||
// For Diffusion moel
|
||||
prompt: string
|
||||
negativePrompt: string
|
||||
seed: number
|
||||
seedFixed: boolean
|
||||
|
||||
// For SD
|
||||
sdMaskBlur: number
|
||||
sdStrength: number
|
||||
sdSteps: number
|
||||
sdGuidanceScale: number
|
||||
sdSampler: SDSampler
|
||||
sdMatchHistograms: boolean
|
||||
sdScale: number
|
||||
|
||||
// Paint by Example
|
||||
paintByExampleSteps: number
|
||||
paintByExampleGuidanceScale: number
|
||||
paintByExampleMaskBlur: number
|
||||
paintByExampleMatchHistograms: boolean
|
||||
|
||||
// InstructPix2Pix
|
||||
p2pSteps: number
|
||||
p2pImageGuidanceScale: number
|
||||
p2pGuidanceScale: number
|
||||
|
||||
// ControlNet
|
||||
controlnetConditioningScale: number
|
||||
controlnetMethod: string
|
||||
}
|
||||
|
||||
type ServerConfig = {
|
||||
plugins: string[]
|
||||
availableControlNet: Record<string, string[]>
|
||||
enableFileManager: boolean
|
||||
enableAutoSaving: boolean
|
||||
}
|
||||
|
||||
type InteractiveSegState = {
|
||||
isInteractiveSeg: boolean
|
||||
isInteractiveSegRunning: boolean
|
||||
clicks: number[][]
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
file: File | null
|
||||
customMask: File | null
|
||||
imageHeight: number
|
||||
imageWidth: number
|
||||
brushSize: number
|
||||
brushSizeScale: number
|
||||
|
||||
isInpainting: boolean
|
||||
isInteractiveSeg: boolean // 是否正处于 sam 状态
|
||||
isInteractiveSegRunning: boolean
|
||||
interactiveSegClicks: number[][]
|
||||
|
||||
prompt: string
|
||||
isPluginRunning: boolean
|
||||
|
||||
interactiveSegState: InteractiveSegState
|
||||
fileManagerState: FileManagerState
|
||||
cropperState: CropperState
|
||||
serverConfig: ServerConfig
|
||||
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
type AppAction = {
|
||||
setFile: (file: File) => void
|
||||
setCustomFile: (file: File) => void
|
||||
setIsInpainting: (newValue: boolean) => void
|
||||
setIsPluginRunning: (newValue: boolean) => void
|
||||
setBrushSize: (newValue: number) => void
|
||||
setImageSize: (width: number, height: number) => void
|
||||
setPrompt: (newValue: string) => void
|
||||
|
||||
setFileManagerSortBy: (newValue: SortBy) => void
|
||||
setFileManagerSortOrder: (newValue: SortOrder) => void
|
||||
setFileManagerLayout: (
|
||||
newValue: AppState["fileManagerState"]["layout"]
|
||||
) => void
|
||||
setFileManagerSearchText: (newValue: string) => void
|
||||
|
||||
setCropperX: (newValue: number) => void
|
||||
setCropperY: (newValue: number) => void
|
||||
setCropperWidth: (newValue: number) => void
|
||||
setCropperHeight: (newValue: number) => void
|
||||
|
||||
setServerConfig: (newValue: ServerConfig) => void
|
||||
setSeed: (newValue: number) => void
|
||||
updateSettings: (newSettings: Partial<Settings>) => void
|
||||
updateFileManagerState: (newState: Partial<FileManagerState>) => void
|
||||
updateInteractiveSegState: (newState: Partial<InteractiveSegState>) => void
|
||||
resetInteractiveSegState: () => void
|
||||
shouldShowPromptInput: () => boolean
|
||||
}
|
||||
|
||||
const defaultValues: AppState = {
|
||||
file: null,
|
||||
customMask: null,
|
||||
imageHeight: 0,
|
||||
imageWidth: 0,
|
||||
brushSize: DEFAULT_BRUSH_SIZE,
|
||||
brushSizeScale: 1,
|
||||
isInpainting: false,
|
||||
isPluginRunning: false,
|
||||
|
||||
interactiveSegState: {
|
||||
isInteractiveSeg: false,
|
||||
isInteractiveSegRunning: false,
|
||||
clicks: [],
|
||||
},
|
||||
|
||||
cropperState: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
fileManagerState: {
|
||||
sortBy: SortBy.CTIME,
|
||||
sortOrder: SortOrder.DESCENDING,
|
||||
layout: "masonry",
|
||||
searchText: "",
|
||||
inputDirectory: "",
|
||||
outputDirectory: "",
|
||||
},
|
||||
serverConfig: {
|
||||
plugins: [],
|
||||
availableControlNet: { SD: [], SD2: [], SDXL: [] },
|
||||
enableFileManager: false,
|
||||
enableAutoSaving: false,
|
||||
},
|
||||
settings: {
|
||||
model: {
|
||||
name: "lama",
|
||||
path: "lama",
|
||||
model_type: "inpaint",
|
||||
support_controlnet: false,
|
||||
support_freeu: false,
|
||||
support_lcm_lora: false,
|
||||
is_single_file_diffusers: false,
|
||||
},
|
||||
showCroper: false,
|
||||
enableDownloadMask: false,
|
||||
enableManualInpainting: false,
|
||||
enableUploadMask: false,
|
||||
ldmSteps: 30,
|
||||
ldmSampler: LDMSampler.ddim,
|
||||
zitsWireframe: true,
|
||||
cv2Radius: 5,
|
||||
cv2Flag: CV2Flag.INPAINT_NS,
|
||||
prompt: "",
|
||||
negativePrompt: "",
|
||||
seed: 42,
|
||||
seedFixed: false,
|
||||
sdMaskBlur: 5,
|
||||
sdStrength: 1.0,
|
||||
sdSteps: 50,
|
||||
sdGuidanceScale: 7.5,
|
||||
sdSampler: SDSampler.uni_pc,
|
||||
sdMatchHistograms: false,
|
||||
sdScale: 100,
|
||||
paintByExampleSteps: 50,
|
||||
paintByExampleGuidanceScale: 7.5,
|
||||
paintByExampleMaskBlur: 5,
|
||||
paintByExampleMatchHistograms: false,
|
||||
p2pSteps: 50,
|
||||
p2pImageGuidanceScale: 1.5,
|
||||
p2pGuidanceScale: 7.5,
|
||||
controlnetConditioningScale: 0.4,
|
||||
controlnetMethod: "lllyasviel/control_v11p_sd15_canny",
|
||||
},
|
||||
}
|
||||
|
||||
export const useStore = create<AppState & AppAction>()(
|
||||
immer(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
file: null,
|
||||
imageHeight: 0,
|
||||
imageWidth: 0,
|
||||
brushSize: DEFAULT_BRUSH_SIZE,
|
||||
brushSizeScale: 1,
|
||||
isInpainting: false,
|
||||
isInteractiveSeg: false,
|
||||
isInteractiveSegRunning: false,
|
||||
interactiveSegClicks: [],
|
||||
prompt: "",
|
||||
cropperState: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
...defaultValues,
|
||||
|
||||
shouldShowPromptInput: (): boolean => {
|
||||
const model_type = get().settings.model.model_type
|
||||
return ["diffusers_sd"].includes(model_type)
|
||||
},
|
||||
fileManagerState: {
|
||||
sortBy: SortBy.CTIME,
|
||||
sortOrder: SortOrder.DESCENDING,
|
||||
layout: "masonry",
|
||||
searchText: "",
|
||||
|
||||
setServerConfig: (newValue: ServerConfig) => {
|
||||
set((state: AppState) => {
|
||||
state.serverConfig = newValue
|
||||
})
|
||||
},
|
||||
|
||||
updateSettings: (newSettings: Partial<Settings>) => {
|
||||
set((state: AppState) => {
|
||||
state.settings = {
|
||||
...state.settings,
|
||||
...newSettings,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
updateFileManagerState: (newState: Partial<FileManagerState>) => {
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState = {
|
||||
...state.fileManagerState,
|
||||
...newState,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
updateInteractiveSegState: (newState: Partial<InteractiveSegState>) => {
|
||||
set((state: AppState) => {
|
||||
state.interactiveSegState = {
|
||||
...state.interactiveSegState,
|
||||
...newState,
|
||||
}
|
||||
})
|
||||
},
|
||||
resetInteractiveSegState: () => {
|
||||
set((state: AppState) => {
|
||||
state.interactiveSegState = defaultValues.interactiveSegState
|
||||
})
|
||||
},
|
||||
|
||||
setIsInpainting: (newValue: boolean) =>
|
||||
set((state: AppState) => {
|
||||
state.isInpainting = newValue
|
||||
}),
|
||||
|
||||
setIsPluginRunning: (newValue: boolean) =>
|
||||
set((state: AppState) => {
|
||||
state.isPluginRunning = newValue
|
||||
}),
|
||||
|
||||
setFile: (file: File) =>
|
||||
set((state: AppState) => {
|
||||
// TODO: 清空各种状态
|
||||
state.file = file
|
||||
}),
|
||||
|
||||
setCustomFile: (file: File) =>
|
||||
set((state: AppState) => {
|
||||
state.customMask = file
|
||||
}),
|
||||
|
||||
setBrushSize: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.brushSize = newValue
|
||||
@@ -107,11 +285,6 @@ export const useStore = create<AppState & AppAction>()(
|
||||
})
|
||||
},
|
||||
|
||||
setPrompt: (newValue: string) =>
|
||||
set((state: AppState) => {
|
||||
state.prompt = newValue
|
||||
}),
|
||||
|
||||
setCropperX: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.cropperState.x = newValue
|
||||
@@ -132,32 +305,18 @@ export const useStore = create<AppState & AppAction>()(
|
||||
state.cropperState.height = newValue
|
||||
}),
|
||||
|
||||
setFileManagerSortBy: (newValue: SortBy) =>
|
||||
setSeed: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.sortBy = newValue
|
||||
}),
|
||||
|
||||
setFileManagerSortOrder: (newValue: SortOrder) =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.sortOrder = newValue
|
||||
}),
|
||||
|
||||
setFileManagerLayout: (newValue: "rows" | "masonry") =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.layout = newValue
|
||||
}),
|
||||
|
||||
setFileManagerSearchText: (newValue: string) =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.searchText = newValue
|
||||
state.settings.seed = newValue
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "ZUSTAND_STATE", // name of the item in the storage (must be unique)
|
||||
version: 0,
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) =>
|
||||
["fileManagerState", "prompt"].includes(key)
|
||||
["fileManagerState", "prompt", "settings"].includes(key)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom, selector } from "recoil"
|
||||
import _ from "lodash"
|
||||
import { CV2Flag, HDStrategy, LDMSampler, ModelsHDSettings } from "./types"
|
||||
import { CV2Flag, LDMSampler } from "./types"
|
||||
|
||||
export enum AIModel {
|
||||
LAMA = "lama",
|
||||
@@ -320,7 +320,6 @@ export interface Settings {
|
||||
graduallyInpainting: boolean
|
||||
runInpaintingManually: boolean
|
||||
model: AIModel
|
||||
hdSettings: ModelsHDSettings
|
||||
|
||||
// For LDM
|
||||
ldmSteps: number
|
||||
@@ -363,107 +362,6 @@ export interface Settings {
|
||||
controlnetMethod: string
|
||||
}
|
||||
|
||||
const defaultHDSettings: ModelsHDSettings = {
|
||||
[AIModel.LAMA]: {
|
||||
hdStrategy: HDStrategy.CROP,
|
||||
hdStrategyResizeLimit: 2048,
|
||||
hdStrategyCropTrigerSize: 800,
|
||||
hdStrategyCropMargin: 196,
|
||||
enabled: true,
|
||||
},
|
||||
[AIModel.LDM]: {
|
||||
hdStrategy: HDStrategy.CROP,
|
||||
hdStrategyResizeLimit: 1080,
|
||||
hdStrategyCropTrigerSize: 1080,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: true,
|
||||
},
|
||||
[AIModel.ZITS]: {
|
||||
hdStrategy: HDStrategy.CROP,
|
||||
hdStrategyResizeLimit: 1024,
|
||||
hdStrategyCropTrigerSize: 1024,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: true,
|
||||
},
|
||||
[AIModel.MAT]: {
|
||||
hdStrategy: HDStrategy.CROP,
|
||||
hdStrategyResizeLimit: 1024,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: true,
|
||||
},
|
||||
[AIModel.FCF]: {
|
||||
hdStrategy: HDStrategy.CROP,
|
||||
hdStrategyResizeLimit: 512,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.SD15]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.ANYTHING4]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.REALISTIC_VISION_1_4]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.SD2]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.PAINT_BY_EXAMPLE]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.PIX2PIX]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.Mange]: {
|
||||
hdStrategy: HDStrategy.CROP,
|
||||
hdStrategyResizeLimit: 1280,
|
||||
hdStrategyCropTrigerSize: 1024,
|
||||
hdStrategyCropMargin: 196,
|
||||
enabled: true,
|
||||
},
|
||||
[AIModel.CV2]: {
|
||||
hdStrategy: HDStrategy.RESIZE,
|
||||
hdStrategyResizeLimit: 1080,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: true,
|
||||
},
|
||||
[AIModel.KANDINSKY22]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export enum SDSampler {
|
||||
ddim = "ddim",
|
||||
pndm = "pndm",
|
||||
@@ -487,7 +385,6 @@ export const settingStateDefault: Settings = {
|
||||
graduallyInpainting: true,
|
||||
runInpaintingManually: false,
|
||||
model: AIModel.LAMA,
|
||||
hdSettings: defaultHDSettings,
|
||||
|
||||
ldmSteps: 25,
|
||||
ldmSampler: LDMSampler.plms,
|
||||
@@ -588,24 +485,6 @@ export const seedState = selector({
|
||||
},
|
||||
})
|
||||
|
||||
export const hdSettingsState = selector({
|
||||
key: "hdSettings",
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
return settings.hdSettings[settings.model]
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const settings = get(settingState)
|
||||
const hdSettings = settings.hdSettings[settings.model]
|
||||
const newHDSettings = { ...hdSettings, ...newValue }
|
||||
|
||||
set(settingState, {
|
||||
...settings,
|
||||
hdSettings: { ...settings.hdSettings, [settings.model]: newHDSettings },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const isSDState = selector({
|
||||
key: "isSD",
|
||||
get: ({ get }) => {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
export interface ModelInfo {
|
||||
name: string
|
||||
path: string
|
||||
model_type:
|
||||
| "inpaint"
|
||||
| "diffusers_sd"
|
||||
| "diffusers_sdxl"
|
||||
| "diffusers_sd_inpaint"
|
||||
| "diffusers_sdxl_inpaint"
|
||||
| "diffusers_other"
|
||||
support_controlnet: boolean
|
||||
support_freeu: boolean
|
||||
support_lcm_lora: boolean
|
||||
is_single_file_diffusers: boolean
|
||||
}
|
||||
|
||||
export enum PluginName {
|
||||
RemoveBG = "RemoveBG",
|
||||
AnimeSeg = "AnimeSeg",
|
||||
@@ -18,12 +34,6 @@ export enum SortOrder {
|
||||
ASCENDING = "asc",
|
||||
}
|
||||
|
||||
export enum HDStrategy {
|
||||
ORIGINAL = "Original",
|
||||
RESIZE = "Resize",
|
||||
CROP = "Crop",
|
||||
}
|
||||
|
||||
export enum LDMSampler {
|
||||
ddim = "ddim",
|
||||
plms = "plms",
|
||||
@@ -34,12 +44,9 @@ export enum CV2Flag {
|
||||
INPAINT_TELEA = "INPAINT_TELEA",
|
||||
}
|
||||
|
||||
export interface HDSettings {
|
||||
hdStrategy: HDStrategy
|
||||
hdStrategyResizeLimit: number
|
||||
hdStrategyCropTrigerSize: number
|
||||
hdStrategyCropMargin: number
|
||||
enabled: boolean
|
||||
export interface Rect {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type ModelsHDSettings = { [key in AIModel]: HDSettings }
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { RecoilRoot } from "recoil"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import App from "./App.tsx"
|
||||
import "./globals.css"
|
||||
import { ThemeProvider } from "./components/theme-provider.tsx"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<RecoilRoot>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<App />
|
||||
</RecoilRoot>
|
||||
</ThemeProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user