settings system & movable comfy path

This commit is contained in:
kaj
2023-07-22 12:57:43 -08:00
parent f7fdd8202d
commit b867894c19
10 changed files with 199 additions and 882 deletions
+14 -1
View File
@@ -85,6 +85,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-store",
"tauri-plugin-upload",
"tiny_http",
"zip 0.6.6",
@@ -3157,10 +3158,22 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin-store"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#c3a6dab24ccbaeada2d15caaf79e97064730e54f"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"thiserror",
]
[[package]]
name = "tauri-plugin-upload"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#0863f800b81925884a6a37c042f3d92d433d4a37"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#c3a6dab24ccbaeada2d15caaf79e97064730e54f"
dependencies = [
"futures-util",
"log",
@@ -19,6 +19,7 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.1", features = [ "os-all", "fs-all", "path-all", "process-command-api", "devtools"] }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
zip = "0.6.6"
zip-extensions = "0.6.1"
reqwest = { version = "0.11.6", features = ["blocking"] }
@@ -1,531 +0,0 @@
export const defaultGraph = {
last_node_id: 28,
last_link_id: 69,
nodes: [
{
id: 9,
type: "SaveImage",
pos: [730, 362],
size: {
0: 404.3651428222656,
1: 451.4970703125,
},
flags: {},
order: 7,
mode: 0,
inputs: [
{
name: "images",
type: "IMAGE",
link: 9,
},
],
properties: {},
widgets_values: ["ComfyUI"],
},
{
id: 3,
type: "KSampler",
pos: [374, 555],
size: {
0: 315,
1: 262,
},
flags: {},
order: 5,
mode: 0,
inputs: [
{
name: "model",
type: "MODEL",
link: 1,
},
{
name: "positive",
type: "CONDITIONING",
link: 4,
},
{
name: "negative",
type: "CONDITIONING",
link: 6,
},
{
name: "latent_image",
type: "LATENT",
link: 2,
},
{
name: "cfg",
type: "FLOAT",
link: 62,
widget: {
name: "cfg",
config: [
"FLOAT",
{
default: 8,
min: 0,
max: 100,
},
],
},
slot_index: 5,
},
{
name: "seed",
type: "INT",
link: 67,
widget: {
name: "seed",
config: [
"INT",
{
default: 0,
min: 0,
max: 18446744073709552000,
},
],
},
slot_index: 6,
},
{
name: "steps",
type: "INT",
link: 68,
widget: {
name: "steps",
config: [
"INT",
{
default: 20,
min: 1,
max: 10000,
},
],
},
slot_index: 7,
},
{
name: "sampler_name",
type: "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2",
link: 61,
widget: {
name: "sampler_name",
config: [
[
"euler",
"euler_ancestral",
"heun",
"dpm_2",
"dpm_2_ancestral",
"lms",
"dpm_fast",
"dpm_adaptive",
"dpmpp_2s_ancestral",
"dpmpp_sde",
"dpmpp_sde_gpu",
"dpmpp_2m",
"dpmpp_2m_sde",
"dpmpp_2m_sde_gpu",
"ddim",
"uni_pc",
"uni_pc_bh2",
],
],
},
slot_index: 7,
},
],
outputs: [
{
name: "LATENT",
type: "LATENT",
links: [7],
slot_index: 0,
},
],
properties: {
"Node name for S&R": "KSampler",
},
widgets_values: [
983092648072402,
"randomize",
50,
7,
"dpmpp_sde",
"normal",
1,
],
},
{
id: 6,
type: "CLIPTextEncode",
pos: [22, 503],
size: {
0: 210,
1: 54,
},
flags: {
collapsed: false,
},
order: 3,
mode: 0,
inputs: [
{
name: "clip",
type: "CLIP",
link: 3,
},
{
name: "text",
type: "STRING",
link: 64,
widget: {
name: "text",
config: [
"STRING",
{
multiline: true,
},
],
},
slot_index: 1,
},
],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [4],
slot_index: 0,
},
],
properties: {
"Node name for S&R": "CLIPTextEncode",
},
widgets_values: [
"Surreal carnival scene with bright lights, strange creatures, and a full moon, dreamlike, vibrant, stylized, surreal, high detail",
],
},
{
id: 4,
type: "CheckpointLoaderSimple",
pos: [23, 355],
size: {
0: 315,
1: 98,
},
flags: {
collapsed: false,
},
order: 2,
mode: 0,
inputs: [
{
name: "ckpt_name",
type: "v2-1_768-ema-pruned.safetensors",
link: 60,
widget: {
name: "ckpt_name",
config: [["v2-1_768-ema-pruned.safetensors"]],
},
},
],
outputs: [
{
name: "MODEL",
type: "MODEL",
links: [1],
slot_index: 0,
},
{
name: "CLIP",
type: "CLIP",
links: [3, 5],
slot_index: 1,
},
{
name: "VAE",
type: "VAE",
links: [8],
slot_index: 2,
},
],
properties: {
"Node name for S&R": "CheckpointLoaderSimple",
},
widgets_values: ["v2-1_768-ema-pruned.safetensors"],
},
{
id: 7,
type: "CLIPTextEncode",
pos: [29, 606],
size: {
0: 210,
1: 54,
},
flags: {
collapsed: false,
},
order: 4,
mode: 0,
inputs: [
{
name: "clip",
type: "CLIP",
link: 5,
},
{
name: "text",
type: "STRING",
link: 63,
widget: {
name: "text",
config: [
"STRING",
{
multiline: true,
},
],
},
slot_index: 1,
},
],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [6],
slot_index: 0,
},
],
properties: {
"Node name for S&R": "CLIPTextEncode",
},
widgets_values: [""],
},
{
id: 5,
type: "EmptyLatentImage",
pos: [32, 705],
size: {
0: 315,
1: 106,
},
flags: {},
order: 1,
mode: 0,
inputs: [
{
name: "batch_size",
type: "INT",
link: 69,
widget: {
name: "batch_size",
config: [
"INT",
{
default: 1,
min: 1,
max: 64,
},
],
},
},
{
name: "width",
type: "INT",
link: 66,
widget: {
name: "width",
config: [
"INT",
{
default: 512,
min: 64,
max: 8192,
step: 8,
},
],
},
slot_index: 1,
},
{
name: "height",
type: "INT",
link: 65,
widget: {
name: "height",
config: [
"INT",
{
default: 512,
min: 64,
max: 8192,
step: 8,
},
],
},
slot_index: 2,
},
],
outputs: [
{
name: "LATENT",
type: "LATENT",
links: [2],
slot_index: 0,
},
],
properties: {
"Node name for S&R": "EmptyLatentImage",
},
widgets_values: [768, 768, 4],
},
{
id: 8,
type: "VAEDecode",
pos: [428, 417],
size: {
0: 210,
1: 46,
},
flags: {},
order: 6,
mode: 0,
inputs: [
{
name: "samples",
type: "LATENT",
link: 7,
},
{
name: "vae",
type: "VAE",
link: 8,
},
],
outputs: [
{
name: "IMAGE",
type: "IMAGE",
links: [9],
slot_index: 0,
},
],
properties: {
"Node name for S&R": "VAEDecode",
},
},
{
id: 28,
type: "StableStudioNode",
pos: [-311, 506],
size: {
0: 151.1999969482422,
1: 206,
},
flags: {},
order: 0,
mode: 0,
outputs: [
{
name: "batch_size",
type: "INT",
links: [69],
slot_index: 0,
},
{
name: "steps",
type: "INT",
links: [68],
slot_index: 1,
},
{
name: "seed",
type: "INT",
links: [67],
slot_index: 2,
},
{
name: "width",
type: "INT",
links: [66],
slot_index: 3,
},
{
name: "height",
type: "INT",
links: [65],
slot_index: 4,
},
{
name: "positive_prompt",
type: "STRING",
links: [64],
slot_index: 5,
},
{
name: "negative_prompt",
type: "STRING",
links: [63],
slot_index: 6,
},
{
name: "cfg",
type: "FLOAT",
links: [62],
slot_index: 7,
},
{
name: "sampler_name",
type: "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2",
links: [61],
slot_index: 8,
},
{
name: "ckpt_name",
type: "v2-1_768-ema-pruned.safetensors",
links: [60],
slot_index: 9,
},
],
properties: {},
color: "#323",
bgcolor: "#535",
},
],
links: [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"],
[60, 28, 9, 4, 0, "v2-1_768-ema-pruned.safetensors"],
[
61,
28,
8,
3,
7,
"euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2",
],
[62, 28, 7, 3, 4, "FLOAT"],
[63, 28, 6, 7, 1, "STRING"],
[64, 28, 5, 6, 1, "STRING"],
[65, 28, 4, 5, 2, "INT"],
[66, 28, 3, 5, 1, "INT"],
[67, 28, 2, 3, 5, "INT"],
[68, 28, 1, 3, 6, "INT"],
[69, 28, 0, 5, 0, "INT"],
],
groups: [],
config: {},
extra: {},
version: 0.4,
};
@@ -1,286 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-this-alias */
import { app } from "../scripts/app.js";
function getWidgetType(config) {
// Special handling for COMBO so we restrict links based on the entries
let type = config[0];
let linkType = type;
if (type instanceof Array) {
type = "COMBO";
linkType = linkType.join(",");
}
return { type, linkType };
}
app.registerExtension({
name: "StableStudio.Inputs",
registerCustomNodes() {
class StableStudioNode {
color = LGraphCanvas.node_colors.purple.color;
bgcolor = LGraphCanvas.node_colors.purple.bgcolor;
constructor() {
this.addOutput("batch_size", "INT");
this.addOutput("steps", "INT");
this.addOutput("seed", "INT");
this.addOutput("width", "INT");
this.addOutput("height", "INT");
this.addOutput("positive_prompt", "STRING");
this.addOutput("negative_prompt", "STRING");
this.addOutput("cfg", "FLOAT");
this.addOutput(
"sampler_name",
"euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2"
);
this.addOutput("ckpt_name", null);
this.serialize_widgets = false;
this.isVirtualNode = true;
this.updateLoop = setInterval(async () => {
const resp = await fetch("/object_info", { cache: "no-cache" });
const data = await resp.json();
if (data) {
const models =
data.CheckpointLoader?.input?.required?.ckpt_name?.[0] ?? [];
const samplers =
data.KSampler?.input?.required?.sampler_name?.[0] ?? [];
if (!this.stableValues.ckpt_name) {
this.stableValues.ckpt_name = models[0];
}
if (!this.stableValues.sampler_name) {
this.stableValues.sampler_name = samplers[0];
}
this.outputs[8].type = samplers.join(",");
this.outputs[9].type = models.join(",");
}
}, 15000);
this.stableValues = {
batch_size: 4,
steps: 50,
seed: 0,
width: 512,
height: 512,
positive_prompt: "",
negative_prompt: "",
cfg: 7,
sampler_name: "",
ckpt_name: "",
};
}
applyToGraph() {
if (!this.outputs.some((o) => o.links?.length)) return;
function get_links(node) {
let links = [];
for (const o of node.outputs) {
if (!o.links?.length) continue;
for (const l of o.links) {
const linkInfo = app.graph.links[l];
const n = node.graph.getNodeById(linkInfo.target_id);
if (n.type == "Reroute") {
links = links.concat(get_links(n));
} else {
links.push(l);
}
}
}
return links;
}
let links = get_links(this);
// For each output link copy our value over the original widget value
for (const l of links) {
const linkInfo = app.graph.links[l];
const node = this.graph.getNodeById(linkInfo.target_id);
const input = node.inputs[linkInfo.target_slot];
const widgetName = input.widget.name;
if (widgetName) {
const widget = node.widgets.find((w) => w.name === widgetName);
if (widget) {
let stableValue = this.outputs.find(
(o) => o.slot_index === linkInfo.origin_slot
)?.name;
if (!stableValue) {
// try to match with name
stableValue = this.outputs.find(
(o) => o.name === input.name
)?.name;
}
if (!stableValue) continue;
widget.value = this.stableValues[stableValue] ?? widget.value;
if (widget.callback) {
widget.callback(
widget.value,
app.canvas,
node,
app.canvas.graph_mouse,
{}
);
}
}
}
}
}
onConnectionsChange(_, index, connected) {
console.log("onConnectionsChange", index, connected);
// if (connected) {
// if (this.outputs[0].links?.length) {
// if (!this.widgets?.length) {
// this.#onFirstConnection();
// }
// if (!this.widgets?.length && this.outputs[0].widget) {
// // On first load it often cant recreate the widget as the other node doesnt exist yet
// // Manually recreate it from the output info
// this.#createWidget(this.outputs[0].widget.config);
// }
// }
// } else if (!this.outputs[0].links?.length) {
// this.#onLastDisconnect();
// }
}
onConnectOutput(slot, type, input) {
// Fires before the link is made allowing us to reject it if it isn't valid
// No widget, we cant connect
// if (!input.widget) {
// if (!(input.type in ComfyWidgets)) return false;
// }
// if (this.outputs[slot].links?.length) {
// return this.#isValidConnection(input);
// }
// let a STRING connect to a ckpt_name
if (
this.outputs[slot].type === "STRING" &&
input.name === "ckpt_name"
) {
return true;
}
}
#onFirstConnection() {
// First connection can fire before the graph is ready on initial load so random things can be missing
const linkId = this.outputs[0].links[0];
const link = this.graph.links[linkId];
if (!link) return;
const theirNode = this.graph.getNodeById(link.target_id);
if (!theirNode || !theirNode.inputs) return;
const input = theirNode.inputs[link.target_slot];
if (!input) return;
var _widget;
if (!input.widget) {
if (!(input.type in ComfyWidgets)) return;
_widget = { name: input.name, config: [input.type, {}] }; //fake widget
} else {
_widget = input.widget;
}
const widget = _widget;
const { type, linkType } = getWidgetType(widget.config);
// Update our output to restrict to the widget type
this.outputs[0].type = linkType;
this.outputs[0].name = type;
this.outputs[0].widget = widget;
this.#createWidget(widget.config, theirNode, widget.name);
}
#createWidget(inputData, node, widgetName) {
let type = inputData[0];
if (type instanceof Array) {
type = "COMBO";
}
let widget;
if (type in ComfyWidgets) {
widget = (ComfyWidgets[type](this, "value", inputData, app) || {})
.widget;
} else {
widget = this.addWidget(type, "value", null, () => {}, {});
}
if (node?.widgets && widget) {
const theirWidget = node.widgets.find((w) => w.name === widgetName);
if (theirWidget) {
widget.value = theirWidget.value;
}
}
if (widget.type === "number" || widget.type === "combo") {
addValueControlWidget(this, widget, "fixed");
}
// When our value changes, update other widgets to reflect our changes
// e.g. so LoadImage shows correct image
const callback = widget.callback;
const self = this;
widget.callback = function () {
const r = callback ? callback.apply(this, arguments) : undefined;
self.applyToGraph();
return r;
};
// Grow our node if required
const sz = this.computeSize();
if (this.size[0] < sz[0]) {
this.size[0] = sz[0];
}
if (this.size[1] < sz[1]) {
this.size[1] = sz[1];
}
requestAnimationFrame(() => {
if (this.onResize) {
this.onResize(this.size);
}
});
}
#isValidConnection(input) {
// Only allow connections where the configs match
const config1 = this.outputs[0].widget.config;
const config2 = input.widget.config;
if (config1[0] instanceof Array) {
// These checks shouldnt actually be necessary as the types should match
// but double checking doesn't hurt
// New input isnt a combo
if (!(config2[0] instanceof Array)) return false;
// New imput combo has a different size
if (config1[0].length !== config2[0].length) return false;
// New input combo has different elements
if (config1[0].find((v, i) => config2[0][i] !== v)) return false;
} else if (config1[0] !== config2[0]) {
// Configs dont match
return false;
}
for (const k in config1[1]) {
if (k !== "default") {
if (config1[1][k] !== config2[1][k]) {
return false;
}
}
}
return true;
}
}
LiteGraph.registerNodeType(
"StableStudioNode",
Object.assign(StableStudioNode, {
title: "StableStudio Input",
})
);
StableStudioNode.category = "stablestudio";
},
});
+54 -29
View File
@@ -1,6 +1,7 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use settings::Settings;
use std::collections::HashMap;
use std::fs::File;
use std::sync::OnceLock;
@@ -15,6 +16,7 @@ use tauri::{RunEvent, Window, WindowBuilder, WindowUrl};
use tauri_plugin_upload;
mod server;
mod settings;
mod show_path;
static WINDOW: OnceLock<Window> = OnceLock::new();
@@ -54,6 +56,23 @@ fn main() {
)
.build()?;
let app_data_dir = app.path_resolver().app_data_dir().unwrap();
let _ = std::fs::create_dir_all(&app_data_dir);
let mut store_location = app_data_dir.clone();
store_location.push("settings.json");
let comfy_location = app_data_dir.clone();
settings::create_settings(
store_location,
Settings {
comfyui_location: Box::new(comfy_location.to_str().unwrap().to_string()),
comfyui_url: Box::new("http://localhost:5000".to_string()),
},
)
.unwrap();
_ = WINDOW.set(window);
Ok(())
@@ -62,7 +81,9 @@ fn main() {
.invoke_handler(tauri::generate_handler![
extract_comfy,
launch_comfy,
show_path::show_in_folder
show_path::show_in_folder,
settings::get_setting,
settings::set_setting,
])
.build(context)
.expect("error while building tauri application")
@@ -78,12 +99,18 @@ fn main() {
// tauri command to extract a zip from an arbitrary file path
#[tauri::command]
fn extract_comfy(
handle: tauri::AppHandle,
path: String,
target_dir: String,
) -> Result<String, String> {
println!("extracting zip from {} to {}", path, target_dir);
fn extract_comfy(handle: tauri::AppHandle) -> Result<String, String> {
let mut path = handle.path_resolver().app_data_dir().unwrap();
path.push("comfyui.zip");
let target_dir = *settings::SETTINGS
.get()
.unwrap()
.data
.comfyui_location
.clone();
println!("extracting zip from {:?} to {}", path, target_dir);
let file = File::open(path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
@@ -96,25 +123,6 @@ fn extract_comfy(
// dont block the main thread
extract_thread.join().unwrap();
let default_graph = handle
.path_resolver()
.resolve_resource("resources/defaultGraph.js")
.expect("failed to resolve resource");
let stable_inputs = handle
.path_resolver()
.resolve_resource("resources/stableStudioInputs.js")
.expect("failed to resolve resource");
let mut dest = std::path::PathBuf::from(&target_dir);
dest.push("ComfyUI/ComfyUI/web/scripts/defaultGraph.js");
std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
std::fs::copy(default_graph, dest).unwrap();
let mut dest = std::path::PathBuf::from(&target_dir);
dest.push("ComfyUI/ComfyUI/web/extensions/stableStudioInputs.js");
std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
std::fs::copy(stable_inputs, dest).unwrap();
println!("extracted zip");
Ok("completed".to_string())
}
@@ -168,7 +176,17 @@ async fn watch_comfy(
// tauri command to launch python process
#[tauri::command]
async fn launch_comfy(path: String) -> Result<String, String> {
async fn launch_comfy() -> Result<String, String> {
let comfyui_location = *settings::SETTINGS
.get()
.unwrap()
.data
.comfyui_location
.clone();
let mut path = std::path::PathBuf::from(comfyui_location.clone());
path.push("ComfyUI");
let url = *settings::SETTINGS.get().unwrap().data.comfyui_url.clone();
// set working directory
std::env::set_current_dir(path.clone()).unwrap();
@@ -178,7 +196,7 @@ async fn launch_comfy(path: String) -> Result<String, String> {
.connect_timeout(std::time::Duration::from_secs(1))
.build()
.unwrap();
let resp = client.get("http://localhost:5000").send().await;
let resp = client.get(url.clone()).send().await;
if resp.is_ok() {
println!("Comfy already running, skipping launch.");
return Ok("completed".to_string());
@@ -198,7 +216,14 @@ async fn launch_comfy(path: String) -> Result<String, String> {
panic!("Unsupported platform")
}
})
.args(["-s", "ComfyUI/main.py", "--port", "5000"])
.args([
"-s",
"ComfyUI/main.py",
"--port",
url.split(":").collect::<Vec<&str>>().last().unwrap(),
"--preview-method",
"auto",
])
.envs(HashMap::from([("PYTHONUNBUFFERED".into(), "1".into())]))
.spawn()
.expect("Failed to spawn ComfyUI process");
@@ -7,6 +7,8 @@ use tauri::{
};
use tiny_http::{Header, Response as HttpResponse, Server};
use crate::settings;
pub struct Response {
headers: HashMap<String, String>,
}
@@ -36,14 +38,10 @@ impl Builder {
let server = Server::http(&format!("localhost:{port}")).expect("Unable to spawn server");
let server = Arc::new(server);
let mut guards = Vec::with_capacity(4);
let path_resolver = app.path_resolver();
let appdata_path = path_resolver.app_data_dir().unwrap().as_os_str().to_str().unwrap().to_string();
for _ in 0..5 {
let server = server.clone();
let asset_resolver = app.asset_resolver();
let appdata_path = appdata_path.clone();
let guard = std::thread::spawn(move || {
// aquire a copy of appdata_path
for mut req in server.incoming_requests() {
@@ -84,7 +82,7 @@ impl Builder {
None,
None,
Some(
"http://localhost:5000/".to_string() + &sliced_path.join("/") + "?" + &req.url().split("?").collect::<Vec<&str>>()[1..].join("?"),
settings::SETTINGS.get().unwrap().data.comfyui_url.to_string() + "/" + &sliced_path.join("/") + "?" + &req.url().split("?").collect::<Vec<&str>>()[1..].join("?"),
),
),
[] | [_, _, ..] | [&_] => (None, None, None),
@@ -93,7 +91,7 @@ impl Builder {
if let Some(file_name) = file_name {
let path_name = format!(
"{}/ComfyUI/ComfyUI/web/{}",
appdata_path.clone(),
settings::SETTINGS.get().unwrap().data.comfyui_location.to_string(),
file_name
);
@@ -0,0 +1,100 @@
use std::{
error::Error,
fs::File,
io::BufReader,
path::{Path, PathBuf},
sync::OnceLock,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Settings {
pub comfyui_location: Box<String>,
pub comfyui_url: Box<String>,
}
#[derive(Clone)]
pub struct Store {
pub path: PathBuf,
pub data: Settings,
}
impl Store {
pub fn save(&self) -> Result<(), Box<dyn Error>> {
let file = File::create(&self.path)?;
serde_json::to_writer(file, &self.data)?;
Ok(())
}
}
impl Clone for Settings {
fn clone(&self) -> Self {
Settings {
comfyui_location: self.comfyui_location.clone(),
comfyui_url: self.comfyui_url.clone(),
}
}
}
pub static SETTINGS: OnceLock<Store> = OnceLock::new();
pub fn create_settings(path: PathBuf, default: Settings) -> Result<&'static Store, Box<dyn Error>> {
let file = match File::open(&path) {
Ok(f) => f,
Err(_) => {
let file = File::create(&path.as_path())?;
serde_json::to_writer(file, &default)?;
return create_settings(path, default);
}
};
let reader = BufReader::new(file);
let u = match serde_json::from_reader::<_, Settings>(reader) {
Ok(u) => u,
Err(_) => {
let file = File::create(&path.as_path())?;
serde_json::to_writer(file, &default)?;
default
}
};
let s = Store { path, data: u };
let _ = SETTINGS.set(s);
Ok(SETTINGS.get().unwrap())
}
#[tauri::command]
pub fn get_setting(key: String) -> Result<String, String> {
let s = SETTINGS.get().unwrap();
match key.as_str() {
"comfyui_location" => Ok(s.data.comfyui_location.to_string()),
"comfyui_url" => Ok(s.data.comfyui_url.to_string()),
_ => Err("Invalid key".to_string()),
}
}
#[tauri::command]
pub fn set_setting(key: String, value: String) -> Result<(), String> {
let mut s = SETTINGS.get().unwrap().clone();
match key.as_str() {
"comfyui_location" => {
let path = Path::new(&value);
if !path.exists() {
return Err("Invalid path".to_string());
}
s.data.comfyui_location = Box::new(value);
}
"comfyui_url" => {
s.data.comfyui_url = Box::new(value);
}
_ => return Err("Invalid key".to_string()),
}
let _ = s.save();
let _ = SETTINGS.set(s);
Ok(())
}
@@ -13,7 +13,7 @@
"allowlist": {
"fs": {
"all": true,
"scope": ["$APPDATA/**", "$RESOURCE/**"]
"scope": ["$APPDATA/**"]
},
"path": {
"all": true
@@ -46,9 +46,7 @@
"providerShortName": null,
"signingIdentity": null
},
"resources": [
"resources/*"
],
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
+17 -22
View File
@@ -117,38 +117,40 @@ export namespace App {
if (isSetup !== SetupState.NotStarted || nonce.current !== 0) return;
nonce.current++;
const appDataPath = await appDataDir();
const comfyui_location = await invoke("get_setting", {
key: "comfyui_location",
})
.then((res) => res)
.catch(() => appDataPath);
let entries: FileEntry[] = await listAppDataDir(
"comfyui/ComfyUI/models/checkpoints"
`${comfyui_location}/ComfyUI/ComfyUI/models/checkpoints`
);
// filter for actual files (not directories or symlinks or "put_checkpoints_here" files)
entries = entries.filter((entry) => entry.name?.includes("."));
console.log(entries);
const appDataPath = await appDataDir();
if (entries.length === 0) {
const comfyExists = await exists("comdyui/ComfyUI/main.py", {
dir: BaseDirectory.AppData,
});
const comfyExists = await exists(
`${comfyui_location}/ComfyUI/ComfyUI/main.py`
);
if (!comfyExists) {
setIsSetup(SetupState.NotStarted);
setMessage("Downloading ComfyUI...");
// delete the old comfyui zip if it exists
if (
!(await exists("comfyui.zip", {
dir: BaseDirectory.AppData,
}))
) {
if (!(await exists(`${comfyui_location}/comfyui.zip`))) {
let comulativeProgress = 0;
console.log("downloading comfyui");
await download(
"https://pub-5e5adf378ed14628a527d735b7743e4e.r2.dev/stability-downloads/ComfyUI/ComfyUI_windows_portable.zip",
`${appDataPath}\\comfyui.zip`,
`${comfyui_location}\\comfyui.zip`,
(p, total) => {
comulativeProgress += p;
setProgress(comulativeProgress / total);
@@ -159,10 +161,7 @@ export namespace App {
setMessage("Extracting ComfyUI...");
setProgress(null);
try {
const result = await invoke("extract_comfy", {
path: `${appDataPath}/comfyui.zip`,
targetDir: `${appDataPath}`,
});
const result = await invoke("extract_comfy");
if (result !== "completed") {
throw new Error("Failed to extract comfyui.zip");
@@ -174,9 +173,7 @@ export namespace App {
return;
}
await removeFile("comfyui.zip", {
dir: BaseDirectory.AppData,
});
await removeFile(`${comfyui_location}/comfyui.zip`);
}
setIsSetup(SetupState.ComfyInstalled);
@@ -188,7 +185,7 @@ export namespace App {
await download(
"https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors",
`${appDataPath}/comfyui/ComfyUI/models/checkpoints/v2-1_768-ema-pruned.safetensors`,
`${comfyui_location}/ComfyUI/ComfyUI/models/checkpoints/v2-1_768-ema-pruned.safetensors`,
(p, total) => {
comulativeProgress += p;
setProgress(comulativeProgress / total);
@@ -211,9 +208,7 @@ export namespace App {
setUnlisteners([unlisten]);
// start comfy
const result = await invoke("launch_comfy", {
path: `${appDataPath}/comfyui`,
});
const result = await invoke("launch_comfy");
if (result !== "completed") {
setMessage(`Error launching ComfyUI: ${result}`);
@@ -1,7 +1,7 @@
import { PluginStatus } from "@stability/stablestudio-plugin";
import { getTauriVersion, getVersion } from "@tauri-apps/api/app";
import { version as getOsVerison, platform } from "@tauri-apps/api/os";
import { appDataDir } from "@tauri-apps/api/path";
import { appDataDir, sep } from "@tauri-apps/api/path";
import { invoke } from "@tauri-apps/api/tauri";
import { Link } from "react-router-dom";
@@ -60,8 +60,12 @@ export function Settings() {
useEffect(() => {
async function fetchComfyLocation() {
const path = await appDataDir();
setComfyLocation(`${path}ComfyUI`);
const path = await invoke("get_setting", {
key: "comfyui_location",
})
.then((res) => res)
.catch(() => appDataDir());
setComfyLocation(`${path}${sep}ComfyUI`);
}
async function fetchVersion() {