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"