| import { app } from '../../../scripts/app.js' |
| import { api } from '../../../scripts/api.js' |
| import { ComfyWidgets } from '../../../scripts/widgets.js' |
| import { $el } from '../../../scripts/ui.js' |
|
|
| import PhotoSwipeLightbox from '/extensions/comfyui-mixlab-nodes/lib/photoswipe-lightbox.esm.min.js' |
| function loadCSS (url) { |
| var link = document.createElement('link') |
| link.rel = 'stylesheet' |
| link.type = 'text/css' |
| link.href = url |
| document.getElementsByTagName('head')[0].appendChild(link) |
|
|
| |
| const style = document.createElement('style') |
| |
| const cssRule = `.pswp__custom-caption { |
| background: rgb(20 27 70); |
| font-size: 16px; |
| color: #fff; |
| width: calc(100% - 32px); |
| max-width: 980px; |
| 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; |
| }` |
| |
| style.appendChild(document.createTextNode(cssRule)) |
|
|
| |
| document.head.appendChild(style) |
| } |
| loadCSS('/extensions/comfyui-mixlab-nodes/lib/photoswipe.min.css') |
|
|
| function initLightBox () { |
| const lightbox = new PhotoSwipeLightbox({ |
| gallery: '.prompt_image_output', |
| children: 'a', |
| pswpModule: () => |
| import('/extensions/comfyui-mixlab-nodes/lib/photoswipe.esm.min.js') |
| }) |
|
|
| lightbox.on('uiRegister', function () { |
| lightbox.pswp.ui.registerElement({ |
| name: 'custom-caption', |
| order: 9, |
| isButton: false, |
| appendTo: 'root', |
| html: 'Caption text', |
| onInit: (el, pswp) => { |
| lightbox.pswp.on('change', () => { |
| const currSlideElement = lightbox.pswp.currSlide.data.element |
| let captionHTML = '' |
| if (currSlideElement) { |
| const hiddenCaption = currSlideElement.querySelector( |
| '.hidden-caption-content' |
| ) |
| if (hiddenCaption) { |
| |
| captionHTML = hiddenCaption.innerHTML |
| } else { |
| |
| captionHTML = currSlideElement |
| .querySelector('img') |
| .getAttribute('alt') |
| } |
| } |
| el.innerHTML = captionHTML || '' |
| }) |
| } |
| }) |
| }) |
|
|
| lightbox.init() |
| } |
|
|
| function get_position_style (ctx, widget_width, y, node_height) { |
| const MARGIN = 4 |
|
|
| |
| 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 - 24}px`, |
| |
| |
| paddingLeft: '12px', |
| display: 'flex', |
| flexDirection: 'row', |
| |
| justifyContent: 'space-between' |
| } |
| } |
| function createImage (url) { |
| let im = new Image() |
| return new Promise((res, rej) => { |
| im.onload = () => res(im) |
| im.src = url |
| }) |
| } |
|
|
| async function fetchImage (url) { |
| try { |
| const response = await fetch(url) |
| const blob = await response.blob() |
|
|
| return blob |
| } catch (error) { |
| console.error('出现错误:', error) |
| } |
| } |
|
|
| const getLocalData = key => { |
| let data = {} |
| try { |
| data = JSON.parse(localStorage.getItem(key)) || {} |
| } catch (error) { |
| return {} |
| } |
| return data |
| } |
|
|
| const setLocalDataOfWin = (key, value) => { |
| localStorage.setItem(key, JSON.stringify(value)) |
| |
| } |
|
|
| const createSelect = (select, opts, targetWidget) => { |
| select.style.display = 'block' |
| let html = '' |
| let isMatch = false |
| for (const opt of opts) { |
| html += `<option value='${opt}' ${ |
| targetWidget.value === opt ? 'selected' : '' |
| }>${opt}</option>` |
| if (targetWidget.value === opt) isMatch = true |
| } |
| select.innerHTML = html |
| if (!isMatch) targetWidget.value = opts[0] |
| |
| select.addEventListener('change', function () { |
| |
| var selectedOption = select.options[select.selectedIndex].value |
| targetWidget.value = selectedOption |
| |
| }) |
| } |
|
|
| app.registerExtension({ |
| name: 'Mixlab.prompt.RandomPrompt', |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeType.comfyClass == 'RandomPrompt') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
| nodeType.prototype.onNodeCreated = async function () { |
| orig_nodeCreated?.apply(this, arguments) |
| |
| const mutable_prompt = this.widgets.filter( |
| w => w.name == 'mutable_prompt' |
| )[0] |
| |
|
|
| const widget = { |
| type: 'div', |
| name: 'upload', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign( |
| this.div.style, |
| get_position_style(ctx, widget_width, y, node.size[1]) |
| ) |
| } |
| } |
|
|
| widget.div = $el('div', {}) |
|
|
| const btn = document.createElement('button') |
| btn.innerText = 'Upload Keywords' |
|
|
| btn.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; |
| ` |
|
|
| |
| |
| btn.addEventListener('click', () => { |
| let inp = document.createElement('input') |
| inp.type = 'file' |
| inp.accept = '.txt' |
| inp.click() |
| inp.addEventListener('change', event => { |
| |
| const file = event.target.files[0] |
| this.title = file.name.split('.')[0] |
|
|
| |
| |
| const reader = new FileReader() |
|
|
| |
| reader.onload = event => { |
| |
| const fileContent = event.target.result.split('\n') |
| const keywords = Array.from(fileContent, f => f.trim()).filter( |
| f => f |
| ) |
| |
| |
|
|
| mutable_prompt.value = keywords.join('\n') |
|
|
| inp.remove() |
| } |
|
|
| |
| reader.readAsText(file) |
| }) |
| }) |
|
|
| widget.div.appendChild(btn) |
| document.body.appendChild(widget.div) |
| this.addCustomWidget(widget) |
|
|
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| widget.div.remove() |
| return onRemoved?.() |
| } |
|
|
| if (this.onResize) { |
| this.onResize(this.size) |
| } |
|
|
| this.serialize_widgets = true |
| } |
| } |
| }, |
| async loadedGraphNode (node, app) { |
| if (node.type === 'RandomPrompt') { |
| |
| } |
| } |
| }) |
|
|
| app.registerExtension({ |
| name: 'Mixlab.prompt.PromptSlide', |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeType.comfyClass == 'PromptSlide') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
| nodeType.prototype.onNodeCreated = async function () { |
| orig_nodeCreated?.apply(this, arguments) |
|
|
| const prompt_keyword = this.widgets.filter( |
| w => w.name == 'prompt_keyword' |
| )[0] |
| |
|
|
| const widget = { |
| type: 'div', |
| name: 'upload', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign( |
| this.div.style, |
| get_position_style(ctx, widget_width, y, node.size[1]) |
| ) |
| } |
| } |
|
|
| widget.div = $el('div', {}) |
|
|
| const btn = document.createElement('button') |
| btn.innerText = 'Upload Keywords' |
|
|
| btn.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; |
| ` |
|
|
| const select = document.createElement('select') |
| select.style = `display:none;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: 100px; |
| ` |
| widget.select = select |
|
|
| |
| |
| btn.addEventListener('click', () => { |
| let inp = document.createElement('input') |
| inp.type = 'file' |
| inp.accept = '.txt' |
| inp.click() |
| inp.addEventListener('change', event => { |
| |
| const file = event.target.files[0] |
| this.title = file.name.split('.')[0] |
|
|
| |
| |
| const reader = new FileReader() |
|
|
| |
| reader.onload = event => { |
| |
| const fileContent = event.target.result.split('\n') |
| const keywords = Array.from(fileContent, f => f.trim()).filter( |
| f => f |
| ) |
| |
| |
|
|
| widget.value = JSON.stringify(keywords) |
|
|
| |
| |
| |
|
|
| createSelect(select, keywords, prompt_keyword) |
|
|
| inp.remove() |
| } |
|
|
| |
| reader.readAsText(file) |
| }) |
| }) |
|
|
| widget.div.appendChild(btn) |
| widget.div.appendChild(select) |
| document.body.appendChild(widget.div) |
| this.addCustomWidget(widget) |
|
|
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| widget.div.remove() |
| return onRemoved?.() |
| } |
|
|
| if (this.onResize) { |
| this.onResize(this.size) |
| } |
|
|
| this.serialize_widgets = true |
| } |
| } |
| }, |
| async loadedGraphNode (node, app) { |
| if (node.type === 'PromptSlide') { |
| try { |
| let prompt = node.widgets.filter(w => w.name === 'prompt_keyword')[0] |
| |
| let uploadWidget = node.widgets.filter(w => w.name == 'upload')[0] |
| |
| let keywords = JSON.parse(uploadWidget.value) |
| |
| let widget = node.widgets.filter(w => w.select)[0] |
| if (keywords && keywords[0]) { |
| widget.select.style.display = 'block' |
| createSelect(widget.select, keywords, prompt) |
| } |
| } catch (error) {} |
| } |
| } |
| }) |
|
|
| const _createResult = async (node, widget, message) => { |
| widget.div.innerHTML = `` |
|
|
| const width = node.size[0] * 0.5 - 12 |
|
|
| let height_add = 0 |
| |
| for (let index = 0; index < message._images.length; index++) { |
| const imgs = message._images[index] |
|
|
| for (const img of imgs) { |
| let url = api.apiURL( |
| `/view?filename=${encodeURIComponent(img.filename)}&type=${ |
| img.type |
| }&subfolder=${ |
| img.subfolder |
| }${app.getPreviewFormatParam()}${app.getRandParam()}` |
| ) |
|
|
| let image = await createImage(url) |
|
|
| |
| let div = document.createElement('div') |
| div.className = 'card' |
| div.draggable = true |
|
|
| div.ondragend = async event => { |
| console.log('拖动停止') |
| let url = div.querySelector('img').src |
|
|
| let blob = await fetchImage(url) |
|
|
| let imageNode = null |
| |
| if (!imageNode) { |
| const newNode = LiteGraph.createNode('LoadImage') |
| newNode.pos = [...app.canvas.graph_mouse] |
| imageNode = app.graph.add(newNode) |
| app.graph.change() |
| } |
|
|
| |
| imageNode.pasteFile(blob) |
| } |
|
|
| div.setAttribute('data-scale', image.naturalHeight / image.naturalWidth) |
|
|
| let h = (image.naturalHeight * width) / image.naturalWidth |
| if (index % 2 === 0) height_add += h |
| div.style = `width: ${width}px;height:${h}px;position: relative;margin: 4px;` |
|
|
| div.innerHTML = `<a href="${url}" |
| data-pswp-width="${image.naturalWidth}" |
| data-pswp-height="${image.naturalHeight}" |
| target="_blank"> |
| <img src="${url}" style='width: 100%' alt="${message.prompts[index]}"/> |
| </a> |
| <p style="position: absolute; |
| bottom: 0; |
| left: 0; |
| opacity: 0.6; |
| background-color: var(--comfy-input-bg); |
| color: var(--descrip-text); |
| margin: 0; |
| font-size: 12px; |
| padding: 5px; |
| text-align: left;">${message.prompts[index]}</p>` |
| widget.div.appendChild(div) |
| } |
| } |
|
|
| node.size[1] = 98 + height_add |
| } |
|
|
| app.registerExtension({ |
| name: 'Mixlab.prompt.PromptImage', |
|
|
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeType.comfyClass == 'PromptImage') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
| nodeType.prototype.onNodeCreated = function () { |
| orig_nodeCreated?.apply(this, arguments) |
| console.log('#orig_nodeCreated', this) |
| const widget = { |
| type: 'div', |
| name: 'result', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign(this.div.style, { |
| ...get_position_style(ctx, widget_width, y, node.size[1]), |
| flexWrap: 'wrap', |
| justifyContent: 'space-between', |
| |
| paddingLeft: '0px', |
| width: widget_width + 'px' |
| }) |
| } |
| } |
|
|
| widget.div = $el('div', {}) |
| widget.div.className = 'prompt_image_output' |
|
|
| document.body.appendChild(widget.div) |
|
|
| this.addCustomWidget(widget) |
|
|
| initLightBox() |
|
|
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| widget.div.remove() |
| return onRemoved?.() |
| } |
|
|
| const onResize = this.onResize |
| this.onResize = function () { |
| |
| |
| let w = this.size[0] * 0.5 - 12 |
| Array.from(widget.div.querySelectorAll('.card'), card => { |
| card.style.width = `${w}px` |
| card.style.height = `${ |
| w * parseFloat(card.getAttribute('data-scale')) |
| }px` |
| }) |
| return onResize?.apply(this, arguments) |
| } |
|
|
| |
| } |
|
|
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = async function (message) { |
| onExecuted?.apply(this, arguments) |
| console.log('#PromptImage', message.prompts, message._images) |
| |
| try { |
| let widget = this.widgets.filter(w => w.name === 'result')[0] |
| widget.value = message |
| _createResult(this, widget, { ...message }) |
| } catch (error) { |
| console.log(error) |
| } |
| } |
|
|
| this.serialize_widgets = true |
| } |
| }, |
| async loadedGraphNode (node, app) { |
| if (node.type === 'PromptImage') { |
| |
| let widget = node.widgets.filter(w => w.name === 'result')[0] |
| console.log('widget.value', widget.value) |
|
|
| initLightBox() |
|
|
| let cards = widget.div.querySelectorAll('.card') |
| if (cards.length == 0) node.size = [280, 120] |
| if(widget.value) _createResult(node, widget, widget.value) |
| } |
| } |
| }) |
|
|