| import { app } from '../../../scripts/app.js' |
| import { api } from '../../../scripts/api.js' |
| import { ComfyWidgets } from '../../../scripts/widgets.js' |
|
|
| import { $el } from '../../../scripts/ui.js' |
|
|
| |
|
|
| function injectCSS (css) { |
| |
| const existingStyle = document.querySelector('style') |
| if (existingStyle && existingStyle.textContent === css) { |
| return |
| } |
|
|
| |
| const style = document.createElement('style') |
| style.textContent = css |
|
|
| |
| const head = document.querySelector('head') |
| head.appendChild(style) |
| } |
|
|
| injectCSS(` |
| .hidden{ |
| display:none !important |
| }`) |
|
|
| 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}px`, |
| |
| |
| display: 'flex', |
| flexDirection: 'column', |
| |
| justifyContent: 'space-around' |
| } |
| } |
|
|
| function videoUpload (node, inputName, inputData, app) { |
| const imageWidget = node.widgets.find(w => w.name === 'video') |
| let uploadWidget |
|
|
| const widget = { |
| type: 'div', |
| name: 'upload-preview', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign( |
| this.div.style, |
| get_position_style(ctx, widget_width, 220, node.size[1]), |
| { |
| outline: '1px solid' |
| } |
| ) |
| } |
| } |
|
|
| widget.div = $el('div', {}) |
| widget.div.style.width = `120px` |
| document.body.appendChild(widget.div) |
| node.addCustomWidget(widget) |
| |
| const displayDiv = document.createElement('video') |
| displayDiv.controls = true |
| |
| imageWidget.callback = () => { |
| displayDiv.src = `/view?filename=${ |
| imageWidget.value |
| }&type=input&subfolder=${''}&rand=${Math.random()}` |
|
|
| |
| |
| |
| |
| |
| |
| } |
|
|
| if (imageWidget.value) { |
| |
| displayDiv.src = `/view?filename=${ |
| imageWidget.value |
| }&type=input&subfolder=${''}&rand=${Math.random()}` |
| } |
|
|
| widget.div.appendChild(displayDiv) |
|
|
| const onRemoved = node.onRemoved |
| node.onRemoved = () => { |
| widget.div.remove() |
| return onRemoved?.() |
| } |
|
|
| var default_value = imageWidget.value |
| Object.defineProperty(imageWidget, 'value', { |
| set: function (value) { |
| this._real_value = value |
| }, |
|
|
| get: function () { |
| let value = '' |
| if (this._real_value) { |
| value = this._real_value |
| } else { |
| return default_value |
| } |
|
|
| if (value.filename) { |
| let real_value = value |
| value = '' |
| if (real_value.subfolder) { |
| value = real_value.subfolder + '/' |
| } |
|
|
| value += real_value.filename |
|
|
| if (real_value.type && real_value.type !== 'input') |
| value += ` [${real_value.type}]` |
| } |
| return value |
| } |
| }) |
| async function uploadFile (file, updateNode, pasted = false) { |
| try { |
| |
| const body = new FormData() |
| body.append('image', file) |
| if (pasted) body.append('subfolder', 'pasted') |
| const resp = await api.fetchApi('/upload/image', { |
| method: 'POST', |
| body |
| }) |
|
|
| if (resp.status === 200) { |
| const data = await resp.json() |
| |
| let path = data.name |
| if (data.subfolder) path = data.subfolder + '/' + path |
|
|
| if (!imageWidget.options.values.includes(path)) { |
| imageWidget.options.values.push(path) |
| } |
|
|
| if (updateNode) { |
| imageWidget.value = path |
| } |
|
|
| return `/view?filename=${path}&type=input&subfolder=${ |
| pasted ? 'pasted' : '' |
| }&rand=${Math.random()}` |
| } else { |
| alert(resp.status + ' - ' + resp.statusText) |
| } |
| } catch (error) { |
| alert(error) |
| } |
| } |
|
|
| const fileInput = document.createElement('input') |
| Object.assign(fileInput, { |
| type: 'file', |
| accept: 'video/*,.mkv,video/webm,video/mp4,video/x-matroska,image/gif', |
| style: 'display: none', |
| onchange: async () => { |
| if (fileInput.files.length) { |
| let file = fileInput.files[0] |
|
|
| const url = await uploadFile(file, true) |
|
|
| |
| var reader = new FileReader() |
| reader.onload = function () { |
| displayDiv.src = url |
| displayDiv.onloadedmetadata = function () { |
| |
| |
| |
| |
| |
| |
| } |
| } |
| reader.readAsDataURL(file) |
| } |
| } |
| }) |
| document.body.append(fileInput) |
|
|
| |
| uploadWidget = node.addWidget('button', 'upload file', 'video', () => { |
| fileInput.click() |
| }) |
| uploadWidget.serialize = false |
| return { widget: uploadWidget } |
| } |
| ComfyWidgets.VIDEOUPLOAD_ = videoUpload |
|
|
| app.registerExtension({ |
| name: 'Mixlab.Video.LoadVideoAndSegment_', |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeData?.name == 'LoadVideoAndSegment_') { |
| nodeData.input.required.upload = ['VIDEOUPLOAD_'] |
| } |
| }, |
| async loadedGraphNode (node, app) { |
| if (node.type === 'LoadVideoAndSegment_') { |
| const imageWidget = node.widgets.find(w => w.name === 'video') |
| const uploadPreview = node.widgets.find(w => w.name === 'upload-preview') |
| if (imageWidget.value) { |
| |
| uploadPreview.div.querySelector('video').src = `/view?filename=${ |
| imageWidget.value |
| }&type=input&subfolder=${''}&rand=${Math.random()}` |
| } |
| } |
| } |
| }) |
|
|
| function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) { |
| const margin = 10 |
| const elRect = ctx.canvas.getBoundingClientRect() |
| const transform = new DOMMatrix() |
| .scaleSelf( |
| elRect.width / ctx.canvas.width, |
| elRect.height / ctx.canvas.height |
| ) |
| .multiplySelf(ctx.getTransform()) |
| .translateSelf(0, widgetY + margin) |
|
|
| const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) |
| Object.assign(widget.inputEl.style, { |
| transformOrigin: '0 0', |
| transform: scale, |
| left: `${transform.e}px`, |
| top: `${transform.d + transform.f}px`, |
| width: `${widgetWidth}px`, |
| height: `${(height || widget.parent?.inputHeight || 32) - margin}px`, |
| position: 'absolute', |
| background: !node.color ? '' : node.color, |
| color: !node.color ? '' : 'white', |
| zIndex: 5 |
| }) |
| } |
|
|
| export const hasWidgets = node => { |
| if (!node.widgets || !node.widgets?.[Symbol.iterator]) { |
| return false |
| } |
| return true |
| } |
|
|
| export const cleanupNode = node => { |
| if (!hasWidgets(node)) { |
| return |
| } |
|
|
| for (const w of node.widgets) { |
| if (w.canvas) { |
| w.canvas.remove() |
| } |
| if (w.inputEl) { |
| w.inputEl.remove() |
| } |
| |
| w.onRemoved?.() |
| } |
| } |
|
|
| const createPreviewElement = (name, val, format) => { |
| const [type] = format.split('/') |
| const w = { |
| name, |
| type, |
| value: val, |
| draw: function (ctx, node, widgetWidth, widgetY, height) { |
| const [cw, ch] = this.computeSize(widgetWidth) |
| offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) |
| }, |
| computeSize: function (_) { |
| const ratio = this.inputRatio || 1 |
| const width = Math.max(220, this.parent.size[0]) |
| return [width, width / ratio + 10] |
| }, |
| onRemoved: function () { |
| if (this.inputEl) { |
| this.inputEl.remove() |
| } |
| } |
| } |
|
|
| w.inputEl = document.createElement(type === 'video' ? 'video' : 'img') |
| w.inputEl.src = w.value |
|
|
| if (type === 'video' || format.match('.mp4')) { |
| w.inputEl.setAttribute('type', 'video/webm') |
| w.inputEl.autoplay = true |
| w.inputEl.loop = true |
| w.inputEl.controls = true |
| } |
| w.inputEl.onload = function () { |
| w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight |
| } |
| document.body.appendChild(w.inputEl) |
| return w |
| } |
|
|
| app.registerExtension({ |
| name: 'Mixlab.Video.ImageListReplace', |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeData?.name == 'ImageListReplace_') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
| nodeType.prototype.onNodeCreated = function () { |
| orig_nodeCreated?.apply(this, arguments) |
| const widget = { |
| type: 'div', |
| name: 'preview', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign( |
| this.div.style, |
| get_position_style(ctx, widget_width, 188, node.size[1]), |
| { |
| outline: '1px solid', |
| display: 'flex', |
| flexWrap: 'wrap', |
| flexDirection: 'row', |
| justifyContent: 'flex-start' |
| } |
| ) |
| } |
| } |
|
|
| widget.div = $el('div', {}) |
| widget.div.style.width = `120px` |
| widget.div.className = 'hidden' |
| document.body.appendChild(widget.div) |
| this.addCustomWidget(widget) |
| |
|
|
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| widget.div.remove() |
| return onRemoved?.() |
| } |
| } |
|
|
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| onExecuted?.apply(this, arguments) |
|
|
| |
| |
| |
| |
|
|
| let preview = this.widgets.filter(w => w.name == 'preview')[0] |
|
|
| if (message._images.length > 0) { |
| preview.div.className = '' |
| |
| } |
|
|
| preview.div.innerHTML = '' |
| for (const img_ of message._images) { |
| let img = new Image() |
| img.style = `width: 100px; |
| margin: 4px;` |
| img.src = `/view?filename=${img_.filename}&type=${ |
| img_.type |
| }&subfolder=${img_.subfolder}&rand=${Math.random()}` |
| preview.div.appendChild(img) |
| } |
|
|
| let start_index = this.widgets.filter(w => w.name == 'start_index')[0] |
| let end_index = this.widgets.filter(w => w.name == 'end_index')[0] |
| let invert = this.widgets.filter(w => w.name == 'invert')[0] |
| let _sc = start_index.callback.bind(start_index) |
| let _ec = end_index.callback.bind(end_index) |
|
|
| const selectImages = () => { |
| |
| let s = start_index.value, |
| e = end_index.value |
| let imgs = preview.div.querySelectorAll('img') |
| for (let index = 0; index < imgs.length; index++) { |
| if (invert.value) { |
| imgs[index].style.outline = |
| index >= s && index <= e ? 'none' : '4px solid #cbd3fe' |
| } else { |
| imgs[index].style.outline = |
| index >= s && index <= e ? '4px solid #cbd3fe' : 'none' |
| } |
| } |
| } |
|
|
| selectImages() |
|
|
| start_index.callback = v => { |
| let s = v, |
| e = end_index.value |
| let imgs = preview.div.querySelectorAll('img') |
| for (let index = 0; index < imgs.length; index++) { |
| if (invert.value) { |
| imgs[index].style.outline = |
| index >= s && index <= e ? 'none' : '4px solid #cbd3fe' |
| } else { |
| imgs[index].style.outline = |
| index >= s && index <= e ? '4px solid #cbd3fe' : 'none' |
| } |
| } |
|
|
| _sc(v) |
| } |
|
|
| end_index.callback = v => { |
| let s = start_index.value, |
| e = v |
| let imgs = preview.div.querySelectorAll('img') |
| for (let index = 0; index < imgs.length; index++) { |
| if (invert.value) { |
| imgs[index].style.outline = |
| index >= s && index <= e ? 'none' : '4px solid #cbd3fe' |
| } else { |
| imgs[index].style.outline = |
| index >= s && index <= e ? '4px solid #cbd3fe' : 'none' |
| } |
| } |
|
|
| _ec(v) |
| } |
|
|
| invert.callback = v => { |
| selectImages() |
| } |
|
|
| try { |
| } catch (error) {} |
| } |
| } |
|
|
| if ( |
| nodeData?.name == 'VideoCombine_Adv' || |
| nodeData?.name == 'CombineAudioVideo' |
| ) { |
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| const prefix = 'vhs_gif_preview_' |
| const r = onExecuted ? onExecuted.apply(this, message) : undefined |
|
|
| if (this.widgets) { |
| const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`) |
| if (pos !== -1) { |
| for (let i = pos; i < this.widgets.length; i++) { |
| this.widgets[i].onRemoved?.() |
| } |
| this.widgets.length = pos |
| } |
| if (message?.gifs) { |
| message.gifs.forEach((params, i) => { |
| const previewUrl = api.apiURL( |
| '/view?' + new URLSearchParams(params).toString() |
| ) |
| const w = this.addCustomWidget( |
| createPreviewElement( |
| `${prefix}_${i}`, |
| previewUrl, |
| params.format || 'image/gif' |
| ) |
| ) |
| w.parent = this |
| }) |
| } |
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| cleanupNode(this) |
| return onRemoved?.() |
| } |
| } |
| this.setSize([ |
| this.size[0], |
| this.computeSize([this.size[0], this.size[1]])[1] |
| ]) |
| return r |
| } |
| } |
| } |
| }) |
|
|