diff --git a/__init__.py b/__init__.py index 393bffec..86a4927e 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ import configparser +import mimetypes import shutil import folder_paths import os @@ -577,7 +578,6 @@ def check_custom_nodes_installed(json_obj, do_fetch=False, do_update_check=True, elif do_update_check: print(f"\x1b[2K\rUpdate check done.") - @server.PromptServer.instance.routes.get("/customnode/getmappings") async def fetch_customnode_mappings(request): if request.rel_url.query["mode"] == "local": @@ -1408,6 +1408,228 @@ async def channel_url_list(request): return web.Response(status=200) +def get_matrix_auth(): + if not os.path.exists(os.path.join(folder_paths.base_path, "matrix_auth")): + return None + try: + with open(os.path.join(folder_paths.base_path, "matrix_auth"), "r") as f: + matrix_auth = f.read() + homeserver, username, password = matrix_auth.strip().split("\n") + if not homeserver or not username or not password: + return None + return { + "homeserver": homeserver, + "username": username, + "password": password, + } + except: + return None + +def get_comfyworkflows_auth(): + if not os.path.exists(os.path.join(folder_paths.base_path, "comfyworkflows_sharekey")): + return None + try: + with open(os.path.join(folder_paths.base_path, "comfyworkflows_sharekey"), "r") as f: + share_key = f.read() + if not share_key.strip(): + return None + return share_key + except: + return None + +@server.PromptServer.instance.routes.get("/manager/get_matrix_auth") +async def api_get_matrix_auth(request): + # print("Getting stored Matrix credentials...") + matrix_auth = get_matrix_auth() + if not matrix_auth: + return web.Response(status=404) + return web.json_response(matrix_auth) + +@server.PromptServer.instance.routes.get("/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}) + +def set_matrix_auth(json_data): + homeserver = json_data['homeserver'] + username = json_data['username'] + password = json_data['password'] + with open(os.path.join(folder_paths.base_path, "matrix_auth"), "w") as f: + f.write("\n".join([homeserver, username, password])) + +def set_comfyworkflows_auth(comfyworkflows_sharekey): + with open(os.path.join(folder_paths.base_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() + +@server.PromptServer.instance.routes.post("/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'] + + try: + output_to_share = potential_outputs[int(selected_output_index)] + except: + # for now, pick the first output + output_to_share = potential_outputs[0] + + assert output_to_share['type'] in ('image', 'output') + output_dir = folder_paths.get_output_directory() + + if output_to_share['type'] == 'image': + asset_filename = output_to_share['image']['filename'] + asset_subfolder = output_to_share['image']['subfolder'] + + if output_to_share['image']['type'] == 'temp': + output_dir = folder_paths.get_temp_directory() + else: + asset_filename = output_to_share['output']['filename'] + asset_subfolder = output_to_share['output']['subfolder'] + + if asset_subfolder: + asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename) + else: + asset_filepath = os.path.join(output_dir, asset_filename) + + # get the mime type of the asset + assetFileType = mimetypes.guess_type(asset_filepath)[0] + + 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 + + # 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()) + + 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" in share_destinations: + comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org' + filename = os.path.basename(asset_filepath) + content_type = assetFileType + + try: + from matrix_client.api import MatrixHttpApi + from matrix_client.client import MatrixClient + + homeserver = 'matrix.org' + if matrix_auth: + homeserver = matrix_auth.get('homeserver', 'matrix.org') + homeserver = homeserver.replace("http://", "https://") + if not homeserver.startswith("https://"): + homeserver = "https://" + homeserver + + client = MatrixClient(homeserver) + try: + token = client.login(username=matrix_auth['username'], password=matrix_auth['password']) + if not token: + return web.json_response({"error" : "Invalid Matrix credentials."}, content_type='application/json', status=400) + except: + return web.json_response({"error" : "Invalid Matrix credentials."}, content_type='application/json', status=400) + + matrix = MatrixHttpApi(homeserver, token=token) + with open(asset_filepath, 'rb') as f: + mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri'] + + workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['content_uri'] + + text_content = "" + if title: + text_content += f"{title}\n" + if description: + text_content += f"{description}\n" + if credits: + text_content += f"\ncredits: {credits}\n" + response = matrix.send_message(comfyui_share_room_id, text_content) + response = matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image') + response = matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file') + except: + import traceback + traceback.print_exc() + 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 + } + }, content_type='application/json', status=200) WEB_DIRECTORY = "js" NODE_CLASS_MAPPINGS = {} diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index d4ddf463..a26eec7d 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -1,14 +1,15 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" import { ComfyDialog, $el } from "../../scripts/ui.js"; +import { ShareDialog, SUPPORTED_OUTPUT_NODE_TYPES, getPotentialOutputsAndOutputNodes } from "./comfyui-share.js"; import { CustomNodesInstaller } from "./custom-nodes-downloader.js"; import { AlternativesInstaller } from "./a1111-alter-downloader.js"; import { SnapshotManager } from "./snapshot.js"; import { ModelInstaller } from "./model-downloader.js"; import { manager_instance, setManagerInstance, install_via_git_url } from "./common.js"; -var style = document.createElement('style'); -style.innerHTML = ` +var docStyle = document.createElement('style'); +docStyle.innerHTML = ` .cm-menu-container { column-gap: 20px; display: flex; @@ -29,43 +30,78 @@ style.innerHTML = ` } `; -document.head.appendChild(style); +document.head.appendChild(docStyle); var update_comfyui_button = null; var fetch_updates_button = null; var update_all_button = null; var badge_mode = "none"; +// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts +const style = ` +#comfyworkflows-button { + position: relative; + overflow: hidden; + } +.pysssss-workflow-arrow-2 { + position: absolute; + top: 0; + bottom: 0; + right: 0; + font-size: 12px; + display: flex; + align-items: center; + width: 24px; + justify-content: center; + background: rgba(255,255,255,0.1); + content: "▼"; +} +.pysssss-workflow-arrow-2:after { + content: "▼"; + } + .pysssss-workflow-arrow-2:hover { + filter: brightness(1.6); + background-color: var(--comfy-menu-bg); + } +.pysssss-workflow-popup-2 ~ .litecontextmenu { + transform: scale(1.3); +} +#comfyworkflows-button-menu { + z-index: 10000000000 !important; +} +`; + + + async function init_badge_mode() { - api.fetchApi('/manager/badge_mode') - .then(response => response.text()) - .then(data => { badge_mode = data; }) + api.fetchApi('/manager/badge_mode') + .then(response => response.text()) + .then(data => { badge_mode = data; }) } await init_badge_mode(); - async function fetchNicknames() { - const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`); - const mappings = await response1.json(); + const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`); + const mappings = await response1.json(); - let result = {}; + let result = {}; - for(let i in mappings) { - let item = mappings[i]; - var nickname; - if(item[1].title) { - nickname = item[1].title; - } - else { - nickname = item[1].title_aux; - } + for (let i in mappings) { + let item = mappings[i]; + var nickname; + if (item[1].title) { + nickname = item[1].title; + } + else { + nickname = item[1].title_aux; + } - for(let j in item[0]) { - result[item[0][j]] = nickname; - } - } + for (let j in item[0]) { + result[item[0][j]] = nickname; + } + } return result; } @@ -74,7 +110,7 @@ let nicknames = await fetchNicknames(); async function updateComfyUI() { - let prev_text = update_comfyui_button.innerText; + let prev_text = update_comfyui_button.innerText; update_comfyui_button.innerText = "Updating ComfyUI..."; update_comfyui_button.disabled = true; update_comfyui_button.style.backgroundColor = "gray"; @@ -82,13 +118,13 @@ async function updateComfyUI() { try { const response = await api.fetchApi('/comfyui_manager/update_comfyui'); - if(response.status == 400) { + if (response.status == 400) { app.ui.dialog.show('Failed to update ComfyUI.'); app.ui.dialog.element.style.zIndex = 10010; return false; } - if(response.status == 201) { + if (response.status == 201) { app.ui.dialog.show('ComfyUI has been successfully updated.'); app.ui.dialog.element.style.zIndex = 10010; } @@ -99,7 +135,7 @@ async function updateComfyUI() { return true; } - catch(exception) { + catch (exception) { app.ui.dialog.show(`Failed to update ComfyUI / ${exception}`); app.ui.dialog.element.style.zIndex = 10010; return false; @@ -107,12 +143,12 @@ async function updateComfyUI() { finally { update_comfyui_button.disabled = false; update_comfyui_button.innerText = prev_text; - update_comfyui_button.style.backgroundColor = ""; + update_comfyui_button.style.backgroundColor = ""; } } async function fetchUpdates(update_check_checkbox) { - let prev_text = fetch_updates_button.innerText; + let prev_text = fetch_updates_button.innerText; fetch_updates_button.innerText = "Fetching updates..."; fetch_updates_button.disabled = true; fetch_updates_button.style.backgroundColor = "gray"; @@ -124,13 +160,13 @@ async function fetchUpdates(update_check_checkbox) { const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`); - if(response.status != 200 && response.status != 201) { + if (response.status != 200 && response.status != 201) { app.ui.dialog.show('Failed to fetch updates.'); app.ui.dialog.element.style.zIndex = 10010; return false; } - if(response.status == 201) { + if (response.status == 201) { app.ui.dialog.show('There is an updated extension available.'); app.ui.dialog.element.style.zIndex = 10010; update_check_checkbox.checked = false; @@ -142,7 +178,7 @@ async function fetchUpdates(update_check_checkbox) { return true; } - catch(exception) { + catch (exception) { app.ui.dialog.show(`Failed to update custom nodes / ${exception}`); app.ui.dialog.element.style.zIndex = 10010; return false; @@ -155,7 +191,7 @@ async function fetchUpdates(update_check_checkbox) { } async function updateAll(update_check_checkbox) { - let prev_text = update_all_button.innerText; + let prev_text = update_all_button.innerText; update_all_button.innerText = "Updating all...(ComfyUI)"; update_all_button.disabled = true; update_all_button.style.backgroundColor = "gray"; @@ -169,7 +205,7 @@ async function updateAll(update_check_checkbox) { const response1 = await api.fetchApi('/comfyui_manager/update_comfyui'); const response2 = await api.fetchApi(`/customnode/update_all?mode=${mode}`); - if(response1.status != 200 && response2.status != 201) { + if (response1.status != 200 && response2.status != 201) { app.ui.dialog.show('Failed to update ComfyUI or several extensions.

See terminal log.
'); app.ui.dialog.element.style.zIndex = 10010; return false; @@ -185,7 +221,7 @@ async function updateAll(update_check_checkbox) { return true; } - catch(exception) { + catch (exception) { app.ui.dialog.show(`Failed to update ComfyUI or several extensions / ${exception}`); app.ui.dialog.element.style.zIndex = 10010; return false; @@ -197,6 +233,19 @@ async function updateAll(update_check_checkbox) { } } +function newDOMTokenList(initialTokens) { + const tmp = document.createElement(`div`); + + const classList = tmp.classList; + if (initialTokens) { + initialTokens.forEach(token => { + classList.add(token); + }); + } + + return classList; + } + // ----------- class ManagerMenuDialog extends ComfyDialog { @@ -204,28 +253,28 @@ class ManagerMenuDialog extends ComfyDialog { createControlsMid() { update_comfyui_button = - $el("button", { - type: "button", - textContent: "Update ComfyUI", - onclick: - () => updateComfyUI() - }); + $el("button", { + type: "button", + textContent: "Update ComfyUI", + onclick: + () => updateComfyUI() + }); fetch_updates_button = - $el("button", { - type: "button", - textContent: "Fetch Updates", - onclick: - () => fetchUpdates(this.update_check_checkbox) - }); + $el("button", { + type: "button", + textContent: "Fetch Updates", + onclick: + () => fetchUpdates(this.update_check_checkbox) + }); update_all_button = - $el("button", { - type: "button", - textContent: "Update All", - onclick: - () => updateAll(this.update_check_checkbox) - }); + $el("button", { + type: "button", + textContent: "Update All", + onclick: + () => updateAll(this.update_check_checkbox) + }); const res = [ @@ -298,59 +347,59 @@ class ManagerMenuDialog extends ComfyDialog { // preview method let preview_combo = document.createElement("select"); - preview_combo.appendChild($el('option', {value:'auto', text:'Preview method: Auto'}, [])); - preview_combo.appendChild($el('option', {value:'taesd', text:'Preview method: TAESD (slow)'}, [])); - preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB (fast)'}, [])); - preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None (very fast)'}, [])); + preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, [])); + preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, [])); + preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, [])); + preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, [])); - api.fetchApi('/manager/preview_method') - .then(response => response.text()) - .then(data => { preview_combo.value = data; }) + api.fetchApi('/manager/preview_method') + .then(response => response.text()) + .then(data => { preview_combo.value = data; }) - preview_combo.addEventListener('change', function(event) { - api.fetchApi(`/manager/preview_method?value=${event.target.value}`); + preview_combo.addEventListener('change', function (event) { + api.fetchApi(`/manager/preview_method?value=${event.target.value}`); }); - // nickname + // nickname let badge_combo = document.createElement("select"); - badge_combo.appendChild($el('option', {value:'none', text:'Badge: None'}, [])); - badge_combo.appendChild($el('option', {value:'nick', text:'Badge: Nickname'}, [])); - badge_combo.appendChild($el('option', {value:'id_nick', text:'Badge: #ID Nickname'}, [])); + badge_combo.appendChild($el('option', { value: 'none', text: 'Badge: None' }, [])); + badge_combo.appendChild($el('option', { value: 'nick', text: 'Badge: Nickname' }, [])); + badge_combo.appendChild($el('option', { value: 'id_nick', text: 'Badge: #ID Nickname' }, [])); - api.fetchApi('/manager/badge_mode') - .then(response => response.text()) - .then(data => { badge_combo.value = data; badge_mode = data; }); + api.fetchApi('/manager/badge_mode') + .then(response => response.text()) + .then(data => { badge_combo.value = data; badge_mode = data; }); - badge_combo.addEventListener('change', function(event) { - api.fetchApi(`/manager/badge_mode?value=${event.target.value}`); - badge_mode = event.target.value; - app.graph.setDirtyCanvas(true); + badge_combo.addEventListener('change', function (event) { + api.fetchApi(`/manager/badge_mode?value=${event.target.value}`); + badge_mode = event.target.value; + app.graph.setDirtyCanvas(true); }); - // channel + // channel let channel_combo = document.createElement("select"); - api.fetchApi('/manager/channel_url_list') - .then(response => response.json()) - .then(async data => { - try { - let urls = data.list; - for(let i in urls) { - if(urls[i] != '') { - let name_url = urls[i].split('::'); - channel_combo.appendChild($el('option', {value:name_url[0], text:`Channel: ${name_url[0]}`}, [])); - } - } + api.fetchApi('/manager/channel_url_list') + .then(response => response.json()) + .then(async data => { + try { + let urls = data.list; + for (let i in urls) { + if (urls[i] != '') { + let name_url = urls[i].split('::'); + channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, [])); + } + } - channel_combo.addEventListener('change', function(event) { - api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`); - }); + channel_combo.addEventListener('change', function (event) { + api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`); + }); - channel_combo.value = data.selected; - } - catch(exception) { + channel_combo.value = data.selected; + } + catch (exception) { - } - }); + } + }); return [ $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]), @@ -395,10 +444,70 @@ class ManagerMenuDialog extends ComfyDialog { }), $el("button", { + id: 'comfyworkflows-button', type: "button", textContent: "ComfyUI Workflow Gallery", onclick: () => { window.open("https://comfyworkflows.com/", "comfyui-workflow-gallery"); } - }), + }, [ + $el("div.pysssss-workflow-arrow-2", { + id: `comfyworkflows-button-arrow`, + // parent: document.getElementById(`comfyworkflows-button`), + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + + LiteGraph.closeAllContextMenus(); + const menu = new LiteGraph.ContextMenu( + [ + { + title: "Share your art", + callback: () => { + this.close(); + if (!ShareDialog.instance) { + ShareDialog.instance = new ShareDialog(); + } + + app.graphToPrompt().then(prompt => { + // console.log({ prompt }) + return app.graph._nodes; + }).then(nodes => { + // console.log({ nodes }); + const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes); + + if (potential_outputs.length === 0) { + if (potential_output_nodes.length === 0) { + // todo: add support for other output node types (animatediff combine, etc.) + const supported_nodes_string = SUPPORTED_OUTPUT_NODE_TYPES.join(", "); + alert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`); + } else { + alert("To share this, first run a prompt. Once it's done, click 'Share'."); + } + return; + } + + ShareDialog.instance.show({ potential_outputs, potential_output_nodes }); + }); + }, + }, + { + title: "Close", + callback: () => { + this.close(); + }, + } + ], + { + event: e, + scale: 1.3, + }, + window + ); + // set the id so that we can override the context menu's z-index to be above the comfyui manager menu + menu.root.id = "comfyworkflows-button-menu"; + menu.root.classList.add("pysssss-workflow-popup-2"); + }, + }) + ]), $el("button", { type: "button", @@ -447,9 +556,15 @@ class ManagerMenuDialog extends ComfyDialog { } } + app.registerExtension({ name: "Comfy.ManagerMenu", - + init() { + $el("style", { + textContent: style, + parent: document.head, + }); + }, async setup() { const menu = document.querySelector(".comfy-menu"); const separator = document.createElement("hr"); @@ -466,102 +581,152 @@ app.registerExtension({ manager_instance.show(); } menu.append(managerButton); + + + const shareButton = document.createElement("button"); + shareButton.textContent = "Share"; + shareButton.onclick = () => { + if (!ShareDialog.instance) { + ShareDialog.instance = new ShareDialog(); + } + + app.graphToPrompt().then(prompt => { + // console.log({ prompt }) + return app.graph._nodes; + }).then(nodes => { + // console.log({ nodes }); + const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes); + + if (potential_outputs.length === 0) { + if (potential_output_nodes.length === 0) { + // todo: add support for other output node types (animatediff combine, etc.) + const supported_nodes_string = SUPPORTED_OUTPUT_NODE_TYPES.join(", "); + alert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`); + } else { + alert("To share this, first run a prompt. Once it's done, click 'Share'."); + } + return; + } + + ShareDialog.instance.show({ potential_outputs, potential_output_nodes }); + }); + } + // make the background color a gradient of blue to green + shareButton.style.background = "linear-gradient(90deg, #00C9FF 0%, #92FE9D 100%)"; + shareButton.style.color = "black"; + + app.ui.settings.addSetting({ + id: "ComfyUIManager.ShowShareButtonInMainMenu", + name: "Show 'Share' button in the main menu", + type: "boolean", + defaultValue: true, + onChange: (value) => { + if (value) { + // show the button + shareButton.style.display = "inline-block"; + } else { + // hide the button + shareButton.style.display = "none"; + } + } + }); + + menu.append(shareButton); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { - const onDrawForeground = nodeType.prototype.onDrawForeground; - nodeType.prototype.onDrawForeground = function (ctx) { - const r = onDrawForeground?.apply?.(this, arguments); + const onDrawForeground = nodeType.prototype.onDrawForeground; + nodeType.prototype.onDrawForeground = function (ctx) { + const r = onDrawForeground?.apply?.(this, arguments); - if(!this.flags.collapsed && badge_mode != 'none' && nodeType.title_mode != LiteGraph.NO_TITLE) { - let text = ""; - if(badge_mode == 'id_nick') - text = `#${this.id} `; + if (!this.flags.collapsed && badge_mode != 'none' && nodeType.title_mode != LiteGraph.NO_TITLE) { + let text = ""; + if (badge_mode == 'id_nick') + text = `#${this.id} `; - if(nicknames[nodeData.name.trim()]) { - let nick = nicknames[nodeData.name.trim()]; + if (nicknames[nodeData.name.trim()]) { + let nick = nicknames[nodeData.name.trim()]; - if(nick.length > 25) { - text += nick.substring(0,23)+".."; - } - else { - text += nick; - } - } + if (nick.length > 25) { + text += nick.substring(0, 23) + ".."; + } + else { + text += nick; + } + } - if(text != "") { - let fgColor = "white"; - let bgColor = "#0F1F0F"; - let visible = true; + if (text != "") { + let fgColor = "white"; + let bgColor = "#0F1F0F"; + let visible = true; - ctx.save(); - ctx.font = "12px sans-serif"; - const sz = ctx.measureText(text); - ctx.fillStyle = bgColor; - ctx.beginPath(); - ctx.roundRect(this.size[0]-sz.width-12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); - ctx.fill(); + ctx.save(); + ctx.font = "12px sans-serif"; + const sz = ctx.measureText(text); + ctx.fillStyle = bgColor; + ctx.beginPath(); + ctx.roundRect(this.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); + ctx.fill(); - ctx.fillStyle = fgColor; - ctx.fillText(text, this.size[0]-sz.width-6, -LiteGraph.NODE_TITLE_HEIGHT - 6); - ctx.restore(); - } - } - - return r; - }; + ctx.fillStyle = fgColor; + ctx.fillText(text, this.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); + ctx.restore(); + } + } + return r; + }; }, async loadedGraphNode(node, app) { - if(node.has_errors) { - const onDrawForeground = node.onDrawForeground; - node.onDrawForeground = function (ctx) { - const r = onDrawForeground?.apply?.(this, arguments); + if (node.has_errors) { + const onDrawForeground = node.onDrawForeground; + node.onDrawForeground = function (ctx) { + const r = onDrawForeground?.apply?.(this, arguments); - if(!this.flags.collapsed && badge_mode != 'none') { - let text = ""; - if(badge_mode == 'id_nick') - text = `#${this.id} `; + if (!this.flags.collapsed && badge_mode != 'none') { + let text = ""; + if (badge_mode == 'id_nick') + text = `#${this.id} `; - if(nicknames[node.type.trim()]) { - let nick = nicknames[node.type.trim()]; + if (nicknames[node.type.trim()]) { + let nick = nicknames[node.type.trim()]; - if(nick.length > 25) { - text += nick.substring(0,23)+".."; - } - else { - text += nick; - } - } + if (nick.length > 25) { + text += nick.substring(0, 23) + ".."; + } + else { + text += nick; + } + } - if(text != "") { - let fgColor = "white"; - let bgColor = "#0F1F0F"; - let visible = true; + if (text != "") { + let fgColor = "white"; + let bgColor = "#0F1F0F"; + let visible = true; - ctx.save(); - ctx.font = "12px sans-serif"; - const sz = ctx.measureText(text); - ctx.fillStyle = bgColor; - ctx.beginPath(); - ctx.roundRect(this.size[0]-sz.width-12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); - ctx.fill(); + ctx.save(); + ctx.font = "12px sans-serif"; + const sz = ctx.measureText(text); + ctx.fillStyle = bgColor; + ctx.beginPath(); + ctx.roundRect(this.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); + ctx.fill(); - ctx.fillStyle = fgColor; - ctx.fillText(text, this.size[0]-sz.width-6, -LiteGraph.NODE_TITLE_HEIGHT - 6); - ctx.restore(); + ctx.fillStyle = fgColor; + ctx.fillText(text, this.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); + ctx.restore(); - ctx.save(); - ctx.font = "bold 14px sans-serif"; - const sz2 = ctx.measureText(node.type); - ctx.fillStyle = 'white'; - ctx.fillText(node.type, this.size[0]/2-sz2.width/2, this.size[1]/2); - ctx.restore(); - } - } + ctx.save(); + ctx.font = "bold 14px sans-serif"; + const sz2 = ctx.measureText(node.type); + ctx.fillStyle = 'white'; + ctx.fillText(node.type, this.size[0] / 2 - sz2.width / 2, this.size[1] / 2); + ctx.restore(); + } + } - return r; - }; - } + return r; + }; + } } -}); +}); \ No newline at end of file diff --git a/js/comfyui-share.js b/js/comfyui-share.js new file mode 100644 index 00000000..4ce453b3 --- /dev/null +++ b/js/comfyui-share.js @@ -0,0 +1,684 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js" +import { ComfyDialog, $el } from "../../scripts/ui.js"; + +export const SUPPORTED_OUTPUT_NODE_TYPES = [ + "PreviewImage", + "SaveImage", + "VHS_VideoCombine", + "ADE_AnimateDiffCombine", +] + +var docStyle = document.createElement('style'); +docStyle.innerHTML = ` +.cm-menu-container { + column-gap: 20px; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.cm-menu-column { + display: flex; + flex-direction: column; +} + +.cm-title { + padding: 10px 10px 0 10p; + background-color: black; + text-align: center; + height: 45px; +} +`; +document.head.appendChild(docStyle); + +export function getPotentialOutputsAndOutputNodes(nodes) { + const potential_outputs = []; + const potential_output_nodes = []; + + // iterate over the array of nodes to find the ones that are marked as SaveImage + // TODO: Add support for AnimateDiffCombine, etc. nodes that save videos/gifs, etc. + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (!SUPPORTED_OUTPUT_NODE_TYPES.includes(node.type)) { + continue; + } + + if (node.type === "SaveImage") { + potential_output_nodes.push(node); + + // check if node has an 'images' array property + if (node.hasOwnProperty("images") && Array.isArray(node.images)) { + // iterate over the images array and add each image to the potential_outputs array + for (let j = 0; j < node.images.length; j++) { + potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title }); + } + } + } + else if (node.type === "PreviewImage") { + potential_output_nodes.push(node); + + // check if node has an 'images' array property + if (node.hasOwnProperty("images") && Array.isArray(node.images)) { + // iterate over the images array and add each image to the potential_outputs array + for (let j = 0; j < node.images.length; j++) { + potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title }); + } + } + } + else if (node.type === "VHS_VideoCombine") { + potential_output_nodes.push(node); + + // check if node has a 'widgets' array property, with type 'image' + if (node.hasOwnProperty("widgets") && Array.isArray(node.widgets)) { + // iterate over the widgets array and add each image to the potential_outputs array + for (let j = 0; j < node.widgets.length; j++) { + if (node.widgets[j].type === "image") { + const widgetValue = node.widgets[j].value; + const parsedURLVals = parseURLPath(widgetValue); + + // ensure that the parsedURLVals have 'filename', 'subfolder', 'type', and 'format' properties + if (parsedURLVals.hasOwnProperty("filename") && parsedURLVals.hasOwnProperty("subfolder") && parsedURLVals.hasOwnProperty("type") && parsedURLVals.hasOwnProperty("format")) { + if (parsedURLVals.type !== "output") { + // TODO + } + potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": widgetValue, "format": parsedURLVals.format } }); + } + } else if (node.widgets[j].type === "preview") { + const widgetValue = node.widgets[j].value; + const parsedURLVals = widgetValue.params; + + // ensure that the parsedURLVals have 'filename', 'subfolder', 'type', and 'format' properties + if (parsedURLVals.hasOwnProperty("filename") && parsedURLVals.hasOwnProperty("subfolder") && parsedURLVals.hasOwnProperty("type") && parsedURLVals.hasOwnProperty("format")) { + if (parsedURLVals.type !== "output") { + // TODO + } + potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": `/view?filename=${parsedURLVals.filename}&subfolder=${parsedURLVals.subfolder}&type=${parsedURLVals.type}&format=${parsedURLVals.format}`, "format": parsedURLVals.format } }); + } + } + } + } + } + else if (node.type === "ADE_AnimateDiffCombine") { + potential_output_nodes.push(node); + + // check if node has a 'widgets' array property, with type 'image' + if (node.hasOwnProperty("widgets") && Array.isArray(node.widgets)) { + // iterate over the widgets array and add each image to the potential_outputs array + for (let j = 0; j < node.widgets.length; j++) { + if (node.widgets[j].type === "image") { + const widgetValue = node.widgets[j].value; + const parsedURLVals = parseURLPath(widgetValue); + // ensure that the parsedURLVals have 'filename', 'subfolder', 'type', and 'format' properties + if (parsedURLVals.hasOwnProperty("filename") && parsedURLVals.hasOwnProperty("subfolder") && parsedURLVals.hasOwnProperty("type") && parsedURLVals.hasOwnProperty("format")) { + if (parsedURLVals.type !== "output") { + // TODO + continue; + } + potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "type": parsedURLVals.type, "value": widgetValue, "format": parsedURLVals.format } }); + } + } + } + } + } + } + return { potential_outputs, potential_output_nodes }; +} + + +export function parseURLPath(urlPath) { + // Extract the query string from the URL path + var queryString = urlPath.split('?')[1]; + + // Use the URLSearchParams API to parse the query string + var params = new URLSearchParams(queryString); + + // Create an object to store the parsed parameters + var parsedParams = {}; + + // Iterate over each parameter and add it to the object + for (var pair of params.entries()) { + parsedParams[pair[0]] = pair[1]; + } + + // Return the object with the parsed parameters + return parsedParams; +} + +export class ShareDialog extends ComfyDialog { + static instance = null; + static matrix_auth = { homeserver: "matrix.org", username: "", password: "" }; + static cw_sharekey = ""; + + constructor() { + super(); + this.element = $el("div.comfy-modal", { + parent: document.body, style: { + 'overflow-y': "auto", + } + }, + [$el("div.comfy-modal-content", + {}, + [...this.createButtons()]), + ]); + this.selectedOutputIndex = 0; + } + + createButtons() { + this.radio_buttons = $el("div", { + 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 = false; //true; + + 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 = true; + + 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", + required: false, + }, []); + + this.title_input = $el("input", { + type: "text", + placeholder: "ex: My awesome art", + required: false + }, []); + + this.description_input = $el("textarea", { + placeholder: "ex: Trying out a new workflow... ", + required: false, + }, []); + + this.share_button = $el("button", { + type: "submit", + textContent: "Share", + style: { + backgroundColor: "blue" + } + }, []); + + this.final_message = $el("div", { + style: { + color: "white", + textAlign: "center", + // marginTop: "10px", + // backgroundColor: "black", + padding: "10px", + } + }, []); + + this.share_finalmessage_container = $el("div.cm-menu-container", { + id: "comfyui-share-finalmessage-container", + style: { + display: "none", + } + }, [ + $el("div.cm-menu-column", [ + this.final_message, + $el("button", { + type: "button", + textContent: "Close", + onclick: () => { + // Reset state + this.matrix_destination_checkbox.checked = false; + this.comfyworkflows_destination_checkbox.checked = true; + this.share_button.textContent = "Share"; + this.share_button.style.display = "inline-block"; + this.final_message.innerHTML = ""; + this.final_message.style.color = "white"; + 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 + this.share_finalmessage_container.style.display = "none"; + + // show the share container + this.share_container.style.display = "flex"; + + this.close() + } + }), + ]) + ]); + this.share_container = $el("div.cm-menu-container", { + id: "comfyui-share-container" + }, [ + $el("div.cm-menu-column", [ + $el("details", { + style: { + border: "1px solid #999", + padding: "5px", + borderRadius: "5px", + backgroundColor: "#222" + } + }, [ + $el("summary", { + style: { + color: "white", + cursor: "pointer", + } + }, [`Matrix account`]), + $el("div", { + style: { + display: "flex", + flexDirection: "row", + } + }, [ + $el("div", { + textContent: "Homeserver", + style: { + marginRight: "10px", + } + }, []), + this.matrix_homeserver_input, + ]), + + $el("div", { + style: { + display: "flex", + flexDirection: "row", + } + }, [ + $el("div", { + textContent: "Username", + style: { + marginRight: "10px", + } + }, []), + this.matrix_username_input, + ]), + + $el("div", { + style: { + display: "flex", + flexDirection: "row", + } + }, [ + $el("div", { + textContent: "Password", + style: { + marginRight: "10px", + } + }, []), + this.matrix_password_input, + ]), + + ]), + $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" }, ["When provided, your art will be saved to your account."]), + this.cw_sharekey_input, + ]), + + $el("div", {}, [ + $el("p", { + size: 3, color: "white", style: { + color: 'white' + } + }, [`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", { + textContent: "Credits (optional)", + size: 3, + color: "white", + style: { + color: 'white' + } + }, []), + this.credits_input, + // $el("br", {}, []), + + $el("h4", { + textContent: "Title (optional)", + size: 3, + color: "white", + style: { + color: 'white' + } + }, []), + this.title_input, + // $el("br", {}, []), + + $el("h4", { + textContent: "Description (optional)", + size: 3, + color: "white", + style: { + color: 'white' + } + }, []), + this.description_input, + $el("br", {}, []), + + $el("div", {}, [this.is_nsfw_checkbox, is_nsfw_checkbox_text]), + // $el("br", {}, []), + + // this.final_message, + // $el("br", {}, []), + ]), + $el("div.cm-menu-column", [ + this.radio_buttons, + $el("br", {}, []), + + this.share_button, + + $el("button", { + type: "button", + textContent: "Close", + onclick: () => { + // Reset state + this.matrix_destination_checkbox.checked = false; + this.comfyworkflows_destination_checkbox.checked = true; + this.share_button.textContent = "Share"; + this.share_button.style.display = "inline-block"; + this.final_message.innerHTML = ""; + this.final_message.style.color = "white"; + 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 + this.share_finalmessage_container.style.display = "none"; + + // show the share container + this.share_container.style.display = "flex"; + + this.close() + } + }), + $el("br", {}, []), + ]), + ]); + + // get the user's existing matrix auth and share key + ShareDialog.matrix_auth = { homeserver: "matrix.org", username: "", password: "" }; + try { + api.fetchApi(`/manager/get_matrix_auth`) + .then(response => response.json()) + .then(data => { + ShareDialog.matrix_auth = data; + this.matrix_homeserver_input.value = ShareDialog.matrix_auth.homeserver; + this.matrix_username_input.value = ShareDialog.matrix_auth.username; + this.matrix_password_input.value = ShareDialog.matrix_auth.password; + }) + .catch(error => { + // console.log(error); + }); + } catch (error) { + // console.log(error); + } + + // get the user's existing comfyworkflows share key + ShareDialog.cw_sharekey = ""; + try { + // console.log("Fetching comfyworkflows share key") + api.fetchApi(`/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; + + // console.log({ prompt, nodes }); + + const destinations = []; + 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")) { + let definedMatrixAuth = !!this.matrix_homeserver_input.value && !!this.matrix_username_input.value && !!this.matrix_password_input.value; + if (!definedMatrixAuth) { + alert("Please set your Matrix account details."); + return; + } + } + + if (destinations.includes("comfyworkflows") && !this.cw_sharekey_input.value && !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 }) + + if (potential_outputs.length === 0) { + if (potential_output_nodes.length === 0) { + // todo: add support for other output node types (animatediff combine, etc.) + const supported_nodes_string = SUPPORTED_OUTPUT_NODE_TYPES.join(", "); + alert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`); + } else { + alert("To share this, first run a prompt. Once it's done, click 'Share'."); + } + this.selectedOutputIndex = 0; + this.close(); + return; + } + + // Change the text of the share button to "Sharing..." to indicate that the share process has started + this.share_button.textContent = "Sharing..."; + + const response = await api.fetchApi(`/manager/share`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + matrix_auth: { + homeserver: this.matrix_homeserver_input.value, + 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, + // potential_output_nodes + }) + }); + + if (response.status != 200) { + try { + const response_json = await response.json(); + if (response_json.error) { + alert(response_json.error); + this.close(); + return; + } else { + alert("Failed to share your art. Please try again."); + this.close(); + return; + } + } catch (e) { + alert("Failed to share your art. Please try again."); + this.close(); + return; + } + } + + const response_json = await response.json(); + + if (response_json.comfyworkflows.url) { + this.final_message.innerHTML = "Your art has been shared: " + response_json.comfyworkflows.url + ""; + 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!"; + } + } + + this.final_message.style.color = "green"; + + // hide #comfyui-share-container and show #comfyui-share-finalmessage-container + this.share_container.style.display = "none"; + this.share_finalmessage_container.style.display = "block"; + + // hide the share button + this.share_button.textContent = "Shared!"; + this.share_button.style.display = "none"; + // this.close(); + } + + const res = + [ + $el("tr.td", { width: "100%" }, [ + $el("font", { size: 6, color: "white" }, [`Share your art`]), + ]), + $el("br", {}, []), + + this.share_finalmessage_container, + this.share_container, + ]; + + res[0].style.padding = "10px 10px 10px 10px"; + res[0].style.backgroundColor = "black"; //"linear-gradient(90deg, #00C9FF 0%, #92FE9D 100%)"; + res[0].style.textAlign = "center"; + res[0].style.height = "45px"; + return res; + } + + show({ potential_outputs, potential_output_nodes }) { + // console.log({ potential_outputs, potential_output_nodes }) + this.radio_buttons.innerHTML = ""; // clear the radio buttons + const new_radio_buttons = $el("div", { + id: "selectOutput-Options", + style: { + 'overflow-y': 'scroll', + 'max-height': '400px', + } + }, potential_outputs.map((output, index) => { + const radio_button = $el("input", { type: 'radio', name: "selectOutputImages", value: index, required: index === 0 }, []) + let radio_button_img; + if (output.type === "image" || output.type === "temp") { + radio_button_img = $el("img", { src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`, style: { width: "auto", height: "100px" } }, []); + } else if (output.type === "output") { + radio_button_img = $el("img", { src: output.output.value, style: { width: "auto", height: "100px" } }, []); + } 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("label", { + // style: { + // color: 'white' + // } + }, [output.title]) + radio_button.style.color = "var(--fg-color)"; + radio_button.checked = index === 0; + if (radio_button.checked) { + this.selectedOutputIndex = index; + } + + radio_button.onchange = () => { + this.selectedOutputIndex = parseInt(radio_button.value); + }; + + return $el("div", { + style: { + display: "flex", + 'align-items': 'center', + 'justify-content': 'space-between', + 'margin-bottom': '10px', + } + }, [radio_button, radio_button_text, radio_button_img]); + })); + const header = $el("h3", { + textContent: "Select an image to share", + size: 3, + color: "white", + style: { + 'text-align': 'center', + color: 'white', + backgroundColor: 'black', + padding: '10px', + 'margin-top': '0px', + } + }, [ + $el("p", { + textContent: "Scroll to see all outputs", + size: 2, + color: "white", + style: { + 'text-align': 'center', + color: 'white', + 'margin-bottom': '5px', + 'font-style': 'italic', + 'font-size': '12px', + }, + }, []) + ]); + this.radio_buttons.appendChild(header); + // this.radio_buttons.appendChild(subheader); + this.radio_buttons.appendChild(new_radio_buttons); + this.element.style.display = "block"; + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5779f396..3467b3e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -GitPython \ No newline at end of file +GitPython +matrix-client==0.4.0 \ No newline at end of file