3v324v23's picture
lfs
1e3b872
raw
history blame
138 kB
<!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:hover {
box-shadow: 0px 0px 10px 10px #e9fbfa
} */
.card h5 {
font-size: 14px;
margin: 0 0 10px 0
}
.card p {
margin: 0;
padding: 0;
/* max-height: 35px; */
overflow: hidden;
/* width: 98px; */
}
.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;
/* background: #eee; */
margin: 24px;
flex: 1;
align-items: center;
/* justify-content: 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;
/*min-height: 200px;*/
}
.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;
/* justify-content: center;
align-items: center; */
}
.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';
/* min-width: 300px; */
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;
/* align-items: center; */
/* justify-content: space-around; */
/* width: calc(50% - 100px); */
flex-direction: column;
background-color: #f7f7f7;
box-shadow: 3px 3px 8px #cacaca;
border-radius: 8px;
}
.status {
/* background: #e7e7e7; */
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;
/* width: calc(50% - 100px); */
min-width: 200px;
max-width: 460px;
height: 48px;
border-radius: 8px;
cursor: pointer;
border: 3px solid;
}
button:hover {
/* border-color: yellow !important; */
color: #ffffff !important;
/* font-weight: 400; */
background: #232222;
cursor: pointer;
}
.disabled {
background-color: #eee !important;
color: #4a4a4a !important;
}
.upload_btn {
width: 188px;
cursor: pointer;
/* height: 188px; */
/* background: black; */
color: white;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 14px;
color: black;
}
/* .upload_btn:hover {
outline: 4px solid yellow;
color: yellow;
} */
.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);
/* Set the initial content using the data-content attribute */
/* position: absolute; */
/* top: 100%;
left: 0; */
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 节点使用 */
.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;
/* 角落颜色 */
}
/*
.dynamic_prompt::after {
content: attr(title);
position: absolute;
color: black;
padding: 4px;
padding-left: 25px;
border-radius: 4px;
} */
summary {
user-select: none;
}
</style>
<!-- <script src="../../../scripts/api.js" type="module"></script> -->
<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 src="/extensions/comfyui-mixlab-nodes/lib/filerobot-image-editor.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" src="https://cdn.bootcdn.net/ajax/libs/photoswipe/5.4.0/photoswipe-lightbox.esm.min.js"></script> -->
<script type="module">
import PhotoSwipeLightbox from '/extensions/comfyui-mixlab-nodes/lib/photoswipe-lightbox.esm.min.js'
// console.log(Lightbox)
import { api } from "../../../scripts/api.js";
// ComfyUI\web\extensions\core\dynamicPrompts.js
// 官方实现修改
// Allows for simple dynamic prompt replacement
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
/*
* Strips C-style line and block comments from a string
*/
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
}
// console.log('api', api)
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()
// console.log(data)
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
})
// console.log(resp)
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)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
.catch(error => {
console.log('发生错误:', error)
})
})
}
//给load image to batch节点使用的输入
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)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
}
function base64ToBlob(base64) {
// 去除base64编码中的前缀
const base64WithoutPrefix = base64.replace(/^data:image\/\w+;base64,/, '');
// 将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);
}
// 创建blob对象
const blob = new Blob(byteArrays, { type: 'image/png' }); // 根据实际情况设置MIME类型
return blob;
}
// 获取 rembg 模型
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()
// console.log(data)
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()
// console.log(data)
return data.data
} catch (error) {
console.error(error)
}
}
function convertImageToBlackBasedOnAlpha(image) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Draw the image onto the canvas
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
// Get the image data from the canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
// Modify the RGB values based on the alpha channel
for (let i = 0; i < pixels.length; i += 4) {
const alpha = pixels[i + 3];
if (alpha !== 0) {
// Set non-transparent pixels to black
pixels[i] = 0; // Red
pixels[i + 1] = 0; // Green
pixels[i + 2] = 0; // Blue
}
}
// Put the modified image data back onto the canvas
ctx.putImageData(imageData, 0, 0);
// Convert the modified canvas to base64 data URL
const base64ImageData = canvas.toDataURL('image/png'); // Replace 'png' with your desired image format
return base64ImageData;
}
// 图像编辑
async function editImage(image, data) {
//判断mask是否有输出
let isMask = data.options.hasMask;
//app
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()
}
//取image
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()
}
//add mask
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);
}
//add image
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);
}
//默认的画笔size设置大
// let inputSize = (iframe.contentDocument.getElementById('size')).querySelector('input');
// inputSize.value=50;
// (iframe.contentDocument.getElementById('size')).querySelector('.increase_number').click()
//自动抠图
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 => {
//api请求
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)
// 获取 id 为 "mix" 的 button 元素
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();
//image的合成,排除mask和brush
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;
//todo 获取Image更新后的数据(暂不支持image的修改)
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] = 255 - imageDataData[i];
// imageDataData[i + 1] = 255 - imageDataData[i + 1];
// imageDataData[i + 2] = 255 - imageDataData[i + 2];
imageDataData[i + 3] = 255 - imageDataData[i + 3]; // 反相透明度
}
// 更新画布
tempCtx.putImageData(imageData, 0, 0);
let base64 = tempCanvas.toDataURL();
// console.log(base64);
editor.style.display = 'none';
let fileBlob = base64ToBlob(base64)
// // 获取读取的文件内容,即 Blob 对象
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))
//mask
let { url, name } = await uploadMask(fileBlob, imgurl);
// 在这里可以对 Blob 对象进行进一步处理
// imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
// console.log("上传的文件:", url, data.id, name);
//更新图片
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);
// 绘制base64图片
// const base64Image = base64
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;
// console.log(Layers)
//判断是否已经存在
let layers1 = Layers.get_layers();
//通过layer.link.src 判断是否图片更新
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)
// console.log(layers1, layers1.length)
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 action uses a different endpoint for cancelling
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
//1849378600828930
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)
// console.log('new Seed', data[id])
}
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)
}
// class_type:"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) {
// console.log(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) {
// appinfo升级后 兼容,补丁
for (const id in promptWorkflow) {
if (promptWorkflow[id].class_type == 'AppInfo') {
promptWorkflow[id].inputs.category = promptWorkflow[id].inputs.category || ""
}
}
// 随机seed
promptWorkflow = randomSeed(seed, promptWorkflow);
// //动态提示,改为输入的时候,手动触发
// for (const id in promptWorkflow) {
// let node = promptWorkflow[id]
// if (["TextInput_", "CLIPTextEncode", "PromptSimplification", "ChinesePrompt_Mix"].includes(
// node.class_type
// )) {
// if (node.class_type == "PromptSimplification") {
// promptWorkflow[id].inputs.prompt = dynamicPrompts(node.inputs.prompt);
// } else {
// promptWorkflow[id].inputs.text = dynamicPrompts(node.inputs.text);
// }
// console.log('#动态提示', promptWorkflow[id].inputs)
// }
// }
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 => {
// Handle response here
// console.log(response)
let res = await response.json();
window.prompt_ids[res.prompt_id] = {
appInfo,
prompt_id: res.prompt_id
}
})
.catch(error => {
// Handle error here
});
}
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) {
}
// new URL(link)
if (isURL || isAElement) {
const linkBtn = document.createElement('button');
if (isURL) {
// linkBtn.href = link;
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);
}
// if(isURL) 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();
// copyHtmlWithImagesToClipboard(output_card.outerHTML)
copyImagesToClipboard(output_card.outerHTML, (r) => success(r, copyImage, 'copy image'))
// copyTextToClipboard()
})
copyHTML.addEventListener('click', e => {
e.preventDefault();
copyHtmlWithImagesToClipboard((window._appData.share_prefix || '') + " " + output_card.outerHTML, (r) => success(r, copyHTML, 'copy as html'))
// copyImagesToClipboard(output_card.outerHTML)
})
//是否显示复制图片,复制html两个按钮
let isShowImageFn = false;
for (const node of outputData) {
// console.log('output', node)
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)
// a.setAttribute('data-pswp-width', img.naturalWidth);
// a.setAttribute('data-pswp-height', img.naturalHeight);
// a.setAttribute('target', "_blank");
// a.setAttribute('href', url);
// a.setAttribute('title', node.title);
output_card.appendChild(a);
isShowImageFn = true;
}
//3d
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;
img.src = base64Df;
a.appendChild(img)
output_card.appendChild(a);
}
// video ,gif
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);
// TODO 支持视频 https://photoswipe.com/custom-content/
// let v = document.createElement('div');
let video = document.createElement('video'), img = new Image();
video.style.display = 'none'
video.controls = 'true'
video.autoplay = 'true'
video.loop = 'true'
// v.id = `output_${node.id}`;
img.src = base64Df;
a.appendChild(video);
a.appendChild(img);
output_card.appendChild(a);
}
}
if (isShowImageFn === false) {
copyImage.remove();
copyHTML.remove();
}
return container
}
function generateRainbowVideo() {
// 创建一个canvas元素
const canvas = document.createElement('canvas');
canvas.width = 640; // 设置canvas宽度
canvas.height = 480; // 设置canvas高度
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); // 设置录制时长,这里设置为1秒
})
}
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) {
//data.class_type === 'LoadImagesToBatch'
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);
// // 获取读取的文件内容,即 Blob 对象
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);
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
//更换option里的default image
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) {
// 创建一个临时div元素
const tempDiv = document.createElement('div');
// 将HTML字符串赋值给div的innerHTML属性
tempDiv.innerHTML = data;
// 获取div中的所有图像元素
const images = tempDiv.getElementsByTagName('img');
// 遍历图像元素,并将图像数据转换为Base64编码
for (let i = 0; i < images.length; i++) {
const image = images[i];
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置canvas尺寸与图像尺寸相同
canvas.width = image.width;
canvas.height = image.height;
// 在canvas上绘制图像
context.drawImage(image, 0, 0);
// 将canvas转换为Base64编码
const imageData = canvas.toDataURL();
// 将Base64编码替换图像元素的src属性
image.src = imageData;
}
let richText = tempDiv.innerHTML;
// 创建一个新的Blob对象,并将富文本字符串作为数据传递进去
const blob = new Blob([richText], { type: 'text/html' });
// 创建一个ClipboardItem对象,并将Blob对象添加到其中
const clipboardItem = new ClipboardItem({ 'text/html': blob });
// 使用Clipboard API将内容复制到剪贴板
navigator.clipboard.write([clipboardItem])
.then(() => {
console.log('富文本已成功复制到剪贴板');
tempDiv.remove()
if (cb) cb(true)
})
.catch((error) => {
console.error('复制到剪贴板失败:', error);
tempDiv.remove()
if (cb) cb(false)
});
}
// const htmlWithImages = "<p>这是要复制的HTML内容</p><img src='data:image/png;base64,iVBORw0KG...'>"
// copyHtmlWithImagesToClipboard(htmlWithImages);
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()
});
}
// const htmlString = "<p>这是要复制的HTML内容</p><img src='url'><img src='url'>";
// copyImagesToClipboard(htmlString);
// 上传音频转为base64
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) {
// Assuming you have an HTML element with the id "container" to hold the UI
const container = document.createElement("div");
container.className = 'input_card'
// const inputData = [
// {
// inputs: {
// image: "1703554480406.png",
// upload: "image"
// },
// class_type: "LoadImage"
// },
// {
// inputs: {
// image: "6b7f3c570ee13ef22aad3d26dcc7414.png",
// upload: "image"
// },
// class_type: "LoadImage"
// }
// ];
inputData = inputData.filter(inp => inp);
// console.log('inputData',inputData)
inputData.forEach(async data => {
console.log('inputData', data);
// 图片 or 视频输入
if (["LoadImage",
"VHS_LoadVideo",
"ImagesPrompt_",
"LoadImagesToBatch"].includes(data.class_type)) {
let isVideoUpload = data.class_type === "VHS_LoadVideo";
let isBase64Upload = data.class_type === "LoadImagesToBatch";
// Create a container for the upload control
const uploadContainer = document.createElement("div");
uploadContainer.className = 'card';
// Create a label for the upload control
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;`
// Create an input field for the image name
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)
// Create an image element to display the uploaded image
let imageElement = document.createElement("img");
if (isVideoUpload) {
// 视频
imageElement = document.createElement('video');
imageElement.setAttribute('controls', true)
let [subfolder, name] = data.inputs.video.split('/');
// console.log(subfolder,name)
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;
// imageElement.innerHTML=`<img src="${base64Df}"/>`
} else if (data.class_type === 'LoadImage') {
// 图片
let [subfolder, name] = data.inputs.image.split('/');
if (!name) {
subfolder = "";
name = data.inputs.image;
}
// imageElement.src = base64Df
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') {
// 多张base64 图片
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));
// 只有mask有输出,才有编辑功能
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];
// 创建一个 FileReader 对象
const reader = new FileReader();
// 读取文件并在读取完成后执行回调函数
reader.onloadend = async function () {
// 获取读取的文件内容,即 Blob 对象
const fileBlob = new Blob([reader.result], { type: file.type });
// console.log( file.type.split('/')[1])
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
if (data.class_type === 'LoadImagesToBatch') {
// 上传 ,转为base64
let base64 = await blobToBase64(fileBlob)
createBase64ImageForLoadImageToBatch(imageElement, data.id, base64)
} else {
//上传,返回url
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;
}
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
if (isVideoUpload) {
window._appData.data[data.id].inputs.video = name;
} else {
//更换option里的default image
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);
})
// imageElement.src = `${get_url()}/view?filename=${encodeURIComponent(data.inputs.image)}&type=${type}&subfolder=${subfolder}`;
if (data.class_type !== 'ImagesPrompt_') uploadContainer.appendChild(imageElement);
// Append the upload container to the main container
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]
};
// data.title == 'PromptSlide ♾️Mixlab' ? data.inputs.prompt_keyword : data.title
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)) {
// console.log('data.options',data.options)
// 滑块输入
let options = data.options || {
min: 0,
max: data.class_type === 'IntNumber' ? 6000 : 1,
};
let silde = createNumSlide(data.title,
data.inputs.number,
(v) => {
// console.log(data.id,window._appData.data[data.id])
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)) {
// Create a container for the upload control
const uploadContainer = document.createElement("div");
uploadContainer.className = 'card';
// Create a label for the upload control
const nameLabel = document.createElement("label");
nameLabel.textContent = data.title || "CLIPTextEncode: ";
uploadContainer.appendChild(nameLabel);
// Create an input field for the image name
const textInput = document.createElement("textarea");
// textInput.className=;
if (data.class_type == "PromptSimplification") {
textInput.value = data.inputs.prompt;
} else {
textInput.value = data.inputs.text;
};
// uploadImageInput.type = "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);
// autoResize(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) => {
// console.log(textInput.value)
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
}));
})
// Append the upload container to the main container
container.appendChild(uploadContainer);
}
// lora的输入支持
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();
// console.log(selectDom.value)
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);
}
// 加载音频 base64
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: {
// Main components
preview: true,
opacity: true,
hue: true,
// Input / output Options
interaction: {
hex: true,
rgba: true,
hsla: true,
hsva: true,
cmyk: true,
input: true,
// clear: true,
save: true,
cancel: true
}
}
});
pickr
.on('save', (color, instance) => {
try {
// console.log(color)
// window._appData.data[data.id].inputs.color.hex = color.toHEXA().toString();
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');
// Create a checkbox element
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
// Set the default value
checkbox.checked = defaultValue;
// Create a label element
var label = document.createElement('label');
label.innerHTML = labelText;
// Append the checkbox and label to the body or any other container element
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) {
}
// 输入div
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]) {
// label.innerHTML = ``
// 有备选的关键词
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.setAttribute('data-content',labelText);
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)
// 添加change事件监听器
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.className = 'card';
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.className = 'card';
div.style = `margin-top: 24px;`
// Create a label for the upload control
// const nameLabel = document.createElement("label");
// nameLabel.textContent = title;
// div.appendChild(nameLabel);
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);
// if(options[i].selected)
}
// 设置默认值
selectElement.value = defaultValue;
// console.log(defaultValue, options)
return selectElement
}
// 创建下拉选择 - 带说明
function createSelectWithOptions(title, options, defaultValue) {
const div = document.createElement("div");
div.className = 'card';
// Create a label for the upload control
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) {
// appData.input, appData.output, appData.seed, share, appData.link
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;
// rightDiv.style.height='70vh'
// rightDiv.style=`position: fixed;
// right: 0;
// top: 12px;flex:0.6`
// 创建标题
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.style.height = '98px';
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';
// seed 汇总
var seeds = document.createElement('details');
// seeds.textContent = 'Status';
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') {
// console.log('###fiex',1)
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(status);
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'
// 将所有UI元素添加到页面中
// leftDiv.appendChild(titleDiv);
// leftDiv.appendChild(iconDes);
// leftDiv.appendChild(des);
// leftDiv.appendChild(statusDiv);
leftDiv.appendChild(input1);
submitDivBtn.appendChild(submitButton);
if (typeof (data.data) == 'object') mainDiv.appendChild(submitDiv);
rightDiv.appendChild(output);
// mainDiv.appendChild(titleDiv);
// mainDiv.appendChild(iconDes);
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'
})
// 返回每个UI元素的引用和对应的更新方法
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';
// 清空
// Array.from(imgDiv.parentElement.querySelectorAll('.output_images'), im => im.remove());
for (const v of val) {
let url = v, prompt = ''
if (type == 'images_prompts') {
// 是个数组,多了对应的prompt
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;
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.appendChild(a);
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;
// 3d meshes
if (val && type == 'meshes' && output.querySelector(`#output_${id}`)) {
let threeD = output.querySelector('.threeD')
//判断默认的图片,需要去掉后创建model-viewer
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)
// output.insertBefore(threeD, output.firstChild);
};
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) {
// 创建一个div元素
var div = document.createElement('div');
div.className = 'upload_btn card'
div.textContent = '上传并运行你的JSON文件';
div.addEventListener('click', function () {
document.getElementById('jsonFileInput').click();
});
// 创建一个input元素
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);
// res(jsonData);
};
reader.readAsText(file);
});
// 将div和input元素添加到body中
// document.body.appendChild(div);
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
})
}
// if (!enabled) return;
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;
// 3d模型
const meshes = detail?.output?.mesh;
if (images) {
// if (!images) return;
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 = [];
// 支持图片的batch
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]) {
// if (!images) return;
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) {
// #ClipInterrogator: ……
show(`${prompt.join('\n\n')}\n${JSON.stringify(analysis, null, 2)}`, detail.node, 'text')
}
}
async function createApp(appData, share = true) {
// console.log(appData)
// 使用示例:
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") => {
// console.log(src)
ui.output.update(type, src, id)
};
// 暴露给history使用
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) {
}
// console.log(Running, Pending);
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("b_preview", ({ detail }) => {
// // if (!enabled) return;
// console.log("b_preview", detail)
// show(URL.createObjectURL(detail));
// });
api.addEventListener("execution_error", ({ detail }) => {
console.log("execution_error", detail)
window.parent.postMessage({ cmd: 'status', data: `execution_error:${JSON.stringify(detail)}` }, '*');
// show(URL.createObjectURL(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();
// 外挂的UI
createAllColorInput();
const lightbox = new PhotoSwipeLightbox({
gallery: '.output_card',
children: 'a',
pswpModule: () => import('/extensions/comfyui-mixlab-nodes/lib/photoswipe.esm.min.js'),
// appendToEl: document.querySelector('#result')
});
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) {
// get caption from element with class hidden-caption-content
captionHTML = hiddenCaption.innerHTML
} else {
// get caption from alt attribute
captionHTML = currSlideElement
.querySelector('img')
.getAttribute('alt')
}
}
el.innerHTML = captionHTML || ''
})
}
})
})
lightbox.init();
// window._lightbox = lightbox
// lightbox.addFilter('preventPointerEvent', (preventPointerEvent, originalEvent, pointerType) => {
// // return true to preventDefault pointermove/pointerdown events
// // (also applies to touchmove/mousemove)
// return true;
// });
// author信息
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>`
}
}
// 创建app的选择菜单
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)
}
})
// console.log(div)
};
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) {
// console.log('details被展开了');
// 在这里执行展开后的回调操作
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 {
// console.log('details被收起了');
// 在这里执行收起后的回调操作
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();
// console.log(c)
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();
// 支持内嵌app
function checkIsInnerApp() {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const innerApp = params.get("innerApp");
// console.log(window.location.href, innerApp == 1, document.body);
if (innerApp == 1) {
document.body.querySelector('.header').style.display = 'none';
window.parent.postMessage({ innerApp, cmd: 'init' }, '*');
// 在iframe中监听来自父窗口的消息
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);
// await createHistoryList();
createApp(window._appData);
} else {
// todo welcome页面
document.body.innerHTML = `<h3 style="padding: 99px;">Welcome to Mixlab Nodes App!</h3>`
}
});
}
return innerApp == 1
}
</script>
<!-- <script src="/extensions/comfyui-mixlab-nodes/lib/login.js"></script> -->
</body>
</html>