Merge branch 'main' of github.com:choey/ComfyUI-Custom-Scripts into deduplicate-image-feed

This commit is contained in:
cho
2024-04-03 19:29:19 -07:00
12 changed files with 245 additions and 80 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
__pycache__
pysssss.json
user/autocomplete.txt
user/autocomplete.txt
web/js/assets/favicon.user.ico
web/js/assets/favicon-active.user.ico
+18 -6
View File
@@ -19,12 +19,24 @@ class ShowText:
CATEGORY = "utils"
def notify(self, text, unique_id = None, extra_pnginfo=None):
if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]:
workflow = extra_pnginfo[0]["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
if node:
node["widgets_values"] = [text]
def notify(self, text, unique_id=None, extra_pnginfo=None):
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
print("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
+3 -3
View File
@@ -7,11 +7,11 @@ class StringFunction:
"required": {
"action": (["append", "replace"], {}),
"tidy_tags": (["yes", "no"], {}),
"text_a": ("STRING", {"multiline": True}),
"text_b": ("STRING", {"multiline": True}),
"text_a": ("STRING", {"multiline": True, "dynamicPrompts": False}),
"text_b": ("STRING", {"multiline": True, "dynamicPrompts": False}),
},
"optional": {
"text_c": ("STRING", {"multiline": True})
"text_c": ("STRING", {"multiline": True, "dynamicPrompts": False})
}
}
+81 -53
View File
@@ -1,7 +1,7 @@
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
import { api } from "../../../../scripts/api.js";
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { api } from "../../../scripts/api.js";
import { $el, ComfyDialog } from "../../../scripts/ui.js";
import { TextAreaAutoComplete } from "./common/autocomplete.js";
import { ModelInfoDialog } from "./common/modelInfoDialog.js";
import { LoraInfoDialog } from "./modelInfo.js";
@@ -275,57 +275,6 @@ const id = "pysssss.AutoCompleter";
app.registerExtension({
name: id,
init() {
async function addEmbeddings() {
const embeddings = await api.getEmbeddings();
const words = {};
words["embedding:"] = { text: "embedding:" };
for (const emb of embeddings) {
const v = `embedding:${emb}`;
words[v] = {
text: v,
info: () => new EmbeddingInfoDialog(emb).show("embeddings", emb),
use_replacer: false,
};
}
TextAreaAutoComplete.updateWords("pysssss.embeddings", words);
}
async function addLoras() {
const loras = await api
.fetchApi("/pysssss/loras", { cache: "no-store" })
.then(res => res.json());
const words = {};
words["lora:"] = { text: "lora:" };
for (const lora of loras) {
const v = `<lora:${lora}:1.0>`;
words[v] = {
text: v,
info: () => new LoraInfoDialog(lora).show("loras", lora),
use_replacer: false,
};
}
TextAreaAutoComplete.updateWords("pysssss.loras", words);
}
// store global words with/without loras
Promise.all([addEmbeddings(), addCustomWords()])
.then(() => {
TextAreaAutoComplete.globalWordsExclLoras = Object.assign(
{},
TextAreaAutoComplete.globalWords
);
})
.then(addLoras)
.then(() => {
if (!TextAreaAutoComplete.lorasEnabled) {
toggleLoras(); // off by default
}
});
const STRING = ComfyWidgets.STRING;
const SKIP_WIDGETS = new Set(["ttN xyPlot.x_values", "ttN xyPlot.y_values"]);
ComfyWidgets.STRING = function (node, inputName, inputData) {
@@ -517,6 +466,29 @@ app.registerExtension({
),
]
),
$el(
"label",
{
textContent: "Max suggestions: ",
style: {
display: "block",
},
},
[
$el("input", {
type: "number",
value: +TextAreaAutoComplete.suggestionCount,
style: {
width: "80px"
},
onchange: (event) => {
const value = +event.target.value;
TextAreaAutoComplete.suggestionCount = value;;
localStorage.setItem(id + ".SuggestionCount", TextAreaAutoComplete.suggestionCount);
},
}),
]
),
$el("button", {
textContent: "Manage Custom Words",
onclick: () => {
@@ -539,6 +511,62 @@ app.registerExtension({
TextAreaAutoComplete.insertOnTab = localStorage.getItem(id + ".InsertOnTab") !== "false";
TextAreaAutoComplete.insertOnEnter = localStorage.getItem(id + ".InsertOnEnter") !== "false";
TextAreaAutoComplete.lorasEnabled = localStorage.getItem(id + ".ShowLoras") === "true";
TextAreaAutoComplete.suggestionCount = +localStorage.getItem(id + ".SuggestionCount");
},
setup() {
async function addEmbeddings() {
const embeddings = await api.getEmbeddings();
const words = {};
words["embedding:"] = { text: "embedding:" };
for (const emb of embeddings) {
const v = `embedding:${emb}`;
words[v] = {
text: v,
info: () => new EmbeddingInfoDialog(emb).show("embeddings", emb),
use_replacer: false,
};
}
TextAreaAutoComplete.updateWords("pysssss.embeddings", words);
}
async function addLoras() {
let loras;
try {
loras = LiteGraph.registered_node_types["LoraLoader"]?.nodeData.input.required.lora_name[0];
} catch (error) {}
if (!loras?.length) {
loras = await api.fetchApi("/pysssss/loras", { cache: "no-store" }).then((res) => res.json());
}
const words = {};
words["lora:"] = { text: "lora:" };
for (const lora of loras) {
const v = `<lora:${lora}:1.0>`;
words[v] = {
text: v,
info: () => new LoraInfoDialog(lora).show("loras", lora),
use_replacer: false,
};
}
TextAreaAutoComplete.updateWords("pysssss.loras", words);
}
// store global words with/without loras
Promise.all([addEmbeddings(), addCustomWords()])
.then(() => {
TextAreaAutoComplete.globalWordsExclLoras = Object.assign({}, TextAreaAutoComplete.globalWords);
})
.then(addLoras)
.then(() => {
if (!TextAreaAutoComplete.lorasEnabled) {
toggleLoras(); // off by default
}
});
},
beforeRegisterNodeDef(_, def) {
// Process each input to see if there is a custom word list for
+1
View File
@@ -5,6 +5,7 @@
font-family: sans-serif;
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4);
z-index: 9999;
overflow: auto;
}
.pysssss-autocomplete-item {
+42 -3
View File
@@ -238,6 +238,7 @@ class TextAreaCaretHelper {
fontSize: computedStyle.fontSize,
fontFamily: computedStyle.fontFamily,
padding: "0",
position: "absolute",
});
body.appendChild(tempNode);
@@ -291,10 +292,46 @@ class TextAreaCaretHelper {
const startPos = this.el.selectionStart;
const endPos = this.el.selectionEnd;
this.el.value = this.el.value.substring(0, startPos + offset) + value + this.el.value.substring(endPos, this.el.value.length);
// Move selection to beginning of offset
this.el.selectionStart = this.el.selectionStart + offset;
// Using execCommand to support undo, but since it's officially
// 'deprecated' we need a backup solution, but it won't support undo :(
let pasted = true;
try {
if (!document.execCommand("insertText", false, value)) {
pasted = false;
}
} catch (e) {
console.error("Error caught during execCommand:", e);
pasted = false;
}
if (!pasted) {
console.error(
"execCommand unsuccessful; not supported. Adding text manually, no undo support.");
textarea.setRangeText(modifiedText, this.el.selectionStart, this.el.selectionEnd, 'end');
}
this.el.selectionEnd = this.el.selectionStart = startPos + value.length + offset + (finalOffset ?? 0);
} else {
this.el.value += value;
// Using execCommand to support undo, but since it's officially
// 'deprecated' we need a backup solution, but it won't support undo :(
let pasted = true;
try {
if (!document.execCommand("insertText", false, value)) {
pasted = false;
}
} catch (e) {
console.error("Error caught during execCommand:", e);
pasted = false;
}
if (!pasted) {
console.error(
"execCommand unsuccessful; not supported. Adding text manually, no undo support.");
this.el.value += value;
}
}
}
}
@@ -318,6 +355,7 @@ export class TextAreaAutoComplete {
static insertOnEnter = true;
static replacer = undefined;
static lorasEnabled = false;
static suggestionCount = 20;
/** @type {Record<string, Record<string, AutoCompleteEntry>>} */
static groups = {};
@@ -492,7 +530,7 @@ export class TextAreaAutoComplete {
);
const top = priorityMatches.length * 0.2;
return priorityMatches.slice(0, top).concat(prefixMatches, priorityMatches.slice(top), includesMatches).slice(0, 20);
return priorityMatches.slice(0, top).concat(prefixMatches, priorityMatches.slice(top), includesMatches).slice(0, TextAreaAutoComplete.suggestionCount);
}
#update() {
@@ -612,6 +650,7 @@ export class TextAreaAutoComplete {
const position = this.helper.getCursorOffset();
this.dropdown.style.left = (position.left ?? 0) + "px";
this.dropdown.style.top = (position.top ?? 0) + "px";
this.dropdown.style.maxHeight = (window.innerHeight - position.top) + "px";
}
#hide() {
+1 -1
View File
@@ -4,7 +4,7 @@
position: fixed;
top: 0;
left: 0;
z-index: 999;
z-index: 1000;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
+10
View File
@@ -107,6 +107,16 @@ export class Lightbox {
this.img.src = img;
this.img.style.opacity = 1;
}
async updateWithNewImage(img, feedDirection) {
// No-op if lightbox is not open
if (this.el.style.display === "none" || this.el.style.opacity === "0") return;
// Ensure currently shown image does not change
const [method, shift] = feedDirection === "newest first" ? ["unshift", 1] : ["push", 0];
this.images[method](img);
await this.update(shift);
}
}
export const lightbox = new Lightbox();
+5 -5
View File
@@ -1,10 +1,10 @@
.lds-ring {
.pysssss-lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
.pysssss-lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
@@ -16,13 +16,13 @@
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
.pysssss-lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
.pysssss-lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
.pysssss-lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
+1 -1
View File
@@ -4,6 +4,6 @@ addStylesheet(import.meta.url);
export function createSpinner() {
const div = document.createElement("div");
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
div.innerHTML = `<div class="pysssss-lds-ring"><div></div><div></div><div></div><div></div></div>`;
return div.firstElementChild;
}
+16 -2
View File
@@ -6,7 +6,7 @@ import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "pysssss.FaviconStatus",
setup() {
async setup() {
let link = document.querySelector("link[rel~='icon']");
if (!link) {
link = document.createElement("link");
@@ -14,8 +14,22 @@ app.registerExtension({
document.head.appendChild(link);
}
const getUrl = (active, user) => new URL(`assets/favicon${active ? "-active" : ""}${user ? ".user" : ""}.ico`, import.meta.url);
const testUrl = async (active) => {
const url = getUrl(active, true);
const r = await fetch(url, {
method: "HEAD",
});
if (r.status === 200) {
return url;
}
return getUrl(active, false);
};
const activeUrl = await testUrl(true);
const idleUrl = await testUrl(false);
let executing = false;
const update = () => (link.href = new URL(`assets/favicon${executing ? "-active" : ""}.ico`, import.meta.url));
const update = () => (link.href = executing ? activeUrl : idleUrl);
for (const e of ["execution_start", "progress"]) {
api.addEventListener(e, () => {
+64 -5
View File
@@ -15,6 +15,9 @@ $el("style", {
display: flex;
flex-direction: column;
}
div > .pysssss-image-feed {
position: static;
}
.pysssss-image-feed--top, .pysssss-image-feed--bottom {
width: 100vw;
min-height: 30px;
@@ -206,7 +209,7 @@ $el("style", {
app.registerExtension({
name: "pysssss.ImageFeed",
setup() {
async setup() {
let visible = true;
const seenImages = new Map();
const showButton = $el("button.comfy-settings-btn", {
@@ -217,6 +220,17 @@ app.registerExtension({
display: "none",
},
});
let showMenuButton;
if (!app.menu?.element.style.display && app.menu?.settingsGroup) {
showMenuButton = new (await import("../../../scripts/ui/components/button.js")).ComfyButton({
icon: "image-multiple",
action: () => showButton.click(),
tooltip: "Show Image Feed 🐍",
content: "Show Image Feed 🐍",
});
showMenuButton.enabled = false;
app.menu.settingsGroup.append(showMenuButton);
}
const getVal = (n, d) => {
const v = localStorage.getItem("pysssss.ImageFeed." + n);
@@ -230,11 +244,19 @@ app.registerExtension({
localStorage.setItem("pysssss.ImageFeed." + n, v);
};
const imageFeed = $el("div.pysssss-image-feed", {
parent: document.body,
});
const imageFeed = $el("div.pysssss-image-feed");
const imageList = $el("div.pysssss-image-feed-list");
function updateMenuParent(location) {
if (showMenuButton) {
document.querySelector(".comfyui-body-" + location).append(imageFeed);
} else {
if (!imageFeed.parent) {
document.body.append(imageFeed);
}
}
}
const feedLocation = app.ui.settings.addSetting({
id: "pysssss.ImageFeed.Location",
name: "🐍 Image Feed Location",
@@ -256,6 +278,8 @@ app.registerExtension({
oninput: (e) => {
feedLocation.value = e.target.value;
imageFeed.className = `pysssss-image-feed pysssss-image-feed--${feedLocation.value}`;
updateMenuParent(feedLocation.value);
window.dispatchEvent(new Event("resize"));
},
},
["left", "top", "right", "bottom"].map((m) =>
@@ -271,6 +295,7 @@ app.registerExtension({
},
onChange(value) {
imageFeed.className = `pysssss-image-feed pysssss-image-feed--${value}`;
updateMenuParent(value);
},
});
@@ -324,7 +349,10 @@ by reducing the number of images in the feed.`,
const clearButton = $el("button.pysssss-image-feed-btn.clear-btn", {
textContent: "Clear",
onclick: () => imageList.replaceChildren(),
onclick: () => {
imageList.replaceChildren();
window.dispatchEvent(new Event("resize"));
},
});
const hideButton = $el("button.pysssss-image-feed-btn.hide-btn", {
@@ -332,8 +360,10 @@ by reducing the number of images in the feed.`,
onclick: () => {
imageFeed.style.display = "none";
showButton.style.display = "unset";
if (showMenuButton) showMenuButton.enabled = true;
saveVal("Visible", 0);
visible = false;
window.dispatchEvent(new Event("resize"));
},
});
@@ -344,6 +374,28 @@ by reducing the number of images in the feed.`,
saveVal("ImageSize", v);
columnInput.max = Math.max(10, v, columnInput.max);
columnInput.value = v;
window.dispatchEvent(new Event("resize"));
}
function addImageToFeed(href) {
const method = feedDirection.value === "newest first" ? "prepend" : "append";
imageList[method](
$el("div", [
$el(
"a",
{
target: "_blank",
href,
onclick: (e) => {
const imgs = [...imageList.querySelectorAll("img")].map((img) => img.getAttribute("src"));
lightbox.show(imgs, imgs.indexOf(href));
e.preventDefault();
},
},
[$el("img", { src: href })]
),
])
);
}
function addImageToFeed(href) {
@@ -384,6 +436,7 @@ by reducing the number of images in the feed.`,
e.target.parentElement.title = `Controls the maximum size of the image feed panel (${e.target.value}vh)`;
imageFeed.style.setProperty("--max-size", e.target.value);
saveVal("FeedSize", e.target.value);
window.dispatchEvent(new Event("resize"));
},
$: (el) => {
requestAnimationFrame(() => {
@@ -432,10 +485,14 @@ by reducing the number of images in the feed.`,
showButton.onclick = () => {
imageFeed.style.display = "block";
showButton.style.display = "none";
if (showMenuButton) showMenuButton.enabled = false;
saveVal("Visible", 1);
visible = true;
window.dispatchEvent(new Event("resize"));
};
document.querySelector(".comfy-settings-btn").after(showButton);
window.dispatchEvent(new Event("resize"));
if (!+getVal("Visible", 1)) {
hideButton.onclick();
@@ -443,10 +500,12 @@ by reducing the number of images in the feed.`,
api.addEventListener("executed", ({ detail }) => {
if (visible && detail?.output?.images) {
if (detail.node?.includes?.(":")) {
if (detail.node?.includes?.(":")) {
// Ignore group nodes
const n = app.graph.getNodeById(detail.node.split(":")[0]);
if (n?.getInnerNodes) return;
if (n?.getInnerNodes) return;
}
for (const src of detail.output.images) {