585 lines
25 KiB
JavaScript
585 lines
25 KiB
JavaScript
import { app } from '../../../scripts/app.js'
|
|
import { api } from '../../../scripts/api.js'
|
|
|
|
function chainCallback(object, property, callback) {
|
|
if (!object) return;
|
|
if (property in object && object[property]) {
|
|
const original = object[property];
|
|
object[property] = function () {
|
|
const r = original.apply(this, arguments);
|
|
return callback.apply(this, arguments) ?? r;
|
|
};
|
|
} else {
|
|
object[property] = callback;
|
|
}
|
|
}
|
|
|
|
async function getAuthHeader() {
|
|
try {
|
|
const authStore = await api.getAuthStore();
|
|
return authStore ? await authStore.getAuthHeader() : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function uploadFile(file, progressCallback) {
|
|
const body = new FormData();
|
|
const newFile = new File([file], file.name, {
|
|
type: file.type,
|
|
lastModified: file.lastModified,
|
|
});
|
|
body.append('image', newFile);
|
|
|
|
const url = api.apiURL('/upload/image');
|
|
const resp = await new Promise((resolve) => {
|
|
const req = new XMLHttpRequest();
|
|
req.upload.onprogress = (e) => progressCallback?.(e.loaded / e.total);
|
|
req.onload = () => resolve(req);
|
|
req.open('post', url, true);
|
|
getAuthHeader().then((headers) => {
|
|
headers ??= {};
|
|
for (const key in headers) req.setRequestHeader(key, headers[key]);
|
|
req.send(body);
|
|
});
|
|
});
|
|
|
|
if (resp.status !== 200) {
|
|
alert(resp.status + ' - ' + resp.statusText);
|
|
return null;
|
|
}
|
|
return resp;
|
|
}
|
|
|
|
function fitHeight(node) {
|
|
node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]]);
|
|
node?.graph?.setDirtyCanvas(true);
|
|
}
|
|
|
|
function addUploadWidget(nodeType, widgetName) {
|
|
const accept = ['video/webm', 'video/mp4', 'video/x-matroska', 'video/quicktime', 'video/x-msvideo', 'image/gif'];
|
|
chainCallback(nodeType.prototype, 'onNodeCreated', function() {
|
|
const node = this;
|
|
const pathWidget = this.widgets.find((w) => w.name === widgetName);
|
|
const fileInput = document.createElement('input');
|
|
|
|
const doUpload = async (file) => {
|
|
const resp = await uploadFile(file, (p) => node.progress = p);
|
|
node.progress = undefined;
|
|
if (!resp) return false;
|
|
const filename = JSON.parse(resp.responseText).name;
|
|
pathWidget.options.values.push(filename);
|
|
pathWidget.value = filename;
|
|
pathWidget.callback?.(filename);
|
|
return true;
|
|
};
|
|
|
|
Object.assign(fileInput, {
|
|
type: 'file',
|
|
accept: accept.join(','),
|
|
style: 'display: none',
|
|
onchange: async () => {
|
|
if (fileInput.files.length) await doUpload(fileInput.files[0]);
|
|
}
|
|
});
|
|
|
|
document.body.append(fileInput);
|
|
chainCallback(this, 'onRemoved', () => fileInput.remove());
|
|
|
|
const uploadWidget = this.addWidget('button', 'choose video to upload', '', () => {
|
|
app.canvas.node_widget = null;
|
|
fileInput.click();
|
|
});
|
|
uploadWidget.options.serialize = false;
|
|
|
|
this.onDragOver = (e) => !!e?.dataTransfer?.types?.includes?.('Files');
|
|
this.onDragDrop = async function(e) {
|
|
if (!e?.dataTransfer?.types?.includes?.('Files')) return false;
|
|
const item = e.dataTransfer?.files?.[0];
|
|
if (item && accept.includes(item.type)) return await doUpload(item);
|
|
return false;
|
|
};
|
|
});
|
|
}
|
|
|
|
function addVideoPreview(nodeType) {
|
|
chainCallback(nodeType.prototype, 'onNodeCreated', function() {
|
|
const element = document.createElement('div');
|
|
const previewNode = this;
|
|
const previewWidget = this.addDOMWidget('videopreview', 'preview', element, {
|
|
serialize: false,
|
|
hideOnZoom: false,
|
|
getValue() { return element.value; },
|
|
setValue(v) { element.value = v; },
|
|
});
|
|
|
|
previewWidget.computeSize = function(width) {
|
|
if (this.aspectRatio && !this.parentEl.hidden) {
|
|
let height = (previewNode.size[0] - 20) / this.aspectRatio + 10;
|
|
if (!(height > 0)) height = 0;
|
|
this.computedHeight = height + 10;
|
|
return [width, height];
|
|
}
|
|
return [width, -4];
|
|
};
|
|
|
|
const isOverlayEvent = (e) => {
|
|
const t = e?.target;
|
|
return !!(t && t.closest && t.closest('.vls-overlay'));
|
|
};
|
|
|
|
element.addEventListener('contextmenu', (e) => {
|
|
if (isOverlayEvent(e)) return;
|
|
e.preventDefault();
|
|
return app.canvas._mousedown_callback(e);
|
|
}, true);
|
|
element.addEventListener('pointerdown', (e) => {
|
|
if (isOverlayEvent(e)) return;
|
|
e.preventDefault();
|
|
return app.canvas._mousedown_callback(e);
|
|
}, true);
|
|
element.addEventListener('mousewheel', (e) => {
|
|
if (isOverlayEvent(e)) return;
|
|
e.preventDefault();
|
|
return app.canvas._mousewheel_callback(e);
|
|
}, true);
|
|
element.addEventListener('pointermove', (e) => {
|
|
if (isOverlayEvent(e)) return;
|
|
e.preventDefault();
|
|
return app.canvas._mousemove_callback(e);
|
|
}, true);
|
|
element.addEventListener('pointerup', (e) => {
|
|
if (isOverlayEvent(e)) return;
|
|
e.preventDefault();
|
|
return app.canvas._mouseup_callback(e);
|
|
}, true);
|
|
element.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
app.dragOverNode = this;
|
|
});
|
|
|
|
previewWidget.value = { hidden: false, paused: false, params: {}, muted: true };
|
|
previewWidget.parentEl = document.createElement('div');
|
|
previewWidget.parentEl.className = 'vhs_preview';
|
|
previewWidget.parentEl.style.width = '100%';
|
|
previewWidget.parentEl.style.position = 'relative';
|
|
previewWidget.parentEl.style.overflow = 'hidden';
|
|
element.appendChild(previewWidget.parentEl);
|
|
|
|
previewWidget.videoEl = document.createElement('video');
|
|
previewWidget.videoEl.controls = false;
|
|
previewWidget.videoEl.loop = true;
|
|
previewWidget.videoEl.muted = true;
|
|
previewWidget.videoEl.playsInline = true;
|
|
previewWidget.videoEl.style.width = '100%';
|
|
previewWidget.videoEl.style.display = 'block';
|
|
previewWidget.videoEl.addEventListener('loadedmetadata', () => {
|
|
previewWidget.aspectRatio = previewWidget.videoEl.videoWidth / previewWidget.videoEl.videoHeight;
|
|
fitHeight(this);
|
|
});
|
|
previewWidget.videoEl.addEventListener('error', () => {
|
|
previewWidget.parentEl.hidden = true;
|
|
fitHeight(this);
|
|
});
|
|
previewWidget.videoEl.onmouseenter = async () => {
|
|
if (previewWidget.value.hidden || previewWidget.value.paused) return;
|
|
previewWidget.videoEl.muted = false;
|
|
try {
|
|
await previewWidget.videoEl.play();
|
|
} catch {
|
|
previewWidget.videoEl.muted = true;
|
|
}
|
|
};
|
|
previewWidget.videoEl.onmouseleave = () => { previewWidget.videoEl.muted = true; };
|
|
|
|
previewWidget.imgEl = document.createElement('img');
|
|
previewWidget.imgEl.style.width = '100%';
|
|
previewWidget.imgEl.style.display = 'block';
|
|
previewWidget.imgEl.hidden = true;
|
|
previewWidget.imgEl.onload = () => {
|
|
previewWidget.aspectRatio = previewWidget.imgEl.naturalWidth / previewWidget.imgEl.naturalHeight;
|
|
fitHeight(this);
|
|
};
|
|
|
|
previewWidget.parentEl.appendChild(previewWidget.videoEl);
|
|
previewWidget.parentEl.appendChild(previewWidget.imgEl);
|
|
|
|
let timeout = null;
|
|
this.updateParameters = (params, forceUpdate) => {
|
|
if (!previewWidget.value.params) {
|
|
if (typeof(previewWidget.value) !== 'object') previewWidget.value = { hidden: false, paused: false };
|
|
previewWidget.value.params = {};
|
|
}
|
|
if (!Object.entries(params).some(([k, v]) => previewWidget.value.params[k] !== v)) return;
|
|
Object.assign(previewWidget.value.params, params);
|
|
if (timeout) clearTimeout(timeout);
|
|
if (forceUpdate) previewWidget.updateSource();
|
|
else timeout = setTimeout(() => previewWidget.updateSource(), 100);
|
|
};
|
|
|
|
previewWidget.updateSource = function () {
|
|
if (this.value.params == undefined) return;
|
|
const params = { ...this.value.params, timestamp: Date.now() };
|
|
this.parentEl.hidden = this.value.hidden;
|
|
|
|
if (params.format?.split('/')[0] === 'video' || params.format?.split('/')[1] === 'gif' || params.format === 'folder') {
|
|
this.videoEl.autoplay = !this.value.paused && !this.value.hidden;
|
|
this.videoEl.src = api.apiURL('/view?' + new URLSearchParams(params));
|
|
this.videoEl.hidden = false;
|
|
this.imgEl.hidden = true;
|
|
} else if (params.format?.split('/')[0] === 'image') {
|
|
this.imgEl.src = api.apiURL('/view?' + new URLSearchParams(params));
|
|
this.videoEl.hidden = true;
|
|
this.imgEl.hidden = false;
|
|
}
|
|
};
|
|
|
|
previewWidget.callback = previewWidget.updateSource;
|
|
});
|
|
}
|
|
|
|
function addCharacterOverlay(nodeType) {
|
|
chainCallback(nodeType.prototype, 'onNodeCreated', function () {
|
|
this._characterCount = 0;
|
|
this._characterItems = [];
|
|
this._characterDragState = null;
|
|
this._characterResizeState = null;
|
|
this._speakerNames = ['A', 'B', 'C', 'D'];
|
|
|
|
const toRelativePositionText = (xPct, yPct) => {
|
|
const v = yPct < 33.333 ? 'Top' : (yPct < 66.666 ? 'Center' : 'Bottom');
|
|
const h = xPct < 33.333 ? 'left' : (xPct < 66.666 ? 'center' : 'right');
|
|
if (h === 'center' && v === 'Center') return 'Center character';
|
|
return `${v} ${h} character`;
|
|
};
|
|
|
|
const getItemSceneCenterPct = (item) => {
|
|
const overlayRect = this._characterOverlayEl?.getBoundingClientRect();
|
|
const labelRect = item?.el?.getBoundingClientRect();
|
|
if (!overlayRect || !labelRect || overlayRect.width <= 0 || overlayRect.height <= 0) {
|
|
return { xPct: item?.xPct ?? 0, yPct: item?.yPct ?? 0 };
|
|
}
|
|
const centerXPx = (labelRect.left - overlayRect.left) + (labelRect.width / 2);
|
|
const centerYPx = (labelRect.top - overlayRect.top) + (labelRect.height / 2);
|
|
return {
|
|
xPct: (centerXPx / overlayRect.width) * 100,
|
|
yPct: (centerYPx / overlayRect.height) * 100,
|
|
};
|
|
};
|
|
|
|
const syncSpeakerOutputs = () => {
|
|
const widgetMap = [
|
|
this.widgets?.find((w) => w.name === 'speaker_a_position'),
|
|
this.widgets?.find((w) => w.name === 'speaker_b_position'),
|
|
this.widgets?.find((w) => w.name === 'speaker_c_position'),
|
|
this.widgets?.find((w) => w.name === 'speaker_d_position'),
|
|
];
|
|
for (let i = 0; i < 4; i += 1) {
|
|
const widget = widgetMap[i];
|
|
if (!widget) continue;
|
|
const item = this._characterItems[i];
|
|
const value = item
|
|
? (() => {
|
|
const c = getItemSceneCenterPct(item);
|
|
return toRelativePositionText(c.xPct, c.yPct);
|
|
})()
|
|
: `Speaker ${this._speakerNames[i]} not set`;
|
|
widget.value = value;
|
|
widget.callback?.(value);
|
|
}
|
|
};
|
|
|
|
const clampItemInsideOverlay = (item) => {
|
|
const overlayRect = this._characterOverlayEl?.getBoundingClientRect();
|
|
const itemRect = item?.el?.getBoundingClientRect();
|
|
if (!overlayRect || !itemRect || overlayRect.width <= 0 || overlayRect.height <= 0) return;
|
|
const maxX = Math.max(0, overlayRect.width - itemRect.width);
|
|
const maxY = Math.max(0, overlayRect.height - itemRect.height);
|
|
const xPx = (item.xPct / 100) * overlayRect.width;
|
|
const yPx = (item.yPct / 100) * overlayRect.height;
|
|
const clampedXPx = Math.min(Math.max(0, xPx), maxX);
|
|
const clampedYPx = Math.min(Math.max(0, yPx), maxY);
|
|
item.xPct = (clampedXPx / Math.max(1, overlayRect.width)) * 100;
|
|
item.yPct = (clampedYPx / Math.max(1, overlayRect.height)) * 100;
|
|
item.el.style.left = `${item.xPct}%`;
|
|
item.el.style.top = `${item.yPct}%`;
|
|
};
|
|
|
|
const addCharacterWidget = this.addWidget('button', 'Add speaking character', '', () => {
|
|
if (this._characterCount >= 4) return;
|
|
const previewWidget = this.widgets?.find((w) => w.name === 'videopreview');
|
|
const parentEl = previewWidget?.parentEl;
|
|
if (!parentEl) return;
|
|
|
|
if (!this._characterOverlayEl) {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'vls-overlay';
|
|
overlay.style.position = 'absolute';
|
|
overlay.style.inset = '0';
|
|
overlay.style.pointerEvents = 'none';
|
|
overlay.style.touchAction = 'none';
|
|
overlay.style.overflow = 'hidden';
|
|
overlay.style.zIndex = '5';
|
|
parentEl.appendChild(overlay);
|
|
this._characterOverlayEl = overlay;
|
|
}
|
|
|
|
this._characterCount += 1;
|
|
const label = document.createElement('div');
|
|
label.classList.add('vls-overlay');
|
|
const speakerName = this._speakerNames[this._characterCount - 1];
|
|
label.textContent = `Speaker ${speakerName}`;
|
|
label.style.position = 'absolute';
|
|
label.style.left = '10%';
|
|
label.style.top = `${10 + (this._characterCount - 1) * 8}%`;
|
|
label.style.padding = '4px 8px';
|
|
label.style.background = 'rgba(0, 0, 0, 0.65)';
|
|
label.style.color = '#fff';
|
|
label.style.border = '1px solid rgba(255,255,255,0.45)';
|
|
label.style.borderRadius = '6px';
|
|
label.style.fontSize = '12px';
|
|
label.style.fontFamily = 'monospace';
|
|
label.style.cursor = 'move';
|
|
label.style.userSelect = 'none';
|
|
label.style.pointerEvents = 'auto';
|
|
label.style.touchAction = 'none';
|
|
label.style.whiteSpace = 'nowrap';
|
|
label.style.maxWidth = '100%';
|
|
label.style.boxSizing = 'border-box';
|
|
label.ondragstart = () => false;
|
|
this._characterOverlayEl.appendChild(label);
|
|
|
|
const item = { el: label, xPct: 10, yPct: 10 + (this._characterCount - 1) * 8, scale: 1.0 };
|
|
this._characterItems.push(item);
|
|
syncSpeakerOutputs();
|
|
|
|
const resizeHandle = document.createElement('div');
|
|
resizeHandle.style.position = 'absolute';
|
|
resizeHandle.style.width = '10px';
|
|
resizeHandle.style.height = '10px';
|
|
resizeHandle.style.right = '-5px';
|
|
resizeHandle.style.bottom = '-5px';
|
|
resizeHandle.style.borderRadius = '50%';
|
|
resizeHandle.style.background = 'rgba(255,255,255,0.95)';
|
|
resizeHandle.style.border = '1px solid rgba(0,0,0,0.7)';
|
|
resizeHandle.style.cursor = 'nwse-resize';
|
|
resizeHandle.style.pointerEvents = 'auto';
|
|
label.appendChild(resizeHandle);
|
|
|
|
const onPointerMove = (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (!this._characterDragState) return;
|
|
const { overlayRect, targetItem, offsetX, offsetY } = this._characterDragState;
|
|
const currentRect = targetItem.el.getBoundingClientRect();
|
|
const maxX = Math.max(0, overlayRect.width - currentRect.width);
|
|
const maxY = Math.max(0, overlayRect.height - currentRect.height);
|
|
const xPx = Math.min(Math.max(0, ev.clientX - overlayRect.left - offsetX), maxX);
|
|
const yPx = Math.min(Math.max(0, ev.clientY - overlayRect.top - offsetY), maxY);
|
|
targetItem.xPct = (xPx / Math.max(1, overlayRect.width)) * 100;
|
|
targetItem.yPct = (yPx / Math.max(1, overlayRect.height)) * 100;
|
|
targetItem.el.style.left = `${targetItem.xPct}%`;
|
|
targetItem.el.style.top = `${targetItem.yPct}%`;
|
|
clampItemInsideOverlay(targetItem);
|
|
syncSpeakerOutputs();
|
|
};
|
|
|
|
const onPointerUp = (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (this._characterDragState?.targetItem?.el && ev.pointerId !== undefined) {
|
|
try { this._characterDragState.targetItem.el.releasePointerCapture(ev.pointerId); } catch {}
|
|
}
|
|
if (this._characterDragState?.targetItem) clampItemInsideOverlay(this._characterDragState.targetItem);
|
|
this._characterDragState = null;
|
|
window.removeEventListener('pointermove', onPointerMove);
|
|
window.removeEventListener('pointerup', onPointerUp);
|
|
};
|
|
|
|
label.addEventListener('pointerdown', (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (ev.pointerId !== undefined) {
|
|
try { label.setPointerCapture(ev.pointerId); } catch {}
|
|
}
|
|
const overlayRect = this._characterOverlayEl.getBoundingClientRect();
|
|
const labelRect = label.getBoundingClientRect();
|
|
this._characterDragState = {
|
|
overlayRect,
|
|
targetItem: item,
|
|
offsetX: ev.clientX - labelRect.left,
|
|
offsetY: ev.clientY - labelRect.top,
|
|
};
|
|
window.addEventListener('pointermove', onPointerMove);
|
|
window.addEventListener('pointerup', onPointerUp);
|
|
});
|
|
|
|
const onResizeMove = (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (!this._characterResizeState) return;
|
|
const state = this._characterResizeState;
|
|
const dx = ev.clientX - state.startX;
|
|
const dy = ev.clientY - state.startY;
|
|
const delta = (dx + dy) * 0.005;
|
|
const nextScale = Math.min(4.0, Math.max(0.5, state.startScale + delta));
|
|
state.targetItem.scale = nextScale;
|
|
state.targetItem.el.style.transform = `scale(${nextScale})`;
|
|
state.targetItem.el.style.transformOrigin = 'top left';
|
|
clampItemInsideOverlay(state.targetItem);
|
|
syncSpeakerOutputs();
|
|
};
|
|
|
|
const onResizeUp = (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (this._characterResizeState?.handleEl && ev.pointerId !== undefined) {
|
|
try { this._characterResizeState.handleEl.releasePointerCapture(ev.pointerId); } catch {}
|
|
}
|
|
if (this._characterResizeState?.targetItem) clampItemInsideOverlay(this._characterResizeState.targetItem);
|
|
this._characterResizeState = null;
|
|
window.removeEventListener('pointermove', onResizeMove);
|
|
window.removeEventListener('pointerup', onResizeUp);
|
|
};
|
|
|
|
resizeHandle.addEventListener('pointerdown', (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (ev.pointerId !== undefined) {
|
|
try { resizeHandle.setPointerCapture(ev.pointerId); } catch {}
|
|
}
|
|
this._characterResizeState = {
|
|
targetItem: item,
|
|
startX: ev.clientX,
|
|
startY: ev.clientY,
|
|
startScale: item.scale ?? 1.0,
|
|
handleEl: resizeHandle,
|
|
};
|
|
window.addEventListener('pointermove', onResizeMove);
|
|
window.addEventListener('pointerup', onResizeUp);
|
|
});
|
|
});
|
|
addCharacterWidget.options.serialize = false;
|
|
|
|
const deleteCharacterWidget = this.addWidget('button', 'Delete speaker', '', () => {
|
|
if (this._characterItems.length === 0) return;
|
|
const last = this._characterItems.pop();
|
|
last?.el?.remove();
|
|
this._characterCount = Math.max(0, this._characterCount - 1);
|
|
syncSpeakerOutputs();
|
|
});
|
|
deleteCharacterWidget.options.serialize = false;
|
|
|
|
chainCallback(this, 'onResize', () => {
|
|
for (const item of this._characterItems) clampItemInsideOverlay(item);
|
|
syncSpeakerOutputs();
|
|
});
|
|
|
|
syncSpeakerOutputs();
|
|
});
|
|
}
|
|
|
|
function addPreviewOptions(nodeType) {
|
|
chainCallback(nodeType.prototype, 'getExtraMenuOptions', function(_, options) {
|
|
const previewWidget = this.widgets.find((w) => w.name === 'videopreview');
|
|
if (!previewWidget) return;
|
|
|
|
let url = null;
|
|
if (previewWidget.videoEl?.hidden === false && previewWidget.videoEl.src) {
|
|
url = api.apiURL('/view?' + new URLSearchParams(previewWidget.value.params));
|
|
url = url.replace('%2503d', '001');
|
|
} else if (previewWidget.imgEl?.hidden === false && previewWidget.imgEl.src) {
|
|
url = previewWidget.imgEl.src;
|
|
}
|
|
|
|
const extra = [];
|
|
if (url) {
|
|
extra.push({ content: 'Open preview', callback: () => window.open(url, '_blank') });
|
|
extra.push({
|
|
content: 'Save preview',
|
|
callback: () => {
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.setAttribute('download', previewWidget.value.params.filename || 'video');
|
|
document.body.append(a);
|
|
a.click();
|
|
requestAnimationFrame(() => a.remove());
|
|
},
|
|
});
|
|
}
|
|
|
|
const pauseText = (previewWidget.value.paused ? 'Resume' : 'Pause') + ' preview';
|
|
if (previewWidget.videoEl?.hidden === false) {
|
|
extra.push({
|
|
content: pauseText,
|
|
callback: () => {
|
|
if (previewWidget.value.paused) previewWidget.videoEl?.play();
|
|
else previewWidget.videoEl?.pause();
|
|
previewWidget.value.paused = !previewWidget.value.paused;
|
|
},
|
|
});
|
|
}
|
|
|
|
const showText = (previewWidget.value.hidden ? 'Show' : 'Hide') + ' preview';
|
|
extra.push({
|
|
content: showText,
|
|
callback: () => {
|
|
if (!previewWidget.videoEl.hidden && !previewWidget.value.hidden) previewWidget.videoEl.pause();
|
|
else if (previewWidget.value.hidden && !previewWidget.videoEl.hidden && !previewWidget.value.paused) previewWidget.videoEl.play();
|
|
previewWidget.value.hidden = !previewWidget.value.hidden;
|
|
previewWidget.parentEl.hidden = previewWidget.value.hidden;
|
|
fitHeight(this);
|
|
},
|
|
});
|
|
|
|
const muteText = (previewWidget.value.muted ? 'Unmute' : 'Mute') + ' preview';
|
|
extra.push({ content: muteText, callback: () => { previewWidget.value.muted = !previewWidget.value.muted; } });
|
|
|
|
if (options.length > 0 && options[0] != null && extra.length > 0) extra.push(null);
|
|
options.unshift(...extra);
|
|
});
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: 'Comfy.LoadVideoSequentially.UI',
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData?.name !== 'LoadVideoSequentially') return;
|
|
|
|
addUploadWidget(nodeType, 'video');
|
|
addVideoPreview(nodeType);
|
|
addCharacterOverlay(nodeType);
|
|
addPreviewOptions(nodeType);
|
|
|
|
chainCallback(nodeType.prototype, 'onNodeCreated', function () {
|
|
const videoWidget = this.widgets.find((w) => w.name === 'video');
|
|
chainCallback(videoWidget, 'callback', (value) => {
|
|
if (!value) return;
|
|
const extIdx = value.lastIndexOf('.');
|
|
const ext = extIdx >= 0 ? value.slice(extIdx + 1).toLowerCase() : 'mp4';
|
|
const format = (['gif', 'webp', 'avif'].includes(ext) ? 'image' : 'video') + '/' + ext;
|
|
this.updateParameters({ filename: value, type: 'input', format }, true);
|
|
});
|
|
videoWidget?.callback?.(videoWidget.value);
|
|
});
|
|
|
|
chainCallback(nodeType.prototype, 'onExecuted', function (message) {
|
|
const turnWidget = this.widgets?.find((w) => w.name === 'turn');
|
|
if (!turnWidget) return;
|
|
if (!Array.isArray(message) && message?.turn_used !== undefined) {
|
|
const currentTurn = Number(message.turn_used);
|
|
if (!Number.isNaN(currentTurn)) {
|
|
turnWidget.value = currentTurn + 1;
|
|
turnWidget.callback?.(turnWidget.value);
|
|
}
|
|
return;
|
|
}
|
|
const current = Number(turnWidget.value);
|
|
if (!Number.isNaN(current)) {
|
|
turnWidget.value = current + 1;
|
|
turnWidget.callback?.(turnWidget.value);
|
|
}
|
|
});
|
|
},
|
|
});
|