Compare commits
29 Commits
stablestud
...
tauri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3b7d2048e | ||
|
|
4469313106 | ||
|
|
d842066f51 | ||
|
|
e9657bb20f | ||
|
|
0a92760725 | ||
|
|
753eb570b7 | ||
|
|
44aac3087d | ||
|
|
3317206d3e | ||
|
|
0065f05d3b | ||
|
|
d44d5d1d8f | ||
|
|
b867894c19 | ||
|
|
f7fdd8202d | ||
|
|
12a086302c | ||
|
|
47b4fa31e0 | ||
|
|
61d872b37f | ||
|
|
5ed8642175 | ||
|
|
8bb42328d6 | ||
|
|
110c8dcf08 | ||
|
|
69d381aef6 | ||
|
|
d710f32f70 | ||
|
|
ecaf1a9ae3 | ||
|
|
2dadc6f5ec | ||
|
|
fbf24d80b4 | ||
|
|
c359a44be6 | ||
|
|
e7089b8a7b | ||
|
|
4d435a4d6d | ||
|
|
f4a9d4e4af | ||
|
|
bef5fe60f3 | ||
|
|
4881e96a22 |
6
.github/workflows/build.yml
vendored
@@ -6,6 +6,10 @@ on:
|
||||
branches:
|
||||
- tauri
|
||||
|
||||
concurrency:
|
||||
group: tauri-build
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
@@ -39,5 +43,5 @@ jobs:
|
||||
tagName: stablestudio-v__VERSION__
|
||||
releaseName: "StableStudio v__VERSION__"
|
||||
releaseBody: "See the assets to download this version and install."
|
||||
releaseDraft: true
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
|
||||
13
.github/workflows/comfy_windows.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
branches:
|
||||
- tauri
|
||||
|
||||
concurrency:
|
||||
group: comfyui_windows
|
||||
|
||||
jobs:
|
||||
repackage_comfyui:
|
||||
permissions:
|
||||
@@ -20,7 +23,7 @@ jobs:
|
||||
sudo apt-get install -y p7zip-full
|
||||
sudo apt-get install -y zip
|
||||
|
||||
wget -nv https://github.com/comfyanonymous/ComfyUI/releases/download/latest/ComfyUI_windows_portable_nvidia_cu118_or_cpu.7z
|
||||
wget -nv https://github.com/KAJdev/ComfyUI/releases/download/latest/new_ComfyUI_windows_portable_nvidia_cu118_or_cpu.7z -O ComfyUI_windows_portable_nvidia_cu118_or_cpu.7z
|
||||
|
||||
7z x ComfyUI_windows_portable_nvidia_cu118_or_cpu.7z
|
||||
|
||||
@@ -33,13 +36,7 @@ jobs:
|
||||
find ComfyUI -type f -name '*.pyo' -delete
|
||||
find ComfyUI -type d -name '__pycache__' -delete
|
||||
|
||||
# remove ALL .git files recursively
|
||||
rm -rf ComfyUI/.git
|
||||
rm -rf ComfyUI/.github
|
||||
rm -rf ComfyUI/.ci
|
||||
rm -rf ComfyUI/.gitignore
|
||||
|
||||
zip -r -9 -q ComfyUI_windows_portable.zip ComfyUI -x "*.DS_Store" -x "__MACOSX" -x "*.git*" -x "*.ci*" -x "*.github*" -x "*.gitignore*" -x "*.pyc" -x "*.pyo" -x "__pycache__"
|
||||
zip -r -9 -q ComfyUI_windows_portable.zip ComfyUI -x "*.DS_Store" -x "__MACOSX" -x "*.pyc" -x "*.pyo" -x "__pycache__"
|
||||
du -sh ComfyUI_windows_portable.zip
|
||||
|
||||
mkdir ComfyUI_bundle
|
||||
|
||||
105
README.md
@@ -1,57 +1,32 @@
|
||||
<div align="center" style="display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 1em; margin: 4em 0;">
|
||||
|
||||
<img src="./misc/Banner.png" />
|
||||
<img src="./misc/GenerateScreenshot.png" style="width: 400px; max-width: 600px; flex-grow: 1;" />
|
||||
<img src="./misc/EditScreenshot.png" style="width: 400px; max-width: 600px; flex-grow: 1;" />
|
||||
|
||||
<h3>👋 Welcome to StableStudio, the open-source version of <a href="https://dreamstudio.ai" target="_blank">DreamStudio</a>!</h3>
|
||||
|
||||
**🗺 Contents – [🚀 Quick Start](#quick-start) · [ℹ️ About](#about) · [🙋 FAQ](#faq) · [🧑💻 Contributing](#contributing)**
|
||||
|
||||
**📚 Documentation – [🎨 UI](./packages/stablestudio-ui/README.md) · [🔌 Plugins](./packages/stablestudio-plugin/README.md) · <a href="https://platform.stability.ai" target="_blank">⚡️ platform.stability.ai</a>**
|
||||
**📚 Documentation – [🎨 UI](./packages/stablestudio-ui/README.md) · <a href="https://platform.stability.ai" target="_blank">⚡️ platform.stability.ai</a>**
|
||||
|
||||
**🔗 Links – <a href="https://discord.com/channels/1002292111942635562/1108055793674227782" target="_blank">🎮 Discord</a> · <a href="https://dreamstudio.ai" target="_blank">🌈 DreamStudio</a> · <a href="https://github.com/Stability-AI/StableStudio/issues">🛟 Bugs & Support</a> · <a href="https://github.com/Stability-AI/StableStudio/discussions">💬 Discussion</a>**
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
# <a id="quick-start" href="#quick-start">🚀 Quick Start</a>
|
||||
# 🚀 Installation
|
||||
|
||||
You'll need to have [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/) installed. Then run the following commands to install dependencies and launch StableStudio.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Stability-AI/StableStudio.git
|
||||
```
|
||||
|
||||
```bash
|
||||
cd StableStudio
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
You'll need to head over to the [Releases](https://github.com/Stability-AI/StableStudio/releases) page to download StableStudio for your operating system. StableStudio will download and install its own managed copy of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) and the Stable Diffusion weights.
|
||||
|
||||
_**That's it! 🎉**_
|
||||
|
||||
StableStudio will be running at [localhost:3000](http://localhost:3000) by default.
|
||||
|
||||
> If you are using the default Stability API plugin, You'll need to have your [API key](https://platform.stability.ai/docs/getting-started/authentication) handy. Otherwise, you should be good to go!
|
||||
|
||||
# <a id="about" href="#about">About</a>
|
||||
# About
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 1em; margin: 0 0 2em 0;">
|
||||
<img src="./misc/PainterWithRobot.png" style="flex-grow: 1; flex-shrink: 1;" />
|
||||
<img src="./misc/aboutReadmeImage.png" style="flex-grow: 1; flex-shrink: 1;" />
|
||||
</div>
|
||||
|
||||
StableStudio is [Stability AI](https://stability.ai)'s official open-source variant of [DreamStudio](https://www.dreamstudio.ai), our user interface for generative AI. It is a web-based application that allows users to create and edit generated images. We're not entirely sure where this project is going just yet, but we're excited to see what the community does with it!
|
||||
StableStudio is [Stability AI](https://stability.ai)'s official open-source variant of [DreamStudio](https://www.dreamstudio.ai), our user interface for generative AI. It is a desktop application for local inference of Stable Diffusion that allows users to create and edit generated images. We're excited to see what the community does with it!
|
||||
|
||||
# <a id="faq" href="#faq">FAQ</a>
|
||||
# FAQ
|
||||
|
||||
### What's the difference between StableStudio and [DreamStudio](https://dreamstudio.ai)?
|
||||
|
||||
@@ -59,9 +34,9 @@ _Not much!_ There are a few tweaks we made to make the project more community-fr
|
||||
|
||||
- We removed [DreamStudio](https://dreamstudio.ai)-specific branding.
|
||||
|
||||
- All "over-the-wire" API calls have been replaced by a [plugin system](./packages/stablestudio-plugin/README.md) which allows you to easily swap out the back-end.
|
||||
- StableStudio is now packaged as a tauri application
|
||||
|
||||
- With a little bit of TypeScript, you can [create your own plugin](./packages/stablestudio-plugin/README.md) and use StableStudio with any back-end you want!
|
||||
- All "over-the-wire" API calls have been replaced by a [ComfyUI](https://github.com/comfyanonymous/ComfyUI) backend.
|
||||
|
||||
- We removed Stability-specific account features such as billing, API key management, etc.
|
||||
|
||||
@@ -69,12 +44,45 @@ _Not much!_ There are a few tweaks we made to make the project more community-fr
|
||||
|
||||
### Will [DreamStudio](https://dreamstudio.ai) still be supported?
|
||||
|
||||
_Yes!_ Stability's hosted deployment of StableStudio will remain [DreamStudio](https://dreamstudio.ai). It will continue to get updates and stay up-to-date with StableStudio whenever possible.
|
||||
_Yes!_ Stability's hosted deployment of StableStudio will remain [DreamStudio](https://dreamstudio.ai).
|
||||
|
||||
# <a id="contributing" href="#contributing">🧑💻 Contributing</a>
|
||||
# Building From Source
|
||||
|
||||
> Make sure you have the rust toolchain, nodejs, and yarn installed
|
||||
|
||||
1. Clone the repo
|
||||
|
||||
```
|
||||
git clone https://github.com/Stability-AI/StableStudio
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
```
|
||||
cargo install tauri-cli
|
||||
```
|
||||
|
||||
3. Build
|
||||
|
||||
```
|
||||
cargo tauri build
|
||||
```
|
||||
> Or for development
|
||||
|
||||
```
|
||||
cargo tauri dev
|
||||
```
|
||||
|
||||
Installers/executables should be located in `packages/stablestudio-ui/src-tauri/target/release/bundle`
|
||||
|
||||
# Contributing
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 1em; margin: 0 0 2em 0;">
|
||||
<img src="./misc/ProgrammingRobots.png" style="flex-grow: 1; flex-shrink: 1;" />
|
||||
<img src="./misc/contributingReadmeImage.png" style="flex-grow: 1; flex-shrink: 1;" />
|
||||
</div>
|
||||
|
||||
_**Community contributions are encouraged!**_
|
||||
@@ -87,3 +95,26 @@ Here are some useful links...
|
||||
- [Open Issues](https://github.com/Stability-AI/StableStudio/issues)
|
||||
- [Open Pull Requests](https://github.com/Stability-AI/StableStudio/pulls)
|
||||
- [Code of Conduct](./CODE_OF_CONDUCT.md)
|
||||
|
||||
## TODO
|
||||
### Model download
|
||||
- [ ] Design & implement a community model browser w/ download flow
|
||||
- [ ] A way to parse URLs into a URL that downloads a model (from say an input)
|
||||
- [ ] A universal way to download models from URLs
|
||||
- [ ] BONUS: A way to pick up model downloads if app is closed suddenly
|
||||
|
||||
### ComfyUI/SD install config
|
||||
- [ ] Ability to change comfy location in settings (file choose dialog w/ tauri & write to settings.json)
|
||||
- [ ] Ability to change comfy install location when/before downloading during first setup
|
||||
- [ ] Ability to change the particular SD model & location when/before downloading during first setup
|
||||
- [ ] Ability to change comfyui url in settings
|
||||
- [ ] Ability to change comfyui cli flags in settings
|
||||
|
||||
### Custom inputs
|
||||
- [ ] define & implement a strategy for arbitrary inputs in stablestudio UI
|
||||
|
||||
### Global job UI
|
||||
- [ ] List model downloads
|
||||
- [ ] List current/pending gens
|
||||
- [ ] Ability to cancel jobs (global job API?)
|
||||
- [ ] Floating UI for jobs - minimalistic
|
||||
|
||||
BIN
misc/Banner.png
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 1.7 MiB |
BIN
misc/BannerOld.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 1.7 MiB |
BIN
misc/ProjectCardOld.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
misc/aboutReadmeImage.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
misc/contributingReadmeImage.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
@@ -42,6 +42,7 @@
|
||||
"notistack": "^3.0.0-alpha.11",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-konva": "^18.2.3",
|
||||
"react-konva-utils": "^0.3.1",
|
||||
|
||||
7
packages/stablestudio-ui/public/file-code.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#3D3D3D"/>
|
||||
<path d="M5 21H17.25C17.7141 21 18.1592 20.8104 18.4874 20.4728C18.8156 20.1352 19 19.6774 19 19.2V7.95L14.1875 3H6.75C6.28587 3 5.84075 3.18964 5.51256 3.52721C5.18437 3.86477 5 4.32261 5 4.8V8.4" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 3V8H19" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 17L12 14.5L9 12" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 12L3 14.5L6 17" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 757 B |
365
packages/stablestudio-ui/src-tauri/Cargo.lock
generated
@@ -28,15 +28,6 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.0.2"
|
||||
@@ -78,20 +69,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.71"
|
||||
version = "1.0.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fork",
|
||||
"http",
|
||||
"once_cell",
|
||||
"portpicker",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-upload",
|
||||
@@ -507,30 +499,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.16"
|
||||
@@ -574,7 +542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -589,9 +557,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.1"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
|
||||
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
@@ -599,27 +567,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.1"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
|
||||
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.1"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -675,9 +643,9 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "519b83cd10f5f6e969625a409f735182bea5558cd8b64c655806ceaae36f1999"
|
||||
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa-short"
|
||||
@@ -694,12 +662,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.2.0"
|
||||
@@ -730,9 +692,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
@@ -742,7 +704,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -792,7 +754,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.2.16",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -826,6 +788,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "fork"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf2ca97a59201425e7ee4d197c9c4fea282fe87a97d666a580bda889b95b8e88"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.0"
|
||||
@@ -901,7 +872,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1163,11 +1134,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
||||
checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.20",
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
@@ -1329,7 +1300,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.8",
|
||||
"itoa 1.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1376,7 +1347,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.8",
|
||||
"itoa 1.0.9",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -1533,7 +1504,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1550,9 +1521,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "javascriptcore-rs"
|
||||
@@ -1780,7 +1751,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1841,15 +1812,6 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@@ -1987,7 +1949,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2008,6 +1970,17 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.1.4"
|
||||
@@ -2015,7 +1988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2069,7 +2042,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2307,9 +2280,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.64"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
|
||||
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2325,9 +2298,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.29"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
|
||||
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2419,28 +2392,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-progress-stream"
|
||||
version = "1.0.0"
|
||||
@@ -2487,10 +2438,10 @@ version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
|
||||
dependencies = [
|
||||
"aho-corasick 1.0.2",
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.3.2",
|
||||
"regex-syntax 0.7.3",
|
||||
"regex-automata 0.3.3",
|
||||
"regex-syntax 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2504,13 +2455,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
|
||||
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
|
||||
dependencies = [
|
||||
"aho-corasick 1.0.2",
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.3",
|
||||
"regex-syntax 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2521,9 +2472,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
|
||||
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
@@ -2590,20 +2541,20 @@ dependencies = [
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
@@ -2626,7 +2577,7 @@ version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2637,9 +2588,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
@@ -2686,40 +2637,40 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.171"
|
||||
version = "1.0.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
|
||||
checksum = "3c489e660549c5f25abb5e7a546a9de2ea437072d7d6f8d8c993ceab4e76be14"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.171"
|
||||
version = "1.0.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
|
||||
checksum = "d37fc147b26555e4a092321832edb03124603c0a456c449ecb589ed05bf91f89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.100"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
|
||||
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
|
||||
dependencies = [
|
||||
"itoa 1.0.8",
|
||||
"itoa 1.0.9",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -2732,7 +2683,7 @@ checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2751,16 +2702,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.8",
|
||||
"itoa 1.0.9",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
|
||||
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"chrono",
|
||||
@@ -2774,14 +2725,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
|
||||
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2988,9 +2939,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.25"
|
||||
version = "2.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
|
||||
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2998,18 +2949,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.29.5"
|
||||
name = "sys-locale"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b949f01f9c23823744b71e0060472ecbde578ef68cc2a9e46d114efd77c3034"
|
||||
checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"winapi",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3098,9 +3047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.38"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
|
||||
checksum = "ec96d2ffad078296368d46ff1cb309be1c23c513b4ab0e22a45de0185275ac96"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
@@ -3109,9 +3058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.8"
|
||||
version = "0.12.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac"
|
||||
checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
@@ -3134,6 +3083,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"os_info",
|
||||
"os_pipe",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
@@ -3145,6 +3095,7 @@ dependencies = [
|
||||
"serialize-to-javascript",
|
||||
"shared_child",
|
||||
"state",
|
||||
"sys-locale",
|
||||
"tar",
|
||||
"tauri-macros",
|
||||
"tauri-runtime",
|
||||
@@ -3219,7 +3170,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-upload"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#07f3a89df05761ac5572cb86b81f57f9a8da4a3d"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#c3a6dab24ccbaeada2d15caaf79e97064730e54f"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -3324,7 +3275,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3361,7 +3312,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3391,7 +3342,7 @@ version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
|
||||
dependencies = [
|
||||
"itoa 1.0.8",
|
||||
"itoa 1.0.9",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
@@ -3453,7 +3404,7 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3512,9 +3463,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.12"
|
||||
version = "0.19.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78"
|
||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
@@ -3549,7 +3500,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3620,9 +3571,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
|
||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -3659,9 +3610,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
]
|
||||
@@ -3774,7 +3725,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3808,7 +3759,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.25",
|
||||
"syn 2.0.26",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3978,7 +3929,7 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4007,13 +3958,37 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4022,12 +3997,12 @@ version = "0.48.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_gnullvm 0.48.0",
|
||||
"windows_aarch64_msvc 0.48.0",
|
||||
"windows_i686_gnu 0.48.0",
|
||||
"windows_i686_msvc 0.48.0",
|
||||
"windows_x86_64_gnu 0.48.0",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_gnullvm 0.48.0",
|
||||
"windows_x86_64_msvc 0.48.0",
|
||||
]
|
||||
|
||||
@@ -4037,6 +4012,12 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.0"
|
||||
@@ -4049,6 +4030,12 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.0"
|
||||
@@ -4061,6 +4048,12 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.0"
|
||||
@@ -4073,6 +4066,12 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.0"
|
||||
@@ -4085,12 +4084,24 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.0"
|
||||
@@ -4103,6 +4114,12 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.0"
|
||||
@@ -4111,9 +4128,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.4.9"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529"
|
||||
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -17,7 +17,7 @@ tauri-build = { version = "1.4.0", features = [] }
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.4.1", features = [ "fs-all", "path-all", "process-command-api", "devtools"] }
|
||||
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" }
|
||||
zip = "0.6.6"
|
||||
zip-extensions = "0.6.1"
|
||||
@@ -25,7 +25,10 @@ reqwest = { version = "0.11.6", features = ["blocking"] }
|
||||
tiny_http = "0.12.0"
|
||||
portpicker = "0.1.1"
|
||||
http = "0.2.9"
|
||||
sysinfo = "0.29.5"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
fork = "0.1.22"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
// 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 tauri::api::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::api::process::CommandEvent;
|
||||
use tauri::api::process::{
|
||||
Command,
|
||||
CommandEvent::{Error, Stderr, Stdout, Terminated},
|
||||
};
|
||||
use tauri::async_runtime::Receiver;
|
||||
use tauri::utils::config::{AppUrl, WindowConfig};
|
||||
use tauri::{RunEvent, WindowBuilder, WindowUrl};
|
||||
use tauri::{RunEvent, Window, WindowBuilder, WindowUrl};
|
||||
use tauri_plugin_upload;
|
||||
|
||||
mod server;
|
||||
mod settings;
|
||||
mod show_path;
|
||||
mod workflows;
|
||||
|
||||
static WINDOW: OnceLock<Window> = OnceLock::new();
|
||||
|
||||
fn main() {
|
||||
let port = portpicker::pick_unused_port().expect("failed to find unused port");
|
||||
@@ -24,7 +36,7 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(server::Builder::new(port).build())
|
||||
.setup(move |app| {
|
||||
WindowBuilder::from_config(
|
||||
let window = WindowBuilder::from_config(
|
||||
app,
|
||||
WindowConfig {
|
||||
url: {
|
||||
@@ -44,10 +56,39 @@ 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(())
|
||||
})
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.invoke_handler(tauri::generate_handler![extract_zip, launch_comfy])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
extract_comfy,
|
||||
launch_comfy,
|
||||
show_path::show_in_folder,
|
||||
settings::get_setting,
|
||||
settings::set_setting,
|
||||
workflows::fetch_workflows,
|
||||
workflows::save_workflow,
|
||||
workflows::delete_workflow,
|
||||
])
|
||||
.build(context)
|
||||
.expect("error while building tauri application")
|
||||
.run(move |_app_handle, event| match event {
|
||||
@@ -62,62 +103,138 @@ fn main() {
|
||||
|
||||
// tauri command to extract a zip from an arbitrary file path
|
||||
#[tauri::command]
|
||||
fn extract_zip(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();
|
||||
|
||||
// unarchive in a thread
|
||||
let handle = std::thread::spawn(move || {
|
||||
archive.extract(target_dir).unwrap();
|
||||
let extract_parent = target_dir.clone();
|
||||
let extract_thread = std::thread::spawn(move || {
|
||||
archive.extract(extract_parent).unwrap();
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
// dont block the main thread
|
||||
extract_thread.join().unwrap();
|
||||
|
||||
println!("extracted zip");
|
||||
Ok("completed".to_string())
|
||||
}
|
||||
|
||||
fn emit_event(event: &str, data: Option<String>) {
|
||||
match WINDOW.get() {
|
||||
Some(window) => {
|
||||
window
|
||||
.emit(event, data)
|
||||
.unwrap_or_else(|_| println!("[!!] event failed"));
|
||||
}
|
||||
None => {
|
||||
println!("[!!] window not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn watch_comfy(
|
||||
mut rx: Receiver<CommandEvent>,
|
||||
) -> (Receiver<CommandEvent>, Result<String, String>) {
|
||||
while let Some(i) = rx.recv().await {
|
||||
// check if output starts with "To see the GUI go to:"
|
||||
match i {
|
||||
Stdout(line) if line.len() > 1 => {
|
||||
println!("[ComfyUI] stdout: {}", line);
|
||||
emit_event("comfy-output", Some(format!("stdout:{line}")));
|
||||
|
||||
if line.starts_with("To see the GUI go to:") {
|
||||
println!("Comfy launched successfully!");
|
||||
return (rx, Ok("completed".to_string()));
|
||||
}
|
||||
}
|
||||
Stderr(line) => {
|
||||
println!("[ComfyUI] stderr: {}", line);
|
||||
emit_event("comfy-output", Some(format!("stderr:{line}")));
|
||||
}
|
||||
Error(line) => {
|
||||
println!("[ComfyUI] error: {}", line);
|
||||
emit_event("comfy-output", Some(format!("error:{line}")));
|
||||
}
|
||||
Terminated(_) => {
|
||||
println!("Comfy terminated!");
|
||||
return (rx, Err("failed".to_string()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
return (rx, Err("failed".to_string()));
|
||||
}
|
||||
|
||||
// tauri command to launch python process
|
||||
#[tauri::command]
|
||||
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();
|
||||
|
||||
// test to make sure its not already running (just test port 5000)
|
||||
println!("Checking for existing comfy process...");
|
||||
|
||||
// just try to connect to the port
|
||||
let resp = reqwest::blocking::get("http://localhost:5000");
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(1))
|
||||
.build()
|
||||
.unwrap();
|
||||
let resp = client.get(url.clone()).send().await;
|
||||
if resp.is_ok() {
|
||||
println!("Comfy already running, skipping launch.");
|
||||
return Ok("completed".to_string());
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let (mut rx, _child) = Command::new({
|
||||
if cfg!(unix) {
|
||||
"python_embeded/python"
|
||||
} else if cfg!(windows) {
|
||||
"python_embeded/python.exe"
|
||||
} else if cfg!(macos) {
|
||||
"python_embeded/python.app"
|
||||
} else {
|
||||
panic!("Unsupported platform")
|
||||
}
|
||||
})
|
||||
.args(["-s", "ComfyUI/main.py", "--port", "5000"])
|
||||
.envs(HashMap::from([("PYTHONUNBUFFERED".into(), "1".into())]))
|
||||
.spawn()
|
||||
.expect("Failed to spawn ComfyUI process");
|
||||
|
||||
// print output from python process
|
||||
while let Some(i) = rx.recv().await {
|
||||
println!("ComfyUI: {:?}", i);
|
||||
}
|
||||
});
|
||||
|
||||
println!("--> launching comfy...");
|
||||
|
||||
Ok("completed".to_string())
|
||||
#[allow(unused_mut)]
|
||||
let (mut rx, _child) = Command::new({
|
||||
if cfg!(unix) {
|
||||
"python_embeded/python"
|
||||
} else if cfg!(windows) {
|
||||
"python_embeded/python.exe"
|
||||
} else if cfg!(macos) {
|
||||
"python_embeded/python.app"
|
||||
} else {
|
||||
panic!("Unsupported platform")
|
||||
}
|
||||
})
|
||||
.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");
|
||||
|
||||
// print output from python process
|
||||
let (rx, result) = watch_comfy(rx).await;
|
||||
tauri::async_runtime::spawn(watch_comfy(rx));
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -63,7 +61,11 @@ impl Builder {
|
||||
) = match sliced_path {
|
||||
["lib" | "scripts", ..] => (
|
||||
Some(sliced_path.clone().join("/")),
|
||||
Some("application/javascript".to_string()),
|
||||
Some(if sliced_path.last().unwrap().ends_with(".js") {
|
||||
"text/javascript".to_string()
|
||||
} else {
|
||||
"text/css".to_string()
|
||||
}),
|
||||
None,
|
||||
),
|
||||
["style.css"] => (
|
||||
@@ -76,11 +78,11 @@ impl Builder {
|
||||
Some("text/html".to_string()),
|
||||
None,
|
||||
),
|
||||
["api" | "prompt" | "object_info" | "view" | "queue" | "extensions", ..] => (
|
||||
["api" | "prompt" | "object_info" | "view" | "upload" | "history" | "queue" | "interrupt" | "extensions", ..] => (
|
||||
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),
|
||||
@@ -89,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
|
||||
);
|
||||
|
||||
@@ -103,9 +105,14 @@ impl Builder {
|
||||
// replace "/ws" with "/api/ws" in the file
|
||||
let mut file_contents = fs::read_to_string(&path_name).unwrap();
|
||||
file_contents = file_contents.replace(
|
||||
"`ws${window.location.protocol === \"https:\" ? \"s\" : \"\"}://${location.host}/ws${existingSession}`",
|
||||
"`ws${window.location.protocol === \"https:\" ? \"s\" : \"\"}://${this.api_host}${this.api_base}/ws${existingSession}`",
|
||||
"`ws://localhost:5000/ws${existingSession}`"
|
||||
);
|
||||
|
||||
// add some stuff to app.js
|
||||
if path_name.ends_with("app.js") && !file_contents.ends_with("app.api = api;") {
|
||||
file_contents = file_contents + "\napp.api = api;";
|
||||
}
|
||||
|
||||
let response = HttpResponse::from_data(file_contents.as_bytes()).with_status_code(200).with_header(
|
||||
Header::from_bytes("Content-Type", mimetype.unwrap().as_str()).unwrap(),
|
||||
|
||||
100
packages/stablestudio-ui/src-tauri/src/settings.rs
Normal file
@@ -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(())
|
||||
}
|
||||
52
packages/stablestudio-ui/src-tauri/src/show_path.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
use fork::{daemon, Fork};
|
||||
use std::process::Command;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::{fs::metadata, path::PathBuf};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn show_in_folder(path: String) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("explorer")
|
||||
.args(["/select,", &path]) // The comma after select is not a typo
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if path.contains(",") {
|
||||
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
|
||||
let new_path = match metadata(&path).unwrap().is_dir() {
|
||||
true => path,
|
||||
false => {
|
||||
let mut path2 = PathBuf::from(path);
|
||||
path2.pop();
|
||||
path2.into_os_string().into_string().unwrap()
|
||||
}
|
||||
};
|
||||
Command::new("xdg-open").arg(&new_path).spawn().unwrap();
|
||||
} else {
|
||||
if let Ok(Fork::Child) = daemon(false, false) {
|
||||
Command::new("dbus-send")
|
||||
.args([
|
||||
"--session",
|
||||
"--dest=org.freedesktop.FileManager1",
|
||||
"--type=method_call",
|
||||
"/org/freedesktop/FileManager1",
|
||||
"org.freedesktop.FileManager1.ShowItems",
|
||||
format!("array:string:\"file://{path}\"").as_str(),
|
||||
"string:\"\"",
|
||||
])
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
||||
}
|
||||
}
|
||||
94
packages/stablestudio-ui/src-tauri/src/workflows.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufReader, BufWriter},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Workflow {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub serialized_workflow: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_workflows(handle: tauri::AppHandle) -> Result<Vec<Workflow>, String> {
|
||||
let mut workflows: Vec<Workflow> = Vec::new();
|
||||
let mut path = handle.path_resolver().app_data_dir().unwrap();
|
||||
path.push("workflows");
|
||||
|
||||
// create directory if it doesn't exist
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
|
||||
for entry in fs::read_dir(path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
let metadata = fs::metadata(&path).unwrap();
|
||||
if metadata.is_file() {
|
||||
let workflow_file = fs::File::open(path).unwrap();
|
||||
let reader = BufReader::new(workflow_file);
|
||||
if let Ok(workflow_data) = serde_json::from_reader(reader) {
|
||||
workflows.push(workflow_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(workflows)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_workflow(
|
||||
handle: tauri::AppHandle,
|
||||
serialized_workflow: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let mut workflow_path = handle.path_resolver().app_data_dir().unwrap();
|
||||
workflow_path.push("workflows");
|
||||
|
||||
// create directory if it doesn't exist
|
||||
std::fs::create_dir_all(&workflow_path).unwrap();
|
||||
|
||||
let file_name = name.replace(" ", "_").to_lowercase();
|
||||
workflow_path.push(format!("{file_name}.json"));
|
||||
|
||||
let workflow_file = fs::File::create(workflow_path).unwrap();
|
||||
let writer = BufWriter::new(workflow_file);
|
||||
|
||||
match serde_json::to_writer(
|
||||
writer,
|
||||
&Workflow {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
serialized_workflow,
|
||||
created_at: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.to_string(),
|
||||
},
|
||||
) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_workflow(handle: tauri::AppHandle, name: String) -> Result<(), String> {
|
||||
let mut workflow_path = handle.path_resolver().app_data_dir().unwrap();
|
||||
workflow_path.push("workflows");
|
||||
|
||||
// create directory if it doesn't exist
|
||||
std::fs::create_dir_all(&workflow_path).unwrap();
|
||||
|
||||
workflow_path.push(format!("{name}.json"));
|
||||
|
||||
match fs::remove_file(workflow_path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function Section(props: Section.Props) {
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<div className="flex items-center py-2 px-2">
|
||||
<div className="flex items-center px-2 py-2">
|
||||
<Theme.Button
|
||||
{...buttonProps}
|
||||
className={classes(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import {
|
||||
BaseDirectory,
|
||||
exists,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
import { appDataDir } from "@tauri-apps/api/path";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { download } from "tauri-plugin-upload";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import { Comfy } from "~/Comfy";
|
||||
import { Router } from "~/Router";
|
||||
import { Shortcut } from "~/Shortcut";
|
||||
@@ -33,10 +35,12 @@ export function App() {
|
||||
<h1 className="text-6xl font-bold">Welcome to StableStudio</h1>
|
||||
<div className="flex w-full flex-col items-center gap-4">
|
||||
<p className="font-mono opacity-75">{message}</p>
|
||||
<Theme.Progress
|
||||
className="max-w-[25rem]"
|
||||
value={progress * 100}
|
||||
/>
|
||||
{typeof progress === "number" && (
|
||||
<Theme.Progress
|
||||
className="max-w-[25rem]"
|
||||
value={progress * 100}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -102,45 +106,51 @@ export namespace App {
|
||||
export const useSetupState = () => {
|
||||
const [isSetup, setIsSetup] = useState<SetupState>(SetupState.NotStarted);
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [progress, setProgress] = useState<number | null>(null);
|
||||
const [setUnlisteners, print] = Comfy.use(
|
||||
(state) => [state.setUnlisteners, state.print],
|
||||
shallow
|
||||
);
|
||||
const nonce = useRef<number>(0);
|
||||
|
||||
const check = useCallback(async () => {
|
||||
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("Installing ComfyUI...");
|
||||
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);
|
||||
@@ -149,11 +159,9 @@ export namespace App {
|
||||
}
|
||||
|
||||
setMessage("Extracting ComfyUI...");
|
||||
setProgress(null);
|
||||
try {
|
||||
const result = await invoke("extract_zip", {
|
||||
path: `${appDataPath}/comfyui.zip`,
|
||||
targetDir: `${appDataPath}`,
|
||||
});
|
||||
const result = await invoke("extract_comfy");
|
||||
|
||||
if (result !== "completed") {
|
||||
throw new Error("Failed to extract comfyui.zip");
|
||||
@@ -165,9 +173,7 @@ export namespace App {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeFile("comfyui.zip", {
|
||||
dir: BaseDirectory.AppData,
|
||||
});
|
||||
await removeFile(`${comfyui_location}/comfyui.zip`);
|
||||
}
|
||||
|
||||
setIsSetup(SetupState.ComfyInstalled);
|
||||
@@ -179,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);
|
||||
@@ -191,11 +197,18 @@ export namespace App {
|
||||
|
||||
setIsSetup(SetupState.WeightsInstalled);
|
||||
setMessage("Starting ComfyUI...");
|
||||
setProgress(null);
|
||||
|
||||
// add listener
|
||||
const unlisten = await listen("comfy-output", (event) => {
|
||||
const [t, ...d] = `${event.payload}`.split(":");
|
||||
console.log("[COMFYUI]", t, d);
|
||||
print(t, d.join(":"));
|
||||
});
|
||||
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}`);
|
||||
@@ -203,7 +216,8 @@ export namespace App {
|
||||
}
|
||||
|
||||
setIsSetup(SetupState.ComfyRunning);
|
||||
}, [isSetup]);
|
||||
Comfy.registerListeners();
|
||||
}, [isSetup, print, setUnlisteners]);
|
||||
|
||||
useEffect(() => {
|
||||
check();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { create } from "zustand";
|
||||
import { Generation } from "~/Generation";
|
||||
|
||||
export type Comfy = {
|
||||
export type ComfyApp = {
|
||||
setup: () => void;
|
||||
registerNodes: () => void;
|
||||
loadGraphData: (graph: Graph) => void;
|
||||
loadGraphData: (graph?: Graph) => void;
|
||||
graphToPrompt: () => Promise<{
|
||||
workflow: any;
|
||||
prompt: any;
|
||||
@@ -12,6 +14,32 @@ export type Comfy = {
|
||||
refreshComboInNodes: () => Promise<void>;
|
||||
queuePrompt: (number: number, batchCount: number) => Promise<void>;
|
||||
clean: () => void;
|
||||
api: ComfyAPI;
|
||||
graph: {
|
||||
serialize: () => any;
|
||||
_nodes: {
|
||||
title: string;
|
||||
type: string;
|
||||
stableValues?: any;
|
||||
widgets: {
|
||||
name: string;
|
||||
type: string;
|
||||
value: any;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ComfyAPI = {
|
||||
addEventListener: (event: string, callback: (detail: any) => void) => void;
|
||||
};
|
||||
|
||||
export type ComfyOutput = {
|
||||
images: {
|
||||
filename: string;
|
||||
subfolder: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type Graph = {
|
||||
@@ -72,10 +100,225 @@ export function Comfy() {
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_STDOUT_LENGTH = 500;
|
||||
|
||||
type State = {
|
||||
output: {
|
||||
type: string;
|
||||
data: string;
|
||||
}[];
|
||||
max_lines: number;
|
||||
print: (type: string, data: string) => void;
|
||||
|
||||
running: boolean;
|
||||
setRunning: (running: boolean) => void;
|
||||
|
||||
unlisteners: (() => void)[];
|
||||
setUnlisteners: (unlisteners: (() => void)[]) => void;
|
||||
|
||||
runningPrompt: ID | null;
|
||||
setRunningPrompt: (promptID: ID | null) => void;
|
||||
|
||||
lastOuput: ComfyOutput | null;
|
||||
setLastOutput: (output: ComfyOutput | null) => void;
|
||||
|
||||
inlineExpanded: boolean;
|
||||
setInlineExpanded: (expanded: boolean) => void;
|
||||
};
|
||||
|
||||
export namespace Comfy {
|
||||
export const get = (): Comfy | null =>
|
||||
export const get = (): ComfyApp | null =>
|
||||
((
|
||||
(document.getElementById("comfyui-window") as HTMLIFrameElement)
|
||||
?.contentWindow as Window & { app: Comfy }
|
||||
)?.app as Comfy) ?? null;
|
||||
?.contentWindow as Window & { app: ComfyApp }
|
||||
)?.app as ComfyApp) ?? null;
|
||||
|
||||
export const use = create<State>((set) => ({
|
||||
output: [],
|
||||
max_lines: MAX_STDOUT_LENGTH,
|
||||
print: (type, data) =>
|
||||
set((state) => {
|
||||
if (state.output.map((o) => o.data).includes(data)) return state;
|
||||
|
||||
const output = [...state.output, { type, data }];
|
||||
if (output.length > MAX_STDOUT_LENGTH) output.shift();
|
||||
return { output };
|
||||
}),
|
||||
|
||||
running: false,
|
||||
setRunning: (running) => set({ running }),
|
||||
|
||||
unlisteners: [],
|
||||
setUnlisteners: (unlisteners) => set({ unlisteners }),
|
||||
|
||||
runningPrompt: null,
|
||||
setRunningPrompt: (runningPrompt) => set({ runningPrompt }),
|
||||
|
||||
lastOuput: null,
|
||||
setLastOutput: (lastOuput) => set({ lastOuput }),
|
||||
|
||||
inlineExpanded: false,
|
||||
setInlineExpanded: (inlineExpanded) => set({ inlineExpanded }),
|
||||
}));
|
||||
|
||||
export const registerListeners = async () => {
|
||||
let api = get()?.api;
|
||||
|
||||
while (!api) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
api = get()?.api;
|
||||
}
|
||||
|
||||
api.addEventListener("progress", ({ detail }) => {
|
||||
console.log("progress", detail);
|
||||
const runningPrompt = use.getState().runningPrompt;
|
||||
if (runningPrompt) {
|
||||
Generation.Image.Output.set({
|
||||
...Generation.Image.Output.get(runningPrompt),
|
||||
progress: detail.value / detail.max,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("b_preview", ({ detail }) => {
|
||||
console.log("b_preview", detail);
|
||||
});
|
||||
|
||||
const executed = async ({ detail }: any) => {
|
||||
const { output, prompt_id } = detail;
|
||||
|
||||
console.log("executed_in_comfy_domain", detail);
|
||||
use.getState().setRunningPrompt(null);
|
||||
|
||||
const newInputs: Record<ID, Generation.Image.Input> = {};
|
||||
const responses: Generation.Images = [];
|
||||
|
||||
const input = Generation.Image.Input.get(prompt_id);
|
||||
|
||||
const images = await Promise.all(
|
||||
(output as ComfyOutput).images.map(async (image) => {
|
||||
console.log("image", image);
|
||||
const resp = await fetch(
|
||||
`/view?filename=${image.filename}&subfolder=${
|
||||
image.subfolder || ""
|
||||
}&type=${image.type}`,
|
||||
{
|
||||
cache: "no-cache",
|
||||
}
|
||||
);
|
||||
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
console.log("url", url);
|
||||
|
||||
const output = Generation.Image.Output.get(prompt_id);
|
||||
|
||||
return {
|
||||
id: ID.create(),
|
||||
blob,
|
||||
inputID: output?.inputID ?? "",
|
||||
createdAt: new Date(),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const image of images) {
|
||||
const inputID = ID.create();
|
||||
const newInput = {
|
||||
...Generation.Image.Input.initial(inputID),
|
||||
...input,
|
||||
seed: (input?.seed ?? 0) + images.indexOf(image),
|
||||
id: inputID,
|
||||
};
|
||||
|
||||
responses.push({
|
||||
id: image.id,
|
||||
inputID: newInput.id,
|
||||
created: new Date(),
|
||||
src: URL.createObjectURL(image.blob),
|
||||
finishReason: 0,
|
||||
});
|
||||
newInputs[inputID] = newInput;
|
||||
}
|
||||
|
||||
Generation.Image.Inputs.set({
|
||||
...Generation.Image.Inputs.get(),
|
||||
...newInputs,
|
||||
});
|
||||
responses.forEach(Generation.Image.add);
|
||||
Generation.Image.Output.received(prompt_id, responses);
|
||||
use.getState().setLastOutput(detail);
|
||||
};
|
||||
|
||||
api.addEventListener("executed", executed);
|
||||
api.addEventListener("execution_cached", async ({ detail }) => {
|
||||
const last: any = use.getState().lastOuput;
|
||||
console.log("execution_cached", detail, last);
|
||||
if (
|
||||
use.getState().runningPrompt === detail.prompt_id &&
|
||||
detail.nodes.includes(last?.node) &&
|
||||
last.output
|
||||
) {
|
||||
console.log("last", last);
|
||||
const d = { ...last, prompt_id: detail.prompt_id };
|
||||
await executed({ detail: d });
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("execution_error", ({ detail }) => {
|
||||
console.log("execution_error", detail);
|
||||
Generation.Image.Output.clear(detail.prompt_id);
|
||||
use.getState().setRunningPrompt(null);
|
||||
});
|
||||
|
||||
api.addEventListener("execution_start", ({ detail }) => {
|
||||
use.getState().setRunningPrompt(detail?.prompt_id);
|
||||
});
|
||||
|
||||
console.log("registered ComfyUI listeners");
|
||||
use.getState().setRunning(true);
|
||||
};
|
||||
|
||||
export const Output = ({
|
||||
className,
|
||||
small,
|
||||
}: Styleable & {
|
||||
small?: boolean;
|
||||
}) => {
|
||||
const { output, inlineExpanded, setInlineExpanded } = Comfy.use(
|
||||
(state) => ({
|
||||
output: state.output,
|
||||
inlineExpanded: state.inlineExpanded,
|
||||
setInlineExpanded: state.setInlineExpanded,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes(
|
||||
"flex max-h-[25rem] flex-col-reverse overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded bg-black/25 p-2 font-mono text-sm leading-4",
|
||||
small && !inlineExpanded && "overflow-y-hidden",
|
||||
small && "cursor-pointer",
|
||||
className
|
||||
)}
|
||||
onClick={() => setInlineExpanded(!inlineExpanded)}
|
||||
>
|
||||
{(small && !inlineExpanded
|
||||
? output.slice(-1)
|
||||
: [...output].reverse()
|
||||
).map((line, index) => (
|
||||
<p
|
||||
key={`${index}-${line}`}
|
||||
className={classes(
|
||||
"text-white",
|
||||
line.type === "stdout" && "text-green-200",
|
||||
line.type === "stderr" && "text-red-200"
|
||||
)}
|
||||
>
|
||||
{line.data}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,61 +1,57 @@
|
||||
import * as StableStudio from "@stability/stablestudio-plugin";
|
||||
|
||||
import { Comfy } from ".";
|
||||
export const createPlugin = StableStudio.createPlugin(() => {
|
||||
return {
|
||||
manifest: {
|
||||
name: "ComfyUI",
|
||||
},
|
||||
|
||||
export const createPlugin = StableStudio.createPlugin(() => ({
|
||||
manifest: {
|
||||
name: "ComfyUI Backend",
|
||||
author: "StabilityAI",
|
||||
version: "0.0.1",
|
||||
license: "MIT",
|
||||
description: "An interface for generating images with ComfyUI",
|
||||
},
|
||||
getStableDiffusionModels: async () => {
|
||||
const resp = await fetch("/object_info/CheckpointLoader", {
|
||||
cache: "no-cache",
|
||||
});
|
||||
const jsonResp = await resp.json();
|
||||
|
||||
createStableDiffusionImages: async () => {
|
||||
const comfy = Comfy.get();
|
||||
console.log(jsonResp);
|
||||
|
||||
if (!comfy) {
|
||||
console.log(document.getElementById("comfyui-window"));
|
||||
throw new Error("ComfyUI is not loaded");
|
||||
}
|
||||
return jsonResp?.CheckpointLoader?.input?.required?.ckpt_name?.[0]?.map(
|
||||
(fileName: string) => ({
|
||||
id: fileName,
|
||||
name: fileName,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
await comfy.queuePrompt(1, 1);
|
||||
getStableDiffusionSamplers: async () => {
|
||||
const resp = await fetch("/object_info/KSamplerAdvanced", {
|
||||
cache: "no-cache",
|
||||
});
|
||||
const jsonResp = await resp.json();
|
||||
|
||||
const image = await fetch(`${window.location.origin}/DummyImage.png`);
|
||||
const blob = await image.blob();
|
||||
const createdAt = new Date();
|
||||
return jsonResp?.KSamplerAdvanced?.input?.required?.sampler_name?.[0]?.map(
|
||||
(name: string) => ({
|
||||
id: name,
|
||||
name: name
|
||||
.replace(/_/g, " ")
|
||||
.replace("ddim", "DDIM")
|
||||
.replace("lms", "LMS")
|
||||
.replace("dpm", "DPM")
|
||||
.replace("pp", "PP")
|
||||
.replace("sde", "SDE")
|
||||
.replace("2m", "2M")
|
||||
.replace("2s", "2S")
|
||||
.replace("gpu", "GPU")
|
||||
.replace(/\w\S*/g, (w) => w.replace(/^\w/, (c) => c.toUpperCase())),
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
return {
|
||||
id: `${Math.random() * 10000000}`,
|
||||
images: [
|
||||
{
|
||||
id: `${Math.random() * 10000000}`,
|
||||
createdAt,
|
||||
blob,
|
||||
},
|
||||
{
|
||||
id: `${Math.random() * 10000000}`,
|
||||
createdAt,
|
||||
blob,
|
||||
},
|
||||
{
|
||||
id: `${Math.random() * 10000000}`,
|
||||
createdAt,
|
||||
blob,
|
||||
},
|
||||
{
|
||||
id: `${Math.random() * 10000000}`,
|
||||
createdAt,
|
||||
blob,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
getStatus: () => {
|
||||
return {
|
||||
indicator: "success",
|
||||
text: "Ready",
|
||||
};
|
||||
},
|
||||
}));
|
||||
getStatus: async () => {
|
||||
const resp = await fetch("/comfyui", { cache: "no-cache" });
|
||||
return {
|
||||
indicator: resp.ok ? "success" : "error",
|
||||
text: resp.ok ? "Running" : "Not Running",
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { App } from "~/App";
|
||||
import { Comfy } from "~/Comfy";
|
||||
import { Editor } from "~/Editor";
|
||||
import { Generation } from "~/Generation";
|
||||
import { Router } from "~/Router";
|
||||
@@ -39,17 +40,20 @@ export function Sidebar() {
|
||||
|
||||
const bottom = selectedID && (
|
||||
<App.Sidebar.Tab.Bottom>
|
||||
<Generation.Image.Create.Button
|
||||
id={inputID}
|
||||
onIdleClick={() => createDream()}
|
||||
fullWidth
|
||||
disabled={
|
||||
!inputID ||
|
||||
generating ||
|
||||
!(dreams.filter((d) => d.id === selectedID).length > 0)
|
||||
}
|
||||
loading={generating}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Comfy.Output small />
|
||||
<Generation.Image.Create.Button
|
||||
id={inputID}
|
||||
onIdleClick={() => createDream()}
|
||||
fullWidth
|
||||
disabled={
|
||||
!inputID ||
|
||||
generating ||
|
||||
!(dreams.filter((d) => d.id === selectedID).length > 0)
|
||||
}
|
||||
loading={generating}
|
||||
/>
|
||||
</div>
|
||||
</App.Sidebar.Tab.Bottom>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as StableStudio from "@stability/stablestudio-plugin";
|
||||
import throttledQueue from "throttled-queue";
|
||||
import { Comfy } from "~/Comfy";
|
||||
|
||||
import { Generation } from "~/Generation";
|
||||
import { GlobalState } from "~/GlobalState";
|
||||
import { Plugin } from "~/Plugin";
|
||||
|
||||
import { Button } from "./Button";
|
||||
|
||||
@@ -23,35 +21,22 @@ export namespace Create {
|
||||
) => void;
|
||||
};
|
||||
|
||||
namespace Throttle {
|
||||
const requestsPerInterval = 1;
|
||||
const interval = 500;
|
||||
const spaceEvenly = true;
|
||||
|
||||
const queue = throttledQueue(requestsPerInterval, interval, spaceEvenly);
|
||||
export const wait = () => queue(() => Promise.resolve());
|
||||
}
|
||||
|
||||
export const execute = async ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
count = Generation.Image.Count.preset(),
|
||||
input,
|
||||
|
||||
onStarted = doNothing,
|
||||
onException = doNothing,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
onSuccess = doNothing,
|
||||
onFinished = doNothing,
|
||||
}: Handlers & {
|
||||
count: number;
|
||||
input: Generation.Image.Input;
|
||||
}): Promise<Generation.Image.Exception | Generation.Images> => {
|
||||
const { createStableDiffusionImages } = Plugin.get();
|
||||
}): Promise<undefined | Generation.Image.Exception> => {
|
||||
try {
|
||||
if (!createStableDiffusionImages) throw new Error("Plugin not found");
|
||||
|
||||
Latest.set(new Date());
|
||||
onStarted();
|
||||
|
||||
await Throttle.wait();
|
||||
|
||||
const initImg = await Generation.Image.Input.resizeInit(input);
|
||||
const pluginInput = await Generation.Image.Input.toInput(
|
||||
@@ -72,43 +57,58 @@ export namespace Create {
|
||||
pluginInput.width = Math.ceil((pluginInput.width ?? 512) / 64) * 64;
|
||||
}
|
||||
|
||||
const responses: Generation.Images = [];
|
||||
const response = await createStableDiffusionImages({
|
||||
input: pluginInput,
|
||||
count,
|
||||
});
|
||||
const startingSeed =
|
||||
input.seed === 0 ? Math.round(Math.random() * 100000) : input.seed;
|
||||
|
||||
if (response instanceof Error) throw response;
|
||||
if (!response || !response?.images || response?.images?.length <= 0)
|
||||
throw new Error();
|
||||
Comfy.get()
|
||||
?.graph._nodes?.filter((node) => node.type === "StableStudioNode")
|
||||
.forEach((node) => {
|
||||
if (!("stableValues" in node)) return;
|
||||
|
||||
const newInputs: Record<ID, Generation.Image.Input> = {};
|
||||
node.stableValues = {
|
||||
...node.stableValues,
|
||||
batch_size: count,
|
||||
height: input.height,
|
||||
width: input.width,
|
||||
negative_prompt:
|
||||
input.prompts.find((p) => p.weight < 0)?.text ??
|
||||
node.stableValues.negative_prompt,
|
||||
positive_prompt:
|
||||
input.prompts.find((p) => p.weight > 0)?.text ??
|
||||
node.stableValues.positive_prompt,
|
||||
seed: startingSeed,
|
||||
steps: input.steps,
|
||||
cfg: input.cfgScale ?? node.stableValues.cfg,
|
||||
sampler_name:
|
||||
(input.sampler?.id?.length || 0) > 2
|
||||
? input.sampler?.id
|
||||
: node.stableValues.sampler,
|
||||
ckpt_name: input.model || node.stableValues.model,
|
||||
};
|
||||
});
|
||||
Comfy.get()?.refreshComboInNodes();
|
||||
console.log(Comfy.get());
|
||||
const resp: any = await Comfy.get()?.queuePrompt(-1, 1);
|
||||
console.log("queued", resp);
|
||||
const { prompt_id } = resp;
|
||||
|
||||
for (const image of response.images) {
|
||||
const inputID = ID.create();
|
||||
const newInput = {
|
||||
...Generation.Image.Input.initial(inputID),
|
||||
...input,
|
||||
seed: image.input?.seed ?? input.seed,
|
||||
id: inputID,
|
||||
};
|
||||
|
||||
const cropped = await cropImage(image, newInput);
|
||||
if (!cropped) continue;
|
||||
|
||||
responses.push(cropped);
|
||||
newInputs[inputID] = newInput;
|
||||
if (prompt_id) {
|
||||
Generation.Image.Inputs.set((inputs) => ({
|
||||
...inputs,
|
||||
[prompt_id]: {
|
||||
...input,
|
||||
seed: startingSeed,
|
||||
id: prompt_id,
|
||||
},
|
||||
}));
|
||||
const output = Generation.Image.Output.requested(
|
||||
prompt_id,
|
||||
{},
|
||||
prompt_id
|
||||
);
|
||||
Generation.Image.Output.set(output);
|
||||
onStarted(output);
|
||||
}
|
||||
|
||||
Generation.Image.Inputs.set({
|
||||
...Generation.Image.Inputs.get(),
|
||||
...newInputs,
|
||||
});
|
||||
|
||||
onSuccess(responses);
|
||||
onFinished(responses);
|
||||
|
||||
return responses;
|
||||
} catch (caught: unknown) {
|
||||
const exception = Generation.Image.Exception.create(caught);
|
||||
|
||||
@@ -142,30 +142,24 @@ export namespace Create {
|
||||
...modifiers,
|
||||
};
|
||||
|
||||
const output = Generation.Image.Output.requested(inputID, modifiers);
|
||||
|
||||
return execute({
|
||||
count: modifiers.count ?? Generation.Image.Count.get(),
|
||||
input,
|
||||
|
||||
onStarted: () => {
|
||||
Generation.Image.Output.set(output);
|
||||
onStarted: (output) => {
|
||||
onStarted(output);
|
||||
},
|
||||
|
||||
onException: (exception) => {
|
||||
showErrorSnackbar(exception);
|
||||
onException(exception);
|
||||
Generation.Image.Output.clear(output.id);
|
||||
},
|
||||
|
||||
onSuccess: (images) => {
|
||||
images.forEach(Generation.Image.add);
|
||||
onSuccess(images);
|
||||
},
|
||||
|
||||
onFinished: (result) => {
|
||||
Generation.Image.Output.received(output.id, result);
|
||||
onFinished(result);
|
||||
},
|
||||
});
|
||||
@@ -174,10 +168,7 @@ export namespace Create {
|
||||
);
|
||||
};
|
||||
|
||||
export const useIsEnabled = () =>
|
||||
Plugin.use(
|
||||
({ createStableDiffusionImages }) => !!createStableDiffusionImages
|
||||
);
|
||||
export const useIsEnabled = () => Comfy.use(({ running }) => running);
|
||||
|
||||
export type Latest = Date;
|
||||
export namespace Latest {
|
||||
@@ -202,52 +193,3 @@ export namespace Create {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move somewhere else
|
||||
function cropImage(
|
||||
image: StableStudio.StableDiffusionImage,
|
||||
input: Generation.Image.Input
|
||||
) {
|
||||
return new Promise<Generation.Image | void>((resolve) => {
|
||||
const id = image.id;
|
||||
const blob = image.blob;
|
||||
if (!blob || !id) return resolve();
|
||||
|
||||
// crop image to box size
|
||||
const croppedCanvas = document.createElement("canvas");
|
||||
croppedCanvas.width = input.width;
|
||||
croppedCanvas.height = input.height;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const croppedCtx = croppedCanvas.getContext("2d")!;
|
||||
|
||||
const img = new window.Image();
|
||||
img.src = URL.createObjectURL(blob);
|
||||
img.onload = () => {
|
||||
croppedCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
input.width,
|
||||
input.height,
|
||||
0,
|
||||
0,
|
||||
input.width,
|
||||
input.height
|
||||
);
|
||||
|
||||
croppedCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
resolve({
|
||||
id,
|
||||
inputID: input.id,
|
||||
created: new Date(),
|
||||
src: objectURL,
|
||||
finishReason: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export function Images({ className }: Images.Props) {
|
||||
style={{ height: virtualizer.getTotalSize() }}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full"
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${
|
||||
(virtualItems[0]?.start ?? 0) - virtualizer.options.scrollMargin
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { Generation } from "~/Generation";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
@@ -5,14 +6,27 @@ export function Dropdown({ id, className }: Styleable & { id: ID }) {
|
||||
const { setInput, input } = Generation.Image.Input.use(id);
|
||||
const { data: models, isLoading } = Generation.Image.Models.use();
|
||||
|
||||
const [value, setValue] = useLocalStorage<string | undefined>(
|
||||
"default-model-id",
|
||||
undefined
|
||||
);
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setInput((input) => {
|
||||
input.model = value;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback(
|
||||
(value: string) => {
|
||||
setInput((input) => {
|
||||
console.log("model", value);
|
||||
input.model = value;
|
||||
setValue(value);
|
||||
});
|
||||
},
|
||||
[setInput]
|
||||
[setInput, setValue]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
|
||||
@@ -53,8 +53,11 @@ export namespace StableDiffusionV1 {
|
||||
{ text: "", weight: -0.75 },
|
||||
],
|
||||
|
||||
model: pluginDefaultInput?.model ?? "stable-diffusion-xl-beta-v2-2-2",
|
||||
sampler: pluginDefaultInput?.sampler ?? { id: "0", name: "DDIM" },
|
||||
model: pluginDefaultInput?.model ?? "v2-1_768-ema-pruned.safetensors",
|
||||
sampler: pluginDefaultInput?.sampler ?? {
|
||||
id: "dpmpp_sde",
|
||||
name: "DPMPP SDE",
|
||||
},
|
||||
height: pluginDefaultInput?.width ?? 512,
|
||||
width: pluginDefaultInput?.height ?? 512,
|
||||
steps: pluginDefaultInput?.steps ?? 50,
|
||||
@@ -90,14 +93,7 @@ export namespace StableDiffusionV1 {
|
||||
export const baseResolution = (model: string) => {
|
||||
if (model.includes("512")) return 512;
|
||||
if (model.includes("768")) return 768;
|
||||
|
||||
switch (model) {
|
||||
case "stable-diffusion-v1-5":
|
||||
return 512;
|
||||
case "stable-diffusion-v1-4":
|
||||
return 512;
|
||||
}
|
||||
|
||||
if (model.includes("1024") || model.includes("sd_xl")) return 1024;
|
||||
return 512;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ export type State = {
|
||||
|
||||
requested: (
|
||||
inputID: ID,
|
||||
modifiers?: Generation.Image.Input.Modifiers
|
||||
modifiers?: Generation.Image.Input.Modifiers,
|
||||
nextID?: ID
|
||||
) => Generation.Image.Output;
|
||||
|
||||
received: (
|
||||
@@ -37,8 +38,8 @@ export namespace State {
|
||||
|
||||
nextID: ID.create(),
|
||||
|
||||
requested: (inputID, modifiers) => {
|
||||
const id = get().nextID;
|
||||
requested: (inputID, modifiers, nextID) => {
|
||||
const id = nextID ?? get().nextID;
|
||||
const output = {
|
||||
id,
|
||||
inputID,
|
||||
|
||||
@@ -13,9 +13,11 @@ export type Output = {
|
||||
|
||||
requestedAt?: Date;
|
||||
completedAt?: Date;
|
||||
progress?: number;
|
||||
|
||||
count: number;
|
||||
imageIDs: ID[];
|
||||
progressImageIDs?: ID[];
|
||||
|
||||
exception?: Generation.Image.Exception;
|
||||
};
|
||||
@@ -61,6 +63,8 @@ export function Output({ outputID, placeholder, divider }: Props) {
|
||||
key={keys("image", images.length, images.length - index)}
|
||||
placeholder={placeholder}
|
||||
image={image}
|
||||
outputID={output?.id}
|
||||
progress={output?.progress}
|
||||
scale={1}
|
||||
example={
|
||||
Generation.Image.Prompt.Examples.images[
|
||||
@@ -76,7 +80,14 @@ export function Output({ outputID, placeholder, divider }: Props) {
|
||||
{rendered}
|
||||
</div>
|
||||
);
|
||||
}, [count, images, input?.id, placeholder, exampleStartIndex]);
|
||||
}, [
|
||||
count,
|
||||
images,
|
||||
input?.id,
|
||||
placeholder,
|
||||
output?.progress,
|
||||
exampleStartIndex,
|
||||
]);
|
||||
|
||||
const controls = useMemo(
|
||||
() => (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { App } from "~/App";
|
||||
import { Generation } from "~/Generation";
|
||||
import { Theme } from "~/Theme";
|
||||
import { NumberInput } from "~/Theme/NumberInput";
|
||||
|
||||
export function Advanced({
|
||||
id,
|
||||
@@ -10,6 +11,24 @@ export function Advanced({
|
||||
const areModelsEnabled = Generation.Image.Models.useAreEnabled();
|
||||
const areSamplersEnabled = Generation.Image.Samplers.useAreEnabled();
|
||||
|
||||
const onWidthChange = useCallback(
|
||||
(width: number) => {
|
||||
setInput((input) => {
|
||||
input.width = width;
|
||||
});
|
||||
},
|
||||
[setInput]
|
||||
);
|
||||
|
||||
const onHeightChange = useCallback(
|
||||
(height: number) => {
|
||||
setInput((input) => {
|
||||
input.height = height;
|
||||
});
|
||||
},
|
||||
[setInput]
|
||||
);
|
||||
|
||||
const onPromptStrengthChange = useCallback(
|
||||
(cfgScale: number) => {
|
||||
setInput((input) => {
|
||||
@@ -69,7 +88,41 @@ export function Advanced({
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Theme.Tooltip
|
||||
delay={750}
|
||||
placement="right"
|
||||
content={<h1>Manually set the width</h1>}
|
||||
>
|
||||
<NumberInput
|
||||
label="Width"
|
||||
placeholder="512"
|
||||
fullWidth
|
||||
number
|
||||
min={1}
|
||||
step={1}
|
||||
max={2048}
|
||||
value={input.width}
|
||||
onNumberChange={onWidthChange}
|
||||
/>
|
||||
</Theme.Tooltip>
|
||||
<Theme.Tooltip
|
||||
delay={750}
|
||||
placement="right"
|
||||
content={<h1>Manually set the height</h1>}
|
||||
>
|
||||
<NumberInput
|
||||
label="Height"
|
||||
placeholder="512"
|
||||
fullWidth
|
||||
number
|
||||
min={1}
|
||||
step={1}
|
||||
max={2048}
|
||||
value={input.height}
|
||||
onNumberChange={onHeightChange}
|
||||
/>
|
||||
</Theme.Tooltip>
|
||||
<Theme.Tooltip
|
||||
delay={750}
|
||||
placement="right"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { App } from "~/App";
|
||||
import { Comfy } from "~/Comfy";
|
||||
import { Generation } from "~/Generation";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
@@ -11,6 +12,20 @@ export function Sidebar() {
|
||||
const location = useLocation();
|
||||
|
||||
if (!input?.id) return null;
|
||||
|
||||
const bottom = (
|
||||
<App.Sidebar.Tab.Bottom>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Comfy.Output small />
|
||||
<Generation.Image.Create.Button
|
||||
id={input.id}
|
||||
onIdleClick={() => createDream()}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</App.Sidebar.Tab.Bottom>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<App.Sidebar.Tab.Set
|
||||
@@ -25,15 +40,7 @@ export function Sidebar() {
|
||||
location.pathname.startsWith("/edit") ||
|
||||
location.pathname.startsWith("/nodes")
|
||||
}
|
||||
bottom={
|
||||
<App.Sidebar.Tab.Bottom>
|
||||
<Generation.Image.Create.Button
|
||||
id={input.id}
|
||||
onIdleClick={() => createDream()}
|
||||
fullWidth
|
||||
/>
|
||||
</App.Sidebar.Tab.Bottom>
|
||||
}
|
||||
bottom={bottom}
|
||||
>
|
||||
<Sidebar.Tab id={input.id} />
|
||||
</App.Sidebar.Tab.Set>
|
||||
@@ -49,15 +56,7 @@ export function Sidebar() {
|
||||
location.pathname.startsWith("/edit") ||
|
||||
location.pathname.startsWith("/nodes")
|
||||
}
|
||||
bottom={
|
||||
<App.Sidebar.Tab.Bottom>
|
||||
<Generation.Image.Create.Button
|
||||
id={input.id}
|
||||
onIdleClick={() => createDream()}
|
||||
fullWidth
|
||||
/>
|
||||
</App.Sidebar.Tab.Bottom>
|
||||
}
|
||||
bottom={bottom}
|
||||
>
|
||||
<Sidebar.Tab id={input.id} />
|
||||
</App.Sidebar.Tab.Set>
|
||||
@@ -77,13 +76,12 @@ export namespace Sidebar {
|
||||
const areStylesEnabled = Generation.Image.Styles.useAreEnabled();
|
||||
return (
|
||||
<>
|
||||
{areStylesEnabled && (
|
||||
<App.Sidebar.Section divider defaultExpanded padding="sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Generation.Image.Style.Dropdown id={id} />
|
||||
</div>
|
||||
</App.Sidebar.Section>
|
||||
)}
|
||||
<App.Sidebar.Section divider defaultExpanded padding="sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
{areStylesEnabled && <Generation.Image.Style.Dropdown id={id} />}
|
||||
<Generation.Image.Workflow.Dropdown />
|
||||
</div>
|
||||
</App.Sidebar.Section>
|
||||
<Generation.Image.Prompt.Sidebar.Section id={id} />
|
||||
{variant === "generate" && (
|
||||
<Generation.Image.Input.Image.Sidebar.Section id={id} />
|
||||
|
||||
@@ -16,9 +16,7 @@ export function Ratio({
|
||||
}) {
|
||||
const { input } = Generation.Image.Input.use(id);
|
||||
const { closest, ratios } = Ratios.use(id, fullControl);
|
||||
const [timer, setTimer] = React.useState<ReturnType<
|
||||
typeof setTimeout
|
||||
> | null>(null);
|
||||
const [timer, setTimer] = React.useState<NodeJS.Timeout | null>(null);
|
||||
const [forceTooltipOpen, setForceTooltipOpen] = React.useState(false);
|
||||
|
||||
const onChange = useCallback(
|
||||
@@ -156,7 +154,8 @@ export namespace Ratios {
|
||||
export const use = (id?: ID, fullControl = false) => {
|
||||
const { input } = Generation.Image.Input.use(id);
|
||||
const bounds = Generation.Image.Size.Bounds.use(id);
|
||||
const ratios = useMemo(() => {
|
||||
|
||||
const normalRatios = useMemo(() => {
|
||||
if (!input?.width || !input?.height || !bounds) return [];
|
||||
|
||||
const sizing = (ratio: Ratio) => {
|
||||
@@ -188,6 +187,25 @@ export namespace Ratios {
|
||||
);
|
||||
}, [fullControl, bounds, input?.width, input?.height]);
|
||||
|
||||
const sdxlRatios = useMemo(
|
||||
() => [
|
||||
{ width: 21, height: 9, input: { width: 1536, height: 640 } },
|
||||
{ width: 16, height: 9, input: { width: 1344, height: 768 } },
|
||||
{ width: 3, height: 2, input: { width: 1216, height: 832 } },
|
||||
{ width: 4, height: 3, input: { width: 1152, height: 896 } },
|
||||
{ width: 5, height: 4, input: { width: 1152, height: 896 } },
|
||||
{ width: 1, height: 1, input: { width: 1024, height: 1024 } },
|
||||
{ width: 4, height: 5, input: { width: 896, height: 1152 } },
|
||||
{ width: 3, height: 4, input: { width: 896, height: 1152 } },
|
||||
{ width: 2, height: 3, input: { width: 832, height: 1216 } },
|
||||
{ width: 9, height: 16, input: { width: 768, height: 1344 } },
|
||||
{ width: 9, height: 21, input: { width: 640, height: 1536 } },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const ratios = !input?.model?.includes("xl") ? normalRatios : sdxlRatios;
|
||||
|
||||
type Closest = Ratio & {
|
||||
index: number;
|
||||
difference: number;
|
||||
|
||||
@@ -89,27 +89,27 @@ export namespace Size {
|
||||
if (input.model.includes("xl")) {
|
||||
return {
|
||||
length: {
|
||||
min: 512,
|
||||
max: 896,
|
||||
min: 640,
|
||||
max: 1536,
|
||||
},
|
||||
|
||||
area: {
|
||||
max: 512 * 896,
|
||||
max: 1024 * 1024,
|
||||
},
|
||||
|
||||
ratio: {
|
||||
min: 0.57,
|
||||
max: 1.75,
|
||||
min: 9 / 21,
|
||||
max: 21 / 9,
|
||||
},
|
||||
|
||||
width: {
|
||||
min: 512,
|
||||
max: 896,
|
||||
min: 640,
|
||||
max: 1536,
|
||||
},
|
||||
|
||||
height: {
|
||||
min: 512,
|
||||
max: 896,
|
||||
min: 640,
|
||||
max: 1536,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CircularProgressbar } from "react-circular-progressbar";
|
||||
import { Generation } from "~/Generation";
|
||||
import { Theme } from "~/Theme";
|
||||
import "react-circular-progressbar/dist/styles.css";
|
||||
|
||||
import { Filter } from "./Filter";
|
||||
|
||||
@@ -10,6 +12,8 @@ export function SpecialEffects({
|
||||
example,
|
||||
onClick,
|
||||
input,
|
||||
output,
|
||||
progress,
|
||||
}: {
|
||||
showing?: boolean;
|
||||
loading?: boolean;
|
||||
@@ -20,9 +24,26 @@ export function SpecialEffects({
|
||||
};
|
||||
onClick?: () => void;
|
||||
input?: ID;
|
||||
output?: ID;
|
||||
border?: boolean;
|
||||
progress?: number;
|
||||
}) {
|
||||
if (!showing) return null;
|
||||
const [starting] = useState(
|
||||
output ? Generation.Image.Output.get(output).requestedAt : undefined
|
||||
);
|
||||
const [eta, setETA] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) return;
|
||||
if (typeof progress !== "number") return;
|
||||
if (!starting) return;
|
||||
|
||||
const seconds = (Date.now() - starting.getTime()) / 1000;
|
||||
const eta = seconds / progress - seconds;
|
||||
setETA(eta);
|
||||
}, [loading, progress, starting]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes(
|
||||
@@ -74,13 +95,56 @@ export function SpecialEffects({
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute flex h-full w-full items-center justify-center">
|
||||
{loading && (
|
||||
<Theme.Loading.Spinner
|
||||
className={classes(
|
||||
variant === "small" ? "h-1/2 w-1/2" : "h-10 w-10"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{loading &&
|
||||
(typeof progress === "number" ? (
|
||||
<div className="relative flex flex-col items-center justify-center gap-1">
|
||||
<div
|
||||
className={classes(
|
||||
variant === "small" ? "h-1/2 w-1/2" : "h-10 w-10"
|
||||
)}
|
||||
>
|
||||
<CircularProgressbar
|
||||
value={progress}
|
||||
maxValue={1}
|
||||
styles={{
|
||||
// Customize the path, i.e. the "completed progress"
|
||||
path: {
|
||||
// Path color
|
||||
stroke: "#ffffff",
|
||||
// Whether to use rounded or flat corners on the ends - can use 'butt' or 'round'
|
||||
strokeLinecap: "butt",
|
||||
// Customize transition animation
|
||||
transition: "stroke-dashoffset 0.5s ease 0s",
|
||||
transformOrigin: "center center",
|
||||
strokeWidth: 8,
|
||||
},
|
||||
// Customize the circle behind the path, i.e. the "total progress"
|
||||
trail: {
|
||||
// Trail color
|
||||
stroke: "#4c4c4d",
|
||||
// Whether to use rounded or flat corners on the ends - can use 'butt' or 'round'
|
||||
strokeLinecap: "butt",
|
||||
transformOrigin: "center center",
|
||||
strokeWidth: 8,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="absolute top-full mt-1">
|
||||
{(eta ?? 0) > 0 && (
|
||||
<span className="text-sm text-white">
|
||||
{Math.round(eta ?? 0)}s
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Theme.Loading.Spinner
|
||||
className={classes(
|
||||
variant === "small" ? "h-1/2 w-1/2" : "h-10 w-10"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
128
packages/stablestudio-ui/src/Generation/Image/Workflow/Modal.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Generation } from "~/Generation";
|
||||
import { GlobalState } from "~/GlobalState";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
export function Modal() {
|
||||
const {
|
||||
image,
|
||||
open,
|
||||
name,
|
||||
description,
|
||||
setImage,
|
||||
setName,
|
||||
setOpen,
|
||||
setDescription,
|
||||
} = Modal.State.use();
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
return (
|
||||
<Theme.Modal
|
||||
modalName="Download"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Theme.Modal.Panel className="flex w-[25rem] grow">
|
||||
<Theme.Modal.TopBar onClose={() => setOpen(false)}>
|
||||
<Theme.Modal.Title className="text-lg">
|
||||
Save workflow
|
||||
</Theme.Modal.Title>
|
||||
</Theme.Modal.TopBar>
|
||||
<div className="flex flex-col gap-3 p-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Theme.Label className="mb-0 ml-0">Name</Theme.Label>
|
||||
<Theme.Input
|
||||
fullWidth
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Theme.Label className="mb-0 ml-0">Description</Theme.Label>
|
||||
<Theme.Input
|
||||
fullWidth
|
||||
autoSize
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Theme.Label className="mb-0 ml-0">Icon</Theme.Label>
|
||||
<input
|
||||
type={"file"}
|
||||
accept={"image/*"}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const result = e.target?.result;
|
||||
if (typeof result !== "string") return;
|
||||
|
||||
setImage(result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Theme.Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="brand"
|
||||
icon={Theme.Icon.Download}
|
||||
loading={saving}
|
||||
onClick={async () => {
|
||||
if (!image || !name) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await Generation.Image.Workflow.saveWorkflow(
|
||||
name,
|
||||
image,
|
||||
description
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Theme.Button>
|
||||
</div>
|
||||
</Theme.Modal.Panel>
|
||||
</Theme.Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export namespace Modal {
|
||||
export type State = {
|
||||
image?: string | null;
|
||||
name: string;
|
||||
open: boolean;
|
||||
description: string;
|
||||
|
||||
setImage: (image?: string) => void;
|
||||
setName: (name: string) => void;
|
||||
setOpen: (open: boolean) => void;
|
||||
setDescription: (description: string) => void;
|
||||
};
|
||||
|
||||
export namespace State {
|
||||
export const use = GlobalState.create<State>((set) => ({
|
||||
image: null,
|
||||
name: "",
|
||||
open: false,
|
||||
description: "",
|
||||
|
||||
setImage: (image) => set({ image }),
|
||||
setName: (name) => set({ name }),
|
||||
setOpen: (open) => set({ open, name: "", image: null, description: "" }),
|
||||
setDescription: (description) => set({ description }),
|
||||
}));
|
||||
}
|
||||
}
|
||||
188
packages/stablestudio-ui/src/Generation/Image/Workflow/index.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { create } from "zustand";
|
||||
import { Comfy } from "~/Comfy";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
export type Workflow = {
|
||||
name: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
serialized_workflow: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export declare namespace Workflow {
|
||||
export { Modal };
|
||||
}
|
||||
|
||||
export namespace Workflow {
|
||||
export const saveWorkflow = async (
|
||||
name: string,
|
||||
icon: string,
|
||||
description: string
|
||||
) => {
|
||||
const comfyApp = Comfy.get();
|
||||
if (!comfyApp) return;
|
||||
|
||||
const serializedWorkflow = JSON.stringify(comfyApp.graph.serialize(), null);
|
||||
try {
|
||||
await invoke("save_workflow", {
|
||||
name,
|
||||
icon,
|
||||
serializedWorkflow,
|
||||
description,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("saved! reloading workflows");
|
||||
|
||||
try {
|
||||
Workflow.fetchWorkflows();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
function SlightBox({
|
||||
onClick,
|
||||
name,
|
||||
icon,
|
||||
}: {
|
||||
onClick: (e: any) => void;
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
key="save"
|
||||
onClick={onClick}
|
||||
className={classes(
|
||||
"group flex cursor-pointer flex-col rounded duration-100"
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 aspect-square min-h-0 w-full min-w-0 rounded-lg border border-dashed border-transparent border-zinc-600 duration-100 group-hover:border-solid group-hover:border-zinc-400">
|
||||
{icon}
|
||||
</div>
|
||||
<h1
|
||||
className={classes(
|
||||
"w-full grow select-none text-zinc-400 group-hover:text-zinc-200"
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const fetchWorkflows = async () => {
|
||||
const response = await invoke("fetch_workflows");
|
||||
Workflow.use.getState().setWorkflows(response as Workflow[]);
|
||||
};
|
||||
|
||||
export const applyWorkflow = async (workflowId: string) => {
|
||||
const workflow = use
|
||||
.getState()
|
||||
.workflows.find((workflow) => workflow.name === workflowId);
|
||||
|
||||
if (!workflow) return;
|
||||
|
||||
const comfyApp = Comfy.get();
|
||||
if (!comfyApp) return;
|
||||
|
||||
// turn serialized_workflow string into a file
|
||||
const blob = new Blob([workflow.serialized_workflow], {
|
||||
type: "text/json",
|
||||
});
|
||||
const file = new File([blob], "workflow.json", { type: "text/json" });
|
||||
|
||||
// load the file into comfy
|
||||
comfyApp.handleFile(file);
|
||||
};
|
||||
|
||||
export const use = create<{
|
||||
workflows: Workflow[];
|
||||
setWorkflows: (workflows: Workflow[]) => void;
|
||||
}>((set) => ({
|
||||
workflows: [],
|
||||
setWorkflows: (workflows) => set({ workflows }),
|
||||
}));
|
||||
|
||||
export function Dropdown({ className }: Styleable) {
|
||||
const { workflows, setWorkflows } = Workflow.use();
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkflows();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: " save",
|
||||
image: "/save.svg",
|
||||
component: (onClick: any) => (
|
||||
<SlightBox
|
||||
onClick={(e) => {
|
||||
Modal.State.use.getState().setOpen(true);
|
||||
onClick(e);
|
||||
}}
|
||||
name="Save workflow"
|
||||
icon={<Theme.Icon.Save color="#7F7F7F" />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
...(workflows ?? [])
|
||||
.sort((a, b) =>
|
||||
new Date(a.created_at) > new Date(b.created_at) ? -1 : 1
|
||||
)
|
||||
.map((workflow: Workflow) => ({
|
||||
value: workflow.name,
|
||||
name: workflow.name,
|
||||
image: workflow.icon ?? "/file-code.svg",
|
||||
onDelete: async () => {
|
||||
await invoke("delete_workflow", {
|
||||
name: workflow.name.replace(/ /g, "_").toLowerCase(),
|
||||
});
|
||||
setWorkflows(workflows.filter((w) => w.name !== workflow.name));
|
||||
},
|
||||
})),
|
||||
|
||||
{
|
||||
value: " default",
|
||||
image: "/rotate.svg",
|
||||
component: (onClick: any) => (
|
||||
<SlightBox
|
||||
onClick={(e) => {
|
||||
Comfy.get()?.loadGraphData();
|
||||
onClick(e);
|
||||
}}
|
||||
name="Load default"
|
||||
icon={<Theme.Icon.Rotate color="#7F7F7F" />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[setWorkflows, workflows]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Theme.Popout
|
||||
title="Workflows"
|
||||
label="Workflow"
|
||||
options={options}
|
||||
value={null}
|
||||
placeholder={"Select workflow"}
|
||||
className={classes(className)}
|
||||
onClick={applyWorkflow}
|
||||
/>
|
||||
<Modal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { Style, Styles } from "./Style";
|
||||
import { TopBar } from "./TopBar";
|
||||
import { Upscale, Upscales } from "./Upscale";
|
||||
import { Variations } from "./Variation";
|
||||
import { Workflow } from "./Workflow";
|
||||
|
||||
export * from "./Images";
|
||||
|
||||
@@ -41,6 +42,8 @@ type Props = Styleable &
|
||||
example?: Generation.Image.Prompt.Examples.Example;
|
||||
hideControls?: boolean;
|
||||
placeholder?: boolean;
|
||||
progress?: number;
|
||||
outputID?: ID;
|
||||
|
||||
onClick?: () => void;
|
||||
onDelete?: () => void;
|
||||
@@ -51,6 +54,8 @@ export function Image({
|
||||
// example,
|
||||
hideControls,
|
||||
placeholder,
|
||||
progress,
|
||||
outputID,
|
||||
|
||||
scale,
|
||||
preserveAspectRatio,
|
||||
@@ -155,13 +160,15 @@ export function Image({
|
||||
<Image.SpecialEffects
|
||||
showing={shouldShowSpecialEffects}
|
||||
loading={!placeholder && shouldShowSpecialEffects}
|
||||
progress={progress}
|
||||
output={outputID}
|
||||
variant={(style.height ?? 512) < 48 ? "small" : undefined}
|
||||
// example={example}
|
||||
// onClick={example ? onTryTemplate : undefined}
|
||||
// input={currentInput?.id}
|
||||
/>
|
||||
),
|
||||
[placeholder, shouldShowSpecialEffects, style.height]
|
||||
[placeholder, progress, shouldShowSpecialEffects, style.height]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
@@ -221,6 +228,7 @@ export declare namespace Image {
|
||||
Upscales,
|
||||
Upscale,
|
||||
Exception,
|
||||
Workflow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,6 +262,7 @@ export namespace Image {
|
||||
Image.Upscales = Upscales;
|
||||
Image.Upscale = Upscale;
|
||||
Image.Exception = Exception;
|
||||
Image.Workflow = Workflow;
|
||||
|
||||
export const get = (id: ID): Image | undefined =>
|
||||
Images.State.use(({ images }) => images[id]);
|
||||
|
||||
@@ -3,16 +3,14 @@ import {
|
||||
PluginSettings,
|
||||
PluginStatus,
|
||||
} from "@stability/stablestudio-plugin";
|
||||
import { Markdown } from "~/Markdown";
|
||||
import { Comfy } from "~/Comfy";
|
||||
|
||||
import { Plugin } from "~/Plugin";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
import { Panel } from "./Panel";
|
||||
import { Setting } from "./Setting";
|
||||
|
||||
export function Manifest({
|
||||
id,
|
||||
manifest,
|
||||
pluginStatus,
|
||||
settings,
|
||||
@@ -24,11 +22,10 @@ export function Manifest({
|
||||
settings: PluginSettings;
|
||||
setSetting: (key: string, value: any) => void;
|
||||
}) {
|
||||
const unloadPlugin = Plugin.useUnload();
|
||||
return (
|
||||
<Panel className="flex flex-col gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{manifest?.icon && (
|
||||
@@ -82,35 +79,9 @@ export function Manifest({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{id && unloadPlugin && (
|
||||
<Theme.Button
|
||||
className="h-18"
|
||||
icon={Theme.Icon.Trash}
|
||||
onClick={() => id && unloadPlugin(id)}
|
||||
>
|
||||
Uninstall
|
||||
</Theme.Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-5">
|
||||
{manifest?.version && (
|
||||
<MiniManifestField label="Version" value={manifest.version} />
|
||||
)}
|
||||
<MiniManifestField
|
||||
label="License"
|
||||
value={manifest?.license ?? "No license"}
|
||||
/>
|
||||
<MiniManifestField
|
||||
label="Author"
|
||||
value={manifest?.author ?? "No author"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ManifestField
|
||||
label="About"
|
||||
value={manifest?.description ?? "No description"}
|
||||
markdown
|
||||
/>
|
||||
<Comfy.Output />
|
||||
</div>
|
||||
|
||||
{Object.keys(settings).length > 0 && (
|
||||
@@ -129,39 +100,3 @@ export function Manifest({
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function ManifestField({
|
||||
label,
|
||||
value,
|
||||
markdown,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | undefined;
|
||||
markdown?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={classes("flex flex-col gap-2", className)}>
|
||||
<Theme.Label className="ml-0">{label}</Theme.Label>
|
||||
{markdown ? <Markdown text={value ?? ""} /> : <p>{value}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniManifestField({
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | undefined;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p className={classes("opacity-muted text-sm", className)}>
|
||||
<span className="select-none opacity-50">{label} </span>
|
||||
{value}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
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, sep } from "@tauri-apps/api/path";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { GlobalState } from "~/GlobalState";
|
||||
import { Plugin } from "~/Plugin";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
import { Install } from "./Install";
|
||||
import { Manifest } from "./Manifest";
|
||||
import { Setting } from "./Setting";
|
||||
|
||||
export function Settings() {
|
||||
const [pluginStatus, setPluginStatus] = useState<PluginStatus | undefined>();
|
||||
const pluginSetup = Plugin.useSetup();
|
||||
const [comfyLocation, setComfyLocation] = useState<string | undefined>();
|
||||
const [versions, setVersions] = useState<string[]>([]);
|
||||
|
||||
const { manifest, settings, setSetting, getStatus } = Plugin.use(
|
||||
({ manifest, settings, setSetting, getStatus }) => ({
|
||||
@@ -22,8 +25,6 @@ export function Settings() {
|
||||
})
|
||||
);
|
||||
|
||||
const { developerMode, setDeveloperMode } = Settings.use();
|
||||
|
||||
useEffect(() => {
|
||||
function fetchStatus() {
|
||||
if (!getStatus) return;
|
||||
@@ -57,6 +58,33 @@ export function Settings() {
|
||||
[settings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchComfyLocation() {
|
||||
const path = await invoke("get_setting", {
|
||||
key: "comfyui_location",
|
||||
})
|
||||
.then((res) => res)
|
||||
.catch(() => appDataDir());
|
||||
setComfyLocation(`${path}${sep}ComfyUI`);
|
||||
}
|
||||
|
||||
async function fetchVersion() {
|
||||
const version = await getVersion();
|
||||
const tauriVersion = await getTauriVersion();
|
||||
const os = await platform();
|
||||
const osVersion = await getOsVerison();
|
||||
|
||||
setVersions([
|
||||
`StableStudio (${version})`,
|
||||
`Tauri (${tauriVersion})`,
|
||||
`${os} (${osVersion})`,
|
||||
]);
|
||||
}
|
||||
|
||||
fetchComfyLocation();
|
||||
fetchVersion();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full justify-between overflow-y-auto bg-zinc-900 px-5 py-6">
|
||||
@@ -78,24 +106,41 @@ export function Settings() {
|
||||
settings={settings ?? {}}
|
||||
setSetting={setSetting as never}
|
||||
/>
|
||||
<Setting
|
||||
settingKey="developerMode"
|
||||
setSetting={() => setDeveloperMode(!developerMode)}
|
||||
settingValue={{
|
||||
type: "boolean",
|
||||
title: "Developer mode",
|
||||
description:
|
||||
"Enable experimental features such as installing untrusted plugins",
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex justify-between">
|
||||
<label>ComfyUI location</label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Theme.Input
|
||||
className="opacity-50"
|
||||
placeholder={"/path/to/ComfyUI"}
|
||||
value={comfyLocation}
|
||||
onChange={doNothing}
|
||||
disabled
|
||||
/>
|
||||
<Theme.Button
|
||||
className="py-2"
|
||||
onClick={async () => {
|
||||
await invoke("show_in_folder", { path: comfyLocation });
|
||||
}}
|
||||
>
|
||||
<Theme.Icon.ExternalLink className="h-6 w-6" />
|
||||
</Theme.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
required: false,
|
||||
value: developerMode,
|
||||
}}
|
||||
/>
|
||||
{developerMode && (
|
||||
<Install
|
||||
installPlugin={(url) => url && pluginSetup.loadFromURL(url)}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-8 flex gap-3 text-xs font-light opacity-40">
|
||||
{versions.map((version, i) => (
|
||||
<>
|
||||
{i !== 0 && (
|
||||
<div key={`${version}-dot`} className="select-none">
|
||||
·
|
||||
</div>
|
||||
)}
|
||||
<div key={version}>{version}</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -425,6 +425,70 @@ export function Upscale(props: Props) {
|
||||
));
|
||||
}
|
||||
|
||||
export function Rotate(props: Props) {
|
||||
return defaults(props)(({ width, height, color, ...props }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 12C7 12.9889 7.29324 13.9556 7.84265 14.7778C8.39206 15.6001 9.17295 16.241 10.0866 16.6194C11.0002 16.9978 12.0055 17.0969 12.9754 16.9039C13.9454 16.711 14.8363 16.2348 15.5355 15.5355C16.2348 14.8363 16.711 13.9454 16.9039 12.9754C17.0969 12.0055 16.9978 11.0002 16.6194 10.0866C16.241 9.17295 15.6001 8.39206 14.7778 7.84265C13.9556 7.29324 12.9889 7 12 7C10.6022 7.00526 9.26054 7.55068 8.25556 8.52222L7 9.77778"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path
|
||||
d="M7 7V10H10"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
}
|
||||
|
||||
export function Save(props: Props) {
|
||||
return defaults(props)(({ width, height, color, ...props }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
strokeWidth="0.9"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M15.8889 17H8.11111C7.81643 17 7.53381 16.8829 7.32544 16.6746C7.11706 16.4662 7 16.1836 7 15.8889V8.11111C7 7.81643 7.11706 7.53381 7.32544 7.32544C7.53381 7.11706 7.81643 7 8.11111 7H14.2222L17 9.77778V15.8889C17 16.1836 16.8829 16.4662 16.6746 16.6746C16.4662 16.8829 16.1836 17 15.8889 17Z"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path
|
||||
d="M15 17V13H9V17"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path
|
||||
d="M9 7V10H13"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
}
|
||||
|
||||
const defaults =
|
||||
(props: Props) =>
|
||||
(render: (props: Props) => JSX.Element): JSX.Element =>
|
||||
|
||||
@@ -77,6 +77,8 @@ import {
|
||||
Instagram,
|
||||
ModelIcon,
|
||||
Rectangle,
|
||||
Rotate,
|
||||
Save,
|
||||
Scale,
|
||||
ShareIcon,
|
||||
SlidersIcon,
|
||||
@@ -169,6 +171,8 @@ export declare namespace Icon {
|
||||
Upscale,
|
||||
Keyboard,
|
||||
ChevronsLeftRight,
|
||||
Rotate,
|
||||
Save,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -262,6 +266,8 @@ export namespace Icon {
|
||||
Icon.Upscale = makeComponent(Upscale);
|
||||
Icon.Keyboard = makeComponent(Keyboard);
|
||||
Icon.ChevronsLeftRight = makeComponent(ChevronsLeftRight);
|
||||
Icon.Rotate = makeComponent(Rotate);
|
||||
Icon.Save = makeComponent(Save);
|
||||
|
||||
export function Invisible(props: Props) {
|
||||
return (
|
||||
|
||||
@@ -5,11 +5,18 @@ import { Box } from "~/Geometry";
|
||||
import { Theme } from "~/Theme";
|
||||
|
||||
export type Option = {
|
||||
name: React.ReactNode;
|
||||
value: any;
|
||||
image?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
onDelete?: () => void;
|
||||
} & (
|
||||
| {
|
||||
name: React.ReactNode;
|
||||
}
|
||||
| {
|
||||
component: (onClick: (value: any) => void) => React.ReactNode;
|
||||
}
|
||||
);
|
||||
|
||||
export function Popout({
|
||||
onClick,
|
||||
@@ -109,7 +116,11 @@ export function Popout({
|
||||
/>
|
||||
)}
|
||||
<h1 className="w-full grow select-none">
|
||||
{valueOption?.name ?? placeholder ?? "Select"}
|
||||
{(valueOption && "name" in valueOption
|
||||
? valueOption?.name
|
||||
: undefined) ??
|
||||
placeholder ??
|
||||
"Select"}
|
||||
</h1>
|
||||
<Theme.Icon.ChevronRight className="h-6 w-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
@@ -154,40 +165,61 @@ function Floating({
|
||||
>
|
||||
{!!children
|
||||
? children
|
||||
: options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={
|
||||
option.disabled ? doNothing : () => onClick(option.value)
|
||||
}
|
||||
className={classes(
|
||||
"group flex cursor-pointer flex-col rounded duration-100",
|
||||
option.disabled && "opacity-muted cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{option.image ? (
|
||||
<img
|
||||
className="mb-2 aspect-square h-full w-full rounded-lg border border-transparent duration-100 group-hover:border-zinc-200"
|
||||
src={option.image}
|
||||
alt="Preset Image"
|
||||
/>
|
||||
) : (
|
||||
hasImages && (
|
||||
<div className="mb-2 flex aspect-square w-full items-center justify-center rounded-lg border border-transparent bg-black/20 duration-100 group-hover:border-zinc-200">
|
||||
<Theme.Icon.Slash className="opacity-muted h-12 w-12" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<h1
|
||||
className={classes(
|
||||
"w-full grow select-none text-zinc-400 group-hover:text-zinc-200",
|
||||
option.value === value && "font-medium text-white"
|
||||
)}
|
||||
>
|
||||
{option.name}
|
||||
</h1>
|
||||
</div>
|
||||
))}
|
||||
: options.map(
|
||||
(option, index) =>
|
||||
("component" in option && option.component(onClick)) || (
|
||||
<div
|
||||
key={`${option.value}-${index}-option`}
|
||||
onClick={
|
||||
option.disabled ? doNothing : () => onClick(option.value)
|
||||
}
|
||||
className={classes(
|
||||
"group relative flex cursor-pointer flex-col rounded duration-100",
|
||||
option.disabled && "opacity-muted cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{option.onDelete && (
|
||||
<Theme.Tooltip
|
||||
content="Delete"
|
||||
placement="right"
|
||||
showArrow
|
||||
distance={15}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
option.onDelete?.();
|
||||
}}
|
||||
className="group/delete pointer-events-none absolute -right-4 -top-4 h-fit w-fit scale-50 cursor-pointer rounded-xl bg-zinc-900 p-1 text-white/75 opacity-0 duration-150 ease-in-out hover:text-white group-hover:pointer-events-auto group-hover:-right-2 group-hover:-top-2 group-hover:scale-100 group-hover:opacity-100"
|
||||
>
|
||||
<Theme.Icon.X color="currentColor" />
|
||||
</div>
|
||||
</Theme.Tooltip>
|
||||
)}
|
||||
{option.image ? (
|
||||
<img
|
||||
className="mb-2 aspect-square w-full select-none rounded-lg border border-transparent object-cover duration-100 group-hover:border-zinc-200"
|
||||
src={option.image}
|
||||
alt="Preset Image"
|
||||
/>
|
||||
) : (
|
||||
hasImages && (
|
||||
<div className="mb-2 flex aspect-square w-full items-center justify-center rounded-lg border border-transparent bg-black/20 duration-100 group-hover:border-zinc-200">
|
||||
<Theme.Icon.Slash className="opacity-muted h-12 w-12" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<h1
|
||||
className={classes(
|
||||
"w-full grow select-none text-zinc-400 group-hover:text-zinc-200",
|
||||
option.value === value && "font-medium text-white"
|
||||
)}
|
||||
>
|
||||
{"name" in option && option.name}
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,9 @@ export default defineConfig(({ mode }) => {
|
||||
"/object_info": redirectComfy,
|
||||
"/view": redirectComfy,
|
||||
"/queue": redirectComfy,
|
||||
"/history": redirectComfy,
|
||||
"/interrupt": redirectComfy,
|
||||
"/upload": redirectComfy,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
10
yarn.lock
@@ -1263,6 +1263,7 @@ __metadata:
|
||||
prettier-plugin-tailwindcss: ^0.2.1
|
||||
query-string: ^8.1.0
|
||||
react: ^18.2.0
|
||||
react-circular-progressbar: ^2.1.0
|
||||
react-dom: ^18.2.0
|
||||
react-konva: ^18.2.3
|
||||
react-konva-utils: ^0.3.1
|
||||
@@ -6322,6 +6323,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-circular-progressbar@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "react-circular-progressbar@npm:2.1.0"
|
||||
peerDependencies:
|
||||
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: dc118010a8f94733daafac586c969f7e889ad0736d96a0fda79406ee62e9410848abfd3dee4887bb0ff46b99e1f8c86ee1afabc88dfbf4dcb95107b22de1d6d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:^18.2.0":
|
||||
version: 18.2.0
|
||||
resolution: "react-dom@npm:18.2.0"
|
||||
|
||||