Merge pull request #14 from cdb-boop/main

v1.3 Performance and Quality of Life Improvements
This commit is contained in:
Hayden
2024-08-20 09:57:17 +08:00
committed by GitHub
14 changed files with 2311 additions and 698 deletions

View File

@@ -2,55 +2,66 @@
Download, browse and delete models in ComfyUI.
<div>
<img src="demo-tab-download.png" alt="Model Manager Demo Screenshot" width="45%"/>
<img src="demo-tab-models.png" alt="Model Manager Demo Screenshot" width="45%"/>
</div>
Designed to support desktop, mobile and multi-screen devices.
<img src="demo/beta-menu-model-manager-button-settings-group.png" alt="Model Manager Demo Screenshot" width="65%"/>
<img src="demo/tab-models.png" alt="Model Manager Demo Screenshot" width="65%"/>
## Features
### Node Graph
<img src="demo/tab-model-drag-add.gif" alt="Model Manager Demo Screenshot" width="65%"/>
- Drag a model thumbnail onto the graph to add a new node.
- Drag a model thumbnail onto an existing node to set the input field.
- If there are multiple valid possible fields, then the drag must be exact.
- Drag an embedding thumbnail onto a text area, or highlight any number of nodes, to append it onto the end of the text.
- Drag the preview image in a model's info view onto the graph to load the embedded workflow (if it exists).
<img src="demo/tab-model-preview-thumbnail-buttons-example.png" alt="Model Manager Demo Screenshot" width="65%"/>
- Press the "copy" button to copy a model to ComfyUI's clipboard or copy the embedding to the system clipboard. (Copying the embedding to the system clipboard requires a secure http connection.)
- Press the "add" button to add the model to the ComfyUI graph or append the embedding to one or more selected nodes.
- Press the "load workflow" button to try and load a workflow embedded in a model's preview image.
### Download Tab
<img src="demo/tab-download.png" alt="Model Manager Demo Screenshot" width="65%"/>
- View multiple models associated with a url.
- Select a download directory.
- Optionally also download a model preview image (a default image along side the model, from another url or locally uploaded).
- Optionally also download descriptions as a note (`.txt` file).
- Civitai and HuggingFace API token configurable in `server_settings.yaml`.
- Select a save directory and input a filename.
- Optionally set a model's preview image.
- Optionally edit and save descriptions as a .txt note. (Default behavior can be set in the settings tab.)
- Add Civitai and HuggingFace API tokens in `server_settings.yaml`.
### Models Tab
- Search bar in models tab.
- Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`.
- Search `/`subdirectories of model directories based on your file structure (for example, `/0/1.5/styles/clothing`).
- Add `/` at the start of the search bar to see auto-complete suggestions.
- Include models listed in ComfyUI's `extra_model_paths.yaml` or added in `ComfyUI/models/`.
- Sort for models (Date Created, Date Modified, Name).
<img src="demo/tab-models-dropdown.png" alt="Model Manager Demo Screenshot" width="65%"/>
- Search in real-time for models using the search bar.
- Use advance keyword search by typing `"multiple words in quotes"` or a minus sign before to `-exclude` a word or phrase.
- Add `/` at the start of a search to view a dropdown list of subdirectories (for example, `/0/1.5/styles/clothing`).
- Any directory paths in ComfyUI's `extra_model_paths.yaml` or directories added in `ComfyUI/models/` will automatically be detected.
- Sort models by "Date Created", "Date Modified", "Name" and "File Size".
### Model Info View
- View model metadata, including training tags and bucket resolutions.
- Read, edit and save notes in a `.txt` file beside the model.
- Change or remove a model's preview image (add a different one using a url or local upload).
- Rename, move or **permanently** remove models.
<img src="demo/tab-model-info-overview.png" alt="Model Manager Demo Screenshot" width="65%"/>
### ComfyUI Node Graph
- Button to copy a model to the ComfyUI clipboard or embedding to system clipboard. (Embedding copying requires secure http connection.)
- Button to add model to ComfyUI graph or embedding to selected nodes. (For small screens/low resolution.)
- Right, left, top and bottom toggleable sidebar modes.
- Drag a model onto the graph to add a new node.
- Drag a model onto an existing node to set the model field. (Must be exact on input if multiple inputs use model name text.)
- Drag an embedding onto a text area, or highlight any number of nodes, to add it to the end.
- Drag preview image in model info onto the graph to load embedded workflow.
- View file info and metadata.
- Rename, move or **permanently** remove a model and all of it's related files.
- Read, edit and save notes. (Saved as a `.txt` file beside the model).
- `Ctrl+s` or `⌘+S` to save a note when the textarea is in focus.
- Autosave can be enabled in settings. (Note: Once the model info view is closed, the undo history is lost.)
- Change or remove a model's preview image.
- View training tags and use the random tag generator to generate prompt ideas. (Inspired by the one in A1111.)
### Settings Tab
- Settings tab saved in `ui_settings.yaml`.
- Hide/Show 'add' and 'copy-to-clipboard' buttons.
- Text to always search.
- Show/Hide add embedding extension.
- Colors follow ComfyUI's current theme.
<img src="demo/tab-settings.png" alt="Model Manager Demo Screenshot" width="65%"/>
### Known Issues
- Pinch to Zoom can cause an invisible scrolling bug.
- Settings are saved to `ui_settings.yaml`.
- Most settings should update immediately, but a few may require a page reload to take effect.
- Press the "Fix Extensions" button to correct all image file extensions in the model directories. (Note: This may take a minute or so to complete.)

View File

@@ -18,6 +18,7 @@ import json
import requests
requests.packages.urllib3.disable_warnings()
import comfy.utils
import folder_paths
comfyui_model_uri = folder_paths.models_dir
@@ -33,12 +34,14 @@ no_preview_image = os.path.join(extension_uri, "no-preview.png")
ui_settings_uri = os.path.join(extension_uri, "ui_settings.yaml")
server_settings_uri = os.path.join(extension_uri, "server_settings.yaml")
fallback_model_extensions = set([".bin", ".ckpt", ".onnx", ".pt", ".pth", ".safetensors"]) # TODO: magic values
fallback_model_extensions = set([".bin", ".ckpt", ".gguf", ".onnx", ".pt", ".pth", ".safetensors"]) # TODO: magic values
jpeg_format_names = ["JPG", "JPEG", "JFIF"]
image_extensions = (
".png", # order matters
".webp",
".jpeg",
".jpg",
".jfif",
".gif",
".apng",
)
@@ -47,6 +50,7 @@ stable_diffusion_webui_civitai_helper_image_extensions = (
".preview.webp",
".preview.jpeg",
".preview.jpg",
".preview.jfif",
".preview.gif",
".preview.apng",
)
@@ -139,11 +143,9 @@ def search_path_to_system_path(model_path):
def get_safetensor_header(path):
try:
with open(path, "rb") as f:
length_of_header = struct.unpack("<Q", f.read(8))[0]
header_bytes = f.read(length_of_header)
header_json = json.loads(header_bytes)
return header_json
header_bytes = comfy.utils.safetensors_header(path)
header_json = json.loads(header_bytes)
return header_json if header_json is not None else {}
except:
return {}
@@ -174,18 +176,36 @@ def model_type_to_dir_name(model_type):
def ui_rules():
Rule = config_loader.Rule
return [
Rule("sidebar-default-height", 0.5, float, 0.0, 1.0),
Rule("sidebar-default-width", 0.5, float, 0.0, 1.0),
Rule("model-search-always-append", "", str),
Rule("model-default-browser-model-type", "checkpoints", str),
Rule("model-real-time-search", True, bool),
Rule("model-persistent-search", True, bool),
Rule("model-show-label-extensions", False, bool),
Rule("model-preview-thumbnail-type", "AUTO", str),
Rule("model-preview-fallback-search-safetensors-thumbnail", False, bool),
Rule("model-show-label-extensions", False, bool),
Rule("model-show-add-button", True, bool),
Rule("model-show-copy-button", True, bool),
Rule("model-show-load-workflow-button", True, bool),
Rule("model-info-button-on-left", False, bool),
Rule("model-add-embedding-extension", False, bool),
Rule("model-add-drag-strict-on-field", False, bool),
Rule("model-add-offset", 25, int),
Rule("download-save-description-as-text-file", False, bool),
Rule("model-info-autosave-notes", False, bool),
Rule("download-save-description-as-text-file", True, bool),
Rule("sidebar-control-always-compact", False, bool),
Rule("sidebar-default-width", 0.5, float, 0.0, 1.0),
Rule("sidebar-default-height", 0.5, float, 0.0, 1.0),
Rule("text-input-always-hide-search-button", False, bool),
Rule("text-input-always-hide-clear-button", False, bool),
Rule("tag-generator-sampler-method", "Frequency", str),
Rule("tag-generator-count", 10, int),
Rule("tag-generator-threshold", 2, int),
]
@@ -219,6 +239,11 @@ def get_def_headers(url=""):
return def_headers
@server.PromptServer.instance.routes.get("/model-manager/timestamp")
async def get_timestamp(request):
return web.json_response({ "timestamp": datetime.now().timestamp() })
@server.PromptServer.instance.routes.get("/model-manager/settings/load")
async def load_ui_settings(request):
rules = ui_rules()
@@ -272,11 +297,41 @@ def get_safetensors_image_bytes(path):
return base64.b64decode(image_data)
def get_image_info(image):
metadata = None
if len(image.info) > 0:
metadata = PngInfo()
for (key, value) in image.info.items():
value_str = str(PIL_cast_serializable(value)) # not sure if this is correct (sometimes includes exif)
metadata.add_text(key, value_str)
return metadata
def image_format_is_equal(f1, f2):
if not isinstance(f1, str) or not isinstance(f2, str):
return False
if f1[0] == ".": f1 = f1[1:]
if f2[0] == ".": f2 = f2[1:]
f1 = f1.upper()
f2 = f2.upper()
return f1 == f2 or (f1 in jpeg_format_names and f2 in jpeg_format_names)
def get_auto_thumbnail_format(original_format):
if original_format in ["JPEG", "WEBP", "JPG"]: # JFIF?
return original_format
return "JPEG" # default fallback
@server.PromptServer.instance.routes.get("/model-manager/preview/get")
async def get_model_preview(request):
uri = request.query.get("uri")
quality = 75
response_image_format = request.query.get("image-format", None)
if isinstance(response_image_format, str):
response_image_format = response_image_format.upper()
image_path = no_preview_image
image_type = "png"
file_name = os.path.split(no_preview_image)[1]
if uri != "no-preview":
sep = os.path.sep
@@ -285,12 +340,10 @@ async def get_model_preview(request):
head, extension = split_valid_ext(path, preview_extensions)
if os.path.exists(path):
image_path = path
image_type = extension.rsplit(".", 1)[1]
file_name = os.path.split(head)[1] + "." + image_type
file_name = os.path.split(head)[1] + extension
elif os.path.exists(head) and head.endswith(".safetensors"):
image_path = head
image_type = extension.rsplit(".", 1)[1]
file_name = os.path.splitext(os.path.split(head)[1])[0] + "." + image_type
file_name = os.path.splitext(os.path.split(head)[1])[0] + extension
w = request.query.get("width")
h = request.query.get("height")
@@ -314,6 +367,22 @@ async def get_model_preview(request):
else:
with open(image_path, "rb") as image:
image_data = image.read()
fp = io.BytesIO(image_data)
with Image.open(fp) as image:
image_format = image.format
if response_image_format is None:
response_image_format = image_format
elif response_image_format == "AUTO":
response_image_format = get_auto_thumbnail_format(image_format)
if not image_format_is_equal(response_image_format, image_format):
exif = image.getexif()
metadata = get_image_info(image)
if response_image_format in jpeg_format_names:
image = image.convert('RGB')
image_bytes = io.BytesIO()
image.save(image_bytes, format=response_image_format, exif=exif, pnginfo=metadata, quality=quality)
image_data = image_bytes.getvalue()
else:
if image_path.endswith(".safetensors"):
image_data = get_safetensors_image_bytes(image_path)
@@ -322,6 +391,12 @@ async def get_model_preview(request):
fp = image_path
with Image.open(fp) as image:
image_format = image.format
if response_image_format is None:
response_image_format = image_format
elif response_image_format == "AUTO":
response_image_format = get_auto_thumbnail_format(image_format)
w0, h0 = image.size
if w is None:
w = (h * w0) // h0
@@ -329,26 +404,41 @@ async def get_model_preview(request):
h = (w * h0) // w0
exif = image.getexif()
metadata = get_image_info(image)
metadata = None
if len(image.info) > 0:
metadata = PngInfo()
for (key, value) in image.info.items():
value_str = str(PIL_cast_serializable(value)) # not sure if this is correct (sometimes includes exif)
metadata.add_text(key, value_str)
ratio_original = w0 / h0
ratio_thumbnail = w / h
if abs(ratio_original - ratio_thumbnail) < 0.01:
crop_box = (0, 0, w0, h0)
elif ratio_original > ratio_thumbnail:
crop_width_fp = h0 * w / h
x0 = int((w0 - crop_width_fp) / 2)
crop_box = (x0, 0, x0 + int(crop_width_fp), h0)
else:
crop_height_fp = w0 * h / w
y0 = int((h0 - crop_height_fp) / 2)
crop_box = (0, y0, w0, y0 + int(crop_height_fp))
image = image.crop(crop_box)
image.thumbnail((w, h))
if w < w0 and h < h0:
resampling_method = Image.Resampling.BOX
else:
resampling_method = Image.Resampling.BICUBIC
image.thumbnail((w, h), resample=resampling_method)
if not image_format_is_equal(image_format, response_image_format) and response_image_format in jpeg_format_names:
image = image.convert('RGB')
image_bytes = io.BytesIO()
image.save(image_bytes, format=image.format, exif=exif, pnginfo=metadata)
image.save(image_bytes, format=response_image_format, exif=exif, pnginfo=metadata, quality=quality)
image_data = image_bytes.getvalue()
response_file_name = os.path.splitext(file_name)[0] + '.' + response_image_format.lower()
return web.Response(
headers={
"Content-Disposition": f"inline; filename={file_name}",
"Content-Disposition": f"inline; filename={response_file_name}",
},
body=image_data,
content_type="image/" + image_type,
content_type="image/" + response_image_format.lower(),
)
@@ -360,7 +450,7 @@ async def get_image_extensions(request):
def download_model_preview(formdata):
path = formdata.get("path", None)
if type(path) is not str:
raise ("Invalid path!")
raise ValueError("Invalid path!")
path, model_type = search_path_to_system_path(path)
model_type_extensions = folder_paths_get_supported_pt_extensions(model_type)
path_without_extension, _ = split_valid_ext(path, model_type_extensions)
@@ -401,20 +491,37 @@ def download_model_preview(formdata):
else:
content_type = image.content_type
if not content_type.startswith("image/"):
raise ("Invalid content type!")
raise RuntimeError("Invalid content type!")
image_extension = "." + content_type[len("image/"):]
if image_extension not in image_extensions:
raise ("Invalid extension!")
raise RuntimeError("Invalid extension!")
image_path = path_without_extension + image_extension
if not overwrite and os.path.isfile(image_path):
raise ("Image already exists!")
raise RuntimeError("Image already exists!")
file: io.IOBase = image.file
image_data = file.read()
with open(image_path, "wb") as f:
f.write(image_data)
print("Saved file: " + image_path)
delete_same_name_files(path_without_extension, preview_extensions, image_extension)
if overwrite:
delete_same_name_files(path_without_extension, preview_extensions, image_extension)
# detect (and try to fix) wrong file extension
image_format = None
with Image.open(image_path) as image:
image_format = image.format
image_dir_and_name, image_ext = os.path.splitext(image_path)
if not image_format_is_equal(image_format, image_ext):
corrected_image_path = image_dir_and_name + "." + image_format.lower()
if os.path.exists(corrected_image_path) and not overwrite:
print("WARNING: '" + image_path + "' has wrong extension!")
else:
os.rename(image_path, corrected_image_path)
print("Saved file: " + corrected_image_path)
image_path = corrected_image_path
return image_path # return in-case need corrected path
@server.PromptServer.instance.routes.post("/model-manager/preview/set")
@@ -450,6 +557,63 @@ async def delete_model_preview(request):
return web.json_response(result)
def correct_image_extensions(root_dir):
detected_image_count = 0
corrected_image_count = 0
for root, dirs, files in os.walk(root_dir):
for file_name in files:
file_path = root + os.path.sep + file_name
image_format = None
try:
with Image.open(file_path) as image:
image_format = image.format
except:
continue
image_path = file_path
image_dir_and_name, image_ext = os.path.splitext(image_path)
if not image_format_is_equal(image_format, image_ext):
detected_image_count += 1
corrected_image_path = image_dir_and_name + "." + image_format.lower()
if os.path.exists(corrected_image_path):
print("WARNING: '" + image_path + "' has wrong extension!")
else:
try:
os.rename(image_path, corrected_image_path)
except:
print("WARNING: Unable to rename '" + image_path + "'!")
continue
ext0 = os.path.splitext(image_path)[1]
ext1 = os.path.splitext(corrected_image_path)[1]
print(f"({ext0} -> {ext1}): {corrected_image_path}")
corrected_image_count += 1
return (detected_image_count, corrected_image_count)
@server.PromptServer.instance.routes.get("/model-manager/preview/correct-extensions")
async def correct_preview_extensions(request):
result = { "success": False }
detected = 0
corrected = 0
model_types = os.listdir(comfyui_model_uri)
model_types.remove("configs")
model_types.sort()
for model_type in model_types:
for base_path_index, model_base_path in enumerate(folder_paths_get_folder_paths(model_type)):
if not os.path.exists(model_base_path): # TODO: Bug in main code? ("ComfyUI\output\checkpoints", "ComfyUI\output\clip", "ComfyUI\models\t2i_adapter", "ComfyUI\output\vae")
continue
d, c = correct_image_extensions(model_base_path)
detected += d
corrected += c
result["success"] = True
result["detected"] = detected
result["corrected"] = corrected
return web.json_response(result)
@server.PromptServer.instance.routes.get("/model-manager/models/list")
async def get_model_list(request):
use_safetensor_thumbnail = (
@@ -740,8 +904,8 @@ async def get_model_info(request):
stats = pathlib.Path(abs_path).stat()
date_format = "%Y-%m-%d %H:%M:%S"
date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
info["Date Modified"] = date_modified
info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
#info["Date Modified"] = date_modified
#info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
abs_name , _ = split_valid_ext(abs_path, model_extensions)
@@ -759,8 +923,6 @@ async def get_model_info(request):
header = get_safetensor_header(abs_path)
metadata = header.get("__metadata__", None)
#json.dump(metadata, sys.stdout, indent=4)
#print()
if metadata is not None and info.get("Preview", None) is None:
thumbnail = metadata.get("modelspec.thumbnail")
@@ -775,41 +937,10 @@ async def get_model_info(request):
}
if metadata is not None:
train_end = metadata.get("modelspec.date", "").replace("T", " ")
train_start = metadata.get("ss_training_started_at", "")
if train_start != "":
try:
train_start = float(train_start)
train_start = datetime.fromtimestamp(train_start).strftime(date_format)
except:
train_start = ""
info["Date Trained"] = (
train_start +
(" ... " if train_start != "" and train_end != "" else "") +
train_end
)
info["Base Training Model"] = metadata.get("ss_sd_model_name", "")
info["Base Model"] = metadata.get("ss_base_model_version", "")
info["Architecture"] = metadata.get("modelspec.architecture", "")
info["Network Dimension"] = metadata.get("ss_network_dim", "") # features trained
info["Network Alpha"] = metadata.get("ss_network_alpha", "") # trained features applied
info["Model Sampling Type"] = metadata.get("modelspec.prediction_type", "")
clip_skip = metadata.get("ss_clip_skip", "")
if clip_skip == "None" or clip_skip == "1": # assume 1 means no clip skip
clip_skip = ""
info["Clip Skip"] = clip_skip
# it is unclear what these are
#info["Hash SHA256"] = metadata.get("modelspec.hash_sha256", "")
#info["SSHS Model Hash"] = metadata.get("sshs_model_hash", "")
#info["SSHS Legacy Hash"] = metadata.get("sshs_legacy_hash", "")
#info["New SD Model Hash"] = metadata.get("ss_new_sd_model_hash", "")
#info["Output Name"] = metadata.get("ss_output_name", "")
#info["Title"] = metadata.get("modelspec.title", "")
info["Author"] = metadata.get("modelspec.author", "")
info["License"] = metadata.get("modelspec.license", "")
info["Base Model Version"] = metadata.get("ss_base_model_version", "")
info["Network Dimension"] = metadata.get("ss_network_dim", "")
info["Network Alpha"] = metadata.get("ss_network_alpha", "")
if metadata is not None:
training_comment = metadata.get("ss_training_comment", "")
@@ -826,12 +957,18 @@ async def get_model_info(request):
if os.path.isfile(info_text_file):
with open(info_text_file, 'r', encoding="utf-8") as f:
notes = f.read()
info["Notes"] = notes
if metadata is not None:
img_buckets = metadata.get("ss_bucket_info", "{}")
img_buckets = metadata.get("ss_bucket_info", None)
datasets = metadata.get("ss_datasets", None)
if type(img_buckets) is str:
img_buckets = json.loads(img_buckets)
elif type(datasets) is str:
datasets = json.loads(datasets)
if isinstance(datasets, list):
datasets = datasets[0]
img_buckets = datasets.get("bucket_info", None)
resolutions = {}
if img_buckets is not None:
buckets = img_buckets.get("buckets", {})
@@ -844,6 +981,8 @@ async def get_model_info(request):
resolutions.sort(key=lambda x: x[1], reverse=True)
info["Bucket Resolutions"] = resolutions
tags = None
if metadata is not None:
dir_tags = metadata.get("ss_tag_frequency", "{}")
if type(dir_tags) is str:
dir_tags = json.loads(dir_tags)
@@ -853,10 +992,14 @@ async def get_model_info(request):
tags[tag] = tags.get(tag, 0) + count
tags = list(tags.items())
tags.sort(key=lambda x: x[1], reverse=True)
info["Tags"] = tags
result["success"] = True
result["info"] = info
if metadata is not None:
result["metadata"] = metadata
if tags is not None:
result["tags"] = tags
result["notes"] = notes
return web.json_response(result)
@@ -1039,6 +1182,8 @@ async def set_notes(request):
body = await request.json()
result = { "success": False }
dt_epoch = body.get("timestamp", None)
text = body.get("notes", None)
if type(text) is not str:
result["alert"] = "Invalid note!"
@@ -1052,15 +1197,23 @@ async def set_notes(request):
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
file_path_without_extension, _ = split_valid_ext(model_path, model_extensions)
filename = os.path.normpath(file_path_without_extension + model_info_extension)
if dt_epoch is not None and os.path.exists(filename) and os.path.getmtime(filename) > dt_epoch:
# discard late save
result["success"] = True
return web.json_response(result)
if text.isspace() or text == "":
if os.path.exists(filename):
os.remove(filename)
print("Deleted file: " + filename)
#print("Deleted file: " + filename) # autosave -> too verbose
else:
try:
with open(filename, "w", encoding="utf-8") as f:
f.write(text)
print("Saved file: " + filename)
if dt_epoch is not None:
os.utime(filename, (dt_epoch, dt_epoch))
#print("Saved file: " + filename) # autosave -> too verbose
except ValueError as e:
print(e, file=sys.stderr, flush=True)
result["alert"] = "Failed to save notes!\n\n" + str(e)

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
demo/tab-download.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
demo/tab-model-drag-add.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
demo/tab-models.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

BIN
demo/tab-settings.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -9,8 +9,6 @@
padding: 8px;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
z-index: 2000;
@@ -18,9 +16,11 @@
border-radius: 0;
box-shadow: none;
justify-content: unset;
max-height: unset;
max-width: unset;
max-height: 100vh;
max-width: 100vw;
transform: none;
/*disable double-tap zoom on model manager*/
touch-action: manipulation;
}
.model-manager .comfy-modal-content {
@@ -28,32 +28,107 @@
gap: 16px;
}
.model-manager.sidebar-left {
width: 50%;
left: 0%;
.model-manager .no-highlight {
user-select: none;
-moz-user-select: none;
-webkit-text-select: none;
-webkit-user-select: none;
}
.model-manager.sidebar-top {
height: 50%;
top: 0%;
.model-manager label:has(> *){
pointer-events: none;
}
.model-manager.sidebar-bottom {
height: 50%;
top: 50%;
.model-manager label > * {
pointer-events: auto;
}
.model-manager.sidebar-right {
width: 50%;
left: 50%;
/* sidebar */
.model-manager {
--model-manager-sidebar-width-left: 50vw;
--model-manager-sidebar-width-right: 50vw;
--model-manager-sidebar-height-top: 50vh;
--model-manager-sidebar-height-bottom: 50vh;
--model-manager-left: 0;
--model-manager-right: 0;
--model-manager-top: 0;
--model-manager-bottom: 0;
left: var(--model-manager-left);
top: var(--model-manager-right);
right: var(--model-manager-top);
bottom: var(--model-manager-bottom);
}
.model-manager .sidebar-buttons .sidebar-button-active {
.model-manager.cursor-drag-left,
.model-manager.cursor-drag-right {
cursor: ew-resize;
}
.model-manager.cursor-drag-top,
.model-manager.cursor-drag-bottom {
cursor: ns-resize;
}
.model-manager.cursor-drag-top.cursor-drag-left,
.model-manager.cursor-drag-bottom.cursor-drag-right {
cursor: nwse-resize;
}
.model-manager.cursor-drag-top.cursor-drag-right,
.model-manager.cursor-drag-bottom.cursor-drag-left {
cursor: nesw-resize;
}
/* sidebar buttons */
.model-manager .sidebar-buttons {
overflow: hidden;
color: var(--input-text);
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
}
.model-manager .sidebar-buttons .radio-button-group-active {
border-color: var(--fg-color);
color: var(--fg-color);
overflow: hidden;
}
.model-manager[data-sidebar-state="left"] {
width: var(--model-manager-sidebar-width-left);
max-width: 95vw;
min-width: 22vw;
right: auto;
border-right: solid var(--border-color) 2px;
}
.model-manager[data-sidebar-state="top"] {
height: var(--model-manager-sidebar-height-top);
max-height: 95vh;
min-height: 22vh;
bottom: auto;
border-bottom: solid var(--border-color) 2px;
}
.model-manager[data-sidebar-state="bottom"] {
height: var(--model-manager-sidebar-height-bottom);
max-height: 95vh;
min-height: 22vh;
top: auto;
border-top: solid var(--border-color) 2px;
}
.model-manager[data-sidebar-state="right"] {
width: var(--model-manager-sidebar-width-right);
max-width: 95vw;
min-width: 22vw;
left: auto;
border-left: solid var(--border-color) 2px;
}
/* common */
.model-manager h1 {
min-width: 0;
@@ -72,12 +147,16 @@
width: 100%;
}
.model-manager button,
.model-manager button {
margin: 0;
border: 2px solid var(--border-color);
}
.model-manager button:not(.icon-button),
.model-manager select,
.model-manager input {
padding: 4px 8px;
margin: 0;
border: 2px solid var(--border-color);
}
.model-manager button:disabled,
@@ -134,7 +213,7 @@
.model-manager .tab-header {
display: flex;
padding: 8px 0;
padding: 8px 0px;
flex-direction: column;
background-color: var(--bg-color);
}
@@ -144,12 +223,12 @@
min-width: 0;
}
.model-manager .button-success {
.model-manager .comfy-button-success {
color: green;
border-color: green;
}
.model-manager .button-failure {
.model-manager .comfy-button-failure {
color: darkred;
border-color: darkred;
}
@@ -160,49 +239,39 @@
user-select: none;
}
/* sidebar buttons */
.model-manager .sidebar-buttons {
overflow: hidden;
color: var(--input-text);
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
}
/* main content */
.model-manager .model-manager-panel {
color: var(--fg-color);
}
.model-manager .model-manager-tabs {
.model-manager .model-tab-group {
display: flex;
gap: 4px;
height: 40px;
}
.model-manager .model-manager-tabs .head-item {
.model-manager .model-tab-group .tab-button {
background-color: var(--comfy-menu-bg);
border: 2px solid var(--border-color);
border-bottom: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-radius: 8px 8px 0px 0px;
cursor: pointer;
padding: 8px 12px;
margin-bottom: 0px;
z-index: 1;
}
.model-manager .model-manager-tabs .head-item.active {
.model-manager .model-tab-group .tab-button.active {
background-color: var(--bg-color);
cursor: default;
position: relative;
z-index: 1;
pointer-events: none;
}
.model-manager .model-manager-body {
background-color: var(--bg-color);
border: 2px solid var(--border-color);
padding: 16px 0px;
}
.model-manager .model-manager-panel {
@@ -215,28 +284,28 @@
.model-manager .model-manager-body {
flex: 1;
overflow: hidden;
padding: 8px 0px 8px 16px;
}
.model-manager .model-manager-body > div {
.model-manager .model-manager-body .tab-contents {
position: relative;
height: 100%;
width: auto;
padding: 0 16px;
overflow-x: auto;
}
/* model info view */
.model-manager .model-info-view {
background-color: var(--bg-color);
display: flex;
flex-direction: column;
height: 100%;
overflow-wrap: break-word;
overflow-y: auto;
padding: 20px;
position: relative;
width: auto;
overflow-x: auto;
overflow-y: hidden;
}
.model-manager .model-manager-body .tab-content {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding-right: 16px;
}
/* model info view */
.model-manager .model-info-container {
background-color: var(--bg-color);
border-radius: 16px;
@@ -244,12 +313,37 @@
width: auto;
}
.model-manager .model-metadata {
table-layout: fixed;
text-align: left;
width: 100%;
}
.model-manager .model-metadata-key {
overflow-wrap: break-word;
width: 20%;
}
.model-manager .model-metadata-value {
overflow-wrap: anywhere;
width: 80%;
}
.model-manager table {
border-collapse: collapse;
}
.model-manager th {
border: 1px solid;
padding: 4px 8px;
}
/* download tab */
.model-manager .download-model-infos {
display: flex;
flex-direction: column;
padding: 16px 0;
padding: 0;
row-gap: 10px;
}
@@ -283,6 +377,10 @@
row-gap: 16px;
}
.model-manager .download-button {
max-width: fit-content;
}
/* models tab */
.model-manager [data-name="Models"] .row {
position: sticky;
@@ -293,8 +391,8 @@
/* preview image */
.model-manager .item {
position: relative;
width: 230px;
height: 345px;
width: 240px;
height: 360px;
text-align: center;
overflow: hidden;
border-radius: 8px;
@@ -304,6 +402,18 @@
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.model-manager .model-info-container .item {
width: auto;
height: auto;
}
.model-manager .model-info-container .item img {
height: auto;
width: auto;
max-width: 100%;
max-height: 50vh;
}
.model-manager .model-preview-button-left,
@@ -340,7 +450,7 @@
}
.model-manager .comfy-grid .model-label {
background-color: #000a;
background-color: rgb(from var(--content-hover-bg) r g b / 0.5);
width: 100%;
height: 2.2rem;
position: absolute;
@@ -437,7 +547,6 @@
}
.model-manager .model-preview-select-radio-inputs > div {
height: 40px;
padding: 16px 0 8px 0;
}
@@ -475,7 +584,23 @@
float: right;
}
.model-manager .model-manager-head .topbar-right select {
position: relative;
top: 0;
bottom: 0;
font-size: 24px;
-o-appearance: none;
-ms-appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* search dropdown */
.model-manager .input-dropdown-container {
position: relative;
}
.model-manager .search-models {
display: flex;
flex: 1;
@@ -501,46 +626,54 @@
min-height: 40px;
}
.model-manager .search-dropdown {
.model-manager .search-directory-dropdown {
background-color: var(--bg-color);
border: 2px var(--border-color) solid;
border-radius: 10px;
color: var(--fg-color);
max-height: 30vh;
max-height: 40vh;
overflow: auto;
position: absolute;
z-index: 1;
}
.model-manager .search-dropdown:empty {
@media (pointer:none), (pointer:coarse) {
.model-manager .search-directory-dropdown {
max-height: 17.5vh;
}
}
.model-manager .search-directory-dropdown:empty {
display: none;
}
.model-manager .search-dropdown > p {
.model-manager .search-directory-dropdown > p {
margin: 0;
padding: 0.85em 20px;
min-width: 0;
}
.model-manager .search-dropdown > p {
.model-manager .search-directory-dropdown > p {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.model-manager .search-dropdown > p::-webkit-scrollbar {
.model-manager .search-directory-dropdown > p::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.model-manager .search-dropdown > p.search-dropdown-key-selected,
.model-manager .search-dropdown > p.search-dropdown-mouse-selected {
.model-manager .search-directory-dropdown > p.search-directory-dropdown-key-selected,
.model-manager .search-directory-dropdown > p.search-directory-dropdown-mouse-selected {
background-color: var(--border-color);
}
.model-manager .search-dropdown > p.search-dropdown-key-selected {
.model-manager .search-directory-dropdown > p.search-directory-dropdown-key-selected {
border-left: 1mm solid var(--input-text);
}
/* model manager settings */
.model-manager .model-manager-settings > div,
.model-manager .model-manager-settings > label {
.model-manager .model-manager-settings > label,
.model-manager .tag-generator-settings > label,
.model-manager .tag-generator-settings > div {
display: flex;
flex-direction: row;
align-items: center;
@@ -550,10 +683,12 @@
.model-manager .model-manager-settings button {
height: 40px;
width: 120px;
min-width: 120px;
justify-content: center;
}
.model-manager .model-manager-settings input[type="number"] {
.model-manager .model-manager-settings input[type="number"],
.model-manager .tag-generator-settings input[type="number"]{
width: 50px;
}

View File

File diff suppressed because it is too large Load Diff