update
This commit is contained in:
parent
354a1280a4
commit
142aa64cc6
BIN
web_app/src/assets/kofi_button_black.png
Normal file
BIN
web_app/src/assets/kofi_button_black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 203 KiB |
42
web_app/src/components/Coffee.tsx
Normal file
42
web_app/src/components/Coffee.tsx
Normal 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
|
@ -17,7 +17,6 @@ import {
|
||||
generateMask,
|
||||
isMidClick,
|
||||
isRightClick,
|
||||
loadImage,
|
||||
mouseXY,
|
||||
srcToFile,
|
||||
} from "@/lib/utils"
|
||||
@ -25,10 +24,10 @@ import { Eraser, Eye, Redo, Undo, Expand, Download } from "lucide-react"
|
||||
import { useImage } from "@/hooks/useImage"
|
||||
import { Slider } from "./ui/slider"
|
||||
import { PluginName } from "@/lib/types"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { useStore } from "@/lib/states"
|
||||
import Cropper from "./Cropper"
|
||||
import { InteractiveSegPoints } from "./InteractiveSeg"
|
||||
import useHotKey from "@/hooks/useHotkey"
|
||||
|
||||
const TOOLBAR_HEIGHT = 200
|
||||
const MIN_BRUSH_SIZE = 10
|
||||
@ -44,7 +43,7 @@ export default function Editor(props: EditorProps) {
|
||||
const { toast } = useToast()
|
||||
|
||||
const [
|
||||
idForUpdateView,
|
||||
disableShortCuts,
|
||||
windowSize,
|
||||
isInpainting,
|
||||
imageWidth,
|
||||
@ -76,7 +75,7 @@ export default function Editor(props: EditorProps) {
|
||||
runMannually,
|
||||
runInpainting,
|
||||
] = useStore((state) => [
|
||||
state.idForUpdateView,
|
||||
state.disableShortCuts,
|
||||
state.windowSize,
|
||||
state.isInpainting,
|
||||
state.imageWidth,
|
||||
@ -346,7 +345,7 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
useHotkeys("Escape", handleEscPressed, [
|
||||
useHotKey("Escape", handleEscPressed, [
|
||||
isDraging,
|
||||
isInpainting,
|
||||
resetZoom,
|
||||
@ -509,13 +508,13 @@ export default function Editor(props: EditorProps) {
|
||||
keyboardEvent.preventDefault()
|
||||
undo()
|
||||
}
|
||||
useHotkeys("meta+z,ctrl+z", handleUndo)
|
||||
useHotKey("meta+z,ctrl+z", handleUndo)
|
||||
|
||||
const handleRedo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
|
||||
keyboardEvent.preventDefault()
|
||||
redo()
|
||||
}
|
||||
useHotkeys("shift+ctrl+z,shift+meta+z", handleRedo)
|
||||
useHotKey("shift+ctrl+z,shift+meta+z", handleRedo)
|
||||
|
||||
useKeyPressEvent(
|
||||
"Tab",
|
||||
@ -601,7 +600,7 @@ export default function Editor(props: EditorProps) {
|
||||
return undefined
|
||||
}, [showBrush, isPanning])
|
||||
|
||||
useHotkeys(
|
||||
useHotKey(
|
||||
"[",
|
||||
() => {
|
||||
let newBrushSize = baseBrushSize
|
||||
@ -616,7 +615,7 @@ export default function Editor(props: EditorProps) {
|
||||
[baseBrushSize]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
useHotKey(
|
||||
"]",
|
||||
() => {
|
||||
setBaseBrushSize(baseBrushSize + 10)
|
||||
@ -625,7 +624,7 @@ export default function Editor(props: EditorProps) {
|
||||
)
|
||||
|
||||
// Manual Inpainting Hotkey
|
||||
useHotkeys(
|
||||
useHotKey(
|
||||
"shift+r",
|
||||
() => {
|
||||
if (runMannually && hadDrawSomething()) {
|
||||
@ -635,7 +634,7 @@ export default function Editor(props: EditorProps) {
|
||||
[runMannually, runInpainting, hadDrawSomething]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
useHotKey(
|
||||
"ctrl+c, cmd+c",
|
||||
async () => {
|
||||
const hasPermission = await askWritePermission()
|
||||
@ -655,16 +654,20 @@ export default function Editor(props: EditorProps) {
|
||||
useKeyPressEvent(
|
||||
" ",
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setShowBrush(false)
|
||||
setIsPanning(true)
|
||||
if (!disableShortCuts) {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setShowBrush(false)
|
||||
setIsPanning(true)
|
||||
}
|
||||
},
|
||||
(ev) => {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setShowBrush(true)
|
||||
setIsPanning(false)
|
||||
if (!disableShortCuts) {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
setShowBrush(true)
|
||||
setIsPanning(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -738,7 +741,6 @@ export default function Editor(props: EditorProps) {
|
||||
const renderCanvas = () => {
|
||||
return (
|
||||
<TransformWrapper
|
||||
// ref={viewportRef}
|
||||
ref={(r) => {
|
||||
if (r) {
|
||||
viewportRef.current = r
|
||||
@ -865,7 +867,6 @@ export default function Editor(props: EditorProps) {
|
||||
onMouseUp={onPointerUp}
|
||||
>
|
||||
{renderCanvas()}
|
||||
|
||||
{showBrush &&
|
||||
!isInpainting &&
|
||||
!isPanning &&
|
||||
@ -875,7 +876,7 @@ export default function Editor(props: EditorProps) {
|
||||
|
||||
{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
|
||||
className="w-48"
|
||||
defaultValue={[50]}
|
||||
|
@ -32,10 +32,10 @@ import {
|
||||
} from "./ui/select"
|
||||
import { ScrollArea } from "./ui/scroll-area"
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { useStore } from "@/lib/states"
|
||||
import { SortBy, SortOrder } from "@/lib/types"
|
||||
import { FolderClosed } from "lucide-react"
|
||||
import useHotKey from "@/hooks/useHotkey"
|
||||
|
||||
interface Photo {
|
||||
src: string
|
||||
@ -79,7 +79,7 @@ export default function FileManager(props: Props) {
|
||||
state.updateFileManagerState,
|
||||
])
|
||||
|
||||
useHotkeys("f", () => {
|
||||
useHotKey("f", () => {
|
||||
toggleOpen()
|
||||
})
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { PlayIcon } from "@radix-ui/react-icons"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { IconButton, ImageUploadButton } from "@/components/ui/button"
|
||||
import Shortcuts from "@/components/Shortcuts"
|
||||
import emitter, {
|
||||
@ -19,6 +18,8 @@ import { getMediaFile } from "@/lib/api"
|
||||
import { useStore } from "@/lib/states"
|
||||
import SettingsDialog from "./Settings"
|
||||
import { cn } from "@/lib/utils"
|
||||
import useHotKey from "@/hooks/useHotkey"
|
||||
import Coffee from "./Coffee"
|
||||
|
||||
const Header = () => {
|
||||
const [
|
||||
@ -57,7 +58,7 @@ const Header = () => {
|
||||
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE)
|
||||
}
|
||||
|
||||
useHotkeys(
|
||||
useHotKey(
|
||||
"r",
|
||||
() => {
|
||||
if (!isInpainting) {
|
||||
@ -163,7 +164,7 @@ const Header = () => {
|
||||
{model.need_prompt ? <PromptInput /> : <></>}
|
||||
|
||||
<div className="flex gap-1">
|
||||
{/* <CoffeeIcon /> */}
|
||||
<Coffee />
|
||||
<Shortcuts />
|
||||
<SettingsDialog />
|
||||
</div>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { FormEvent } from "react"
|
||||
import React, { FormEvent, useRef } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Input } from "./ui/input"
|
||||
import { useStore } from "@/lib/states"
|
||||
import { useClickAway } from "react-use"
|
||||
|
||||
const PromptInput = () => {
|
||||
const [isProcessing, prompt, updateSettings, runInpainting] = useStore(
|
||||
@ -12,6 +13,14 @@ const PromptInput = () => {
|
||||
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>) => {
|
||||
evt.preventDefault()
|
||||
@ -43,6 +52,7 @@ const PromptInput = () => {
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<Input
|
||||
ref={ref}
|
||||
className="min-w-[500px]"
|
||||
value={prompt}
|
||||
onInput={handleOnInput}
|
||||
|
@ -1,7 +1,6 @@
|
||||
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"
|
||||
@ -20,7 +19,7 @@ import {
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { fetchModelInfos, switchModel } from "@/lib/api"
|
||||
@ -42,6 +41,7 @@ import {
|
||||
MODEL_TYPE_INPAINT,
|
||||
MODEL_TYPE_OTHER,
|
||||
} from "@/lib/const"
|
||||
import useHotKey from "@/hooks/useHotkey"
|
||||
|
||||
const formSchema = z.object({
|
||||
enableFileManager: z.boolean(),
|
||||
@ -68,13 +68,19 @@ export function SettingsDialog() {
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
const [openModelSwitching, toggleOpenModelSwitching] = useToggle(false)
|
||||
const [tab, setTab] = useState(TAB_MODEL)
|
||||
const [settings, updateSettings, fileManagerState, updateFileManagerState] =
|
||||
useStore((state) => [
|
||||
state.settings,
|
||||
state.updateSettings,
|
||||
state.fileManagerState,
|
||||
state.updateFileManagerState,
|
||||
])
|
||||
const [
|
||||
updateAppState,
|
||||
settings,
|
||||
updateSettings,
|
||||
fileManagerState,
|
||||
updateFileManagerState,
|
||||
] = useStore((state) => [
|
||||
state.updateAppState,
|
||||
state.settings,
|
||||
state.updateSettings,
|
||||
state.fileManagerState,
|
||||
state.updateFileManagerState,
|
||||
])
|
||||
const { toast } = useToast()
|
||||
const [model, setModel] = useState<ModelInfo>(settings.model)
|
||||
|
||||
@ -110,6 +116,7 @@ export function SettingsDialog() {
|
||||
})
|
||||
if (model.name !== settings.model.name) {
|
||||
toggleOpenModelSwitching()
|
||||
updateAppState({ disableShortCuts: true })
|
||||
switchModel(model.name)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
@ -126,14 +133,16 @@ export function SettingsDialog() {
|
||||
variant: "destructive",
|
||||
title: `Switch to ${model.name} failed`,
|
||||
})
|
||||
setModel(settings.model)
|
||||
})
|
||||
.finally(() => {
|
||||
toggleOpenModelSwitching()
|
||||
updateAppState({ disableShortCuts: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useHotkeys("s", () => {
|
||||
useHotKey("s", () => {
|
||||
toggleOpen()
|
||||
onSubmit(form.getValues())
|
||||
})
|
||||
@ -183,6 +192,12 @@ export function SettingsDialog() {
|
||||
for (let info of modelInfos) {
|
||||
if (model.name === info.name) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -384,9 +399,12 @@ export function SettingsDialog() {
|
||||
<AlertDialog open={openModelSwitching}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
TODO: 添加加载动画 Switching to {model.name}
|
||||
</AlertDialogDescription>
|
||||
{/* <AlertDialogDescription> */}
|
||||
<div className="flex flex-col justify-center items-center gap-4">
|
||||
<div>logo</div>
|
||||
<div>Switching to {model.name}</div>
|
||||
</div>
|
||||
{/* </AlertDialogDescription> */}
|
||||
</AlertDialogHeader>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@ -434,7 +452,7 @@ export function SettingsDialog() {
|
||||
)} */}
|
||||
|
||||
<div className="absolute right-10 bottom-6">
|
||||
<Button onClick={() => toggleOpen()}>Ok</Button>
|
||||
<Button onClick={() => onOpenChange(false)}>Ok</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import useHotKey from "@/hooks/useHotkey"
|
||||
|
||||
interface ShortcutProps {
|
||||
content: string
|
||||
@ -48,7 +48,7 @@ const CmdOrCtrl = () => {
|
||||
export function Shortcuts() {
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
|
||||
useHotkeys("h", () => {
|
||||
useHotKey("h", () => {
|
||||
toggleOpen()
|
||||
})
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { FormEvent, useState } from "react"
|
||||
import { useToggle, useWindowSize } from "react-use"
|
||||
import { useStore } from "@/lib/states"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { Label } from "./ui/label"
|
||||
import { NumberInput } from "./ui/input"
|
||||
@ -15,38 +14,36 @@ import {
|
||||
} from "./ui/select"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { SDSampler } from "@/lib/types"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion"
|
||||
import { Separator } from "./ui/separator"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { ScrollArea } from "./ui/scroll-area"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "./ui/sheet"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
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 [settings, updateSettings, showSidePanel] = useStore((state) => [
|
||||
state.settings,
|
||||
state.updateSettings,
|
||||
state.showSidePanel(),
|
||||
])
|
||||
const [open, toggleOpen] = useToggle(true)
|
||||
const [expandedAccordionItems, setExpandedAccordionItems] = useState<
|
||||
string[]
|
||||
>([])
|
||||
const [settings, updateSettings, showSidePanel, runInpainting] = useStore(
|
||||
(state) => [
|
||||
state.settings,
|
||||
state.updateSettings,
|
||||
state.showSidePanel(),
|
||||
state.runInpainting,
|
||||
]
|
||||
)
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
|
||||
useHotkeys("c", () => {
|
||||
useHotKey("c", () => {
|
||||
toggleOpen()
|
||||
})
|
||||
|
||||
@ -58,13 +55,8 @@ const SidePanel = () => {
|
||||
|
||||
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||
// negativePrompt 回车触发 inpainting
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
e.ctrlKey &&
|
||||
settings.prompt.length !== 0
|
||||
// !isInpainting
|
||||
) {
|
||||
console.log("trigger negativePrompt")
|
||||
if (e.key === "Enter" && e.ctrlKey && settings.prompt.length !== 0) {
|
||||
runInpainting()
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,41 +67,62 @@ const SidePanel = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<Label htmlFor="controlnet">Controlnet</Label>
|
||||
<Select
|
||||
value={settings.controlnetMethod}
|
||||
onValueChange={(value) => {
|
||||
updateSettings({ controlnetMethod: value })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="controlnet">
|
||||
<SelectValue placeholder="Select control method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectGroup>
|
||||
{Object.values(settings.model.controlnets).map((method) => (
|
||||
<SelectItem key={method} value={method}>
|
||||
{method.split("/")[1]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center pr-2">
|
||||
<Label htmlFor="controlnet">Controlnet</Label>
|
||||
<Switch
|
||||
id="controlnet"
|
||||
checked={settings.enableControlNet}
|
||||
onCheckedChange={(value) => {
|
||||
updateSettings({ enableControlNet: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pl-1 pr-2">
|
||||
<Select
|
||||
value={settings.controlnetMethod}
|
||||
onValueChange={(value) => {
|
||||
updateSettings({ controlnetMethod: value })
|
||||
}}
|
||||
disabled={!settings.enableControlNet}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select control method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectGroup>
|
||||
{Object.values(settings.model.controlnets).map((method) => (
|
||||
<SelectItem key={method} value={method}>
|
||||
{method.split("/")[1]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="controlnet-weight">weight</Label>
|
||||
<RowContainer>
|
||||
<Label
|
||||
htmlFor="controlnet-weight"
|
||||
disabled={!settings.enableControlNet}
|
||||
>
|
||||
weight
|
||||
</Label>
|
||||
<NumberInput
|
||||
id="controlnet-weight"
|
||||
className="w-14"
|
||||
disabled={!settings.enableControlNet}
|
||||
numberValue={settings.controlnetConditioningScale}
|
||||
allowFloat
|
||||
onNumberValueChange={(value) => {
|
||||
updateSettings({ controlnetConditioningScale: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<Separator />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -120,16 +133,19 @@ const SidePanel = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="lcm-lora">LCM Lora</Label>
|
||||
<Switch
|
||||
id="lcm-lora"
|
||||
checked={settings.enableLCMLora}
|
||||
onCheckedChange={(value) => {
|
||||
updateSettings({ enableLCMLora: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<RowContainer>
|
||||
<Label htmlFor="lcm-lora">LCM Lora</Label>
|
||||
<Switch
|
||||
id="lcm-lora"
|
||||
checked={settings.enableLCMLora}
|
||||
onCheckedChange={(value) => {
|
||||
updateSettings({ enableLCMLora: value })
|
||||
}}
|
||||
/>
|
||||
</RowContainer>
|
||||
<Separator />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -140,7 +156,7 @@ const SidePanel = () => {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Switch
|
||||
id="freeu"
|
||||
@ -152,10 +168,13 @@ const SidePanel = () => {
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<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
|
||||
id="freeu-s1"
|
||||
className="w-14"
|
||||
disabled={!settings.enableFreeu}
|
||||
numberValue={settings.freeuConfig.s1}
|
||||
allowFloat
|
||||
onNumberValueChange={(value) => {
|
||||
@ -166,10 +185,13 @@ const SidePanel = () => {
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
id="freeu-s2"
|
||||
className="w-14"
|
||||
disabled={!settings.enableFreeu}
|
||||
numberValue={settings.freeuConfig.s2}
|
||||
allowFloat
|
||||
onNumberValueChange={(value) => {
|
||||
@ -180,10 +202,13 @@ const SidePanel = () => {
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
id="freeu-b1"
|
||||
className="w-14"
|
||||
disabled={!settings.enableFreeu}
|
||||
numberValue={settings.freeuConfig.b1}
|
||||
allowFloat
|
||||
onNumberValueChange={(value) => {
|
||||
@ -194,10 +219,13 @@ const SidePanel = () => {
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
id="freeu-b2"
|
||||
className="w-14"
|
||||
disabled={!settings.enableFreeu}
|
||||
numberValue={settings.freeuConfig.b2}
|
||||
allowFloat
|
||||
onNumberValueChange={(value) => {
|
||||
@ -208,28 +236,51 @@ const SidePanel = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={toggleOpen} modal={false}>
|
||||
<Sheet open={open} modal={false}>
|
||||
<SheetTrigger
|
||||
tabIndex={-1}
|
||||
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} />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
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()}
|
||||
onPointerDownOutside={(event) => event.preventDefault()}
|
||||
>
|
||||
<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 />
|
||||
</SheetHeader>
|
||||
<ScrollArea
|
||||
@ -237,7 +288,7 @@ const SidePanel = () => {
|
||||
className="pr-3"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
<Label htmlFor="cropper">Cropper</Label>
|
||||
<Switch
|
||||
id="cropper"
|
||||
@ -246,9 +297,9 @@ const SidePanel = () => {
|
||||
updateSettings({ showCroper: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
<Label htmlFor="steps">Steps</Label>
|
||||
<NumberInput
|
||||
id="steps"
|
||||
@ -259,9 +310,9 @@ const SidePanel = () => {
|
||||
updateSettings({ sdSteps: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
<Label htmlFor="guidance-scale">Guidance scale</Label>
|
||||
<NumberInput
|
||||
id="guidance-scale"
|
||||
@ -272,22 +323,28 @@ const SidePanel = () => {
|
||||
updateSettings({ sdGuidanceScale: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="strength">Strength</Label>
|
||||
<NumberInput
|
||||
id="strength"
|
||||
className="w-14"
|
||||
numberValue={settings.sdStrength}
|
||||
allowFloat
|
||||
onNumberValueChange={(value) => {
|
||||
updateSettings({ sdStrength: value })
|
||||
}}
|
||||
<RowContainer>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Label htmlFor="strength">Strength</Label>
|
||||
<div className="text-sm">({settings.sdStrength})</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 })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
<Label htmlFor="sampler">Sampler</Label>
|
||||
<Select
|
||||
value={settings.sdSampler as string}
|
||||
@ -312,9 +369,9 @@ const SidePanel = () => {
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
{/* 每次会从服务器返回更新该值 */}
|
||||
<Label htmlFor="seed">Seed</Label>
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
@ -336,24 +393,26 @@ const SidePanel = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Label htmlFor="negative-prompt">Negative prompt</Label>
|
||||
<Textarea
|
||||
rows={4}
|
||||
onKeyUp={onKeyUp}
|
||||
className="max-h-[8rem] overflow-y-auto mb-2"
|
||||
placeholder=""
|
||||
id="negative-prompt"
|
||||
value={settings.negativePrompt}
|
||||
onInput={(evt: FormEvent<HTMLTextAreaElement>) => {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
const target = evt.target as HTMLTextAreaElement
|
||||
updateSettings({ negativePrompt: target.value })
|
||||
}}
|
||||
/>
|
||||
<div className="pl-2 pr-4">
|
||||
<Textarea
|
||||
rows={4}
|
||||
onKeyUp={onKeyUp}
|
||||
className="max-h-[8rem] overflow-y-auto mb-2"
|
||||
placeholder=""
|
||||
id="negative-prompt"
|
||||
value={settings.negativePrompt}
|
||||
onInput={(evt: FormEvent<HTMLTextAreaElement>) => {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
const target = evt.target as HTMLTextAreaElement
|
||||
updateSettings({ negativePrompt: target.value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
@ -361,15 +420,10 @@ const SidePanel = () => {
|
||||
<div className="flex flex-col gap-4">
|
||||
{renderConterNetSetting()}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
{renderFreeu()}
|
||||
|
||||
<Separator />
|
||||
{renderLCMLora()}
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
<Label htmlFor="mask-blur">Mask blur</Label>
|
||||
<NumberInput
|
||||
id="mask-blur"
|
||||
@ -380,9 +434,9 @@ const SidePanel = () => {
|
||||
updateSettings({ sdMaskBlur: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<RowContainer>
|
||||
<Label htmlFor="match-histograms">Match histograms</Label>
|
||||
<Switch
|
||||
id="match-histograms"
|
||||
@ -391,7 +445,7 @@ const SidePanel = () => {
|
||||
updateSettings({ sdMatchHistograms: value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RowContainer>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
|
@ -52,8 +52,12 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, className }),
|
||||
"outline-none cursor-default"
|
||||
)}
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
@ -37,7 +37,8 @@ const DialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
"outline-none"
|
||||
)}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
{...props}
|
||||
|
@ -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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
|
@ -1,12 +1,23 @@
|
||||
import * as React from "react"
|
||||
import { FocusEvent } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useStore } from "@/lib/states"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ 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 (
|
||||
<input
|
||||
type={type}
|
||||
@ -16,6 +27,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
)}
|
||||
ref={ref}
|
||||
autoComplete="off"
|
||||
tabIndex={-1}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
@ -8,14 +8,19 @@ const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
interface LabelProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
VariantProps<typeof labelVariants> &
|
||||
LabelProps
|
||||
>(({ className, disabled, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
className={cn(labelVariants(), className, disabled ? "opacity-50" : "")}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
@ -29,7 +28,7 @@ const SheetOverlay = React.forwardRef<
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
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: {
|
||||
side: {
|
||||
@ -63,10 +62,6 @@ const SheetContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{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>
|
||||
</SheetPortal>
|
||||
))
|
||||
|
@ -2,7 +2,7 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html { font-family: "Inter", "system-ui"; }
|
||||
html { font-family: "Inter", "system-ui"; overflow: hidden; }
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
html { font-family: "Inter var", "system-ui"; }
|
||||
|
11
web_app/src/hooks/useHotkey.tsx
Normal file
11
web_app/src/hooks/useHotkey.tsx
Normal 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
|
@ -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_OTHER = "diffusers_other"
|
||||
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"
|
||||
|
@ -18,7 +18,13 @@ import {
|
||||
SortBy,
|
||||
SortOrder,
|
||||
} 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 inpaint, { runPlugin } from "./api"
|
||||
import { toast, useToast } from "@/components/ui/use-toast"
|
||||
@ -84,6 +90,7 @@ export type Settings = {
|
||||
p2pGuidanceScale: number
|
||||
|
||||
// ControlNet
|
||||
enableControlNet: boolean
|
||||
controlnetConditioningScale: number
|
||||
controlnetMethod: string
|
||||
|
||||
@ -122,8 +129,6 @@ type EditorState = {
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
idForUpdateView: string
|
||||
|
||||
file: File | null
|
||||
customMask: File | null
|
||||
imageHeight: number
|
||||
@ -132,6 +137,7 @@ type AppState = {
|
||||
isPluginRunning: boolean
|
||||
windowSize: Size
|
||||
editorState: EditorState
|
||||
disableShortCuts: boolean
|
||||
|
||||
interactiveSegState: InteractiveSegState
|
||||
fileManagerState: FileManagerState
|
||||
@ -188,14 +194,13 @@ type AppAction = {
|
||||
}
|
||||
|
||||
const defaultValues: AppState = {
|
||||
idForUpdateView: nanoid(),
|
||||
|
||||
file: null,
|
||||
customMask: null,
|
||||
imageHeight: 0,
|
||||
imageWidth: 0,
|
||||
isInpainting: false,
|
||||
isPluginRunning: false,
|
||||
disableShortCuts: false,
|
||||
|
||||
windowSize: {
|
||||
height: 600,
|
||||
@ -254,6 +259,7 @@ const defaultValues: AppState = {
|
||||
is_single_file_diffusers: false,
|
||||
need_prompt: false,
|
||||
},
|
||||
enableControlNet: false,
|
||||
showCroper: false,
|
||||
enableDownloadMask: false,
|
||||
enableManualInpainting: false,
|
||||
@ -311,7 +317,17 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
|
||||
},
|
||||
|
||||
runInpainting: async () => {
|
||||
const { file, imageWidth, imageHeight, settings, cropperState } = get()
|
||||
const {
|
||||
isInpainting,
|
||||
file,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
settings,
|
||||
cropperState,
|
||||
} = get()
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
|
||||
if (file === null) {
|
||||
return
|
||||
@ -656,13 +672,16 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
|
||||
},
|
||||
|
||||
showPromptInput: (): boolean => {
|
||||
const model_type = get().settings.model.model_type
|
||||
return ["diffusers_sd", "diffusers_sd_inpaint"].includes(model_type)
|
||||
const model = get().settings.model
|
||||
return (
|
||||
model.model_type !== MODEL_TYPE_INPAINT &&
|
||||
model.name !== PAINT_BY_EXAMPLE
|
||||
)
|
||||
},
|
||||
|
||||
showSidePanel: (): boolean => {
|
||||
const model_type = get().settings.model.model_type
|
||||
return ["diffusers_sd", "diffusers_sd_inpaint"].includes(model_type)
|
||||
const model = get().settings.model
|
||||
return model.model_type !== MODEL_TYPE_INPAINT
|
||||
},
|
||||
|
||||
setServerConfig: (newValue: ServerConfig) => {
|
||||
|
Loading…
Reference in New Issue
Block a user