refactor: extract duplicated code into shared modules (videoWidgetUtils, uploadUtils, fileUtils)

This commit is contained in:
Suzu008
2026-03-24 08:53:08 +08:00
parent 17c8ede5e9
commit 4e0bfdd82e
7 changed files with 233 additions and 355 deletions
+16 -134
View File
@@ -12,136 +12,22 @@ import {
loadEncryptedVideoFromParams,
downloadDecryptedVideo
} from "./utils/videoLoader.js";
import {
createVideoWidget,
renderVideoInWidget,
clearVideoWidget,
} from "./utils/videoWidgetUtils.js";
const app: ComfyApp = rawApp;
const api: ComfyApi = rawApi;
const DEFAULT_VIDEO_SIZE = 256;
const MIN_WIDGET_HEIGHT = 64;
function fitDimensionsToNodeWidth(
width: number,
height: number,
nodeWidth: number
) {
const aspectRatio = width / height;
if (!aspectRatio || Number.isNaN(aspectRatio)) {
return {
minHeight: DEFAULT_VIDEO_SIZE,
minWidth: nodeWidth || DEFAULT_VIDEO_SIZE,
};
}
const minWidth = nodeWidth || DEFAULT_VIDEO_SIZE;
const minHeight = Math.max(minWidth / aspectRatio, MIN_WIDGET_HEIGHT);
return { minHeight, minWidth };
}
/**
* Create video preview widget
* Load encrypted video from params and render in widget
*/
function createVideoWidget(node: any, videoName: string, videoInfo: any) {
let minHeight = DEFAULT_VIDEO_SIZE;
let minWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const container = document.createElement("div");
container.classList.add("comfy-img-preview");
const widget = node.addDOMWidget(
videoName,
"video-preview",
container,
{
serialize: false,
hideOnZoom: false,
}
);
widget.computeLayoutSize = () => ({
minHeight,
minWidth,
});
widget.updateLayout = (videoWidth: number, videoHeight: number) => {
const nodeWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const dimensions = fitDimensionsToNodeWidth(
videoWidth,
videoHeight,
nodeWidth
);
minHeight = dimensions.minHeight;
minWidth = dimensions.minWidth;
};
widget.element.addEventListener('pointermove', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseMove(event)
}
})
widget.element.addEventListener('pointerdown', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseDown(event)
}
})
widget.element.addEventListener('wheel', (event: WheelEvent) => {
if (event.ctrlKey) {
event.preventDefault()
event.stopPropagation()
return
}
event.preventDefault()
app.canvas.processMouseWheel(event)
})
widget.videoInfo = videoInfo;
widget.value = { hidden: false, videoInfo };
return widget;
}
/**
* Update video preview widget with decrypted video
*/
async function updateVideoWidget(widget: any, videoInfo: any) {
try {
const videoUrl = await loadEncryptedVideoFromParams(api, videoInfo);
// Create video element
const video = document.createElement("video");
video.controls = true;
video.loop = true;
video.muted = true;
video.autoplay = false;
video.playsInline = true;
video.onloadedmetadata = () => {
widget.updateLayout?.(video.videoWidth, video.videoHeight);
widget.element.hidden = false;
widget.element.style.overflow = "hidden";
// Clear previous content
widget.element.replaceChildren();
// Add new video
widget.element.appendChild(video);
widget.videoElement = video;
widget.videoUrl = videoUrl;
app.rootGraph?.setDirtyCanvas(true, false);
};
video.src = videoUrl;
return videoUrl;
} catch (error) {
console.error("Failed to decrypt video for preview:", error);
throw error;
}
async function updateVideoWidget(widget: any, videoInfo: any): Promise<string> {
const videoUrl = await loadEncryptedVideoFromParams(api, videoInfo);
await renderVideoInWidget(widget, videoUrl);
return videoUrl;
}
// Register extension
@@ -173,6 +59,7 @@ app.registerExtension({
w.type === "video-preview"
);
for (const widget of existingWidgets) {
clearVideoWidget(widget);
this.removeWidget(widget);
}
@@ -181,8 +68,10 @@ app.registerExtension({
for (let i = 0; i < message.cryptio_images.length; i++) {
const videoInfo = message.cryptio_images[i];
if (videoInfo.filename && videoInfo.filename.endsWith(".encrypted")) {
const videoName = `video_${i}`;
const widget = createVideoWidget(this, videoName, videoInfo);
const widget = createVideoWidget(this, {
widgetName: `video_${i}`,
videoInfo,
});
updatePromises.push(
updateVideoWidget(widget, videoInfo)
@@ -231,19 +120,12 @@ app.registerExtension({
// Cleanup on node removal
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
// Clean up video widgets
const videoWidgets = this.widgets?.filter((w: any) =>
w.type === "video-preview"
) || [];
for (const widget of videoWidgets) {
if (widget.videoElement) {
widget.videoElement.pause();
widget.videoElement.src = "";
}
if (widget.videoUrl) {
URL.revokeObjectURL(widget.videoUrl);
}
clearVideoWidget(widget);
}
if (onRemoved) {
+11 -181
View File
@@ -9,202 +9,32 @@ import type {
} from "@comfyorg/comfyui-frontend-types";
import { exchangeKeys } from "./utils/cryptoKeys.js";
import { encryptFileWithServerKey } from "./utils/cryptoUtils.js";
import { loadEncryptedVideoFromFilename } from "./utils/videoLoader.js";
import { uploadEncryptedFile } from "./utils/uploadUtils.js";
import {
createVideoWidget,
renderVideoInWidget,
clearVideoWidget,
} from "./utils/videoWidgetUtils.js";
const app: ComfyApp = rawApp;
const api: ComfyApi = rawApi;
const DEFAULT_VIDEO_SIZE = 256;
const MIN_WIDGET_HEIGHT = 64;
function fitDimensionsToNodeWidth(
width: number,
height: number,
nodeWidth: number
) {
const aspectRatio = width / height;
if (!aspectRatio || Number.isNaN(aspectRatio)) {
return {
minHeight: DEFAULT_VIDEO_SIZE,
minWidth: nodeWidth || DEFAULT_VIDEO_SIZE,
};
}
const minWidth = nodeWidth || DEFAULT_VIDEO_SIZE;
const minHeight = Math.max(minWidth / aspectRatio, MIN_WIDGET_HEIGHT);
return { minHeight, minWidth };
}
/**
* Create video preview widget
*/
function createVideoWidget(node: any) {
let minHeight = DEFAULT_VIDEO_SIZE;
let minWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const container = document.createElement("div");
container.classList.add("comfy-img-preview");
const widget = node.addDOMWidget(
"video_preview",
"video-preview",
container,
{
serialize: false,
hideOnZoom: false,
}
);
widget.computeLayoutSize = () => ({
minHeight,
minWidth,
});
widget.element.addEventListener('pointermove', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseMove(event)
}
})
widget.element.addEventListener('pointerdown', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseDown(event)
}
})
widget.element.addEventListener('wheel', (event: WheelEvent) => {
if (event.ctrlKey) {
event.preventDefault()
event.stopPropagation()
return
}
event.preventDefault()
app.canvas.processMouseWheel(event)
})
widget.updateLayout = (videoWidth: number, videoHeight: number) => {
const nodeWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const dimensions = fitDimensionsToNodeWidth(
videoWidth,
videoHeight,
nodeWidth
);
minHeight = dimensions.minHeight;
minWidth = dimensions.minWidth;
};
widget.value = { hidden: true };
widget.element.hidden = true;
return widget;
}
/**
* Update video preview widget with decrypted video
* Load encrypted video from filename and render in widget
*/
async function updateVideoWidget(widget: any, filename: string) {
try {
const videoUrl = await loadEncryptedVideoFromFilename(filename);
// Create video element
const video = document.createElement("video");
video.controls = true;
video.loop = true;
video.muted = true;
video.autoplay = false;
video.style.width = "100%";
video.style.height = "auto";
video.onloadedmetadata = () => {
widget.updateLayout?.(video.videoWidth, video.videoHeight);
widget.element.hidden = false;
widget.element.style.overflow = "hidden";
// Clear previous content
widget.element.replaceChildren();
// Add new video
widget.element.appendChild(video);
widget.videoElement = video;
widget.videoUrl = videoUrl;
widget.value = { hidden: false };
app.rootGraph?.setDirtyCanvas(true, false);
};
video.src = videoUrl;
} catch (error) {
console.error("Failed to decrypt video for preview:", error);
throw error;
}
}
/**
* Clear video preview widget
*/
function clearVideoWidget(widget: any) {
if (widget.videoElement) {
widget.videoElement.pause();
widget.videoElement.src = "";
}
if (widget.videoUrl) {
URL.revokeObjectURL(widget.videoUrl);
}
widget.element.hidden = true;
widget.value = { hidden: true };
widget.element.replaceChildren();
app.rootGraph?.setDirtyCanvas(true, false);
}
/**
* Upload encrypted video to server (encrypted with server public key)
* @param file File to upload
* @returns Server response
*/
async function uploadEncryptedVideo(file: File): Promise<any> {
console.log("Encrypting and uploading video:", file.name);
// Encrypt file (use server public key, or directly read if already encrypted with server public key)
const encryptedData = file?.name?.endsWith(".encrypted")
? new Uint8Array(await file.arrayBuffer())
: await encryptFileWithServerKey(file);
// Create FormData
const formData = new FormData();
const encryptedBlob = new Blob([encryptedData as BlobPart], {
type: "application/octet-stream",
});
formData.append("image", encryptedBlob, file.name);
// Upload to server
const response = await fetch("/cryptio/upload_encrypted", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
return await response.json();
const videoUrl = await loadEncryptedVideoFromFilename(filename);
await renderVideoInWidget(widget, videoUrl);
}
/**
* Handle video file upload to node
* @param node Node instance
* @param file File to upload
* @param app ComfyUI App instance
*/
async function handleVideoUpload(node: any, file: File, app: ComfyApp) {
try {
// Upload encrypted video
const result = await uploadEncryptedVideo(file);
const result = await uploadEncryptedFile(file);
// Update video widget value
const videoWidget = node.widgets.find(
@@ -281,7 +111,7 @@ app.registerExtension({
uploadWidget.label = "choose video to upload";
// Create video preview widget
const previewWidget = createVideoWidget(this);
const previewWidget = createVideoWidget(this, { hidden: true });
// Update preview when video value changes
const videoWidget = this.widgets.find((w: any) => w.name === "video");
+16
View File
@@ -46,3 +46,19 @@ export function getMimeTypeFromFilename(filename: string): string {
return mimeTypes[extension] || 'image/png'; // 默认返回image/png
}
/**
* Download a decrypted file (image or video) from a blob URL.
* Strips the .encrypted suffix from the filename.
* @param blobUrl Blob URL of the decrypted data
* @param filename Original filename (with .encrypted suffix)
*/
export function downloadDecryptedFile(blobUrl: string, filename: string): void {
const a = document.createElement("a");
a.href = blobUrl;
const originalFilename = filename.replace(/\.encrypted$/, "");
a.download = originalFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
+3 -16
View File
@@ -5,7 +5,7 @@
import type { ComfyApi } from "@comfyorg/comfyui-frontend-types";
import { decryptDataWithServerKey, decryptFileWithClientKey } from "./cryptoUtils.js";
import { getMimeTypeFromFilename } from "./fileUtils.js";
import { getMimeTypeFromFilename, downloadDecryptedFile } from "./fileUtils.js";
export interface ImageLoadParams {
filename: string;
@@ -77,18 +77,5 @@ export async function loadEncryptedImageFromParams(api: ComfyApi, params: ImageL
return URL.createObjectURL(blob);
}
/**
* 下载解密后的图片
* @param blobUrl Blob URL
* @param filename 原始文件名(带 .encrypted 后缀)
*/
export function downloadDecryptedImage(blobUrl: string, filename: string): void {
const a = document.createElement("a");
a.href = blobUrl;
// 移除 .encrypted 后缀
const originalFilename = filename.replace(/\.encrypted$/, "");
a.download = originalFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/** @deprecated Use downloadDecryptedFile from fileUtils.ts instead */
export const downloadDecryptedImage = downloadDecryptedFile;
+12 -8
View File
@@ -8,26 +8,27 @@ import { encryptFileWithServerKey } from "./cryptoUtils.js";
import { loadEncryptedImageFromFilename } from "./imageLoader.js";
/**
* 上传加密图片到服务器(使用服务端公钥加密)
* @param file 要上传的文件
* @returns 服务器响应
* Upload an encrypted file to the server (encrypted with server public key).
* Works for both images and videos.
* @param file File to upload
* @returns Server response
*/
export async function uploadEncryptedImage(file: File): Promise<any> {
console.log("Encrypting and uploading image:", file.name);
export async function uploadEncryptedFile(file: File): Promise<any> {
console.log("Encrypting and uploading file:", file.name);
// 加密文件(使用服务端公钥,如果文件已经使用服务端公钥加密则直接读取)
// Encrypt file (use server public key, or directly read if already encrypted)
const encryptedData = file?.name?.endsWith(".encrypted")
? new Uint8Array(await file.arrayBuffer())
: await encryptFileWithServerKey(file);
// 创建FormData
// Create FormData
const formData = new FormData();
const encryptedBlob = new Blob([encryptedData as BlobPart], {
type: "application/octet-stream",
});
formData.append("image", encryptedBlob, file.name);
// 上传到服务器
// Upload to server
const response = await fetch("/cryptio/upload_encrypted", {
method: "POST",
body: formData,
@@ -40,6 +41,9 @@ export async function uploadEncryptedImage(file: File): Promise<any> {
return await response.json();
}
/** @deprecated Use uploadEncryptedFile instead */
export const uploadEncryptedImage = uploadEncryptedFile;
/**
* 处理文件上传到节点的通用方法
* @param node 节点实例
+3 -16
View File
@@ -5,7 +5,7 @@
import type { ComfyApi } from "@comfyorg/comfyui-frontend-types";
import { decryptDataWithServerKey, decryptFileWithClientKey } from "./cryptoUtils.js";
import { getMimeTypeFromFilename } from "./fileUtils.js";
import { getMimeTypeFromFilename, downloadDecryptedFile } from "./fileUtils.js";
export interface VideoLoadParams {
filename: string;
@@ -77,19 +77,6 @@ export async function loadEncryptedVideoFromParams(api: ComfyApi, params: VideoL
return URL.createObjectURL(blob);
}
/**
* 下载解密后的视频
* @param blobUrl Blob URL
* @param filename 原始文件名(带 .encrypted 后缀)
*/
export function downloadDecryptedVideo(blobUrl: string, filename: string): void {
const a = document.createElement("a");
a.href = blobUrl;
// 移除 .encrypted 后缀
const originalFilename = filename.replace(/\.encrypted$/, "");
a.download = originalFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/** @deprecated Use downloadDecryptedFile from fileUtils.ts instead */
export const downloadDecryptedVideo = downloadDecryptedFile;
+172
View File
@@ -0,0 +1,172 @@
/**
* Shared video widget utilities
* Used by both cryptioSaveVideo.ts and cryptioUploadVideo.ts
*/
//@ts-ignore
import { app as rawApp } from "../../../scripts/app.js";
import type { ComfyApp } from "@comfyorg/comfyui-frontend-types";
const app: ComfyApp = rawApp;
export const DEFAULT_VIDEO_SIZE = 256;
const MIN_WIDGET_HEIGHT = 64;
export function fitDimensionsToNodeWidth(
width: number,
height: number,
nodeWidth: number
) {
const aspectRatio = width / height;
if (!aspectRatio || Number.isNaN(aspectRatio)) {
return {
minHeight: DEFAULT_VIDEO_SIZE,
minWidth: nodeWidth || DEFAULT_VIDEO_SIZE,
};
}
const minWidth = nodeWidth || DEFAULT_VIDEO_SIZE;
const minHeight = Math.max(minWidth / aspectRatio, MIN_WIDGET_HEIGHT);
return { minHeight, minWidth };
}
export interface CreateVideoWidgetOptions {
widgetName?: string;
hidden?: boolean;
videoInfo?: any;
}
/**
* Create a video preview DOM widget on a node.
*/
export function createVideoWidget(node: any, options: CreateVideoWidgetOptions = {}) {
const {
widgetName = "video_preview",
hidden = false,
videoInfo,
} = options;
let minHeight = DEFAULT_VIDEO_SIZE;
let minWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const container = document.createElement("div");
container.classList.add("comfy-img-preview");
const widget = node.addDOMWidget(
widgetName,
"video-preview",
container,
{
serialize: false,
hideOnZoom: false,
}
);
widget.computeLayoutSize = () => ({
minHeight,
minWidth,
});
widget.updateLayout = (videoWidth: number, videoHeight: number) => {
const nodeWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const dimensions = fitDimensionsToNodeWidth(
videoWidth,
videoHeight,
nodeWidth
);
minHeight = dimensions.minHeight;
minWidth = dimensions.minWidth;
};
// Forward middle-button events to the canvas for panning
widget.element.addEventListener('pointermove', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseMove(event);
}
});
widget.element.addEventListener('pointerdown', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseDown(event);
}
});
// Forward wheel events to canvas (block ctrl+wheel for browser zoom)
widget.element.addEventListener('wheel', (event: WheelEvent) => {
if (event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
return;
}
event.preventDefault();
app.canvas.processMouseWheel(event);
});
if (videoInfo) {
widget.videoInfo = videoInfo;
widget.value = { hidden: false, videoInfo };
} else {
widget.value = { hidden };
}
if (hidden) {
widget.element.hidden = true;
}
return widget;
}
/**
* Update a video widget with a decrypted video URL.
* @param widget The video widget to update
* @param videoUrl The blob URL of the decrypted video
*/
export function renderVideoInWidget(widget: any, videoUrl: string): Promise<void> {
return new Promise((resolve, reject) => {
const video = document.createElement("video");
video.controls = true;
video.loop = true;
video.muted = true;
video.autoplay = false;
video.playsInline = true;
video.onloadedmetadata = () => {
widget.updateLayout?.(video.videoWidth, video.videoHeight);
widget.element.hidden = false;
widget.element.style.overflow = "hidden";
// Clear previous content
widget.element.replaceChildren();
// Add new video
widget.element.appendChild(video);
widget.videoElement = video;
widget.videoUrl = videoUrl;
widget.value = { ...widget.value, hidden: false };
app.rootGraph?.setDirtyCanvas(true, false);
resolve();
};
video.onerror = () => reject(new Error("Failed to load video element"));
video.src = videoUrl;
});
}
/**
* Clean up a video widget (pause, revoke URL, clear DOM).
*/
export function clearVideoWidget(widget: any) {
if (widget.videoElement) {
widget.videoElement.pause();
widget.videoElement.src = "";
}
if (widget.videoUrl) {
URL.revokeObjectURL(widget.videoUrl);
}
widget.element.hidden = true;
widget.value = { hidden: true };
widget.element.replaceChildren();
app.rootGraph?.setDirtyCanvas(true, false);
}