From 857e2ca5a5439600f73e6e01ce36b0f08c684c16 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" <128333288+ltdrdata@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:47:35 +0900 Subject: [PATCH] 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 --- CHANGELOG.md | 13 + README.md | 3 +- comfyui_manager/glob/share_3rdparty.py | 199 ---- comfyui_manager/js/README.md | 2 - comfyui_manager/js/comfyui-manager.js | 113 +- comfyui_manager/js/comfyui-share-common.js | 263 +---- comfyui_manager/js/comfyui-share-copus.js | 1168 ------------------- comfyui_manager/js/comfyui-share-openart.js | 746 ------------ comfyui_manager/legacy/share_3rdparty.py | 198 ---- tests/playwright/TEST_SCENARIOS.md | 2 +- 10 files changed, 64 insertions(+), 2643 deletions(-) delete mode 100644 comfyui_manager/js/comfyui-share-copus.js delete mode 100644 comfyui_manager/js/comfyui-share-openart.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b11d6fc..3491b4c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + `/__manager/` can be deleted. + ### Security - Allow administrators to exempt selected Registry IDs from the flagged CNR diff --git a/README.md b/README.md index 2a3614b7..2e2812f4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/comfyui_manager/glob/share_3rdparty.py b/comfyui_manager/glob/share_3rdparty.py index a655b2af..af8c8eb3 100644 --- a/comfyui_manager/glob/share_3rdparty.py +++ b/comfyui_manager/glob/share_3rdparty.py @@ -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 } diff --git a/comfyui_manager/js/README.md b/comfyui_manager/js/README.md index 5ceb21ba..ae3ed857 100644 --- a/comfyui_manager/js/README.md +++ b/comfyui_manager/js/README.md @@ -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 diff --git a/comfyui_manager/js/comfyui-manager.js b/comfyui_manager/js/comfyui-manager.js index 7110f1ad..15e8097f 100644 --- a/comfyui_manager/js/comfyui-manager.js +++ b/comfyui_manager/js/comfyui-manager.js @@ -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(); } diff --git a/comfyui_manager/js/comfyui-share-common.js b/comfyui_manager/js/comfyui-share-common.js index 53d9444e..f1f26c47 100644 --- a/comfyui_manager/js/comfyui-share-common.js +++ b/comfyui_manager/js/comfyui-share-common.js @@ -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 🎨

