random commit

This commit is contained in:
FizzleDorf 2023-03-30 21:55:12 -04:00
parent 48efadeccf
commit d7944246df
18 changed files with 882 additions and 95 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ temp/
custom_nodes/ custom_nodes/
!custom_nodes/example_node.py.example !custom_nodes/example_node.py.example
extra_model_paths.yaml extra_model_paths.yaml
/.vs

View File

@ -30,6 +30,11 @@ This ui will let you design and execute advanced stable diffusion pipelines usin
Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/) Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/)
## Shortcuts
- **Ctrl + A** select all nodes
- **Ctrl + M** mute/unmute selected nodes
- **Delete** or **Backspace** delete selected nodes
# Installing # Installing
## Windows ## Windows
@ -64,7 +69,7 @@ AMD users can install rocm and pytorch with pip if you don't have it already ins
Nvidia users should install torch and xformers using this command: Nvidia users should install torch and xformers using this command:
```pip install torch==1.13.1 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu117 xformers``` ```pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118 xformers```
#### Troubleshooting #### Troubleshooting

View File

@ -15,6 +15,8 @@ total_vram_available_mb = -1
import sys import sys
import psutil import psutil
forced_cpu = "--cpu" in sys.argv
set_vram_to = NORMAL_VRAM set_vram_to = NORMAL_VRAM
try: try:
@ -22,7 +24,7 @@ try:
total_vram = torch.cuda.mem_get_info(torch.cuda.current_device())[1] / (1024 * 1024) total_vram = torch.cuda.mem_get_info(torch.cuda.current_device())[1] / (1024 * 1024)
total_ram = psutil.virtual_memory().total / (1024 * 1024) total_ram = psutil.virtual_memory().total / (1024 * 1024)
forced_normal_vram = "--normalvram" in sys.argv forced_normal_vram = "--normalvram" in sys.argv
if not forced_normal_vram: if not forced_normal_vram and not forced_cpu:
if total_vram <= 4096: if total_vram <= 4096:
print("Trying to enable lowvram mode because your GPU seems to have 4GB or less. If you don't want this use: --normalvram") print("Trying to enable lowvram mode because your GPU seems to have 4GB or less. If you don't want this use: --normalvram")
set_vram_to = LOW_VRAM set_vram_to = LOW_VRAM
@ -83,7 +85,7 @@ try:
except: except:
pass pass
if "--cpu" in sys.argv: if forced_cpu:
vram_state = CPU vram_state = CPU
print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM", "MPS"][vram_state]) print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM", "MPS"][vram_state])

View File

@ -242,7 +242,10 @@ def ddim_scheduler(model, steps):
sigs = [] sigs = []
ddim_timesteps = make_ddim_timesteps(ddim_discr_method="uniform", num_ddim_timesteps=steps, num_ddpm_timesteps=model.inner_model.inner_model.num_timesteps, verbose=False) ddim_timesteps = make_ddim_timesteps(ddim_discr_method="uniform", num_ddim_timesteps=steps, num_ddpm_timesteps=model.inner_model.inner_model.num_timesteps, verbose=False)
for x in range(len(ddim_timesteps) - 1, -1, -1): for x in range(len(ddim_timesteps) - 1, -1, -1):
sigs.append(model.t_to_sigma(torch.tensor(ddim_timesteps[x]))) ts = ddim_timesteps[x]
if ts > 999:
ts = 999
sigs.append(model.t_to_sigma(torch.tensor(ts)))
sigs += [0.0] sigs += [0.0]
return torch.FloatTensor(sigs) return torch.FloatTensor(sigs)
@ -373,7 +376,7 @@ class KSampler:
def set_steps(self, steps, denoise=None): def set_steps(self, steps, denoise=None):
self.steps = steps self.steps = steps
if denoise is None: if denoise is None or denoise > 0.9999:
self.sigmas = self._calculate_sigmas(steps) self.sigmas = self._calculate_sigmas(steps)
else: else:
new_steps = int(steps/denoise) new_steps = int(steps/denoise)

View File

@ -439,9 +439,14 @@ class VAE:
model_management.unload_model() model_management.unload_model()
self.first_stage_model = self.first_stage_model.to(self.device) self.first_stage_model = self.first_stage_model.to(self.device)
try: try:
samples = samples_in.to(self.device) free_memory = model_management.get_free_memory(self.device)
pixel_samples = self.first_stage_model.decode(1. / self.scale_factor * samples) batch_number = int((free_memory * 0.7) / (2562 * samples_in.shape[2] * samples_in.shape[3] * 64))
pixel_samples = torch.clamp((pixel_samples + 1.0) / 2.0, min=0.0, max=1.0) batch_number = max(1, batch_number)
pixel_samples = torch.empty((samples_in.shape[0], 3, round(samples_in.shape[2] * 8), round(samples_in.shape[3] * 8)), device="cpu")
for x in range(0, samples_in.shape[0], batch_number):
samples = samples_in[x:x+batch_number].to(self.device)
pixel_samples[x:x+batch_number] = torch.clamp((self.first_stage_model.decode(1. / self.scale_factor * samples) + 1.0) / 2.0, min=0.0, max=1.0).cpu()
except model_management.OOM_EXCEPTION as e: except model_management.OOM_EXCEPTION as e:
print("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") print("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.")
pixel_samples = self.decode_tiled_(samples_in) pixel_samples = self.decode_tiled_(samples_in)

