| | 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; |
| | }); |
| | } |
| | } |
| |
|