|
|
|
import { $el } from "../ui.js";
|
|
|
|
import { api } from "../api.js";
|
|
|
|
import { ComfyDialog } from "./dialog.js";
|
|
|
|
|
|
|
|
export class ComfySettingsDialog extends ComfyDialog {
|
|
|
|
constructor(app) {
|
|
|
|
super();
|
|
|
|
this.app = app;
|
|
|
|
this.settingsValues = {};
|
|
|
|
this.settingsLookup = {};
|
|
|
|
this.element = $el(
|
|
|
|
"dialog",
|
|
|
|
{
|
|
|
|
id: "comfy-settings-dialog",
|
|
|
|
parent: document.body,
|
|
|
|
},
|
|
|
|
[
|
|
|
|
$el("table.comfy-modal-content.comfy-table", [
|
|
|
|
$el(
|
|
|
|
"caption",
|
|
|
|
{ textContent: "Settings" },
|
|
|
|
$el("button.comfy-btn", {
|
|
|
|
type: "button",
|
|
|
|
textContent: "\u00d7",
|
|
|
|
onclick: () => {
|
|
|
|
this.element.close();
|
|
|
|
},
|
|
|
|
})
|
|
|
|
),
|
|
|
|
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
|
|
|
|
$el("button", {
|
|
|
|
type: "button",
|
|
|
|
textContent: "Close",
|
|
|
|
style: {
|
|
|
|
cursor: "pointer",
|
|
|
|
},
|
|
|
|
onclick: () => {
|
|
|
|
this.element.close();
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
get settings() {
|
|
|
|
return Object.values(this.settingsLookup);
|
|
|
|
}
|
|
|
|
|
|
|
|
async load() {
|
|
|
|
if (this.app.storageLocation === "browser") {
|
|
|
|
this.settingsValues = localStorage;
|
|
|
|
} else {
|
|
|
|
this.settingsValues = await api.getSettings();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Trigger onChange for any settings added before load
|
|
|
|
for (const id in this.settingsLookup) {
|
|
|
|
this.settingsLookup[id].onChange?.(this.settingsValues[this.getId(id)]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getId(id) {
|
|
|
|
if (this.app.storageLocation === "browser") {
|
|
|
|
id = "Comfy.Settings." + id;
|
|
|
|
}
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
getSettingValue(id, defaultValue) {
|
|
|
|
let value = this.settingsValues[this.getId(id)];
|
|
|
|
if(value != null) {
|
|
|
|
if(this.app.storageLocation === "browser") {
|
|
|
|
try {
|
|
|
|
value = JSON.parse(value);
|
|
|
|
} catch (error) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value ?? defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
async setSettingValueAsync(id, value) {
|
|
|
|
const json = JSON.stringify(value);
|
|
|
|
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
|
|
|
|
|
|
|
|
let oldValue = this.getSettingValue(id, undefined);
|
|
|
|
this.settingsValues[this.getId(id)] = value;
|
|
|
|
|
|
|
|
if (id in this.settingsLookup) {
|
|
|
|
this.settingsLookup[id].onChange?.(value, oldValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
await api.storeSetting(id, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
setSettingValue(id, value) {
|
|
|
|
this.setSettingValueAsync(id, value).catch((err) => {
|
|
|
|
alert(`Error saving setting '${id}'`);
|
|
|
|
console.error(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined }) {
|
|
|
|
if (!id) {
|
|
|
|
throw new Error("Settings must have an ID");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (id in this.settingsLookup) {
|
|
|
|
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
let skipOnChange = false;
|
|
|
|
let value = this.getSettingValue(id);
|
|
|
|
if (value == null) {
|
|
|
|
if (this.app.isNewUserSession) {
|
|
|
|
// Check if we have a localStorage value but not a setting value and we are a new user
|
|
|
|
const localValue = localStorage["Comfy.Settings." + id];
|
|
|
|
if (localValue) {
|
|
|
|
value = JSON.parse(localValue);
|
|
|
|
this.setSettingValue(id, value); // Store on the server
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (value == null) {
|
|
|
|
value = defaultValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Trigger initial setting of value
|
|
|
|
if (!skipOnChange) {
|
|
|
|
onChange?.(value, undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.settingsLookup[id] = {
|
|
|
|
id,
|
|
|
|
onChange,
|
|
|
|
name,
|
|
|
|
render: () => {
|
|
|
|
const setter = (v) => {
|
|
|
|
if (onChange) {
|
|
|
|
onChange(v, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setSettingValue(id, v);
|
|
|
|
value = v;
|
|
|
|
};
|
|
|
|
value = this.getSettingValue(id, defaultValue);
|
|
|
|
|
|
|
|
let element;
|
|
|
|
const htmlID = id.replaceAll(".", "-");
|
|
|
|
|
|
|
|
const labelCell = $el("td", [
|
|
|
|
$el("label", {
|
|
|
|
for: htmlID,
|
|
|
|
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
|
|
|
|
textContent: name,
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (typeof type === "function") {
|
|
|
|
element = type(name, setter, value, attrs);
|
|
|
|
} else {
|
|
|
|
switch (type) {
|
|
|
|
case "boolean":
|
|
|
|
element = $el("tr", [
|
|
|
|
labelCell,
|
|
|
|
$el("td", [
|
|
|
|
$el("input", {
|
|
|
|
id: htmlID,
|
|
|
|
type: "checkbox",
|
|
|
|
checked: value,
|
|
|
|
onchange: (event) => {
|
|
|
|
const isChecked = event.target.checked;
|
|
|
|
if (onChange !== undefined) {
|
|
|
|
onChange(isChecked);
|
|
|
|
}
|
|
|
|
this.setSettingValue(id, isChecked);
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
break;
|
|
|
|
case "number":
|
|
|
|
element = $el("tr", [
|
|
|
|
labelCell,
|
|
|
|
$el("td", [
|
|
|
|
$el("input", {
|
|
|
|
type,
|
|
|
|
value,
|
|
|
|
id: htmlID,
|
|
|
|
oninput: (e) => {
|
|
|
|
setter(e.target.value);
|
|
|
|
},
|
|
|
|
...attrs,
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
break;
|
|
|
|
case "slider":
|
|
|
|
element = $el("tr", [
|
|
|
|
labelCell,
|
|
|
|
$el("td", [
|
|
|
|
$el(
|
|
|
|
"div",
|
|
|
|
{
|
|
|
|
style: {
|
|
|
|
display: "grid",
|
|
|
|
gridAutoFlow: "column",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[
|
|
|
|
$el("input", {
|
|
|
|
...attrs,
|
|
|
|
value,
|
|
|
|
type: "range",
|
|
|
|
oninput: (e) => {
|
|
|
|
setter(e.target.value);
|
|
|
|
e.target.nextElementSibling.value = e.target.value;
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
$el("input", {
|
|
|
|
...attrs,
|
|
|
|
value,
|
|
|
|
id: htmlID,
|
|
|
|
type: "number",
|
|
|
|
style: { maxWidth: "4rem" },
|
|
|
|
oninput: (e) => {
|
|
|
|
setter(e.target.value);
|
|
|
|
e.target.previousElementSibling.value = e.target.value;
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
break;
|
|
|
|
case "combo":
|
|
|
|
element = $el("tr", [
|
|
|
|
labelCell,
|
|
|
|
$el("td", [
|
|
|
|
$el(
|
|
|
|
"select",
|
|
|
|
{
|
|
|
|
oninput: (e) => {
|
|
|
|
setter(e.target.value);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
(typeof options === "function" ? options(value) : options || []).map((opt) => {
|
|
|
|
if (typeof opt === "string") {
|
|
|
|
opt = { text: opt };
|
|
|
|
}
|
|
|
|
const v = opt.value ?? opt.text;
|
|
|
|
return $el("option", {
|
|
|
|
value: v,
|
|
|
|
textContent: opt.text,
|
|
|
|
selected: value + "" === v + "",
|
|
|
|
});
|
|
|
|
})
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
break;
|
|
|
|
case "text":
|
|
|
|
default:
|
|
|
|
if (type !== "text") {
|
|
|
|
console.warn(`Unsupported setting type '${type}, defaulting to text`);
|
|
|
|
}
|
|
|
|
|
|
|
|
element = $el("tr", [
|
|
|
|
labelCell,
|
|
|
|
$el("td", [
|
|
|
|
$el("input", {
|
|
|
|
value,
|
|
|
|
id: htmlID,
|
|
|
|
oninput: (e) => {
|
|
|
|
setter(e.target.value);
|
|
|
|
},
|
|
|
|
...attrs,
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (tooltip) {
|
|
|
|
element.title = tooltip;
|
|
|
|
}
|
|
|
|
|
|
|
|
return element;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const self = this;
|
|
|
|
return {
|
|
|
|
get value() {
|
|
|
|
return self.getSettingValue(id, defaultValue);
|
|
|
|
},
|
|
|
|
set value(v) {
|
|
|
|
self.setSettingValue(id, v);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
this.textElement.replaceChildren(
|
|
|
|
$el(
|
|
|
|
"tr",
|
|
|
|
{
|
|
|
|
style: { display: "none" },
|
|
|
|
},
|
|
|
|
[$el("th"), $el("th", { style: { width: "33%" } })]
|
|
|
|
),
|
|
|
|
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render())
|
|
|
|
);
|
|
|
|
this.element.showModal();
|
|
|
|
}
|
|
|
|
}
|