View File

@ -10,7 +10,7 @@ import gc
import torch import torch
import nodes import nodes
def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}): def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}):
valid_inputs = class_def.INPUT_TYPES() valid_inputs = class_def.INPUT_TYPES()
input_data_all = {} input_data_all = {}
for x in inputs: for x in inputs:
@ -18,6 +18,8 @@ def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}):
if isinstance(input_data, list): if isinstance(input_data, list):
input_unique_id = input_data[0] input_unique_id = input_data[0]
output_index = input_data[1] output_index = input_data[1]
if input_unique_id not in outputs:
return None
obj = outputs[input_unique_id][output_index] obj = outputs[input_unique_id][output_index]
input_data_all[x] = obj input_data_all[x] = obj
else: else:
@ -32,6 +34,8 @@ def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}):
if h[x] == "EXTRA_PNGINFO": if h[x] == "EXTRA_PNGINFO":
if "extra_pnginfo" in extra_data: if "extra_pnginfo" in extra_data:
input_data_all[x] = extra_data['extra_pnginfo'] input_data_all[x] = extra_data['extra_pnginfo']
if h[x] == "UNIQUE_ID":
input_data_all[x] = unique_id
return input_data_all return input_data_all
def recursive_execute(server, prompt, outputs, current_item, extra_data={}): def recursive_execute(server, prompt, outputs, current_item, extra_data={}):
@ -53,7 +57,7 @@ def recursive_execute(server, prompt, outputs, current_item, extra_data={}):
if input_unique_id not in outputs: if input_unique_id not in outputs:
executed += recursive_execute(server, prompt, outputs, input_unique_id, extra_data) executed += recursive_execute(server, prompt, outputs, input_unique_id, extra_data)
input_data_all = get_input_data(inputs, class_def, outputs, prompt, extra_data) input_data_all = get_input_data(inputs, class_def, unique_id, outputs, prompt, extra_data)
if server.client_id is not None: if server.client_id is not None:
server.last_node_id = unique_id server.last_node_id = unique_id
server.send_sync("executing", { "node": unique_id }, server.client_id) server.send_sync("executing", { "node": unique_id }, server.client_id)
@ -61,8 +65,11 @@ def recursive_execute(server, prompt, outputs, current_item, extra_data={}):
nodes.before_node_execution() nodes.before_node_execution()
outputs[unique_id] = getattr(obj, obj.FUNCTION)(**input_data_all) outputs[unique_id] = getattr(obj, obj.FUNCTION)(**input_data_all)
if "ui" in outputs[unique_id] and server.client_id is not None: if "ui" in outputs[unique_id]:
server.send_sync("executed", { "node": unique_id, "output": outputs[unique_id]["ui"] }, server.client_id) if server.client_id is not None:
server.send_sync("executed", { "node": unique_id, "output": outputs[unique_id]["ui"] }, server.client_id)
if "result" in outputs[unique_id]:
outputs[unique_id] = outputs[unique_id]["result"]
return executed + [unique_id] return executed + [unique_id]
def recursive_will_execute(prompt, outputs, current_item): def recursive_will_execute(prompt, outputs, current_item):
@ -94,9 +101,10 @@ def recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item
if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]:
is_changed_old = old_prompt[unique_id]['is_changed'] is_changed_old = old_prompt[unique_id]['is_changed']
if 'is_changed' not in prompt[unique_id]: if 'is_changed' not in prompt[unique_id]:
input_data_all = get_input_data(inputs, class_def) input_data_all = get_input_data(inputs, class_def, unique_id, outputs)
is_changed = class_def.IS_CHANGED(**input_data_all) if input_data_all is not None:
prompt[unique_id]['is_changed'] = is_changed is_changed = class_def.IS_CHANGED(**input_data_all)
prompt[unique_id]['is_changed'] = is_changed
else: else:
is_changed = prompt[unique_id]['is_changed'] is_changed = prompt[unique_id]['is_changed']
@ -278,7 +286,7 @@ def validate_prompt(prompt):
errors += [(o, reason)] errors += [(o, reason)]
if len(good_outputs) == 0: if len(good_outputs) == 0:
errors_list = "\n".join(map(lambda a: "{}".format(a[1]), errors)) errors_list = "\n".join(set(map(lambda a: "{}".format(a[1]), errors)))
return (False, "Prompt has no properly connected outputs\n {}".format(errors_list)) return (False, "Prompt has no properly connected outputs\n {}".format(errors_list))
return (True, "") return (True, "")

23
main.py
View File

