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