| import { $el, ComfyDialog } from "../../../../scripts/ui.js"; |
| import { api } from "../../../../scripts/api.js"; |
| import { addStylesheet } from "./utils.js"; |
|
|
| addStylesheet(import.meta.url); |
|
|
| class MetadataDialog extends ComfyDialog { |
| constructor() { |
| super(); |
|
|
| this.element.classList.add("pysssss-model-metadata"); |
| } |
| show(metadata) { |
| super.show( |
| $el( |
| "div", |
| Object.keys(metadata).map((k) => |
| $el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })]) |
| ) |
| ) |
| ); |
| } |
| } |
|
|
| export class ModelInfoDialog extends ComfyDialog { |
| constructor(name) { |
| super(); |
| this.name = name; |
| this.element.classList.add("pysssss-model-info"); |
| } |
|
|
| get customNotes() { |
| return this.metadata["pysssss.notes"]; |
| } |
|
|
| set customNotes(v) { |
| this.metadata["pysssss.notes"] = v; |
| } |
|
|
| get hash() { |
| return this.metadata["pysssss.sha256"]; |
| } |
|
|
| async show(type, value) { |
| this.type = type; |
|
|
| const req = api.fetchApi("/pysssss/metadata/" + encodeURIComponent(`${type}/${value}`)); |
| this.info = $el("div", { style: { flex: "auto" } }); |
| this.img = $el("img", { style: { display: "none" } }); |
| this.imgWrapper = $el("div.pysssss-preview", [this.img]); |
| this.main = $el("main", { style: { display: "flex" } }, [this.info, this.imgWrapper]); |
| this.content = $el("div.pysssss-model-content", [$el("h2", { textContent: this.name }), this.main]); |
|
|
| const loading = $el("div", { textContent: "ℹ️ Loading...", parent: this.content }); |
|
|
| super.show(this.content); |
|
|
| this.metadata = await (await req).json(); |
| this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = ""; |
| this.viewMetadata.removeAttribute("disabled"); |
|
|
| loading.remove(); |
| this.addInfo(); |
| } |
|
|
| createButtons() { |
| const btns = super.createButtons(); |
| this.viewMetadata = $el("button", { |
| type: "button", |
| textContent: "View raw metadata", |
| disabled: "disabled", |
| style: { |
| opacity: 0.5, |
| cursor: "not-allowed", |
| }, |
| onclick: (e) => { |
| if (this.metadata) { |
| new MetadataDialog().show(this.metadata); |
| } |
| }, |
| }); |
|
|
| btns.unshift(this.viewMetadata); |
| return btns; |
| } |
|
|
| getNoteInfo() { |
| function parseNote() { |
| if (!this.customNotes) return []; |
|
|
| let notes = []; |
| |
| const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g"); |
| let end = 0; |
| let m; |
| do { |
| m = r.exec(this.customNotes); |
| let pos; |
| let fin = 0; |
| if (m) { |
| pos = m.index; |
| fin = m.index + m[0].length; |
| } else { |
| pos = this.customNotes.length; |
| } |
|
|
| let pre = this.customNotes.substring(end, pos); |
| if (pre) { |
| pre = pre.replaceAll("\n", "<br>"); |
| notes.push( |
| $el("span", { |
| innerHTML: pre, |
| }) |
| ); |
| } |
| if (m) { |
| notes.push( |
| $el("a", { |
| href: m[0], |
| textContent: m[0], |
| target: "_blank", |
| }) |
| ); |
| } |
|
|
| end = fin; |
| } while (m); |
| return notes; |
| } |
|
|
| let textarea; |
| let notesContainer; |
| const editText = "✏️ Edit"; |
| const edit = $el("a", { |
| textContent: editText, |
| href: "#", |
| style: { |
| float: "right", |
| color: "greenyellow", |
| textDecoration: "none", |
| }, |
| onclick: async (e) => { |
| e.preventDefault(); |
|
|
| if (textarea) { |
| this.customNotes = textarea.value; |
|
|
| const resp = await api.fetchApi( |
| "/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`), |
| { |
| method: "POST", |
| body: this.customNotes, |
| } |
| ); |
|
|
| if (resp.status !== 200) { |
| console.error(resp); |
| alert(`Error saving notes (${req.status}) ${req.statusText}`); |
| return; |
| } |
|
|
| e.target.textContent = editText; |
| textarea.remove(); |
| textarea = null; |
|
|
| notesContainer.replaceChildren(...parseNote.call(this)); |
| } else { |
| e.target.textContent = "💾 Save"; |
| textarea = $el("textarea", { |
| style: { |
| width: "100%", |
| minWidth: "200px", |
| minHeight: "50px", |
| }, |
| textContent: this.customNotes, |
| }); |
| e.target.after(textarea); |
| notesContainer.replaceChildren(); |
| textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px"; |
| } |
| }, |
| }); |
|
|
| notesContainer = $el("div.pysssss-model-notes", parseNote.call(this)); |
| return $el( |
| "div", |
| { |
| style: { display: "contents" }, |
| }, |
| [edit, notesContainer] |
| ); |
| } |
|
|
| addInfo() { |
| this.addInfoEntry("Notes", this.getNoteInfo()); |
| } |
|
|
| addInfoEntry(name, value) { |
| return $el( |
| "p", |
| { |
| parent: this.info, |
| }, |
| [ |
| typeof name === "string" ? $el("label", { textContent: name + ": " }) : name, |
| typeof value === "string" ? $el("span", { textContent: value }) : value, |
| ] |
| ); |
| } |
|
|
| async getCivitaiDetails() { |
| const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash); |
| if (req.status === 200) { |
| return await req.json(); |
| } else if (req.status === 404) { |
| throw new Error("Model not found"); |
| } else { |
| throw new Error(`Error loading info (${req.status}) ${req.statusText}`); |
| } |
| } |
|
|
| addCivitaiInfo() { |
| const promise = this.getCivitaiDetails(); |
| const content = $el("span", { textContent: "ℹ️ Loading..." }); |
|
|
| this.addInfoEntry( |
| $el("label", [ |
| $el("img", { |
| style: { |
| width: "18px", |
| position: "relative", |
| top: "3px", |
| margin: "0 5px 0 0", |
| }, |
| src: "https://civitai.com/favicon.ico", |
| }), |
| $el("span", { textContent: "Civitai: " }), |
| ]), |
| content |
| ); |
|
|
| return promise |
| .then((info) => { |
| content.replaceChildren( |
| $el("a", { |
| href: "https://civitai.com/models/" + info.modelId, |
| textContent: "View " + info.model.name, |
| target: "_blank", |
| }) |
| ); |
|
|
| if (info.images?.length) { |
| this.img.src = info.images[0].url; |
| this.img.style.display = ""; |
|
|
| this.imgSave = $el("button", { |
| textContent: "Use as preview", |
| parent: this.imgWrapper, |
| onclick: async () => { |
| |
| const blob = await (await fetch(this.img.src)).blob(); |
|
|
| |
| const name = "temp_preview." + new URL(this.img.src).pathname.split(".")[1]; |
| const body = new FormData(); |
| body.append("image", new File([blob], name)); |
| body.append("overwrite", "true"); |
| body.append("type", "temp"); |
|
|
| const resp = await api.fetchApi("/upload/image", { |
| method: "POST", |
| body, |
| }); |
|
|
| if (resp.status !== 200) { |
| console.error(resp); |
| alert(`Error saving preview (${req.status}) ${req.statusText}`); |
| return; |
| } |
|
|
| |
| await api.fetchApi("/pysssss/save/" + encodeURIComponent(`${this.type}/${this.name}`), { |
| method: "POST", |
| body: JSON.stringify({ |
| filename: name, |
| type: "temp", |
| }), |
| headers: { |
| "content-type": "application/json", |
| }, |
| }); |
| app.refreshComboInNodes(); |
| }, |
| }); |
| } |
|
|
| return info; |
| }) |
| .catch((err) => { |
| content.textContent = "⚠️ " + err.message; |
| }); |
| } |
| } |
|
|