@ -12,12 +12,13 @@ if os.name == "nt":
if __name__ == "__main__": if __name__ == "__main__":
if '--help' in sys.argv: if '--help' in sys.argv:
print("Valid Command line Arguments:") print("Valid Command line Arguments:")
print("\t--listen\t\t\tListen on 0.0.0.0 so the UI can be accessed from other computers.") print("\t--listen [ip]\t\t\tListen on ip or 0.0.0.0 if none given so the UI can be accessed from other computers.")
print("\t--port 8188\t\t\tSet the listen port.") print("\t--port 8188\t\t\tSet the listen port.")
print("\t--dont-upcast-attention\t\tDisable upcasting of attention \n\t\t\t\t\tcan boost speed but increase the chances of black images.\n") print("\t--dont-upcast-attention\t\tDisable upcasting of attention \n\t\t\t\t\tcan boost speed but increase the chances of black images.\n")
print("\t--use-split-cross-attention\tUse the split cross attention optimization instead of the sub-quadratic one.\n\t\t\t\t\tIgnored when xformers is used.") print("\t--use-split-cross-attention\tUse the split cross attention optimization instead of the sub-quadratic one.\n\t\t\t\t\tIgnored when xformers is used.")
print("\t--use-pytorch-cross-attention\tUse the new pytorch 2.0 cross attention function.") print("\t--use-pytorch-cross-attention\tUse the new pytorch 2.0 cross attention function.")
print("\t--disable-xformers\t\tdisables xformers") print("\t--disable-xformers\t\tdisables xformers")
print("\t--cuda-device 1\t\tSet the id of the cuda device this instance will use.")
print() print()
print("\t--highvram\t\t\tBy default models will be unloaded to CPU memory after being used.\n\t\t\t\t\tThis option keeps them in GPU memory.\n") print("\t--highvram\t\t\tBy default models will be unloaded to CPU memory after being used.\n\t\t\t\t\tThis option keeps them in GPU memory.\n")
print("\t--normalvram\t\t\tUsed to force normal vram use if lowvram gets automatically enabled.") print("\t--normalvram\t\t\tUsed to force normal vram use if lowvram gets automatically enabled.")
@ -31,6 +32,14 @@ if __name__ == "__main__":
print("disabling upcasting of attention") print("disabling upcasting of attention")
os.environ['ATTN_PRECISION'] = "fp16" os.environ['ATTN_PRECISION'] = "fp16"
try:
index = sys.argv.index('--cuda-device')
device = sys.argv[index + 1]
os.environ['CUDA_VISIBLE_DEVICES'] = device
print("Set cuda device to:", device)
except:
pass
import execution import execution
import server import server
import folder_paths import folder_paths
@ -92,11 +101,19 @@ if __name__ == "__main__":
hijack_progress(server) hijack_progress(server)
threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start() threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start()
if '--listen' in sys.argv: try:
address = '0.0.0.0' address = '0.0.0.0'
else: p_index = sys.argv.index('--listen')
try:
ip = sys.argv[p_index + 1]
if ip[:2] != '--':
address = ip
except:
pass
except:
address = '127.0.0.1' address = '127.0.0.1'
dont_print = False dont_print = False
if '--dont-print-server' in sys.argv: if '--dont-print-server' in sys.argv:
dont_print = True dont_print = True

View File

@ -1052,4 +1052,4 @@ def load_custom_nodes():
load_custom_nodes() load_custom_nodes()
load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py")) load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py"))

View File

