Basic settings screen
This commit is contained in:
148
src/lib/stores/configDefs.ts
Normal file
148
src/lib/stores/configDefs.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Supported config option types.
|
||||
*/
|
||||
type ConfigDefType = "boolean" | "number" | "string" | "string[]";
|
||||
|
||||
// A simple parameter description interface
|
||||
export interface ConfigDef<IdType extends string, TypeType extends ConfigDefType, ValueType, OptionsType = any> {
|
||||
// This generic `IdType` is what makes the "array of keys and values to new
|
||||
// interface definition" thing work
|
||||
name: IdType;
|
||||
|
||||
type: TypeType,
|
||||
|
||||
description?: string,
|
||||
|
||||
category: string,
|
||||
|
||||
defaultValue: ValueType,
|
||||
|
||||
options: OptionsType
|
||||
}
|
||||
|
||||
export type ConfigDefAny = ConfigDef<string, any, any>
|
||||
type ConfigDefBoolean<IdType extends string> = ConfigDef<IdType, "boolean", boolean>;
|
||||
|
||||
type NumberOptions = {
|
||||
min?: number,
|
||||
max?: number,
|
||||
step: number
|
||||
}
|
||||
type ConfigDefNumber<IdType extends string> = ConfigDef<IdType, "number", number, NumberOptions>;
|
||||
|
||||
type ConfigDefString<IdType extends string> = ConfigDef<IdType, "string", string>;
|
||||
type ConfigDefStringArray<IdType extends string> = ConfigDef<IdType, "string[]", string[]>;
|
||||
|
||||
// Configuration parameters ------------------------------------
|
||||
|
||||
const defComfyUIHostname: ConfigDefString<"comfyUIHostname"> = {
|
||||
name: "comfyUIHostname",
|
||||
type: "string",
|
||||
defaultValue: "localhost",
|
||||
category: "backend",
|
||||
description: "Backend domain for ComfyUI",
|
||||
options: {}
|
||||
};
|
||||
|
||||
const defComfyUIPort: ConfigDefNumber<"comfyUIPort"> = {
|
||||
name: "comfyUIPort",
|
||||
type: "number",
|
||||
defaultValue: 8188,
|
||||
category: "backend",
|
||||
description: "Backend port for ComfyUI",
|
||||
options: {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
step: 1
|
||||
}
|
||||
};
|
||||
|
||||
const defAlwaysStripUserState: ConfigDefBoolean<"alwaysStripUserState"> = {
|
||||
name: "alwaysStripUserState",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: "behavior",
|
||||
description: "Strip user state even if saving to local storage",
|
||||
options: {}
|
||||
};
|
||||
|
||||
const defPromptForWorkflowName: ConfigDefBoolean<"promptForWorkflowName"> = {
|
||||
name: "promptForWorkflowName",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: "behavior",
|
||||
description: "When saving, always prompt for a name to save the workflow as",
|
||||
options: {}
|
||||
};
|
||||
|
||||
const defConfirmWhenUnloadingUnsavedChanges: ConfigDefBoolean<"confirmWhenUnloadingUnsavedChanges"> = {
|
||||
name: "confirmWhenUnloadingUnsavedChanges",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
category: "behavior",
|
||||
description: "When closing the tab, open the confirmation window if there's unsaved changes",
|
||||
options: {}
|
||||
};
|
||||
|
||||
const defCacheBuiltInResources: ConfigDefBoolean<"cacheBuiltInResources"> = {
|
||||
name: "cacheBuiltInResources",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
category: "behavior",
|
||||
description: "Cache loading of built-in resources to save network use",
|
||||
options: {}
|
||||
};
|
||||
|
||||
const defBuiltInTemplates: ConfigDefStringArray<"builtInTemplates"> = {
|
||||
name: "builtInTemplates",
|
||||
type: "string[]",
|
||||
defaultValue: ["ControlNet", "LoRA x5", "Model Loader", "Positive_Negative", "Seed Randomizer"],
|
||||
category: "templates",
|
||||
description: "Basenames of templates that can be loaded from public/templates. Saves LocalStorage space.",
|
||||
options: {}
|
||||
};
|
||||
|
||||
// Configuration exports ------------------------------------
|
||||
|
||||
export const CONFIG_DEFS = [
|
||||
defComfyUIHostname,
|
||||
defComfyUIPort,
|
||||
defAlwaysStripUserState,
|
||||
defPromptForWorkflowName,
|
||||
defConfirmWhenUnloadingUnsavedChanges,
|
||||
defCacheBuiltInResources,
|
||||
defBuiltInTemplates,
|
||||
] as const;
|
||||
|
||||
export const CONFIG_DEFS_BY_NAME: Record<string, ConfigDefAny>
|
||||
= CONFIG_DEFS.reduce((dict, def) => {
|
||||
if (def.name in dict)
|
||||
throw new Error(`Duplicate named config definition: ${def.name}`)
|
||||
dict[def.name] = def;
|
||||
return dict
|
||||
}, {})
|
||||
|
||||
export const CONFIG_DEFS_BY_CATEGORY: Record<string, ConfigDefAny[]>
|
||||
= CONFIG_DEFS.reduce((dict, def) => {
|
||||
dict[def.category] ||= []
|
||||
dict[def.category].push(def)
|
||||
return dict
|
||||
}, {})
|
||||
|
||||
export const CONFIG_CATEGORIES: string[]
|
||||
= CONFIG_DEFS.reduce((arr, def) => {
|
||||
if (!arr.includes(def.category))
|
||||
arr.push(def.category)
|
||||
return arr
|
||||
}, [])
|
||||
|
||||
type Config<T extends ReadonlyArray<Readonly<ConfigDef<string, ConfigDefType, any>>>> = {
|
||||
[K in T[number]["name"]]: Extract<T[number], { name: K }>["defaultValue"]
|
||||
} extends infer O
|
||||
? { [P in keyof O]: O[P] }
|
||||
: never;
|
||||
|
||||
export type ConfigState = Config<typeof CONFIG_DEFS>
|
||||
|
||||
const pairs: [string, any][] = CONFIG_DEFS.map(item => { return [item.name, structuredClone(item.defaultValue)] })
|
||||
export const defaultConfig: ConfigState = pairs.reduce((dict, v) => { dict[v[0]] = v[1]; return dict; }, {}) as any;
|
||||
@@ -1,130 +1,85 @@
|
||||
import { debounce } from '$lib/utils';
|
||||
import { toHashMap } from '@litegraph-ts/core';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { z, type ZodTypeAny } from "zod"
|
||||
|
||||
type ConfigDefType = "boolean" | "number" | "string" | "string[]";
|
||||
import { defaultConfig, type ConfigState, type ConfigDefAny, CONFIG_DEFS_BY_NAME } from './configDefs';
|
||||
|
||||
type ConfigStateOps = {
|
||||
getBackendURL: () => string,
|
||||
save: () => void,
|
||||
load: () => ConfigState
|
||||
load: (data: any) => ConfigState
|
||||
loadDefault: () => ConfigState
|
||||
setConfigOption: (def: ConfigDefAny, v: any) => boolean
|
||||
validateConfigOption: (def: ConfigDefAny, v: any) => boolean
|
||||
}
|
||||
|
||||
export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
||||
const store: Writable<ConfigState> = writable(
|
||||
{
|
||||
comfyUIHostname: "localhost",
|
||||
comfyUIPort: 8188,
|
||||
alwaysStripUserState: false,
|
||||
promptForWorkflowName: false,
|
||||
confirmWhenUnloadingUnsavedChanges: true,
|
||||
builtInTemplates: [],
|
||||
cacheBuiltInResources: true,
|
||||
})
|
||||
|
||||
type Conf2 = {
|
||||
name: string;
|
||||
type: ZodTypeAny;
|
||||
defaultValue: any;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const def2ComfyUIHostname: Conf2 = {
|
||||
name: 'backend.comfyUIHostname',
|
||||
type: z.string(),
|
||||
defaultValue: 'localhost',
|
||||
description: 'Backend domain for ComfyUI',
|
||||
};
|
||||
|
||||
const def2ComfyUIPort: Conf2 = {
|
||||
name: 'backend.comfyUIPort',
|
||||
type: z.number(),
|
||||
defaultValue: 8188,
|
||||
description: 'Backend port for ComfyUI',
|
||||
};
|
||||
|
||||
const def2AlwaysStripUserState: Conf2 = {
|
||||
name: 'behavior.alwaysStripUserState',
|
||||
type: z.boolean(),
|
||||
defaultValue: false,
|
||||
description: 'Strip user state even if saving to local storage',
|
||||
};
|
||||
|
||||
export const allconfs: ReadonlyArray<Conf2> = [
|
||||
def2ComfyUIHostname,
|
||||
def2ComfyUIPort,
|
||||
def2AlwaysStripUserState,
|
||||
] as const;
|
||||
|
||||
const confItems: any = {};
|
||||
const confDefaults: any = {}
|
||||
|
||||
for (const item of allconfs) {
|
||||
const trail = item.name.split('.');
|
||||
let theI = confItems;
|
||||
let theDef = confDefaults;
|
||||
for (const category of trail.slice(0, -1)) {
|
||||
theI[category] ||= { __category__: true };
|
||||
theI = theI[category];
|
||||
|
||||
theDef[category] ||= {};
|
||||
theDef = theDef[category];
|
||||
}
|
||||
const optionName = trail[trail.length - 1];
|
||||
theI[optionName] = item;
|
||||
theDef[optionName] = item.defaultValue;
|
||||
}
|
||||
|
||||
function recurse(item: Record<string, any>, trail: string[] = []): ZodTypeAny {
|
||||
let defaultValue = confDefaults
|
||||
for (const name of trail) {
|
||||
defaultValue = defaultValue[name];
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(item)) {
|
||||
if (value.__category__) {
|
||||
delete value['__category__'];
|
||||
item[key] = recurse(value, trail.concat(key));
|
||||
} else {
|
||||
const result = value.type.safeParse(value.defaultValue);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Default value for config item ${value.name} did not pass type matcher: ${result.error}`
|
||||
);
|
||||
}
|
||||
item[key] = value.type.catch(value.defaultValue);
|
||||
}
|
||||
}
|
||||
return z.object(item).catch({ ...defaultValue });
|
||||
}
|
||||
|
||||
export const Config2 = recurse(confItems);
|
||||
export type ConfigStore = z.infer<typeof Config2>;
|
||||
|
||||
const stor: ConfigStore = {
|
||||
backend: {},
|
||||
a: "foo"
|
||||
}
|
||||
const store: Writable<ConfigState> = writable({ ...defaultConfig })
|
||||
|
||||
function getBackendURL(): string {
|
||||
const state = get(store);
|
||||
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}`
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
||||
function validateConfigOption(def: ConfigDefAny, v: any): boolean {
|
||||
switch (def.type) {
|
||||
case "boolean":
|
||||
return typeof v === "boolean";
|
||||
case "number":
|
||||
return typeof v === "number";
|
||||
case "string":
|
||||
return typeof v === "string";
|
||||
case "string[]":
|
||||
return Array.isArray(v) && v.every(vs => typeof vs === "string");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function load(): ConfigState {
|
||||
function setConfigOption(def: ConfigDefAny, v: any): boolean {
|
||||
let valid = false;
|
||||
store.update(state => {
|
||||
valid = validateConfigOption(def, v);
|
||||
|
||||
if (!valid) {
|
||||
console.warn(`[configState] Invalid value for option ${def.name} (${v}), setting to default (${def.defaultValue})`);
|
||||
state[def.name] = structuredClone(def.defaultValue);
|
||||
}
|
||||
else {
|
||||
state[def.name] = v
|
||||
}
|
||||
|
||||
return state;
|
||||
})
|
||||
return valid;
|
||||
}
|
||||
|
||||
function load(data: any): ConfigState {
|
||||
store.set({ ...defaultConfig })
|
||||
if (data != null && typeof data === "object") {
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
const def = CONFIG_DEFS_BY_NAME[k]
|
||||
if (def == null) {
|
||||
delete data[k]
|
||||
continue;
|
||||
}
|
||||
|
||||
setConfigOption(def, v);
|
||||
}
|
||||
}
|
||||
|
||||
return get(store);
|
||||
}
|
||||
|
||||
function loadDefault() {
|
||||
return load(null);
|
||||
}
|
||||
|
||||
const configStateStore: WritableConfigStateStore =
|
||||
{
|
||||
...store,
|
||||
getBackendURL,
|
||||
save,
|
||||
load
|
||||
validateConfigOption,
|
||||
setConfigOption,
|
||||
load,
|
||||
loadDefault
|
||||
}
|
||||
export default configStateStore;
|
||||
|
||||
@@ -690,7 +690,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
] as const;
|
||||
|
||||
// This is needed so the specs can be iterated with svelte's keyed #each.
|
||||
let i = 0;
|
||||
|
||||
Reference in New Issue
Block a user