From 4e0bfdd82e59bd55671fb3311d0f6cb24cb69741 Mon Sep 17 00:00:00 2001 From: Suzu008 Date: Tue, 24 Mar 2026 08:53:08 +0800 Subject: [PATCH] refactor: extract duplicated code into shared modules (videoWidgetUtils, uploadUtils, fileUtils) --- ts/cryptioSaveVideo.ts | 150 +++------------------------ ts/cryptioUploadVideo.ts | 192 ++--------------------------------- ts/utils/fileUtils.ts | 16 +++ ts/utils/imageLoader.ts | 19 +--- ts/utils/uploadUtils.ts | 20 ++-- ts/utils/videoLoader.ts | 19 +--- ts/utils/videoWidgetUtils.ts | 172 +++++++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 355 deletions(-) create mode 100644 ts/utils/videoWidgetUtils.ts diff --git a/ts/cryptioSaveVideo.ts b/ts/cryptioSaveVideo.ts index 2f34884..54a5968 100644 --- a/ts/cryptioSaveVideo.ts +++ b/ts/cryptioSaveVideo.ts @@ -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 { + 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) { diff --git a/ts/cryptioUploadVideo.ts b/ts/cryptioUploadVideo.ts index 8e2e2d2..5281b1d 100644 --- a/ts/cryptioUploadVideo.ts +++ b/ts/cryptioUploadVideo.ts @@ -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 { - 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"); diff --git a/ts/utils/fileUtils.ts b/ts/utils/fileUtils.ts index e47206c..6a108bb 100644 --- a/ts/utils/fileUtils.ts +++ b/ts/utils/fileUtils.ts @@ -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); +} diff --git a/ts/utils/imageLoader.ts b/ts/utils/imageLoader.ts index ccb6c18..0bf54cb 100644 --- a/ts/utils/imageLoader.ts +++ b/ts/utils/imageLoader.ts @@ -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; diff --git a/ts/utils/uploadUtils.ts b/ts/utils/uploadUtils.ts index b881f03..a645758 100644 --- a/ts/utils/uploadUtils.ts +++ b/ts/utils/uploadUtils.ts @@ -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 { - console.log("Encrypting and uploading image:", file.name); +export async function uploadEncryptedFile(file: File): Promise { + 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 { return await response.json(); } +/** @deprecated Use uploadEncryptedFile instead */ +export const uploadEncryptedImage = uploadEncryptedFile; + /** * 处理文件上传到节点的通用方法 * @param node 节点实例 diff --git a/ts/utils/videoLoader.ts b/ts/utils/videoLoader.ts index 5ffd8ee..39df0c4 100644 --- a/ts/utils/videoLoader.ts +++ b/ts/utils/videoLoader.ts @@ -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; diff --git a/ts/utils/videoWidgetUtils.ts b/ts/utils/videoWidgetUtils.ts new file mode 100644 index 0000000..0de3546 --- /dev/null +++ b/ts/utils/videoWidgetUtils.ts @@ -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 { + 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); +}