@ -47,7 +47,7 @@
" !git pull\n", " !git pull\n",
"\n", "\n",
"!echo -= Install dependencies =-\n", "!echo -= Install dependencies =-\n",
"!pip -q install xformers -r requirements.txt" "!pip install xformers==0.0.16 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu117"
] ]
}, },
{ {

View File

@ -3,7 +3,7 @@ torchdiffeq
torchsde torchsde
einops einops
open-clip-torch open-clip-torch
transformers transformers>=4.25.1
safetensors safetensors
pytorch_lightning pytorch_lightning
aiohttp aiohttp

View File

@ -152,6 +152,7 @@ class PromptServer():
info = {} info = {}
info['input'] = obj_class.INPUT_TYPES() info['input'] = obj_class.INPUT_TYPES()
info['output'] = obj_class.RETURN_TYPES info['output'] = obj_class.RETURN_TYPES
info['output_name'] = obj_class.RETURN_NAMES if hasattr(obj_class, 'RETURN_NAMES') else info['output']
info['name'] = x #TODO info['name'] = x #TODO
info['description'] = '' info['description'] = ''
info['category'] = 'sd' info['category'] = 'sd'

View File

@ -0,0 +1,351 @@
import { app } from "/scripts/app.js";
import { $el } from "/scripts/ui.js";
import { api } from "/scripts/api.js";
// Manage color palettes
const colorPalettes = {
"palette_1": {
"id": "palette_1",
"name": "Palette 1",
"colors": {
"node_slot": {
"CLIP": "#FFD500", // bright yellow
"CLIP_VISION": "#A8DADC", // light blue-gray
"CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange
"CONDITIONING": "#FFA931", // vibrant orange-yellow
"CONTROL_NET": "#6EE7B7", // soft mint green
"IMAGE": "#64B5F6", // bright sky blue
"LATENT": "#FF9CF9", // light pink-purple
"MASK": "#81C784", // muted green
"MODEL": "#B39DDB", // light lavender-purple
"STYLE_MODEL": "#C2FFAE", // light green-yellow
"VAE": "#FF6E6E", // bright red
}
}
},
"palette_2": {
"id": "palette_2",
"name": "Palette 2",
"colors": {
"node_slot": {
"CLIP": "#556B2F", // Dark Olive Green
"CLIP_VISION": "#4B0082", // Indigo
"CLIP_VISION_OUTPUT": "#006400", // Green
"CONDITIONING": "#FF1493", // Deep Pink
"CONTROL_NET": "#8B4513", // Saddle Brown
"IMAGE": "#8B0000", // Dark Red
"LATENT": "#00008B", // Dark Blue
"MASK": "#2F4F4F", // Dark Slate Grey
"MODEL": "#FF8C00", // Dark Orange
"STYLE_MODEL": "#004A4A", // Sherpa Blue
"UPSCALE_MODEL": "#4A004A", // Tyrian Purple
"VAE": "#4F394F", // Loulou
}
}
}
};
const id = "Comfy.ColorPalette";
const idCustomColorPalettes = "Comfy.CustomColorPalettes";
const defaultColorPaletteId = "palette_1";
const els = {}
// const ctxMenu = LiteGraph.ContextMenu;
app.registerExtension({
name: id,
init() {
const sortObjectKeys = (unordered) => {
return Object.keys(unordered).sort().reduce((obj, key) => {
obj[key] = unordered[key];
return obj;
}, {});
};
const getSlotTypes = async () => {
var types = [];
const defs = await api.getNodeDefs();
for (const nodeId in defs) {
const nodeData = defs[nodeId];
var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined){
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
}
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
if (!Array.isArray(type)) {
types.push(type);
}
}
for (const o in nodeData["output"]) {
const output = nodeData["output"][o];
types.push(output);
}
}
return types;
};
const completeColorPalette = async (colorPalette) => {
var types = await getSlotTypes();
for (const type of types) {
if (!colorPalette.colors.node_slot[type]) {
colorPalette.colors.node_slot[type] = "";
}
}
colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot);
return colorPalette;
};
const getColorPaletteTemplate = async () => {
let colorPalette = {
"id": "my_color_palette_unique_id",
"name": "My Color Palette",
"colors": {
"node_slot": {
}
}
};
return completeColorPalette(colorPalette);
};
const getCustomColorPalettes = () => {
return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
};
const setCustomColorPalettes = (customColorPalettes) => {
return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes);
};
const addCustomColorPalette = async (colorPalette) => {
if (typeof(colorPalette) !== "object") {
app.ui.dialog.show("Invalid color palette");
return;
}
if (!colorPalette.id) {
app.ui.dialog.show("Color palette missing id");
return;
}
if (!colorPalette.name) {
app.ui.dialog.show("Color palette missing name");
return;
}
if (!colorPalette.colors) {
app.ui.dialog.show("Color palette missing colors");
return;
}
if (colorPalette.colors.node_slot && typeof(colorPalette.colors.node_slot) !== "object") {
app.ui.dialog.show("Invalid color palette colors.node_slot");
return;
}
let customColorPalettes = getCustomColorPalettes();
customColorPalettes[colorPalette.id] = colorPalette;
setCustomColorPalettes(customColorPalettes);
for (const option of els.select.childNodes) {
if (option.value === "custom_" + colorPalette.id) {
els.select.removeChild(option);
}
}
els.select.append($el("option", { textContent: colorPalette.name + " (custom)", value: "custom_" + colorPalette.id, selected: true }));
setColorPalette("custom_" + colorPalette.id);
await loadColorPalette(colorPalette);
};
const deleteCustomColorPalette = async (colorPaletteId) => {
let customColorPalettes = getCustomColorPalettes();
delete customColorPalettes[colorPaletteId];
setCustomColorPalettes(customColorPalettes);
for (const option of els.select.childNodes) {
if (option.value === defaultColorPaletteId) {
option.selected = true;
}
if (option.value === "custom_" + colorPaletteId) {
els.select.removeChild(option);
}
}
setColorPalette(defaultColorPaletteId);
await loadColorPalette(getColorPalette());
};
const loadColorPalette = async (colorPalette) => {
colorPalette = await completeColorPalette(colorPalette);
if (colorPalette.colors) {
if (colorPalette.colors.node_slot) {
Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot);
app.canvas.draw(true, true);
}
}
};
const getColorPalette = (colorPaletteId) => {
if (!colorPaletteId) {
colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
}
if (colorPaletteId.startsWith("custom_")) {
colorPaletteId = colorPaletteId.substr(7);
let customColorPalettes = getCustomColorPalettes();
if (customColorPalettes[colorPaletteId]) {
return customColorPalettes[colorPaletteId];
}
}
return colorPalettes[colorPaletteId];
};
const setColorPalette = (colorPaletteId) => {
app.ui.settings.setSettingValue(id, colorPaletteId);
};
const fileInput = $el("input", {
type: "file",
accept: ".json",
style: { display: "none" },
parent: document.body,
onchange: () => {
let file = fileInput.files[0];
if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = async () => {
await addCustomColorPalette(JSON.parse(reader.result));
};
reader.readAsText(file);
}
},
});
app.ui.settings.addSetting({
id,
name: "Color Palette",
type: (name, setter, value) => {
let options = [];
for (const c in colorPalettes) {
const colorPalette = colorPalettes[c];
options.push($el("option", { textContent: colorPalette.name, value: colorPalette.id, selected: colorPalette.id === value }));
}
let customColorPalettes = getCustomColorPalettes();
for (const c in customColorPalettes) {
const colorPalette = customColorPalettes[c];
options.push($el("option", { textContent: colorPalette.name + " (custom)", value: "custom_" + colorPalette.id, selected: "custom_" + colorPalette.id === value }));
}
return $el("div", [
$el("label", { textContent: name || id }, [
els.select = $el("select", {
onchange: (e) => {
setter(e.target.value);
}
}, options)
]),
$el("input", {
type: "button",
value: "Export",
onclick: async () => {
const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId));
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: colorPaletteId + ".json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
},
}),
$el("input", {
type: "button",
value: "Import",
onclick: () => {
fileInput.click();
}
}),
$el("input", {
type: "button",
value: "Template",
onclick: async () => {
const colorPalette = await getColorPaletteTemplate();
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: "color_palette.json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
}),
$el("input", {
type: "button",
value: "Delete",
onclick: async () => {
let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
if (colorPalettes[colorPaletteId]) {
app.ui.dialog.show("You cannot delete built-in color palette");
return;
}
if (colorPaletteId.startsWith("custom_")) {
colorPaletteId = colorPaletteId.substr(7);
}
await deleteCustomColorPalette(colorPaletteId);
}
}),
]);
},
defaultValue: defaultColorPaletteId,
async onChange(value) {
if (!value) {
return;
}
if (colorPalettes[value]) {
await loadColorPalette(colorPalettes[value]);
} else if (value.startsWith("custom_")) {
value = value.substr(7);
let customColorPalettes = getCustomColorPalettes();
if (customColorPalettes[value]) {
await loadColorPalette(customColorPalettes[value]);
}
}
},
});
},
});

