diff --git a/__init__.py b/__init__.py index 7fec35a..dabba90 100644 --- a/__init__.py +++ b/__init__.py @@ -193,8 +193,31 @@ if hasattr(PromptServer, "instance"): } PromptServer.instance.app.router.add_static( - "/mtb-assets/", path=(here / "html").as_posix(), name="static" + "/mtb-assets/", path=(here / "html").as_posix() ) + + @PromptServer.instance.routes.get("/mtb/manage") + async def manage(request): + from . import endpoint + + reload(endpoint) + + endlog.debug("Initializing Manager") + if "text/html" in request.headers.get("Accept", ""): + csv_editor = endpoint.csv_editor() + + tabview = endpoint.render_tab_view(Styles=csv_editor) + return web.Response( + text=endpoint.render_base_template("MTB", tabview), + content_type="text/html", + ) + + return web.json_response( + { + "message": "manage only has a POST api for now", + } + ) + @PromptServer.instance.routes.get("/mtb/status") async def get_full_library(request): from . import endpoint @@ -258,6 +281,7 @@ if hasattr(PromptServer, "instance"): # # Return an HTML page html_response = """ diff --git a/endpoint.py b/endpoint.py index 2adc583..c0288a7 100644 --- a/endpoint.py +++ b/endpoint.py @@ -1,7 +1,13 @@ -from .utils import here, run_command, comfy_mode, import_install +from .utils import ( + here, + import_install, + styles_dir, + backup_file, +) from aiohttp import web from .log import mklog -import sys +import csv + endlog = mklog("mtb endpoint") @@ -49,6 +55,32 @@ def ACTIONS_getStyles(style_name=None): return {"error": "No styles found"} +def ACTIONS_saveStyle(data): + # endlog.debug(f"Received Save Styles for {data.keys()}") + # endlog.debug(data) + + styles = [f.name for f in styles_dir.iterdir() if f.suffix == ".csv"] + target = None + rows = [] + for fp, content in data.items(): + if fp in styles: + endlog.debug(f"Overwriting {fp}") + target = styles_dir / fp + rows = content + break + + if not target: + endlog.warning(f"Could not determine the target file for {data.keys()}") + return {"error": "Could not determine the target file for the style"} + + backup_file(target) + + with target.open("w", newline="", encoding="utf-8") as file: + csv_writer = csv.writer(file, quoting=csv.QUOTE_ALL) + for row in rows: + csv_writer.writerow(row) + + async def do_action(request) -> web.Response: endlog.debug("Init action request") request_data = await request.json() @@ -84,6 +116,129 @@ def dependencies_button(name, dependencies): """ +def csv_editor(): + inputs = [f for f in styles_dir.iterdir() if f.suffix == ".csv"] + # rows = {f.stem: list(csv.reader(f.read_text("utf8"))) for f in styles} + + style_files = {} + for file in inputs: + with open(file, "r", encoding="utf8") as f: + parsed = csv.reader(f) + style_files[file.name] = [] + for row in parsed: + endlog.debug(f"Adding style {row[0]}") + style_files[file.name].append((row[0], row[1], row[2])) + + html_out = """ +
+

Style Editor

