| 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) |
| } |
| } |
| } |
| }) |
|
|