View File

@ -0,0 +1,100 @@
import { app } from "/scripts/app.js";
// Use widget values and dates in output filenames
app.registerExtension({
name: "Comfy.SaveImageExtraOutput",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SaveImage") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
// Simple date formatter
const parts = {
d: (d) => d.getDate(),
M: (d) => d.getMonth() + 1,
h: (d) => d.getHours(),
m: (d) => d.getMinutes(),
s: (d) => d.getSeconds(),
};
const format =
Object.keys(parts)
.map((k) => k + k + "?")
.join("|") + "|yyy?y?";
function formatDate(text, date) {
return text.replace(new RegExp(format, "g"), function (text) {
if (text === "yy") return (date.getFullYear() + "").substring(2);
if (text === "yyyy") return date.getFullYear();
if (text[0] in parts) {
const p = parts[text[0]](date);
return (p + "").padStart(text.length, "0");
}
return text;
});
}
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const widget = this.widgets.find((w) => w.name === "filename_prefix");
widget.serializeValue = () => {
return widget.value.replace(/%([^%]+)%/g, function (match, text) {
const split = text.split(".");
if (split.length !== 2) {
// Special handling for dates
if (split[0].startsWith("date:")) {
return formatDate(split[0].substring(5), new Date());
}
if (text !== "width" && text !== "height") {
// Dont warn on standard replacements
console.warn("Invalid replacement pattern", text);
}
return match;
}
// Find node with matching S&R property name
let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]);
// If we cant, see if there is a node with that title
if (!nodes.length) {
nodes = app.graph._nodes.filter((n) => n.title === split[0]);
}
if (!nodes.length) {
console.warn("Unable to find node", split[0]);
return match;
}
if (nodes.length > 1) {
console.warn("Multiple nodes matched", split[0], "using first match");
}
const node = nodes[0];
const widget = node.widgets?.find((w) => w.name === split[1]);
if (!widget) {
console.warn("Unable to find widget", split[1], "on node", split[0], node);
return match;
}
return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_");
});
};
return r;
};
} else {
// When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty("Node name for S&R", this.title, "string");
}
return r;
};
}
},
});

View File

@ -1,13 +1,17 @@
import { ComfyWidgets, addRandomizeWidget } from "/scripts/widgets.js"; import { ComfyWidgets, addSeedControlWidget } from "/scripts/widgets.js";
import { app } from "/scripts/app.js"; import { app } from "/scripts/app.js";
const CONVERTED_TYPE = "converted-widget"; const CONVERTED_TYPE = "converted-widget";
const VALID_TYPES = ["STRING", "combo", "number"]; const VALID_TYPES = ["STRING", "combo", "number"];
function isConvertableWidget(widget, config) { function isConvertableWidget(widget, config) {
return VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0]); if (widget.name == "seed control after generating")
widget.allowConvertToInput = false;
else
return VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0]);
} }
function hideWidget(node, widget, suffix = "") { function hideWidget(node, widget, suffix = "") {
widget.origType = widget.type; widget.origType = widget.type;
widget.origComputeSize = widget.computeSize; widget.origComputeSize = widget.computeSize;
@ -23,7 +27,7 @@ function hideWidget(node, widget, suffix = "") {
return widget.value; return widget.value;
}; };
// Hide any linked widgets, e.g. seed+randomize // Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) { if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) { for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name); hideWidget(node, w, ":" + widget.name);
@ -40,7 +44,7 @@ function showWidget(widget) {
delete widget.origComputeSize; delete widget.origComputeSize;
delete widget.origSerializeValue; delete widget.origSerializeValue;
// Hide any linked widgets, e.g. seed+randomize // Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) { if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) { for (const w of widget.linkedWidgets) {
showWidget(w); showWidget(w);
@ -163,6 +167,8 @@ app.registerExtension({
const node = LiteGraph.createNode("PrimitiveNode"); const node = LiteGraph.createNode("PrimitiveNode");
app.graph.add(node); app.graph.add(node);
node.widgets.addSeedControlWidget(node,node.widgets[0],"randomize");
// Calculate a position that wont directly overlap another node // Calculate a position that wont directly overlap another node
const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]]; const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]];
while (isNodeAtPos(pos)) { while (isNodeAtPos(pos)) {
@ -284,8 +290,8 @@ app.registerExtension({
} }
} }
if (widget.type === "number") { if (widget.type === "combo") {
addRandomizeWidget(this, widget, "Random after every gen"); addSeedControlWidget(this, widget, "randomize");
} }
// When our value changes, update other widgets to reflect our changes // When our value changes, update other widgets to reflect our changes

