diff --git a/js/comfyui-share-common.js b/js/comfyui-share-common.js index e821f867..20723164 100644 --- a/js/comfyui-share-common.js +++ b/js/comfyui-share-common.js @@ -52,7 +52,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) { // iterate over the images array and add each image to the potential_outputs array for (let j = 0; j < node.images.length; j++) { potential_output_nodes.push(node); - potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title }); + potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title, "node_id": node.id }); } } } @@ -62,7 +62,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) { // iterate over the images array and add each image to the potential_outputs array for (let j = 0; j < node.images.length; j++) { potential_output_nodes.push(node); - potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title }); + potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title, "node_id": node.id }); } } } @@ -81,7 +81,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) { // TODO } potential_output_nodes.push(node); - potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": widgetValue, "format": parsedURLVals.format } }); + potential_outputs.push({ "type": "output", 'title': node.title, "node_id": node.id , "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": widgetValue, "format": parsedURLVals.format } }); } } else if (node.widgets[j].type === "preview") { const widgetValue = node.widgets[j].value; @@ -98,7 +98,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) { // TODO } potential_output_nodes.push(node); - potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": `/view?filename=${parsedURLVals.filename}&subfolder=${parsedURLVals.subfolder}&type=${parsedURLVals.type}&format=${parsedURLVals.format}`, "format": parsedURLVals.format } }); + potential_outputs.push({ "type": "output", 'title': node.title, "node_id": node.id , "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": `/view?filename=${parsedURLVals.filename}&subfolder=${parsedURLVals.subfolder}&type=${parsedURLVals.type}&format=${parsedURLVals.format}`, "format": parsedURLVals.format } }); } } } @@ -137,7 +137,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) { } } - // Note: make sure that two arrays are the same length + // Note: make sure that two arrays are the same length return { potential_outputs, potential_output_nodes }; } @@ -850,29 +850,34 @@ export class ShareDialog extends ComfyDialog { return res; } - show({ potential_outputs, potential_output_nodes, share_option }) { - // Sort `potential_output_nodes` by node ID to make the order always - // consistent, but we should also keep `potential_outputs` in the same - // order as `potential_output_nodes`. - const potential_output_to_order = {}; - potential_output_nodes.forEach((node, index) => { - potential_output_to_order[node.id] = [node, potential_outputs[index]]; - }) - // Sort the object `potential_output_to_order` by key (node ID) - const sorted_potential_output_to_order = Object.fromEntries( - Object.entries(potential_output_to_order).sort((a, b) => a[0].id - b[0].id) - ); - const sorted_potential_outputs = [] - const sorted_potential_output_nodes = [] - for (const [key, value] of Object.entries(sorted_potential_output_to_order)) { - sorted_potential_output_nodes.push(value[0]); - sorted_potential_outputs.push(value[1]); - } - potential_output_nodes = sorted_potential_output_nodes; - potential_outputs = sorted_potential_outputs; + show({potential_outputs, potential_output_nodes, share_option}) { + // Sort `potential_output_nodes` by node ID to make the order always + // consistent, but we should also keep `potential_outputs` in the same + // order as `potential_output_nodes`. + const potential_output_to_order = {}; + potential_output_nodes.forEach((node, index) => { + if (node.id in potential_output_to_order) { + potential_output_to_order[node.id][1].push(potential_outputs[index]); + } else { + potential_output_to_order[node.id] = [node, [potential_outputs[index]]]; + } + }) + // Sort the object `potential_output_to_order` by key (node ID) + const sorted_potential_output_to_order = Object.fromEntries( + Object.entries(potential_output_to_order).sort((a, b) => a[0].id - b[0].id) + ); + const sorted_potential_outputs = [] + const sorted_potential_output_nodes = [] + for (const [key, value] of Object.entries(sorted_potential_output_to_order)) { + sorted_potential_output_nodes.push(value[0]); + sorted_potential_outputs.push(...value[1]); + } + potential_output_nodes = sorted_potential_output_nodes; + potential_outputs = sorted_potential_outputs; // console.log({ potential_outputs, potential_output_nodes }) this.radio_buttons.innerHTML = ""; // clear the radio buttons + let is_radio_button_checked = false; // only check the first radio button if multiple images from the same node const new_radio_buttons = $el("div", { id: "selectOutput-Options", style: { @@ -880,7 +885,7 @@ export class ShareDialog extends ComfyDialog { 'max-height': '400px', } }, potential_outputs.map((output, index) => { - const potential_output_node = potential_output_nodes[index]; + const {node_id} = output; const radio_button = $el("input", { type: 'radio', name: "selectOutputImages", value: index, required: index === 0 }, []) let radio_button_img; if (output.type === "image" || output.type === "temp") { @@ -903,8 +908,9 @@ export class ShareDialog extends ComfyDialog { // Make the radio button checked if it's the selected node, // otherwise make the first radio button checked. if (this.selectedNodeId) { - if (this.selectedNodeId === potential_output_node.id) { + if (this.selectedNodeId === node_id && !is_radio_button_checked) { radio_button.checked = true; + is_radio_button_checked = true; } } else { radio_button.checked = index === 0; diff --git a/js/comfyui-share-openart.js b/js/comfyui-share-openart.js index 77af0c51..6fb22b21 100644 --- a/js/comfyui-share-openart.js +++ b/js/comfyui-share-openart.js @@ -1,6 +1,6 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; +import {app} from "../../scripts/app.js"; +import {api} from "../../scripts/api.js"; +import {ComfyDialog, $el} from "../../scripts/ui.js"; const LOCAL_STORAGE_KEY = "openart_comfy_workflow_key"; const DEFAULT_HOMEPAGE_URL = "https://openart.ai/workflows/dev?developer=true"; @@ -83,11 +83,11 @@ export class OpenArtShareDialog extends ComfyDialog { async saveKey(value) { await api.fetchApi(`/manager/set_openart_auth`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - openart_key: value - }) - }); + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + openart_key: value + }) + }); } createButtons() { @@ -195,14 +195,14 @@ export class OpenArtShareDialog extends ComfyDialog { // Header Section const headerSection = $el("h3", { textContent: "Share your workflow to OpenArt", - size: 3, - color: "white", - style: { - 'text-align': 'center', - color: 'white', - margin: '0 0 10px 0', - } - }); + size: 3, + color: "white", + style: { + 'text-align': 'center', + color: 'white', + margin: '0 0 10px 0', + } + }); // LinkSection this.communityLink = $el("a", { @@ -234,14 +234,19 @@ export class OpenArtShareDialog extends ComfyDialog { ); // Account Section - const accountSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["1️⃣ OpenArt API Key"]), + const accountSection = $el("div", {style: sectionStyle}, [ + $el("label", {style: labelStyle}, ["1️⃣ OpenArt API Key"]), this.keyInput, ]); // Output Upload Section - const outputUploadSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: {...labelStyle, margin: "10px 0 0 0"} }, ["2️⃣ Image/Thumbnail (Required)"]), + const outputUploadSection = $el("div", {style: sectionStyle}, [ + $el("label", { + style: { + ...labelStyle, + margin: "10px 0 0 0" + } + }, ["2️⃣ Image/Thumbnail (Required)"]), this.previewImage, this.uploadImagesInput, ]); @@ -249,17 +254,20 @@ export class OpenArtShareDialog extends ComfyDialog { // Outputs Section this.outputsSection = $el("div", { id: "selectOutputs", - }, []); + }, []); // Additional Inputs Section - const additionalInputsSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["3️⃣ Workflow Information"]), + const additionalInputsSection = $el("div", {style: sectionStyle}, [ + $el("label", {style: labelStyle}, ["3️⃣ Workflow Information"]), this.NameInput, this.descriptionInput, ]); // OpenArt Contest Section - this.joinContestCheckbox = $el("input",{type:'checkbox', id:"join_contest"},[]) + this.joinContestCheckbox = $el("input", { + type: 'checkbox', + id: "join_contest" + }, []) this.joinContestDescription = $el("a", { style: { ...hyperLinkStyle, @@ -278,12 +286,12 @@ export class OpenArtShareDialog extends ComfyDialog { alignItems: 'center', cursor: 'pointer', } - },[this.joinContestCheckbox, this.joinContestDescription]) - const contestSection = $el("div", { style: sectionStyle }, [ + }, [this.joinContestCheckbox, this.joinContestDescription]) + const contestSection = $el("div", {style: sectionStyle}, [ this.joinContestLabel, ]); - // Message Section + // Message Section this.message = $el( "div", { @@ -397,7 +405,7 @@ export class OpenArtShareDialog extends ComfyDialog { ); if (res.ok && res.data) { - const { image_url, width, height } = res.data; + const {image_url, width, height} = res.data; this.uploadedImages.push({ url: image_url, width, @@ -485,7 +493,7 @@ export class OpenArtShareDialog extends ComfyDialog { "/workflows/publish", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workflow_json: workflowJSON, upload_images: this.uploadedImages, @@ -501,7 +509,7 @@ export class OpenArtShareDialog extends ComfyDialog { ); if (response.ok) { - const { workflow_id } = response.data; + const {workflow_id} = response.data; if (workflow_id) { const url = `https://openart.ai/workflows/-/-/${workflow_id}`; this.message.innerHTML = `Workflow has been shared successfully. Click here to view it.`; @@ -530,23 +538,27 @@ export class OpenArtShareDialog extends ComfyDialog { return blob; } - async show({ potential_outputs, potential_output_nodes } = {}) { + async show({potential_outputs, potential_output_nodes} = {}) { // Sort `potential_output_nodes` by node ID to make the order always // consistent, but we should also keep `potential_outputs` in the same // order as `potential_output_nodes`. const potential_output_to_order = {}; potential_output_nodes.forEach((node, index) => { - potential_output_to_order[node.id] = [node, potential_outputs[index]]; + if (node.id in potential_output_to_order) { + potential_output_to_order[node.id][1].push(potential_outputs[index]); + } else { + potential_output_to_order[node.id] = [node, [potential_outputs[index]]]; + } }) // Sort the object `potential_output_to_order` by key (node ID) const sorted_potential_output_to_order = Object.fromEntries( - Object.entries(potential_output_to_order).sort((a, b) => a[0].id - b[0].id) + Object.entries(potential_output_to_order).sort((a, b) => a[0].id - b[0].id) ); const sorted_potential_outputs = [] const sorted_potential_output_nodes = [] for (const [key, value] of Object.entries(sorted_potential_output_to_order)) { - sorted_potential_output_nodes.push(value[0]); - sorted_potential_outputs.push(value[1]); + sorted_potential_output_nodes.push(value[0]); + sorted_potential_outputs.push(...value[1]); } potential_output_nodes = sorted_potential_output_nodes; potential_outputs = sorted_potential_outputs; @@ -561,9 +573,9 @@ export class OpenArtShareDialog extends ComfyDialog { this.uploadedImages = []; // If `selectedNodeId` is provided, we will select the corresponding radio - // button for the node. In addition, we move the selected radio button to - // the top of the list. - if (this.selectedNodeId) { + // button for the node. In addition, we move the selected radio button to + // the top of the list. + if (this.selectedNodeId) { const index = potential_output_nodes.findIndex(node => node.id === this.selectedNodeId); if (index >= 0) { this.selectedOutputIndex = index; @@ -573,13 +585,13 @@ export class OpenArtShareDialog extends ComfyDialog { this.radioButtons = []; const new_radio_buttons = $el("div", { - id: "selectOutput-Options", - style: { - 'overflow-y': 'scroll', - 'max-height': '200px', + id: "selectOutput-Options", + style: { + 'overflow-y': 'scroll', + 'max-height': '200px', - 'display': 'grid', - 'grid-template-columns': 'repeat(auto-fit, minmax(100px, 1fr))', + 'display': 'grid', + 'grid-template-columns': 'repeat(auto-fit, minmax(100px, 1fr))', 'grid-template-rows': 'auto', 'grid-column-gap': '10px', 'grid-row-gap': '10px', @@ -588,121 +600,146 @@ export class OpenArtShareDialog extends ComfyDialog { 'border-radius': '8px', 'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.05)', 'background-color': 'var(--bg-color)', - } - }, - potential_outputs.map((output, index) => { - const radio_button = $el("input", { type: 'radio', name: "selectOutputImages", value: index, required: index === 0 }, []) - let radio_button_img; - let filename; - if (output.type === "image" || output.type === "temp") { - radio_button_img = $el("img", { src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`, style: { width: "100px", height: "100px", objectFit: "cover", borderRadius: "5px" } }, []); - filename = output.image.filename - } else if (output.type === "output") { - radio_button_img = $el("img", { src: output.output.value, style: { width: "auto", height: "100px", objectFit: "cover", borderRadius: "5px" } }, []); - filename = output.filename - } else { - // unsupported output type - // this should never happen - // TODO - radio_button_img = $el("img", { src: "", style: { width: "auto", height: "100px" } }, []); - } - const radio_button_text = $el("span", { - style: { - color: 'gray', - display: 'block', - fontSize: '12px', - overflowX: 'hidden', - textOverflow: 'ellipsis', - textWrap: 'nowrap', - maxWidth: '100px', - } - }, [output.title]) - const node_id_chip = $el("span", { - style: { - color: '#FBFBFD', - display: 'block', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - fontSize: '12px', - overflowX: 'hidden', - padding: '2px 3px', - textOverflow: 'ellipsis', - textWrap: 'nowrap', - maxWidth: '100px', - position: 'absolute', - top: '3px', - left: '3px', - borderRadius: '3px', - } - }, [`Node: ${potential_output_nodes[index].id}`]) - radio_button.style.color = "var(--fg-color)"; - radio_button.checked = this.selectedOutputIndex === index; + } + }, + potential_outputs.map((output, index) => { + const {node_id} = output; + const radio_button = $el("input", { + type: 'radio', + name: "selectOutputImages", + value: index, + required: index === 0 + }, []) + let radio_button_img; + let filename; + if (output.type === "image" || output.type === "temp") { + radio_button_img = $el("img", { + src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`, + style: { + width: "100px", + height: "100px", + objectFit: "cover", + borderRadius: "5px" + } + }, []); + filename = output.image.filename + } else if (output.type === "output") { + radio_button_img = $el("img", { + src: output.output.value, + style: { + width: "auto", + height: "100px", + objectFit: "cover", + borderRadius: "5px" + } + }, []); + filename = output.filename + } else { + // unsupported output type + // this should never happen + // TODO + radio_button_img = $el("img", { + src: "", + style: {width: "auto", height: "100px"} + }, []); + } + const radio_button_text = $el("span", { + style: { + color: 'gray', + display: 'block', + fontSize: '12px', + overflowX: 'hidden', + textOverflow: 'ellipsis', + textWrap: 'nowrap', + maxWidth: '100px', + } + }, [output.title]) + const node_id_chip = $el("span", { + style: { + color: '#FBFBFD', + display: 'block', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + fontSize: '12px', + overflowX: 'hidden', + padding: '2px 3px', + textOverflow: 'ellipsis', + textWrap: 'nowrap', + maxWidth: '100px', + position: 'absolute', + top: '3px', + left: '3px', + borderRadius: '3px', + } + }, [`Node: ${node_id}`]) + radio_button.style.color = "var(--fg-color)"; + radio_button.checked = this.selectedOutputIndex === index; - radio_button.onchange = async () => { - this.selectedOutputIndex = parseInt(radio_button.value); + radio_button.onchange = async () => { + this.selectedOutputIndex = parseInt(radio_button.value); // Remove the "checked" class from all radio buttons this.radioButtons.forEach((ele) => { ele.parentElement.classList.remove("checked"); }); - radio_button.parentElement.classList.add("checked"); + radio_button.parentElement.classList.add("checked"); - this.fetchImageBlob(radio_button_img.src).then((blob) => { - const file = new File([blob], filename, { + this.fetchImageBlob(radio_button_img.src).then((blob) => { + const file = new File([blob], filename, { type: blob.type, }); - this.previewImage.src = radio_button_img.src; + this.previewImage.src = radio_button_img.src; this.previewImage.style.display = "block"; this.selectedFile = file; - }) + }) - // Add the opacity style toggle here to indicate that they only need + // Add the opacity style toggle here to indicate that they only need // to upload one image or choose one from the outputs. this.outputsSection.style.opacity = 1; this.uploadImagesInput.style.opacity = 0.35; - }; + }; - if (radio_button.checked) { - this.fetchImageBlob(radio_button_img.src).then((blob) => { - const file = new File([blob], filename, { + if (radio_button.checked) { + this.fetchImageBlob(radio_button_img.src).then((blob) => { + const file = new File([blob], filename, { type: blob.type, }); - this.previewImage.src = radio_button_img.src; + this.previewImage.src = radio_button_img.src; this.previewImage.style.display = "block"; this.selectedFile = file; - }) - // Add the opacity style toggle here to indicate that they only need + }) + // Add the opacity style toggle here to indicate that they only need // to upload one image or choose one from the outputs. this.outputsSection.style.opacity = 1; this.uploadImagesInput.style.opacity = 0.35; - } + } - this.radioButtons.push(radio_button); + this.radioButtons.push(radio_button); - return $el(`label.output_label${radio_button.checked ? '.checked' : ''}`, { - style: { - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - marginBottom: "10px", - cursor: "pointer", - position: 'relative', - } - }, [radio_button_img, radio_button_text, radio_button, node_id_chip]); - }) - ); + return $el(`label.output_label${radio_button.checked ? '.checked' : ''}`, { + style: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + marginBottom: "10px", + cursor: "pointer", + position: 'relative', + } + }, [radio_button_img, radio_button_text, radio_button, node_id_chip]); + }) + ); - const header = - $el("p", { - textContent: this.radioButtons.length === 0 ? "Queue Prompt to see the outputs" : "Or choose one from the outputs (scroll to see all)", - size: 2, - color: "white", - style: { - color: 'white', - margin: '0 0 5px 0', - fontSize: '12px', - }, - }, []) + const header = + $el("p", { + textContent: this.radioButtons.length === 0 ? "Queue Prompt to see the outputs" : "Or choose one from the outputs (scroll to see all)", + size: 2, + color: "white", + style: { + color: 'white', + margin: '0 0 5px 0', + fontSize: '12px', + }, + }, []) this.outputsSection.innerHTML = ""; this.outputsSection.appendChild(header); this.outputsSection.appendChild(new_radio_buttons);