new web init
This commit is contained in:
270
web_app/src/lib/api.ts
Normal file
270
web_app/src/lib/api.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { PluginName } from "@/lib/types"
|
||||
import { ControlNetMethodMap, Rect, Settings } from "@/lib/store"
|
||||
import { dataURItoBlob, loadImage, srcToFile } from "@/lib/utils"
|
||||
|
||||
export const API_ENDPOINT = import.meta.env.VITE_BACKEND
|
||||
? import.meta.env.VITE_BACKEND
|
||||
: ""
|
||||
|
||||
export default async function inpaint(
|
||||
imageFile: File,
|
||||
settings: Settings,
|
||||
croperRect: Rect,
|
||||
prompt?: string,
|
||||
negativePrompt?: string,
|
||||
seed?: number,
|
||||
maskBase64?: string,
|
||||
customMask?: File,
|
||||
paintByExampleImage?: File
|
||||
) {
|
||||
// 1080, 2000, Original
|
||||
const fd = new FormData()
|
||||
fd.append("image", imageFile)
|
||||
if (maskBase64 !== undefined) {
|
||||
fd.append("mask", dataURItoBlob(maskBase64))
|
||||
} else if (customMask !== undefined) {
|
||||
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("prompt", prompt === undefined ? "" : prompt)
|
||||
fd.append(
|
||||
"negativePrompt",
|
||||
negativePrompt === undefined ? "" : 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("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("sdMatchHistograms", settings.sdMatchHistograms ? "true" : "false")
|
||||
fd.append("sdScale", (settings.sdScale / 100).toString())
|
||||
|
||||
fd.append("cv2Radius", settings.cv2Radius.toString())
|
||||
fd.append("cv2Flag", settings.cv2Flag.toString())
|
||||
|
||||
fd.append("paintByExampleSteps", settings.paintByExampleSteps.toString())
|
||||
fd.append(
|
||||
"paintByExampleGuidanceScale",
|
||||
settings.paintByExampleGuidanceScale.toString()
|
||||
)
|
||||
fd.append("paintByExampleSeed", seed ? seed.toString() : "-1")
|
||||
fd.append(
|
||||
"paintByExampleMaskBlur",
|
||||
settings.paintByExampleMaskBlur.toString()
|
||||
)
|
||||
fd.append(
|
||||
"paintByExampleMatchHistograms",
|
||||
settings.paintByExampleMatchHistograms ? "true" : "false"
|
||||
)
|
||||
// TODO: resize image's shortest_edge to 224 before pass to backend, save network time?
|
||||
// https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPImageProcessor
|
||||
if (paintByExampleImage) {
|
||||
fd.append("paintByExampleImage", paintByExampleImage)
|
||||
}
|
||||
|
||||
// InstructPix2Pix
|
||||
fd.append("p2pSteps", settings.p2pSteps.toString())
|
||||
fd.append("p2pImageGuidanceScale", settings.p2pImageGuidanceScale.toString())
|
||||
fd.append("p2pGuidanceScale", settings.p2pGuidanceScale.toString())
|
||||
|
||||
// ControlNet
|
||||
fd.append(
|
||||
"controlnet_conditioning_scale",
|
||||
settings.controlnetConditioningScale.toString()
|
||||
)
|
||||
fd.append(
|
||||
"controlnet_method",
|
||||
ControlNetMethodMap[settings.controlnetMethod.toString()]
|
||||
)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/inpaint`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
if (res.ok) {
|
||||
const blob = await res.blob()
|
||||
const newSeed = res.headers.get("x-seed")
|
||||
return { blob: URL.createObjectURL(blob), seed: newSeed }
|
||||
}
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerConfig() {
|
||||
return fetch(`${API_ENDPOINT}/server_config`, {
|
||||
method: "GET",
|
||||
})
|
||||
}
|
||||
|
||||
export function switchModel(name: string) {
|
||||
const fd = new FormData()
|
||||
fd.append("name", name)
|
||||
return fetch(`${API_ENDPOINT}/model`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
}
|
||||
|
||||
export function currentModel() {
|
||||
return fetch(`${API_ENDPOINT}/model`, {
|
||||
method: "GET",
|
||||
})
|
||||
}
|
||||
|
||||
export function isDesktop() {
|
||||
return fetch(`${API_ENDPOINT}/is_desktop`, {
|
||||
method: "GET",
|
||||
})
|
||||
}
|
||||
|
||||
export function modelDownloaded(name: string) {
|
||||
return fetch(`${API_ENDPOINT}/model_downloaded/${name}`, {
|
||||
method: "GET",
|
||||
})
|
||||
}
|
||||
|
||||
export async function runPlugin(
|
||||
name: string,
|
||||
imageFile: File,
|
||||
upscale?: number,
|
||||
maskFile?: File | null,
|
||||
clicks?: number[][]
|
||||
) {
|
||||
const fd = new FormData()
|
||||
fd.append("name", name)
|
||||
fd.append("image", imageFile)
|
||||
if (upscale) {
|
||||
fd.append("upscale", upscale.toString())
|
||||
}
|
||||
if (clicks) {
|
||||
fd.append("clicks", JSON.stringify(clicks))
|
||||
}
|
||||
if (maskFile) {
|
||||
fd.append("mask", maskFile)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/run_plugin`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
if (res.ok) {
|
||||
const blob = await res.blob()
|
||||
return { blob: URL.createObjectURL(blob) }
|
||||
}
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMediaFile(tab: string, filename: string) {
|
||||
const res = await fetch(
|
||||
`${API_ENDPOINT}/media/${tab}/${encodeURIComponent(filename)}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
const blob = await res.blob()
|
||||
const file = new File([blob], filename)
|
||||
return file
|
||||
}
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
export async function getMedias(tab: string) {
|
||||
const res = await fetch(`${API_ENDPOINT}/medias/${tab}`, {
|
||||
method: "GET",
|
||||
})
|
||||
if (res.ok) {
|
||||
const filenames = await res.json()
|
||||
return filenames
|
||||
}
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
export async function downloadToOutput(
|
||||
image: HTMLImageElement,
|
||||
filename: string,
|
||||
mimeType: string
|
||||
) {
|
||||
const file = await srcToFile(image.src, filename, mimeType)
|
||||
const fd = new FormData()
|
||||
fd.append("image", file)
|
||||
fd.append("filename", filename)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/save_image`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeGif(
|
||||
originFile: File,
|
||||
cleanImage: HTMLImageElement,
|
||||
filename: string,
|
||||
mimeType: string
|
||||
) {
|
||||
const cleanFile = await srcToFile(cleanImage.src, filename, mimeType)
|
||||
const fd = new FormData()
|
||||
fd.append("name", PluginName.MakeGIF)
|
||||
fd.append("image", originFile)
|
||||
fd.append("clean_img", cleanFile)
|
||||
fd.append("filename", filename)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/run_plugin`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
const blob = await res.blob()
|
||||
const newImage = new Image()
|
||||
await loadImage(newImage, URL.createObjectURL(blob))
|
||||
return newImage
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`)
|
||||
}
|
||||
}
|
||||
22
web_app/src/lib/event.ts
Normal file
22
web_app/src/lib/event.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import mitt from "mitt"
|
||||
|
||||
export const EVENT_PROMPT = "prompt"
|
||||
|
||||
export const EVENT_CUSTOM_MASK = "custom_mask"
|
||||
export interface CustomMaskEventData {
|
||||
mask: File
|
||||
}
|
||||
|
||||
export const EVENT_PAINT_BY_EXAMPLE = "paint_by_example"
|
||||
export interface PaintByExampleEventData {
|
||||
image: File
|
||||
}
|
||||
|
||||
export const RERUN_LAST_MASK = "rerun_last_mask"
|
||||
|
||||
export const DREAM_BUTTON_MOUSE_ENTER = "dream_button_mouse_enter"
|
||||
export const DREAM_BUTTON_MOUSE_LEAVE = "dream_btoon_mouse_leave"
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
export default emitter
|
||||
902
web_app/src/lib/store.ts
Normal file
902
web_app/src/lib/store.ts
Normal file
@@ -0,0 +1,902 @@
|
||||
import { atom, selector } from "recoil"
|
||||
import _ from "lodash"
|
||||
|
||||
export enum HDStrategy {
|
||||
ORIGINAL = "Original",
|
||||
RESIZE = "Resize",
|
||||
CROP = "Crop",
|
||||
}
|
||||
|
||||
export enum LDMSampler {
|
||||
ddim = "ddim",
|
||||
plms = "plms",
|
||||
}
|
||||
|
||||
function strEnum<T extends string>(o: Array<T>): { [K in T]: K } {
|
||||
return o.reduce((res, key) => {
|
||||
res[key] = key
|
||||
return res
|
||||
}, Object.create(null))
|
||||
}
|
||||
|
||||
export enum AIModel {
|
||||
LAMA = "lama",
|
||||
LDM = "ldm",
|
||||
ZITS = "zits",
|
||||
MAT = "mat",
|
||||
FCF = "fcf",
|
||||
SD15 = "sd1.5",
|
||||
ANYTHING4 = "anything4",
|
||||
REALISTIC_VISION_1_4 = "realisticVision1.4",
|
||||
SD2 = "sd2",
|
||||
CV2 = "cv2",
|
||||
Mange = "manga",
|
||||
PAINT_BY_EXAMPLE = "paint_by_example",
|
||||
PIX2PIX = "instruct_pix2pix",
|
||||
KANDINSKY22 = "kandinsky2.2",
|
||||
}
|
||||
|
||||
export enum ControlNetMethod {
|
||||
canny = "canny",
|
||||
inpaint = "inpaint",
|
||||
openpose = "openpose",
|
||||
depth = "depth",
|
||||
}
|
||||
|
||||
export const ControlNetMethodMap: any = {
|
||||
canny: "control_v11p_sd15_canny",
|
||||
inpaint: "control_v11p_sd15_inpaint",
|
||||
openpose: "control_v11p_sd15_openpose",
|
||||
depth: "control_v11f1p_sd15_depth",
|
||||
}
|
||||
|
||||
export const ControlNetMethodMap2: any = {
|
||||
control_v11p_sd15_canny: "canny",
|
||||
control_v11p_sd15_inpaint: "inpaint",
|
||||
control_v11p_sd15_openpose: "openpose",
|
||||
control_v11f1p_sd15_depth: "depth",
|
||||
}
|
||||
|
||||
export const maskState = atom<File | undefined>({
|
||||
key: "maskState",
|
||||
default: undefined,
|
||||
})
|
||||
|
||||
export const paintByExampleImageState = atom<File | undefined>({
|
||||
key: "paintByExampleImageState",
|
||||
default: undefined,
|
||||
})
|
||||
|
||||
export interface Rect {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
file: File | undefined
|
||||
imageHeight: number
|
||||
imageWidth: number
|
||||
disableShortCuts: boolean
|
||||
isInpainting: boolean
|
||||
isDisableModelSwitch: boolean
|
||||
isEnableAutoSaving: boolean
|
||||
isInteractiveSeg: boolean
|
||||
isInteractiveSegRunning: boolean
|
||||
interactiveSegClicks: number[][]
|
||||
enableFileManager: boolean
|
||||
gifImage: HTMLImageElement | undefined
|
||||
brushSize: number
|
||||
isControlNet: boolean
|
||||
controlNetMethod: string
|
||||
plugins: string[]
|
||||
isPluginRunning: boolean
|
||||
}
|
||||
|
||||
export const appState = atom<AppState>({
|
||||
key: "appState",
|
||||
default: {
|
||||
file: undefined,
|
||||
imageHeight: 0,
|
||||
imageWidth: 0,
|
||||
disableShortCuts: false,
|
||||
isInpainting: false,
|
||||
isDisableModelSwitch: false,
|
||||
isEnableAutoSaving: false,
|
||||
isInteractiveSeg: false,
|
||||
isInteractiveSegRunning: false,
|
||||
interactiveSegClicks: [],
|
||||
enableFileManager: false,
|
||||
gifImage: undefined,
|
||||
brushSize: 40,
|
||||
isControlNet: false,
|
||||
controlNetMethod: ControlNetMethod.canny,
|
||||
plugins: [],
|
||||
isPluginRunning: false,
|
||||
},
|
||||
})
|
||||
|
||||
export const propmtState = atom<string>({
|
||||
key: "promptState",
|
||||
default: "",
|
||||
})
|
||||
|
||||
export const negativePropmtState = atom<string>({
|
||||
key: "negativePromptState",
|
||||
default: "",
|
||||
})
|
||||
|
||||
export const isInpaintingState = selector({
|
||||
key: "isInpainting",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isInpainting
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isInpainting: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const isPluginRunningState = selector({
|
||||
key: "isPluginRunningState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isPluginRunning
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isPluginRunning: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const serverConfigState = selector({
|
||||
key: "serverConfigState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return {
|
||||
isControlNet: app.isControlNet,
|
||||
controlNetMethod: app.controlNetMethod,
|
||||
isDisableModelSwitchState: app.isDisableModelSwitch,
|
||||
isEnableAutoSaving: app.isEnableAutoSaving,
|
||||
enableFileManager: app.enableFileManager,
|
||||
plugins: app.plugins,
|
||||
}
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
const methodShortName = ControlNetMethodMap2[newValue.controlNetMethod]
|
||||
set(appState, { ...app, ...newValue, controlnetMethod: methodShortName })
|
||||
|
||||
const setting = get(settingState)
|
||||
set(settingState, {
|
||||
...setting,
|
||||
controlnetMethod: methodShortName,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const brushSizeState = selector({
|
||||
key: "brushSizeState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.brushSize
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, brushSize: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const imageHeightState = selector({
|
||||
key: "imageHeightState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.imageHeight
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, imageHeight: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const imageWidthState = selector({
|
||||
key: "imageWidthState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.imageWidth
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, imageWidth: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const enableFileManagerState = selector({
|
||||
key: "enableFileManagerState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.enableFileManager
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, enableFileManager: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const gifImageState = selector({
|
||||
key: "gifImageState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.gifImage
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, gifImage: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const fileState = selector({
|
||||
key: "fileState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.file
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, {
|
||||
...app,
|
||||
file: newValue,
|
||||
interactiveSegClicks: [],
|
||||
isInteractiveSeg: false,
|
||||
isInteractiveSegRunning: false,
|
||||
})
|
||||
|
||||
const setting = get(settingState)
|
||||
set(settingState, {
|
||||
...setting,
|
||||
sdScale: 100,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const isInteractiveSegState = selector({
|
||||
key: "isInteractiveSegState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isInteractiveSeg
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isInteractiveSeg: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const isInteractiveSegRunningState = selector({
|
||||
key: "isInteractiveSegRunningState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isInteractiveSegRunning
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isInteractiveSegRunning: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const isProcessingState = selector({
|
||||
key: "isProcessingState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return (
|
||||
app.isInteractiveSegRunning || app.isPluginRunning || app.isInpainting
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const interactiveSegClicksState = selector({
|
||||
key: "interactiveSegClicksState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.interactiveSegClicks
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, interactiveSegClicks: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const isDisableModelSwitchState = selector({
|
||||
key: "isDisableModelSwitchState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isDisableModelSwitch
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isDisableModelSwitch: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const isControlNetState = selector({
|
||||
key: "isControlNetState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isControlNet
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isControlNet: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const isEnableAutoSavingState = selector({
|
||||
key: "isEnableAutoSavingState",
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isEnableAutoSaving
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isEnableAutoSaving: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperState = atom<Rect>({
|
||||
key: "croperState",
|
||||
default: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
})
|
||||
|
||||
export const SIDE_PANEL_TAB = strEnum(["inpainting", "outpainting"])
|
||||
export type SIDE_PANEL_TAB_TYPE = keyof typeof SIDE_PANEL_TAB
|
||||
|
||||
export interface SidePanelState {
|
||||
tab: SIDE_PANEL_TAB_TYPE
|
||||
}
|
||||
|
||||
export const sidePanelTabState = atom<SidePanelState>({
|
||||
key: "sidePanelTabState",
|
||||
default: {
|
||||
tab: SIDE_PANEL_TAB.inpainting,
|
||||
},
|
||||
})
|
||||
|
||||
export const croperX = selector({
|
||||
key: "croperX",
|
||||
get: ({ get }) => get(croperState).x,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, x: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperY = selector({
|
||||
key: "croperY",
|
||||
get: ({ get }) => get(croperState).y,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, y: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperHeight = selector({
|
||||
key: "croperHeight",
|
||||
get: ({ get }) => get(croperState).height,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, height: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperWidth = selector({
|
||||
key: "croperWidth",
|
||||
get: ({ get }) => get(croperState).width,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, width: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const extenderState = atom<Rect>({
|
||||
key: "extenderState",
|
||||
default: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
})
|
||||
|
||||
export const extenderX = selector({
|
||||
key: "extenderX",
|
||||
get: ({ get }) => get(extenderState).x,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(extenderState)
|
||||
set(extenderState, { ...rect, x: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const extenderY = selector({
|
||||
key: "extenderY",
|
||||
get: ({ get }) => get(extenderState).y,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(extenderState)
|
||||
set(extenderState, { ...rect, y: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const extenderHeight = selector({
|
||||
key: "extenderHeight",
|
||||
get: ({ get }) => get(extenderState).height,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(extenderState)
|
||||
set(extenderState, { ...rect, height: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const extenderWidth = selector({
|
||||
key: "extenderWidth",
|
||||
get: ({ get }) => get(extenderState).width,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(extenderState)
|
||||
set(extenderState, { ...rect, width: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
interface ToastAtomState {
|
||||
open: boolean
|
||||
desc: string
|
||||
state: ToastState
|
||||
duration: number
|
||||
}
|
||||
|
||||
export const toastState = atom<ToastAtomState>({
|
||||
key: "toastState",
|
||||
default: {
|
||||
open: false,
|
||||
desc: "",
|
||||
state: "default",
|
||||
duration: 3000,
|
||||
},
|
||||
})
|
||||
|
||||
export const shortcutsState = atom<boolean>({
|
||||
key: "shortcutsState",
|
||||
default: false,
|
||||
})
|
||||
|
||||
export interface HDSettings {
|
||||
hdStrategy: HDStrategy
|
||||
hdStrategyResizeLimit: number
|
||||
hdStrategyCropTrigerSize: number
|
||||
hdStrategyCropMargin: number
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
type ModelsHDSettings = { [key in AIModel]: HDSettings }
|
||||
|
||||
export enum CV2Flag {
|
||||
INPAINT_NS = "INPAINT_NS",
|
||||
INPAINT_TELEA = "INPAINT_TELEA",
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
show: boolean
|
||||
showCroper: boolean
|
||||
downloadMask: boolean
|
||||
graduallyInpainting: boolean
|
||||
runInpaintingManually: boolean
|
||||
model: AIModel
|
||||
hdSettings: ModelsHDSettings
|
||||
|
||||
// For LDM
|
||||
ldmSteps: number
|
||||
ldmSampler: LDMSampler
|
||||
|
||||
// For ZITS
|
||||
zitsWireframe: boolean
|
||||
|
||||
// For SD
|
||||
sdMaskBlur: number
|
||||
sdMode: SDMode
|
||||
sdStrength: number
|
||||
sdSteps: number
|
||||
sdGuidanceScale: number
|
||||
sdSampler: SDSampler
|
||||
sdSeed: number
|
||||
sdSeedFixed: boolean // true: use sdSeed, false: random generate seed on backend
|
||||
sdNumSamples: number
|
||||
sdMatchHistograms: boolean
|
||||
sdScale: number
|
||||
|
||||
// For OpenCV2
|
||||
cv2Radius: number
|
||||
cv2Flag: CV2Flag
|
||||
|
||||
// Paint by Example
|
||||
paintByExampleSteps: number
|
||||
paintByExampleGuidanceScale: number
|
||||
paintByExampleSeed: number
|
||||
paintByExampleSeedFixed: boolean
|
||||
paintByExampleMaskBlur: number
|
||||
paintByExampleMatchHistograms: boolean
|
||||
|
||||
// InstructPix2Pix
|
||||
p2pSteps: number
|
||||
p2pImageGuidanceScale: number
|
||||
p2pGuidanceScale: number
|
||||
|
||||
// ControlNet
|
||||
controlnetConditioningScale: number
|
||||
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",
|
||||
klms = "k_lms",
|
||||
kEuler = "k_euler",
|
||||
kEulerA = "k_euler_a",
|
||||
dpmPlusPlus = "dpm++",
|
||||
uni_pc = "uni_pc",
|
||||
}
|
||||
|
||||
export enum SDMode {
|
||||
text2img = "text2img",
|
||||
img2img = "img2img",
|
||||
inpainting = "inpainting",
|
||||
}
|
||||
|
||||
export const settingStateDefault: Settings = {
|
||||
show: false,
|
||||
showCroper: false,
|
||||
downloadMask: false,
|
||||
graduallyInpainting: true,
|
||||
runInpaintingManually: false,
|
||||
model: AIModel.LAMA,
|
||||
hdSettings: defaultHDSettings,
|
||||
|
||||
ldmSteps: 25,
|
||||
ldmSampler: LDMSampler.plms,
|
||||
|
||||
zitsWireframe: true,
|
||||
|
||||
// SD
|
||||
sdMaskBlur: 5,
|
||||
sdMode: SDMode.inpainting,
|
||||
sdStrength: 0.75,
|
||||
sdSteps: 50,
|
||||
sdGuidanceScale: 7.5,
|
||||
sdSampler: SDSampler.uni_pc,
|
||||
sdSeed: 42,
|
||||
sdSeedFixed: false,
|
||||
sdNumSamples: 1,
|
||||
sdMatchHistograms: false,
|
||||
sdScale: 100,
|
||||
|
||||
// CV2
|
||||
cv2Radius: 5,
|
||||
cv2Flag: CV2Flag.INPAINT_NS,
|
||||
|
||||
// Paint by Example
|
||||
paintByExampleSteps: 50,
|
||||
paintByExampleGuidanceScale: 7.5,
|
||||
paintByExampleSeed: 42,
|
||||
paintByExampleMaskBlur: 5,
|
||||
paintByExampleSeedFixed: false,
|
||||
paintByExampleMatchHistograms: false,
|
||||
|
||||
// InstructPix2Pix
|
||||
p2pSteps: 50,
|
||||
p2pImageGuidanceScale: 1.5,
|
||||
p2pGuidanceScale: 7.5,
|
||||
|
||||
// ControlNet
|
||||
controlnetConditioningScale: 0.4,
|
||||
controlnetMethod: ControlNetMethod.canny,
|
||||
}
|
||||
|
||||
const localStorageEffect =
|
||||
(key: string) =>
|
||||
({ setSelf, onSet }: any) => {
|
||||
const savedValue = localStorage.getItem(key)
|
||||
if (savedValue != null) {
|
||||
const storageSettings = JSON.parse(savedValue)
|
||||
storageSettings.show = false
|
||||
|
||||
const restored = _.merge(
|
||||
_.cloneDeep(settingStateDefault),
|
||||
storageSettings
|
||||
)
|
||||
setSelf(restored)
|
||||
}
|
||||
|
||||
onSet((newValue: Settings, val: string, isReset: boolean) =>
|
||||
isReset
|
||||
? localStorage.removeItem(key)
|
||||
: localStorage.setItem(key, JSON.stringify(newValue))
|
||||
)
|
||||
}
|
||||
|
||||
const ROOT_STATE_KEY = "settingsState4"
|
||||
// Each atom can reference an array of these atom effect functions which are called in priority order when the atom is initialized
|
||||
// https://recoiljs.org/docs/guides/atom-effects/#local-storage-persistence
|
||||
export const settingState = atom<Settings>({
|
||||
key: ROOT_STATE_KEY,
|
||||
default: settingStateDefault,
|
||||
effects: [localStorageEffect(ROOT_STATE_KEY)],
|
||||
})
|
||||
|
||||
export const seedState = selector({
|
||||
key: "seed",
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
switch (settings.model) {
|
||||
case AIModel.PAINT_BY_EXAMPLE:
|
||||
return settings.paintByExampleSeedFixed
|
||||
? settings.paintByExampleSeed
|
||||
: -1
|
||||
default:
|
||||
return settings.sdSeedFixed ? settings.sdSeed : -1
|
||||
}
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const settings = get(settingState)
|
||||
switch (settings.model) {
|
||||
case AIModel.PAINT_BY_EXAMPLE:
|
||||
if (!settings.paintByExampleSeedFixed) {
|
||||
set(settingState, { ...settings, paintByExampleSeed: newValue })
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!settings.sdSeedFixed) {
|
||||
set(settingState, { ...settings, sdSeed: newValue })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
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 }) => {
|
||||
const settings = get(settingState)
|
||||
return (
|
||||
settings.model === AIModel.SD15 ||
|
||||
settings.model === AIModel.SD2 ||
|
||||
settings.model === AIModel.ANYTHING4 ||
|
||||
settings.model === AIModel.REALISTIC_VISION_1_4 ||
|
||||
settings.model === AIModel.KANDINSKY22
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const isPaintByExampleState = selector({
|
||||
key: "isPaintByExampleState",
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
return settings.model === AIModel.PAINT_BY_EXAMPLE
|
||||
},
|
||||
})
|
||||
|
||||
export const isPix2PixState = selector({
|
||||
key: "isPix2PixState",
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
return settings.model === AIModel.PIX2PIX
|
||||
},
|
||||
})
|
||||
|
||||
export const runManuallyState = selector({
|
||||
key: "runManuallyState",
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
const isSD = get(isSDState)
|
||||
const isPaintByExample = get(isPaintByExampleState)
|
||||
const isPix2Pix = get(isPix2PixState)
|
||||
return (
|
||||
settings.runInpaintingManually || isSD || isPaintByExample || isPix2Pix
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const isDiffusionModelsState = selector({
|
||||
key: "isDiffusionModelsState",
|
||||
get: ({ get }) => {
|
||||
const isSD = get(isSDState)
|
||||
const isPaintByExample = get(isPaintByExampleState)
|
||||
const isPix2Pix = get(isPix2PixState)
|
||||
return isSD || isPaintByExample || isPix2Pix
|
||||
},
|
||||
})
|
||||
|
||||
export enum SortBy {
|
||||
NAME = "name",
|
||||
CTIME = "ctime",
|
||||
MTIME = "mtime",
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
DESCENDING = "desc",
|
||||
ASCENDING = "asc",
|
||||
}
|
||||
|
||||
interface FileManagerState {
|
||||
sortBy: SortBy
|
||||
sortOrder: SortOrder
|
||||
layout: "rows" | "masonry"
|
||||
searchText: string
|
||||
}
|
||||
|
||||
const FILE_MANAGER_STATE_KEY = "fileManagerState"
|
||||
|
||||
export const fileManagerState = atom<FileManagerState>({
|
||||
key: FILE_MANAGER_STATE_KEY,
|
||||
default: {
|
||||
sortBy: SortBy.CTIME,
|
||||
sortOrder: SortOrder.DESCENDING,
|
||||
layout: "masonry",
|
||||
searchText: "",
|
||||
},
|
||||
effects: [localStorageEffect(FILE_MANAGER_STATE_KEY)],
|
||||
})
|
||||
|
||||
export const fileManagerSortBy = selector({
|
||||
key: "fileManagerSortBy",
|
||||
get: ({ get }) => get(fileManagerState).sortBy,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const val = get(fileManagerState)
|
||||
set(fileManagerState, { ...val, sortBy: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const fileManagerSortOrder = selector({
|
||||
key: "fileManagerSortOrder",
|
||||
get: ({ get }) => get(fileManagerState).sortOrder,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const val = get(fileManagerState)
|
||||
set(fileManagerState, { ...val, sortOrder: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const fileManagerLayout = selector({
|
||||
key: "fileManagerLayout",
|
||||
get: ({ get }) => get(fileManagerState).layout,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const val = get(fileManagerState)
|
||||
set(fileManagerState, { ...val, layout: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const fileManagerSearchText = selector({
|
||||
key: "fileManagerSearchText",
|
||||
get: ({ get }) => get(fileManagerState).searchText,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const val = get(fileManagerState)
|
||||
set(fileManagerState, { ...val, searchText: newValue })
|
||||
},
|
||||
})
|
||||
9
web_app/src/lib/types.ts
Normal file
9
web_app/src/lib/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum PluginName {
|
||||
RemoveBG = "RemoveBG",
|
||||
AnimeSeg = "AnimeSeg",
|
||||
RealESRGAN = "RealESRGAN",
|
||||
GFPGAN = "GFPGAN",
|
||||
RestoreFormer = "RestoreFormer",
|
||||
InteractiveSeg = "InteractiveSeg",
|
||||
MakeGIF = "MakeGIF",
|
||||
}
|
||||
133
web_app/src/lib/utils.ts
Normal file
133
web_app/src/lib/utils.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { SyntheticEvent } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function keepGUIAlive() {
|
||||
async function getRequest(url = "") {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
})
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const keepAliveServer = () => {
|
||||
const url = document.location
|
||||
const route = "/flaskwebgui-keep-server-alive"
|
||||
getRequest(url + route).then((data) => {
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
const intervalRequest = 3 * 1000
|
||||
keepAliveServer()
|
||||
setInterval(keepAliveServer, intervalRequest)
|
||||
}
|
||||
|
||||
export function dataURItoBlob(dataURI: string) {
|
||||
const mime = dataURI.split(",")[0].split(":")[1].split(";")[0]
|
||||
const binary = atob(dataURI.split(",")[1])
|
||||
const array = []
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
array.push(binary.charCodeAt(i))
|
||||
}
|
||||
return new Blob([new Uint8Array(array)], { type: mime })
|
||||
}
|
||||
|
||||
export function loadImage(image: HTMLImageElement, src: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const initSRC = image.src
|
||||
const img = image
|
||||
img.onload = resolve
|
||||
img.onerror = (err) => {
|
||||
img.src = initSRC
|
||||
reject(err)
|
||||
}
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
||||
export function srcToFile(src: string, fileName: string, mimeType: string) {
|
||||
return fetch(src)
|
||||
.then(function (res) {
|
||||
return res.arrayBuffer()
|
||||
})
|
||||
.then(function (buf) {
|
||||
return new File([buf], fileName, { type: mimeType })
|
||||
})
|
||||
}
|
||||
|
||||
export async function askWritePermission() {
|
||||
try {
|
||||
// The clipboard-write permission is granted automatically to pages
|
||||
// when they are the active tab. So it's not required, but it's more safe.
|
||||
const { state } = await navigator.permissions.query({
|
||||
name: "clipboard-write" as PermissionName,
|
||||
})
|
||||
return state === "granted"
|
||||
} catch (error) {
|
||||
// Browser compatibility / Security error (ONLY HTTPS) ...
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function canvasToBlob(canvas: HTMLCanvasElement, mime: string): Promise<any> {
|
||||
return new Promise((resolve, reject) =>
|
||||
canvas.toBlob(async (d) => {
|
||||
if (d) {
|
||||
resolve(d)
|
||||
} else {
|
||||
reject(new Error("Expected toBlob() to be defined"))
|
||||
}
|
||||
}, mime)
|
||||
)
|
||||
}
|
||||
|
||||
const setToClipboard = async (blob: any) => {
|
||||
const data = [new ClipboardItem({ [blob.type]: blob })]
|
||||
await navigator.clipboard.write(data)
|
||||
}
|
||||
|
||||
export function isRightClick(ev: SyntheticEvent) {
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent
|
||||
return mouseEvent.button === 2
|
||||
}
|
||||
|
||||
export function isMidClick(ev: SyntheticEvent) {
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent
|
||||
return mouseEvent.button === 1
|
||||
}
|
||||
|
||||
export async function copyCanvasImage(canvas: HTMLCanvasElement) {
|
||||
const blob = await canvasToBlob(canvas, "image/png")
|
||||
try {
|
||||
await setToClipboard(blob)
|
||||
} catch {
|
||||
console.log("Copy image failed!")
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadImage(uri: string, name: string) {
|
||||
const link = document.createElement("a")
|
||||
link.href = uri
|
||||
link.download = name
|
||||
|
||||
// this is necessary as link.click() does not work on the latest firefox
|
||||
link.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
})
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
// For Firefox it is necessary to delay revoking the ObjectURL
|
||||
// window.URL.revokeObjectURL(base64)
|
||||
link.remove()
|
||||
}, 100)
|
||||
}
|
||||
Reference in New Issue
Block a user