View File

@ -371,14 +371,158 @@ class ComfyApp {
}); });
} }
/**
* Handle mouse
*
* Move group by header
*/
#addProcessMouseHandler() {
const self = this;
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function(e) {
const res = origProcessMouseDown.apply(this, arguments);
this.selected_group_moving = false;
if (this.selected_group && !this.selected_group_resizing) {
var font_size =
this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
var height = font_size * 1.4;
// Move group by header
if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) {
this.selected_group_moving = true;
}
}
return res;
}
const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove;
LGraphCanvas.prototype.processMouseMove = function(e) {
const orig_selected_group = this.selected_group;
if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
this.selected_group = null;
}
const res = origProcessMouseMove.apply(this, arguments);
if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
this.selected_group = orig_selected_group;
}
return res;
};
}
/**
* Handle keypress
*
* Ctrl + M mute/unmute selected nodes
*/
#addProcessKeyHandler() {
const self = this;
const origProcessKey = LGraphCanvas.prototype.processKey;
LGraphCanvas.prototype.processKey = function(e) {
const res = origProcessKey.apply(this, arguments);
if (res === false) {
return res;
}
if (!this.graph) {
return;
}
var block_default = false;
if (e.target.localName == "input") {
return;
}
if (e.type == "keydown") {
// Ctrl + M mute/unmute
if (e.keyCode == 77 && e.ctrlKey) {
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].mode === 2) { // never
this.selected_nodes[i].mode = 0; // always
} else {
this.selected_nodes[i].mode = 2; // never
}
}
}
block_default = true;
}
}
this.graph.change();
if (block_default) {
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
return res;
};
}
/**
* Draws group header bar
*/
#addDrawGroupsHandler() {
const self = this;
const origDrawGroups = LGraphCanvas.prototype.drawGroups;
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
if (!this.graph) {
return;
}
var groups = this.graph._groups;
ctx.save();
ctx.globalAlpha = 0.7 * this.editor_alpha;
for (var i = 0; i < groups.length; ++i) {
var group = groups[i];
if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) {
continue;
} //out of the visible area
ctx.fillStyle = group.color || "#335";
ctx.strokeStyle = group.color || "#335";
var pos = group._pos;
var size = group._size;
ctx.globalAlpha = 0.25 * this.editor_alpha;
ctx.beginPath();
var font_size =
group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4);
ctx.fill();
ctx.globalAlpha = this.editor_alpha;
}
ctx.restore();
const res = origDrawGroups.apply(this, arguments);
return res;
}
}
/** /**
* Draws node highlights (executing, drag drop) and progress bar * Draws node highlights (executing, drag drop) and progress bar
*/ */
#addDrawNodeHandler() { #addDrawNodeHandler() {
const orig = LGraphCanvas.prototype.drawNodeShape; const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
const self = this; const self = this;
LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
const res = orig.apply(this, arguments); const res = origDrawNodeShape.apply(this, arguments);
let color = null; let color = null;
if (node.id === +self.runningNodeId) { if (node.id === +self.runningNodeId) {
@ -427,6 +571,21 @@ class ComfyApp {
return res; return res;
}; };
const origDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function (node, ctx) {
var editor_alpha = this.editor_alpha;
if (node.mode === 2) { // never
this.editor_alpha = 0.4;
}
const res = origDrawNode.apply(this, arguments);
this.editor_alpha = editor_alpha;
return res;
};
} }
/** /**
@ -458,6 +617,10 @@ class ComfyApp {
api.addEventListener("executed", ({ detail }) => { api.addEventListener("executed", ({ detail }) => {
this.nodeOutputs[detail.node] = detail.output; this.nodeOutputs[detail.node] = detail.output;
const node = this.graph.getNodeById(detail.node);
if (node?.onExecuted) {
node.onExecuted(detail.output);
}
}); });
api.init(); api.init();
@ -486,27 +649,6 @@ class ComfyApp {
} }
} }
/**
* Setup slot colors for types
*/
setupSlotColors() {
let colors = {
"CLIP": "#FFD500", // bright yellow
"CLIP_VISION": "#A8DADC", // light blue-gray
"CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange
"CONDITIONING": "#FFA931", // vibrant orange-yellow
"CONTROL_NET": "#6EE7B7", // soft mint green
"IMAGE": "#64B5F6", // bright sky blue
"LATENT": "#FF9CF9", // light pink-purple
"MASK": "#81C784", // muted green
"MODEL": "#B39DDB", // light lavender-purple
"STYLE_MODEL": "#C2FFAE", // light green-yellow
"VAE": "#FF6E6E", // bright red
};
Object.assign(this.canvas.default_connection_color_byType, colors);
}
/** /**
* Set up the app on the page * Set up the app on the page
*/ */
@ -518,12 +660,13 @@ class ComfyApp {
canvasEl.tabIndex = "1"; canvasEl.tabIndex = "1";
document.body.prepend(canvasEl); document.body.prepend(canvasEl);
this.#addProcessMouseHandler();
this.#addProcessKeyHandler();
this.graph = new LGraph(); this.graph = new LGraph();
const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph)); const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph));
this.ctx = canvasEl.getContext("2d"); this.ctx = canvasEl.getContext("2d");
this.setupSlotColors();
this.graph.start(); this.graph.start();
function resizeCanvas() { function resizeCanvas() {
@ -561,6 +704,7 @@ class ComfyApp {
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000); setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000);
this.#addDrawNodeHandler(); this.#addDrawNodeHandler();
this.#addDrawGroupsHandler();
this.#addApiUpdateHandlers(); this.#addApiUpdateHandlers();
this.#addDropHandler(); this.#addDropHandler();
this.#addPasteHandler(); this.#addPasteHandler();
@ -590,29 +734,38 @@ class ComfyApp {
const nodeData = defs[nodeId]; const nodeData = defs[nodeId];
const node = Object.assign( const node = Object.assign(
function ComfyNode() { function ComfyNode() {
const inputs = nodeData["input"]["required"]; var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined){
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
}
const config = { minWidth: 1, minHeight: 1 }; const config = { minWidth: 1, minHeight: 1 };
for (const inputName in inputs) { for (const inputName in inputs) {
const inputData = inputs[inputName]; const inputData = inputs[inputName];
const type = inputData[0]; const type = inputData[0];
if (Array.isArray(type)) { if(inputData[1]?.forceInput) {
// Enums
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type); this.addInput(inputName, type);
} else {
if (Array.isArray(type)) {
// Enums
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
}
} }
} }
for (const output of nodeData["output"]) { for (const o in nodeData["output"]) {
this.addOutput(output, output); const output = nodeData["output"][o];
const outputName = nodeData["output_name"][o] || output;
this.addOutput(outputName, output);
} }
const s = this.computeSize(); const s = this.computeSize();
@ -646,6 +799,8 @@ class ComfyApp {
* @param {*} graphData A serialized graph object * @param {*} graphData A serialized graph object
*/ */
loadGraphData(graphData) { loadGraphData(graphData) {
this.clean();
if (!graphData) { if (!graphData) {
graphData = defaultGraph; graphData = defaultGraph;
} }
@ -673,6 +828,12 @@ class ComfyApp {
widget.value = widget.value.slice(7); widget.value = widget.value.slice(7);
} }
} }
if (widget.name == "seed control after generating") {
if (widget.value == true) {
widget.value = "randomize";
}
}
} }
} }
} }
@ -700,6 +861,11 @@ class ComfyApp {
continue; continue;
} }
if (node.mode === 2) {
// Don't serialize muted nodes
continue;
}
const inputs = {}; const inputs = {};
const widgets = node.widgets; const widgets = node.widgets;
@ -739,6 +905,18 @@ class ComfyApp {
}; };
} }
// Remove inputs connected to removed nodes
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
}
}
}
return { workflow, output }; return { workflow, output };
} }
@ -803,6 +981,38 @@ class ComfyApp {
} }
this.extensions.push(extension); this.extensions.push(extension);
} }
/**
* Refresh combo list on whole nodes
*/
async refreshComboInNodes() {
const defs = await api.getNodeDefs();
for(let nodeNum in this.graph._nodes) {
const node = this.graph._nodes[nodeNum];
const def = defs[node.type];
for(const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if(!widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
}
}
}
}
}
/**
* Clean current state
*/
clean() {
this.nodeOutputs = {};
}
} }
export const app = new ComfyApp(); export const app = new ComfyApp();

