This commit is contained in:
Qing 2023-12-13 22:56:09 +08:00
parent 354a1280a4
commit 142aa64cc6
19 changed files with 360 additions and 181 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@ -0,0 +1,42 @@
import { Coffee as CoffeeIcon } from "lucide-react"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog"
import { IconButton } from "./ui/button"
import { DialogDescription } from "@radix-ui/react-dialog"
import Kofi from "@/assets/kofi_button_black.png"
export function Coffee() {
return (
<Dialog>
<DialogTrigger asChild>
<IconButton tooltip="Buy me a coffee">
<CoffeeIcon />
</IconButton>
</DialogTrigger>
<DialogContent>
<DialogTitle>Buy me a coffee</DialogTitle>
<DialogDescription className="mb-8">
Hi, if you found my project is useful, please conside buy me a coffee
to support my work. Thanks!
</DialogDescription>
<div className="w-full flex items-center justify-center">
<a
href="https://ko-fi.com/Z8Z1CZJGY"
target="_blank"
rel="noreferrer"
>
<img src={Kofi} className="h-[32px]" />
</a>
</div>
</DialogContent>
</Dialog>
)
}
export default Coffee

View File

@ -17,7 +17,6 @@ import {
generateMask, generateMask,
isMidClick, isMidClick,
isRightClick, isRightClick,
loadImage,
mouseXY, mouseXY,
srcToFile, srcToFile,
} from "@/lib/utils" } from "@/lib/utils"
@ -25,10 +24,10 @@ import { Eraser, Eye, Redo, Undo, Expand, Download } from "lucide-react"
import { useImage } from "@/hooks/useImage" import { useImage } from "@/hooks/useImage"
import { Slider } from "./ui/slider" import { Slider } from "./ui/slider"
import { PluginName } from "@/lib/types" import { PluginName } from "@/lib/types"
import { useHotkeys } from "react-hotkeys-hook"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import Cropper from "./Cropper" import Cropper from "./Cropper"
import { InteractiveSegPoints } from "./InteractiveSeg" import { InteractiveSegPoints } from "./InteractiveSeg"
import useHotKey from "@/hooks/useHotkey"
const TOOLBAR_HEIGHT = 200 const TOOLBAR_HEIGHT = 200
const MIN_BRUSH_SIZE = 10 const MIN_BRUSH_SIZE = 10
@ -44,7 +43,7 @@ export default function Editor(props: EditorProps) {
const { toast } = useToast() const { toast } = useToast()
const [ const [
idForUpdateView, disableShortCuts,
windowSize, windowSize,
isInpainting, isInpainting,
imageWidth, imageWidth,
@ -76,7 +75,7 @@ export default function Editor(props: EditorProps) {
runMannually, runMannually,
runInpainting, runInpainting,
] = useStore((state) => [ ] = useStore((state) => [
state.idForUpdateView, state.disableShortCuts,
state.windowSize, state.windowSize,
state.isInpainting, state.isInpainting,
state.imageWidth, state.imageWidth,
@ -346,7 +345,7 @@ export default function Editor(props: EditorProps) {
} }
} }
useHotkeys("Escape", handleEscPressed, [ useHotKey("Escape", handleEscPressed, [
isDraging, isDraging,
isInpainting, isInpainting,
resetZoom, resetZoom,
@ -509,13 +508,13 @@ export default function Editor(props: EditorProps) {
keyboardEvent.preventDefault() keyboardEvent.preventDefault()
undo() undo()
} }
useHotkeys("meta+z,ctrl+z", handleUndo) useHotKey("meta+z,ctrl+z", handleUndo)
const handleRedo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => { const handleRedo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
keyboardEvent.preventDefault() keyboardEvent.preventDefault()
redo() redo()
} }
useHotkeys("shift+ctrl+z,shift+meta+z", handleRedo) useHotKey("shift+ctrl+z,shift+meta+z", handleRedo)
useKeyPressEvent( useKeyPressEvent(
"Tab", "Tab",
@ -601,7 +600,7 @@ export default function Editor(props: EditorProps) {
return undefined return undefined
}, [showBrush, isPanning]) }, [showBrush, isPanning])
useHotkeys( useHotKey(
"[", "[",
() => { () => {
let newBrushSize = baseBrushSize let newBrushSize = baseBrushSize
@ -616,7 +615,7 @@ export default function Editor(props: EditorProps) {
[baseBrushSize] [baseBrushSize]
) )
useHotkeys( useHotKey(
"]", "]",
() => { () => {
setBaseBrushSize(baseBrushSize + 10) setBaseBrushSize(baseBrushSize + 10)
@ -625,7 +624,7 @@ export default function Editor(props: EditorProps) {
) )
// Manual Inpainting Hotkey // Manual Inpainting Hotkey
useHotkeys( useHotKey(
"shift+r", "shift+r",
() => { () => {
if (runMannually && hadDrawSomething()) { if (runMannually && hadDrawSomething()) {
@ -635,7 +634,7 @@ export default function Editor(props: EditorProps) {
[runMannually, runInpainting, hadDrawSomething] [runMannually, runInpainting, hadDrawSomething]
) )
useHotkeys( useHotKey(
"ctrl+c, cmd+c", "ctrl+c, cmd+c",
async () => { async () => {
const hasPermission = await askWritePermission() const hasPermission = await askWritePermission()
@ -655,17 +654,21 @@ export default function Editor(props: EditorProps) {
useKeyPressEvent( useKeyPressEvent(
" ", " ",
(ev) => { (ev) => {
if (!disableShortCuts) {
ev?.preventDefault() ev?.preventDefault()
ev?.stopPropagation() ev?.stopPropagation()
setShowBrush(false) setShowBrush(false)
setIsPanning(true) setIsPanning(true)
}
}, },
(ev) => { (ev) => {
if (!disableShortCuts) {
ev?.preventDefault() ev?.preventDefault()
ev?.stopPropagation() ev?.stopPropagation()
setShowBrush(true) setShowBrush(true)
setIsPanning(false) setIsPanning(false)
} }
}
) )
useKeyPressEvent( useKeyPressEvent(
@ -738,7 +741,6 @@ export default function Editor(props: EditorProps) {
const renderCanvas = () => { const renderCanvas = () => {
return ( return (
<TransformWrapper <TransformWrapper
// ref={viewportRef}
ref={(r) => { ref={(r) => {
if (r) { if (r) {
viewportRef.current = r viewportRef.current = r
@ -865,7 +867,6 @@ export default function Editor(props: EditorProps) {
onMouseUp={onPointerUp} onMouseUp={onPointerUp}
> >
{renderCanvas()} {renderCanvas()}
{showBrush && {showBrush &&
!isInpainting && !isInpainting &&
!isPanning && !isPanning &&
@ -875,7 +876,7 @@ export default function Editor(props: EditorProps) {
{showRefBrush && renderBrush(getBrushStyle(windowCenterX, windowCenterY))} {showRefBrush && renderBrush(getBrushStyle(windowCenterX, windowCenterY))}
<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/70"> <div className=" overflow-hidden fixed flex bottom-5 border px-4 py-2 rounded-[3rem] gap-8 items-center justify-center backdrop-filter backdrop-blur-md bg-background/70">
<Slider <Slider
className="w-48" className="w-48"
defaultValue={[50]} defaultValue={[50]}

View File

@ -32,10 +32,10 @@ import {
} from "./ui/select" } from "./ui/select"
import { ScrollArea } from "./ui/scroll-area" import { ScrollArea } from "./ui/scroll-area"
import { DialogTrigger } from "@radix-ui/react-dialog" import { DialogTrigger } from "@radix-ui/react-dialog"
import { useHotkeys } from "react-hotkeys-hook"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import { SortBy, SortOrder } from "@/lib/types" import { SortBy, SortOrder } from "@/lib/types"
import { FolderClosed } from "lucide-react" import { FolderClosed } from "lucide-react"
import useHotKey from "@/hooks/useHotkey"
interface Photo { interface Photo {
src: string src: string
@ -79,7 +79,7 @@ export default function FileManager(props: Props) {
state.updateFileManagerState, state.updateFileManagerState,
]) ])
useHotkeys("f", () => { useHotKey("f", () => {
toggleOpen() toggleOpen()
}) })

View File

@ -1,6 +1,5 @@
import { PlayIcon } from "@radix-ui/react-icons" import { PlayIcon } from "@radix-ui/react-icons"
import { useCallback, useState } from "react" import { useCallback, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import { IconButton, ImageUploadButton } from "@/components/ui/button" import { IconButton, ImageUploadButton } from "@/components/ui/button"
import Shortcuts from "@/components/Shortcuts" import Shortcuts from "@/components/Shortcuts"
import emitter, { import emitter, {
@ -19,6 +18,8 @@ import { getMediaFile } from "@/lib/api"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import SettingsDialog from "./Settings" import SettingsDialog from "./Settings"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import useHotKey from "@/hooks/useHotkey"
import Coffee from "./Coffee"
const Header = () => { const Header = () => {
const [ const [
@ -57,7 +58,7 @@ const Header = () => {
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE) emitter.emit(DREAM_BUTTON_MOUSE_LEAVE)
} }
useHotkeys( useHotKey(
"r", "r",
() => { () => {
if (!isInpainting) { if (!isInpainting) {
@ -163,7 +164,7 @@ const Header = () => {
{model.need_prompt ? <PromptInput /> : <></>} {model.need_prompt ? <PromptInput /> : <></>}
<div className="flex gap-1"> <div className="flex gap-1">
{/* <CoffeeIcon /> */} <Coffee />
<Shortcuts /> <Shortcuts />
<SettingsDialog /> <SettingsDialog />
</div> </div>

View File

@ -1,7 +1,8 @@
import React, { FormEvent } from "react" import React, { FormEvent, useRef } from "react"
import { Button } from "./ui/button" import { Button } from "./ui/button"
import { Input } from "./ui/input" import { Input } from "./ui/input"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import { useClickAway } from "react-use"
const PromptInput = () => { const PromptInput = () => {
const [isProcessing, prompt, updateSettings, runInpainting] = useStore( const [isProcessing, prompt, updateSettings, runInpainting] = useStore(
@ -12,6 +13,14 @@ const PromptInput = () => {
state.runInpainting, state.runInpainting,
] ]
) )
const ref = useRef(null)
useClickAway<MouseEvent>(ref, () => {
if (ref?.current) {
const input = ref.current as HTMLInputElement
input.blur()
}
})
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => { const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault() evt.preventDefault()
@ -43,6 +52,7 @@ const PromptInput = () => {
return ( return (
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<Input <Input
ref={ref}
className="min-w-[500px]" className="min-w-[500px]"
value={prompt} value={prompt}
onInput={handleOnInput} onInput={handleOnInput}

View File

@ -1,7 +1,6 @@
import { IconButton } from "@/components/ui/button" import { IconButton } from "@/components/ui/button"
import { useToggle } from "@uidotdev/usehooks" import { useToggle } from "@uidotdev/usehooks"
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog" import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog"
import { useHotkeys } from "react-hotkeys-hook"
import { Info, Settings } from "lucide-react" import { Info, Settings } from "lucide-react"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
@ -20,7 +19,7 @@ import {
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Switch } from "./ui/switch" import { Switch } from "./ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { useEffect, useState } from "react" import { useState } from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { fetchModelInfos, switchModel } from "@/lib/api" import { fetchModelInfos, switchModel } from "@/lib/api"
@ -42,6 +41,7 @@ import {
MODEL_TYPE_INPAINT, MODEL_TYPE_INPAINT,
MODEL_TYPE_OTHER, MODEL_TYPE_OTHER,
} from "@/lib/const" } from "@/lib/const"
import useHotKey from "@/hooks/useHotkey"
const formSchema = z.object({ const formSchema = z.object({
enableFileManager: z.boolean(), enableFileManager: z.boolean(),
@ -68,8 +68,14 @@ export function SettingsDialog() {
const [open, toggleOpen] = useToggle(false) const [open, toggleOpen] = useToggle(false)
const [openModelSwitching, toggleOpenModelSwitching] = useToggle(false) const [openModelSwitching, toggleOpenModelSwitching] = useToggle(false)
const [tab, setTab] = useState(TAB_MODEL) const [tab, setTab] = useState(TAB_MODEL)
const [settings, updateSettings, fileManagerState, updateFileManagerState] = const [
useStore((state) => [ updateAppState,
settings,
updateSettings,
fileManagerState,
updateFileManagerState,
] = useStore((state) => [
state.updateAppState,
state.settings, state.settings,
state.updateSettings, state.updateSettings,
state.fileManagerState, state.fileManagerState,
@ -110,6 +116,7 @@ export function SettingsDialog() {
}) })
if (model.name !== settings.model.name) { if (model.name !== settings.model.name) {
toggleOpenModelSwitching() toggleOpenModelSwitching()
updateAppState({ disableShortCuts: true })
switchModel(model.name) switchModel(model.name)
.then((res) => { .then((res) => {
if (res.ok) { if (res.ok) {
@ -126,14 +133,16 @@ export function SettingsDialog() {
variant: "destructive", variant: "destructive",
title: `Switch to ${model.name} failed`, title: `Switch to ${model.name} failed`,
}) })
setModel(settings.model)
}) })
.finally(() => { .finally(() => {
toggleOpenModelSwitching() toggleOpenModelSwitching()
updateAppState({ disableShortCuts: false })
}) })
} }
} }
useHotkeys("s", () => { useHotKey("s", () => {
toggleOpen() toggleOpen()
onSubmit(form.getValues()) onSubmit(form.getValues())
}) })
@ -183,6 +192,12 @@ export function SettingsDialog() {
for (let info of modelInfos) { for (let info of modelInfos) {
if (model.name === info.name) { if (model.name === info.name) {
defaultTab = info.model_type defaultTab = info.model_type
if (defaultTab === MODEL_TYPE_DIFFUSERS_SDXL) {
defaultTab = MODEL_TYPE_DIFFUSERS_SD
}
if (defaultTab === MODEL_TYPE_DIFFUSERS_SDXL_INPAINT) {
defaultTab = MODEL_TYPE_DIFFUSERS_SD_INPAINT
}
break break
} }
} }
@ -384,9 +399,12 @@ export function SettingsDialog() {
<AlertDialog open={openModelSwitching}> <AlertDialog open={openModelSwitching}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogDescription> {/* <AlertDialogDescription> */}
TODO: 添加加载动画 Switching to {model.name} <div className="flex flex-col justify-center items-center gap-4">
</AlertDialogDescription> <div>logo</div>
<div>Switching to {model.name}</div>
</div>
{/* </AlertDialogDescription> */}
</AlertDialogHeader> </AlertDialogHeader>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
@ -434,7 +452,7 @@ export function SettingsDialog() {
)} */} )} */}
<div className="absolute right-10 bottom-6"> <div className="absolute right-10 bottom-6">
<Button onClick={() => toggleOpen()}>Ok</Button> <Button onClick={() => onOpenChange(false)}>Ok</Button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -8,7 +8,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "./ui/dialog" } from "./ui/dialog"
import { useHotkeys } from "react-hotkeys-hook" import useHotKey from "@/hooks/useHotkey"
interface ShortcutProps { interface ShortcutProps {
content: string content: string
@ -48,7 +48,7 @@ const CmdOrCtrl = () => {
export function Shortcuts() { export function Shortcuts() {
const [open, toggleOpen] = useToggle(false) const [open, toggleOpen] = useToggle(false)
useHotkeys("h", () => { useHotKey("h", () => {
toggleOpen() toggleOpen()
}) })

View File

@ -1,7 +1,6 @@
import { FormEvent, useState } from "react" import { FormEvent, useState } from "react"
import { useToggle, useWindowSize } from "react-use" import { useToggle, useWindowSize } from "react-use"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
import { Switch } from "./ui/switch" import { Switch } from "./ui/switch"
import { Label } from "./ui/label" import { Label } from "./ui/label"
import { NumberInput } from "./ui/input" import { NumberInput } from "./ui/input"
@ -15,38 +14,36 @@ import {
} from "./ui/select" } from "./ui/select"
import { Textarea } from "./ui/textarea" import { Textarea } from "./ui/textarea"
import { SDSampler } from "@/lib/types" import { SDSampler } from "@/lib/types"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion"
import { Separator } from "./ui/separator" import { Separator } from "./ui/separator"
import { useHotkeys } from "react-hotkeys-hook"
import { ScrollArea } from "./ui/scroll-area" import { ScrollArea } from "./ui/scroll-area"
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from "./ui/sheet" } from "./ui/sheet"
import { ChevronLeft } from "lucide-react" import { ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "./ui/button" import { Button } from "./ui/button"
import useHotKey from "@/hooks/useHotkey"
import { Slider } from "./ui/slider"
const RowContainer = ({ children }: { children: React.ReactNode }) => (
<div className="flex justify-between items-center pr-2">{children}</div>
)
const SidePanel = () => { const SidePanel = () => {
const [settings, updateSettings, showSidePanel] = useStore((state) => [ const [settings, updateSettings, showSidePanel, runInpainting] = useStore(
(state) => [
state.settings, state.settings,
state.updateSettings, state.updateSettings,
state.showSidePanel(), state.showSidePanel(),
]) state.runInpainting,
const [open, toggleOpen] = useToggle(true) ]
const [expandedAccordionItems, setExpandedAccordionItems] = useState< )
string[] const [open, toggleOpen] = useToggle(false)
>([])
useHotkeys("c", () => { useHotKey("c", () => {
toggleOpen() toggleOpen()
}) })
@ -58,13 +55,8 @@ const SidePanel = () => {
const onKeyUp = (e: React.KeyboardEvent) => { const onKeyUp = (e: React.KeyboardEvent) => {
// negativePrompt 回车触发 inpainting // negativePrompt 回车触发 inpainting
if ( if (e.key === "Enter" && e.ctrlKey && settings.prompt.length !== 0) {
e.key === "Enter" && runInpainting()
e.ctrlKey &&
settings.prompt.length !== 0
// !isInpainting
) {
console.log("trigger negativePrompt")
} }
} }
@ -75,15 +67,27 @@ const SidePanel = () => {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col items-start gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-between items-center pr-2">
<Label htmlFor="controlnet">Controlnet</Label> <Label htmlFor="controlnet">Controlnet</Label>
<Switch
id="controlnet"
checked={settings.enableControlNet}
onCheckedChange={(value) => {
updateSettings({ enableControlNet: value })
}}
/>
</div>
<div className="pl-1 pr-2">
<Select <Select
value={settings.controlnetMethod} value={settings.controlnetMethod}
onValueChange={(value) => { onValueChange={(value) => {
updateSettings({ controlnetMethod: value }) updateSettings({ controlnetMethod: value })
}} }}
disabled={!settings.enableControlNet}
> >
<SelectTrigger id="controlnet"> <SelectTrigger>
<SelectValue placeholder="Select control method" /> <SelectValue placeholder="Select control method" />
</SelectTrigger> </SelectTrigger>
<SelectContent align="end"> <SelectContent align="end">
@ -97,19 +101,28 @@ const SidePanel = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="controlnet-weight">weight</Label> <Label
htmlFor="controlnet-weight"
disabled={!settings.enableControlNet}
>
weight
</Label>
<NumberInput <NumberInput
id="controlnet-weight" id="controlnet-weight"
className="w-14" className="w-14"
disabled={!settings.enableControlNet}
numberValue={settings.controlnetConditioningScale} numberValue={settings.controlnetConditioningScale}
allowFloat allowFloat
onNumberValueChange={(value) => { onNumberValueChange={(value) => {
updateSettings({ controlnetConditioningScale: value }) updateSettings({ controlnetConditioningScale: value })
}} }}
/> />
</div> </RowContainer>
<Separator />
</div> </div>
) )
} }
@ -120,7 +133,8 @@ const SidePanel = () => {
} }
return ( return (
<div className="flex justify-between items-center"> <>
<RowContainer>
<Label htmlFor="lcm-lora">LCM Lora</Label> <Label htmlFor="lcm-lora">LCM Lora</Label>
<Switch <Switch
id="lcm-lora" id="lcm-lora"
@ -129,7 +143,9 @@ const SidePanel = () => {
updateSettings({ enableLCMLora: value }) updateSettings({ enableLCMLora: value })
}} }}
/> />
</div> </RowContainer>
<Separator />
</>
) )
} }
@ -140,7 +156,7 @@ const SidePanel = () => {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center pr-2">
<Label htmlFor="freeu">Freeu</Label> <Label htmlFor="freeu">Freeu</Label>
<Switch <Switch
id="freeu" id="freeu"
@ -152,10 +168,13 @@ const SidePanel = () => {
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<div className="flex flex-col gap-2 items-start"> <div className="flex flex-col gap-2 items-start">
<Label htmlFor="freeu-s1">s1</Label> <Label htmlFor="freeu-s1" disabled={!settings.enableFreeu}>
s1
</Label>
<NumberInput <NumberInput
id="freeu-s1" id="freeu-s1"
className="w-14" className="w-14"
disabled={!settings.enableFreeu}
numberValue={settings.freeuConfig.s1} numberValue={settings.freeuConfig.s1}
allowFloat allowFloat
onNumberValueChange={(value) => { onNumberValueChange={(value) => {
@ -166,10 +185,13 @@ const SidePanel = () => {
/> />
</div> </div>
<div className="flex flex-col gap-2 items-start"> <div className="flex flex-col gap-2 items-start">
<Label htmlFor="freeu-s2">s2</Label> <Label htmlFor="freeu-s2" disabled={!settings.enableFreeu}>
s2
</Label>
<NumberInput <NumberInput
id="freeu-s2" id="freeu-s2"
className="w-14" className="w-14"
disabled={!settings.enableFreeu}
numberValue={settings.freeuConfig.s2} numberValue={settings.freeuConfig.s2}
allowFloat allowFloat
onNumberValueChange={(value) => { onNumberValueChange={(value) => {
@ -180,10 +202,13 @@ const SidePanel = () => {
/> />
</div> </div>
<div className="flex flex-col gap-2 items-start"> <div className="flex flex-col gap-2 items-start">
<Label htmlFor="freeu-b1">b1</Label> <Label htmlFor="freeu-b1" disabled={!settings.enableFreeu}>
b1
</Label>
<NumberInput <NumberInput
id="freeu-b1" id="freeu-b1"
className="w-14" className="w-14"
disabled={!settings.enableFreeu}
numberValue={settings.freeuConfig.b1} numberValue={settings.freeuConfig.b1}
allowFloat allowFloat
onNumberValueChange={(value) => { onNumberValueChange={(value) => {
@ -194,10 +219,13 @@ const SidePanel = () => {
/> />
</div> </div>
<div className="flex flex-col gap-2 items-start"> <div className="flex flex-col gap-2 items-start">
<Label htmlFor="freeu-b2">b2</Label> <Label htmlFor="freeu-b2" disabled={!settings.enableFreeu}>
b2
</Label>
<NumberInput <NumberInput
id="freeu-b2" id="freeu-b2"
className="w-14" className="w-14"
disabled={!settings.enableFreeu}
numberValue={settings.freeuConfig.b2} numberValue={settings.freeuConfig.b2}
allowFloat allowFloat
onNumberValueChange={(value) => { onNumberValueChange={(value) => {
@ -208,28 +236,51 @@ const SidePanel = () => {
/> />
</div> </div>
</div> </div>
<Separator />
</div> </div>
) )
} }
return ( return (
<Sheet open={open} onOpenChange={toggleOpen} modal={false}> <Sheet open={open} modal={false}>
<SheetTrigger <SheetTrigger
tabIndex={-1} tabIndex={-1}
className="z-10 outline-none absolute top-[68px] right-6 rounded-lg border bg-background" className="z-10 outline-none absolute top-[68px] right-6 rounded-lg border bg-background"
> >
<Button variant="ghost" size="icon" asChild className="p-1.5"> <Button
variant="ghost"
size="icon"
asChild
className="p-1.5"
onClick={toggleOpen}
>
<ChevronLeft strokeWidth={1} /> <ChevronLeft strokeWidth={1} />
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent <SheetContent
side="right" side="right"
className="w-[300px] mt-[60px] outline-none pl-4 pr-1" className="w-[300px] mt-[60px] outline-none pl-4 pr-1 backdrop-filter backdrop-blur-md bg-background/70"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onPointerDownOutside={(event) => event.preventDefault()} onPointerDownOutside={(event) => event.preventDefault()}
> >
<SheetHeader className="mb-4"> <SheetHeader className="mb-4">
<SheetTitle>Diffusion Paramers</SheetTitle> <RowContainer>
<div className="overflow-hidden mr-8">
{
settings.model.name.split("/")[
settings.model.name.split("/").length - 1
]
}
</div>
<Button
variant="ghost"
size="icon"
className="border h-6 w-6"
onClick={toggleOpen}
>
<ChevronRight strokeWidth={1} />
</Button>
</RowContainer>
<Separator /> <Separator />
</SheetHeader> </SheetHeader>
<ScrollArea <ScrollArea
@ -237,7 +288,7 @@ const SidePanel = () => {
className="pr-3" className="pr-3"
> >
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="cropper">Cropper</Label> <Label htmlFor="cropper">Cropper</Label>
<Switch <Switch
id="cropper" id="cropper"
@ -246,9 +297,9 @@ const SidePanel = () => {
updateSettings({ showCroper: value }) updateSettings({ showCroper: value })
}} }}
/> />
</div> </RowContainer>
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="steps">Steps</Label> <Label htmlFor="steps">Steps</Label>
<NumberInput <NumberInput
id="steps" id="steps"
@ -259,9 +310,9 @@ const SidePanel = () => {
updateSettings({ sdSteps: value }) updateSettings({ sdSteps: value })
}} }}
/> />
</div> </RowContainer>
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="guidance-scale">Guidance scale</Label> <Label htmlFor="guidance-scale">Guidance scale</Label>
<NumberInput <NumberInput
id="guidance-scale" id="guidance-scale"
@ -272,22 +323,28 @@ const SidePanel = () => {
updateSettings({ sdGuidanceScale: value }) updateSettings({ sdGuidanceScale: value })
}} }}
/> />
</div> </RowContainer>
<div className="flex justify-between items-center"> <RowContainer>
<div className="flex gap-2 items-center">
<Label htmlFor="strength">Strength</Label> <Label htmlFor="strength">Strength</Label>
<NumberInput <div className="text-sm">({settings.sdStrength})</div>
id="strength"
className="w-14"
numberValue={settings.sdStrength}
allowFloat
onNumberValueChange={(value) => {
updateSettings({ sdStrength: value })
}}
/>
</div> </div>
<Slider
className="w-24"
defaultValue={[100]}
min={10}
max={100}
step={1}
tabIndex={-1}
value={[Math.floor(settings.sdStrength * 100)]}
onValueChange={(vals) =>
updateSettings({ sdStrength: vals[0] / 100 })
}
/>
</RowContainer>
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="sampler">Sampler</Label> <Label htmlFor="sampler">Sampler</Label>
<Select <Select
value={settings.sdSampler as string} value={settings.sdSampler as string}
@ -312,9 +369,9 @@ const SidePanel = () => {
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </RowContainer>
<div className="flex justify-between items-center"> <RowContainer>
{/* 每次会从服务器返回更新该值 */} {/* 每次会从服务器返回更新该值 */}
<Label htmlFor="seed">Seed</Label> <Label htmlFor="seed">Seed</Label>
<div className="flex gap-2 justify-center items-center"> <div className="flex gap-2 justify-center items-center">
@ -336,10 +393,11 @@ const SidePanel = () => {
}} }}
/> />
</div> </div>
</div> </RowContainer>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Label htmlFor="negative-prompt">Negative prompt</Label> <Label htmlFor="negative-prompt">Negative prompt</Label>
<div className="pl-2 pr-4">
<Textarea <Textarea
rows={4} rows={4}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
@ -355,21 +413,17 @@ const SidePanel = () => {
}} }}
/> />
</div> </div>
</div>
<Separator /> <Separator />
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{renderConterNetSetting()} {renderConterNetSetting()}
</div> </div>
<Separator />
{renderFreeu()} {renderFreeu()}
<Separator />
{renderLCMLora()} {renderLCMLora()}
<Separator />
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="mask-blur">Mask blur</Label> <Label htmlFor="mask-blur">Mask blur</Label>
<NumberInput <NumberInput
id="mask-blur" id="mask-blur"
@ -380,9 +434,9 @@ const SidePanel = () => {
updateSettings({ sdMaskBlur: value }) updateSettings({ sdMaskBlur: value })
}} }}
/> />
</div> </RowContainer>
<div className="flex justify-between items-center"> <RowContainer>
<Label htmlFor="match-histograms">Match histograms</Label> <Label htmlFor="match-histograms">Match histograms</Label>
<Switch <Switch
id="match-histograms" id="match-histograms"
@ -391,7 +445,7 @@ const SidePanel = () => {
updateSettings({ sdMatchHistograms: value }) updateSettings({ sdMatchHistograms: value })
}} }}
/> />
</div> </RowContainer>
</div> </div>
</ScrollArea> </ScrollArea>
</SheetContent> </SheetContent>

View File

@ -52,8 +52,12 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(
buttonVariants({ variant, size, className }),
"outline-none cursor-default"
)}
ref={ref} ref={ref}
tabIndex={-1}
{...props} {...props}
/> />
) )

View File

@ -37,7 +37,8 @@ const DialogContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 flex flex-col 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", "fixed left-[50%] top-[50%] z-50 flex flex-col 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 className,
"outline-none"
)} )}
onCloseAutoFocus={(event) => event.preventDefault()} onCloseAutoFocus={(event) => event.preventDefault()}
{...props} {...props}

View File

@ -71,8 +71,8 @@ const DropdownMenuContent = React.forwardRef<
"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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className
)} )}
{...props}
onCloseAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()}
{...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ))

View File

@ -1,12 +1,23 @@
import * as React from "react" import * as React from "react"
import { FocusEvent } from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useStore } from "@/lib/states"
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
const updateAppState = useStore((state) => state.updateAppState)
const handleOnFocus = (evt: FocusEvent<any>) => {
updateAppState({ disableShortCuts: true })
}
const handleOnBlur = (evt: FocusEvent<any>) => {
updateAppState({ disableShortCuts: false })
}
return ( return (
<input <input
type={type} type={type}
@ -16,6 +27,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
)} )}
ref={ref} ref={ref}
autoComplete="off" autoComplete="off"
tabIndex={-1}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
{...props} {...props}
/> />
) )

View File

@ -8,14 +8,19 @@ const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
) )
interface LabelProps {
disabled?: boolean
}
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants> VariantProps<typeof labelVariants> &
>(({ className, ...props }, ref) => ( LabelProps
>(({ className, disabled, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root
ref={ref} ref={ref}
className={cn(labelVariants(), className)} className={cn(labelVariants(), className, disabled ? "opacity-50" : "")}
{...props} {...props}
/> />
)) ))

View File

@ -1,6 +1,5 @@
import * as React from "react" import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -29,7 +28,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva( const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300",
{ {
variants: { variants: {
side: { side: {
@ -63,10 +62,6 @@ const SheetContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
)) ))

View File

@ -2,7 +2,7 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html { font-family: "Inter", "system-ui"; } html { font-family: "Inter", "system-ui"; overflow: hidden; }
@supports (font-variation-settings: normal) { @supports (font-variation-settings: normal) {
html { font-family: "Inter var", "system-ui"; } html { font-family: "Inter var", "system-ui"; }

View File

@ -0,0 +1,11 @@
import { useStore } from "@/lib/states"
import { useHotkeys } from "react-hotkeys-hook"
const useHotKey = (keys: string, callback: any, deps?: any[]) => {
const disableShortCuts = useStore((state) => state.disableShortCuts)
const ref = useHotkeys(keys, callback, { enabled: !disableShortCuts }, deps)
return ref
}
export default useHotKey

View File

@ -7,3 +7,7 @@ export const MODEL_TYPE_DIFFUSERS_SD_INPAINT = "diffusers_sd_inpaint"
export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint" export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
export const MODEL_TYPE_OTHER = "diffusers_other" export const MODEL_TYPE_OTHER = "diffusers_other"
export const BRUSH_COLOR = "#ffcc00bb" export const BRUSH_COLOR = "#ffcc00bb"
export const PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example"
export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix"
export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint"

View File

@ -18,7 +18,13 @@ import {
SortBy, SortBy,
SortOrder, SortOrder,
} from "./types" } from "./types"
import { DEFAULT_BRUSH_SIZE, MODEL_TYPE_INPAINT } from "./const" import {
DEFAULT_BRUSH_SIZE,
INSTRUCT_PIX2PIX,
MODEL_TYPE_INPAINT,
MODEL_TYPE_OTHER,
PAINT_BY_EXAMPLE,
} from "./const"
import { dataURItoBlob, generateMask, loadImage, srcToFile } from "./utils" import { dataURItoBlob, generateMask, loadImage, srcToFile } from "./utils"
import inpaint, { runPlugin } from "./api" import inpaint, { runPlugin } from "./api"
import { toast, useToast } from "@/components/ui/use-toast" import { toast, useToast } from "@/components/ui/use-toast"
@ -84,6 +90,7 @@ export type Settings = {
p2pGuidanceScale: number p2pGuidanceScale: number
// ControlNet // ControlNet
enableControlNet: boolean
controlnetConditioningScale: number controlnetConditioningScale: number
controlnetMethod: string controlnetMethod: string
@ -122,8 +129,6 @@ type EditorState = {
} }
type AppState = { type AppState = {
idForUpdateView: string
file: File | null file: File | null
customMask: File | null customMask: File | null
imageHeight: number imageHeight: number
@ -132,6 +137,7 @@ type AppState = {
isPluginRunning: boolean isPluginRunning: boolean
windowSize: Size windowSize: Size
editorState: EditorState editorState: EditorState
disableShortCuts: boolean
interactiveSegState: InteractiveSegState interactiveSegState: InteractiveSegState
fileManagerState: FileManagerState fileManagerState: FileManagerState
@ -188,14 +194,13 @@ type AppAction = {
} }
const defaultValues: AppState = { const defaultValues: AppState = {
idForUpdateView: nanoid(),
file: null, file: null,
customMask: null, customMask: null,
imageHeight: 0, imageHeight: 0,
imageWidth: 0, imageWidth: 0,
isInpainting: false, isInpainting: false,
isPluginRunning: false, isPluginRunning: false,
disableShortCuts: false,
windowSize: { windowSize: {
height: 600, height: 600,
@ -254,6 +259,7 @@ const defaultValues: AppState = {
is_single_file_diffusers: false, is_single_file_diffusers: false,
need_prompt: false, need_prompt: false,
}, },
enableControlNet: false,
showCroper: false, showCroper: false,
enableDownloadMask: false, enableDownloadMask: false,
enableManualInpainting: false, enableManualInpainting: false,
@ -311,7 +317,17 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
}, },
runInpainting: async () => { runInpainting: async () => {
const { file, imageWidth, imageHeight, settings, cropperState } = get() const {
isInpainting,
file,
imageWidth,
imageHeight,
settings,
cropperState,
} = get()
if (isInpainting) {
return
}
if (file === null) { if (file === null) {
return return
@ -656,13 +672,16 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
}, },
showPromptInput: (): boolean => { showPromptInput: (): boolean => {
const model_type = get().settings.model.model_type const model = get().settings.model
return ["diffusers_sd", "diffusers_sd_inpaint"].includes(model_type) return (
model.model_type !== MODEL_TYPE_INPAINT &&
model.name !== PAINT_BY_EXAMPLE
)
}, },
showSidePanel: (): boolean => { showSidePanel: (): boolean => {
const model_type = get().settings.model.model_type const model = get().settings.model
return ["diffusers_sd", "diffusers_sd_inpaint"].includes(model_type) return model.model_type !== MODEL_TYPE_INPAINT
}, },
setServerConfig: (newValue: ServerConfig) => { setServerConfig: (newValue: ServerConfig) => {