|
import { app } from '../../../scripts/app.js' |
|
import { $el } from '../../../scripts/ui.js' |
|
import { api } from '../../../scripts/api.js' |
|
|
|
import { td_bg } from './td_background.js' |
|
console.log('td_bg', td_bg) |
|
|
|
window._nodesAll = null |
|
|
|
|
|
function getObjectInfo () { |
|
return new Promise(async (resolve, reject) => { |
|
let url = getUrl() |
|
|
|
try { |
|
const response = await fetch(`${url}/object_info`) |
|
const data = await response.json() |
|
resolve(data) |
|
} catch (error) { |
|
reject(error) |
|
} |
|
}) |
|
} |
|
|
|
const base64Df = |
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
|
|
|
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 get_position_style (ctx, widget_width, y, node_height) { |
|
const MARGIN = 12 |
|
|
|
|
|
const elRect = ctx.canvas.getBoundingClientRect() |
|
const transform = new DOMMatrix() |
|
.scaleSelf( |
|
elRect.width / ctx.canvas.width, |
|
elRect.height / ctx.canvas.height |
|
) |
|
.multiplySelf(ctx.getTransform()) |
|
.translateSelf(MARGIN, MARGIN + y) |
|
|
|
return { |
|
transformOrigin: '0 0', |
|
transform: transform, |
|
left: `0`, |
|
top: `0`, |
|
cursor: 'pointer', |
|
position: 'absolute', |
|
maxWidth: `${widget_width - MARGIN * 2}px`, |
|
|
|
width: `${widget_width - MARGIN * 2}px`, |
|
|
|
|
|
display: 'flex', |
|
flexDirection: 'column', |
|
|
|
justifyContent: 'flex-start', |
|
zIndex: 9999999 |
|
} |
|
} |
|
|
|
async function drawImageToCanvas (imageUrl, sFactor = 320) { |
|
var canvas = document.createElement('canvas') |
|
var ctx = canvas.getContext('2d') |
|
var img = new Image() |
|
|
|
await new Promise((resolve, reject) => { |
|
img.onload = function () { |
|
var scaleFactor = sFactor / img.width |
|
var canvasWidth = img.width * scaleFactor |
|
var canvasHeight = img.height * scaleFactor |
|
|
|
canvas.width = canvasWidth |
|
canvas.height = canvasHeight |
|
|
|
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight) |
|
|
|
resolve() |
|
} |
|
|
|
img.onerror = function () { |
|
reject(new Error('Failed to load image')) |
|
} |
|
|
|
img.src = imageUrl |
|
}) |
|
|
|
var base64 = canvas.toDataURL('image/jpeg') |
|
|
|
return base64 |
|
|
|
} |
|
|
|
async function extractInputAndOutputData ( |
|
jsonData, |
|
inputIds = [], |
|
outputIds = [] |
|
) { |
|
|
|
|
|
|
|
|
|
const data = jsonData.output |
|
let input = [] |
|
let output = [] |
|
const seed = {} |
|
const seedTitle = {} |
|
|
|
for (const id in data) { |
|
if (data.hasOwnProperty(id)) { |
|
let node = app.graph.getNodeById(id) |
|
if (inputIds.includes(id)) { |
|
|
|
let options = {} |
|
|
|
try { |
|
if (node.type === 'CheckpointLoaderSimple') { |
|
options = node.widgets.filter(w => w.name === 'ckpt_name')[0] |
|
.options.values |
|
} else if (node.type === 'LoraLoader') { |
|
options = node.widgets.filter(w => w.name === 'lora_name')[0] |
|
.options.values |
|
} |
|
} catch (error) {} |
|
|
|
if (node.type == 'IntNumber' || node.type == 'FloatSlider') { |
|
|
|
let [v, min, max, step] = Array.from(node.widgets, w => w.value) |
|
options = { min, max, step } |
|
|
|
} |
|
|
|
if (node.type == 'PromptSlide') { |
|
|
|
options = node.widgets.filter(w => w.type === 'slider')[0].options |
|
|
|
try { |
|
let keywords = node.widgets.filter(w => w.name === 'upload')[0] |
|
.value |
|
keywords = JSON.parse(keywords) |
|
options.keywords = keywords |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} |
|
|
|
if (node.type == 'ImagesPrompt_') { |
|
|
|
|
|
let image_base64 = data[id].inputs.image_base64 |
|
let img_index = 0 |
|
let imgsData = JSON.parse(data[id].inputs.upload) |
|
for (let index = 0; index < imgsData.length; index++) { |
|
const imgd = imgsData[index].imgurl |
|
imgsData[index].index = index |
|
|
|
imgsData[index].imgurl = await parseImageToBase64(imgd) |
|
if (image_base64 == imgsData[index].imgurl) { |
|
img_index = index |
|
} |
|
} |
|
options.images = imgsData |
|
delete data[id].inputs.upload |
|
delete data[id].inputs.image_base64 |
|
|
|
data[id].inputs.imageIndex = img_index |
|
} |
|
|
|
if (node.type == 'Color') { |
|
} |
|
|
|
|
|
if (node.type == 'LoadAndCombinedAudio_') { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
input[inputIds.indexOf(id)] = { |
|
...data[id], |
|
title: node.title, |
|
id, |
|
options |
|
} |
|
} |
|
|
|
if (node.type === 'LoadImage') { |
|
|
|
let output = node.outputs.filter(ot => ot.type == 'MASK')[0] |
|
if (output.links) { |
|
|
|
options.hasMask = true |
|
} |
|
|
|
let imgurl = app.graph.getNodeById(id).imgs[0].src + '&channel=rgb' |
|
|
|
options.defaultImage = await drawImageToCanvas(imgurl, 512) |
|
console.log('#loadImage的默认图', options) |
|
} |
|
|
|
input[inputIds.indexOf(id)] = { |
|
...data[id], |
|
title: node.title, |
|
id, |
|
options |
|
} |
|
|
|
} |
|
if (outputIds.includes(id)) { |
|
let options = {} |
|
|
|
if ( |
|
node.type === 'SaveImageAndMetadata_' && |
|
app.graph.getNodeById(id).imgs |
|
) { |
|
|
|
let imgurl = app.graph.getNodeById(id).imgs[0].src |
|
|
|
options.defaultImage = await drawImageToCanvas(imgurl, 512) |
|
console.log('#SaveImageAndMetadata_的默认图', options) |
|
} |
|
|
|
|
|
|
|
output[outputIds.indexOf(id)] = { |
|
...data[id], |
|
title: node.title, |
|
id, |
|
options |
|
} |
|
} |
|
|
|
if ( |
|
node.type === 'KSampler' || |
|
node.type == 'SamplerCustom' || |
|
node.type === 'ChinesePrompt_Mix' || |
|
node.type === 'Seed_' |
|
) { |
|
|
|
try { |
|
seed[id] = node.widgets.filter( |
|
w => w.name === 'seed' || w.name == 'noise_seed' |
|
)[0].linkedWidgets[0].value |
|
seedTitle[id] = node.title |
|
} catch (error) {} |
|
} |
|
} |
|
} |
|
|
|
|
|
input = input.filter(i => i) |
|
output = output.filter(i => i) |
|
|
|
return { input, output, seed, seedTitle } |
|
} |
|
|
|
function getUrl () { |
|
let api_host = `${window.location.hostname}:${window.location.port}` |
|
let api_base = '' |
|
let url = `${window.location.protocol}//${api_host}${api_base}` |
|
return url |
|
} |
|
|
|
const getLocalData = key => { |
|
let data = {} |
|
try { |
|
data = JSON.parse(localStorage.getItem(key)) || {} |
|
} catch (error) { |
|
return {} |
|
} |
|
return data |
|
} |
|
|
|
async function save_app (json) { |
|
let url = getUrl() |
|
|
|
const res = await fetch(`${url}/mixlab/workflow`, { |
|
method: 'POST', |
|
body: JSON.stringify({ |
|
data: json, |
|
task: 'save_app', |
|
filename: json.app.filename, |
|
category: json.app.category |
|
}) |
|
}) |
|
return await res.json() |
|
} |
|
|
|
function downloadJsonFile (jsonData, fileName = 'mix_app.json') { |
|
const dataString = JSON.stringify(jsonData) |
|
const blob = new Blob([dataString], { type: 'application/json' }) |
|
const url = URL.createObjectURL(blob) |
|
|
|
const link = document.createElement('a') |
|
link.href = url |
|
link.download = fileName |
|
link.click() |
|
|
|
|
|
setTimeout(() => { |
|
URL.revokeObjectURL(url) |
|
}, 0) |
|
} |
|
|
|
async function save (json, download = false, showInfo = true) { |
|
let nodesAll = window._nodesAll || (await getObjectInfo()) |
|
|
|
console.log('####SAVE', nodesAll, json[0]) |
|
|
|
const name = json[0], |
|
version = json[5], |
|
share_prefix = json[6], |
|
link = json[7], |
|
category = json[8] || '', |
|
description = json[4], |
|
inputIds = json[2].split('\n').filter(f => f), |
|
outputIds = json[3].split('\n').filter(f => f) |
|
|
|
const iconData = json[1][0] |
|
let { filename, subfolder, type } = iconData |
|
let iconUrl = api.apiURL( |
|
`/view?filename=${encodeURIComponent( |
|
filename |
|
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` |
|
) |
|
|
|
try { |
|
let data = await app.graphToPrompt() |
|
|
|
|
|
data.nodesMap = {} |
|
for (const id in data.output) { |
|
data.nodesMap[data.output[id].class_type] = |
|
nodesAll[data.output[id].class_type] |
|
} |
|
|
|
let { input, output, seed, seedTitle } = await extractInputAndOutputData( |
|
data, |
|
inputIds, |
|
outputIds |
|
) |
|
|
|
let authorAvatar = |
|
localStorage.getItem('_mixlab_author_avatar') || base64Df, |
|
authorName = |
|
localStorage.getItem('_mixlab_author_name') || |
|
localStorage.getItem('Comfy.userName'), |
|
authorLink = localStorage.getItem('_mixlab_author_link') || '' |
|
|
|
data.app = { |
|
name, |
|
description, |
|
version, |
|
input, |
|
output, |
|
seed, |
|
seedTitle, |
|
share_prefix, |
|
link, |
|
category, |
|
filename: `${name}_${version}.json`, |
|
author: { |
|
avatar: authorAvatar, |
|
name: authorName, |
|
link: authorLink |
|
} |
|
} |
|
|
|
try { |
|
data.app.icon = await drawImageToCanvas(iconUrl) |
|
} catch (error) {} |
|
|
|
|
|
await save_app(data) |
|
if (download) { |
|
await downloadJsonFile(data, data.app.filename) |
|
} |
|
|
|
if (showInfo) { |
|
let open = window.confirm( |
|
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent( |
|
data.app.filename |
|
)}&category=${encodeURIComponent(data.app.category)}` |
|
) |
|
if (open) |
|
window.open( |
|
`${getUrl()}/mixlab/app?filename=${encodeURIComponent( |
|
data.app.filename |
|
)}&category=${encodeURIComponent(data.app.category)}` |
|
) |
|
} |
|
} catch (error) { |
|
console.log('###error', error) |
|
} |
|
} |
|
|
|
function getInputsAndOutputs () { |
|
const inputs = |
|
`LoadImage LoadImagesToBatch ImagesPrompt_ LoadAndCombinedAudio_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split( |
|
' ' |
|
), |
|
outputs = |
|
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,CombineAudioVideo,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split( |
|
',' |
|
) |
|
|
|
let inputsId = [], |
|
outputsId = [] |
|
|
|
for (let node of app.graph._nodes) { |
|
if (inputs.includes(node.type)) { |
|
inputsId.push(node.id) |
|
} |
|
|
|
if (outputs.includes(node.type)) { |
|
outputsId.push(node.id) |
|
} |
|
} |
|
|
|
return { |
|
input: inputsId, |
|
output: outputsId |
|
} |
|
} |
|
|
|
app.registerExtension({ |
|
name: 'Mixlab.utils.AppInfo', |
|
init () { |
|
if (!window._nodesAll) { |
|
getObjectInfo().then(r => (window._nodesAll = r)) |
|
} |
|
}, |
|
async beforeRegisterNodeDef (nodeType, nodeData, app) { |
|
if (nodeType.comfyClass == 'AppInfo') { |
|
const orig_nodeCreated = nodeType.prototype.onNodeCreated |
|
nodeType.prototype.onNodeCreated = function () { |
|
orig_nodeCreated?.apply(this, arguments) |
|
|
|
|
|
|
|
let input_ids = this.widgets.filter(w => w.name == 'input_ids')[0], |
|
output_ids = this.widgets.filter(w => w.name == 'output_ids')[0] |
|
|
|
const { input, output } = getInputsAndOutputs() |
|
input_ids.value = input.join('\n') |
|
output_ids.value = output.join('\n') |
|
|
|
const widget = { |
|
type: 'div', |
|
name: 'AppInfoRun', |
|
draw (ctx, node, widget_width, y, widget_height) { |
|
Object.assign( |
|
this.div.style, |
|
get_position_style( |
|
ctx, |
|
widget_width, |
|
node.size[1] - widget_height, |
|
node.size[1] |
|
) |
|
) |
|
} |
|
} |
|
|
|
const style = ` |
|
flex-direction: row; |
|
background-color: var(--comfy-input-bg); |
|
border-radius: 8px; |
|
border-color: var(--border-color); |
|
border-style: solid; |
|
color: var(--descrip-text);` |
|
|
|
widget.div = $el('div', {}) |
|
|
|
const btn = document.createElement('button') |
|
btn.innerText = 'Save & Open' |
|
btn.style = style |
|
|
|
btn.addEventListener('click', () => { |
|
|
|
if (window._mixlab_app_json) { |
|
save(window._mixlab_app_json) |
|
} else { |
|
alert('Please run the workflow before saving') |
|
|
|
this.widgets.filter(w => w.name === 'version')[0].value += 1 |
|
} |
|
}) |
|
|
|
const download = document.createElement('button') |
|
download.innerText = 'Download For App' |
|
download.style = style |
|
download.style.marginLeft = '12px' |
|
|
|
download.addEventListener('click', () => { |
|
|
|
if (window._mixlab_app_json) { |
|
save(window._mixlab_app_json, true) |
|
} else { |
|
alert('Please run the workflow before saving') |
|
|
|
this.widgets.filter(w => w.name === 'version')[0].value += 1 |
|
} |
|
}) |
|
|
|
|
|
const tdBG = document.createElement('button') |
|
tdBG.innerText = 'Canvas Mode' |
|
tdBG.style = style |
|
tdBG.style.marginLeft = '12px' |
|
|
|
tdBG.addEventListener('click', () => { |
|
td_bg.toggle() |
|
if (td_bg.running) { |
|
tdBG.style.background = 'yellow' |
|
} else { |
|
tdBG.style.background = 'transparent' |
|
} |
|
}) |
|
|
|
|
|
let author = document.createElement('div') |
|
|
|
|
|
let authorAvatar = document.createElement('img') |
|
authorAvatar.className = `${'comfy-multiline-input'}` |
|
authorAvatar.style = `outline: none; |
|
border: none; |
|
padding: 4px; |
|
width: 32px; |
|
cursor: pointer; |
|
height: 32px;` |
|
|
|
if (localStorage.getItem('_mixlab_author_avatar')) { |
|
authorAvatar.src = |
|
localStorage.getItem('_mixlab_author_avatar') || base64Df |
|
} |
|
|
|
let authorAvatarUpload = document.createElement('input') |
|
authorAvatarUpload.type = 'file' |
|
authorAvatarUpload.style = `display:none` |
|
|
|
let authorAvatarInput = document.createElement('div') |
|
authorAvatarInput.style = `display: flex;justify-content: flex-start; |
|
align-items: center;` |
|
let authorAvatarInputLabel = document.createElement('p') |
|
authorAvatarInputLabel.innerText = 'Author Avatar' |
|
authorAvatarInputLabel.className = `${'comfy-multiline-input'}` |
|
authorAvatarInputLabel.style = `font-size:12px` |
|
|
|
authorAvatar.addEventListener('click', e => { |
|
authorAvatarUpload.click() |
|
}) |
|
|
|
authorAvatarInputLabel.addEventListener('click', e => { |
|
authorAvatarUpload.click() |
|
}) |
|
|
|
authorAvatarUpload.addEventListener('change', event => { |
|
const file = event.target.files[0] |
|
const reader = new FileReader() |
|
|
|
reader.onload = async e => { |
|
let im = new Image() |
|
im.src = e.target.result |
|
authorAvatar.src = e.target.result |
|
im.onload = () => { |
|
let c = document.createElement('canvas') |
|
let ctx = c.getContext('2d') |
|
c.width = 72 |
|
c.height = 72 |
|
ctx.drawImage( |
|
im, |
|
0, |
|
0, |
|
im.naturalWidth, |
|
im.naturalHeight, |
|
0, |
|
0, |
|
c.width, |
|
c.height |
|
) |
|
window._mixlab_author_avatar = c.toDataURL() |
|
localStorage.setItem( |
|
'_mixlab_author_avatar', |
|
window._mixlab_author_avatar |
|
) |
|
} |
|
} |
|
|
|
|
|
reader.readAsDataURL(file) |
|
}) |
|
|
|
author.appendChild(authorAvatarInput) |
|
authorAvatarInput.appendChild(authorAvatarInputLabel) |
|
authorAvatarInput.appendChild(authorAvatar) |
|
authorAvatarInput.appendChild(authorAvatarUpload) |
|
|
|
let authorName = document.createElement('input') |
|
authorName.type = 'text' |
|
authorName.value = |
|
localStorage.getItem('_mixlab_author_name') || |
|
localStorage.getItem('Comfy.userName') |
|
authorName.placeholder = 'author name' |
|
authorName.className = `${'comfy-multiline-input'}` |
|
authorName.style = ` |
|
outline: none; |
|
border: none; |
|
padding: 4px; |
|
width: 100%; |
|
cursor: pointer; |
|
height: 32px;` |
|
|
|
let authorNameInput = document.createElement('div') |
|
authorNameInput.style = `display: flex;justify-content: flex-start; |
|
align-items: center;` |
|
let authorNameInputLabel = document.createElement('p') |
|
authorNameInputLabel.innerText = 'Author Name' |
|
authorNameInputLabel.className = `${'comfy-multiline-input'}` |
|
authorNameInputLabel.style = `font-size:12px;width: 110px` |
|
|
|
authorName.addEventListener('change', e => { |
|
window._mixlab_author_name = authorName.value.trim() |
|
localStorage.setItem( |
|
'_mixlab_author_name', |
|
window._mixlab_author_name |
|
) |
|
}) |
|
|
|
author.appendChild(authorNameInput) |
|
authorNameInput.appendChild(authorNameInputLabel) |
|
authorNameInput.appendChild(authorName) |
|
|
|
|
|
let authorLink = document.createElement('input') |
|
authorLink.type = 'text' |
|
authorLink.value = localStorage.getItem('_mixlab_author_link') || '' |
|
authorLink.placeholder = 'author link' |
|
authorLink.className = `${'comfy-multiline-input'}` |
|
authorLink.style = ` |
|
outline: none; |
|
border: none; |
|
padding: 4px; |
|
width: 100%; |
|
cursor: pointer; |
|
height: 32px;` |
|
|
|
let authorLinkInput = document.createElement('div') |
|
authorLinkInput.style = `display: flex;justify-content: flex-start; |
|
align-items: center;` |
|
let authorLinkInputLabel = document.createElement('p') |
|
authorLinkInputLabel.innerText = 'Author Link' |
|
authorLinkInputLabel.className = `${'comfy-multiline-input'}` |
|
authorLinkInputLabel.style = `font-size:12px;width: 110px` |
|
|
|
authorLink.addEventListener('change', e => { |
|
window._mixlab_author_link = authorLink.value.trim() |
|
localStorage.setItem( |
|
'_mixlab_author_link', |
|
window._mixlab_author_link |
|
) |
|
}) |
|
|
|
author.appendChild(authorLinkInput) |
|
authorLinkInput.appendChild(authorLinkInputLabel) |
|
authorLinkInput.appendChild(authorLink) |
|
|
|
widget.div.appendChild(author) |
|
|
|
let btns = document.createElement('div') |
|
|
|
widget.div.appendChild(btns) |
|
|
|
btns.appendChild(btn) |
|
btns.appendChild(download) |
|
btns.appendChild(tdBG) |
|
|
|
document.body.appendChild(widget.div) |
|
this.addCustomWidget(widget) |
|
|
|
const onRemoved = this.onRemoved |
|
this.onRemoved = () => { |
|
widget.div.remove() |
|
return onRemoved?.() |
|
} |
|
|
|
this.serialize_widgets = true |
|
|
|
window._mixlab_app_json = null |
|
|
|
} |
|
|
|
const onExecuted = nodeType.prototype.onExecuted |
|
nodeType.prototype.onExecuted = function (message) { |
|
onExecuted?.apply(this, arguments) |
|
console.log(message.json) |
|
window._mixlab_app_json = message.json |
|
try { |
|
let a = this.widgets.filter(w => w.name === 'AppInfoRun')[0] |
|
if (a) { |
|
if (!a.value) a.value = 0 |
|
a.value += 1 |
|
} |
|
|
|
const div = this.widgets.filter(w => w.div)[0].div |
|
Array.from(div.querySelectorAll('button'), b => |
|
b.innerText != 'Canvas Mode' ? (b.style.background = 'yellow') : '' |
|
) |
|
} catch (error) {} |
|
} |
|
} |
|
}, |
|
async loadedGraphNode (node, app) { |
|
|
|
window._mixlab_app_json = null |
|
if (node.type === 'AppInfo') { |
|
let auto_save = node.widgets.filter(w => w.name == 'auto_save')[0] |
|
if (auto_save) { |
|
if (!['enable', 'disable'].includes(auto_save.value)) { |
|
auto_save.value = 'enable' |
|
} |
|
} |
|
|
|
|
|
|
|
} |
|
} |
|
}) |
|
|
|
api.addEventListener('execution_start', async ({ detail }) => { |
|
console.log('#execution_start', detail) |
|
window._mixlab_app_json = null |
|
}) |
|
|
|
api.addEventListener('executed', async ({ detail }) => { |
|
console.log('#executed', detail) |
|
|
|
const { output } = getInputsAndOutputs() |
|
if (output.includes(parseInt(detail.node))) { |
|
let appinfo = app.graph.findNodesByType('AppInfo')[0] |
|
if (appinfo) { |
|
let auto_save = appinfo.widgets.filter(w => w.name == 'auto_save')[0] |
|
if (auto_save?.value === 'enable') { |
|
|
|
console.log('auto_save') |
|
if (window._mixlab_app_json) save(window._mixlab_app_json, false, false) |
|
} |
|
} |
|
} |
|
}) |
|
|