View File

@ -1,6 +1,6 @@
import { api } from "./api.js"; import { api } from "./api.js";
function $el(tag, propsOrChildren, children) { export function $el(tag, propsOrChildren, children) {
const split = tag.split("."); const split = tag.split(".");
const element = document.createElement(split.shift()); const element = document.createElement(split.shift());
element.classList.add(...split); element.classList.add(...split);
@ -114,6 +114,17 @@ class ComfySettingsDialog extends ComfyDialog {
this.settings = []; this.settings = [];
} }
getSettingValue(id, defaultValue) {
const settingId = "Comfy.Settings." + id;
const v = localStorage[settingId];
return v == null ? defaultValue : JSON.parse(v);
}
setSettingValue(id, value) {
const settingId = "Comfy.Settings." + id;
localStorage[settingId] = JSON.stringify(value);
}
addSetting({ id, name, type, defaultValue, onChange }) { addSetting({ id, name, type, defaultValue, onChange }) {
if (!id) { if (!id) {
throw new Error("Settings must have an ID"); throw new Error("Settings must have an ID");
@ -142,7 +153,7 @@ class ComfySettingsDialog extends ComfyDialog {
}; };
if (typeof type === "function") { if (typeof type === "function") {
return type(name, setter); return type(name, setter, value);
} }
switch (type) { switch (type) {
@ -376,7 +387,11 @@ export class ComfyUI {
}, },
}), }),
$el("button", { textContent: "Load", onclick: () => fileInput.click() }), $el("button", { textContent: "Load", onclick: () => fileInput.click() }),
$el("button", { textContent: "Clear", onclick: () => app.graph.clear() }), $el("button", { textContent: "Refresh", onclick: () => app.refreshComboInNodes() }),
$el("button", { textContent: "Clear", onclick: () => {
app.clean();
app.graph.clear();
}}),
$el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }),
]); ]);

View File

