From 32bb6f1f044daa473297fa78a3d9eaa6f7421d0e Mon Sep 17 00:00:00 2001
From: Davo KiwiCloudNinja ${this.parser.parseInline(tokens)} An error occurred:
\n';
+ }
+ return ''
+ + (escaped ? code : escape$1(code, true))
+ + '
\n';
+ }
+ blockquote({ tokens }) {
+ const body = this.parser.parse(tokens);
+ return `'
+ + (escaped ? code : escape$1(code, true))
+ + '\n${body}
\n`;
+ }
+ html({ text }) {
+ return text;
+ }
+ heading({ tokens, depth }) {
+ return `
\n';
+ }
+ list(token) {
+ const ordered = token.ordered;
+ const start = token.start;
+ let body = '';
+ for (let j = 0; j < token.items.length; j++) {
+ const item = token.items[j];
+ body += this.listitem(item);
+ }
+ const type = ordered ? 'ol' : 'ul';
+ const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : '';
+ return '<' + type + startAttr + '>\n' + body + '' + type + '>\n';
+ }
+ listitem(item) {
+ let itemBody = '';
+ if (item.task) {
+ const checkbox = this.checkbox({ checked: !!item.checked });
+ if (item.loose) {
+ if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') {
+ item.tokens[0].text = checkbox + ' ' + item.tokens[0].text;
+ if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') {
+ item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text;
+ }
+ }
+ else {
+ item.tokens.unshift({
+ type: 'text',
+ raw: checkbox + ' ',
+ text: checkbox + ' ',
+ });
+ }
+ }
+ else {
+ itemBody += checkbox + ' ';
+ }
+ }
+ itemBody += this.parser.parse(item.tokens, !!item.loose);
+ return `\n'
+ + '\n'
+ + header
+ + '\n'
+ + body
+ + '
\n';
+ }
+ tablerow({ text }) {
+ return `\n${text} \n`;
+ }
+ tablecell(token) {
+ const content = this.parser.parseInline(token.tokens);
+ const type = token.header ? 'th' : 'td';
+ const tag = token.align
+ ? `<${type} align="${token.align}">`
+ : `<${type}>`;
+ return tag + content + `${type}>\n`;
+ }
+ /**
+ * span level renderer
+ */
+ strong({ tokens }) {
+ return `${this.parser.parseInline(tokens)}`;
+ }
+ em({ tokens }) {
+ return `${this.parser.parseInline(tokens)}`;
+ }
+ codespan({ text }) {
+ return `${text}`;
+ }
+ br(token) {
+ return '
';
+ }
+ del({ tokens }) {
+ return `${this.parser.parseInline(tokens)}`;
+ }
+ link({ href, title, tokens }) {
+ const text = this.parser.parseInline(tokens);
+ const cleanHref = cleanUrl(href);
+ if (cleanHref === null) {
+ return text;
+ }
+ href = cleanHref;
+ let out = '' + text + '';
+ return out;
+ }
+ image({ href, title, text }) {
+ const cleanHref = cleanUrl(href);
+ if (cleanHref === null) {
+ return text;
+ }
+ href = cleanHref;
+ let out = `';
+ return out;
+ }
+ text(token) {
+ return 'tokens' in token && token.tokens ? this.parser.parseInline(token.tokens) : token.text;
+ }
+}
+
+/**
+ * TextRenderer
+ * returns only the textual part of the token
+ */
+class _TextRenderer {
+ // no need for block level renderers
+ strong({ text }) {
+ return text;
+ }
+ em({ text }) {
+ return text;
+ }
+ codespan({ text }) {
+ return text;
+ }
+ del({ text }) {
+ return text;
+ }
+ html({ text }) {
+ return text;
+ }
+ text({ text }) {
+ return text;
+ }
+ link({ text }) {
+ return '' + text;
+ }
+ image({ text }) {
+ return '' + text;
+ }
+ br() {
+ return '';
+ }
+}
+
+/**
+ * Parsing & Compiling
+ */
+class _Parser {
+ options;
+ renderer;
+ textRenderer;
+ constructor(options) {
+ this.options = options || _defaults;
+ this.options.renderer = this.options.renderer || new _Renderer();
+ this.renderer = this.options.renderer;
+ this.renderer.options = this.options;
+ this.renderer.parser = this;
+ this.textRenderer = new _TextRenderer();
+ }
+ /**
+ * Static Parse Method
+ */
+ static parse(tokens, options) {
+ const parser = new _Parser(options);
+ return parser.parse(tokens);
+ }
+ /**
+ * Static Parse Inline Method
+ */
+ static parseInline(tokens, options) {
+ const parser = new _Parser(options);
+ return parser.parseInline(tokens);
+ }
+ /**
+ * Parse Loop
+ */
+ parse(tokens, top = true) {
+ let out = '';
+ for (let i = 0; i < tokens.length; i++) {
+ const anyToken = tokens[i];
+ // Run any renderer extensions
+ if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[anyToken.type]) {
+ const genericToken = anyToken;
+ const ret = this.options.extensions.renderers[genericToken.type].call({ parser: this }, genericToken);
+ if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'paragraph', 'text'].includes(genericToken.type)) {
+ out += ret || '';
+ continue;
+ }
+ }
+ const token = anyToken;
+ switch (token.type) {
+ case 'space': {
+ out += this.renderer.space(token);
+ continue;
+ }
+ case 'hr': {
+ out += this.renderer.hr(token);
+ continue;
+ }
+ case 'heading': {
+ out += this.renderer.heading(token);
+ continue;
+ }
+ case 'code': {
+ out += this.renderer.code(token);
+ continue;
+ }
+ case 'table': {
+ out += this.renderer.table(token);
+ continue;
+ }
+ case 'blockquote': {
+ out += this.renderer.blockquote(token);
+ continue;
+ }
+ case 'list': {
+ out += this.renderer.list(token);
+ continue;
+ }
+ case 'html': {
+ out += this.renderer.html(token);
+ continue;
+ }
+ case 'paragraph': {
+ out += this.renderer.paragraph(token);
+ continue;
+ }
+ case 'text': {
+ let textToken = token;
+ let body = this.renderer.text(textToken);
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'text') {
+ textToken = tokens[++i];
+ body += '\n' + this.renderer.text(textToken);
+ }
+ if (top) {
+ out += this.renderer.paragraph({
+ type: 'paragraph',
+ raw: body,
+ text: body,
+ tokens: [{ type: 'text', raw: body, text: body }],
+ });
+ }
+ else {
+ out += body;
+ }
+ continue;
+ }
+ default: {
+ const errMsg = 'Token with "' + token.type + '" type was not found.';
+ if (this.options.silent) {
+ console.error(errMsg);
+ return '';
+ }
+ else {
+ throw new Error(errMsg);
+ }
+ }
+ }
+ }
+ return out;
+ }
+ /**
+ * Parse Inline Tokens
+ */
+ parseInline(tokens, renderer) {
+ renderer = renderer || this.renderer;
+ let out = '';
+ for (let i = 0; i < tokens.length; i++) {
+ const anyToken = tokens[i];
+ // Run any renderer extensions
+ if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[anyToken.type]) {
+ const ret = this.options.extensions.renderers[anyToken.type].call({ parser: this }, anyToken);
+ if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(anyToken.type)) {
+ out += ret || '';
+ continue;
+ }
+ }
+ const token = anyToken;
+ switch (token.type) {
+ case 'escape': {
+ out += renderer.text(token);
+ break;
+ }
+ case 'html': {
+ out += renderer.html(token);
+ break;
+ }
+ case 'link': {
+ out += renderer.link(token);
+ break;
+ }
+ case 'image': {
+ out += renderer.image(token);
+ break;
+ }
+ case 'strong': {
+ out += renderer.strong(token);
+ break;
+ }
+ case 'em': {
+ out += renderer.em(token);
+ break;
+ }
+ case 'codespan': {
+ out += renderer.codespan(token);
+ break;
+ }
+ case 'br': {
+ out += renderer.br(token);
+ break;
+ }
+ case 'del': {
+ out += renderer.del(token);
+ break;
+ }
+ case 'text': {
+ out += renderer.text(token);
+ break;
+ }
+ default: {
+ const errMsg = 'Token with "' + token.type + '" type was not found.';
+ if (this.options.silent) {
+ console.error(errMsg);
+ return '';
+ }
+ else {
+ throw new Error(errMsg);
+ }
+ }
+ }
+ }
+ return out;
+ }
+}
+
+class _Hooks {
+ options;
+ block;
+ constructor(options) {
+ this.options = options || _defaults;
+ }
+ static passThroughHooks = new Set([
+ 'preprocess',
+ 'postprocess',
+ 'processAllTokens',
+ ]);
+ /**
+ * Process markdown before marked
+ */
+ preprocess(markdown) {
+ return markdown;
+ }
+ /**
+ * Process HTML after marked is finished
+ */
+ postprocess(html) {
+ return html;
+ }
+ /**
+ * Process all tokens before walk tokens
+ */
+ processAllTokens(tokens) {
+ return tokens;
+ }
+ /**
+ * Provide function to tokenize markdown
+ */
+ provideLexer() {
+ return this.block ? _Lexer.lex : _Lexer.lexInline;
+ }
+ /**
+ * Provide function to parse tokens
+ */
+ provideParser() {
+ return this.block ? _Parser.parse : _Parser.parseInline;
+ }
+}
+
+class Marked {
+ defaults = _getDefaults();
+ options = this.setOptions;
+ parse = this.parseMarkdown(true);
+ parseInline = this.parseMarkdown(false);
+ Parser = _Parser;
+ Renderer = _Renderer;
+ TextRenderer = _TextRenderer;
+ Lexer = _Lexer;
+ Tokenizer = _Tokenizer;
+ Hooks = _Hooks;
+ constructor(...args) {
+ this.use(...args);
+ }
+ /**
+ * Run callback for every token
+ */
+ walkTokens(tokens, callback) {
+ let values = [];
+ for (const token of tokens) {
+ values = values.concat(callback.call(this, token));
+ switch (token.type) {
+ case 'table': {
+ const tableToken = token;
+ for (const cell of tableToken.header) {
+ values = values.concat(this.walkTokens(cell.tokens, callback));
+ }
+ for (const row of tableToken.rows) {
+ for (const cell of row) {
+ values = values.concat(this.walkTokens(cell.tokens, callback));
+ }
+ }
+ break;
+ }
+ case 'list': {
+ const listToken = token;
+ values = values.concat(this.walkTokens(listToken.items, callback));
+ break;
+ }
+ default: {
+ const genericToken = token;
+ if (this.defaults.extensions?.childTokens?.[genericToken.type]) {
+ this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => {
+ const tokens = genericToken[childTokens].flat(Infinity);
+ values = values.concat(this.walkTokens(tokens, callback));
+ });
+ }
+ else if (genericToken.tokens) {
+ values = values.concat(this.walkTokens(genericToken.tokens, callback));
+ }
+ }
+ }
+ }
+ return values;
+ }
+ use(...args) {
+ const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} };
+ args.forEach((pack) => {
+ // copy options to new object
+ const opts = { ...pack };
+ // set async to true if it was set to true before
+ opts.async = this.defaults.async || opts.async || false;
+ // ==-- Parse "addon" extensions --== //
+ if (pack.extensions) {
+ pack.extensions.forEach((ext) => {
+ if (!ext.name) {
+ throw new Error('extension name required');
+ }
+ if ('renderer' in ext) { // Renderer extensions
+ const prevRenderer = extensions.renderers[ext.name];
+ if (prevRenderer) {
+ // Replace extension with func to run new extension but fall back if false
+ extensions.renderers[ext.name] = function (...args) {
+ let ret = ext.renderer.apply(this, args);
+ if (ret === false) {
+ ret = prevRenderer.apply(this, args);
+ }
+ return ret;
+ };
+ }
+ else {
+ extensions.renderers[ext.name] = ext.renderer;
+ }
+ }
+ if ('tokenizer' in ext) { // Tokenizer Extensions
+ if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {
+ throw new Error("extension level must be 'block' or 'inline'");
+ }
+ const extLevel = extensions[ext.level];
+ if (extLevel) {
+ extLevel.unshift(ext.tokenizer);
+ }
+ else {
+ extensions[ext.level] = [ext.tokenizer];
+ }
+ if (ext.start) { // Function to check for start of token
+ if (ext.level === 'block') {
+ if (extensions.startBlock) {
+ extensions.startBlock.push(ext.start);
+ }
+ else {
+ extensions.startBlock = [ext.start];
+ }
+ }
+ else if (ext.level === 'inline') {
+ if (extensions.startInline) {
+ extensions.startInline.push(ext.start);
+ }
+ else {
+ extensions.startInline = [ext.start];
+ }
+ }
+ }
+ }
+ if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens
+ extensions.childTokens[ext.name] = ext.childTokens;
+ }
+ });
+ opts.extensions = extensions;
+ }
+ // ==-- Parse "overwrite" extensions --== //
+ if (pack.renderer) {
+ const renderer = this.defaults.renderer || new _Renderer(this.defaults);
+ for (const prop in pack.renderer) {
+ if (!(prop in renderer)) {
+ throw new Error(`renderer '${prop}' does not exist`);
+ }
+ if (['options', 'parser'].includes(prop)) {
+ // ignore options property
+ continue;
+ }
+ const rendererProp = prop;
+ const rendererFunc = pack.renderer[rendererProp];
+ const prevRenderer = renderer[rendererProp];
+ // Replace renderer with func to run extension, but fall back if false
+ renderer[rendererProp] = (...args) => {
+ let ret = rendererFunc.apply(renderer, args);
+ if (ret === false) {
+ ret = prevRenderer.apply(renderer, args);
+ }
+ return ret || '';
+ };
+ }
+ opts.renderer = renderer;
+ }
+ if (pack.tokenizer) {
+ const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults);
+ for (const prop in pack.tokenizer) {
+ if (!(prop in tokenizer)) {
+ throw new Error(`tokenizer '${prop}' does not exist`);
+ }
+ if (['options', 'rules', 'lexer'].includes(prop)) {
+ // ignore options, rules, and lexer properties
+ continue;
+ }
+ const tokenizerProp = prop;
+ const tokenizerFunc = pack.tokenizer[tokenizerProp];
+ const prevTokenizer = tokenizer[tokenizerProp];
+ // Replace tokenizer with func to run extension, but fall back if false
+ // @ts-expect-error cannot type tokenizer function dynamically
+ tokenizer[tokenizerProp] = (...args) => {
+ let ret = tokenizerFunc.apply(tokenizer, args);
+ if (ret === false) {
+ ret = prevTokenizer.apply(tokenizer, args);
+ }
+ return ret;
+ };
+ }
+ opts.tokenizer = tokenizer;
+ }
+ // ==-- Parse Hooks extensions --== //
+ if (pack.hooks) {
+ const hooks = this.defaults.hooks || new _Hooks();
+ for (const prop in pack.hooks) {
+ if (!(prop in hooks)) {
+ throw new Error(`hook '${prop}' does not exist`);
+ }
+ if (['options', 'block'].includes(prop)) {
+ // ignore options and block properties
+ continue;
+ }
+ const hooksProp = prop;
+ const hooksFunc = pack.hooks[hooksProp];
+ const prevHook = hooks[hooksProp];
+ if (_Hooks.passThroughHooks.has(prop)) {
+ // @ts-expect-error cannot type hook function dynamically
+ hooks[hooksProp] = (arg) => {
+ if (this.defaults.async) {
+ return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => {
+ return prevHook.call(hooks, ret);
+ });
+ }
+ const ret = hooksFunc.call(hooks, arg);
+ return prevHook.call(hooks, ret);
+ };
+ }
+ else {
+ // @ts-expect-error cannot type hook function dynamically
+ hooks[hooksProp] = (...args) => {
+ let ret = hooksFunc.apply(hooks, args);
+ if (ret === false) {
+ ret = prevHook.apply(hooks, args);
+ }
+ return ret;
+ };
+ }
+ }
+ opts.hooks = hooks;
+ }
+ // ==-- Parse WalkTokens extensions --== //
+ if (pack.walkTokens) {
+ const walkTokens = this.defaults.walkTokens;
+ const packWalktokens = pack.walkTokens;
+ opts.walkTokens = function (token) {
+ let values = [];
+ values.push(packWalktokens.call(this, token));
+ if (walkTokens) {
+ values = values.concat(walkTokens.call(this, token));
+ }
+ return values;
+ };
+ }
+ this.defaults = { ...this.defaults, ...opts };
+ });
+ return this;
+ }
+ setOptions(opt) {
+ this.defaults = { ...this.defaults, ...opt };
+ return this;
+ }
+ lexer(src, options) {
+ return _Lexer.lex(src, options ?? this.defaults);
+ }
+ parser(tokens, options) {
+ return _Parser.parse(tokens, options ?? this.defaults);
+ }
+ parseMarkdown(blockType) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const parse = (src, options) => {
+ const origOpt = { ...options };
+ const opt = { ...this.defaults, ...origOpt };
+ const throwError = this.onError(!!opt.silent, !!opt.async);
+ // throw error if an extension set async to true but parse was called with async: false
+ if (this.defaults.async === true && origOpt.async === false) {
+ return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.'));
+ }
+ // throw error in case of non string input
+ if (typeof src === 'undefined' || src === null) {
+ return throwError(new Error('marked(): input parameter is undefined or null'));
+ }
+ if (typeof src !== 'string') {
+ return throwError(new Error('marked(): input parameter is of type '
+ + Object.prototype.toString.call(src) + ', string expected'));
+ }
+ if (opt.hooks) {
+ opt.hooks.options = opt;
+ opt.hooks.block = blockType;
+ }
+ const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);
+ const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);
+ if (opt.async) {
+ return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
+ .then(src => lexer(src, opt))
+ .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens)
+ .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens)
+ .then(tokens => parser(tokens, opt))
+ .then(html => opt.hooks ? opt.hooks.postprocess(html) : html)
+ .catch(throwError);
+ }
+ try {
+ if (opt.hooks) {
+ src = opt.hooks.preprocess(src);
+ }
+ let tokens = lexer(src, opt);
+ if (opt.hooks) {
+ tokens = opt.hooks.processAllTokens(tokens);
+ }
+ if (opt.walkTokens) {
+ this.walkTokens(tokens, opt.walkTokens);
+ }
+ let html = parser(tokens, opt);
+ if (opt.hooks) {
+ html = opt.hooks.postprocess(html);
+ }
+ return html;
+ }
+ catch (e) {
+ return throwError(e);
+ }
+ };
+ return parse;
+ }
+ onError(silent, async) {
+ return (e) => {
+ e.message += '\nPlease report this to https://github.com/markedjs/marked.';
+ if (silent) {
+ const msg = '
'
+ + escape$1(e.message + '', true)
+ + '
';
+ if (async) {
+ return Promise.resolve(msg);
+ }
+ return msg;
+ }
+ if (async) {
+ return Promise.reject(e);
+ }
+ throw e;
+ };
+ }
+}
+
+const markedInstance = new Marked();
+function marked(src, opt) {
+ return markedInstance.parse(src, opt);
+}
+/**
+ * Sets the default options.
+ *
+ * @param options Hash of options
+ */
+marked.options =
+ marked.setOptions = function (options) {
+ markedInstance.setOptions(options);
+ marked.defaults = markedInstance.defaults;
+ changeDefaults(marked.defaults);
+ return marked;
+ };
+/**
+ * Gets the original marked default options.
+ */
+marked.getDefaults = _getDefaults;
+marked.defaults = _defaults;
+/**
+ * Use Extension
+ */
+marked.use = function (...args) {
+ markedInstance.use(...args);
+ marked.defaults = markedInstance.defaults;
+ changeDefaults(marked.defaults);
+ return marked;
+};
+/**
+ * Run callback for every token
+ */
+marked.walkTokens = function (tokens, callback) {
+ return markedInstance.walkTokens(tokens, callback);
+};
+/**
+ * Compiles markdown to HTML without enclosing `p` tag.
+ *
+ * @param src String of markdown source to be compiled
+ * @param options Hash of options
+ * @return String of compiled HTML
+ */
+marked.parseInline = markedInstance.parseInline;
+/**
+ * Expose
+ */
+marked.Parser = _Parser;
+marked.parser = _Parser.parse;
+marked.Renderer = _Renderer;
+marked.TextRenderer = _TextRenderer;
+marked.Lexer = _Lexer;
+marked.lexer = _Lexer.lex;
+marked.Tokenizer = _Tokenizer;
+marked.Hooks = _Hooks;
+marked.parse = marked;
+const options = marked.options;
+const setOptions = marked.setOptions;
+const use = marked.use;
+const walkTokens = marked.walkTokens;
+const parseInline = marked.parseInline;
+const parse = marked;
+const parser = _Parser.parse;
+const lexer = _Lexer.lex;
+
+export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens };
+//# sourceMappingURL=marked.esm.js.map
\ No newline at end of file
diff --git a/web/model-manager.js b/web/model-manager.js
index 6eb9e52..d771a3a 100644
--- a/web/model-manager.js
+++ b/web/model-manager.js
@@ -2,7 +2,8 @@ import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { ComfyButton } from "../../scripts/ui/components/button.js";
-import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
+import { marked } from "./marked.js";
+import("./downshow.js");
function clamp(x, min, max) {
return Math.min(Math.max(x, min), max);
@@ -2707,7 +2708,7 @@ class ModelInfo {
const notesElement = this.elements.tabContents[3]; // TODO: remove magic value
notesElement.innerHTML = "";
const markdown = $el("div", {}, "");
- markdown.innerHTML = marked.parse(noteText);
+ markdown.innerHTML = marked.parse(noteText);
notesElement.append.apply(notesElement,
(() => {
@@ -3175,31 +3176,7 @@ async function getModelInfos(urlText) {
version["description"],
civitaiInfo["description"] !== undefined ? "# " + name : undefined,
civitaiInfo["description"],
- ].filter(x => x !== undefined).join("\n\n")
- .replaceAll("
", "\n\n") - .replaceAll("", "**").replaceAll("", "**") - .replaceAll("
", "`").replaceAll("", "`")
- .replaceAll("", "\n") - .replaceAll("
", "\n\n---\n\n") - .replaceAll("", "\n") - .replaceAll("
", "\n") - .replaceAll("
", "\n") - .replaceAll("
", "\n") - .replaceAll("
", "\n") - .replaceAll("
", "\n") - .replace(/href="(\S*)">/g, 'href=""> $1 ') - .replace(/src="(\S*)">/g, 'src=""> $1
') - // - // - .replace(/<[^>]+>/g, "") // quick hack - .replaceAll("<", "<").replaceAll(">", ">") - .replaceAll("<e;", "<=").replaceAll(">e;", ">=") - .replaceAll("&", "&"); + ].filter(x => x !== undefined).join("\n\n"); version["files"].forEach((file) => { infos.push({ "images": images, @@ -3207,7 +3184,7 @@ async function getModelInfos(urlText) { "modelType": type, "downloadUrl": file["downloadUrl"], "downloadFilePath": "", - "description": description, + "description": downshow(description), "details": { "fileSizeKB": file["sizeKB"], "fileType": file["type"], From ab7c62e929c29ceefe140f5886d7b2a36fc3150e Mon Sep 17 00:00:00 2001 From: korutech-ai
Date: Wed, 28 Aug 2024 07:48:30 +1200 Subject: [PATCH 4/9] DEVCONFIG: Adding macOS ignore settings. --- .gitignore | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.gitignore b/.gitignore index 32b09a4..542369d 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,31 @@ cython_debug/ #.idea/ ui_settings.yaml server_settings.yaml + +# macOS: +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk From 1bedfaa7a25162aa01cbcbfb84d6a42bf35378ec Mon Sep 17 00:00:00 2001 From: korutech-ai Date: Wed, 28 Aug 2024 08:14:39 +1200 Subject: [PATCH 5/9] LINT: No code change, just whitespace cleanup. --- web/model-manager.js | 550 +++++++++++++++++++++---------------------- 1 file changed, 275 insertions(+), 275 deletions(-) diff --git a/web/model-manager.js b/web/model-manager.js index d771a3a..498b098 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -41,16 +41,16 @@ function debounce(callback, delay) { class KeyComboListener { /** @type {string[]} */ #keyCodes = []; - + /** @type {() => Promise } */ action; - + /** @type {Element} */ element; - + /** @type {string[]} */ #combo = []; - + /** * @param {string[]} keyCodes * @param {() => Promise } action @@ -60,7 +60,7 @@ class KeyComboListener { this.#keyCodes = keyCodes; this.action = action; this.element = element; - + document.addEventListener("keydown", (e) => { const code = e.code; const keyCodes = this.#keyCodes; @@ -161,21 +161,21 @@ const modelNodeType = { const MODEL_EXTENSIONS = [".bin", ".ckpt", "gguf", ".onnx", ".pt", ".pth", ".safetensors"]; // TODO: ask server for? const IMAGE_EXTENSIONS = [ - ".png", - ".webp", - ".jpeg", - ".jpg", - ".jfif", - ".gif", - ".apng", + ".png", + ".webp", + ".jpeg", + ".jpg", + ".jfif", + ".gif", + ".apng", - ".preview.png", - ".preview.webp", - ".preview.jpeg", - ".preview.jpg", - ".preview.jfif", - ".preview.gif", - ".preview.apng", + ".preview.png", + ".preview.webp", + ".preview.jpeg", + ".preview.jpg", + ".preview.jfif", + ".preview.gif", + ".preview.apng", ]; // TODO: /model-manager/image/extensions /** @@ -267,7 +267,7 @@ const PREVIEW_THUMBNAIL_WIDTH = 320; const PREVIEW_THUMBNAIL_HEIGHT = 480; /** - * + * * @param {HTMLButtonElement} element * @returns {[HTMLButtonElement | undefined, HTMLElement | undefined, HTMLSpanElement | undefined]} [button, icon, span] */ @@ -304,18 +304,18 @@ function comfyButtonDisambiguate(element) { */ function comfyButtonAlert(element, success, successClassName = undefined, failureClassName = undefined, disableCallback = false) { if (element === undefined || element === null) { return; } - + const [button, icon, span] = comfyButtonDisambiguate(element); if (button === undefined) { console.warn("Unable to find button element!"); console.warn(element); return; } - + // TODO: debounce would be nice, but needs some sort of "global" to avoid creating/destroying many objects - + const colorClassName = success ? "comfy-button-success" : "comfy-button-failure"; - + if (icon) { const iconClassName = (success ? successClassName : failureClassName) ?? ""; if (iconClassName !== "") { @@ -331,7 +331,7 @@ function comfyButtonAlert(element, success, successClassName = undefined, failur }, 1000, icon, iconClassName, colorClassName); } } - + button.classList.add(colorClassName); if (!disableCallback) { window.setTimeout((element, colorClassName) => { @@ -341,7 +341,7 @@ function comfyButtonAlert(element, success, successClassName = undefined, failur } /** - * + * * @param {string} modelPath * @param {string} newValue * @returns {Promise } @@ -424,11 +424,11 @@ function $select(x = { $: (el) => {}, textContent: "", options: [""] }) { */ function $radioGroup(attr) { const { name = Date.now(), onchange, options = [], $ } = attr; - + /** @type {HTMLDivElement[]} */ const radioGroup = options.map((item, index) => { const inputRef = { value: null }; - + return $el( "div.comfy-radio", { onclick: () => inputRef.value.click() }, @@ -444,7 +444,7 @@ function $radioGroup(attr) { ] ); }); - + const element = $el("input", { name: name + "-group", value: options[0]?.value, @@ -458,7 +458,7 @@ function $radioGroup(attr) { onchange?.(selectedValue); }); }); - + return $el("div.comfy-radio-group", radioGroup); } @@ -516,7 +516,7 @@ function GenerateTabGroup(tabData) { tabButtons.push(tab); tabContents.push(content); }); - + return [tabButtons, tabContents]; } @@ -578,28 +578,28 @@ class ImageSelect { /** @constant {string} */ #PREVIEW_UPLOAD = "Upload"; /** @constant {string} */ #PREVIEW_URL = "URL"; /** @constant {string} */ #PREVIEW_NONE = "No Preview"; - + elements = { /** @type {HTMLDivElement} */ radioGroup: null, /** @type {HTMLDivElement} */ radioButtons: null, /** @type {HTMLDivElement} */ previews: null, - + /** @type {HTMLImageElement} */ defaultPreviewNoImage: null, /** @type {HTMLDivElement} */ defaultPreviews: null, /** @type {HTMLDivElement} */ defaultUrl: null, - + /** @type {HTMLImageElement} */ customUrlPreview: null, /** @type {HTMLInputElement} */ customUrl: null, /** @type {HTMLDivElement} */ custom: null, - + /** @type {HTMLImageElement} */ uploadPreview: null, /** @type {HTMLInputElement} */ uploadFile: null, /** @type {HTMLDivElement} */ upload: null, }; - + /** @type {string} */ #name = null; - + /** @returns {Promise | Promise } */ async getImage() { const name = this.#name; @@ -651,7 +651,7 @@ class ImageSelect { } return ""; } - + /** @returns {void} */ resetModelInfoPreview() { let noimage = this.elements.defaultUrl.dataset.noimage; @@ -680,7 +680,7 @@ class ImageSelect { this.elements.upload.style.display = "none"; this.elements.custom.style.display = "none"; } - + /** @returns {boolean} */ defaultIsChecked() { const children = this.elements.radioButtons.children; @@ -693,7 +693,7 @@ class ImageSelect { }; return false; } - + /** @returns {void} */ checkDefault() { const children = this.elements.radioButtons.children; @@ -707,9 +707,9 @@ class ImageSelect { } }; } - + /** - * @param {1 | -1} step + * @param {1 | -1} step */ stepDefaultPreviews(step) { const children = this.elements.defaultPreviews.children; @@ -730,7 +730,7 @@ class ImageSelect { else if (currentIndex < 0) { currentIndex = children.length - 1; } children[currentIndex].style.display = "block"; } - + /** * @param {string} radioGroupName - Should be unique for every radio group. * @param {string[]|undefined} defaultPreviews @@ -740,20 +740,20 @@ class ImageSelect { defaultPreviews = [PREVIEW_NONE_URI]; } this.#name = radioGroupName; - + const el_defaultUri = $el("div", { $: (el) => (this.elements.defaultUrl = el), style: { display: "none" }, "data-noimage": PREVIEW_NONE_URI, }); - + const el_defaultPreviewNoImage = $el("img", { $: (el) => (this.elements.defaultPreviewNoImage = el), loading: "lazy", /* `loading` BEFORE `src`; Known bug in Firefox 124.0.2 and Safari for iOS 17.4.1 (https://stackoverflow.com/a/76252772) */ src: PREVIEW_NONE_URI, style: { display: "none" }, }); - + const el_defaultPreviews = $el("div", { $: (el) => (this.elements.defaultPreviews = el), style: { @@ -776,7 +776,7 @@ class ImageSelect { } return imgs; })()); - + const el_uploadPreview = $el("img", { $: (el) => (this.elements.uploadPreview = el), src: PREVIEW_NONE_URI, @@ -806,9 +806,9 @@ class ImageSelect { }, [ el_uploadFile, ]); - + /** - * @param {string} url + * @param {string} url * @returns {Promise } */ const getCustomPreviewUrl = async (url) => { @@ -833,7 +833,7 @@ class ImageSelect { return url; } }; - + const el_customUrlPreview = $el("img", { $: (el) => (this.elements.customUrlPreview = el), src: PREVIEW_NONE_URI, @@ -877,7 +877,7 @@ class ImageSelect { }, }).element, ]); - + const el_previewButtons = $el("div.model-preview-overlay", { style: { display: el_defaultPreviews.children.length > 1 ? "block" : "none", @@ -914,20 +914,20 @@ class ImageSelect { ), el_previewButtons, ]); - + const el_radioButtons = $radioGroup({ name: radioGroupName, onchange: (value) => { el_custom.style.display = "none"; el_upload.style.display = "none"; - + el_defaultPreviews.style.display = "none"; el_previewButtons.style.display = "none"; - + el_defaultPreviewNoImage.style.display = "none"; el_uploadPreview.style.display = "none"; el_customUrlPreview.style.display = "none"; - + switch (value) { case this.#PREVIEW_DEFAULT: el_defaultPreviews.style.display = "block"; @@ -957,7 +957,7 @@ class ImageSelect { }), }); this.elements.radioButtons = el_radioButtons; - + const children = el_radioButtons.children; for (let i = 0; i < children.length; i++) { const child = children[i]; @@ -967,7 +967,7 @@ class ImageSelect { break; } }; - + const el_radioGroup = $el("div.model-preview-select-radio-container", { $: (el) => (this.elements.radioGroup = el), }, [ @@ -981,7 +981,7 @@ class ImageSelect { } /** - * @typedef {Object} DirectoryItem + * @typedef {Object} DirectoryItem * @property {String} name * @property {number | undefined} childCount * @property {number | undefined} childIndex @@ -1084,7 +1084,7 @@ class ModelDirectories { } return index + start; } - + /** * Returns a list of matching search results and valid path. * @param {string} filter @@ -1167,16 +1167,16 @@ const DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS = "search-directory-dropdown-mous class ModelData { /** @type {string} */ searchSeparator = "/"; // TODO: other client or server code may be assuming this to always be "/" - + /** @type {string} */ systemSeparator = null; - + /** @type {Object} */ models = {}; - + /** @type {ModelDirectories} */ directories = null; - + constructor() { this.directories = new ModelDirectories(); } @@ -1185,34 +1185,34 @@ class ModelData { class DirectoryDropdown { /** @type {HTMLDivElement} */ element = null; - + /** @type {Boolean} */ showDirectoriesOnly = false; - + /** @type {HTMLInputElement} */ #input = null; - + /** @type {() => string} */ #getModelType = null; - + /** @type {ModelData} */ #modelData = null; // READ ONLY - + /** @type {() => void} */ #updateCallback = null; - + /** @type {() => Promise } */ #submitCallback = null; - + /** @type {string} */ #deepestPreviousPath = "/"; - + /** @type {Any} */ #touchSelectionStart = null; - + /** @type {() => Boolean} */ #isDynamicSearch = () => { return false; }; - + /** * @param {ModelData} modelData * @param {HTMLInputElement} input @@ -1237,7 +1237,7 @@ class DirectoryDropdown { this.#submitCallback = submitCallback; this.showDirectoriesOnly = showDirectoriesOnly; this.#isDynamicSearch = isDynamicSearch; - + input.addEventListener("input", async(e) => { const path = this.#updateOptions(); if (path !== undefined) { @@ -1401,7 +1401,7 @@ class DirectoryDropdown { }, ); } - + /** * @param {HTMLInputElement} input * @param {HTMLParagraphElement | undefined | null} selection @@ -1457,7 +1457,7 @@ class DirectoryDropdown { if (i1 !== -1) { name = name.substring(0, i1); } - + const dropdown = this.element; const options = dropdown.children; let iSelection; @@ -1615,7 +1615,7 @@ class ModelGrid { static modelWidgetIndex(nodeType) { return nodeType === undefined ? -1 : 0; } - + /** * @param {string} text * @param {string} file @@ -1630,7 +1630,7 @@ class ModelGrid { const sep = text.length === 0 || text.slice(-1).match(/\s/) ? "" : " "; return text + sep + "(embedding:" + name + ":1.0)"; } - + /** * @param {Array} list * @param {string} searchString @@ -1666,7 +1666,7 @@ class ModelGrid { }, true); }); } - + /** * In-place sort. Returns an array alias. * @param {Array} list @@ -1696,7 +1696,7 @@ class ModelGrid { const sorted = list.sort(compareFn); return reverse ? sorted.reverse() : sorted; } - + /** * @param {Event} event * @param {string} modelType @@ -1775,7 +1775,7 @@ class ModelGrid { if (modelType !== "embeddings" && target.id === "graph-canvas") { //const pos = app.canvas.convertEventToCanvasOffset(event); const pos = app.canvas.convertEventToCanvasOffset({ clientX: clientX, clientY: clientY }); - + const node = app.graph.getNodeOnPos(pos[0], pos[1], app.canvas.visible_nodes); let widgetIndex = -1; @@ -1835,7 +1835,7 @@ class ModelGrid { } } } - + /** * @param {Event} event * @param {string} modelType @@ -1870,7 +1870,7 @@ class ModelGrid { } comfyButtonAlert(event.target, success, "mdi-check-bold", "mdi-close-thick"); } - + /** * @param {Array} models * @param {string} modelType @@ -2001,7 +2001,7 @@ class ModelGrid { return [$el("h2", ["No Models"])]; } } - + /** * @param {HTMLDivElement} modelGrid * @param {ModelData} modelData @@ -2067,7 +2067,7 @@ class ModelGrid { class ModelInfo { /** @type {HTMLDivElement} */ element = null; - + elements = { /** @type {HTMLDivElement[]} */ tabButtons: null, /** @type {HTMLDivElement[]} */ tabContents: null, @@ -2076,16 +2076,16 @@ class ModelInfo { /** @type {HTMLButtonElement} */ setPreviewButton: null, /** @type {HTMLInputElement} */ moveDestinationInput: null, }; - + /** @type {ImageSelect} */ previewSelect = null; - + /** @type {string} */ #savedNotesValue = null; - + /** @type {[HTMLElement][]} */ #settingsElements = null; - + /** * @param {ModelData} modelData * @param {() => Promise } updateModels @@ -2100,17 +2100,17 @@ class ModelInfo { value: modelData.searchSeparator, }); this.elements.moveDestinationInput = moveDestinationInput; - + const searchDropdown = new DirectoryDropdown( modelData, moveDestinationInput, true, ); - + const previewSelect = new ImageSelect("model-info-preview-model-FYUIKMNVB"); this.previewSelect = previewSelect; previewSelect.elements.previews.style.display = "flex"; - + const setPreviewButton = new ComfyButton({ tooltip: "Overwrite currrent preview with selected image", content: "Set as Preview", @@ -2182,7 +2182,7 @@ class ModelInfo { previewSelect.elements.radioButtons.addEventListener("change", (e) => { setPreviewButton.style.display = previewSelect.defaultIsChecked() ? "none" : "block"; }); - + this.element = $el("div", { style: { display: "none" }, }, [ @@ -2294,7 +2294,7 @@ class ModelInfo { "data-path": "", }), ]); - + [this.elements.tabButtons, this.elements.tabContents] = GenerateTabGroup([ { name: "Overview", icon: "information-box-outline", tabContent: this.element }, { name: "Metadata", icon: "file-document-outline", tabContent: $el("div", ["Metadata"]) }, @@ -2302,13 +2302,13 @@ class ModelInfo { { name: "Notes", icon: "pencil-outline", tabContent: $el("div", ["Notes"]) }, ]); } - + /** @returns {void} */ show() { this.element.style = ""; this.element.scrollTop = 0; } - + /** * @param {boolean} promptUser * @returns {Promise } @@ -2317,7 +2317,7 @@ class ModelInfo { if (this.element.style.display === "none") { return true; } - + const noteValue = this.elements.notes.value; const savedNotesValue = this.#savedNotesValue; if (noteValue.trim() === savedNotesValue.trim()) { @@ -2345,7 +2345,7 @@ class ModelInfo { } return true; } - + /** * @param {boolean?} promptSave * @returns {Promise } @@ -2363,7 +2363,7 @@ class ModelInfo { this.element.style.display = "none"; return true; } - + /** * @param {string} searchPath * @param {() => Promise } updateModels @@ -2469,7 +2469,7 @@ class ModelInfo { ]), ); } - + const fileDirectory = info["File Directory"]; if (fileDirectory !== undefined && fileDirectory !== null && fileDirectory !== "") { this.elements.moveDestinationInput.placeholder = fileDirectory @@ -2479,7 +2479,7 @@ class ModelInfo { this.elements.moveDestinationInput.placeholder = searchSeparator; this.elements.moveDestinationInput.value = searchSeparator; } - + const previewSelect = this.previewSelect; const defaultUrl = previewSelect.elements.defaultUrl; if (info["Preview"]) { @@ -2493,7 +2493,7 @@ class ModelInfo { previewSelect.resetModelInfoPreview(); const setPreviewButton = this.elements.setPreviewButton; setPreviewButton.style.display = previewSelect.defaultIsChecked() ? "none" : "block"; - + innerHtml.push($el("div", [ previewSelect.elements.previews, $el("div.row.tab-header", [ @@ -2522,7 +2522,7 @@ class ModelInfo { if (value === undefined || value === null) { continue; } - + if (Array.isArray(value)) { // currently only used for "Bucket Resolutions" if (value.length > 0) { @@ -2556,7 +2556,7 @@ class ModelInfo { ])); infoHtml.append.apply(infoHtml, innerHtml); // TODO: set default value of dropdown and value to model type? - + /** @type {HTMLDivElement} */ const metadataElement = this.elements.tabContents[1]; // TODO: remove magic value const isMetadata = typeof metadata === 'object' && metadata !== null && Object.keys(metadata).length > 0; @@ -2584,7 +2584,7 @@ class ModelInfo { ]); const metadataButton = this.elements.tabButtons[1]; // TODO: remove magic value metadataButton.style.display = isMetadata ? "" : "none"; - + /** @type {HTMLDivElement} */ const tagsElement = this.elements.tabContents[2]; // TODO: remove magic value const isTags = Array.isArray(tags) && tags.length > 0; @@ -2676,10 +2676,10 @@ class ModelInfo { ]); const tagButton = this.elements.tabButtons[2]; // TODO: remove magic value tagButton.style.display = isTags ? "" : "none"; - + const saveIcon = "content-save"; const savingIcon = "cloud-upload-outline"; - + const saveNotesButton = new ComfyButton({ icon: saveIcon, tooltip: "Save note", @@ -2692,7 +2692,7 @@ class ModelInfo { button.disabled = false; }, }).element; - + const saveDebounce = debounce(async() => { const saveIconClass = "mdi-" + saveIcon; const savingIconClass = "mdi-" + savingIcon; @@ -2703,7 +2703,7 @@ class ModelInfo { iconElement.classList.remove(savingIconClass); iconElement.classList.add(saveIconClass); }, 1000); - + /** @type {HTMLDivElement} */ const notesElement = this.elements.tabContents[3]; // TODO: remove magic value notesElement.innerHTML = ""; @@ -2721,7 +2721,7 @@ class ModelInfo { } }, }); - + if (navigator.userAgent.includes("Mac")) { new KeyComboListener( ["MetaLeft", "KeyS"], @@ -2746,7 +2746,7 @@ class ModelInfo { notes, ); } - + this.elements.notes = notes; this.elements.markdown = markdown; this.#savedNotesValue = noteText; @@ -2767,7 +2767,7 @@ class ModelInfo { })() ); } - + static UniformTagSampling(tagsAndCounts, sampleCount, frequencyThreshold = 0) { const data = tagsAndCounts.filter(x => x[1] >= frequencyThreshold); let count = data.length; @@ -2782,7 +2782,7 @@ class ModelInfo { const sortedSamples = samples.sort((x1, x2) => { return parseInt(x2[1]) - parseInt(x1[1]) }); return sortedSamples.map(x => x[0]); } - + static ProbabilisticTagSampling(tagsAndCounts, sampleCount, frequencyThreshold = 0) { const data = tagsAndCounts.filter(x => x[1] >= frequencyThreshold); let tagFrequenciesSum = data.reduce((accumulator, x) => accumulator + x[1], 0); @@ -2832,7 +2832,7 @@ class Civitai { return {}; } } - + /** * Extract file information from the given model version information. * @@ -2849,21 +2849,21 @@ class Civitai { const modelVersionFiles = modelVersionInfo["files"]; for (let i = 0; i < modelVersionFiles.length; i++) { const modelVersionFile = modelVersionFiles[i]; - + const fileType = modelVersionFile["type"]; if (type instanceof String && type != fileType) { continue; } - + const fileMeta = modelVersionFile["metadata"]; - + const fileFp = fileMeta["fp"]; if (fp instanceof String && fp != fileFp) { continue; } - + const fileSize = fileMeta["size"]; if (size instanceof String && size != fileSize) { continue; } - + const fileFormat = fileMeta["format"]; if (format instanceof String && format != fileFormat) { continue; } - + files.push({ "downloadUrl": modelVersionFile["downloadUrl"], "format": fileFormat, @@ -2887,7 +2887,7 @@ class Civitai { "tags": modelVersionInfo["trainedWords"], }; } - + /** * @param {string} stringUrl - Model url. * @@ -2961,21 +2961,21 @@ class Civitai { return {}; } } - + /** * @returns {string} */ static imagePostUrlPrefix() { return "https://civitai.com/images/"; } - + /** * @returns {string} */ static imageUrlPrefix() { return "https://image.civitai.com/"; } - + /** * @param {string} stringUrl - https://civitai.com/images/{imageId}. * @@ -2998,7 +2998,7 @@ class Civitai { return {}; } } - + /** * @param {string} stringUrl - https://image.civitai.com/... * @@ -3054,9 +3054,9 @@ class HuggingFace { return {}; } } - + /** - * + * * * @param {string} stringUrl - Model url. * @@ -3080,13 +3080,13 @@ class HuggingFace { } const modelId = urlPath.substring(i0, i2); const urlPathEnd = urlPath.substring(i2); - + const isValidBranch = ( urlPathEnd.startsWith("/resolve") || urlPathEnd.startsWith("/blob") || urlPathEnd.startsWith("/tree") ); - + let branch = "/main"; let filePath = ""; if (isValidBranch) { @@ -3105,11 +3105,11 @@ class HuggingFace { } } } - + const modelInfo = await HuggingFace.requestInfo(modelId); //const modelInfo = await requestInfo(modelId + "/tree" + branch); // this only gives you the files at the given branch path... // oid: SHA-1?, lfs.oid: SHA-256 - + const clippedFilePath = filePath.substring(filePath[0] === "/" ? 1 : 0); const modelFiles = modelInfo["siblings"].filter((sib) => { const filename = sib["rfilename"]; @@ -3126,9 +3126,9 @@ class HuggingFace { if (modelFiles.length === 0) { return {}; } - + const baseDownloadUrl = url.origin + urlPath.substring(0, i2) + "/resolve" + branch; - + const images = modelInfo["siblings"].filter((sib) => { const filename = sib["rfilename"]; for (let i = 0; i < IMAGE_EXTENSIONS.length; i++) { @@ -3140,7 +3140,7 @@ class HuggingFace { }).map((sib) => { return baseDownloadUrl + "/" + sib["rfilename"]; }); - + return { "baseDownloadUrl": baseDownloadUrl, "modelFiles": modelFiles, @@ -3257,7 +3257,7 @@ async function getModelInfos(urlText) { class DownloadView { /** @type {HTMLDivElement} */ element = null; - + elements = { /** @type {HTMLInputElement} */ url: null, /** @type {HTMLDivElement} */ infos: null, @@ -3266,16 +3266,16 @@ class DownloadView { /** @type {HTMLButtonElement} */ searchButton: null, /** @type {HTMLButtonElement} */ clearSearchButton: null, }; - + /** @type {DOMParser} */ #domParser = null; - + /** @type {Object. } */ #settings = null; - + /** @type {() => Promise } */ #updateModels = () => {}; - + /** * @param {ModelData} modelData * @param {Object. } settings @@ -3291,7 +3291,7 @@ class DownloadView { $el("h1", ["Input a URL to select a model to download."]) ); }; - + const searchButton = new ComfyButton({ icon: "magnify", tooltip: "Search url", @@ -3317,7 +3317,7 @@ class DownloadView { searchButton.style.display = hideSearchButton ? "none" : ""; }); this.elements.searchButton = searchButton; - + const clearSearchButton = new ComfyButton({ icon: "close", tooltip: "Clear search", @@ -3333,7 +3333,7 @@ class DownloadView { clearSearchButton.style.display = hideClearButton ? "none" : ""; }); this.elements.clearSearchButton = clearSearchButton; - + $el("div.tab-header", { $: (el) => (this.element = el), }, [ @@ -3367,7 +3367,7 @@ class DownloadView { ]), ]); } - + /** * Tries to return the related ComfyUI model directory if unambiguous. * @@ -3381,7 +3381,7 @@ class DownloadView { const f = fileType.toLowerCase(); if (f == "diffusers") { return "diffusers"; } // TODO: is this correct? } - + if (modelType !== undefined && modelType !== null) { const m = modelType.toLowerCase(); // TODO: somehow allow for SERVER to set dir? @@ -3408,10 +3408,10 @@ class DownloadView { } return null; } - + /** * Returns empty string on failure - * @param {float | undefined} fileSizeKB + * @param {float | undefined} fileSizeKB * @returns {string} */ static #fileSizeToFormattedString(fileSizeKB) { @@ -3430,7 +3430,7 @@ class DownloadView { fileSizeString = fileSizeString.substring(0, fileSizeString.indexOf(".") + 3); return `(${fileSizeString} ${sizes[sizeIndex]})`; } - + /** * @param {Object} info * @param {ModelData} modelData @@ -3443,7 +3443,7 @@ class DownloadView { "model-download-info-preview-model" + "-" + id, info["images"], ); - + const comfyUIModelType = ( DownloadView.modelTypeToComfyUiDirectory(info["details"]["fileType"]) ?? DownloadView.modelTypeToComfyUiDirectory(info["modelType"]) ?? @@ -3451,7 +3451,7 @@ class DownloadView { ); const searchSeparator = modelData.searchSeparator; const defaultBasePath = searchSeparator + (comfyUIModelType === "" ? "" : comfyUIModelType + searchSeparator + "0"); - + const el_saveDirectoryPath = $el("input.search-text-area", { type: "text", name: "save directory", @@ -3464,7 +3464,7 @@ class DownloadView { el_saveDirectoryPath, true, ); - + const default_name = (() => { const filename = info["fileName"]; // TODO: only remove valid model file extensions @@ -3484,7 +3484,7 @@ class DownloadView { } }, }); - + const infoNotes = $el("textarea.comfy-multiline-input.model-info-notes", { name: "model info notes", value: info["description"]??"", @@ -3492,7 +3492,7 @@ class DownloadView { disabled: true, style: { display: info["description"] === undefined || info["description"] === "" ? "none" : "" }, }); - + const filepath = info["downloadFilePath"]; const modelInfo = $el("details.download-details", [ $el("summary", [filepath + info["fileName"]]), @@ -3571,7 +3571,7 @@ class DownloadView { ]), ]), ]); - + return modelInfo; } @@ -3601,7 +3601,7 @@ class DownloadView { if (modelInfosHtml.length === 1) { modelInfosHtml[0].open = true; } - + const header = $el("div", [ $el("h1", [name]), $el("div.model-manager-settings", [ @@ -3623,7 +3623,7 @@ class DownloadView { const infosHtml = this.elements.infos; infosHtml.innerHTML = ""; infosHtml.append.apply(infosHtml, modelInfosHtml); - + const downloadNotes = this.elements.downloadNotes; if (downloadNotes !== undefined && downloadNotes !== null) { downloadNotes.addEventListener("change", (e) => { @@ -3636,10 +3636,10 @@ class DownloadView { downloadNotes.checked = settings["download-save-description-as-text-file"].checked; downloadNotes.dispatchEvent(new Event('change')); } - + const hideSearchButtons = settings["text-input-always-hide-search-button"].checked; this.elements.searchButton.style.display = hideSearchButtons ? "none" : ""; - + const hideClearSearchButtons = settings["text-input-always-hide-clear-button"].checked; this.elements.clearSearchButton.style.display = hideClearSearchButtons ? "none" : ""; } @@ -3648,7 +3648,7 @@ class DownloadView { class BrowseView { /** @type {HTMLDivElement} */ element = null; - + elements = { /** @type {HTMLDivElement} */ modelGrid: null, /** @type {HTMLSelectElement} */ modelTypeSelect: null, @@ -3657,28 +3657,28 @@ class BrowseView { /** @type {HTMLButtonElement} */ searchButton: null, /** @type {HTMLButtonElement} */ clearSearchButton: null, }; - + /** @type {Array} */ previousModelFilters = []; - + /** @type {Object.<{value: string}>} */ previousModelType = { value: null }; - + /** @type {DirectoryDropdown} */ directoryDropdown = null; - + /** @type {ModelData} */ #modelData = null; - + /** @type {@param {() => Promise }} */ #updateModels = null; - + /** */ #settingsElements = null; - + /** @type {() => void} */ updateModelGrid = () => {}; - + /** * @param {() => Promise } updateModels * @param {ModelData} modelData @@ -3690,11 +3690,11 @@ class BrowseView { /** @type {HTMLDivElement} */ const modelGrid = $el("div.comfy-grid"); this.elements.modelGrid = modelGrid; - + this.#updateModels = updateModels; this.#modelData = modelData; this.#settingsElements = settingsElements; - + const searchInput = $el("input.search-text-area", { $: (el) => (this.elements.modelContentFilter = el), type: "text", @@ -3702,13 +3702,13 @@ class BrowseView { autocomplete: "off", placeholder: "/Search", }); - + const updatePreviousModelFilter = () => { const modelType = this.elements.modelTypeSelect.value; const value = this.elements.modelContentFilter.value; this.previousModelFilters[modelType] = value; }; - + const updateModelGrid = () => { const sortValue = this.elements.modelSortSelect.value; const reverseSort = sortValue[0] === "-"; @@ -3726,18 +3726,18 @@ class BrowseView { showModelInfo, ); updateModelGridCallback(); - + const hideSearchButtons = ( this.#settingsElements["model-real-time-search"].checked | this.#settingsElements["text-input-always-hide-search-button"].checked ); this.elements.searchButton.style.display = hideSearchButtons ? "none" : ""; - + const hideClearSearchButtons = this.#settingsElements["text-input-always-hide-clear-button"].checked; this.elements.clearSearchButton.style.display = hideClearSearchButtons ? "none" : ""; } this.updateModelGrid = updateModelGrid; - + const searchDropdown = new DirectoryDropdown( modelData, searchInput, @@ -3748,7 +3748,7 @@ class BrowseView { () => { return this.#settingsElements["model-real-time-search"].checked; }, ); this.directoryDropdown = searchDropdown; - + const searchButton = new ComfyButton({ icon: "magnify", tooltip: "Search models", @@ -3776,7 +3776,7 @@ class BrowseView { searchButton.style.display = hideSearchButton ? "none" : ""; }); this.elements.searchButton = searchButton; - + const clearSearchButton = new ComfyButton({ icon: "close", tooltip: "Clear search", @@ -3795,7 +3795,7 @@ class BrowseView { clearSearchButton.style.display = hideClearSearchButton ? "none" : ""; }); this.elements.clearSearchButton = clearSearchButton; - + this.element = $el("div", [ $el("div.row.tab-header", [ $el("div.row.tab-header-flex-block", [ @@ -3860,7 +3860,7 @@ class BrowseView { class SettingsView { /** @type {HTMLDivElement} */ element = null; - + elements = { /** @type {HTMLButtonElement} */ reloadButton: null, /** @type {HTMLButtonElement} */ saveButton: null, @@ -3870,7 +3870,7 @@ class SettingsView { /** @type {HTMLInputElement} */ "model-default-browser-model-type": null, /** @type {HTMLInputElement} */ "model-real-time-search": null, /** @type {HTMLInputElement} */ "model-persistent-search": null, - + /** @type {HTMLInputElement} */ "model-preview-thumbnail-type": null, /** @type {HTMLInputElement} */ "model-preview-fallback-search-safetensors-thumbnail": null, /** @type {HTMLInputElement} */ "model-show-label-extensions": null, @@ -3878,33 +3878,33 @@ class SettingsView { /** @type {HTMLInputElement} */ "model-show-copy-button": null, /** @type {HTMLInputElement} */ "model-show-load-workflow-button": null, /** @type {HTMLInputElement} */ "model-info-button-on-left": null, - + /** @type {HTMLInputElement} */ "model-add-embedding-extension": null, /** @type {HTMLInputElement} */ "model-add-drag-strict-on-field": null, /** @type {HTMLInputElement} */ "model-add-offset": null, - + /** @type {HTMLInputElement} */ "model-info-autosave-notes": null, - + /** @type {HTMLInputElement} */ "download-save-description-as-text-file": null, - + /** @type {HTMLInputElement} */ "sidebar-default-width": null, /** @type {HTMLInputElement} */ "sidebar-default-height": null, /** @type {HTMLInputElement} */ "sidebar-control-always-compact": null, /** @type {HTMLInputElement} */ "text-input-always-hide-search-button": null, /** @type {HTMLInputElement} */ "text-input-always-hide-clear-button": null, - + /** @type {HTMLInputElement} */ "tag-generator-sampler-method": null, /** @type {HTMLInputElement} */ "tag-generator-count": null, /** @type {HTMLInputElement} */ "tag-generator-threshold": null, }, }; - + /** @return {() => Promise } */ #updateModels = () => {}; - + /** - * @param {Object} settingsData - * @param {boolean} updateModels + * @param {Object} settingsData + * @param {boolean} updateModels */ async #setSettings(settingsData, updateModels) { const settings = this.elements.settings; @@ -3928,7 +3928,7 @@ class SettingsView { await this.#updateModels(); // Is this slow? } } - + /** * @param {boolean} updateModels * @returns {Promise } @@ -3939,7 +3939,7 @@ class SettingsView { await this.#setSettings(settingsData, updateModels); comfyButtonAlert(this.elements.reloadButton, true); } - + /** @returns {Promise } */ async save() { let settingsData = {}; @@ -3957,7 +3957,7 @@ class SettingsView { } settingsData[setting] = value; } - + const data = await comfyRequest( "/model-manager/settings/save", { @@ -3974,7 +3974,7 @@ class SettingsView { } comfyButtonAlert(this.elements.saveButton, success); } - + /** * @param {() => Promise } updateModels * @param {() => void} updateSidebarButtons @@ -3982,7 +3982,7 @@ class SettingsView { constructor(updateModels, updateSidebarButtons) { this.#updateModels = updateModels; const settings = this.elements.settings; - + const sidebarControl = $checkbox({ $: (el) => (settings["sidebar-control-always-compact"] = el), textContent: "Sidebar controls always compact", @@ -3990,7 +3990,7 @@ class SettingsView { sidebarControl.getElementsByTagName('input')[0].addEventListener("change", () => { updateSidebarButtons(); }); - + const reloadButton = new ComfyButton({ content: "Reload", tooltip: "Reload settings and model manager files", @@ -4002,7 +4002,7 @@ class SettingsView { }, }).element; this.elements.reloadButton = reloadButton; - + const saveButton = new ComfyButton({ content: "Save", tooltip: "Save settings and reload model manager", @@ -4014,7 +4014,7 @@ class SettingsView { }, }).element; this.elements.saveButton = saveButton; - + const correctPreviewsButton = new ComfyButton({ content: "Fix Extensions", tooltip: "Correct image file extensions in all model directories", @@ -4040,7 +4040,7 @@ class SettingsView { button.disabled = false; }, }).element; - + $el("div.model-manager-settings", { $: (el) => (this.element = el), }, [ @@ -4289,7 +4289,7 @@ function GenerateSidebarToggleRadioAndSelect(labels, activationCallbacks = []) { const RADIO_BUTTON_GROUP_ACTIVE = "radio-button-group-active"; const radioButtonGroup = $el("div.radio-button-group", []); const buttons = []; - + const select = $el("select", { name: "sidebar-select", onchange: (event) => { @@ -4320,7 +4320,7 @@ function GenerateSidebarToggleRadioAndSelect(labels, activationCallbacks = []) { }, option); }) ); - + for (let i = 0; i < labels.length; i++) { const text = labels[i]; const activationCallback = activationCallbacks[i] ?? (() => {}); @@ -4363,72 +4363,72 @@ function GenerateSidebarToggleRadioAndSelect(labels, activationCallbacks = []) { radioButtonGroup.append.apply(radioButtonGroup, buttons); buttons[0].click(); buttons[0].style.display = "none"; - + return [radioButtonGroup, select]; } class ModelManager extends ComfyDialog { /** @type {HTMLDivElement} */ element = null; - + /** @type {ModelData} */ #modelData = null; - + /** @type {ModelInfo} */ #modelInfo = null; - + /** @type {DownloadView} */ #downloadView = null; - + /** @type {BrowseView} */ #browseView = null; - + /** @type {SettingsView} */ #settingsView = null; - + /** @type {HTMLDivElement} */ #topbarRight = null; - + /** @type {HTMLDivElement} */ #tabManagerButtons = null; - + /** @type {HTMLDivElement} */ #tabManagerContents = null; - + /** @type {HTMLDivElement} */ #tabInfoButtons = null; - + /** @type {HTMLDivElement} */ #tabInfoContents = null; - + /** @type {HTMLButtonElement} */ #sidebarButtonGroup = null; - + /** @type {HTMLButtonElement} */ #sidebarSelect = null; - + /** @type {HTMLButtonElement} */ #closeModelInfoButton = null; - + /** @type {String} */ #dragSidebarState = ""; - + constructor() { super(); - + this.#modelData = new ModelData(); - + this.#settingsView = new SettingsView( this.#refreshModels, () => this.#updateSidebarButtons(), ); - + this.#modelInfo = new ModelInfo( this.#modelData, this.#refreshModels, this.#settingsView.elements.settings, ); - + this.#browseView = new BrowseView( this.#refreshModels, this.#modelData, @@ -4436,27 +4436,27 @@ class ModelManager extends ComfyDialog { this.#resetManagerContentsScroll, this.#settingsView.elements.settings, // TODO: decouple settingsData from elements? ); - + this.#downloadView = new DownloadView( this.#modelData, this.#settingsView.elements.settings, this.#refreshModels, ); - + const [tabManagerButtons, tabManagerContents] = GenerateTabGroup([ { name: "Download", icon: "arrow-collapse-down", tabContent: this.#downloadView.element }, { name: "Models", icon: "folder-search-outline", tabContent: this.#browseView.element }, { name: "Settings", icon: "cog-outline", tabContent: this.#settingsView.element }, ]); tabManagerButtons[0]?.click(); - + const tabInfoButtons = this.#modelInfo.elements.tabButtons; const tabInfoContents = this.#modelInfo.elements.tabContents; - + const [sidebarButtonGroup, sidebarSelect] = GenerateSidebarToggleRadioAndSelect( ["◼", "◨", "⬒", "⬓", "◧"], [ - () => { + () => { const element = this.element; if (element) { // callback on initialization as default state element.dataset["sidebarState"] = "none"; @@ -4475,7 +4475,7 @@ class ModelManager extends ComfyDialog { for (let i = 0; i < sidebarButtonGroupChildren.length; i++) { sidebarButtonGroupChildren[i].classList.add("icon-button"); } - + const closeModelInfoButton = new ComfyButton({ icon: "arrow-u-left-bottom", tooltip: "Return to model search", @@ -4484,7 +4484,7 @@ class ModelManager extends ComfyDialog { }).element; this.#closeModelInfoButton = closeModelInfoButton; closeModelInfoButton.style.display = "none"; - + const modelManager = $el( "div.comfy-modal.model-manager", { @@ -4545,36 +4545,36 @@ class ModelManager extends ComfyDialog { ]), ] ); - + new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 704)).observe(modelManager); new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabInfoButtons, 704)).observe(modelManager); new ResizeObserver(() => this.#updateSidebarButtons()).observe(modelManager); window.addEventListener('resize', () => { const width = window.innerWidth; const height = window.innerHeight; - + const leftDecimal = modelManager.dataset["sidebarLeftWidthDecimal"]; const rightDecimal = modelManager.dataset["sidebarRightWidthDecimal"]; const topDecimal = modelManager.dataset["sidebarTopHeightDecimal"]; const bottomDecimal = modelManager.dataset["sidebarBottomHeightDecimal"]; - + // restore decimal after resize modelManager.style.setProperty("--model-manager-sidebar-width-left", (leftDecimal * width) + "px"); modelManager.style.setProperty("--model-manager-sidebar-width-right", (rightDecimal * width) + "px"); modelManager.style.setProperty("--model-manager-sidebar-height-top", + (topDecimal * height) + "px"); modelManager.style.setProperty("--model-manager-sidebar-height-bottom", (bottomDecimal * height) + "px"); }); - + const EDGE_DELTA = 8; - + const endDragSidebar = (e) => { this.#dragSidebarState = ""; - + modelManager.classList.remove("cursor-drag-left"); modelManager.classList.remove("cursor-drag-top"); modelManager.classList.remove("cursor-drag-right"); modelManager.classList.remove("cursor-drag-bottom"); - + // cache for window resize modelManager.dataset["sidebarLeftWidthDecimal"] = parseInt(modelManager.style.getPropertyValue("--model-manager-sidebar-width-left")) / window.innerWidth; modelManager.dataset["sidebarRightWidthDecimal"] = parseInt(modelManager.style.getPropertyValue("--model-manager-sidebar-width-right")) / window.innerWidth; @@ -4583,7 +4583,7 @@ class ModelManager extends ComfyDialog { }; document.addEventListener("mouseup", (e) => endDragSidebar(e)); document.addEventListener("touchend", (e) => endDragSidebar(e)); - + const detectDragSidebar = (e, x, y) => { const left = modelManager.offsetLeft; const top = modelManager.offsetTop; @@ -4591,17 +4591,17 @@ class ModelManager extends ComfyDialog { const height = modelManager.offsetHeight; const right = left + width; const bottom = top + height; - + if (!(x >= left && x <= right && y >= top && y <= bottom)) { // click was not in model manager return; } - + const isOnEdgeLeft = x - left <= EDGE_DELTA; const isOnEdgeRight = right - x <= EDGE_DELTA; const isOnEdgeTop = y - top <= EDGE_DELTA; const isOnEdgeBottom = bottom - y <= EDGE_DELTA; - + const sidebarState = this.element.dataset["sidebarState"]; if (sidebarState === "left" && isOnEdgeRight) { this.#dragSidebarState = sidebarState; @@ -4615,7 +4615,7 @@ class ModelManager extends ComfyDialog { else if (sidebarState === "bottom" && isOnEdgeTop) { this.#dragSidebarState = sidebarState; } - + if (this.#dragSidebarState !== "") { e.preventDefault(); e.stopPropagation(); @@ -4623,25 +4623,25 @@ class ModelManager extends ComfyDialog { }; modelManager.addEventListener("mousedown", (e) => detectDragSidebar(e, e.clientX, e.clientY)); modelManager.addEventListener("touchstart", (e) => detectDragSidebar(e, e.touches[0].clientX, e.touches[0].clientY)); - + const updateSidebarCursor = (e, x, y) => { if (this.#dragSidebarState !== "") { // do not update cursor style while dragging return; } - + const left = modelManager.offsetLeft; const top = modelManager.offsetTop; const width = modelManager.offsetWidth; const height = modelManager.offsetHeight; const right = left + width; const bottom = top + height; - + const isOnEdgeLeft = x - left <= EDGE_DELTA; const isOnEdgeRight = right - x <= EDGE_DELTA; const isOnEdgeTop = y - top <= EDGE_DELTA; const isOnEdgeBottom = bottom - y <= EDGE_DELTA; - + const updateClass = (add, className) => { if (add) { modelManager.classList.add(className); @@ -4650,7 +4650,7 @@ class ModelManager extends ComfyDialog { modelManager.classList.remove(className); } }; - + const sidebarState = this.element.dataset["sidebarState"]; updateClass(sidebarState === "right" && isOnEdgeLeft, "cursor-drag-left"); updateClass(sidebarState === "bottom" && isOnEdgeTop, "cursor-drag-top"); @@ -4659,18 +4659,18 @@ class ModelManager extends ComfyDialog { }; modelManager.addEventListener("mousemove", (e) => updateSidebarCursor(e, e.clientX, e.clientY)); modelManager.addEventListener("touchmove", (e) => updateSidebarCursor(e, e.touches[0].clientX, e.touches[0].clientY)); - + const updateDragSidebar = (e, x, y) => { const sidebarState = this.#dragSidebarState; if (sidebarState === "") { return; } - + e.preventDefault(); - + const width = window.innerWidth; const height = window.innerHeight; - + if (sidebarState === "left") { const pixels = clamp(x, 0, width).toString() + "px"; modelManager.style.setProperty("--model-manager-sidebar-width-left", pixels); @@ -4690,16 +4690,16 @@ class ModelManager extends ComfyDialog { }; document.addEventListener("mousemove", (e) => updateDragSidebar(e, e.clientX, e.clientY)); document.addEventListener("touchmove", (e) => updateDragSidebar(e, e.touches[0].clientX, e.touches[0].clientY)); - + this.#init(); } - + async #init() { await this.#settingsView.reload(false); await this.#refreshModels(); - + const settings = this.#settingsView.elements.settings; - + { // initialize buttons' visibility state const hideSearchButtons = settings["text-input-always-hide-search-button"].checked; @@ -4707,41 +4707,41 @@ class ModelManager extends ComfyDialog { this.#downloadView.elements.searchButton.style.display = hideSearchButtons ? "none" : ""; this.#downloadView.elements.clearSearchButton.style.display = hideClearSearchButtons ? "none" : ""; } - + { // set initial sidebar widths & heights const width = window.innerWidth; const height = window.innerHeight; - + const xDecimal = settings["sidebar-default-width"].value; const yDecimal = settings["sidebar-default-height"].value; - + this.element.dataset["sidebarLeftWidthDecimal"] = xDecimal; this.element.dataset["sidebarRightWidthDecimal"] = xDecimal; this.element.dataset["sidebarTopHeightDecimal"] = yDecimal; this.element.dataset["sidebarBottomHeightDecimal"] = yDecimal; - + const x = Math.floor(width * xDecimal); const y = Math.floor(height * yDecimal); - + const leftPixels = x.toString() + "px"; this.element.style.setProperty("--model-manager-sidebar-width-left", leftPixels); - + const rightPixels = x.toString() + "px"; this.element.style.setProperty("--model-manager-sidebar-width-right", rightPixels); - + const topPixels = y.toString() + "px"; this.element.style.setProperty("--model-manager-sidebar-height-top", topPixels); - + const bottomPixels = y.toString() + "px"; this.element.style.setProperty("--model-manager-sidebar-height-bottom", bottomPixels); } } - + #resetManagerContentsScroll = () => { this.#tabManagerContents.scrollTop = 0; } - + #refreshModels = async() => { const modelData = this.#modelData; modelData.systemSeparator = await comfyRequest("/model-manager/system-separator"); @@ -4749,38 +4749,38 @@ class ModelManager extends ComfyDialog { Object.assign(modelData.models, newModels); // NOTE: do NOT create a new object const newModelDirectories = await comfyRequest("/model-manager/models/directory-list"); modelData.directories.data.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array - + this.#browseView.updateModelGrid(); await this.#tryHideModelInfo(false); - + document.getElementById("comfy-refresh-button")?.click(); } - + /** * @param {searchPath: string} * @return {Promise } */ #showModelInfo = async(searchPath) => { await this.#modelInfo.update( - searchPath, - this.#refreshModels, - this.#modelData.searchSeparator, + searchPath, + this.#refreshModels, + this.#modelData.searchSeparator, ).then(() => { this.#tabManagerButtons.style.display = "none"; this.#tabManagerContents.style.display = "none"; - + this.#closeModelInfoButton.style.display = ""; this.#tabInfoButtons.style.display = ""; this.#tabInfoContents.style.display = ""; - + this.#tabInfoButtons.children[0]?.click(); this.#modelInfo.show(); this.#tabInfoContents.scrollTop = 0; }); } - + /** - * @param {boolean} promptSave + * @param {boolean} promptSave * @returns {Promise } */ #tryHideModelInfo = async(promptSave) => { @@ -4788,17 +4788,17 @@ class ModelManager extends ComfyDialog { if (!await this.#modelInfo.tryHide(promptSave)) { return false; } - + this.#closeModelInfoButton.style.display = "none"; this.#tabInfoButtons.style.display = "none"; this.#tabInfoContents.style.display = "none"; - + this.#tabManagerButtons.style.display = ""; this.#tabManagerContents.style.display = ""; } return true; } - + #updateSidebarButtons = () => { const managerRect = this.element.getBoundingClientRect(); const isNarrow = managerRect.width < 768; // TODO: `minWidth` is a magic value @@ -4848,7 +4848,7 @@ app.registerExtension({ rel: "stylesheet", href: "./extensions/ComfyUI-Model-Manager/model-manager.css", }); - + app.ui?.menuContainer?.appendChild( $el("button", { id: "comfyui-model-manager-button", @@ -4857,7 +4857,7 @@ app.registerExtension({ onclick: () => toggleModelManager(), }) ); - + // [Beta] mobile menu app.menu?.settingsGroup?.append(new ComfyButton({ icon: "folder-search", From 1a52cf50d55115a155127808e37cd60cdd0b1f75 Mon Sep 17 00:00:00 2001 From: korutech-ai Date: Wed, 28 Aug 2024 10:13:11 +1200 Subject: [PATCH 6/9] LINTING: - Added eslint. - Cleaned up lexical declarations in case clauses. --- .vscode/settings.json | 19 +++++++++++++++++++ web/eslint.config.mjs | 8 ++++++++ web/model-manager.js | 8 +++++--- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 web/eslint.config.mjs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9fbad55 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "cSpell.words": [ + "apng", + "Civitai", + "ckpt", + "comfyui", + "FYUIKMNVB", + "gguf", + "gligen", + "jfif", + "locon", + "loras", + "noimage", + "onnx", + "rfilename", + "unet", + "upscaler" + ] +} \ No newline at end of file diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000..2edbef8 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,8 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, +]; \ No newline at end of file diff --git a/web/model-manager.js b/web/model-manager.js index 498b098..469c14a 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -606,7 +606,7 @@ class ImageSelect { const value = document.querySelector(`input[name="${name}"]:checked`).value; const elements = this.elements; switch (value) { - case this.#PREVIEW_DEFAULT: + case this.#PREVIEW_DEFAULT: { const children = elements.defaultPreviews.children; const noImage = PREVIEW_NONE_URI; let url = ""; @@ -626,7 +626,8 @@ class ImageSelect { }); } return url; - case this.#PREVIEW_URL: + } + case this.#PREVIEW_URL: { const value = elements.customUrl.value; if (value.startsWith(Civitai.imagePostUrlPrefix())) { try { @@ -644,6 +645,7 @@ class ImageSelect { } } return value; + } case this.#PREVIEW_UPLOAD: return elements.uploadFile.files[0] ?? ""; case this.#PREVIEW_NONE: @@ -2112,7 +2114,7 @@ class ModelInfo { previewSelect.elements.previews.style.display = "flex"; const setPreviewButton = new ComfyButton({ - tooltip: "Overwrite currrent preview with selected image", + tooltip: "Overwrite current preview with selected image", content: "Set as Preview", action: async(e) => { const [button, icon, span] = comfyButtonDisambiguate(e.target); From 3943034a18e3338d2a81ed26f6e2697ed3a052f3 Mon Sep 17 00:00:00 2001 From: korutech-ai Date: Wed, 28 Aug 2024 11:18:53 +1200 Subject: [PATCH 7/9] UPDATE: Added a notes editing toggle button to improve notes tab layout. --- web/model-manager.js | 46 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/web/model-manager.js b/web/model-manager.js index 469c14a..bc7b4dd 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -2716,7 +2716,7 @@ class ModelInfo { (() => { const notes = $el("textarea.comfy-multiline-input", { name: "model notes", - value: noteText, + value: noteText, oninput: (e) => { if (this.#settingsElements["model-info-autosave-notes"].checked) { saveDebounce(); @@ -2752,19 +2752,51 @@ class ModelInfo { this.elements.notes = notes; this.elements.markdown = markdown; this.#savedNotesValue = noteText; + + const notes_editor = $el( + "div", + { + style: { + "display": noteText == "" ? "flex" : "none", + "height": "100%", + "min-height": "60px" + }, + }, + notes + ); + const notes_viewer = $el( + "div", + { + style: { + "display": noteText == "" ? "none" : "flex", + "height": "100%", + "min-height": "60px", + "overflow": "scroll" + }, + }, + markdown + ); + + const editNotesButton = new ComfyButton({ + icon: "pencil", + tooltip: "Change file name", + classList: "comfyui-button icon-button", + action: async () => { + notes_editor.style.display = notes_editor.style.display == "flex" ? "none" : "flex"; + notes_viewer.style.display = notes_viewer.style.display == "none" ? "flex" : "none"; + }, + }).element; + return [ $el("div.row", { style: { "align-items": "center" }, }, [ $el("h1", ["Notes"]), saveNotesButton, + editNotesButton, ]), - $el("div", { - style: { "display": "block", "height": "20%", "min-height": "120px", "z-index": "1" }, - }, notes), - $el("div", { - style: { "display": "block", "height": "70%", "min-height": "60px", "overflow": "scroll" }, - }, markdown), + notes_editor, + notes_viewer, ]; })() ); From 6a3eed6157f3066415d3ff5d738ecbb6278879c3 Mon Sep 17 00:00:00 2001 From: korutech-ai Date: Wed, 28 Aug 2024 11:52:04 +1200 Subject: [PATCH 8/9] BUGFIX: Modified layout of tags tab so elements don't overlap. --- web/model-manager.js | 83 +++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/web/model-manager.js b/web/model-manager.js index bc7b4dd..40a66bc 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -2635,46 +2635,57 @@ class ModelInfo { break; } } + const tagGenerator = $el( + "div", [ + $el("h1", ["Tags"]), + $el("h2", { style: { margin: "0px 0px 16px 0px" } }, ["Random Tag Generator"]), + $el("div", [ + $el("details.tag-generator-settings", { + style: { margin: "10px 0", display: "none" }, + open: false, + }, [ + $el("summary", ["Settings"]), + $el("div", [ + "Sampling Method", + samplerRadioGroup, + ]), + $el("label", [ + "Count", + tagGenerationCount, + ]), + $el("label", [ + "Threshold", + tagGenerationThreshold, + ]), + ]), + tagGeneratorRandomizedOutput, + new ComfyButton({ + content: "Randomize", + tooltip: "Randomly generate subset of tags", + action: () => { + const samplerName = document.querySelector(`input[name="${TAG_GENERATOR_SAMPLER_NAME}"]:checked`).value; + const sampler = samplerName === "Frequency" ? ModelInfo.ProbabilisticTagSampling : ModelInfo.UniformTagSampling; + const sampleCount = tagGenerationCount.value; + const frequencyThreshold = tagGenerationThreshold.value; + const tags = ParseTagParagraph(tagsParagraph.innerText); + const sampledTags = sampler(tags, sampleCount, frequencyThreshold); + tagGeneratorRandomizedOutput.value = sampledTags.join(", "); + }, + }).element, + ]), + ] + ) tagsElement.innerHTML = ""; tagsElement.append.apply(tagsElement, [ - $el("h1", ["Tags"]), - $el("h2", { style: { margin: "0px 0px 16px 0px" } }, ["Random Tag Generator"]), + tagGenerator, $el("div", [ - $el("details.tag-generator-settings", { - style: { margin: "10px 0", display: "none" }, - open: false, - }, [ - $el("summary", ["Settings"]), - $el("div", [ - "Sampling Method", - samplerRadioGroup, - ]), - $el("label", [ - "Count", - tagGenerationCount, - ]), - $el("label", [ - "Threshold", - tagGenerationThreshold, - ]), - ]), - tagGeneratorRandomizedOutput, - new ComfyButton({ - content: "Randomize", - tooltip: "Randomly generate subset of tags", - action: () => { - const samplerName = document.querySelector(`input[name="${TAG_GENERATOR_SAMPLER_NAME}"]:checked`).value; - const sampler = samplerName === "Frequency" ? ModelInfo.ProbabilisticTagSampling : ModelInfo.UniformTagSampling; - const sampleCount = tagGenerationCount.value; - const frequencyThreshold = tagGenerationThreshold.value; - const tags = ParseTagParagraph(tagsParagraph.innerText); - const sampledTags = sampler(tags, sampleCount, frequencyThreshold); - tagGeneratorRandomizedOutput.value = sampledTags.join(", "); - }, - }).element, + $el("h2", { + style: { + margin: "24px 0px 8px 0px" + } + }, ["Tags"]), + tagsParagraph, ]), - $el("h2", {style: { margin: "24px 0px 8px 0px" } }, ["Training Tags"]), - tagsParagraph, ]); const tagButton = this.elements.tabButtons[2]; // TODO: remove magic value tagButton.style.display = isTags ? "" : "none"; From 89b95405c854f5d706cba2773b18c6ddda70f646 Mon Sep 17 00:00:00 2001 From: korutech-ai Date: Thu, 29 Aug 2024 08:02:01 +1200 Subject: [PATCH 9/9] CSS: Added overflow-wrap to improve text readability. --- web/model-manager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/model-manager.js b/web/model-manager.js index 40a66bc..81fae94 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -2782,7 +2782,8 @@ class ModelInfo { "display": noteText == "" ? "none" : "flex", "height": "100%", "min-height": "60px", - "overflow": "scroll" + "overflow": "scroll", + "overflow-wrap": "anywhere" }, }, markdown