ComfyWorkflows.com", - 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 esheep.com", - 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: " + sharedUrl + ""; - if (response_json.matrix.success) { - this.final_message.innerHTML += "
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; } } diff --git a/comfyui_manager/js/comfyui-share-copus.js b/comfyui_manager/js/comfyui-share-copus.js deleted file mode 100644 index af14f32b..00000000 --- a/comfyui_manager/js/comfyui-share-copus.js +++ /dev/null @@ -1,1168 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { $el, ComfyDialog } from "../../scripts/ui.js"; -import { customAlert, sanitizeHTML, sanitizeUrl } from "./common.js"; - -const env = "prod"; - -let DEFAULT_HOMEPAGE_URL = "https://copus.io"; - -let API_ENDPOINT = "https://api.client.prod.copus.io"; - -if (env !== "prod") { - API_ENDPOINT = "https://api.test.copus.io"; - DEFAULT_HOMEPAGE_URL = "https://test.copus.io"; -} - -const style = ` - .copus-share-dialog a { - color: #f8f8f8; - } - .copus-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", - position: "relative", -}; - -export class CopusShareDialog extends ComfyDialog { - static instance = null; - - constructor() { - super(); - $el("style", { - textContent: style, - parent: document.head, - }); - this.element = $el( - "div.comfy-modal.copus-share-dialog", - { - parent: document.body, - style: { - "overflow-y": "auto", - }, - }, - [$el("div.comfy-modal-content", {}, [...this.createButtons()])] - ); - this.selectedOutputIndex = 0; - this.selectedOutput_lock = 0; - this.selectedNodeId = null; - this.uploadedImages = []; - this.allFilesImages = []; - this.selectedFile = null; - this.allFiles = []; - this.titleNum = 0; - } - - createButtons() { - const inputStyle = { - display: "block", - minWidth: "500px", - width: "100%", - padding: "10px", - margin: "10px 0", - borderRadius: "4px", - border: "1px solid #ddd", - boxSizing: "border-box", - }; - - const textAreaStyle = { - display: "block", - minWidth: "500px", - width: "100%", - padding: "10px", - margin: "10px 0", - borderRadius: "4px", - border: "1px solid #ddd", - boxSizing: "border-box", - minHeight: "100px", - background: "#222", - resize: "vertical", - color: "#f2f2f2", - fontFamily: "Arial", - fontWeight: "400", - fontSize: "15px", - }; - - 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.TitleInput = $el("input", { - type: "text", - placeholder: "Title (Required)", - style: inputStyle, - maxLength: "70", - oninput: () => { - const titleNum = this.TitleInput.value.length; - titleNumDom.textContent = `${titleNum}/70`; - }, - }); - this.SubTitleInput = $el("input", { - type: "text", - placeholder: "Subtitle (Optional)", - style: inputStyle, - maxLength: "350", - oninput: () => { - const titleNum = this.SubTitleInput.value.length; - subTitleNumDom.textContent = `${titleNum}/350`; - }, - }); - this.LockInput = $el("input", { - type: "text", - placeholder: "0", - style: { - width: "100px", - padding: "7px", - paddingLeft: "30px", - borderRadius: "4px", - border: "1px solid #ddd", - boxSizing: "border-box", - position: "relative", - }, - oninput: (event) => { - let input = event.target.value; - // Use a regular expression to match a number with up to two decimal places - const regex = /^\d*\.?\d{0,2}$/; - if (!regex.test(input)) { - // If the input doesn't match, remove the last entered character - event.target.value = input.slice(0, -1); - } - const numericValue = parseFloat(input); - if (numericValue > 9999) { - input = "9999"; - } - // Update the input field with the valid value - event.target.value = input; - }, - }); - this.descriptionInput = $el("textarea", { - placeholder: "Content (Optional)", - style: { - ...textAreaStyle, - minHeight: "100px", - }, - }); - - // Header Section - const headerSection = $el("h3", { - textContent: "Share your workflow to Copus", - size: 3, - color: "white", - style: { - "text-align": "center", - color: "white", - margin: "0 0 10px 0", - }, - }); - this.getAPIKeyLink = $el( - "a", - { - style: { - ...hyperLinkStyle, - color: "#59E8C6", - }, - href: `${DEFAULT_HOMEPAGE_URL}?fromPage=comfyUI`, - 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️⃣ Copus 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", - }, - [] - ); - - const titleNumDom = $el( - "label", - { - style: { - fontSize: "12px", - position: "absolute", - right: "10px", - bottom: "-10px", - color: "#999", - }, - }, - ["0/70"] - ); - const subTitleNumDom = $el( - "label", - { - style: { - fontSize: "12px", - position: "absolute", - right: "10px", - bottom: "-10px", - color: "#999", - }, - }, - ["0/350"] - ); - const descriptionNumDom = $el( - "label", - { - style: { - fontSize: "12px", - position: "absolute", - right: "10px", - bottom: "-10px", - color: "#999", - }, - }, - ["0/70"] - ); - // Additional Inputs Section - const additionalInputsSection = $el("div", { style: { ...sectionStyle } }, [ - $el("label", { style: labelStyle }, ["3️⃣ Title "]), - this.TitleInput, - titleNumDom, - ]); - const SubtitleSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["4️⃣ Subtitle "]), - this.SubTitleInput, - subTitleNumDom, - ]); - const DescriptionSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["5️⃣ Description "]), - this.descriptionInput, - // descriptionNumDom, - ]); - // switch between outputs section and additional inputs section - this.radioButtons_lock = []; - - this.radioButtonsCheck_lock = $el("input", { - type: "radio", - name: "output_type_lock", - value: "0", - id: "blockchain1_lock", - checked: true, - }); - this.radioButtonsCheckOff_lock = $el("input", { - type: "radio", - name: "output_type_lock", - value: "1", - id: "blockchain_lock", - }); - - const blockChainSection_lock = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["6️⃣ Download threshold"]), - $el( - "label", - { - style: { - marginTop: "10px", - display: "flex", - alignItems: "center", - cursor: "pointer", - }, - }, - [ - this.radioButtonsCheck_lock, - $el( - "div", - { - style: { - marginLeft: "5px", - display: "flex", - alignItems: "center", - position: "relative", - }, - }, - [ - $el("span", { style: { marginLeft: "5px" } }, ["ON"]), - $el( - "span", - { - style: { - marginLeft: "20px", - marginRight: "10px", - color: "#fff", - }, - }, - ["Unlock with"] - ), - $el("img", { - style: { - width: "16px", - height: "16px", - position: "absolute", - right: "75px", - zIndex: "100", - }, - src: "https://static.copus.io/images/admin/202507/prod/e2919a1d8f3c2d99d3b8fe27ff94b841.png", - }), - this.LockInput, - ] - ), - ] - ), - $el( - "label", - { style: { display: "flex", alignItems: "center", cursor: "pointer" } }, - [ - this.radioButtonsCheckOff_lock, - $el( - "div", - { - style: { - marginLeft: "5px", - display: "flex", - alignItems: "center", - }, - }, - [$el("span", { style: { marginLeft: "5px" } }, ["OFF"])] - ), - ] - ), - - $el( - "p", - { style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } }, - [ - ] - ), - ]); - - this.radioButtons = []; - - this.radioButtonsCheck = $el("input", { - type: "radio", - name: "output_type", - value: "0", - id: "blockchain1", - checked: true, - }); - this.radioButtonsCheckOff = $el("input", { - type: "radio", - name: "output_type", - value: "1", - id: "blockchain", - }); - - const blockChainSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["8️⃣ Store on blockchain "]), - $el( - "label", - { - style: { - marginTop: "10px", - display: "flex", - alignItems: "center", - cursor: "pointer", - }, - }, - [ - this.radioButtonsCheck, - $el("span", { style: { marginLeft: "5px" } }, ["ON"]), - ] - ), - $el( - "label", - { style: { display: "flex", alignItems: "center", cursor: "pointer" } }, - [ - this.radioButtonsCheckOff, - $el("span", { style: { marginLeft: "5px" } }, ["OFF"]), - ] - ), - $el( - "p", - { style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } }, - ["Secure ownership with a permanent & decentralized storage"] - ), - ]); - - this.ratingRadioButtonsCheck0 = $el("input", { - type: "radio", - name: "content_rating", - value: "0", - id: "content_rating0", - }); - this.ratingRadioButtonsCheck1 = $el("input", { - type: "radio", - name: "content_rating", - value: "1", - id: "content_rating1", - }); - this.ratingRadioButtonsCheck2 = $el("input", { - type: "radio", - name: "content_rating", - value: "2", - id: "content_rating2", - }); - this.ratingRadioButtonsCheck_1 = $el("input", { - type: "radio", - name: "content_rating", - value: "-1", - id: "content_rating_1", - checked: true, - }); - - // content rating - const contentRatingSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["7️⃣ Content rating "]), - $el( - "label", - { - style: { - marginTop: "10px", - display: "flex", - alignItems: "center", - cursor: "pointer", - }, - }, - [ - this.ratingRadioButtonsCheck0, - $el("img", { - style: { - width: "12px", - height: "12px", - marginLeft: "5px", - }, - src: "https://static.copus.io/images/client/202507/test/b9f17da83b054d53cd0cb4508c2c30dc.png", - }), - $el("span", { style: { marginLeft: "5px", color: "#fff" } }, [ - "All ages", - ]), - ] - ), - $el( - "p", - { style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } }, - ["Safe for all viewers; no profanity, violence, or mature themes."] - ), - $el( - "label", - { style: { display: "flex", alignItems: "center", cursor: "pointer" } }, - [ - this.ratingRadioButtonsCheck1, - $el("img", { - style: { - width: "12px", - height: "12px", - marginLeft: "5px", - }, - src: "https://static.copus.io/images/client/202507/test/7848bc0d3690671df21c7cf00c4cfc81.png", - }), - $el("span", { style: { marginLeft: "5px", color: "#fff" } }, [ - "13+ (Teen)", - ]), - ] - ), - $el( - "p", - { style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } }, - [ - "Mild language, light themes, or cartoon violence; no explicit content. ", - ] - ), - $el( - "label", - { style: { display: "flex", alignItems: "center", cursor: "pointer" } }, - [ - this.ratingRadioButtonsCheck2, - $el("img", { - style: { - width: "12px", - height: "12px", - marginLeft: "5px", - }, - src: "https://static.copus.io/images/client/202507/test/bc51839c208d68d91173e43c23bff039.png", - }), - $el("span", { style: { marginLeft: "5px", color: "#fff" } }, [ - "18+ (Explicit)", - ]), - ] - ), - $el( - "p", - { style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } }, - [ - "Explicit content, including sexual content, strong violence, or intense themes. ", - ] - ), - $el( - "label", - { style: { display: "flex", alignItems: "center", cursor: "pointer" } }, - [ - this.ratingRadioButtonsCheck_1, - $el("img", { - style: { - width: "12px", - height: "12px", - marginLeft: "5px", - }, - src: "https://static.copus.io/images/client/202507/test/5c802fdcaaea4e7bbed37393eec0d5ba.png", - }), - $el("span", { style: { marginLeft: "5px", color: "#fff" } }, [ - "Not Rated", - ]), - ] - ), - $el( - "p", - { style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } }, - ["No age rating provided."] - ), - ]); - - // 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, - SubtitleSection, - DescriptionSection, - // contestSection, - blockChainSection_lock, - contentRatingSection, - blockChainSection, - this.message, - buttonsSection, - ]; - - return layout; - } - /** - * api - * @param {url} path - * @param {params} options - * @param {statusText} statusText - * @returns - */ - async fetchApi(path, options, statusText) { - if (statusText) { - this.message.textContent = statusText; - } - const fullPath = new URL(API_ENDPOINT + path); - 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, - }; - } - /** - * @param {file} uploadFile - */ - async uploadThumbnail(uploadFile, type) { - const form = new FormData(); - form.append("file", uploadFile); - form.append("apiToken", this.keyInput.value); - try { - const res = await this.fetchApi( - `/client/common/opus/uploadImage`, - { - method: "POST", - body: form, - }, - "Uploading thumbnail..." - ); - if (res.status && res.data.status && res.data) { - const { data } = res.data; - if (type) { - this.allFilesImages.push({ - url: data, - }); - } - this.uploadedImages.push({ - url: data, - }); - } else { - throw new Error( - "make sure your API key is correct and try again later" - ); - } - } 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 = ""; - 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"; - } - /** - * share - * @param {string} title - * @param {string} subtitle - * @param {string} content - * @param {boolean} storeOnChain - * @param {string} coverUrl - * @param {string[]} imageUrls - * @param {string} apiToken - */ - async share() { - const prompt = await app.graphToPrompt(); - const workflowJSON = prompt["workflow"]; - const form_values = { - title: this.TitleInput.value, - subTitle: this.SubTitleInput.value, - content: this.descriptionInput.value, - storeOnChain: this.radioButtonsCheck.checked ? true : false, - lockState: this.radioButtonsCheck_lock.checked ? 2 : 0, - unlockPrice: this.LockInput.value, - rating: this.ratingRadioButtonsCheck0.checked - ? 0 - : this.ratingRadioButtonsCheck1.checked - ? 1 - : this.ratingRadioButtonsCheck2.checked - ? 2 - : -1, - }; - - 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.title) { - throw new Error("Title is required"); - } - - if (this.radioButtonsCheck_lock.checked) { - if (!this.LockInput.value) { - throw new Error("Price is required"); - } - } - - 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"); - } - } - } - if (this.allFiles.length > 0) { - for (const file of this.allFiles) { - try { - await this.uploadThumbnail(file, true); - } catch (e) { - this.allFilesImages = []; - throw new Error(e.message); - } - } - } - try { - const res = await this.fetchApi( - "/client/common/opus/shareFromComfyUI", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - workflowJson: workflowJSON, - apiToken: this.keyInput.value, - coverUrl: this.uploadedImages[0].url, - imageUrls: this.allFilesImages.map((image) => image.url), - ...form_values, - }), - }, - "Uploading workflow..." - ); - - if (res.status && res.data.status && res.data) { - localStorage.setItem("copus_token", this.keyInput.value); - const { data } = res.data; - if (data) { - const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`; - this.message.innerHTML = `Workflow has been shared successfully. Click here to view it.`; - this.previewImage.src = ""; - this.previewImage.style.display = "none"; - this.uploadedImages = []; - this.allFilesImages = []; - this.allFiles = []; - this.TitleInput.value = ""; - this.SubTitleInput.value = ""; - this.descriptionInput.value = ""; - 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; - const apiToken = localStorage.getItem("copus_token"); - this.message.innerHTML = ""; - this.message.textContent = ""; - this.element.style.display = "block"; - this.previewImage.src = ""; - this.previewImage.style.display = "none"; - this.keyInput.value = apiToken != null ? apiToken : ""; - this.uploadedImages = []; - this.allFilesImages = []; - this.allFiles = []; - // 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 - 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); - let src = ""; - if (output.type === "image" || output.type === "temp") { - filename = output.image.filename; - src = `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`; - } else if (output.type === "output") { - src = output.output.value; - filename = output.filename; - } - if (src) { - this.fetchImageBlob(src).then((blob) => { - const file = new File([blob], filename, { - type: blob.type, - }); - this.allFiles.push(file); - }); - } - 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", - }, - }, - [] - ); - this.outputsSection.innerHTML = ""; - this.outputsSection.appendChild(header); - this.outputsSection.appendChild(new_radio_buttons); - } -} diff --git a/comfyui_manager/js/comfyui-share-openart.js b/comfyui_manager/js/comfyui-share-openart.js deleted file mode 100644 index cb294bab..00000000 --- a/comfyui_manager/js/comfyui-share-openart.js +++ /dev/null @@ -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. Click here to view it.`; - 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); - } -} diff --git a/comfyui_manager/legacy/share_3rdparty.py b/comfyui_manager/legacy/share_3rdparty.py index 9a30619f..af8c8eb3 100644 --- a/comfyui_manager/legacy/share_3rdparty.py +++ b/comfyui_manager/legacy/share_3rdparty.py @@ -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 } diff --git a/tests/playwright/TEST_SCENARIOS.md b/tests/playwright/TEST_SCENARIOS.md index 6a5ecbd2..cdabb194 100644 --- a/tests/playwright/TEST_SCENARIOS.md +++ b/tests/playwright/TEST_SCENARIOS.md @@ -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