fix: reuse video preview widgets to support Vue Nodes 2.0 mode

- SaveVideo/PreviewVideo: reuse existing video-preview widgets instead of removing and recreating them on each execution, preventing WidgetDOM from losing its mounted element reference in VueNodes mode

- UploadVideo: remove duplicate updateVideoWidget call in handleVideoUpload that caused parallel fetch/decrypt race on the same widget element
This commit is contained in:
Suzu008
2026-05-31 18:02:55 +08:00
parent bc931fb343
commit 919dcffaf2
3 changed files with 39 additions and 31 deletions
+2 -1
View File
@@ -114,4 +114,5 @@ yarn-error.log*
node.zip
.vscode/
.claude/
.*/
.*/
opencode.json
+35 -20
View File
@@ -55,38 +55,53 @@ app.registerExtension({
if (message?.cryptio_images) {
const autoDownload = this.widgets.find((n: any) => n.name === "auto_download")?.value;
// Remove existing video preview widgets
// Get existing video preview widgets (reuse them to avoid re-mount in VueNodes mode)
const existingWidgets = this.widgets.filter((w: any) =>
w.type === "video-preview"
);
for (const widget of existingWidgets) {
clearVideoWidget(widget);
this.removeWidget(widget);
// Remove excess widgets if fewer videos than existing widgets
for (let i = message.cryptio_images.length; i < existingWidgets.length; i++) {
clearVideoWidget(existingWidgets[i]);
this.removeWidget(existingWidgets[i]);
}
// Create widgets and decrypt videos in parallel
// Reuse existing widgets or create new ones as needed
const updatePromises: Promise<void>[] = [];
for (let i = 0; i < message.cryptio_images.length; i++) {
const videoInfo = message.cryptio_images[i];
if (videoInfo.filename && videoInfo.filename.endsWith(".encrypted")) {
const widget = createVideoWidget(this, {
if (!videoInfo.filename || !videoInfo.filename.endsWith(".encrypted")) continue;
let widget = existingWidgets[i];
if (widget) {
// Clean up previous video content before reusing the widget
if (widget.videoElement) {
widget.videoElement.pause();
widget.videoElement.src = "";
}
if (widget.videoUrl) {
URL.revokeObjectURL(widget.videoUrl);
}
widget.videoInfo = videoInfo;
widget.value = { ...widget.value, videoInfo };
} else {
widget = createVideoWidget(this, {
widgetName: `video_${i}`,
videoInfo,
});
updatePromises.push(
updateVideoWidget(widget, videoInfo)
.then((videoUrl) => {
if (autoDownload) {
downloadDecryptedVideo(videoUrl, videoInfo.filename);
console.log(`Auto-downloaded decrypted video: ${videoInfo.filename.replace(/\.encrypted$/, "")}`);
}
})
.catch((error) => {
console.error("Failed to load video:", error);
})
);
}
updatePromises.push(
updateVideoWidget(widget, videoInfo)
.then((videoUrl) => {
if (autoDownload) {
downloadDecryptedVideo(videoUrl, videoInfo.filename);
}
})
.catch((error) => {
console.error("Failed to load video:", error);
})
);
}
await Promise.allSettled(updatePromises);
}
+2 -10
View File
@@ -48,21 +48,13 @@ async function handleVideoUpload(node: CryptIONode, file: File, app: CryptIOApp)
// Upload encrypted video
const result = await uploadEncryptedFile(file);
// Update video widget value
// Update video widget value (callback handles preview update)
const videoWidget = node.widgets.find(
(w: any) => w.name === "video"
);
if (videoWidget) {
videoWidget.value = result.name;
videoWidget.callback?.call(videoWidget, result.name);
}
// Update preview
const previewWidget = node.widgets.find(
(w: any) => w.type === "video-preview"
) as VideoPreviewWidget;
if (previewWidget) {
await updateVideoWidget(previewWidget, result.name);
await videoWidget.callback?.call(videoWidget, result.name);
}
if (app.rootGraph) {