mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2025-12-16 18:02:58 +08:00
Merge pull request #179 from OpenArt-AI/dev
Move share setting to ComfyUI Manager and add more share options
This commit is contained in:
commit
b88c5aeae1
@ -5,7 +5,8 @@
|
||||

|
||||
|
||||
## NOTICE
|
||||
* If you wish to hide the 'Share' button, please refer to the settings menu.
|
||||
* 🏆 John us for the [ComfyUI Workflow Contest](https://contest.openart.ai/), hosted by OpenArt AI (11.27.2023 - 12.15.2023). Our esteemed judge panel includes Scott E. Detweiler, Olivio Sarikas, MERJIC麦橘, among others. We're also thrilled to have the authors of ComfyUI Manager and AnimateDiff as our special guests!
|
||||
* If you wish to hide the "Share" button, click "Manager" and choose "Share: None" option.
|
||||
* You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page.
|
||||
* Versions prior to V0.22.2 will no longer detect missing nodes unless using a local database. Please update ComfyUI-Manager to the latest version.
|
||||
|
||||
|
||||
82
__init__.py
82
__init__.py
@ -119,6 +119,7 @@ def write_config():
|
||||
'git_exe': get_config()['git_exe'],
|
||||
'channel_url': get_config()['channel_url'],
|
||||
'channel_url_list': get_config()['channel_url_list'],
|
||||
'share_option': get_config()['share_option'],
|
||||
'bypass_ssl': get_config()['bypass_ssl']
|
||||
}
|
||||
with open(config_path, 'w') as configfile:
|
||||
@ -150,6 +151,7 @@ def read_config():
|
||||
'git_exe': default_conf['git_exe'] if 'git_exe' in default_conf else '',
|
||||
'channel_url': default_conf['channel_url'] if 'channel_url' in default_conf else 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main',
|
||||
'channel_url_list': ch_url_list,
|
||||
'share_option': default_conf['share_option'] if 'share_option' in default_conf else 'all',
|
||||
'bypass_ssl': default_conf['bypass_ssl'] if 'bypass_ssl' in default_conf else False,
|
||||
}
|
||||
|
||||
@ -160,6 +162,7 @@ def read_config():
|
||||
'git_exe': '',
|
||||
'channel_url': 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main',
|
||||
'channel_url_list': '',
|
||||
'share_option': 'all',
|
||||
'bypass_ssl': False
|
||||
}
|
||||
|
||||
@ -410,7 +413,7 @@ def git_pull(path):
|
||||
origin = repo.remote(name='origin')
|
||||
origin.pull()
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
|
||||
|
||||
repo.close()
|
||||
|
||||
return True
|
||||
@ -823,14 +826,22 @@ def get_current_snapshot():
|
||||
|
||||
|
||||
def save_snapshot_with_postfix(postfix):
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
file_name = f"{date_time_format}_{postfix}"
|
||||
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
file_name = f"{date_time_format}_{postfix}"
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{file_name}.json")
|
||||
with open(path, "w") as json_file:
|
||||
json.dump(get_current_snapshot(), json_file, indent=4)
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{file_name}.json")
|
||||
with open(path, "w") as json_file:
|
||||
json.dump(get_current_snapshot(), json_file, indent=4)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/snapshot/get_current")
|
||||
async def get_current_snapshot_api(request):
|
||||
try:
|
||||
return web.json_response(get_current_snapshot(), content_type='application/json')
|
||||
except:
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/snapshot/save")
|
||||
@ -1357,7 +1368,7 @@ async def install_model(request):
|
||||
if json_data['url'].startswith('https://github.com') or json_data['url'].startswith('https://huggingface.co'):
|
||||
model_dir = get_model_dir(json_data)
|
||||
download_url(json_data['url'], model_dir)
|
||||
|
||||
|
||||
return web.json_response({}, content_type='application/json')
|
||||
else:
|
||||
res = download_url_with_agent(json_data['url'], model_path)
|
||||
@ -1421,6 +1432,29 @@ async def channel_url_list(request):
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/manager/share_option")
|
||||
async def share_option(request):
|
||||
if "value" in request.rel_url.query:
|
||||
get_config()['share_option'] = request.rel_url.query['value']
|
||||
write_config()
|
||||
else:
|
||||
return web.Response(text=get_config()['share_option'], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
def get_openart_auth():
|
||||
if not os.path.exists(os.path.join(folder_paths.base_path, ".openart_key")):
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(folder_paths.base_path, ".openart_key"), "r") as f:
|
||||
openart_key = f.read().strip()
|
||||
return openart_key if openart_key else None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_matrix_auth():
|
||||
if not os.path.exists(os.path.join(folder_paths.base_path, "matrix_auth")):
|
||||
return None
|
||||
@ -1450,6 +1484,22 @@ def get_comfyworkflows_auth():
|
||||
except:
|
||||
return None
|
||||
|
||||
@server.PromptServer.instance.routes.get("/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})
|
||||
|
||||
@server.PromptServer.instance.routes.post("/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(folder_paths.base_path, ".openart_key"), "w") as f:
|
||||
f.write(openart_key)
|
||||
return web.Response(status=200)
|
||||
|
||||
@server.PromptServer.instance.routes.get("/manager/get_matrix_auth")
|
||||
async def api_get_matrix_auth(request):
|
||||
# print("Getting stored Matrix credentials...")
|
||||
@ -1504,13 +1554,13 @@ async def share_art(request):
|
||||
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()
|
||||
|
||||
@ -1535,7 +1585,7 @@ async def share_art(request):
|
||||
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(
|
||||
@ -1543,7 +1593,7 @@ async def share_art(request):
|
||||
json={
|
||||
"assetFileName": asset_filename,
|
||||
"assetFileType": assetFileType,
|
||||
"workflowJsonFileName" : 'workflow.json',
|
||||
"workflowJsonFileName" : 'workflow.json',
|
||||
"workflowJsonFileType" : 'application/json',
|
||||
|
||||
},
|
||||
@ -1554,7 +1604,7 @@ async def share_art(request):
|
||||
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:
|
||||
@ -1605,7 +1655,7 @@ async def share_art(request):
|
||||
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'])
|
||||
@ -1617,7 +1667,7 @@ async def share_art(request):
|
||||
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 = ""
|
||||
@ -1634,7 +1684,7 @@ async def share_art(request):
|
||||
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}",
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
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 { ShareDialog, SUPPORTED_OUTPUT_NODE_TYPES, getPotentialOutputsAndOutputNodes, ShareDialogChooser, showOpenArtShareDialog, showShareDialog } from "./comfyui-share-common.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import { CustomNodesInstaller } from "./custom-nodes-downloader.js";
|
||||
import { AlternativesInstaller } from "./a1111-alter-downloader.js";
|
||||
import { SnapshotManager } from "./snapshot.js";
|
||||
@ -36,13 +37,14 @@ var update_comfyui_button = null;
|
||||
var fetch_updates_button = null;
|
||||
var update_all_button = null;
|
||||
var badge_mode = "none";
|
||||
let share_option = 'all';
|
||||
|
||||
// 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;
|
||||
@ -79,7 +81,16 @@ async function init_badge_mode() {
|
||||
.then(data => { badge_mode = data; })
|
||||
}
|
||||
|
||||
async function init_share_option() {
|
||||
api.fetchApi('/manager/share_option')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
share_option = data || 'all';
|
||||
});
|
||||
}
|
||||
|
||||
await init_badge_mode();
|
||||
await init_share_option();
|
||||
|
||||
|
||||
async function fetchNicknames() {
|
||||
@ -235,17 +246,29 @@ 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;
|
||||
}
|
||||
|
||||
return classList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the node is a potential output node (img, gif or video output)
|
||||
*/
|
||||
const isOutputNode = (node) => {
|
||||
return [
|
||||
"VHS_VideoCombine",
|
||||
"PreviewImage",
|
||||
"SaveImage",
|
||||
"ADE_AnimateDiffCombine",
|
||||
"SaveAnimatedWEBP",
|
||||
].includes(node.type);
|
||||
}
|
||||
|
||||
// -----------
|
||||
class ManagerMenuDialog extends ComfyDialog {
|
||||
@ -401,12 +424,45 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
});
|
||||
|
||||
// share
|
||||
let share_combo = document.createElement("select");
|
||||
const share_options = [
|
||||
['none', 'None'],
|
||||
['openart', 'OpenArt AI'],
|
||||
['matrix', 'Matrix Server'],
|
||||
['comfyworkflows', 'ComfyWorkflows'],
|
||||
['all', 'All'],
|
||||
];
|
||||
for (const option of share_options) {
|
||||
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
||||
}
|
||||
|
||||
api.fetchApi('/manager/share_option')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
share_combo.value = data || 'all';
|
||||
share_option = data || 'all';
|
||||
});
|
||||
|
||||
share_combo.addEventListener('change', function (event) {
|
||||
const value = event.target.value;
|
||||
share_option = value;
|
||||
api.fetchApi(`/manager/share_option?value=${value}`);
|
||||
const shareButton = document.getElementById("shareButton");
|
||||
if (value === 'none') {
|
||||
shareButton.style.display = "none";
|
||||
} else {
|
||||
shareButton.style.display = "inline-block";
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
$el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]),
|
||||
$el("br", {}, []),
|
||||
preview_combo,
|
||||
badge_combo,
|
||||
channel_combo,
|
||||
share_combo,
|
||||
|
||||
$el("hr", {}, []),
|
||||
$el("center", {}, ["!! EXPERIMENTAL !!"]),
|
||||
@ -466,14 +522,14 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
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.)
|
||||
@ -484,7 +540,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ShareDialog.instance.show({ potential_outputs, potential_output_nodes });
|
||||
});
|
||||
},
|
||||
@ -582,54 +638,31 @@ app.registerExtension({
|
||||
}
|
||||
menu.append(managerButton);
|
||||
|
||||
|
||||
const shareButton = document.createElement("button");
|
||||
shareButton.id = "shareButton";
|
||||
shareButton.textContent = "Share";
|
||||
shareButton.onclick = () => {
|
||||
if (!ShareDialog.instance) {
|
||||
ShareDialog.instance = new ShareDialog();
|
||||
if (share_option === 'openart') {
|
||||
showOpenArtShareDialog();
|
||||
return;
|
||||
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
|
||||
showShareDialog(share_option);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ShareDialogChooser.instance) {
|
||||
ShareDialogChooser.instance = new ShareDialogChooser();
|
||||
}
|
||||
|
||||
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'.\n\nNOTE: Images of the Share target can only be selected in the PreviewImage, SaveImage, and VHS_VideoCombine nodes. In the case of VHS_VideoCombine, only the image/gif and image/webp formats are supported.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ShareDialog.instance.show({ potential_outputs, potential_output_nodes });
|
||||
});
|
||||
ShareDialogChooser.instance.show();
|
||||
}
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
});
|
||||
// Load share option from local storage to determine whether to show
|
||||
// the share button.
|
||||
const shouldShowShareButton = share_option !== 'none';
|
||||
shareButton.style.display = shouldShowShareButton ? "inline-block" : "none";
|
||||
|
||||
menu.append(shareButton);
|
||||
},
|
||||
@ -675,10 +708,12 @@ app.registerExtension({
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
this._addExtraNodeContextMenu(nodeType, app);
|
||||
},
|
||||
|
||||
async loadedGraphNode(node, app) {
|
||||
if (node.has_errors) {
|
||||
if (node.has_errors) {
|
||||
const onDrawForeground = node.onDrawForeground;
|
||||
node.onDrawForeground = function (ctx) {
|
||||
const r = onDrawForeground?.apply?.(this, arguments);
|
||||
@ -728,5 +763,45 @@ app.registerExtension({
|
||||
return r;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_addExtraNodeContextMenu(node, app) {
|
||||
const origGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
|
||||
node.prototype.getExtraMenuOptions = function (_, options) {
|
||||
origGetExtraMenuOptions?.apply?.(this, arguments);
|
||||
if (isOutputNode(node)) {
|
||||
const { potential_outputs } = getPotentialOutputsAndOutputNodes([this]);
|
||||
const hasOutput = potential_outputs.length > 0;
|
||||
|
||||
// Check if the previous menu option is `null`. If it's not,
|
||||
// then we need to add a `null` as a separator.
|
||||
if (options[options.length - 1] !== null) {
|
||||
options.push(null);
|
||||
}
|
||||
|
||||
options.push({
|
||||
content: "🏞️ Share Output",
|
||||
disabled: !hasOutput,
|
||||
callback: (obj) => {
|
||||
if (!ShareDialog.instance) {
|
||||
ShareDialog.instance = new ShareDialog();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
|
||||
export const SUPPORTED_OUTPUT_NODE_TYPES = [
|
||||
"PreviewImage",
|
||||
"SaveImage",
|
||||
"VHS_VideoCombine",
|
||||
"ADE_AnimateDiffCombine",
|
||||
"SaveAnimatedWEBP"
|
||||
"SaveAnimatedWEBP",
|
||||
]
|
||||
|
||||
var docStyle = document.createElement('style');
|
||||
@ -46,30 +47,26 @@ export function getPotentialOutputsAndOutputNodes(nodes) {
|
||||
}
|
||||
|
||||
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_output_nodes.push(node);
|
||||
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_output_nodes.push(node);
|
||||
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
|
||||
@ -83,13 +80,14 @@ export function getPotentialOutputsAndOutputNodes(nodes) {
|
||||
if (parsedURLVals.type !== "output") {
|
||||
// TODO
|
||||
}
|
||||
potential_output_nodes.push(node);
|
||||
potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": widgetValue, "format": parsedURLVals.format } });
|
||||
}
|
||||
} else if (node.widgets[j].type === "preview") {
|
||||
const widgetValue = node.widgets[j].value;
|
||||
const parsedURLVals = widgetValue.params;
|
||||
|
||||
if (!parsedURLVals.format.startsWith('image')) {
|
||||
if(!parsedURLVals.format.startsWith('image')) {
|
||||
// video isn't supported format
|
||||
continue;
|
||||
}
|
||||
@ -99,6 +97,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) {
|
||||
if (parsedURLVals.type !== "output") {
|
||||
// TODO
|
||||
}
|
||||
potential_output_nodes.push(node);
|
||||
potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "value": `/view?filename=${parsedURLVals.filename}&subfolder=${parsedURLVals.subfolder}&type=${parsedURLVals.type}&format=${parsedURLVals.format}`, "format": parsedURLVals.format } });
|
||||
}
|
||||
}
|
||||
@ -106,8 +105,6 @@ export function getPotentialOutputsAndOutputNodes(nodes) {
|
||||
}
|
||||
}
|
||||
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
|
||||
@ -121,6 +118,7 @@ export function getPotentialOutputsAndOutputNodes(nodes) {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
potential_output_nodes.push(node);
|
||||
potential_outputs.push({ "type": "output", 'title': node.title, "output": { "filename": parsedURLVals.filename, "subfolder": parsedURLVals.subfolder, "type": parsedURLVals.type, "value": widgetValue, "format": parsedURLVals.format } });
|
||||
}
|
||||
}
|
||||
@ -128,17 +126,18 @@ export function getPotentialOutputsAndOutputNodes(nodes) {
|
||||
}
|
||||
}
|
||||
else if (node.type === "SaveAnimatedWEBP") {
|
||||
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_output_nodes.push(node);
|
||||
potential_outputs.push({ "type": "image", "image": node.images[j], "title": node.title });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: make sure that two arrays are the same length
|
||||
return { potential_outputs, potential_output_nodes };
|
||||
}
|
||||
|
||||
@ -162,13 +161,242 @@ export function parseURLPath(urlPath) {
|
||||
return parsedParams;
|
||||
}
|
||||
|
||||
|
||||
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 showShareDialog = async (share_option) => {
|
||||
if (!ShareDialog.instance) {
|
||||
ShareDialog.instance = new ShareDialog(share_option);
|
||||
}
|
||||
return 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'.\n\nNOTE: Images of the Share target can only be selected in the PreviewImage, SaveImage, and VHS_VideoCombine nodes. In the case of VHS_VideoCombine, only the image/gif and image/webp formats are supported.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ShareDialog.instance.show({ potential_outputs, potential_output_nodes, share_option });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export class ShareDialogChooser extends ComfyDialog {
|
||||
static instance = null;
|
||||
constructor() {
|
||||
super();
|
||||
this.element = $el("div.comfy-modal", {
|
||||
parent: document.body, style: {
|
||||
'overflow-y': "auto",
|
||||
}
|
||||
},
|
||||
[$el("div.comfy-modal-content",
|
||||
{},
|
||||
[...this.createButtons()]),
|
||||
]);
|
||||
this.selectedNodeId = null;
|
||||
}
|
||||
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: "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) => {
|
||||
suc && this.close();
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "comfyworkflows",
|
||||
textContent: "ComfyWorkflows",
|
||||
website: "https://comfyworkflows.com",
|
||||
description: "Share ComfyUI art on comfyworkflows.com",
|
||||
onclick: () => {
|
||||
showShareDialog('comfyworkflows').then((suc) => {
|
||||
suc && this.close();
|
||||
})
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
function createShareButtonsWithDescriptions() {
|
||||
// Responsive container
|
||||
const container = $el("div", {
|
||||
style: {
|
||||
display: "flex",
|
||||
'flex-wrap': 'wrap',
|
||||
'justify-content': 'space-around',
|
||||
'padding': '20px',
|
||||
}
|
||||
});
|
||||
|
||||
buttons.forEach(b => {
|
||||
const button = $el("button", {
|
||||
type: "button",
|
||||
textContent: b.textContent,
|
||||
onclick: b.onclick,
|
||||
style: {
|
||||
'width': '25%',
|
||||
'minWidth': '200px',
|
||||
'background-color': b.backgroundColor || '',
|
||||
'border-radius': '5px',
|
||||
'cursor': 'pointer',
|
||||
'padding': '5px 5px',
|
||||
'margin-bottom': '5px',
|
||||
'transition': 'background-color 0.3s',
|
||||
}
|
||||
});
|
||||
button.addEventListener('mouseover', () => {
|
||||
button.style.backgroundColor = '#007BFF'; // Change color on hover
|
||||
});
|
||||
button.addEventListener('mouseout', () => {
|
||||
button.style.backgroundColor = b.backgroundColor || '';
|
||||
});
|
||||
|
||||
const description = $el("p", {
|
||||
textContent: b.description,
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
color: 'white',
|
||||
'font-size': '14px',
|
||||
'margin-bottom': '10px',
|
||||
},
|
||||
});
|
||||
|
||||
const websiteLink = $el("a", {
|
||||
textContent: "🌐 Website",
|
||||
href: b.website,
|
||||
target: "_blank",
|
||||
style: {
|
||||
color: 'white',
|
||||
'margin-left': '10px',
|
||||
'font-size': '12px',
|
||||
'text-decoration': 'none',
|
||||
'align-self': 'center',
|
||||
},
|
||||
});
|
||||
|
||||
// Add highlight to the website link
|
||||
websiteLink.addEventListener('mouseover', () => {
|
||||
websiteLink.style.opacity = '0.7';
|
||||
});
|
||||
|
||||
websiteLink.addEventListener('mouseout', () => {
|
||||
websiteLink.style.opacity = '1';
|
||||
});
|
||||
|
||||
const buttonLinkContainer = $el("div", {
|
||||
style: {
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
'margin-bottom': '10px',
|
||||
}
|
||||
}, [button, websiteLink]);
|
||||
|
||||
const column = $el("div", {
|
||||
style: {
|
||||
'flex-basis': '100%',
|
||||
'margin': '10px',
|
||||
'padding': '20px',
|
||||
'border': '1px solid #ddd',
|
||||
'border-radius': '5px',
|
||||
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
}
|
||||
}, [buttonLinkContainer, description]);
|
||||
|
||||
container.appendChild(column);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
return [
|
||||
$el("p", {
|
||||
textContent: 'Choose a platform to share your workflow',
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
'color': 'white',
|
||||
'font-size': '18px',
|
||||
'margin-bottom': '10px',
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
$el("div.cm-menu-container", {
|
||||
id: "comfyui-share-container"
|
||||
}, [
|
||||
$el("div.cm-menu-column", [
|
||||
createShareButtonsWithDescriptions(),
|
||||
$el("br", {}, []),
|
||||
]),
|
||||
]),
|
||||
$el("div.cm-menu-container", {
|
||||
id: "comfyui-share-container"
|
||||
}, [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
style: {
|
||||
margin: "0 25px",
|
||||
width: "100%",
|
||||
},
|
||||
textContent: "Close",
|
||||
onclick: () => {
|
||||
this.close()
|
||||
}
|
||||
}),
|
||||
$el("br", {}, []),
|
||||
]),
|
||||
];
|
||||
}
|
||||
show() {
|
||||
this.element.style.display = "block";
|
||||
}
|
||||
}
|
||||
export class ShareDialog extends ComfyDialog {
|
||||
static instance = null;
|
||||
static matrix_auth = { homeserver: "matrix.org", username: "", password: "" };
|
||||
static cw_sharekey = "";
|
||||
|
||||
constructor() {
|
||||
constructor(share_option) {
|
||||
super();
|
||||
this.share_option = share_option;
|
||||
this.element = $el("div.comfy-modal", {
|
||||
parent: document.body, style: {
|
||||
'overflow-y': "auto",
|
||||
@ -195,12 +423,12 @@ export class ShareDialog extends ComfyDialog {
|
||||
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.matrix_destination_checkbox.checked = this.share_option === 'matrix'; //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.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 || '' }, []);
|
||||
@ -257,8 +485,8 @@ export class ShareDialog extends ComfyDialog {
|
||||
textContent: "Close",
|
||||
onclick: () => {
|
||||
// Reset state
|
||||
this.matrix_destination_checkbox.checked = false;
|
||||
this.comfyworkflows_destination_checkbox.checked = true;
|
||||
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 = "";
|
||||
@ -429,8 +657,8 @@ export class ShareDialog extends ComfyDialog {
|
||||
textContent: "Close",
|
||||
onclick: () => {
|
||||
// Reset state
|
||||
this.matrix_destination_checkbox.checked = false;
|
||||
this.comfyworkflows_destination_checkbox.checked = true;
|
||||
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 = "";
|
||||
@ -622,7 +850,27 @@ export class ShareDialog extends ComfyDialog {
|
||||
return res;
|
||||
}
|
||||
|
||||
show({ potential_outputs, potential_output_nodes }) {
|
||||
show({ potential_outputs, potential_output_nodes, share_option }) {
|
||||
// Sort `potential_output_nodes` by node ID to make the order always
|
||||
// consistent, but we should also keep `potential_outputs` in the same
|
||||
// order as `potential_output_nodes`.
|
||||
const potential_output_to_order = {};
|
||||
potential_output_nodes.forEach((node, index) => {
|
||||
potential_output_to_order[node.id] = [node, potential_outputs[index]];
|
||||
})
|
||||
// Sort the object `potential_output_to_order` by key (node ID)
|
||||
const sorted_potential_output_to_order = Object.fromEntries(
|
||||
Object.entries(potential_output_to_order).sort((a, b) => a[0].id - b[0].id)
|
||||
);
|
||||
const sorted_potential_outputs = []
|
||||
const sorted_potential_output_nodes = []
|
||||
for (const [key, value] of Object.entries(sorted_potential_output_to_order)) {
|
||||
sorted_potential_output_nodes.push(value[0]);
|
||||
sorted_potential_outputs.push(value[1]);
|
||||
}
|
||||
potential_output_nodes = sorted_potential_output_nodes;
|
||||
potential_outputs = sorted_potential_outputs;
|
||||
|
||||
// console.log({ potential_outputs, potential_output_nodes })
|
||||
this.radio_buttons.innerHTML = ""; // clear the radio buttons
|
||||
const new_radio_buttons = $el("div", {
|
||||
@ -632,6 +880,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
'max-height': '400px',
|
||||
}
|
||||
}, potential_outputs.map((output, index) => {
|
||||
const potential_output_node = potential_output_nodes[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") {
|
||||
@ -650,7 +899,17 @@ export class ShareDialog extends ComfyDialog {
|
||||
// }
|
||||
}, [output.title])
|
||||
radio_button.style.color = "var(--fg-color)";
|
||||
radio_button.checked = index === 0;
|
||||
|
||||
// Make the radio button checked if it's the selected node,
|
||||
// otherwise make the first radio button checked.
|
||||
if (this.selectedNodeId) {
|
||||
if (this.selectedNodeId === potential_output_node.id) {
|
||||
radio_button.checked = true;
|
||||
}
|
||||
} else {
|
||||
radio_button.checked = index === 0;
|
||||
}
|
||||
|
||||
if (radio_button.checked) {
|
||||
this.selectedOutputIndex = index;
|
||||
}
|
||||
@ -697,5 +956,14 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
710
js/comfyui-share-openart.js
Normal file
710
js/comfyui-share-openart.js
Normal file
@ -0,0 +1,710 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
|
||||
const LOCAL_STORAGE_KEY = "openart_comfy_workflow_key";
|
||||
const DEFAULT_HOMEPAGE_URL = "https://openart.ai/workflows/dev?developer=true";
|
||||
//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(`/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(`/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: "text",
|
||||
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: 'white',
|
||||
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"},[])
|
||||
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(
|
||||
`/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) {
|
||||
alert(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(`/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");
|
||||
}
|
||||
|
||||
if (this.uploadImagesInput.files.length === 0) {
|
||||
throw new Error("No thumbnail uploaded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const join_contest = this.joinContestCheckbox.checked;
|
||||
|
||||
try {
|
||||
const response = await this.fetchApi(
|
||||
"/workflows/publish",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
workflow_json: workflowJSON,
|
||||
upload_images: this.uploadedImages,
|
||||
form_values,
|
||||
advanced_config: {
|
||||
workflow_api_json: workflowAPIJSON,
|
||||
snapshot: current_snapshot,
|
||||
},
|
||||
join_contest,
|
||||
}),
|
||||
},
|
||||
"Uploading workflow..."
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const { workflow_id } = response.data;
|
||||
if (workflow_id) {
|
||||
const url = `https://openart.ai/workflows/-/-/${workflow_id}`;
|
||||
this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`;
|
||||
this.previewImage.src = "";
|
||||
this.previewImage.style.display = "none";
|
||||
this.uploadedImages = [];
|
||||
this.NameInput.value = "";
|
||||
this.descriptionInput.value = "";
|
||||
this.radioButtons.forEach((ele) => {
|
||||
ele.checked = false;
|
||||
ele.parentElement.classList.remove("checked");
|
||||
});
|
||||
this.selectedOutputIndex = 0;
|
||||
this.selectedNodeId = null;
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Error sharing workflow: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchImageBlob(url) {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return blob;
|
||||
}
|
||||
|
||||
async show({ potential_outputs, potential_output_nodes } = {}) {
|
||||
// Sort `potential_output_nodes` by node ID to make the order always
|
||||
// consistent, but we should also keep `potential_outputs` in the same
|
||||
// order as `potential_output_nodes`.
|
||||
const potential_output_to_order = {};
|
||||
potential_output_nodes.forEach((node, index) => {
|
||||
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 radio_button = $el("input", { type: 'radio', name: "selectOutputImages", value: index, required: index === 0 }, [])
|
||||
let radio_button_img;
|
||||
let filename;
|
||||
if (output.type === "image" || output.type === "temp") {
|
||||
radio_button_img = $el("img", { src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`, style: { width: "100px", height: "100px", objectFit: "cover", borderRadius: "5px" } }, []);
|
||||
filename = output.image.filename
|
||||
} else if (output.type === "output") {
|
||||
radio_button_img = $el("img", { src: output.output.value, style: { width: "auto", height: "100px", objectFit: "cover", borderRadius: "5px" } }, []);
|
||||
filename = output.filename
|
||||
} else {
|
||||
// unsupported output type
|
||||
// this should never happen
|
||||
// TODO
|
||||
radio_button_img = $el("img", { src: "", style: { width: "auto", height: "100px" } }, []);
|
||||
}
|
||||
const radio_button_text = $el("span", {
|
||||
style: {
|
||||
color: 'gray',
|
||||
display: 'block',
|
||||
fontSize: '12px',
|
||||
overflowX: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textWrap: 'nowrap',
|
||||
maxWidth: '100px',
|
||||
}
|
||||
}, [output.title])
|
||||
const node_id_chip = $el("span", {
|
||||
style: {
|
||||
color: '#FBFBFD',
|
||||
display: 'block',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
fontSize: '12px',
|
||||
overflowX: 'hidden',
|
||||
padding: '2px 3px',
|
||||
textOverflow: 'ellipsis',
|
||||
textWrap: 'nowrap',
|
||||
maxWidth: '100px',
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
left: '3px',
|
||||
borderRadius: '3px',
|
||||
}
|
||||
}, [`Node: ${potential_output_nodes[index].id}`])
|
||||
radio_button.style.color = "var(--fg-color)";
|
||||
radio_button.checked = this.selectedOutputIndex === index;
|
||||
|
||||
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: 'white',
|
||||
margin: '0 0 5px 0',
|
||||
fontSize: '12px',
|
||||
},
|
||||
}, [])
|
||||
this.outputsSection.innerHTML = "";
|
||||
this.outputsSection.appendChild(header);
|
||||
this.outputsSection.appendChild(new_radio_buttons);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user