v4: Remove share destinations that no longer accept workflows (#3500)

Four of the six share destinations of the legacy UI are gone: the
comfyworkflows.com domain registration has expired, the Copus API
answers 410 "ComfyUI direct publishing is no longer supported", the
OpenArt workflow API and pages return 404 or redirect to the home page,
and esheep.com no longer resolves. Manager still offered them, and the
ComfyWorkflows flow would hand the share key, the workflow and the
installed snapshot to whoever answers at that domain next.

- Remove ComfyWorkflows, Copus, OpenArt and eSheep from the share
  chooser, the Share setting, the Workflow Gallery menu and the share
  dialog, with their server routes, the two dialog modules and what the
  Copus theme added to the chooser
- Treat a saved Share setting that names a removed site as All, and do
  not open a removed site saved as the last visited Workflow Gallery
- Sharing to YouML and to Matrix is unchanged; the share request no
  longer carries cw_auth or is_nsfw, which only ComfyWorkflows used
- Update README, CHANGELOG and the Playwright scenario notes
This commit is contained in:
Dr.Lt.Data
2026-11-12 21:47:35 +09:00
committed by GitHub
parent 9e72648931
commit 857e2ca5a5
10 changed files with 64 additions and 2643 deletions
+13
View File
@@ -12,6 +12,19 @@ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/
- Require Python 3.10 or newer to match ComfyUI's minimum supported version.
Python 3.9 is no longer supported.
### Removed
- Remove four share destinations of the legacy UI that no longer take workflows
from Manager: ComfyWorkflows (its domain registration has expired), Copus (its
API answers that publishing from ComfyUI is no longer supported), OpenArt (its
workflow API and pages are gone), and eSheep (its site no longer resolves).
Sharing to YouML and to the Matrix server is unchanged. A saved `Share`
setting that names a removed site now acts as `All`, and the Workflow Gallery
button no longer opens a removed site that was visited last. The files saved
for the removed sites are no longer read; `comfyworkflows_sharekey`,
`.openart_key`, and `esheep_share_message.json` under
`<USER_DIRECTORY>/__manager/` can be deleted.
### Security
- Allow administrators to exempt selected Registry IDs from the flagged CNR
+1 -2
View File
@@ -79,8 +79,7 @@ Requires Python 3.10 or newer, matching ComfyUI's minimum Python version.
![menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/topbar.jpg) ![share](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/share.jpg)
* You can share the workflow by clicking the Share button at the bottom of the main menu or selecting Share Output from the Context Menu of the Image node.
* Currently, it supports sharing via [https://comfyworkflows.com/](https://comfyworkflows.com/),
[https://openart.ai](https://openart.ai/workflows/dev), [https://youml.com](https://youml.com)
* Currently, it supports sharing via [https://youml.com](https://youml.com)
as well as through the Matrix channel.
![menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/share-setting.jpg)
-199
View File
@@ -1,13 +1,10 @@
import mimetypes
from ..common import context
from ..common import manager_util
from . import manager_core as core
import os
from aiohttp import web
import aiohttp
import json
import hashlib
import folder_paths
from server import PromptServer
@@ -23,49 +20,6 @@ except Exception:
matrix_nio_is_available = False
def extract_model_file_names(json_data):
"""Extract unique file names from the input JSON data."""
file_names = set()
model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
# Recursively search for file names in the JSON data
def recursive_search(data):
if isinstance(data, dict):
for value in data.values():
recursive_search(value)
elif isinstance(data, list):
for item in data:
recursive_search(item)
elif isinstance(data, str) and '.' in data:
file_names.add(os.path.basename(data)) # file_names.add(data)
recursive_search(json_data)
return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
def find_file_paths(base_dir, file_names):
"""Find the paths of the files in the base directory."""
file_paths = {}
for root, dirs, files in os.walk(base_dir):
# Exclude certain directories
dirs[:] = [d for d in dirs if d not in ['.git']]
for file in files:
if file in file_names:
file_paths[file] = os.path.join(root, file)
return file_paths
def compute_sha256_checksum(filepath):
"""Compute the SHA256 checksum of a file, in chunks"""
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
@PromptServer.instance.routes.get("/v2/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
@@ -77,17 +31,6 @@ async def share_option(request):
return web.Response(status=200)
def get_openart_auth():
if not os.path.exists(os.path.join(context.manager_files_path, ".openart_key")):
return None
try:
with open(os.path.join(context.manager_files_path, ".openart_key"), "r") as f:
openart_key = f.read().strip()
return openart_key if openart_key else None
except Exception:
return None
def get_matrix_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "matrix_auth")):
return None
@@ -106,19 +49,6 @@ def get_matrix_auth():
return None
def get_comfyworkflows_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "comfyworkflows_sharekey")):
return None
try:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
share_key = f.read()
if not share_key.strip():
return None
return share_key
except Exception:
return None
def get_youml_settings():
if not os.path.exists(os.path.join(context.manager_files_path, ".youml")):
return None
@@ -135,24 +65,6 @@ def set_youml_settings(settings):
f.write(settings)
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
if not openart_key:
return web.Response(status=404)
return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
async def api_set_openart_auth(request):
json_data = await request.json()
openart_key = json_data['openart_key']
with open(os.path.join(context.manager_files_path, ".openart_key"), "w") as f:
f.write(openart_key)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...")
@@ -177,32 +89,6 @@ async def api_set_youml_settings(request):
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
# print("Getting stored Comfyworkflows.com auth...")
comfyworkflows_auth = get_comfyworkflows_auth()
if not comfyworkflows_auth:
return web.Response(status=404)
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request):
json_data = await request.json()
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
json.dump(json_data, file, indent=4)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request):
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
@@ -219,35 +105,23 @@ def set_matrix_auth(json_data):
f.write("\n".join([homeserver, username, password]))
def set_comfyworkflows_auth(comfyworkflows_sharekey):
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
f.write(comfyworkflows_sharekey)
def has_provided_matrix_auth(matrix_auth):
return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
return comfyworkflows_sharekey.strip()
@PromptServer.instance.routes.post("/v2/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
matrix_auth = json_data['matrix_auth']
comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
set_matrix_auth(matrix_auth)
set_comfyworkflows_auth(comfyworkflows_sharekey)
share_destinations = json_data['share_destinations']
credits = json_data['credits']
title = json_data['title']
description = json_data['description']
is_nsfw = json_data['is_nsfw']
prompt = json_data['prompt']
potential_outputs = json_data['potential_outputs']
selected_output_index = json_data['selected_output_index']
@@ -279,76 +153,6 @@ async def share_art(request):
# get the mime type of the asset
assetFileType = mimetypes.guess_type(asset_filepath)[0]
share_website_host = "UNKNOWN"
if "comfyworkflows" in share_destinations:
share_website_host = "https://comfyworkflows.com"
share_endpoint = f"{share_website_host}/api"
# get presigned urls
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not manager_util.bypass_ssl)) as session:
async with session.post(
f"{share_endpoint}/get_presigned_urls",
json={
"assetFileName": asset_filename,
"assetFileType": assetFileType,
"workflowJsonFileName": 'workflow.json',
"workflowJsonFileType": 'application/json',
},
) as resp:
assert resp.status == 200
presigned_urls_json = await resp.json()
assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
assetFileKey = presigned_urls_json["assetFileKey"]
workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
# upload asset
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not manager_util.bypass_ssl)) as session:
async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
assert resp.status == 200
# upload workflow json
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not manager_util.bypass_ssl)) as session:
async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
assert resp.status == 200
model_filenames = extract_model_file_names(prompt['workflow'])
model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
models_info = {}
for filename, filepath in model_file_paths.items():
models_info[filename] = {
"filename": filename,
"sha256_checksum": compute_sha256_checksum(filepath),
"relative_path": os.path.relpath(filepath, folder_paths.base_path),
}
# make a POST request to /api/upload_workflow with form data key values
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not manager_util.bypass_ssl)) as session:
form = aiohttp.FormData()
if comfyworkflows_sharekey:
form.add_field("shareKey", comfyworkflows_sharekey)
form.add_field("source", "comfyui_manager")
form.add_field("assetFileKey", assetFileKey)
form.add_field("assetFileType", assetFileType)
form.add_field("workflowJsonFileKey", workflowJsonFileKey)
form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
form.add_field("shareWorkflowCredits", credits)
form.add_field("shareWorkflowTitle", title)
form.add_field("shareWorkflowDescription", description)
form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
form.add_field("currentSnapshot", json.dumps(await core.get_current_snapshot()))
form.add_field("modelsInfo", json.dumps(models_info))
async with session.post(
f"{share_endpoint}/upload_workflow",
data=form,
) as resp:
assert resp.status == 200
upload_workflow_json = await resp.json()
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if matrix_nio_is_available and "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
@@ -443,9 +247,6 @@ async def share_art(request):
return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
return web.json_response({
"comfyworkflows": {
"url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
},
"matrix": {
"success": None if "matrix" not in share_destinations else True
}
-2
View File
@@ -13,8 +13,6 @@ This directory contains the JavaScript frontend implementation for ComfyUI-Manag
## Sharing Components
- **comfyui-share-common.js**: Base functionality for workflow sharing features.
- **comfyui-share-copus.js**: Integration with the ComfyUI Copus sharing platform.
- **comfyui-share-openart.js**: Integration with the OpenArt sharing platform.
- **comfyui-share-youml.js**: Integration with the YouML sharing platform.
## Utility Components
+34 -79
View File
@@ -6,11 +6,9 @@ import {
ShareDialog,
ShareDialogChooser,
getPotentialOutputsAndOutputNodes,
showOpenArtShareDialog,
showShareDialog,
showYouMLShareDialog
} from "./comfyui-share-common.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import {
free_models, install_pip, install_via_git_url, manager_instance,
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
@@ -236,6 +234,25 @@ var restart_stop_button = null;
var update_policy_combo = null;
let share_option = 'all';
const share_options = [
['none', 'None'],
['youml', 'YouML'],
['matrix', 'Matrix Server'],
['all', 'All'],
];
// The saved choice may be a share site that Manager no longer has
function to_share_option(value) {
return share_options.some(option => option[0] === value) ? value : 'all';
}
const youml_gallery_url = "https://youml.com/?from=comfyui-share";
// The gallery opened last, unless Manager no longer has it
function last_visited_gallery() {
const url = localStorage.getItem("wg_last_visited");
return url === youml_gallery_url ? url : null;
}
var batch_id = null;
@@ -410,7 +427,7 @@ async function init_share_option() {
api.fetchApi('/v2/manager/share_option')
.then(response => response.text())
.then(data => {
share_option = data || 'all';
share_option = to_share_option(data);
});
}
@@ -1000,15 +1017,6 @@ class ManagerMenuDialog extends ComfyDialog {
let share_combo = document.createElement("select");
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
share_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
const share_options = [
['none', 'None'],
['openart', 'OpenArt AI'],
['youml', 'YouML'],
['matrix', 'Matrix Server'],
['comfyworkflows', 'ComfyWorkflows'],
['copus', 'Copus'],
['all', 'All'],
];
for (const option of share_options) {
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
}
@@ -1016,8 +1024,8 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi('/v2/manager/share_option')
.then(response => response.text())
.then(data => {
share_combo.value = data || 'all';
share_option = data || 'all';
share_option = to_share_option(data);
share_combo.value = share_option;
});
share_combo.addEventListener('change', function (event) {
@@ -1149,7 +1157,7 @@ class ManagerMenuDialog extends ComfyDialog {
// ...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
},
onclick: (e) => {
const last_visited_site = localStorage.getItem("wg_last_visited")
const last_visited_site = last_visited_gallery();
if (!!last_visited_site) {
window.open(last_visited_site, last_visited_site);
} else {
@@ -1169,7 +1177,7 @@ class ManagerMenuDialog extends ComfyDialog {
}, [
$el("p", {
id: 'workflowgallery-button-last-visited-label',
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : 'none selected'})`,
textContent: `(${last_visited_gallery()?.split('/')[2] ?? 'none selected'})`,
style: {
'text-align': 'center',
'color': 'var(--input-text)',
@@ -1180,7 +1188,7 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("div.pysssss-workflow-arrow-2", {
id: `comfyworkflows-button-arrow`,
id: `workflowgallery-button-arrow`,
onclick: this.handleWorkflowGalleryButtonClick
})
]),
@@ -1259,11 +1267,8 @@ class ManagerMenuDialog extends ComfyDialog {
{
title: "Share your art",
callback: () => {
if (share_option === 'openart') {
showOpenArtShareDialog();
return;
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
showShareDialog(share_option);
if (share_option === 'matrix') {
showShareDialog();
return;
} else if (share_option === 'youml') {
showYouMLShareDialog();
@@ -1276,49 +1281,12 @@ class ManagerMenuDialog extends ComfyDialog {
ShareDialogChooser.instance.show();
},
},
{
title: "Open 'openart.ai'",
callback: () => {
const url = "https://openart.ai/workflows/dev";
localStorage.setItem("wg_last_visited", url);
window.open(url, url);
modifyButtonStyle(url);
},
},
{
title: "Open 'youml.com'",
callback: () => {
const url = "https://youml.com/?from=comfyui-share";
localStorage.setItem("wg_last_visited", url);
window.open(url, url);
modifyButtonStyle(url);
},
},
{
title: "Open 'comfyworkflows.com'",
callback: () => {
const url = "https://comfyworkflows.com/";
localStorage.setItem("wg_last_visited", url);
window.open(url, url);
modifyButtonStyle(url);
},
},
{
title: "Open 'esheep'",
callback: () => {
const url = "https://www.esheep.com";
localStorage.setItem("wg_last_visited", url);
window.open(url, url);
modifyButtonStyle(url);
},
},
{
title: "Open 'Copus.io'",
callback: () => {
const url = "https://www.copus.io";
localStorage.setItem("wg_last_visited", url);
window.open(url, url);
modifyButtonStyle(url);
localStorage.setItem("wg_last_visited", youml_gallery_url);
window.open(youml_gallery_url, youml_gallery_url);
modifyButtonStyle(youml_gallery_url);
},
},
{
@@ -1450,11 +1418,8 @@ app.registerExtension({
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "share",
action: () => {
if (share_option === 'openart') {
showOpenArtShareDialog();
return;
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
showShareDialog(share_option);
if (share_option === 'matrix') {
showShareDialog();
return;
} else if (share_option === 'youml') {
showYouMLShareDialog();
@@ -1488,11 +1453,8 @@ app.registerExtension({
shareButton.id = "shareButton";
shareButton.textContent = "Share";
shareButton.onclick = () => {
if (share_option === 'openart') {
showOpenArtShareDialog();
return;
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
showShareDialog(share_option);
if (share_option === 'matrix') {
showShareDialog();
return;
} else if (share_option === 'youml') {
showYouMLShareDialog();
@@ -1546,13 +1508,6 @@ app.registerExtension({
const shareButton = document.getElementById("shareButton");
if (shareButton) {
const currentNode = this;
if (!OpenArtShareDialog.instance) {
OpenArtShareDialog.instance = new OpenArtShareDialog();
}
OpenArtShareDialog.instance.selectedNodeId = currentNode.id;
if (!ShareDialog.instance) {
ShareDialog.instance = new ShareDialog(share_option);
}
ShareDialog.instance.selectedNodeId = currentNode.id;
shareButton.click();
}
+15 -248
View File
@@ -1,10 +1,8 @@
import { api } from "../../scripts/api.js";
import { app } from "../../scripts/app.js";
import { $el, ComfyDialog } from "../../scripts/ui.js";
import { CopusShareDialog } from "./comfyui-share-copus.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import { YouMLShareDialog } from "./comfyui-share-youml.js";
import { customAlert, sanitizeHTML, sanitizeUrl } from "./common.js";
import { customAlert } from "./common.js";
export const SUPPORTED_OUTPUT_NODE_TYPES = [
"PreviewImage",
@@ -166,61 +164,6 @@ export function parseURLPath(urlPath) {
}
export const shareToEsheep= () => {
app.graphToPrompt()
.then(prompt => {
const nodes = app.graph._nodes
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
const workflow = prompt['workflow']
api.fetchApi(`/v2/manager/set_esheep_workflow_and_images`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workflow: workflow,
images: potential_outputs
})
}).then(response => {
var domain = window.location.hostname;
var port = window.location.port;
port = port || (window.location.protocol === 'http:' ? '80' : window.location.protocol === 'https:' ? '443' : '');
var full_domin = domain + ':' + port
window.open('https://www.esheep.com/app/workflow_upload?from_local='+ full_domin, '_blank');
});
})
}
export const showCopusShareDialog = () => {
if (!CopusShareDialog.instance) {
CopusShareDialog.instance = new CopusShareDialog();
}
return app.graphToPrompt()
.then(prompt => {
return app.graph._nodes;
})
.then(nodes => {
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
CopusShareDialog.instance.show({ potential_outputs, potential_output_nodes});
})
}
export const showOpenArtShareDialog = () => {
if (!OpenArtShareDialog.instance) {
OpenArtShareDialog.instance = new OpenArtShareDialog();
}
return app.graphToPrompt()
.then(prompt => {
// console.log({ prompt })
return app.graph._nodes;
})
.then(nodes => {
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
OpenArtShareDialog.instance.show({ potential_outputs, potential_output_nodes});
})
}
export const showYouMLShareDialog = () => {
if (!YouMLShareDialog.instance) {
YouMLShareDialog.instance = new YouMLShareDialog();
@@ -237,9 +180,9 @@ export const showYouMLShareDialog = () => {
}
export const showShareDialog = async (share_option) => {
export const showShareDialog = async () => {
if (!ShareDialog.instance) {
ShareDialog.instance = new ShareDialog(share_option);
ShareDialog.instance = new ShareDialog();
}
return app.graphToPrompt()
.then(prompt => {
@@ -259,7 +202,7 @@ export const showShareDialog = async (share_option) => {
}
return false;
}
ShareDialog.instance.show({ potential_outputs, potential_output_nodes, share_option });
ShareDialog.instance.show({ potential_outputs, potential_output_nodes });
return true;
});
}
@@ -282,17 +225,6 @@ export class ShareDialogChooser extends ComfyDialog {
createButtons() {
const buttons = [
{
key: "openart",
textContent: "OpenArt AI",
website: "https://openart.ai/workflows/",
description: "Share ComfyUI workflows and art on OpenArt.ai",
onclick: () => {
showOpenArtShareDialog();
this.close();
}
},
{
key: "youml",
textContent: "YouML",
website: "https://youml.com",
description: "Share your workflow or transform it into an interactive app on YouML.com",
@@ -302,47 +234,15 @@ export class ShareDialogChooser extends ComfyDialog {
}
},
{
key: "matrix",
textContent: "Matrix Server",
website: "https://app.element.io/#/room/%23comfyui_space%3Amatrix.org",
description: "Share your art on the official ComfyUI matrix server",
onclick: async () => {
showShareDialog('matrix').then((suc) => {
showShareDialog().then((suc) => {
suc && this.close();
})
}
},
{
key: "comfyworkflows",
textContent: "ComfyWorkflows",
website: "https://comfyworkflows.com",
description: "Share & browse thousands of ComfyUI workflows and art 🎨<br/><br/><a style='color:var(--input-text);' href='https://comfyworkflows.com' target='_blank'>ComfyWorkflows.com</a>",
onclick: () => {
showShareDialog('comfyworkflows').then((suc) => {
suc && this.close();
})
}
},
{
key: "esheep",
textContent: "eSheep",
website: "https://www.esheep.com",
description: "Share & download thousands of ComfyUI workflows on <a style='color:var(--input-text);' href='https://www.esheep.com' target='_blank'>esheep.com</a>",
onclick: () => {
shareToEsheep();
this.close();
}
},
{
key: "Copus",
textContent: "Copus",
website: "https://www.copus.io",
description: "🔴 Earn simple. Get paid from your ComfyUI workflows—no revenue sharing. Ever.",
onclick: () => {
showCopusShareDialog();
this.close();
}
},
];
function createShareButtonsWithDescriptions() {
@@ -357,8 +257,7 @@ export class ShareDialogChooser extends ComfyDialog {
});
buttons.forEach(b => {
const button = $el("button",
{
const button = $el("button", {
type: "button",
textContent: b.textContent,
onclick: b.onclick,
@@ -371,14 +270,8 @@ export class ShareDialogChooser extends ComfyDialog {
'padding': '5px 5px',
'margin-bottom': '5px',
'transition': 'background-color 0.3s',
'position':'relative'
}
},
[
$el("span", { style: {
} }),
]
);
});
button.addEventListener('mouseover', () => {
button.style.backgroundColor = '#007BFF'; // Change color on hover
});
@@ -387,7 +280,7 @@ export class ShareDialogChooser extends ComfyDialog {
});
const description = $el("p", {
innerHTML: b.description,
textContent: b.description,
style: {
'text-align': 'left',
color: 'var(--input-text)',
@@ -396,28 +289,6 @@ export class ShareDialogChooser extends ComfyDialog {
},
});
const copus_ui =$el("div", { style: {
'position': 'absolute',
'height': '100%',
'left': '-25px',
'top': '-26px',
'width': '100%',
'z-index':'-1',
'background':'url("https://static.copus.io/images/client/202412/test/f28ac6ef8f4c6f3d5d50856a272ed02c.png")',
'background-repeat': 'no-repeat',
} });
const copus_ui_bottom =$el("div", { style: {
'position': 'absolute',
'height': '100%',
'left': '25px',
'bottom': '-26px',
'width': '100%',
'transform':'scale(-1, -1)',
'z-index':'-1',
'background':'url("https://static.copus.io/images/client/202412/test/f28ac6ef8f4c6f3d5d50856a272ed02c.png")',
'background-repeat': 'no-repeat',
} });
const websiteLink = $el("a", {
textContent: "🌐 Website",
href: b.website,
@@ -455,17 +326,8 @@ export class ShareDialogChooser extends ComfyDialog {
'border': '1px solid #ddd',
'border-radius': '5px',
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
'position':'relative'
}
}, [buttonLinkContainer, description
,
b.key ==='Copus' ?
copus_ui
:'',
b.key ==='Copus' ?
copus_ui_bottom
:'',
]);
}, [buttonLinkContainer, description]);
container.appendChild(column);
});
@@ -519,11 +381,9 @@ export class ShareDialogChooser extends ComfyDialog {
export class ShareDialog extends ComfyDialog {
static instance = null;
static matrix_auth = { homeserver: "matrix.org", username: "", password: "" };
static cw_sharekey = "";
constructor(share_option) {
constructor() {
super();
this.share_option = share_option;
this.element = $el("div.comfy-modal", {
parent: document.body, style: {
'overflow-y': "auto",
@@ -541,16 +401,9 @@ export class ShareDialog extends ComfyDialog {
id: "selectOutputImages",
}, []);
this.is_nsfw_checkbox = $el("input", { type: 'checkbox', id: "is_nsfw" }, [])
const is_nsfw_checkbox_text = $el("label", {
}, [" Is this NSFW?"])
this.is_nsfw_checkbox.style.color = "var(--fg-color)";
this.is_nsfw_checkbox.checked = false;
this.matrix_destination_checkbox = $el("input", { type: 'checkbox', id: "matrix_destination" }, [])
const matrix_destination_checkbox_text = $el("label", {}, [" ComfyUI Matrix server"])
this.matrix_destination_checkbox.style.color = "var(--fg-color)";
this.matrix_destination_checkbox.checked = this.share_option === 'matrix'; //true;
try {
api.fetchApi(`/v2/manager/get_matrix_dep_status`)
@@ -566,18 +419,10 @@ export class ShareDialog extends ComfyDialog {
.catch(error => {});
} catch (error) {}
this.comfyworkflows_destination_checkbox = $el("input", { type: 'checkbox', id: "comfyworkflows_destination" }, [])
const comfyworkflows_destination_checkbox_text = $el("label", {}, [" ComfyWorkflows.com"])
this.comfyworkflows_destination_checkbox.style.color = "var(--fg-color)";
this.comfyworkflows_destination_checkbox.checked = this.share_option !== 'matrix';
this.matrix_homeserver_input = $el("input", { type: 'text', id: "matrix_homeserver", placeholder: "matrix.org", value: ShareDialog.matrix_auth.homeserver || 'matrix.org' }, []);
this.matrix_username_input = $el("input", { type: 'text', placeholder: "Username", value: ShareDialog.matrix_auth.username || '' }, []);
this.matrix_password_input = $el("input", { type: 'password', placeholder: "Password", value: ShareDialog.matrix_auth.password || '' }, []);
this.cw_sharekey_input = $el("input", { type: 'text', placeholder: "Share key (found on your profile page)", value: ShareDialog.cw_sharekey || '' }, []);
this.cw_sharekey_input.style.width = "100%";
this.credits_input = $el("input", {
type: "text",
placeholder: "This will be used to give credits",
@@ -626,8 +471,6 @@ export class ShareDialog extends ComfyDialog {
textContent: "Close",
onclick: () => {
// Reset state
this.matrix_destination_checkbox.checked = this.share_option === 'matrix';
this.comfyworkflows_destination_checkbox.checked = this.share_option !== 'matrix';
this.share_button.textContent = "Share";
this.share_button.style.display = "inline-block";
this.final_message.innerHTML = "";
@@ -635,7 +478,6 @@ export class ShareDialog extends ComfyDialog {
this.credits_input.value = "";
this.title_input.value = "";
this.description_input.value = "";
this.is_nsfw_checkbox.checked = false;
this.selectedOutputIndex = 0;
// hide the final message
@@ -713,28 +555,6 @@ export class ShareDialog extends ComfyDialog {
]),
]),
$el("details", {
style: {
border: "1px solid #999",
marginTop: "10px",
padding: "5px",
borderRadius: "5px",
backgroundColor: "#222"
},
}, [
$el("summary", {
style: {
color: "white",
cursor: "pointer",
}
}, [`Comfyworkflows.com account`]),
$el("h4", {
textContent: "Share key (found on your profile page)",
}, []),
$el("p", { size: 3, color: "white" }, ["If provided, your art will be saved to your account. Otherwise, it will be shared anonymously."]),
this.cw_sharekey_input,
]),
$el("div", {}, [
$el("p", {
size: 3, color: "white", style: {
@@ -743,9 +563,6 @@ export class ShareDialog extends ComfyDialog {
}, [`Select where to share your art:`]),
this.matrix_destination_checkbox,
matrix_destination_checkbox_text,
$el("br", {}, []),
this.comfyworkflows_destination_checkbox,
comfyworkflows_destination_checkbox_text,
]),
$el("h4", {
@@ -779,9 +596,6 @@ export class ShareDialog extends ComfyDialog {
}
}, []),
this.description_input,
$el("br", {}, []),
$el("div", {}, [this.is_nsfw_checkbox, is_nsfw_checkbox_text]),
// $el("br", {}, []),
// this.final_message,
@@ -798,8 +612,6 @@ export class ShareDialog extends ComfyDialog {
textContent: "Close",
onclick: () => {
// Reset state
this.matrix_destination_checkbox.checked = this.share_option === 'matrix';
this.comfyworkflows_destination_checkbox.checked = this.share_option !== 'matrix';
this.share_button.textContent = "Share";
this.share_button.style.display = "inline-block";
this.final_message.innerHTML = "";
@@ -807,7 +619,6 @@ export class ShareDialog extends ComfyDialog {
this.credits_input.value = "";
this.title_input.value = "";
this.description_input.value = "";
this.is_nsfw_checkbox.checked = false;
this.selectedOutputIndex = 0;
// hide the final message
@@ -823,7 +634,7 @@ export class ShareDialog extends ComfyDialog {
]),
]);
// get the user's existing matrix auth and share key
// get the user's existing matrix auth
ShareDialog.matrix_auth = { homeserver: "matrix.org", username: "", password: "" };
try {
api.fetchApi(`/v2/manager/get_matrix_auth`)
@@ -841,23 +652,6 @@ export class ShareDialog extends ComfyDialog {
// console.log(error);
}
// get the user's existing comfyworkflows share key
ShareDialog.cw_sharekey = "";
try {
// console.log("Fetching comfyworkflows share key")
api.fetchApi(`/v2/manager/get_comfyworkflows_auth`)
.then(response => response.json())
.then(data => {
ShareDialog.cw_sharekey = data.comfyworkflows_sharekey;
this.cw_sharekey_input.value = ShareDialog.cw_sharekey;
})
.catch(error => {
// console.log(error);
});
} catch (error) {
// console.log(error);
}
this.share_button.onclick = async () => {
const prompt = await app.graphToPrompt();
const nodes = app.graph._nodes;
@@ -868,9 +662,6 @@ export class ShareDialog extends ComfyDialog {
if (this.matrix_destination_checkbox.checked) {
destinations.push("matrix");
}
if (this.comfyworkflows_destination_checkbox.checked) {
destinations.push("comfyworkflows");
}
// if destinations includes matrix, make an api call to /manager/check_matrix to ensure that the user has configured their matrix settings
if (destinations.includes("matrix")) {
@@ -881,10 +672,6 @@ export class ShareDialog extends ComfyDialog {
}
}
if (destinations.includes("comfyworkflows") && !this.cw_sharekey_input.value && false) { //!confirm("You have NOT set your ComfyWorkflows.com share key. Your art will NOT be connected to your account (it will be shared anonymously). Continue?")) {
return;
}
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
// console.log({ potential_outputs, potential_output_nodes })
@@ -914,14 +701,10 @@ export class ShareDialog extends ComfyDialog {
username: this.matrix_username_input.value,
password: this.matrix_password_input.value,
},
cw_auth: {
cw_sharekey: this.cw_sharekey_input.value,
},
share_destinations: destinations,
credits: this.credits_input.value,
title: this.title_input.value,
description: this.description_input.value,
is_nsfw: this.is_nsfw_checkbox.checked,
prompt,
potential_outputs,
selected_output_index: this.selectedOutputIndex,
@@ -950,16 +733,8 @@ export class ShareDialog extends ComfyDialog {
const response_json = await response.json();
if (response_json.comfyworkflows.url) {
const sharedUrl = sanitizeHTML(sanitizeUrl(response_json.comfyworkflows.url));
this.final_message.innerHTML = "Your art has been shared: <a href='" + sharedUrl + "' target='_blank' rel='noopener noreferrer'>" + sharedUrl + "</a>";
if (response_json.matrix.success) {
this.final_message.innerHTML += "<br>Your art has been shared in the ComfyUI Matrix server's #share channel!";
}
} else {
if (response_json.matrix.success) {
this.final_message.innerHTML = "Your art has been shared in the ComfyUI Matrix server's #share channel!";
}
if (response_json.matrix.success) {
this.final_message.innerHTML = "Your art has been shared in the ComfyUI Matrix server's #share channel!";
}
this.final_message.style.color = "green";
@@ -992,7 +767,7 @@ export class ShareDialog extends ComfyDialog {
return res;
}
show({potential_outputs, potential_output_nodes, share_option}) {
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`.
@@ -1104,14 +879,6 @@ export class ShareDialog extends ComfyDialog {
// this.radio_buttons.appendChild(subheader);
this.radio_buttons.appendChild(new_radio_buttons);
this.element.style.display = "block";
share_option = share_option || this.share_option;
if (share_option === 'comfyworkflows') {
this.matrix_destination_checkbox.checked = false;
this.comfyworkflows_destination_checkbox.checked = true;
} else {
this.matrix_destination_checkbox.checked = true;
this.comfyworkflows_destination_checkbox.checked = false;
}
this.matrix_destination_checkbox.checked = true;
}
}
File diff suppressed because it is too large Load Diff
-746
View File
@@ -1,746 +0,0 @@
import {app} from "../../scripts/app.js";
import {api} from "../../scripts/api.js";
import {ComfyDialog, $el} from "../../scripts/ui.js";
import { customAlert, sanitizeHTML, sanitizeUrl } from "./common.js";
const LOCAL_STORAGE_KEY = "openart_comfy_workflow_key";
const DEFAULT_HOMEPAGE_URL = "https://openart.ai/workflows/dev?developer=true";
//const DEFAULT_HOMEPAGE_URL = "http://localhost:8080/workflows/dev?developer=true";
const API_ENDPOINT = "https://openart.ai/api";
//const API_ENDPOINT = "http://localhost:8080/api";
const style = `
.openart-share-dialog a {
color: #f8f8f8;
}
.openart-share-dialog a:hover {
color: #007bff;
}
.output_label {
border: 5px solid transparent;
}
.output_label:hover {
border: 5px solid #59E8C6;
}
.output_label.checked {
border: 5px solid #59E8C6;
}
`;
// Shared component styles
const sectionStyle = {
marginBottom: 0,
padding: 0,
borderRadius: "8px",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)",
display: "flex",
flexDirection: "column",
justifyContent: "center",
};
export class OpenArtShareDialog extends ComfyDialog {
static instance = null;
constructor() {
super();
$el("style", {
textContent: style,
parent: document.head,
});
this.element = $el(
"div.comfy-modal.openart-share-dialog",
{
parent: document.body,
style: {
"overflow-y": "auto",
},
},
[$el("div.comfy-modal-content", {}, [...this.createButtons()])]
);
this.selectedOutputIndex = 0;
this.selectedNodeId = null;
this.uploadedImages = [];
this.selectedFile = null;
}
async readKey() {
let key = ""
try {
key = await api.fetchApi(`/v2/manager/get_openart_auth`)
.then(response => response.json())
.then(data => {
return data.openart_key;
})
.catch(error => {
// console.log(error);
});
} catch (error) {
// console.log(error);
}
return key || "";
}
async saveKey(value) {
await api.fetchApi(`/v2/manager/set_openart_auth`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
openart_key: value
})
});
}
createButtons() {
const inputStyle = {
display: "block",
minWidth: "500px",
width: "100%",
padding: "10px",
margin: "10px 0",
borderRadius: "4px",
border: "1px solid #ddd",
boxSizing: "border-box",
};
const hyperLinkStyle = {
display: "block",
marginBottom: "15px",
fontWeight: "bold",
fontSize: "14px",
};
const labelStyle = {
color: "#f8f8f8",
display: "block",
margin: "10px 0 0 0",
fontWeight: "bold",
textDecoration: "none",
};
const buttonStyle = {
padding: "10px 80px",
margin: "10px 5px",
borderRadius: "4px",
border: "none",
cursor: "pointer",
color: "#fff",
backgroundColor: "#007bff",
};
// upload images input
this.uploadImagesInput = $el("input", {
type: "file",
multiple: false,
style: inputStyle,
accept: "image/*",
});
this.uploadImagesInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) {
this.previewImage.src = "";
this.previewImage.style.display = "none";
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
const imgData = e.target.result;
this.previewImage.src = imgData;
this.previewImage.style.display = "block";
this.selectedFile = null
// Once user uploads an image, we uncheck all radio buttons
this.radioButtons.forEach((ele) => {
ele.checked = false;
ele.parentElement.classList.remove("checked");
});
// 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 = 0.35;
this.uploadImagesInput.style.opacity = 1;
};
reader.readAsDataURL(file);
});
// preview image
this.previewImage = $el("img", {
src: "",
style: {
width: "100%",
maxHeight: "100px",
objectFit: "contain",
display: "none",
marginTop: '10px',
},
});
this.keyInput = $el("input", {
type: "password",
placeholder: "Copy & paste your API key",
style: inputStyle,
});
this.NameInput = $el("input", {
type: "text",
placeholder: "Title (required)",
style: inputStyle,
});
this.descriptionInput = $el("textarea", {
placeholder: "Description (optional)",
style: {
...inputStyle,
minHeight: "100px",
},
});
// Header Section
const headerSection = $el("h3", {
textContent: "Share your workflow to OpenArt",
size: 3,
color: "white",
style: {
'text-align': 'center',
color: 'var(--input-text)',
margin: '0 0 10px 0',
}
});
// LinkSection
this.communityLink = $el("a", {
style: hyperLinkStyle,
href: DEFAULT_HOMEPAGE_URL,
target: "_blank"
}, ["👉 Check out thousands of workflows shared from the community"])
this.getAPIKeyLink = $el("a", {
style: {
...hyperLinkStyle,
color: "#59E8C6"
},
href: DEFAULT_HOMEPAGE_URL,
target: "_blank"
}, ["👉 Get your API key here"])
const linkSection = $el(
"div",
{
style: {
marginTop: "10px",
display: "flex",
flexDirection: "column",
},
},
[
this.communityLink,
this.getAPIKeyLink,
]
);
// Account Section
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)"]),
this.previewImage,
this.uploadImagesInput,
]);
// Outputs Section
this.outputsSection = $el("div", {
id: "selectOutputs",
}, []);
// Additional Inputs Section
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"s
}, [])
this.joinContestDescription = $el("a", {
style: {
...hyperLinkStyle,
display: 'inline-block',
color: "#59E8C6",
fontSize: '12px',
marginLeft: '10px',
marginBottom: 0,
},
href: "https://contest.openart.ai/",
target: "_blank"
}, ["🏆 I'm participating in the OpenArt workflow contest"])
this.joinContestLabel = $el("label", {
style: {
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}
}, [this.joinContestCheckbox, this.joinContestDescription])
const contestSection = $el("div", {style: sectionStyle}, [
this.joinContestLabel,
]);
*/
// Message Section
this.message = $el(
"div",
{
style: {
color: "#ff3d00",
textAlign: "center",
padding: "10px",
fontSize: "20px",
},
},
[]
);
this.shareButton = $el("button", {
type: "submit",
textContent: "Share",
style: buttonStyle,
onclick: () => {
this.handleShareButtonClick();
},
});
// Share and Close Buttons
const buttonsSection = $el(
"div",
{
style: {
textAlign: "right",
marginTop: "20px",
display: "flex",
justifyContent: "space-between",
},
},
[
$el("button", {
type: "button",
textContent: "Close",
style: {
...buttonStyle,
backgroundColor: undefined,
},
onclick: () => {
this.close();
},
}),
this.shareButton,
]
);
// Composing the full layout
const layout = [
headerSection,
linkSection,
accountSection,
outputUploadSection,
this.outputsSection,
additionalInputsSection,
// contestSection,
this.message,
buttonsSection,
];
return layout;
}
async fetchApi(path, options, statusText) {
if (statusText) {
this.message.textContent = statusText;
}
const addSearchParams = (url, params = {}) =>
new URL(
`${url.origin}${url.pathname}?${new URLSearchParams([
...Array.from(url.searchParams.entries()),
...Object.entries(params),
])}`
);
const fullPath = addSearchParams(new URL(API_ENDPOINT + path), {
workflow_api_key: this.keyInput.value,
});
const response = await fetch(fullPath, options);
if (!response.ok) {
throw new Error(response.statusText);
}
if (statusText) {
this.message.textContent = "";
}
const data = await response.json();
return {
ok: response.ok,
statusText: response.statusText,
status: response.status,
data,
};
}
async uploadThumbnail(uploadFile) {
const form = new FormData();
form.append("file", uploadFile);
try {
const res = await this.fetchApi(
`/v2/workflows/upload_thumbnail`,
{
method: "POST",
body: form,
},
"Uploading thumbnail..."
);
if (res.ok && res.data) {
const {image_url, width, height} = res.data;
this.uploadedImages.push({
url: image_url,
width,
height,
});
}
} catch (e) {
if (e?.response?.status === 413) {
throw new Error("File size is too large (max 20MB)");
} else {
throw new Error("Error uploading thumbnail: " + e.message);
}
}
}
async handleShareButtonClick() {
this.message.textContent = "";
await this.saveKey(this.keyInput.value);
try {
this.shareButton.disabled = true;
this.shareButton.textContent = "Sharing...";
await this.share();
} catch (e) {
customAlert(e.message);
}
this.shareButton.disabled = false;
this.shareButton.textContent = "Share";
}
async share() {
const prompt = await app.graphToPrompt();
const workflowJSON = prompt["workflow"];
const workflowAPIJSON = prompt["output"];
const form_values = {
name: this.NameInput.value,
description: this.descriptionInput.value,
};
if (!this.keyInput.value) {
throw new Error("API key is required");
}
if (!this.uploadImagesInput.files[0] && !this.selectedFile) {
throw new Error("Thumbnail is required");
}
if (!form_values.name) {
throw new Error("Title is required");
}
const current_snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
.then(response => response.json())
.catch(error => {
// console.log(error);
});
if (!this.uploadedImages.length) {
if (this.selectedFile) {
await this.uploadThumbnail(this.selectedFile);
} else {
for (const file of this.uploadImagesInput.files) {
try {
await this.uploadThumbnail(file);
} catch (e) {
this.uploadedImages = [];
throw new Error(e.message);
}
}
if (this.uploadImagesInput.files.length === 0) {
throw new Error("No thumbnail uploaded");
}
}
}
// const join_contest = this.joinContestCheckbox.checked;
try {
const response = await this.fetchApi(
"/v2/workflows/publish",
{
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
workflow_json: workflowJSON,
upload_images: this.uploadedImages,
form_values,
advanced_config: {
workflow_api_json: workflowAPIJSON,
snapshot: current_snapshot,
},
// join_contest,
}),
},
"Uploading workflow..."
);
if (response.ok) {
const {workflow_id} = response.data;
if (workflow_id) {
const url = `https://openart.ai/workflows/-/-/${workflow_id}`;
this.message.innerHTML = `Workflow has been shared successfully. <a href="${sanitizeHTML(sanitizeUrl(url))}" target="_blank" rel="noopener noreferrer">Click here to view it.</a>`;
this.previewImage.src = "";
this.previewImage.style.display = "none";
this.uploadedImages = [];
this.NameInput.value = "";
this.descriptionInput.value = "";
this.radioButtons.forEach((ele) => {
ele.checked = false;
ele.parentElement.classList.remove("checked");
});
this.selectedOutputIndex = 0;
this.selectedNodeId = null;
this.selectedFile = null;
}
}
} catch (e) {
throw new Error("Error sharing workflow: " + e.message);
}
}
async fetchImageBlob(url) {
const response = await fetch(url);
const blob = await response.blob();
return blob;
}
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) => {
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;
this.message.innerHTML = "";
this.message.textContent = "";
this.element.style.display = "block";
this.previewImage.src = "";
this.previewImage.style.display = "none";
const key = await this.readKey();
this.keyInput.value = key;
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) {
const index = potential_output_nodes.findIndex(node => node.id === this.selectedNodeId);
if (index >= 0) {
this.selectedOutputIndex = index;
}
}
this.radioButtons = [];
const new_radio_buttons = $el("div",
{
id: "selectOutput-Options",
style: {
'overflow-y': 'scroll',
'max-height': '200px',
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(100px, 1fr))',
'grid-template-rows': 'auto',
'grid-column-gap': '10px',
'grid-row-gap': '10px',
'margin-bottom': '10px',
'padding': '10px',
'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 {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);
// Remove the "checked" class from all radio buttons
this.radioButtons.forEach((ele) => {
ele.parentElement.classList.remove("checked");
});
radio_button.parentElement.classList.add("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.style.display = "block";
this.selectedFile = file;
})
// 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, {
type: blob.type,
});
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
// 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);
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: 'var(--input-text)',
margin: '0 0 5px 0',
fontSize: '12px',
},
}, [])
this.outputsSection.innerHTML = "";
this.outputsSection.appendChild(header);
this.outputsSection.appendChild(new_radio_buttons);
}
}
-198
View File
@@ -4,9 +4,7 @@ from . import manager_core as core
import os
from aiohttp import web
import aiohttp
import json
import hashlib
import folder_paths
from server import PromptServer
@@ -22,49 +20,6 @@ except Exception:
matrix_nio_is_available = False
def extract_model_file_names(json_data):
"""Extract unique file names from the input JSON data."""
file_names = set()
model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
# Recursively search for file names in the JSON data
def recursive_search(data):
if isinstance(data, dict):
for value in data.values():
recursive_search(value)
elif isinstance(data, list):
for item in data:
recursive_search(item)
elif isinstance(data, str) and '.' in data:
file_names.add(os.path.basename(data)) # file_names.add(data)
recursive_search(json_data)
return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
def find_file_paths(base_dir, file_names):
"""Find the paths of the files in the base directory."""
file_paths = {}
for root, dirs, files in os.walk(base_dir):
# Exclude certain directories
dirs[:] = [d for d in dirs if d not in ['.git']]
for file in files:
if file in file_names:
file_paths[file] = os.path.join(root, file)
return file_paths
def compute_sha256_checksum(filepath):
"""Compute the SHA256 checksum of a file, in chunks"""
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
@PromptServer.instance.routes.get("/v2/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
@@ -76,17 +31,6 @@ async def share_option(request):
return web.Response(status=200)
def get_openart_auth():
if not os.path.exists(os.path.join(context.manager_files_path, ".openart_key")):
return None
try:
with open(os.path.join(context.manager_files_path, ".openart_key"), "r") as f:
openart_key = f.read().strip()
return openart_key if openart_key else None
except Exception:
return None
def get_matrix_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "matrix_auth")):
return None
@@ -105,19 +49,6 @@ def get_matrix_auth():
return None
def get_comfyworkflows_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "comfyworkflows_sharekey")):
return None
try:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
share_key = f.read()
if not share_key.strip():
return None
return share_key
except Exception:
return None
def get_youml_settings():
if not os.path.exists(os.path.join(context.manager_files_path, ".youml")):
return None
@@ -134,24 +65,6 @@ def set_youml_settings(settings):
f.write(settings)
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
if not openart_key:
return web.Response(status=404)
return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
async def api_set_openart_auth(request):
json_data = await request.json()
openart_key = json_data['openart_key']
with open(os.path.join(context.manager_files_path, ".openart_key"), "w") as f:
f.write(openart_key)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...")
@@ -176,32 +89,6 @@ async def api_set_youml_settings(request):
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
# print("Getting stored Comfyworkflows.com auth...")
comfyworkflows_auth = get_comfyworkflows_auth()
if not comfyworkflows_auth:
return web.Response(status=404)
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request):
json_data = await request.json()
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
json.dump(json_data, file, indent=4)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request):
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
@@ -218,35 +105,23 @@ def set_matrix_auth(json_data):
f.write("\n".join([homeserver, username, password]))
def set_comfyworkflows_auth(comfyworkflows_sharekey):
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
f.write(comfyworkflows_sharekey)
def has_provided_matrix_auth(matrix_auth):
return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
return comfyworkflows_sharekey.strip()
@PromptServer.instance.routes.post("/v2/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
matrix_auth = json_data['matrix_auth']
comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
set_matrix_auth(matrix_auth)
set_comfyworkflows_auth(comfyworkflows_sharekey)
share_destinations = json_data['share_destinations']
credits = json_data['credits']
title = json_data['title']
description = json_data['description']
is_nsfw = json_data['is_nsfw']
prompt = json_data['prompt']
potential_outputs = json_data['potential_outputs']
selected_output_index = json_data['selected_output_index']
@@ -278,76 +153,6 @@ async def share_art(request):
# get the mime type of the asset
assetFileType = mimetypes.guess_type(asset_filepath)[0]
share_website_host = "UNKNOWN"
if "comfyworkflows" in share_destinations:
share_website_host = "https://comfyworkflows.com"
share_endpoint = f"{share_website_host}/api"
# get presigned urls
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.post(
f"{share_endpoint}/get_presigned_urls",
json={
"assetFileName": asset_filename,
"assetFileType": assetFileType,
"workflowJsonFileName": 'workflow.json',
"workflowJsonFileType": 'application/json',
},
) as resp:
assert resp.status == 200
presigned_urls_json = await resp.json()
assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
assetFileKey = presigned_urls_json["assetFileKey"]
workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
# upload asset
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
assert resp.status == 200
# upload workflow json
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
assert resp.status == 200
model_filenames = extract_model_file_names(prompt['workflow'])
model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
models_info = {}
for filename, filepath in model_file_paths.items():
models_info[filename] = {
"filename": filename,
"sha256_checksum": compute_sha256_checksum(filepath),
"relative_path": os.path.relpath(filepath, folder_paths.base_path),
}
# make a POST request to /api/upload_workflow with form data key values
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
form = aiohttp.FormData()
if comfyworkflows_sharekey:
form.add_field("shareKey", comfyworkflows_sharekey)
form.add_field("source", "comfyui_manager")
form.add_field("assetFileKey", assetFileKey)
form.add_field("assetFileType", assetFileType)
form.add_field("workflowJsonFileKey", workflowJsonFileKey)
form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
form.add_field("shareWorkflowCredits", credits)
form.add_field("shareWorkflowTitle", title)
form.add_field("shareWorkflowDescription", description)
form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
form.add_field("currentSnapshot", json.dumps(await core.get_current_snapshot()))
form.add_field("modelsInfo", json.dumps(models_info))
async with session.post(
f"{share_endpoint}/upload_workflow",
data=form,
) as resp:
assert resp.status == 200
upload_workflow_json = await resp.json()
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if matrix_nio_is_available and "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
@@ -442,9 +247,6 @@ async def share_art(request):
return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
return web.json_response({
"comfyworkflows": {
"url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
},
"matrix": {
"success": None if "matrix" not in share_destinations else True
}
+1 -1
View File
@@ -191,7 +191,7 @@ Note: skipped when no import-failed nodes are present.
## Exclusions
- `share_option` — per user instruction
- **External-service auth/integration** (9 endpoints) — external services are unreachable from E2E
- **External-service auth/integration** (4 endpoints) — external services are unreachable from E2E
- **Individual queue endpoints** (6) — unused by JS; delegated internally through `queue/batch`
- `queue/abort_current` — unused by JS (Stop uses `queue/reset`)
- `/manager/notice` (v1) — superseded by v2