+ + """ + for current, styles in style_files.items(): + current_out = f"

{current}

" + table_rows = [] + for index, style in enumerate(styles): + table_rows += ( + ([""] + [f"{cell}" for cell in style] + [""]) + if index == 0 + else ( + [""] + + [ + f"" + if i == 0 + else f"" + for i, cell in enumerate(style) + ] + + [""] + ) + ) + current_out += ( + f"" + + "".join(table_rows) + + "
" + ) + current_out += f"" + + html_out += add_foldable_region(current, current_out) + + html_out += "
" + html_out += """""" + + return html_out + + +def render_tab_view(**kwargs): + tab_headers = [] + tab_contents = [] + + for idx, (tab_name, content) in enumerate(kwargs.items()): + active_class = "active" if idx == 0 else "" + tab_headers.append( + f"" + ) + tab_contents.append( + f"
{content}
" + ) + + headers_str = "\n".join(tab_headers) + contents_str = "\n".join(tab_contents) + + return f""" +
+
+ {headers_str} +
+ {contents_str} +
+ + """ + + +def add_foldable_region(title, content): + symbol_id = f"{title}-symbol" + return f""" +
+
+ ▷ + {title} +
+
+ {content} +
+
+ + """ + + +def add_split_pane(left_content, right_content, vertical=True): + orientation = "vertical" if vertical else "horizontal" + return f""" +
+
+ {left_content} +
+
+
+ {right_content} +
+
+ + + """ + + +def add_dropdown(title, options): + option_str = "\n".join([f"" for opt in options]) + return f""" + + """ + + def render_table(table_dict, sort=True, title=None): table_dict = sorted( table_dict.items(), key=lambda item: item[0] diff --git a/html/js/foldable.js b/html/js/foldable.js new file mode 100644 index 0000000..71f036c --- /dev/null +++ b/html/js/foldable.js @@ -0,0 +1,20 @@ +/** + * File: foldable.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +function toggleFoldable(elementId, symbolId) { + const content = document.getElementById(elementId) + const symbol = document.getElementById(symbolId) + if (content.style.display === 'none' || content.style.display === '') { + content.style.display = 'flex' + symbol.innerHTML = '▽' // Down arrow + } else { + content.style.display = 'none' + symbol.innerHTML = '▷' // Right arrow + } +} diff --git a/html/js/saveTableData.js b/html/js/saveTableData.js new file mode 100644 index 0000000..a31fd32 --- /dev/null +++ b/html/js/saveTableData.js @@ -0,0 +1,54 @@ +/** + * File: saveTableData.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +function saveTableData(identifier) { + const table = document.querySelector( + `#style-editor table[data-id='${identifier}']` + ) + + let currentData = [] + const rows = table.querySelectorAll('tr') + const filename = table.getAttribute('data-id') + + rows.forEach((row, rowIndex) => { + const rowData = [] + const cells = + rowIndex === 0 + ? row.querySelectorAll('th') + : row.querySelectorAll('td input, td textarea') + + cells.forEach((cell) => { + rowData.push(rowIndex === 0 ? cell.textContent : cell.value) + }) + + currentData.push(rowData) + }) + + let tablesData = {} + tablesData[filename] = currentData + + console.debug('Sending styles to manage endpoint:', tablesData) + fetch('/mtb/actions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'saveStyle', + args: tablesData, + }), + }) + .then((response) => response.json()) + .then((data) => { + console.debug('Success:', data) + }) + .catch((error) => { + console.error('Error:', error) + }) +} diff --git a/html/js/splitPane.js b/html/js/splitPane.js new file mode 100644 index 0000000..a34b71b --- /dev/null +++ b/html/js/splitPane.js @@ -0,0 +1,34 @@ +/** + * File: splitPane.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +function initSplitPane(vertical) { + let resizer = document.getElementById('resizer') + let left = document.getElementById('leftPane') + let right = document.getElementById('rightPane') + resizer.addEventListener('mousedown', function (e) { + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', function () { + document.removeEventListener('mousemove', onMouseMove) + }) + }) + + const onMouseMove = (e) => { + if (vertical) { + let leftWidth = e.clientX + let rightWidth = window.innerWidth - e.clientX + left.style.width = leftWidth + 'px' + right.style.width = rightWidth + 'px' + } else { + let topHeight = e.clientY + let bottomHeight = window.innerHeight - e.clientY + left.style.height = topHeight + 'px' + right.style.height = bottomHeight + 'px' + } + } +} diff --git a/html/js/tabSwitch.js b/html/js/tabSwitch.js new file mode 100644 index 0000000..31604d6 --- /dev/null +++ b/html/js/tabSwitch.js @@ -0,0 +1,22 @@ +/** + * File: tabSwitch.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +function openTab(evt, tabName) { + var i, tabcontent, tablinks + tabcontent = document.getElementsByClassName('tabcontent') + for (i = 0; i < tabcontent.length; i++) { + tabcontent[i].style.display = 'none' + } + tablinks = document.getElementsByClassName('tablinks') + for (i = 0; i < tablinks.length; i++) { + tablinks[i].className = tablinks[i].className.replace(' active', '') + } + document.getElementById(tabName).style.display = 'block' + evt.currentTarget.className += ' active' +} diff --git a/html/style.css b/html/style.css index 83d9fec..1a3b9bc 100644 --- a/html/style.css +++ b/html/style.css @@ -18,7 +18,7 @@ a { } table { - + width: 100%; border-collapse: collapse; } @@ -119,7 +119,7 @@ main { justify-content: center; padding: 1em; margin: 0; - height: 80%; + /* height: 80%; */ } .flex-container { @@ -132,9 +132,97 @@ main { text-align: center; } -input { +input, button, textarea { background-color: rgba(0,0,0,0.5); color: white; border: none; } +button:hover { + background-color: rgba(0,0,0,0.3); + +} +button { + padding: 14px 16px; + +} +/* -STYLES EDITOR */ + +#style-editor { + display: flex; + flex-direction: column; + width:100%; + +} + +#style-editor > table { + /* background-color: red; */ + width:100%; +} +#style-editor input, #style-editor textarea { + /* background-color: blue; */ + width:100%; +} + +#style-editor td{ + width: 33.33%; +} + +/* -TABS */ + +.tab { + overflow: hidden; + width: 100%; + display: flex; + flex-direction: row; + +} + +.tab-container{ + width: 100%; + display: flex; + flex-direction: column; +} + +.tab button { + background-color: transparent; + color:white; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + transition: 0.3s; + width:100%; + font-size: 1.5em; +} + +.tab button.active { + background-color: #2e2e2e; +} + +.tabcontent { + display: none; +} + +.tabcontent.active { + display: block; +} + + + +.foldable-title { + cursor: pointer; + font-weight: bold; + user-select: none; +} + +.foldable-symbol { + margin-right: 10px; +} + +.foldable-content { + display: none; + flex-direction: column; + margin-left: 20px; +} diff --git a/utils.py b/utils.py index d038c21..401ba4c 100644 --- a/utils.py +++ b/utils.py @@ -3,7 +3,7 @@ import numpy as np import torch from pathlib import Path import sys -from typing import List +from typing import List, Optional import signal from contextlib import suppress from queue import Queue, Empty @@ -14,6 +14,8 @@ import math import functools import socket import requests +import shutil +import uuid try: from .log import log @@ -64,6 +66,33 @@ class IPChecker: # region MISC Utilities +def backup_file( + fp: Path, + target: Optional[Path] = None, + backup_dir: str = ".bak", + suffix: Optional[str] = None, + prefix: Optional[str] = None, +): + if not fp.exists(): + raise FileNotFoundError(f"No file found at {fp}") + + backup_directory = target or fp.parent / backup_dir + backup_directory.mkdir(parents=True, exist_ok=True) + + stem = fp.stem + + if suffix or prefix: + new_stem = f"{prefix or ''}{stem}{suffix or ''}" + else: + new_stem = f"{stem}_{uuid.uuid4()}" + + backup_file_path = backup_directory / f"{new_stem}{fp.suffix}" + + # Perform the backup + shutil.copy(fp, backup_file_path) + log.debug(f"File backed up to {backup_file_path}") + + @functools.lru_cache(maxsize=1) def get_server_info(): from comfy.cli_args import args @@ -219,6 +248,8 @@ here = Path(__file__).parent.resolve() # - Construct the absolute path to the ComfyUI directory comfy_dir = here.parent.parent +styles_dir = comfy_dir / "styles" + # - Construct the path to the font file font_path = here / "font.ttf"