|
<!DOCTYPE html> |
|
<html lang="en"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Mixlab APP</title> |
|
<style> |
|
body { |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
.header { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-around; |
|
} |
|
|
|
.app { |
|
display: flex; |
|
width: 90%; |
|
min-width: 400px; |
|
margin-left: 5%; |
|
user-select: none; |
|
margin-top: 32px; |
|
margin-bottom: 120px; |
|
} |
|
|
|
.apps { |
|
margin: 0 32px; |
|
background: whitesmoke; |
|
color: black; |
|
padding: 12px; |
|
cursor: pointer; |
|
margin: 8px 44px; |
|
} |
|
|
|
.apps .content { |
|
display: flex; |
|
flex-wrap: wrap; |
|
} |
|
|
|
|
|
.apps .card { |
|
width: 320px; |
|
margin: 12px; |
|
display: flex; |
|
flex-direction: row; |
|
background: #ffffff; |
|
cursor: pointer; |
|
user-select: none; |
|
} |
|
|
|
#history_container .card { |
|
width: 200px; |
|
margin: 12px; |
|
display: flex; |
|
flex-direction: row; |
|
background: #ffffff; |
|
cursor: pointer; |
|
user-select: none; |
|
} |
|
|
|
.selected { |
|
box-shadow: 0px 0px 10px 10px #fbe9f0 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
.card h5 { |
|
font-size: 14px; |
|
margin: 0 0 10px 0 |
|
} |
|
|
|
.card p { |
|
margin: 0; |
|
padding: 0; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
.apps .card img { |
|
width: auto; |
|
height: 100%; |
|
margin: 0px; |
|
} |
|
|
|
#history_container .card img { |
|
width: auto; |
|
height: 100%; |
|
margin: 0px; |
|
} |
|
|
|
.toggle { |
|
border: none; |
|
} |
|
|
|
.card .item { |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: space-between; |
|
align-items: flex-start; |
|
} |
|
|
|
.card .icon { |
|
width: 48px; |
|
height: 48px; |
|
overflow: hidden; |
|
background: #e3e3e3; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
} |
|
|
|
|
|
.card .version { |
|
font-size: 12px; |
|
} |
|
|
|
.card .toggle { |
|
margin-left: 12px; |
|
background: none; |
|
color: black; |
|
} |
|
|
|
.card .toggle:hover { |
|
color: #7f7f7f !important; |
|
} |
|
|
|
em .toggle:hover { |
|
color: #7f7f7f !important; |
|
} |
|
|
|
.status_seed { |
|
display: flex; |
|
flex-direction: column; |
|
margin: 12px; |
|
} |
|
|
|
.seeds { |
|
font-size: 12px; |
|
margin-top: 4px; |
|
} |
|
|
|
|
|
.description { |
|
display: flex; |
|
margin: 12px; |
|
background: white; |
|
padding: 8px; |
|
width: 80%; |
|
} |
|
|
|
.description p { |
|
max-width: 200px; |
|
word-wrap: break-word; |
|
} |
|
|
|
.description img { |
|
min-height: unset !important; |
|
} |
|
|
|
.panel { |
|
display: flex; |
|
flex-direction: column; |
|
min-width: 400px; |
|
|
|
margin: 24px; |
|
flex: 1; |
|
align-items: center; |
|
|
|
} |
|
|
|
.panel .header { |
|
margin-bottom: 8px; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
|
|
} |
|
|
|
.panel h1 { |
|
padding: 0 12px; |
|
margin: 0; |
|
} |
|
|
|
.panel img, |
|
video { |
|
height: fit-content; |
|
width: fit-content; |
|
max-width: 100%; |
|
margin-left: 12px; |
|
|
|
} |
|
|
|
.input_card { |
|
display: flex; |
|
flex-direction: column; |
|
width: 100%; |
|
} |
|
|
|
.output { |
|
width: 90%; |
|
margin: 12px; |
|
} |
|
|
|
.output_card { |
|
height: 100%; |
|
width: 100%; |
|
|
|
margin-top: 24px; |
|
display: flex; |
|
flex-wrap: wrap; |
|
|
|
|
|
|
|
} |
|
|
|
.output_card img, |
|
video { |
|
max-width: 200px; |
|
max-height: 600px; |
|
margin: 8px; |
|
min-width: 200px; |
|
box-shadow: 0px 0px 20px 7px #e6e7e7; |
|
} |
|
|
|
.card { |
|
background-color: #eee; |
|
display: flex; |
|
flex-direction: column; |
|
padding: 8px; |
|
font-size: 12px; |
|
} |
|
|
|
.card textarea { |
|
width: 100%; |
|
height: 'fit-content'; |
|
|
|
margin-top: 12px; |
|
resize: none; |
|
overflow: hidden; |
|
} |
|
|
|
.card img { |
|
width: 100%; |
|
margin-top: 12px; |
|
} |
|
|
|
.card .select { |
|
margin-top: 12px; |
|
} |
|
|
|
.run_div { |
|
position: fixed; |
|
bottom: 28px; |
|
display: flex; |
|
left: 144px; |
|
z-index: 100; |
|
|
|
|
|
|
|
flex-direction: column; |
|
background-color: #f7f7f7; |
|
box-shadow: 3px 3px 8px #cacaca; |
|
border-radius: 8px; |
|
} |
|
|
|
.status { |
|
|
|
color: black; |
|
display: flex; |
|
width: 120px; |
|
padding-left: 11px; |
|
font-size: 12px; |
|
border-radius: 8px; |
|
justify-content: flex-start; |
|
align-items: center; |
|
height: 48px; |
|
} |
|
|
|
.run_btn { |
|
background: black; |
|
color: white; |
|
|
|
min-width: 200px; |
|
max-width: 460px; |
|
height: 48px; |
|
border-radius: 8px; |
|
cursor: pointer; |
|
border: 3px solid; |
|
} |
|
|
|
button:hover { |
|
|
|
color: #ffffff !important; |
|
|
|
background: #232222; |
|
cursor: pointer; |
|
} |
|
|
|
.disabled { |
|
background-color: #eee !important; |
|
color: #4a4a4a !important; |
|
} |
|
|
|
.upload_btn { |
|
width: 188px; |
|
cursor: pointer; |
|
|
|
|
|
color: white; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
text-align: center; |
|
font-size: 14px; |
|
color: black; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
.show_text { |
|
font-size: 14px; |
|
user-select: text; |
|
margin: 8px; |
|
padding: 32px; |
|
min-width: 200px; |
|
background: #242424; |
|
color: white; |
|
} |
|
|
|
.link { |
|
text-decoration: none; |
|
color: gray; |
|
font-size: 12px; |
|
font-weight: 300; |
|
} |
|
|
|
label::after { |
|
content: attr(data-content); |
|
|
|
|
|
|
|
|
|
margin-left: 4px; |
|
font-size: 12px; |
|
color: #555; |
|
} |
|
|
|
|
|
.card select, |
|
.card button, |
|
.card input { |
|
height: 32px; |
|
cursor: pointer; |
|
background: #000000bf; |
|
color: white; |
|
outline: none; |
|
border: none; |
|
border-radius: 4px; |
|
margin-top: 8px; |
|
} |
|
|
|
|
|
.pickr .pcr-button { |
|
width: 56px !important; |
|
height: 56px !important; |
|
outline: 1px solid white; |
|
} |
|
|
|
|
|
|
|
.prompt_image { |
|
color: #2f2f2f; |
|
padding: 0 10px; |
|
font-size: 12px; |
|
width: 200px; |
|
} |
|
|
|
|
|
|
|
#editor_container { |
|
position: fixed; |
|
top: 0; |
|
z-index: 9; |
|
background: #eee; |
|
height: 100vh; |
|
width: 100%; |
|
display: none; |
|
} |
|
|
|
|
|
|
|
.pswp__custom-caption { |
|
background: rgb(20 27 70); |
|
font-size: 16px; |
|
color: #fff; |
|
width: calc(100% - 32px); |
|
max-width: 400px; |
|
padding: 2px 8px; |
|
border-radius: 4px; |
|
position: absolute; |
|
left: 50%; |
|
bottom: 16px; |
|
transform: translateX(-50%); |
|
} |
|
|
|
.pswp__custom-caption a { |
|
color: #fff; |
|
text-decoration: underline; |
|
} |
|
|
|
.hidden-caption-content { |
|
display: none; |
|
} |
|
|
|
|
|
|
|
::-webkit-scrollbar-track { |
|
background-color: #f1f1f1; |
|
|
|
} |
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb { |
|
background-color: #888; |
|
|
|
border-radius: 4px; |
|
|
|
} |
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background-color: #555; |
|
|
|
} |
|
|
|
|
|
|
|
::-webkit-scrollbar-corner { |
|
background-color: transparent; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
summary { |
|
user-select: none; |
|
} |
|
</style> |
|
|
|
<link href="/extensions/comfyui-mixlab-nodes/lib/photoswipe.min.css" rel="stylesheet"> |
|
<link href="/extensions/comfyui-mixlab-nodes/lib/classic.min.css" rel="stylesheet"> |
|
<script src="/extensions/comfyui-mixlab-nodes/lib/pickr.min.js"></script> |
|
|
|
<script type="module" src="/extensions/comfyui-mixlab-nodes/lib/model-viewer.min.js"></script> |
|
<link rel="stylesheet" href="/extensions/comfyui-mixlab-nodes/lib/login.css"> |
|
</head> |
|
|
|
<body> |
|
|
|
<div id="editor_container"> |
|
<iframe style="width:100%; height:100vh;" id="miniPaint" |
|
src="/extensions/comfyui-mixlab-nodes/lib/miniPaint-4.14.2/index.html" allow="camera"></iframe> |
|
</div> |
|
<div class="header"> |
|
<div id="logo" style="margin: 0 24px; |
|
margin-bottom: 24px; |
|
padding: 8px; |
|
color: #4a4a4a; |
|
border-bottom: 1px dashed #595959;">Explore your creative potential with <a class="link" |
|
href="https://github.com/shadowcz007/comfyui-mixlab-nodes" target="_blank">mixlab-nodes</a> / 尽情发挥你的创意 |
|
<br> |
|
<a class="link" href="https://www.mixcomfy.com" target="_blank">ComfyUI中文爱好者社区推荐</a> |
|
</div> |
|
|
|
<a id="login_btn" target="_blank" href="https://discord.gg/xbP2GZF6gn" style="text-decoration: none; |
|
color: black;font-size:12px"> |
|
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" |
|
class="octicon octicon-mark-github v-align-middle color-fg-default"> |
|
<path |
|
d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"> |
|
</path> |
|
</svg> HELP/帮助</a> |
|
|
|
</div> |
|
<a id="author"></a> |
|
|
|
<script type="module"> |
|
import PhotoSwipeLightbox from '/extensions/comfyui-mixlab-nodes/lib/photoswipe-lightbox.esm.min.js' |
|
|
|
import { api } from "../../../scripts/api.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function stripComments(str) { |
|
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); |
|
} |
|
|
|
function dynamicPrompts(prompt) { |
|
prompt = stripComments(prompt); |
|
while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) { |
|
const startIndex = prompt.replace("\\{", "00").indexOf("{"); |
|
const endIndex = prompt.replace("\\}", "00").indexOf("}"); |
|
|
|
const optionsString = prompt.substring(startIndex + 1, endIndex); |
|
const options = optionsString.split("|"); |
|
|
|
const randomIndex = Math.floor(Math.random() * options.length); |
|
const randomOption = options[randomIndex]; |
|
|
|
prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1); |
|
} |
|
return prompt |
|
} |
|
|
|
|
|
|
|
const base64Df = |
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
|
|
|
|
|
function get_url() { |
|
let api_host = `${window.location.hostname}:${window.location.port}` |
|
let api_base = '' |
|
let url = `${window.location.protocol}//${api_host}${api_base}` |
|
return url |
|
} |
|
|
|
async function uploadImage(blob, fileType = '.png', filename) { |
|
const body = new FormData() |
|
body.append( |
|
'image', |
|
new File([blob], (filename || new Date().getTime()) + fileType) |
|
) |
|
|
|
const url = get_url() |
|
|
|
const resp = await fetch(`${url}/upload/image`, { |
|
method: 'POST', |
|
body |
|
}) |
|
|
|
let data = await resp.json() |
|
|
|
let { name, subfolder } = data |
|
let src = `${url}/view?filename=${encodeURIComponent( |
|
name |
|
)}&type=input&subfolder=${subfolder}&rand=${Math.random()}` |
|
|
|
return { url: src, name } |
|
|
|
}; |
|
|
|
async function uploadMask(arrayBuffer, imgurl) { |
|
const body = new FormData() |
|
const filename = 'clipspace-mask-' + performance.now() + '.png' |
|
|
|
let original_url = new URL(imgurl) |
|
|
|
const original_ref = { filename: original_url.searchParams.get('filename') } |
|
|
|
let original_subfolder = original_url.searchParams.get('subfolder') |
|
if (original_subfolder) original_ref.subfolder = original_subfolder |
|
|
|
let original_type = original_url.searchParams.get('type') |
|
if (original_type) original_ref.type = original_type |
|
|
|
body.append('image', arrayBuffer, filename) |
|
body.append('original_ref', JSON.stringify(original_ref)) |
|
body.append('type', 'input') |
|
body.append('subfolder', 'clipspace') |
|
|
|
const url = get_url() |
|
|
|
const resp = await fetch(`${url}/upload/mask`, { |
|
method: 'POST', |
|
body |
|
}) |
|
|
|
|
|
let data = await resp.json() |
|
let { name, subfolder, type } = data |
|
let src = `${url}/view?filename=${encodeURIComponent( |
|
name |
|
)}&type=${type}&subfolder=${subfolder}&rand=${Math.random()}` |
|
|
|
return { url: src, name: 'clipspace/' + name } |
|
} |
|
|
|
|
|
const parseImageToBase64 = url => { |
|
return new Promise((res, rej) => { |
|
fetch(url) |
|
.then(response => response.blob()) |
|
.then(blob => { |
|
const reader = new FileReader() |
|
reader.onloadend = () => { |
|
const base64data = reader.result |
|
res(base64data) |
|
|
|
} |
|
reader.readAsDataURL(blob) |
|
}) |
|
.catch(error => { |
|
console.log('发生错误:', error) |
|
}) |
|
}) |
|
} |
|
|
|
|
|
function createBase64ImageForLoadImageToBatch(imageElement, nodeId, bs) { |
|
let im = new Image() |
|
im.src = bs; |
|
im.className = "base64" |
|
imageElement.appendChild(im); |
|
|
|
let base64s = imageElement.querySelectorAll('.base64') |
|
|
|
window._appData.data[nodeId].inputs.images.base64 = Array.from(base64s, (b) => b.src) |
|
|
|
|
|
im.addEventListener('click', e => { |
|
e.preventDefault(); |
|
im.remove(); |
|
let base64s = imageElement.querySelectorAll('.base64') |
|
|
|
window._appData.data[nodeId].inputs.images.base64 = Array.from(base64s, (b) => b.src) |
|
}) |
|
} |
|
|
|
const blobToBase64 = blob => { |
|
return new Promise((res, rej) => { |
|
const reader = new FileReader() |
|
reader.onloadend = () => { |
|
const base64data = reader.result |
|
res(base64data) |
|
|
|
} |
|
reader.readAsDataURL(blob) |
|
}) |
|
} |
|
|
|
|
|
function base64ToBlob(base64) { |
|
|
|
const base64WithoutPrefix = base64.replace(/^data:image\/\w+;base64,/, ''); |
|
|
|
|
|
const byteCharacters = atob(base64WithoutPrefix); |
|
|
|
|
|
const byteArrays = []; |
|
|
|
|
|
for (let offset = 0; offset < byteCharacters.length; offset += 1024) { |
|
const slice = byteCharacters.slice(offset, offset + 1024); |
|
|
|
const byteNumbers = new Array(slice.length); |
|
for (let i = 0; i < slice.length; i++) { |
|
byteNumbers[i] = slice.charCodeAt(i); |
|
} |
|
|
|
const byteArray = new Uint8Array(byteNumbers); |
|
byteArrays.push(byteArray); |
|
} |
|
|
|
|
|
const blob = new Blob(byteArrays, { type: 'image/png' }); |
|
|
|
return blob; |
|
} |
|
|
|
|
|
|
|
async function get_rembg_models() { |
|
try { |
|
const response = await fetch(`${get_url()}/mixlab/folder_paths`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ |
|
type: 'rembg' |
|
}) |
|
}) |
|
|
|
const data = await response.json() |
|
|
|
return data.names |
|
} catch (error) { |
|
console.error(error) |
|
} |
|
} |
|
|
|
|
|
async function run_rembg(model, base64) { |
|
try { |
|
const response = await fetch(`${get_url()}/mixlab/rembg`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ |
|
model, |
|
base64 |
|
}) |
|
}) |
|
|
|
const data = await response.json() |
|
|
|
return data.data |
|
} catch (error) { |
|
console.error(error) |
|
} |
|
} |
|
|
|
function convertImageToBlackBasedOnAlpha(image) { |
|
const canvas = document.createElement('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
canvas.width = image.width; |
|
canvas.height = image.height; |
|
ctx.drawImage(image, 0, 0); |
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
const pixels = imageData.data; |
|
|
|
|
|
for (let i = 0; i < pixels.length; i += 4) { |
|
const alpha = pixels[i + 3]; |
|
if (alpha !== 0) { |
|
|
|
pixels[i] = 0; |
|
pixels[i + 1] = 0; |
|
pixels[i + 2] = 0; |
|
} |
|
} |
|
|
|
|
|
ctx.putImageData(imageData, 0, 0); |
|
|
|
|
|
const base64ImageData = canvas.toDataURL('image/png'); |
|
|
|
return base64ImageData; |
|
} |
|
|
|
|
|
|
|
|
|
async function editImage(image, data) { |
|
|
|
let isMask = data.options.hasMask; |
|
|
|
|
|
document.body.querySelector('.app').style.display = 'none' |
|
document.body.querySelector('#author').style.display = 'none' |
|
|
|
let editor = document.querySelector('#editor_container') |
|
editor.style.display = 'block'; |
|
const iframe = editor.querySelector('iframe'); |
|
|
|
const sleep = (t = 1000) => { |
|
return new Promise((res, rej) => { |
|
setTimeout(() => { |
|
res(true) |
|
}, t) |
|
}) |
|
} |
|
|
|
|
|
const removeAllLayer = () => { |
|
let Layers = iframe.contentWindow.Layers; |
|
|
|
Layers.reset_layers() |
|
Layers.refresh_gui() |
|
} |
|
|
|
|
|
const resetLayer = () => { |
|
let Layers = iframe.contentWindow.Layers; |
|
for (const layer of Layers.get_layers()) { |
|
layer.visible = true; |
|
} |
|
Layers.refresh_gui() |
|
} |
|
|
|
const getImageBase64FromLayer = () => { |
|
let Layers = iframe.contentWindow.Layers; |
|
let tempCanvas = document.createElement("canvas"); |
|
let tempCtx = tempCanvas.getContext("2d"); |
|
let dim = Layers.get_dimensions(); |
|
tempCanvas.width = dim.width; |
|
tempCanvas.height = dim.height; |
|
for (const layer of Layers.get_layers()) { |
|
if (layer.name === 'Image_' + data.id) { |
|
layer.visible = true; |
|
} else { |
|
layer.visible = false; |
|
} |
|
} |
|
Layers.refresh_gui() |
|
|
|
Layers.convert_layers_to_canvas(tempCtx); |
|
return tempCanvas.toDataURL() |
|
} |
|
|
|
|
|
const addMask = (id, name, image) => { |
|
let Layers = iframe.contentWindow.Layers; |
|
var new_mask_layer = { |
|
id, |
|
name, |
|
type: 'brush', |
|
data: [], |
|
render_function: ['brush', 'render'], |
|
width: image.naturalWidth || image.width, |
|
height: image.naturalHeight || image.height, |
|
|
|
}; |
|
Layers.insert(new_mask_layer); |
|
} |
|
|
|
|
|
const addImage = (id, name, image) => { |
|
let Layers = iframe.contentWindow.Layers; |
|
var new_mask_layer = { |
|
id, |
|
name, |
|
type: 'image', |
|
data: image, |
|
width: image.naturalWidth || image.width, |
|
height: image.naturalHeight || image.height, |
|
width_original: image.naturalWidth || image.width, |
|
height_original: image.naturalHeight || image.height, |
|
}; |
|
Layers.insert(new_mask_layer); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let autoMaskSelect = iframe.contentDocument.getElementById('automask_image_mixlab'); |
|
const select = iframe.contentDocument.getElementById('automask_models_mixlab'); |
|
if (select.children.length === 0) { |
|
let rembgModels = await get_rembg_models() |
|
|
|
for (const model of rembgModels) { |
|
const option = document.createElement('option'); |
|
option.value = model; |
|
option.textContent = model; |
|
select.appendChild(option); |
|
} |
|
} |
|
|
|
let autoMaskBtn = iframe.contentDocument.getElementById('automask_image_mixlab'); |
|
if (!autoMaskBtn.getAttribute('init')) autoMaskBtn.addEventListener('click', async e => { |
|
|
|
let base64 = getImageBase64FromLayer() |
|
resetLayer() |
|
|
|
let res = await run_rembg(select.value, base64) |
|
const match = res.match(/^data:image\/(\w+);base64,/); |
|
if (!match) { |
|
res = 'data:image/png;base64,' + res |
|
} |
|
|
|
let image = await createImage(res) |
|
|
|
let mb = convertImageToBlackBasedOnAlpha(image) |
|
let mask = await createImage(mb) |
|
let id = Layers.auto_increment; |
|
addImage(id, 'Mask_' + data.id + id, mask) |
|
}) |
|
|
|
autoMaskBtn.setAttribute('init', 1) |
|
let cancelImageBtn = iframe.contentDocument.getElementById('cancel_image_mixlab'); |
|
if (!cancelImageBtn.getAttribute('init')) cancelImageBtn.addEventListener('click', e => { |
|
editor.style.display = 'none'; |
|
document.body.querySelector('.app').style.display = 'flex' |
|
document.body.querySelector('#author').style.display = 'block' |
|
}) |
|
|
|
cancelImageBtn.setAttribute('init', 1) |
|
|
|
let saveImageBtn = iframe.contentDocument.getElementById('save_image_mixlab'); |
|
saveImageBtn.style = `width: 98px; |
|
height: 36px; |
|
margin: 0 12px; |
|
background-color: var(--background-color-active); |
|
color: var(--text-color-active);` |
|
if (!saveImageBtn.getAttribute('init')) saveImageBtn.addEventListener('click', async e => { |
|
|
|
e.preventDefault(); |
|
|
|
|
|
if (isMask) { |
|
|
|
let Layers = iframe.contentWindow.Layers; |
|
let tempCanvas = document.createElement("canvas"); |
|
let tempCtx = tempCanvas.getContext("2d"); |
|
let dim = Layers.get_dimensions(); |
|
tempCanvas.width = dim.width; |
|
tempCanvas.height = dim.height; |
|
|
|
|
|
|
|
for (const layer of Layers.get_layers()) { |
|
if (layer.name !== 'Image_' + data.id) { |
|
layer.visible = true; |
|
} else { |
|
layer.visible = false; |
|
} |
|
} |
|
Layers.refresh_gui() |
|
|
|
Layers.convert_layers_to_canvas(tempCtx); |
|
|
|
|
|
resetLayer() |
|
|
|
|
|
const imageData = tempCtx.getImageData(0, 0, dim.width, dim.height); |
|
const imageDataData = imageData.data; |
|
|
|
|
|
for (let i = 0; i < imageDataData.length; i += 4) { |
|
|
|
|
|
|
|
imageDataData[i + 3] = 255 - imageDataData[i + 3]; |
|
} |
|
|
|
|
|
tempCtx.putImageData(imageData, 0, 0); |
|
|
|
|
|
let base64 = tempCanvas.toDataURL(); |
|
|
|
editor.style.display = 'none'; |
|
|
|
let fileBlob = base64ToBlob(base64) |
|
|
|
let hashId = await calculateImageHash(fileBlob) |
|
|
|
if (hashId == window._appData.data[data.id].hashId) { |
|
document.body.querySelector('.app').style.display = 'flex' |
|
document.body.querySelector('#author').style.display = 'block' |
|
return; |
|
} |
|
|
|
|
|
const { url: imgurl } = await uploadImage(base64ToBlob(data.options.defaultImage)) |
|
|
|
let { url, name } = await uploadMask(fileBlob, imgurl); |
|
|
|
|
|
window._appData.data[data.id].inputs.image = name; |
|
window._appData.data[data.id].hashId = hashId; |
|
|
|
|
|
|
|
const canvas = document.createElement("canvas"); |
|
canvas.width = dim.width; |
|
canvas.height = dim.height; |
|
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
const defaultImage = await createImage(data.options.defaultImage) |
|
ctx.drawImage(defaultImage, 0, 0, dim.width, dim.height); |
|
|
|
|
|
|
|
const base64ImageObj = await createImage(base64) |
|
ctx.globalCompositeOperation = 'destination-in'; |
|
ctx.drawImage(base64ImageObj, 0, 0, dim.width, dim.height); |
|
image.src = canvas.toDataURL(); |
|
|
|
} |
|
|
|
document.body.querySelector('.app').style.display = 'flex' |
|
document.body.querySelector('#author').style.display = 'block' |
|
}) |
|
|
|
saveImageBtn.setAttribute('init', 1) |
|
|
|
var Layers = iframe.contentWindow.Layers; |
|
|
|
|
|
|
|
let layers1 = Layers.get_layers(); |
|
|
|
|
|
console.log(layers1.filter(l => l.name == 'Image_' + data.id)[0]?.link?.currentSrc !== data.options.defaultImage) |
|
if (layers1.filter(l => l.name == 'Image_' + data.id)[0] |
|
&& layers1.filter(l => l.name == 'Image_' + data.id)[0].link.src !== data.options.defaultImage) { |
|
|
|
removeAllLayer(); |
|
layers1 = []; |
|
await sleep() |
|
} |
|
|
|
let im = await createImage(data.options.defaultImage) |
|
|
|
if (!layers1.filter(l => l.name == 'Image_' + data.id)[0]) { |
|
addImage(0, 'Image_' + data.id, im) |
|
} |
|
|
|
if (isMask) { |
|
if (!layers1.filter(l => l.name == 'Mask_' + data.id)[0]) { |
|
addMask(1, 'Mask_' + data.id, im) |
|
} |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function getQueue(clientId) { |
|
try { |
|
|
|
const res = await fetch(`${get_url()}/queue`); |
|
const data = await res.json(); |
|
return { |
|
|
|
Running: Array.from(data.queue_running, prompt => { |
|
if (prompt[3].client_id === clientId) { |
|
let prompt_id = prompt[1]; |
|
return { |
|
prompt_id, |
|
remove: () => interrupt(), |
|
} |
|
} |
|
}), |
|
Pending: data.queue_pending.map((prompt) => ({ prompt })), |
|
}; |
|
} catch (error) { |
|
console.error(error); |
|
return { Running: [], Pending: [] }; |
|
} |
|
} |
|
|
|
|
|
async function interrupt() { |
|
try { |
|
await fetch(`${get_url()}/interrupt`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: undefined |
|
}); |
|
|
|
} catch (error) { |
|
console.error(error) |
|
} |
|
return true |
|
} |
|
|
|
|
|
function randomSeed(seed, data) { |
|
let max_seed = 4294967295 |
|
|
|
for (const id in data) { |
|
if (data[id].inputs.seed != undefined |
|
&& !Array.isArray(data[id].inputs.seed) |
|
&& ['increment', 'decrement', 'randomize'].includes(seed[id])) { |
|
data[id].inputs.seed = Math.round(Math.random() * max_seed) |
|
|
|
} |
|
if (data[id].inputs.noise_seed != undefined |
|
&& !Array.isArray(data[id].inputs.noise_seed) |
|
&& ['increment', 'decrement', 'randomize'].includes(seed[id])) { |
|
data[id].inputs.noise_seed = Math.round(Math.random() * max_seed) |
|
} |
|
|
|
if (data[id].class_type == "Seed_" && ['increment', 'decrement', 'randomize'].includes(seed[id])) { |
|
data[id].inputs.seed = Math.round(Math.random() * max_seed) |
|
} |
|
console.log('new Seed', data[id]) |
|
} |
|
return data |
|
} |
|
|
|
function updateSeed(id, val) { |
|
|
|
if (window._appData.data[id].inputs.seed && !Array.isArray(window._appData.data[id].inputs.seed)) window._appData.data[id].inputs.seed = Math.round(val); |
|
if (window._appData.data[id].inputs.noise_seed && !Array.isArray(window._appData.data[id].inputs.noise_seed)) window._appData.data[id].inputs.noise_seed = Math.round(val); |
|
} |
|
|
|
|
|
function queuePrompt(appInfo, promptWorkflow, seed, client_id) { |
|
|
|
for (const id in promptWorkflow) { |
|
if (promptWorkflow[id].class_type == 'AppInfo') { |
|
promptWorkflow[id].inputs.category = promptWorkflow[id].inputs.category || "" |
|
} |
|
} |
|
|
|
promptWorkflow = randomSeed(seed, promptWorkflow); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let url = get_url() |
|
const data = JSON.stringify({ prompt: promptWorkflow, client_id }); |
|
fetch(`${url}/prompt`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: data, |
|
}) |
|
.then(async response => { |
|
|
|
|
|
let res = await response.json(); |
|
window.prompt_ids[res.prompt_id] = { |
|
appInfo, |
|
prompt_id: res.prompt_id |
|
} |
|
}) |
|
.catch(error => { |
|
|
|
}); |
|
} |
|
|
|
|
|
function success(isSuccess, btn, text) { |
|
isSuccess ? btn.innerText = 'success' : text; |
|
setTimeout(() => { |
|
btn.innerText = text; |
|
}, 5000) |
|
} |
|
|
|
async function get_my_app(category = "", filename = null) { |
|
let url = get_url() |
|
const res = await fetch(`${url}/mixlab/workflow`, { |
|
method: 'POST', |
|
mode: 'cors', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ |
|
task: 'my_app', |
|
filename, |
|
category |
|
}) |
|
}) |
|
let result = await res.json(); |
|
let data = []; |
|
try { |
|
for (const res of result.data) { |
|
let { output, app } = res.data; |
|
if (app.filename) data.push({ |
|
...app, |
|
data: output, |
|
date: res.date |
|
}) |
|
} |
|
} catch (error) { |
|
} |
|
|
|
|
|
let appSelected = localStorage.getItem('app_selected') |
|
if (appSelected) { |
|
async function moveElementToFront(array, targetId) { |
|
|
|
for (let i = 0; i < array.length; i++) { |
|
if (array[i].id === targetId) { |
|
|
|
if (i !== 0) { |
|
const targetElement = array.splice(i, 1)[0]; |
|
|
|
let nt = (await get_my_app(targetElement.category, targetElement.filename))[0]; |
|
|
|
array.unshift(nt); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
return array; |
|
} |
|
data = await moveElementToFront(data, appSelected) |
|
} |
|
|
|
return data |
|
} |
|
|
|
|
|
async function createOutputs(outputData, link) { |
|
const url = new URL(window.location.href); |
|
const params = new URLSearchParams(url.search); |
|
const innerApp = params.get("innerApp"); |
|
|
|
const container = document.createElement('div'); |
|
container.className = "output"; |
|
|
|
const action = document.createElement('div'); |
|
container.appendChild(action) |
|
|
|
const copyHTML = document.createElement('button'); |
|
copyHTML.innerText = 'copy as html' |
|
if (!innerApp) action.appendChild(copyHTML) |
|
|
|
const copyImage = document.createElement('button'); |
|
copyImage.innerText = 'copy image' |
|
if (!innerApp) action.appendChild(copyImage) |
|
copyImage.style.marginLeft = '18px'; |
|
|
|
let isURL = false; |
|
try { |
|
new URL(link) |
|
isURL = true; |
|
} catch (error) { |
|
isURL = false; |
|
} |
|
|
|
let isAElement = undefined; |
|
try { |
|
let div = document.createElement('div'); |
|
div.innerHTML = link; |
|
let a = div.querySelector('a'); |
|
if (a.href) { |
|
new URL(a.href); |
|
isAElement = div.innerHTML; |
|
} |
|
} catch (error) { |
|
|
|
} |
|
|
|
if (isURL || isAElement) { |
|
const linkBtn = document.createElement('button'); |
|
|
|
if (isURL) { |
|
|
|
linkBtn.innerText = 'go to' |
|
} else if (isAElement) { |
|
linkBtn.innerHTML = isAElement |
|
} |
|
|
|
action.appendChild(linkBtn) |
|
linkBtn.style.marginLeft = '18px'; |
|
linkBtn.addEventListener('click', e => { |
|
|
|
if (isURL) { |
|
e.preventDefault(); |
|
window.open(link); |
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
const output_card = document.createElement("div"); |
|
output_card.className = 'output_card' |
|
container.appendChild(output_card) |
|
|
|
if (window._appData.share_prefix) { |
|
const copyText = document.createElement('button'); |
|
copyText.innerText = 'copy text for share' |
|
action.appendChild(copyText) |
|
copyText.style.marginLeft = '18px'; |
|
copyText.addEventListener('click', e => { |
|
e.preventDefault(); |
|
copyTextToClipboard((window._appData.share_prefix || '') + " " + output_card.outerHTML, (r) => success(r, copyText, 'copy text for share')) |
|
}) |
|
} |
|
|
|
copyImage.addEventListener('click', e => { |
|
e.preventDefault(); |
|
|
|
copyImagesToClipboard(output_card.outerHTML, (r) => success(r, copyImage, 'copy image')) |
|
|
|
}) |
|
copyHTML.addEventListener('click', e => { |
|
e.preventDefault(); |
|
copyHtmlWithImagesToClipboard((window._appData.share_prefix || '') + " " + output_card.outerHTML, (r) => success(r, copyHTML, 'copy as html')) |
|
|
|
}) |
|
|
|
|
|
let isShowImageFn = false; |
|
|
|
for (const node of outputData) { |
|
|
|
if (node.class_type == "ShowTextForGPT") { |
|
let div = document.createElement('div'); |
|
div.className = "show_text" |
|
div.id = `output_${node.id}`; |
|
div.innerText = Array.isArray(node.inputs.text) ? node.inputs.text[0] : node.inputs.text |
|
output_card.appendChild(div); |
|
}; |
|
|
|
if (node.class_type == "ClipInterrogator") { |
|
let div = document.createElement('div'); |
|
div.className = "show_text"; |
|
div.id = `output_${node.id}`; |
|
div.innerText = '#ClipInterrogator: …… ' |
|
output_card.appendChild(div); |
|
}; |
|
|
|
if (["SaveImage", |
|
"PreviewImage", |
|
"PromptImage", |
|
"Image Save", |
|
"SaveImageAndMetadata_", |
|
"TransparentImage"].includes(node.class_type)) { |
|
|
|
const url = node.options?.defaultImage || window._appData?.icon || base64Df; |
|
let a = document.createElement('div'); |
|
a.id = `output_${node.id}` |
|
|
|
let img = await createImage(url) |
|
|
|
a.appendChild(img) |
|
|
|
|
|
|
|
|
|
|
|
|
|
output_card.appendChild(a); |
|
isShowImageFn = true; |
|
|
|
} |
|
|
|
|
|
if (["SaveTripoSRMesh"].includes(node.class_type)) { |
|
let a = document.createElement('a'); |
|
a.id = `output_${node.id}` |
|
a.setAttribute('data-pswp-width', "200"); |
|
a.setAttribute('data-pswp-height', "200"); |
|
a.setAttribute('target', "_blank"); |
|
a.setAttribute('href', base64Df); |
|
|
|
let img = new Image(); |
|
|
|
img.src = base64Df; |
|
a.appendChild(img) |
|
output_card.appendChild(a); |
|
} |
|
|
|
|
|
if (["VHS_VideoCombine", "VideoCombine_Adv", "CombineAudioVideo"].includes(node.class_type)) { |
|
|
|
let a = document.createElement('a'); |
|
a.id = `output_${node.id}` |
|
a.setAttribute('data-pswp-width', "200"); |
|
a.setAttribute('data-pswp-height', "200"); |
|
a.setAttribute('target', "_blank"); |
|
a.setAttribute('href', base64Df); |
|
|
|
|
|
|
|
|
|
let video = document.createElement('video'), img = new Image(); |
|
video.style.display = 'none' |
|
video.controls = 'true' |
|
video.autoplay = 'true' |
|
video.loop = 'true' |
|
|
|
|
|
img.src = base64Df; |
|
|
|
a.appendChild(video); |
|
a.appendChild(img); |
|
output_card.appendChild(a); |
|
} |
|
|
|
} |
|
|
|
if (isShowImageFn === false) { |
|
copyImage.remove(); |
|
copyHTML.remove(); |
|
} |
|
|
|
return container |
|
} |
|
|
|
|
|
function generateRainbowVideo() { |
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
canvas.width = 640; |
|
canvas.height = 480; |
|
const context = canvas.getContext('2d'); |
|
|
|
|
|
context.fillStyle = 'red'; |
|
context.fillRect(0, 0, canvas.width / 2, canvas.height); |
|
context.fillStyle = 'orange'; |
|
context.fillRect(canvas.width / 2, 0, canvas.width / 2, canvas.height); |
|
|
|
|
|
context.fillStyle = 'yellow'; |
|
context.fillRect(0, 0, canvas.width / 2, canvas.height); |
|
context.fillStyle = 'green'; |
|
context.fillRect(canvas.width / 2, 0, canvas.width / 2, canvas.height); |
|
|
|
const stream = canvas.captureStream(); |
|
|
|
return new Promise((res, rej) => { |
|
|
|
const mediaRecorder = new MediaRecorder(stream); |
|
const chunks = []; |
|
mediaRecorder.ondataavailable = function (event) { |
|
chunks.push(event.data); |
|
}; |
|
mediaRecorder.onstop = function () { |
|
const blob = new Blob(chunks, { type: 'video/mp4' }); |
|
const url = URL.createObjectURL(blob); |
|
res(url) |
|
}; |
|
mediaRecorder.start(); |
|
setTimeout(function () { |
|
mediaRecorder.stop(); |
|
}, 1000); |
|
}) |
|
|
|
} |
|
|
|
|
|
async function calculateImageHash(blob) { |
|
const buffer = await blob.arrayBuffer(); |
|
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); |
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); |
|
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); |
|
return hashHex; |
|
} |
|
|
|
async function handleClipboardImage(imageElement, data) { |
|
|
|
try { |
|
const clipboardItems = await navigator.clipboard.read(); |
|
for (const clipboardItem of clipboardItems) { |
|
for (const type of clipboardItem.types) { |
|
|
|
if (type.startsWith('image/')) { |
|
const fileBlob = await clipboardItem.getType(type); |
|
|
|
let hashId = await calculateImageHash(fileBlob) |
|
|
|
if (hashId == window._appData.data[data.id].hashId) return |
|
|
|
let base64 = await blobToBase64(fileBlob) |
|
|
|
if (data.class_type === 'LoadImagesToBatch') { |
|
createBase64ImageForLoadImageToBatch(imageElement, data.id, base64) |
|
} else { |
|
let { url, name } = await uploadImage(fileBlob); |
|
|
|
imageElement.src = url; |
|
window._appData.data[data.id].inputs.image = name; |
|
window._appData.data[data.id].hashId = hashId; |
|
console.log("上传的文件:", url, data.id, name); |
|
|
|
|
|
window._appData.input = Array.from(window._appData.input, inp => { |
|
if (inp.id === data.id) { |
|
inp.options.defaultImage = base64; |
|
} |
|
return inp |
|
}) |
|
} |
|
} |
|
} |
|
} |
|
} catch (error) { |
|
console.error('无法读取剪贴板中的图片:', error); |
|
} |
|
} |
|
|
|
|
|
function copyHtmlWithImagesToClipboard(data, cb) { |
|
|
|
const tempDiv = document.createElement('div'); |
|
|
|
|
|
tempDiv.innerHTML = data; |
|
|
|
|
|
const images = tempDiv.getElementsByTagName('img'); |
|
|
|
|
|
for (let i = 0; i < images.length; i++) { |
|
const image = images[i]; |
|
const canvas = document.createElement('canvas'); |
|
const context = canvas.getContext('2d'); |
|
|
|
|
|
canvas.width = image.width; |
|
canvas.height = image.height; |
|
|
|
|
|
context.drawImage(image, 0, 0); |
|
|
|
|
|
const imageData = canvas.toDataURL(); |
|
|
|
|
|
image.src = imageData; |
|
} |
|
|
|
|
|
let richText = tempDiv.innerHTML; |
|
|
|
|
|
const blob = new Blob([richText], { type: 'text/html' }); |
|
|
|
|
|
const clipboardItem = new ClipboardItem({ 'text/html': blob }); |
|
|
|
|
|
navigator.clipboard.write([clipboardItem]) |
|
.then(() => { |
|
console.log('富文本已成功复制到剪贴板'); |
|
tempDiv.remove() |
|
if (cb) cb(true) |
|
}) |
|
.catch((error) => { |
|
console.error('复制到剪贴板失败:', error); |
|
tempDiv.remove() |
|
if (cb) cb(false) |
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
function copyImagesToClipboard(html, cb) { |
|
const tempDiv = document.createElement('div'); |
|
tempDiv.innerHTML = html; |
|
const images = tempDiv.querySelectorAll('img'); |
|
const promises = Array.from(images).map((image) => { |
|
return new Promise((resolve) => { |
|
const img = new Image(); |
|
img.src = image.src; |
|
img.onload = () => { |
|
const canvas = document.createElement('canvas'); |
|
const context = canvas.getContext('2d'); |
|
canvas.width = img.width; |
|
canvas.height = img.height; |
|
context.drawImage(img, 0, 0); |
|
canvas.toBlob((blob) => { |
|
const clipboardItem = new ClipboardItem({ 'image/png': blob }); |
|
navigator.clipboard.write([clipboardItem]) |
|
.then(() => { |
|
resolve(); |
|
tempDiv.remove() |
|
if (cb) cb(true) |
|
}) |
|
.catch((error) => { |
|
reject(error); |
|
tempDiv.remove() |
|
if (cb) cb(false) |
|
}); |
|
}); |
|
}; |
|
}); |
|
}); |
|
Promise.all([...promises]) |
|
.then(() => { |
|
console.log('所有图片已成功复制到剪贴板'); |
|
if (cb) cb(true) |
|
tempDiv.remove() |
|
}) |
|
.catch((error) => { |
|
console.error('复制到剪贴板失败:', error); |
|
if (cb) cb(false) |
|
tempDiv.remove() |
|
}); |
|
} |
|
|
|
function copyTextToClipboard(html, cb) { |
|
const tempDiv = document.createElement('div'); |
|
tempDiv.innerHTML = html; |
|
|
|
const text = tempDiv.innerText; |
|
const textData = new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }) }); |
|
|
|
navigator.clipboard.write([textData]) |
|
.then(() => { |
|
console.log('所有文本已成功复制到剪贴板', text); |
|
if (cb) cb(true) |
|
tempDiv.remove() |
|
}) |
|
.catch((error) => { |
|
console.error('复制到剪贴板失败:', error); |
|
if (cb) cb(false) |
|
tempDiv.remove() |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadAndConvertAudio(file) { |
|
if (!file) { |
|
alert('Please select a WAV file.') |
|
return |
|
} |
|
|
|
if (file.type !== 'audio/wav') { |
|
alert('Only WAV files are supported.') |
|
return |
|
} |
|
|
|
try { |
|
const base64Audio = await readFileAsDataURL(file) |
|
return base64Audio |
|
} catch (error) { |
|
console.error('Error reading file:', error) |
|
alert('Error reading file.') |
|
} |
|
} |
|
|
|
function readFileAsDataURL(file) { |
|
return new Promise((resolve, reject) => { |
|
const reader = new FileReader() |
|
|
|
reader.onload = function (event) { |
|
resolve(event.target.result) |
|
} |
|
|
|
reader.onerror = function (error) { |
|
reject(error) |
|
} |
|
|
|
reader.readAsDataURL(file) |
|
}) |
|
} |
|
|
|
function createInputs(inputData) { |
|
|
|
const container = document.createElement("div"); |
|
container.className = 'input_card' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inputData = inputData.filter(inp => inp); |
|
|
|
inputData.forEach(async data => { |
|
console.log('inputData', data); |
|
|
|
|
|
|
|
if (["LoadImage", |
|
"VHS_LoadVideo", |
|
"ImagesPrompt_", |
|
"LoadImagesToBatch"].includes(data.class_type)) { |
|
|
|
let isVideoUpload = data.class_type === "VHS_LoadVideo"; |
|
|
|
let isBase64Upload = data.class_type === "LoadImagesToBatch"; |
|
|
|
|
|
const uploadContainer = document.createElement("div"); |
|
uploadContainer.className = 'card'; |
|
|
|
|
|
const nameLabel = document.createElement("label"); |
|
nameLabel.textContent = data.title || (isVideoUpload ? "LoadVideo: " : "LoadImage: "); |
|
nameLabel.style.marginBottom = '12px' |
|
uploadContainer.appendChild(nameLabel); |
|
|
|
let actionDiv = document.createElement('div'); |
|
actionDiv.style = `padding: 0 8px;` |
|
|
|
|
|
const uploadImageInput = document.createElement("button"); |
|
uploadImageInput.style = `width: 88px;`; |
|
uploadImageInput.innerText = 'upload' |
|
const uploadImageInputHide = document.createElement('input'); |
|
uploadImageInputHide.type = "file"; |
|
uploadImageInputHide.style.display = "none" |
|
actionDiv.appendChild(uploadImageInput); |
|
actionDiv.appendChild(uploadImageInputHide); |
|
|
|
const btnFromClipboard = document.createElement("button"); |
|
btnFromClipboard.style = `width: 156px; margin-left: 18px;` |
|
btnFromClipboard.innerText = 'paste from clipboard' |
|
if (!isVideoUpload) actionDiv.appendChild(btnFromClipboard); |
|
|
|
const btnForImageEdit = document.createElement("button"); |
|
btnForImageEdit.style = ` width: 32px; background: none;margin-left: 18px;` |
|
btnForImageEdit.innerHTML = '<?xml version="1.0" ?><svg version="1.1" style="width: 24px;" viewBox="0 0 50 50" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1_1_"><path d="M18.293,31.707h6.414l24-24l-6.414-6.414l-24,24V31.707z M45.879,7.707l-3.586,3.586l-3.586-3.586l3.586-3.586 L45.879,7.707z M20.293,26.121l17-17l3.586,3.586l-17,17h-3.586V26.121z"/><polygon points="43.293,19.707 41.293,19.707 41.293,46.707 3.293,46.707 3.293,8.707 31.293,8.707 31.293,6.707 1.293,6.707 1.293,48.707 43.293,48.707 "/></g></svg>' |
|
if ((!isVideoUpload && !isBase64Upload) && data.class_type !== 'ImagesPrompt_') actionDiv.appendChild(btnForImageEdit); |
|
|
|
|
|
uploadContainer.appendChild(actionDiv) |
|
|
|
|
|
let imageElement = document.createElement("img"); |
|
if (isVideoUpload) { |
|
|
|
imageElement = document.createElement('video'); |
|
imageElement.setAttribute('controls', true) |
|
let [subfolder, name] = data.inputs.video.split('/'); |
|
|
|
if (!name) { |
|
subfolder = ""; |
|
name = data.inputs.video; |
|
} |
|
let url = `${get_url()}/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}&rand=${Math.random()}` |
|
imageElement.src = url; |
|
|
|
|
|
|
|
} else if (data.class_type === 'LoadImage') { |
|
|
|
let [subfolder, name] = data.inputs.image.split('/'); |
|
if (!name) { |
|
subfolder = ""; |
|
name = data.inputs.image; |
|
} |
|
|
|
let url = `${get_url()}/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}&rand=${Math.random()}` |
|
|
|
imageElement.src = data.options?.defaultImage || url; |
|
imageElement.setAttribute('onerror', `this.src='${base64Df}'`) |
|
|
|
} else if (data.class_type === 'ImagesPrompt_') { |
|
|
|
const [imgDiv, mainImage] = createSelectForImages( |
|
data.title, |
|
data.options.images, |
|
data.inputs.imageIndex, |
|
(base64, text) => { |
|
window._appData.data[data.id].inputs.image_base64 = base64; |
|
window._appData.data[data.id].inputs.text = text; |
|
}) |
|
uploadContainer.appendChild(imgDiv); |
|
window._appData.data[data.id].inputs.image_base64 = mainImage.querySelector('.images_prompt_main').src; |
|
} else if (data.class_type === 'LoadImagesToBatch') { |
|
|
|
let base64 = data.inputs.images.base64 |
|
|
|
imageElement = document.createElement('div'); |
|
imageElement.className = "images" |
|
for (const bs of base64) { |
|
createBase64ImageForLoadImageToBatch(imageElement, data.id, bs) |
|
} |
|
|
|
} |
|
|
|
|
|
imageElement.style.maxWidth = '200px'; |
|
|
|
if (!isVideoUpload) btnFromClipboard.addEventListener('click', (event) => handleClipboardImage(imageElement, data)); |
|
|
|
|
|
if (!isVideoUpload && !isBase64Upload && data.options.hasMask) btnForImageEdit.addEventListener('click', e => editImage(imageElement, data)) |
|
|
|
|
|
uploadImageInput.addEventListener('click', (event) => { |
|
uploadImageInputHide.click() |
|
}) |
|
uploadImageInputHide.addEventListener('change', (event) => { |
|
|
|
|
|
const file = event.target.files[0]; |
|
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
|
reader.onloadend = async function () { |
|
|
|
const fileBlob = new Blob([reader.result], { type: file.type }); |
|
|
|
let hashId = await calculateImageHash(fileBlob) |
|
|
|
if (hashId == window._appData.data[data.id].hashId) return |
|
|
|
if (data.class_type === 'LoadImagesToBatch') { |
|
|
|
let base64 = await blobToBase64(fileBlob) |
|
createBase64ImageForLoadImageToBatch(imageElement, data.id, base64) |
|
} else { |
|
|
|
let { url, name } = await uploadImage(fileBlob, '.' + file.type.split('/')[1]) |
|
|
|
let base64 = await parseImageToBase64(url); |
|
|
|
if (data.class_type === 'ImagesPrompt_') { |
|
uploadContainer.querySelector('.images_prompt_main').src = base64 |
|
window._appData.data[data.id].inputs.image_base64 = base64; |
|
} else { |
|
if (isVideoUpload) { |
|
imageElement.srcObject = null; |
|
} |
|
|
|
imageElement.src = url; |
|
|
|
if (isVideoUpload) { |
|
window._appData.data[data.id].inputs.video = name; |
|
} else { |
|
|
|
window._appData.input = Array.from(window._appData.input, inp => { |
|
if (inp.id === data.id) { |
|
inp.options.defaultImage = base64; |
|
} |
|
return inp |
|
}) |
|
|
|
window._appData.data[data.id].inputs.image = name; |
|
} |
|
|
|
} |
|
|
|
|
|
window._appData.data[data.id].hashId = hashId; |
|
|
|
console.log("上传的文件:", url, data.id, name); |
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
reader.readAsArrayBuffer(file); |
|
|
|
|
|
}) |
|
|
|
|
|
if (data.class_type !== 'ImagesPrompt_') uploadContainer.appendChild(imageElement); |
|
|
|
|
|
container.appendChild(uploadContainer); |
|
} |
|
|
|
|
|
if (["PromptSlide"].includes(data.class_type)) { |
|
|
|
let options = data.options || { |
|
min: -3, |
|
max: 3, |
|
}; |
|
|
|
const label = data.title; |
|
|
|
if (options.keywords && !options.keywords?.includes(data.inputs.prompt_keyword)) { |
|
options.keywords = [data.inputs.prompt_keyword, ...options.keywords] |
|
}; |
|
|
|
|
|
let silde = createNumSlide(label, |
|
data.inputs.weight, |
|
(v) => { |
|
window._appData.data[data.id].inputs.weight = parseFloat(v); |
|
}, |
|
options.min, |
|
options.max, |
|
'float', |
|
options.keywords, |
|
data.id |
|
) |
|
container.appendChild(silde); |
|
} |
|
|
|
|
|
|
|
if (['FloatSlider', 'IntNumber'].includes(data.class_type)) { |
|
|
|
|
|
let options = data.options || { |
|
min: 0, |
|
max: data.class_type === 'IntNumber' ? 6000 : 1, |
|
}; |
|
let silde = createNumSlide(data.title, |
|
data.inputs.number, |
|
(v) => { |
|
|
|
window._appData.data[data.id].inputs.number = data.class_type === 'IntNumber' ? parseInt(v) : parseFloat(v); |
|
}, |
|
options.min, |
|
options.max, |
|
data.class_type === 'IntNumber' ? 'int' : 'float', |
|
null, |
|
data.id |
|
) |
|
container.appendChild(silde); |
|
} |
|
|
|
|
|
if (["TextInput_", "CLIPTextEncode", "PromptSimplification", "ChinesePrompt_Mix"].includes(data.class_type)) { |
|
|
|
const uploadContainer = document.createElement("div"); |
|
uploadContainer.className = 'card'; |
|
|
|
|
|
const nameLabel = document.createElement("label"); |
|
nameLabel.textContent = data.title || "CLIPTextEncode: "; |
|
uploadContainer.appendChild(nameLabel); |
|
|
|
|
|
const textInput = document.createElement("textarea"); |
|
|
|
if (data.class_type == "PromptSimplification") { |
|
textInput.value = data.inputs.prompt; |
|
} else { |
|
textInput.value = data.inputs.text; |
|
}; |
|
|
|
|
|
let json = localStorage.getItem(`t_${data.id}`) |
|
try { |
|
|
|
const { value, height } = JSON.parse(json); |
|
textInput.value = value; |
|
textInput.style.height = height; |
|
|
|
if (data.class_type == "PromptSimplification") { |
|
window._appData.data[data.id].inputs.prompt = textInput.value; |
|
} else { |
|
window._appData.data[data.id].inputs.text = textInput.value; |
|
} |
|
|
|
} catch (error) { |
|
|
|
} |
|
|
|
uploadContainer.appendChild(textInput); |
|
|
|
|
|
|
|
|
|
const dynamicPromptsBtn = document.createElement('button'); |
|
dynamicPromptsBtn.className = "dynamic_prompt" |
|
dynamicPromptsBtn.innerText = 'dynamic'; |
|
dynamicPromptsBtn.style.width = '88px' |
|
uploadContainer.appendChild(dynamicPromptsBtn); |
|
dynamicPromptsBtn.addEventListener('click', e => { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
let prompt = dynamicPrompts(textInput.value) |
|
textInput.setAttribute('title', prompt) |
|
dynamicPromptsBtn.setAttribute('title', prompt) |
|
if (data.class_type == "PromptSimplification") { |
|
window._appData.data[data.id].inputs.prompt = prompt; |
|
} else { |
|
window._appData.data[data.id].inputs.text = prompt; |
|
} |
|
}) |
|
|
|
|
|
function autoResize(textarea) { |
|
textarea.style.height = 'auto'; |
|
textarea.style.height = textarea.scrollHeight + 'px'; |
|
} |
|
|
|
textInput.addEventListener('input', (event) => { |
|
|
|
autoResize(textInput); |
|
|
|
if (data.class_type == "PromptSimplification") { |
|
window._appData.data[data.id].inputs.prompt = textInput.value; |
|
} else { |
|
window._appData.data[data.id].inputs.text = textInput.value; |
|
} |
|
localStorage.setItem(`t_${data.id}`, JSON.stringify({ |
|
value: textInput.value, |
|
height: textInput.style.height |
|
})); |
|
}) |
|
|
|
|
|
container.appendChild(uploadContainer); |
|
} |
|
|
|
|
|
if (["CheckpointLoaderSimple", "LoraLoader"].includes(data.class_type)) { |
|
let value = data.inputs.ckpt_name || data.inputs.lora_name; |
|
|
|
|
|
try { |
|
let t = ''; |
|
if (data.class_type == 'CheckpointLoaderSimple') { |
|
t = 'checkpoints' |
|
} else if (data.class_type == 'LoraLoader') { |
|
t = 'loras' |
|
} |
|
if (t) { |
|
const response = await fetch(`${get_url()}/mixlab/folder_paths`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ type: t }) |
|
}); |
|
const data = await response.json(); |
|
data.options = data.names; |
|
console.log(data.names); |
|
} |
|
|
|
} catch (error) { |
|
console.error(error); |
|
} |
|
|
|
|
|
try { |
|
let v = localStorage.getItem(`_model_${data.id}_${data.class_type}`) |
|
if (v) { |
|
value = v; |
|
if (data.class_type === 'CheckpointLoaderSimple') { |
|
window._appData.data[data.id].inputs.ckpt_name = value; |
|
} |
|
if (data.class_type === 'LoraLoader') { |
|
window._appData.data[data.id].inputs.lora_name = value; |
|
} |
|
} |
|
} catch (error) { |
|
|
|
} |
|
|
|
|
|
let [div, selectDom] = createSelectWithOptions(data.title, Array.from(data.options, o => { |
|
return { |
|
value: o, |
|
text: o |
|
} |
|
}), value); |
|
|
|
|
|
selectDom.addEventListener('change', e => { |
|
e.preventDefault(); |
|
|
|
if (data.class_type === 'CheckpointLoaderSimple') { |
|
window._appData.data[data.id].inputs.ckpt_name = selectDom.value; |
|
} |
|
if (data.class_type === 'LoraLoader') { |
|
window._appData.data[data.id].inputs.lora_name = selectDom.value; |
|
} |
|
|
|
localStorage.setItem(`_model_${data.id}_${data.class_type}`, selectDom.value) |
|
}) |
|
|
|
container.appendChild(div); |
|
} |
|
|
|
|
|
if (["Color"].includes(data.class_type)) { |
|
let value = data.inputs.color.hex || '#000000'; |
|
let d = document.createElement('div'); |
|
d.className = 'card'; |
|
|
|
let label = document.createElement('label'); |
|
label.innerText = data.title; |
|
|
|
let color = document.createElement('div'); |
|
color.id = `color_input_${data.id}`; |
|
color.className = 'color_input'; |
|
color.setAttribute('data-color', value); |
|
color.setAttribute('data-id', data.id); |
|
|
|
d.appendChild(label); |
|
d.appendChild(color); |
|
|
|
container.appendChild(d); |
|
} |
|
|
|
|
|
if (['LoadAndCombinedAudio_'].includes(data.class_type)) { |
|
let inpAudio = document.createElement('div'); |
|
|
|
let inputAudio = document.createElement('button'); |
|
|
|
inputAudio.innerText = data.title || 'Upload Audio' |
|
inputAudio.style = `cursor: pointer; |
|
font-weight: 300; |
|
margin: 2px; |
|
color: var(--descrip-text); |
|
background-color: var(--comfy-input-bg); |
|
border-radius: 8px; |
|
border-color: var(--border-color); |
|
border-style: solid;height: 30px;min-width: 122px; |
|
`; |
|
|
|
let audioE = document.createElement('audio'); |
|
audioE.setAttribute('controls', 'on') |
|
|
|
audioE.src = data.inputs.audios.base64[0]; |
|
|
|
inputAudio.addEventListener('click', e => { |
|
e.preventDefault() |
|
let inp = document.createElement('input') |
|
inp.type = 'file' |
|
inp.style.display = 'none' |
|
inp.addEventListener('change', async e => { |
|
e.preventDefault() |
|
const file = e.target.files[0] |
|
let base64 = await uploadAndConvertAudio(file) |
|
|
|
audioE.src = base64; |
|
|
|
window._appData.data[data.id].inputs.audios.base64 = [base64]; |
|
}) |
|
|
|
inp.click() |
|
inp.remove() |
|
}) |
|
|
|
inpAudio.appendChild(inputAudio) |
|
inpAudio.appendChild(audioE) |
|
|
|
container.appendChild(inpAudio); |
|
|
|
} |
|
}); |
|
return container |
|
} |
|
|
|
function createColorInput(elId, value, nodeId) { |
|
const pickr = Pickr.create({ |
|
el: `#${elId}`, |
|
theme: 'classic', |
|
default: value, |
|
swatches: [ |
|
'rgba(244, 67, 54, 1)', |
|
'rgba(233, 30, 99, 0.95)', |
|
'rgba(156, 39, 176, 0.9)', |
|
'rgba(103, 58, 183, 0.85)', |
|
'rgba(63, 81, 181, 0.8)', |
|
'rgba(33, 150, 243, 0.75)', |
|
'rgba(3, 169, 244, 0.7)', |
|
'rgba(0, 188, 212, 0.7)', |
|
'rgba(0, 150, 136, 0.75)', |
|
'rgba(76, 175, 80, 0.8)', |
|
'rgba(139, 195, 74, 0.85)', |
|
'rgba(205, 220, 57, 0.9)', |
|
'rgba(255, 235, 59, 0.95)', |
|
'rgba(255, 193, 7, 1)' |
|
], |
|
components: { |
|
|
|
preview: true, |
|
opacity: true, |
|
hue: true, |
|
|
|
interaction: { |
|
hex: true, |
|
rgba: true, |
|
hsla: true, |
|
hsva: true, |
|
cmyk: true, |
|
input: true, |
|
|
|
save: true, |
|
cancel: true |
|
} |
|
} |
|
}); |
|
|
|
pickr |
|
.on('save', (color, instance) => { |
|
try { |
|
|
|
|
|
let [r, g, b, a] = color.toRGBA(); |
|
window._appData.data[nodeId].inputs.color = { |
|
...window._appData.data[nodeId].inputs.color, |
|
r, g, b, a, |
|
hex: color.toHEXA().toString() |
|
} |
|
} catch (error) { } |
|
}) |
|
.on('cancel', instance => { |
|
pickr && pickr.hide() |
|
}) |
|
} |
|
|
|
function createAllColorInput() { |
|
for (const cInp of document.querySelectorAll('.color_input')) { |
|
createColorInput(cInp.id, cInp.getAttribute('data-color'), cInp.getAttribute('data-id')); |
|
} |
|
} |
|
|
|
function createToggleBtn() { |
|
var toggleButton = document.createElement("button"); |
|
toggleButton.innerHTML = `<svg style="width:24px;height: 24px;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3972"><path d="M514 114.3c-219.9 0-398.9 178.9-398.9 398.8 0.1 220 179 398.9 398.9 398.9 219.9 0 398.8-178.9 398.8-398.8S733.9 114.3 514 114.3z m218.3 489v1.7c0 0.5-0.1 1-0.1 1.6 0 0.3 0 0.6-0.1 0.9 0 0.5-0.1 1-0.2 1.5 0 0.3-0.1 0.7-0.1 1-0.1 0.4-0.1 0.8-0.2 1.2-0.1 0.4-0.2 0.9-0.2 1.3-0.1 0.3-0.1 0.6-0.2 0.8-0.1 0.6-0.3 1.2-0.4 1.8 0 0.1-0.1 0.2-0.1 0.3-2.2 8.5-6.6 16.6-13.3 23.3L600.7 755.4c-20 20-52.7 20-72.6 0-20-20-20-52.7 0-72.6l28.9-28.9H347c-28.3 0-51.4-23.1-51.4-51.4 0-28.3 23.1-51.4 51.4-51.4h334c13.2 0 26.4 5 36.4 15s15 23.2 15 36.4c0 0.3-0.1 0.6-0.1 0.8z m0.1-179.5c0 28.3-23.1 51.4-51.4 51.4H347c-13.2 0-26.4-5-36.4-15s-15-23.2-15-36.4v-0.8-1.6c0-0.5 0.1-1.1 0.1-1.6 0-0.3 0-0.6 0.1-0.9 0-0.5 0.1-1 0.2-1.5 0-0.3 0.1-0.7 0.1-1 0.1-0.4 0.1-0.8 0.2-1.2 0.1-0.4 0.2-0.9 0.2-1.3 0.1-0.3 0.1-0.6 0.2-0.8 0.1-0.6 0.3-1.2 0.4-1.8 0-0.1 0.1-0.2 0.1-0.3 2.2-8.5 6.6-16.6 13.3-23.3l116.6-116.6c20-20 52.7-20 72.6 0 20 20 20 52.7 0 72.6L471 372.5h210c28.2 0 51.4 23.1 51.4 51.3z" p-id="3973"></path></svg>`; |
|
toggleButton.className = 'toggle' |
|
return toggleButton |
|
} |
|
|
|
function createCheckbox(defaultValue, labelText) { |
|
let div = document.createElement('div'); |
|
|
|
var checkbox = document.createElement('input'); |
|
checkbox.type = 'checkbox'; |
|
|
|
|
|
checkbox.checked = defaultValue; |
|
|
|
|
|
var label = document.createElement('label'); |
|
label.innerHTML = labelText; |
|
|
|
|
|
div.appendChild(checkbox); |
|
div.appendChild(label); |
|
|
|
return [div, checkbox] |
|
|
|
} |
|
|
|
function createNumSlide(labelText, value = 0, callback, minValue = 0, maxValue = 1, type = 'float', keywords = null, targetId = null) { |
|
|
|
value = 'float' ? parseFloat(value.toFixed(3)) : parseInt(value) |
|
try { |
|
let i = parseFloat(localStorage.getItem(`_slider_${targetId}`)) |
|
if (!!i) { |
|
value = i; |
|
callback && callback(value) |
|
} |
|
} catch (error) { |
|
|
|
} |
|
|
|
|
|
let inputDiv = document.createElement('div'); |
|
inputDiv.style.display = 'flex' |
|
|
|
|
|
var slider = document.createElement("input"); |
|
slider.type = "range"; |
|
slider.min = minValue; |
|
slider.max = maxValue; |
|
slider.step = type == 'float' ? 0.01 : 1 |
|
slider.value = value; |
|
slider.style.width = '150px' |
|
|
|
|
|
var label = document.createElement("label"); |
|
label.innerHTML = labelText; |
|
label.setAttribute('data-content', value); |
|
|
|
if (keywords && keywords[0]) { |
|
|
|
|
|
|
|
let defaultValue = (targetId ? localStorage.getItem(`_slide_${targetId}`) : '') || keywords[0]; |
|
|
|
window._appData.data[targetId].inputs.prompt_keyword = defaultValue; |
|
|
|
let selectTag = createSelect(Array.from(keywords, (k, i) => { |
|
return { |
|
value: k, |
|
text: k, |
|
selected: i == 0 |
|
} |
|
}), defaultValue); |
|
|
|
selectTag.style = `background: none; |
|
color: black; max-width: 300px; |
|
border-bottom: 1px solid #acacac; |
|
border-radius: 0;` |
|
|
|
selectTag.addEventListener('change', e => { |
|
e.preventDefault(); |
|
window._appData.data[targetId].inputs.prompt_keyword = selectTag.value; |
|
|
|
targetId ? localStorage.setItem(`_slide_${targetId}`, selectTag.value) : '' |
|
}) |
|
label.appendChild(selectTag); |
|
} |
|
|
|
|
|
var container = document.createElement("div"); |
|
container.appendChild(label); |
|
|
|
container.className = 'card'; |
|
|
|
|
|
|
|
var toggleButton = createToggleBtn() |
|
|
|
toggleButton.addEventListener("click", function () { |
|
if (slider.type === 'range') { |
|
slider.type = 'number' |
|
} else { |
|
slider.type = 'range' |
|
} |
|
}); |
|
|
|
inputDiv.appendChild(slider); |
|
inputDiv.appendChild(toggleButton); |
|
container.appendChild(inputDiv) |
|
|
|
|
|
slider.addEventListener("input", function (event) { |
|
var value = event.target.value; |
|
value = type == 'float' ? value : Math.round(value) |
|
console.log("滑块输入的值为:" + value); |
|
label.setAttribute('data-content', value); |
|
|
|
callback && callback(value) |
|
|
|
localStorage.setItem(`_slider_${targetId}`, value) |
|
}); |
|
|
|
|
|
return container; |
|
|
|
} |
|
|
|
function createImageForSelect(isMain, imgurl, keyword) { |
|
let im = new Image(); |
|
im.className = isMain ? 'images_prompt_main' : '' |
|
im.src = imgurl; |
|
im.title = keyword |
|
im.style = ` |
|
width:${isMain ? 120 : 56}px; |
|
height:auto; |
|
min-height:${isMain ? 120 : 56}px; |
|
filter: brightness(${isMain ? 1 : 0.8}); |
|
${isMain ? 'filter: drop-shadow(1px 1px 4px black);' : ''} |
|
` |
|
let p = document.createElement('p'); |
|
p.innerText = keyword; |
|
p.style = `position: absolute; |
|
top: 14px; |
|
left: 20px; |
|
background-color: #00000075; |
|
padding: 2px 4px; |
|
color: white; |
|
font-size: 12px;` |
|
const div = document.createElement("div"); |
|
|
|
div.appendChild(im) |
|
if (isMain) div.appendChild(p) |
|
im.setAttribute('onerror', `this.src='${base64Df}'`) |
|
return div |
|
} |
|
|
|
|
|
function createSelectForImages(title, options, index = 0, callback) { |
|
|
|
const div = document.createElement("div"); |
|
|
|
div.style = `margin-top: 24px;` |
|
|
|
|
|
|
|
|
|
|
|
|
|
var mainImg = createImageForSelect(true, options[index].imgurl, options[index].keyword); |
|
div.appendChild(mainImg); |
|
|
|
let imgs = document.createElement('div'); |
|
div.appendChild(imgs); |
|
imgs.style = `display:flex; flex-wrap: wrap;` |
|
for (const opt of options) { |
|
var selectElement = createImageForSelect(false, opt.imgurl, opt.keyword); |
|
imgs.appendChild(selectElement); |
|
selectElement.addEventListener('click', async e => { |
|
e.preventDefault(); |
|
mainImg.querySelector('p').innerText = opt.keyword; |
|
mainImg.querySelector('img').src = opt.imgurl; |
|
if (callback) { |
|
if (!opt.imgurl.match('data:image')) { |
|
opt.imgurl = await parseImageToBase64(opt.imgurl) |
|
} |
|
callback(opt.imgurl, opt.keyword); |
|
} |
|
}) |
|
} |
|
|
|
return [div, mainImg]; |
|
} |
|
|
|
|
|
|
|
function createSelect(options, defaultValue) { |
|
var selectElement = document.createElement("select"); |
|
selectElement.className = "select" |
|
|
|
|
|
for (var i = 0; i < options.length; i++) { |
|
var option = document.createElement("option"); |
|
option.value = options[i].value; |
|
option.innerText = options[i].text; |
|
selectElement.appendChild(option); |
|
|
|
} |
|
|
|
|
|
selectElement.value = defaultValue; |
|
|
|
return selectElement |
|
} |
|
|
|
|
|
function createSelectWithOptions(title, options, defaultValue) { |
|
|
|
const div = document.createElement("div"); |
|
div.className = 'card'; |
|
|
|
|
|
const nameLabel = document.createElement("label"); |
|
nameLabel.textContent = title; |
|
div.appendChild(nameLabel); |
|
|
|
var selectElement = createSelect(options, defaultValue); |
|
|
|
div.appendChild(selectElement) |
|
|
|
return [div, selectElement]; |
|
} |
|
|
|
function getFilenameAndCategoryFromUrl(url) { |
|
const queryString = url.split('?')[1]; |
|
if (!queryString) { |
|
return {}; |
|
} |
|
|
|
const params = new URLSearchParams(queryString); |
|
|
|
const filename = params.get('filename') ? decodeURIComponent(params.get('filename')) : null; |
|
const category = params.get('category') ? decodeURIComponent(params.get('category') || '') : ''; |
|
|
|
return { category, filename }; |
|
} |
|
|
|
function createImage(url) { |
|
let im = new Image() |
|
return new Promise((res, rej) => { |
|
im.onload = () => res(im) |
|
im.src = url |
|
}) |
|
} |
|
|
|
async function createUI(data, share = true) { |
|
|
|
if (!data) return |
|
const { input: inputData, output: outputData, data: workflow, seed, seedTitle, link, name } = data; |
|
|
|
let mainDiv = document.createElement('div'); |
|
|
|
if (document.body.querySelector('#app_container')) document.body.querySelector('#app_container').remove() |
|
let appDetails = document.createElement('details'); |
|
appDetails.id = "app_container" |
|
appDetails.setAttribute('open', true) |
|
appDetails.innerHTML = `<summary>${name}</summary>`; |
|
appDetails.style = `background: whitesmoke; |
|
color: black; |
|
padding: 12px; |
|
cursor: pointer; |
|
margin: 8px 44px;` |
|
|
|
|
|
let leftDetails = document.createElement('details'); |
|
leftDetails.setAttribute('open', 'true') |
|
leftDetails.id = 'app_input_pannel' |
|
leftDetails.innerHTML = `<summary>INPUT</summary> |
|
<div class="content"></div>` |
|
|
|
let leftDiv = leftDetails.querySelector('.content'); |
|
let rightDiv = document.createElement('div'); |
|
|
|
mainDiv.className = 'app' |
|
leftDiv.className = 'panel' |
|
leftDiv.style.alignItems = 'flex-start'; |
|
rightDiv.className = 'panel' |
|
leftDiv.style.flex = 0.4 |
|
rightDiv.style.flex = 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
let titleDiv = document.createElement('div'); |
|
titleDiv.className = 'header' |
|
|
|
var title = document.createElement('h1'); |
|
title.textContent = 'My Application'; |
|
titleDiv.appendChild(title); |
|
|
|
if (share) { |
|
const shareBtn = document.createElement('button'); |
|
shareBtn.innerText = 'copy url'; |
|
shareBtn.addEventListener('click', e => { |
|
e.preventDefault(); |
|
let url = `${get_url()}/mixlab/app?filename=${encodeURIComponent(window._appData.filename)}&category=${encodeURIComponent(window._appData.category || '')}`; |
|
copyTextToClipboard(url, success(e, shareBtn, 'copy url')); |
|
}) |
|
|
|
titleDiv.appendChild(shareBtn); |
|
} |
|
|
|
|
|
|
|
let iconDes = document.createElement('div'); |
|
|
|
var icon = document.createElement('img'); |
|
icon.style.width = '48px'; |
|
|
|
icon.src = base64Df; |
|
|
|
var des = document.createElement('p'); |
|
des.style = `margin-left: 12px; font-size: 14px;` |
|
iconDes.appendChild(icon) |
|
iconDes.appendChild(des); |
|
iconDes.className = 'description' |
|
|
|
|
|
let statusDiv = document.createElement('div'); |
|
statusDiv.className = 'status_seed' |
|
var status = document.createElement('div'); |
|
status.textContent = 'Status'; |
|
status.className = 'status'; |
|
|
|
|
|
var seeds = document.createElement('details'); |
|
|
|
seeds.className = 'seeds'; |
|
|
|
try { |
|
if (Object.keys(seed).length > 0) { |
|
seeds.innerHTML = `<summary>SEED</summary> |
|
<div class="content"> </div>`; |
|
const content = seeds.querySelector('.content') |
|
for (const id in seed) { |
|
const s = seed[id]; |
|
if (!Array.isArray(workflow[id].inputs.seed)) { |
|
|
|
let seedInput = document.createElement('div'); |
|
content.appendChild(seedInput) |
|
seedInput.style = `outline: 1px dashed gray;margin-bottom: 12px;margin-top: 12px;` |
|
|
|
let em = document.createElement('em'); |
|
let emText = document.createElement('span'); |
|
emText.innerText = `#${seedTitle && seedTitle[id] ? seedTitle[id] : id} ${s.toUpperCase()}`; |
|
em.appendChild(emText); |
|
|
|
seedInput.appendChild(em) |
|
|
|
if (s === 'fixed') { |
|
|
|
let inSeed = createNumSlide(``, 0, (newSeed) => updateSeed(id, newSeed), 0, 1849378600828930, 'int'); |
|
inSeed.style = `padding: 8px;background: none` |
|
|
|
let toggleRandomize = createToggleBtn(); |
|
toggleRandomize.style = `background:none;color:black`; |
|
|
|
toggleRandomize.addEventListener("click", function () { |
|
if (data.seed[id] === 'randomize') { |
|
data.seed[id] = 'fixed'; |
|
inSeed.style.display = 'block' |
|
} else { |
|
data.seed[id] = 'randomize'; |
|
inSeed.style.display = 'none' |
|
} |
|
emText.innerText = `#${seedTitle && seedTitle[id] ? seedTitle[id] : id} ${data.seed[id].toUpperCase()}`; |
|
}); |
|
|
|
seedInput.appendChild(inSeed); |
|
em.appendChild(toggleRandomize); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
} |
|
} |
|
|
|
} catch (error) { |
|
console.log(error) |
|
} |
|
|
|
|
|
|
|
statusDiv.appendChild(seeds); |
|
|
|
|
|
var input1 = createInputs(inputData) |
|
|
|
var output = await createOutputs(outputData, link) |
|
|
|
|
|
let submitDiv = document.createElement('div'); |
|
submitDiv.appendChild(statusDiv); |
|
|
|
let submitDivBtn = document.createElement('div'); |
|
submitDivBtn.style.display = 'flex' |
|
submitDiv.appendChild(submitDivBtn); |
|
|
|
submitDivBtn.appendChild(status); |
|
|
|
submitDiv.className = 'run_div'; |
|
var submitButton = document.createElement('button'); |
|
submitButton.textContent = 'Create'; |
|
submitButton.className = 'run_btn' |
|
|
|
|
|
|
|
|
|
|
|
|
|
leftDiv.appendChild(input1); |
|
|
|
submitDivBtn.appendChild(submitButton); |
|
if (typeof (data.data) == 'object') mainDiv.appendChild(submitDiv); |
|
|
|
rightDiv.appendChild(output); |
|
|
|
|
|
|
|
|
|
mainDiv.appendChild(leftDetails); |
|
mainDiv.appendChild(rightDiv); |
|
|
|
appDetails.appendChild(mainDiv); |
|
document.body.appendChild(appDetails) |
|
|
|
appDetails.addEventListener('toggle', e => { |
|
e.preventDefault(); |
|
document.body.querySelector('#author').style.display = appDetails.open ? 'flex' : 'none' |
|
}) |
|
|
|
|
|
return { |
|
title: { |
|
element: title, |
|
update: function (newTitle) { |
|
title.textContent = newTitle; |
|
} |
|
}, |
|
icon: { |
|
element: icon, |
|
update: function (newIconPath) { |
|
icon.src = newIconPath; |
|
} |
|
}, |
|
des: { |
|
element: des, |
|
update: function (text) { |
|
des.textContent = text; |
|
} |
|
}, |
|
status: { |
|
element: status, |
|
update: function (newStatus) { |
|
status.textContent = newStatus; |
|
} |
|
}, |
|
input1: { |
|
element: input1, |
|
update: function () { |
|
|
|
} |
|
}, |
|
output: { |
|
element: output, |
|
update: async function (type = "image", val, id) { |
|
console.log(val, id) |
|
if (val && type == "image" && output.querySelector(`#output_${id} img`)) { |
|
|
|
let im = await createImage(val) |
|
|
|
output.querySelector(`#output_${id} img`).src = val; |
|
|
|
let a = output.querySelector(`#output_${id}`); |
|
a.setAttribute('data-pswp-width', im.naturalWidth); |
|
a.setAttribute('data-pswp-height', im.naturalHeight); |
|
a.setAttribute('target', "_blank"); |
|
a.setAttribute('href', val); |
|
|
|
} |
|
|
|
if (val && (type == "images" || type == 'images_prompts') && output.querySelector(`#output_${id} img`)) { |
|
let imgDiv = output.querySelector(`#output_${id}`) |
|
imgDiv.style.display = 'none'; |
|
|
|
|
|
|
|
|
|
for (const v of val) { |
|
|
|
let url = v, prompt = '' |
|
|
|
if (type == 'images_prompts') { |
|
|
|
url = v[0]; |
|
prompt = v[1]; |
|
} |
|
|
|
let im = await createImage(url); |
|
|
|
|
|
let a = document.createElement('a'); |
|
a.className = `${imgDiv.id} output_images` |
|
a.setAttribute('data-pswp-width', im.naturalWidth); |
|
a.setAttribute('data-pswp-height', im.naturalHeight); |
|
a.setAttribute('target', "_blank"); |
|
a.setAttribute('href', url); |
|
|
|
|
|
let img = new Image(); |
|
|
|
img.src = url; |
|
a.appendChild(img); |
|
|
|
if (prompt) { |
|
a.style.textDecoration = 'none'; |
|
let p = document.createElement('p') |
|
p.className = 'prompt_image' |
|
p.innerText = prompt; |
|
a.appendChild(p) |
|
img.alt = prompt |
|
} |
|
|
|
|
|
imgDiv.parentElement.insertBefore(a, imgDiv.parentElement.firstChild); |
|
} |
|
|
|
} |
|
|
|
if (val && type == "video" && output.querySelector(`#output_${id} video`)) { |
|
|
|
let video = output.querySelector(`#output_${id} video`); |
|
let img = output.querySelector(`#output_${id} img`); |
|
img.style.display = 'none'; |
|
video.style.display = 'block'; |
|
|
|
video.onloadeddata = function () { |
|
|
|
let a = output.querySelector(`#output_${id}`); |
|
a.setAttribute('data-pswp-width', video.videoWidth); |
|
a.setAttribute('data-pswp-height', video.videoHeight); |
|
a.setAttribute('target', "_blank"); |
|
a.setAttribute('href', val); |
|
}; |
|
|
|
video.src = val; |
|
|
|
} |
|
|
|
if (val && type == "text" && output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).innerText = val; |
|
|
|
|
|
if (val && type == 'meshes' && output.querySelector(`#output_${id}`)) { |
|
|
|
let threeD = output.querySelector('.threeD') |
|
|
|
let imgDf = output.querySelector(`#output_${id} img`); |
|
if (!threeD) { |
|
if (imgDf) imgDf.parentElement.remove(); |
|
threeD = document.createElement('div'); |
|
threeD.className = 'threeD' |
|
threeD.id = `output_${id}` |
|
output.querySelector('.output_card').appendChild(threeD) |
|
|
|
}; |
|
|
|
|
|
for (const meshUrl of val) { |
|
const modelViewer = document.createElement('div'); |
|
modelViewer.style = `width:300px;margin:4px;height:300px;display:block` |
|
modelViewer.innerHTML = `<model-viewer src="${meshUrl}" |
|
min-field-of-view="0deg" max-field-of-view="180deg" |
|
shadow-intensity="1" |
|
camera-controls |
|
touch-action="pan-y" |
|
style="width:300px;height:300px;" |
|
> |
|
<div class="controls"> |
|
<button class="export">Save As</button> |
|
</div> |
|
</model-viewer>` |
|
|
|
const btn = modelViewer.querySelector('.export'); |
|
btn.addEventListener('click', async e => { |
|
e.preventDefault(); |
|
const glTF = await (modelViewer.querySelector('model-viewer')).exportScene() |
|
const file = new File([glTF], 'mixlab.glb') |
|
const link = document.createElement('a') |
|
link.download = file.name |
|
link.href = URL.createObjectURL(file) |
|
link.click() |
|
}) |
|
threeD.appendChild(modelViewer) |
|
} |
|
|
|
} |
|
} |
|
}, |
|
submitButton: { |
|
element: submitButton, |
|
update: function (runFn, cancelFn) { |
|
submitButton.addEventListener('dblclick', (e) => { |
|
e.preventDefault() |
|
submitButton.classList.remove('disabled'); |
|
}); |
|
submitButton.addEventListener('click', (e) => { |
|
e.preventDefault(); |
|
|
|
if (submitButton.classList.contains('data-click')) { |
|
return |
|
} else { |
|
submitButton.classList.add('data-click') |
|
setTimeout(() => submitButton.classList.remove('data-click'), 500) |
|
} |
|
|
|
if (!submitButton.classList.contains('disabled')) { |
|
runFn && runFn(); |
|
submitButton.classList.add('disabled'); |
|
submitButton.innerText = 'Cancel' |
|
} else { |
|
|
|
let canCancel = cancelFn && cancelFn(); |
|
if (canCancel) submitButton.classList.remove('disabled'); |
|
if (canCancel) submitButton.innerText = 'Create'; |
|
} |
|
|
|
}); |
|
}, |
|
running: () => { |
|
submitButton.innerText = 'Cancel'; |
|
if (!submitButton.classList.contains('disabled')) submitButton.classList.add('disabled'); |
|
}, |
|
reset: () => { |
|
submitButton.innerText = 'Create'; |
|
submitButton.classList.remove('disabled'); |
|
submitButton.classList.remove('data-click'); |
|
} |
|
}, |
|
}; |
|
} |
|
|
|
function createUploadJson(detail) { |
|
|
|
|
|
var div = document.createElement('div'); |
|
div.className = 'upload_btn card' |
|
div.textContent = '上传并运行你的JSON文件'; |
|
div.addEventListener('click', function () { |
|
document.getElementById('jsonFileInput').click(); |
|
}); |
|
|
|
|
|
var input = document.createElement('input'); |
|
input.type = 'file'; |
|
input.id = 'jsonFileInput'; |
|
input.style.display = 'none'; |
|
input.addEventListener('change', function (event) { |
|
var file = event.target.files[0]; |
|
var reader = new FileReader(); |
|
reader.onload = function (e) { |
|
var contents = e.target.result; |
|
var jsonData = JSON.parse(contents); |
|
|
|
|
|
Array.from(detail.querySelectorAll('.card'), c => c.classList.remove('selected')); |
|
div.className = 'upload_btn card selected' |
|
|
|
let { output, app } = jsonData; |
|
|
|
window._appData = { |
|
...app, |
|
data: output |
|
}; |
|
if (document.body.querySelector('.app')) document.body.querySelector('.app').remove() |
|
createApp(window._appData, false); |
|
|
|
|
|
}; |
|
reader.readAsText(file); |
|
}); |
|
|
|
|
|
|
|
document.body.appendChild(input); |
|
return div |
|
} |
|
|
|
function executed(detail, show) { |
|
console.log('#executed', window.prompt_ids, detail) |
|
if (detail?.node |
|
&& window.prompt_ids[detail.prompt_id] |
|
&& window._appData?.output.filter(f => f.id === detail.node)[0]) { |
|
|
|
window.prompt_ids[detail.prompt_id].data = detail |
|
window.prompt_ids[detail.prompt_id].createTime = (new Date()).getTime() |
|
savePromptResult({ |
|
...window.prompt_ids[detail.prompt_id], |
|
prompt_id: detail.prompt_id |
|
}) |
|
} |
|
|
|
const images = detail?.output?.images; |
|
const text = detail?.output?.text; |
|
const gifs = detail?.output?.gifs; |
|
|
|
const prompt = detail?.output?.prompt; |
|
const analysis = detail?.output?.analysis; |
|
|
|
const _images = detail?.output?._images; |
|
const prompts = detail?.output?.prompts; |
|
|
|
|
|
const meshes = detail?.output?.mesh; |
|
|
|
if (images) { |
|
|
|
|
|
let url = get_url(); |
|
|
|
show(Array.from(images, img => { |
|
return `${url}/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}&t=${+new Date()}`; |
|
}), detail.node, 'images'); |
|
|
|
} else if (meshes) { |
|
|
|
let url = get_url(); |
|
|
|
show(Array.from(meshes, mesh => { |
|
return `${url}/view?filename=${encodeURIComponent(mesh.filename)}&type=${mesh.type}&subfolder=${encodeURIComponent(mesh.subfolder)}&t=${+new Date()}`; |
|
}), detail.node, 'meshes'); |
|
|
|
} else if (_images && prompts) { |
|
let url = get_url(); |
|
|
|
let items = []; |
|
|
|
Array.from(_images, (imgs, i) => { |
|
|
|
for (const img of imgs) { |
|
items.push([`${url}/view?filename=${encodeURIComponent(img.filename) |
|
}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder) |
|
}&t=${+new Date()}`, prompts[i]]) |
|
} |
|
|
|
}) |
|
|
|
show(items, detail.node, 'images_prompts'); |
|
|
|
} else if (text) { |
|
show(Array.isArray(text) ? text.join('\n\n') : text, detail.node, 'text') |
|
} else if (gifs && gifs[0]) { |
|
|
|
const src = `${get_url()}/view?filename=${encodeURIComponent(gifs[0].filename)}&type=${gifs[0].type}&subfolder=${encodeURIComponent(gifs[0].subfolder) |
|
}&&format=${gifs[0].format}&t=${+new Date()}`; |
|
|
|
show(src, detail.node, gifs[0].format.match('video') ? 'video' : 'image'); |
|
} else if (prompt && analysis) { |
|
|
|
show(`${prompt.join('\n\n')}\n${JSON.stringify(analysis, null, 2)}`, detail.node, 'text') |
|
} |
|
} |
|
|
|
|
|
|
|
async function createApp(appData, share = true) { |
|
|
|
|
|
var ui = await createUI(appData, share); |
|
|
|
|
|
ui.title.update(appData.name || 'Mixlab APP'); |
|
|
|
|
|
ui.icon.update(appData.icon || appData.output[0]?.options?.defaultImage || base64Df); |
|
|
|
ui.des.update(appData.description || '-'); |
|
|
|
|
|
ui.status.update(appData ? 'READY' : '-'); |
|
|
|
|
|
ui.submitButton.update( |
|
() => { |
|
|
|
queuePrompt({ |
|
name: window._appData.name, |
|
id: window._appData.id, |
|
icon: window._appData.icon, |
|
category: window._appData.category, |
|
filename: window._appData.filename |
|
}, window._appData.data, window._appData.seed, api.clientId); |
|
}, () => { |
|
|
|
if (api.runningCancel) { |
|
api.runningCancel(); |
|
api.runningCancel = null; |
|
return true |
|
} |
|
|
|
}); |
|
|
|
|
|
const show = (src, id, type = "image") => { |
|
|
|
ui.output.update(type, src, id) |
|
}; |
|
|
|
window._show = show; |
|
|
|
api.addEventListener("status", ({ detail }) => { |
|
console.log("status", detail, detail?.exec_info?.queue_remaining); |
|
try { |
|
ui.status.update(`queue#${detail.exec_info?.queue_remaining}`); |
|
window.parent.postMessage({ cmd: 'status', data: `queue#${detail.exec_info?.queue_remaining}` }, '*'); |
|
if (detail.exec_info?.queue_remaining === 0) { |
|
|
|
ui.submitButton.reset() |
|
console.log('运行按钮重设') |
|
} |
|
} catch (error) { |
|
console.log(error) |
|
window.parent.postMessage({ cmd: 'status' }, '*'); |
|
} |
|
|
|
}); |
|
|
|
api.addEventListener("progress", ({ detail }) => { |
|
console.log("progress", detail); |
|
const class_type = window._appData.data[detail?.node]?.class_type || '' |
|
try { |
|
ui.status.update(`${parseFloat(100 * detail.value / detail.max).toFixed(1)}% ${class_type}`); |
|
ui.submitButton.running() |
|
} catch (error) { |
|
|
|
} |
|
}); |
|
|
|
api.addEventListener("executed", async ({ detail }) => { |
|
console.log("executed", detail) |
|
executed(detail, show); |
|
|
|
try { |
|
ui.status.update(`executed_#${window._appData.data[detail.node]?.class_type}`); |
|
ui.submitButton.reset() |
|
} catch (error) { |
|
|
|
} |
|
|
|
|
|
try { |
|
const { Running, Pending } = await getQueue(api.clientId); |
|
if (Running && Running[0]) { |
|
api.runningCancel = Running[0].remove; |
|
ui.submitButton.running() |
|
} else { |
|
api.runningCancel = null; |
|
} |
|
} catch (error) { |
|
api.runningCancel = null; |
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
api.addEventListener("execution_error", ({ detail }) => { |
|
|
|
console.log("execution_error", detail) |
|
window.parent.postMessage({ cmd: 'status', data: `execution_error:${JSON.stringify(detail)}` }, '*'); |
|
|
|
}); |
|
|
|
|
|
api.addEventListener('execution_start', async ({ detail }) => { |
|
console.log("execution_start", detail) |
|
try { |
|
ui.status.update(`execution_start`); |
|
ui.submitButton.running() |
|
} catch (error) { |
|
|
|
} |
|
|
|
try { |
|
const { Running, Pending } = await getQueue(api.clientId); |
|
if (Running && Running[0]) { |
|
api.runningCancel = Running[0].remove; |
|
} else { |
|
api.runningCancel = null; |
|
} |
|
} catch (error) { |
|
api.runningCancel = null; |
|
} |
|
}) |
|
|
|
api.api_base = "" |
|
api.init(); |
|
|
|
|
|
createAllColorInput(); |
|
|
|
|
|
const lightbox = new PhotoSwipeLightbox({ |
|
gallery: '.output_card', |
|
children: 'a', |
|
pswpModule: () => import('/extensions/comfyui-mixlab-nodes/lib/photoswipe.esm.min.js'), |
|
|
|
}); |
|
lightbox.on('uiRegister', function () { |
|
lightbox.pswp.ui.registerElement({ |
|
name: 'custom-caption', |
|
order: 9, |
|
isButton: false, |
|
appendTo: 'root', |
|
html: 'Caption text', |
|
onInit: (el, pswp) => { |
|
lightbox.pswp.on('change', () => { |
|
const currSlideElement = lightbox.pswp.currSlide.data.element |
|
let captionHTML = '' |
|
if (currSlideElement) { |
|
const hiddenCaption = currSlideElement.querySelector( |
|
'.hidden-caption-content' |
|
) |
|
if (hiddenCaption) { |
|
|
|
captionHTML = hiddenCaption.innerHTML |
|
} else { |
|
|
|
captionHTML = currSlideElement |
|
.querySelector('img') |
|
.getAttribute('alt') |
|
} |
|
} |
|
el.innerHTML = captionHTML || '' |
|
}) |
|
} |
|
}) |
|
}) |
|
lightbox.init(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (appData.author) { |
|
let div = document.body.querySelector('#author'); |
|
if (appData.author.link) div.href = appData.author.link |
|
div.style = `z-index:20;display: flex;flex-direction: column;position: fixed;bottom: 24px;left: 24px;cursor: pointer;text-decoration: none;color: black;` |
|
div.innerHTML = `<p style="font-size:12px;margin: 8px 0;">Author:</p> |
|
<div style="display: flex;"> <img style="width:32px;height:32px;border-radius: 100%;" |
|
src="${appData.author.avatar || base64Df}"/> |
|
<p style="margin-left:8px;font-size:12px;font-weight:800">${appData.author.name || '-'}</p></div>` |
|
} |
|
|
|
} |
|
|
|
|
|
function createAppList(apps = [], innerApp = false) { |
|
window.prompt_ids = {}; |
|
if (document.body.querySelector('.apps')) { |
|
document.body.querySelector('.apps').remove() |
|
} |
|
let details = document.createElement('details'); |
|
details.className = 'apps'; |
|
|
|
details.innerHTML = `<summary>APP Store / ${apps.length}</summary> |
|
<div class="content"> </div>` |
|
|
|
let div = details.querySelector('div'); |
|
|
|
for (let index = 0; index < apps.length; index++) { |
|
const app = apps[index]; |
|
let d = document.createDocumentFragment(); |
|
let dd = document.createElement('div'); |
|
d.appendChild(dd); |
|
dd.className = 'card' + (index == 0 ? ' selected' : '') |
|
dd.innerHTML = ` |
|
<div class="item icon"> |
|
<img src="${app.icon || base64Df}"/> |
|
</div> |
|
<div class="item" style="margin-left: 24px;"> |
|
<div> |
|
<h5>${app.name}</h5> |
|
<p>${app.description}</p> |
|
</div> |
|
<div > |
|
<p class="version">Version: ${app.version}</p> |
|
</div> |
|
<br> |
|
${app.author && app.author.name ? `<p class="version">Author:</p><div |
|
style="display: flex;justify-content: center;align-items: center;margin-top: 8px;"> |
|
<img style="width:28px;height:28px;border-radius: 100%;" |
|
src="${app.author.avatar || base64Df}"/> |
|
<p class="version" style="margin-left: 12px;">${app.author.name}</p> |
|
</div>`: ''} |
|
</div> |
|
` |
|
|
|
|
|
div.appendChild(d); |
|
dd.addEventListener('click', async e => { |
|
e.preventDefault(); |
|
Array.from(div.querySelectorAll('.card'), c => c.classList.remove('selected')); |
|
dd.className = 'card selected' |
|
|
|
details.removeAttribute('open'); |
|
let res = (await get_my_app(app.category, app.filename)).filter(n => n.filename === app.filename)[0]; |
|
|
|
if (res) { |
|
window._appData = res; |
|
if (document.body.querySelector('.app')) document.body.querySelector('.app').remove() |
|
createApp(window._appData); |
|
localStorage.setItem('app_selected', window._appData.id) |
|
} |
|
|
|
}) |
|
|
|
}; |
|
|
|
if (!innerApp) { |
|
let uploadApp = createUploadJson(details); |
|
div.appendChild(uploadApp); |
|
} |
|
|
|
document.body.appendChild(details); |
|
details.addEventListener('toggle', e => { |
|
e.preventDefault(); |
|
if (document.body.querySelector('#app_container')) { |
|
document.body.querySelector('#app_container').removeAttribute('open'); |
|
document.body.querySelector('#author').style.display = 'none' |
|
} |
|
}) |
|
} |
|
|
|
|
|
async function getPromptResult(category) { |
|
let url = get_url() |
|
try { |
|
const response = await fetch(`${url}/mixlab/prompt_result`, { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify({ |
|
action: "all", |
|
}), |
|
}); |
|
|
|
if (response.ok) { |
|
const data = await response.json(); |
|
console.log("#getPromptResult:", category, data); |
|
|
|
return data.result.filter(r => r.appInfo.category == category) |
|
|
|
} else { |
|
console.log("Error:", response.status); |
|
|
|
} |
|
} catch (error) { |
|
console.log("Error:", error); |
|
|
|
} |
|
} |
|
|
|
async function savePromptResult(data) { |
|
let url = get_url() |
|
try { |
|
const response = await fetch(`${url}/mixlab/prompt_result`, { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify({ |
|
action: "save", |
|
data |
|
}), |
|
}); |
|
|
|
if (response.ok) { |
|
const res = await response.json(); |
|
console.log("Response:", res); |
|
return res |
|
|
|
} else { |
|
console.log("Error:", response.status); |
|
|
|
} |
|
} catch (error) { |
|
console.log("Error:", error); |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
async function createHistoryList(category) { |
|
if (document.body.querySelector('#history_container')) document.body.querySelector('#history_container').remove(); |
|
|
|
window._historyData = await getPromptResult(category); |
|
|
|
if (!window._historyData || (window._historyData && window._historyData.length === 0)) return |
|
|
|
let details = document.createElement('details'); |
|
details.id = "history_container" |
|
details.innerHTML = `<summary>历史</summary>`; |
|
details.style = `background: whitesmoke; |
|
color: black; |
|
padding: 12px; |
|
cursor: pointer; |
|
margin: 8px 44px;`; |
|
|
|
details.addEventListener('toggle', function (event) { |
|
if (details.open) { |
|
|
|
|
|
if (document.body.querySelector('#app_container')) { |
|
document.body.querySelector('#app_container').removeAttribute('open') |
|
} |
|
if (document.body.querySelector('.apps')) { |
|
document.body.querySelector('.apps').removeAttribute('open') |
|
} |
|
} else { |
|
|
|
|
|
if (document.body.querySelector('#app_container')) { |
|
document.body.querySelector('#app_container').removeAttribute('open') |
|
} |
|
if (document.body.querySelector('.apps')) { |
|
document.body.querySelector('.apps').removeAttribute('open') |
|
} |
|
} |
|
}); |
|
|
|
|
|
let cards = document.createElement('div'); |
|
cards.style = `display: flex;flex-wrap: wrap;` |
|
|
|
const addCard = (title, createTime, imgurl) => { |
|
let card = document.createElement('div') |
|
card.className = 'card'; |
|
card.innerHTML = `<div class="item icon"> |
|
<img src="${imgurl || base64Df}"/> |
|
</div> |
|
<div class="item" style="margin-left: 24px;"> |
|
<div> |
|
<h5>${title}</h5> |
|
<p></p> |
|
</div> |
|
<div> |
|
<p class="version">${new Date(createTime || (new Date()))}</p> |
|
|
|
</div> |
|
</div>` |
|
return card |
|
} |
|
|
|
for (const c of window._historyData) { |
|
let card = addCard(c.appInfo.name, c.createTime, c.appInfo.icon) |
|
cards.appendChild(card) |
|
card.addEventListener('click', async e => { |
|
e.preventDefault(); |
|
|
|
|
|
const { category, filename } = c.appInfo; |
|
window._appData = (await get_my_app(category, filename))[0]; |
|
|
|
await createApp(window._appData); |
|
|
|
executed(c.data, window._show); |
|
|
|
try { |
|
document.body.querySelector('#app_container').setAttribute('open', true) |
|
document.body.querySelector('#app_input_pannel').removeAttribute('open') |
|
document.body.querySelector('.apps').removeAttribute('open') |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
}) |
|
} |
|
|
|
details.appendChild(cards); |
|
|
|
document.body.appendChild(details); |
|
} |
|
|
|
async function init_app() { |
|
|
|
const innerApp = checkIsInnerApp(); |
|
|
|
if (!innerApp) { |
|
const { category, filename } = getFilenameAndCategoryFromUrl(location.href); |
|
window._apps = await get_my_app(category, filename); |
|
|
|
window._appData = window._apps[0]; |
|
|
|
createAppList(window._apps); |
|
|
|
if (window._apps.length > 0) await createHistoryList(category || ''); |
|
|
|
createApp(window._appData); |
|
} |
|
|
|
}; |
|
|
|
init_app(); |
|
|
|
|
|
function checkIsInnerApp() { |
|
const url = new URL(window.location.href); |
|
const params = new URLSearchParams(url.search); |
|
const innerApp = params.get("innerApp"); |
|
|
|
if (innerApp == 1) { |
|
document.body.querySelector('.header').style.display = 'none'; |
|
window.parent.postMessage({ innerApp, cmd: 'init' }, '*'); |
|
|
|
|
|
window.addEventListener("message", async function (event) { |
|
console.log("Received message from parent:", event.data); |
|
const { init, url } = event.data; |
|
|
|
window._hostUrl = url; |
|
|
|
window._apps = init; |
|
|
|
window._appData = window._apps[0]; |
|
|
|
if (window._appData) { |
|
createAppList(window._apps, innerApp); |
|
|
|
|
|
|
|
createApp(window._appData); |
|
} else { |
|
|
|
document.body.innerHTML = `<h3 style="padding: 99px;">Welcome to Mixlab Nodes App!</h3>` |
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
return innerApp == 1 |
|
|
|
} |
|
|
|
|
|
</script> |
|
|
|
|
|
|
|
</body> |
|
|
|
</html> |