feat: 🎨 Add an editor for the styles loader
For simplicity I implemented it an endpoint for now. Closes #84
This commit is contained in:
+25
-1
@@ -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 = """
|
||||
<div class="flex-container menu">
|
||||
<a href="/mtb/manage">manage</a>
|
||||
<a href="/mtb/debug">debug</a>
|
||||
<a href="/mtb/status">status</a>
|
||||
</div>
|
||||
|
||||
+157
-2
@@ -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 = """
|
||||
<div id="style-editor">
|
||||
<h1>Style Editor</h1>
|
||||
|
||||
"""
|
||||
for current, styles in style_files.items():
|
||||
current_out = f"<h3>{current}</h3>"
|
||||
table_rows = []
|
||||
for index, style in enumerate(styles):
|
||||
table_rows += (
|
||||
(["<tr>"] + [f"<th>{cell}</th>" for cell in style] + ["</tr>"])
|
||||
if index == 0
|
||||
else (
|
||||
["<tr>"]
|
||||
+ [
|
||||
f"<td><input type='text' value='{cell}'></td>"
|
||||
if i == 0
|
||||
else f"<td><textarea name='Text1' cols='40' rows='5'>{cell}</textarea></td>"
|
||||
for i, cell in enumerate(style)
|
||||
]
|
||||
+ ["</tr>"]
|
||||
)
|
||||
)
|
||||
current_out += (
|
||||
f"<table data-id='{current}' data-filename='{current}'>"
|
||||
+ "".join(table_rows)
|
||||
+ "</table>"
|
||||
)
|
||||
current_out += f"<button data-id='{current}' onclick='saveTableData(this.getAttribute(\"data-id\"))'>Save {current}</button>"
|
||||
|
||||
html_out += add_foldable_region(current, current_out)
|
||||
|
||||
html_out += "</div>"
|
||||
html_out += """<script src='/mtb-assets/js/saveTableData.js'></script>"""
|
||||
|
||||
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"<button class='tablinks {active_class}' onclick=\"openTab(event, '{tab_name}')\">{tab_name}</button>"
|
||||
)
|
||||
tab_contents.append(
|
||||
f"<div id='{tab_name}' class='tabcontent {active_class}'>{content}</div>"
|
||||
)
|
||||
|
||||
headers_str = "\n".join(tab_headers)
|
||||
contents_str = "\n".join(tab_contents)
|
||||
|
||||
return f"""
|
||||
<div class='tab-container'>
|
||||
<div class='tab'>
|
||||
{headers_str}
|
||||
</div>
|
||||
{contents_str}
|
||||
</div>
|
||||
<script src='/mtb-assets/js/tabSwitch.js'></script>
|
||||
"""
|
||||
|
||||
|
||||
def add_foldable_region(title, content):
|
||||
symbol_id = f"{title}-symbol"
|
||||
return f"""
|
||||
<div class='foldable'>
|
||||
<div class='foldable-title' onclick="toggleFoldable('{title}', '{symbol_id}')">
|
||||
<span id='{symbol_id}' class='foldable-symbol'>▷</span>
|
||||
{title}
|
||||
</div>
|
||||
<div id='{title}' class='foldable-content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
<script src='/mtb-assets/js/foldable.js'></script>
|
||||
"""
|
||||
|
||||
|
||||
def add_split_pane(left_content, right_content, vertical=True):
|
||||
orientation = "vertical" if vertical else "horizontal"
|
||||
return f"""
|
||||
<div class="split-pane {orientation}">
|
||||
<div id="leftPane">
|
||||
{left_content}
|
||||
</div>
|
||||
<div id="resizer"></div>
|
||||
<div id="rightPane">
|
||||
{right_content}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
initSplitPane({str(vertical).lower()});
|
||||
</script>
|
||||
<script src='/mtb-assets/js/splitPane.js'></script>
|
||||
"""
|
||||
|
||||
|
||||
def add_dropdown(title, options):
|
||||
option_str = "\n".join([f"<option value='{opt}'>{opt}</option>" for opt in options])
|
||||
return f"""
|
||||
<select>
|
||||
<option disabled selected>{title}</option>
|
||||
{option_str}
|
||||
</select>
|
||||
"""
|
||||
|
||||
|
||||
def render_table(table_dict, sort=True, title=None):
|
||||
table_dict = sorted(
|
||||
table_dict.items(), key=lambda item: item[0]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
+91
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user