@ -10,37 +10,56 @@ function getNumberDefaults(inputData, defaultStep) {
return { val: defaultVal, config: { min, max, step: 10.0 * step } }; return { val: defaultVal, config: { min, max, step: 10.0 * step } };
} }
export function addRandomizeWidget(node, targetWidget, name, defaultValue = false) { export function addSeedControlWidget(node, targetWidget, defaultValue = "randomize", values) {
const randomize = node.addWidget("toggle", name, defaultValue, function (v) {}, { const seedControl = node.addWidget("combo", "seed control after generating", "randomize", function (v) { }, {
on: "enabled", values: ["fixed seed", "increment", "decrement", "randomize"]
off: "disabled", })
serialize: false, // Don't include this in prompt. seedControl.afterQueued = () => {
});
randomize.afterQueued = () => { var v = seedControl.value;
if (randomize.value) {
const min = targetWidget.options?.min; switch (v) {
let max = targetWidget.options?.max; case ("fixed seed"):
if (min != null || max != null) { console.log("Fixed Seed");
if (max) { break;
// limit max to something that javascript can handle case ("increment"):
max = Math.min(1125899906842624, max); targetWidget.value += 1;
console.log("increment");
break;
case ("decrement"):
targetWidget.value -= 1;
console.log("decrement");
break;
case ("randomize"):
const min = targetWidget.options?.min;
let max = targetWidget.options?.max;
if (min != null || max != null) {
if (max) {
// limit max to something that javascript can handle
max = Math.min(1125899906842624, max);
console.log("Random");
}
targetWidget.value = Math.floor(Math.random() * ((max ?? 9999999999) - (min ?? 0) + 1) + (min ?? 0));
console.log("Random");
} else {
targetWidget.value = Math.floor(Math.random() * 1125899906842624);
console.log("Random");
} }
targetWidget.value = Math.floor(Math.random() * ((max ?? 9999999999) - (min ?? 0) + 1) + (min ?? 0)); break;
} else { default:
targetWidget.value = Math.floor(Math.random() * 1125899906842624); console.log("default (fail)");
}
} }
}; };
return randomize;
return seedControl;
} }
function seedWidget(node, inputName, inputData) { function seedWidget(node, inputName, inputData) {
const seed = ComfyWidgets.INT(node, inputName, inputData); const seed = ComfyWidgets.INT(node, inputName, inputData);
const randomize = addRandomizeWidget(node, seed.widget, "Random seed after every gen", true); const seedControl = addSeedControlWidget(node, seed.widget, "randomize");
seed.widget.linkedWidgets = [randomize]; seed.widget.linkedWidgets = [seedControl];
return { widget: seed, randomize }; return { widget: seed, seedControl };
} }
const MultilineSymbol = Symbol(); const MultilineSymbol = Symbol();

View File

@ -64,6 +64,12 @@ body {
margin-bottom: 20px; /* Add some margin between the text and the close button*/ margin-bottom: 20px; /* Add some margin between the text and the close button*/
} }
.comfy-modal select,
.comfy-modal input[type=button],
.comfy-modal input[type=checkbox] {
margin: 3px 3px 3px 4px;
}
.comfy-modal button { .comfy-modal button {
cursor: pointer; cursor: pointer;
color: #aaaaaa; color: #aaaaaa;
@ -95,6 +101,12 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
color: #999;
background-color: #353535;
font-family: sans-serif;
padding: 10px;
border-radius: 0 8px 8px 8px;
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4);
} }
.comfy-menu button { .comfy-menu button {
@ -109,6 +121,22 @@ body {
.comfy-menu-btns button { .comfy-menu-btns button {
font-size: 10px; font-size: 10px;
width: 50%; width: 50%;
color: #999 !important;
}
.comfy-menu > button {
width: 100%;
}
.comfy-menu > button,
.comfy-menu-btns button,
.comfy-menu .comfy-list button {
color: #ddd;
background-color: #222;
border-radius: 8px;
border-color: #4e4e4e;
border-style: solid;
margin-top: 2px;
} }
.comfy-menu span.drag-handle { .comfy-menu span.drag-handle {
@ -141,14 +169,18 @@ body {
} }
.comfy-list { .comfy-list {
background-color: rgb(225, 225, 225); color: #999;
background-color: #333;
margin-bottom: 10px; margin-bottom: 10px;
border-color: #4e4e4e;
border-style: solid;
} }
.comfy-list-items { .comfy-list-items {
overflow-y: scroll; overflow-y: scroll;
max-height: 100px; max-height: 100px;
background-color: #d0d0d0; min-height: 25px;
background-color: #222;
padding: 5px; padding: 5px;
} }
@ -175,6 +207,7 @@ body {
} }
button.comfy-settings-btn { button.comfy-settings-btn {
background-color: rgba(0, 0, 0, 0);
font-size: 12px; font-size: 12px;
padding: 0; padding: 0;
position: absolute; position: absolute;
@ -182,6 +215,10 @@ button.comfy-settings-btn {
border: none; border: none;
} }
button.comfy-queue-btn {
margin: 6px 0 !important;
}
.comfy-modal.comfy-settings { .comfy-modal.comfy-settings {
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--fg-color); color: var(--fg-color);
@ -190,6 +227,13 @@ button.comfy-settings-btn {
@media only screen and (max-height: 850px) { @media only screen and (max-height: 850px) {
.comfy-menu { .comfy-menu {
margin-top: -70px; top: 0 !important;
bottom: 0 !important;
left: auto !important;
right: 0 !important;
border-radius: 0px;
}
.comfy-menu span.drag-handle {
visibility:hidden
} }
} }