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