Compare commits
116
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae919a802b | ||
|
|
b81b0c8e0a | ||
|
|
827a1f1731 | ||
|
|
9afa5b920f | ||
|
|
0ced230c8f | ||
|
|
705633b9d9 | ||
|
|
f97450465d | ||
|
|
7228169ee7 | ||
|
|
4fcf3dc053 | ||
|
|
f7652cbcbe | ||
|
|
727f55d843 | ||
|
|
5343172070 | ||
|
|
1c488fdbe4 | ||
|
|
6176a56ff9 | ||
|
|
dd7c892ef5 | ||
|
|
297dbab260 | ||
|
|
40c9ec735a | ||
|
|
728332d5f7 | ||
|
|
b5efdf65a5 | ||
|
|
3bc35d1d24 | ||
|
|
d8e7522807 | ||
|
|
9f90903f08 | ||
|
|
056d0cf1d1 | ||
|
|
e272cd2c69 | ||
|
|
4bf83146ee | ||
|
|
4059f3f412 | ||
|
|
d6733ce226 | ||
|
|
e76736f72a | ||
|
|
952bae503f | ||
|
|
93db2f5654 | ||
|
|
1827f6ec65 | ||
|
|
ded779d20b | ||
|
|
1d9b1ef665 | ||
|
|
a20fd1f3ed | ||
|
|
2262bd5bb1 | ||
|
|
f040fc294c | ||
|
|
e64e78e68c | ||
|
|
14eadf8582 | ||
|
|
1ebaaca144 | ||
|
|
797b878aa8 | ||
|
|
d972cf8e88 | ||
|
|
352572dd64 | ||
|
|
585adf9e00 | ||
|
|
1995ac6278 | ||
|
|
7d36bcd6ff | ||
|
|
cb9fa6de6d | ||
|
|
5a50adc876 | ||
|
|
22a030b4f6 | ||
|
|
e012933486 | ||
|
|
6bbfe6fdc4 | ||
|
|
f085b4a23c | ||
|
|
37d949f575 | ||
|
|
429a3a0d60 | ||
|
|
73f400340c | ||
|
|
ecd91c636c | ||
|
|
50e69bccdf | ||
|
|
8efdf23c2f | ||
|
|
de29d72afa | ||
|
|
4767dba038 | ||
|
|
01db33312c | ||
|
|
c19423afc3 | ||
|
|
cfd349f05c | ||
|
|
acf12e54ca | ||
|
|
f566650a60 | ||
|
|
0ee3c36f00 | ||
|
|
2c26b90f7d | ||
|
|
03c83f3f01 | ||
|
|
b62c3ef9df | ||
|
|
823bb4cc2c | ||
|
|
dbcf3d434b | ||
|
|
eb1dce7168 | ||
|
|
c9e48dde09 | ||
|
|
cc4a5e4c94 | ||
|
|
4d40613b15 | ||
|
|
b447707173 | ||
|
|
77f524aa55 | ||
|
|
321c996439 | ||
|
|
802bdfd85b | ||
|
|
74cd371af8 | ||
|
|
5dc8ab668f | ||
|
|
8ed164625f | ||
|
|
594525b997 | ||
|
|
4427a5c2a8 | ||
|
|
0383b3ec3b | ||
|
|
5984111a54 | ||
|
|
fd183c641d | ||
|
|
e5a82dce97 | ||
|
|
a7a6b215ab | ||
|
|
a8eed53e26 | ||
|
|
95d32bd6f7 | ||
|
|
2d949971d7 | ||
|
|
1aed7674ed | ||
|
|
d70a0afac5 | ||
|
|
ebeae00012 | ||
|
|
fe06783557 | ||
|
|
db4a896632 | ||
|
|
1d45ea7f15 | ||
|
|
61a899a2bf | ||
|
|
f86417afcf | ||
|
|
dcf41eccd2 | ||
|
|
68bc061d9d | ||
|
|
c18a6f8d72 | ||
|
|
2e1853574a | ||
|
|
39d7515f25 | ||
|
|
4a17c57741 | ||
|
|
3f1bf33a9e | ||
|
|
89c84cfce7 | ||
|
|
5abf796cf0 | ||
|
|
3a98031a89 | ||
|
|
5989874857 | ||
|
|
2c1a3be4ab | ||
|
|
0053caf0ad | ||
|
|
816b4bbef6 | ||
|
|
bbb483b27b | ||
|
|
2b5691b84a | ||
|
|
6ad9609325 |
@@ -0,0 +1,20 @@
|
|||||||
|
name: Publish to Comfy registry
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-node:
|
||||||
|
name: Publish Custom Node to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Publish Custom Node
|
||||||
|
uses: Comfy-Org/publish-node-action@main
|
||||||
|
with:
|
||||||
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||||
@@ -4,13 +4,12 @@ A ComfyUI workflow and model manager extension to **organize and manage all your
|
|||||||
|
|
||||||
If you have questions or suggestions, please [join our Discord](https://discord.gg/HHvfEurv2Z)!
|
If you have questions or suggestions, please [join our Discord](https://discord.gg/HHvfEurv2Z)!
|
||||||
|
|
||||||
[New!] we're building a comfyui custom nodes library wiki like Pypi: https://www.nodecafe.org/
|
[New!] We provide 👭**team workspace** now! If you need to **share workflows, backup workflow versions for enterprise and teams**, please contact me about our team/enterprise product at weixuanfu01@gmail.com or join discord and DM me @briefpeach
|
||||||
welcome any feedbacks!
|
|
||||||
|
|
||||||
🔴‼️ If you updated or cloned this repo between March 14-17, 2024, please update it again to latest verison, there were some bugs that may cause you lose changes in Auto Save mode.
|
|
||||||
|
|
||||||
‼️ [3-23-2024]**search and install models from civitai is not working in previous versions**, because civitai changed something in their api. **Please update to the latest version** if you are seeing blank screen in Install Models dialog
|
‼️ [3-23-2024]**search and install models from civitai is not working in previous versions**, because civitai changed something in their api. **Please update to the latest version** if you are seeing blank screen in Install Models dialog
|
||||||
|
|
||||||
|
[5-7-2024] Author (me) is job hunting in Beijing 🥲 so recently maybe too busy to update this plugin. I will come back to updating this plugin as soon as I finish job hunting 🥲🫠
|
||||||
|
|
||||||
<img width="610" alt="Screenshot 2024-03-08 at 6 41 18 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/30455eb1-3d12-4930-bb81-28b33148fbaa">
|
<img width="610" alt="Screenshot 2024-03-08 at 6 41 18 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/30455eb1-3d12-4930-bb81-28b33148fbaa">
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -21,15 +20,14 @@ Simply search for “ComfyUI Workspace Manager”, install, and restart ComfyUI.
|
|||||||
|
|
||||||
### [Option 2] via git
|
### [Option 2] via git
|
||||||
|
|
||||||
Like any other custom node installation, in your ComfyUI root folder
|
Like any other custom node installation, in your ComfyUI root folder run command and restart your ComfyUI.
|
||||||
|
|
||||||
```
|
```
|
||||||
cd custom_nodes && git clone https://github.com/11cafe/comfyui-workspace-manager.git
|
cd custom_nodes && git clone https://github.com/11cafe/comfyui-workspace-manager.git
|
||||||
```
|
```
|
||||||
|
|
||||||
and restart your ComfyUI.
|
‼️ Be careful with Auto Save mode as it is constantly overwriting/saving your workflow, which can lead to unwanted overwriting of your precious work if not using carefully. If you disabled Auto Save, make sure you don't forget to Shift+S to save your workflows.
|
||||||
|
|
||||||
It is recommended to do git clone than downloading zip folder, because you can get latest updates by `git pull`!
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ It is recommended to do git clone than downloading zip folder, because you can g
|
|||||||
- 📂Save and sync all your workflows in a local folder (by default under `/ComfyUI/my_workflows` customize in Settings)
|
- 📂Save and sync all your workflows in a local folder (by default under `/ComfyUI/my_workflows` customize in Settings)
|
||||||
- **Save workflow by pressing the 💾 button, or ⌨Shift+S shortcut** (customize in Settings).
|
- **Save workflow by pressing the 💾 button, or ⌨Shift+S shortcut** (customize in Settings).
|
||||||
- Every time you save, it will create a new record in **Version History**. You can easily revert to any version.
|
- Every time you save, it will create a new record in **Version History**. You can easily revert to any version.
|
||||||
- **Enable/disable autosave workflow** (please disable autosave if you are experiencing UI lagging)
|
- **Enable/disable autosave workflow** (please disable autosave if you are experiencing UI slowing)
|
||||||
- You can move files using OS File Explorer and it will be **two-way synced** with workspace.
|
- You can move files using OS File Explorer and it will be **two-way synced** with workspace.
|
||||||
- 🕛**Version control** of workflow, create/switch versions
|
- 🕛**Version control** of workflow, create/switch versions
|
||||||
- Bulk import workflows into your workspace
|
- Bulk import workflows into your workspace
|
||||||
@@ -63,17 +61,8 @@ Recommend using Chrome browser for the best experience. Safari doesn't work well
|
|||||||
|
|
||||||
[中文版教程](https://www.uisdc.com/workspace-manager) [Roadmap](https://github.com/orgs/11cafe/projects/1/views/1)
|
[中文版教程](https://www.uisdc.com/workspace-manager) [Roadmap](https://github.com/orgs/11cafe/projects/1/views/1)
|
||||||
|
|
||||||
# <<<<<<< HEAD
|
## Updates
|
||||||
|
**🦩v2.1 New gallery! Inspect image metadata, search image by prompt, model/lora used**
|
||||||
🚧🔴 Drag and drop to insert subworkflow is not working. I have debugged for many hours but just can't make it work perfectly. If anyone is interested here is the code:
|
|
||||||
https://github.com/11cafe/comfyui-workspace-manager/blob/d2d2f0c1f1ace35236e73fa5074e9feb83342773/ui/src/utils.tsx#L192
|
|
||||||
I would really appreciate it if someone can find a solution to insert a subworkflow to current canvas given the json object.
|
|
||||||
|
|
||||||
### 🦄v2.0 beta: Two-way sync with local folder, select model install path, cloud sync workflows
|
|
||||||
|
|
||||||
> > > > > > > dev
|
|
||||||
|
|
||||||
**🦩v2.1 New gallery coming soon! Inspect image metadata, search image by prompt, model/lora used**
|
|
||||||
|
|
||||||
<img width="700" alt="Screenshot 2024-03-23 at 12 58 38 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/5bac9793-7491-4131-b850-037094d03823">
|
<img width="700" alt="Screenshot 2024-03-23 at 12 58 38 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/5bac9793-7491-4131-b850-037094d03823">
|
||||||
|
|
||||||
@@ -81,13 +70,9 @@ Control+P quick spotlight search (like VSCode)
|
|||||||
|
|
||||||
<img width="500" alt="Screenshot 2024-03-27 at 3 49 35 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/47c14904-180c-4c52-a644-223d0936c9fa">
|
<img width="500" alt="Screenshot 2024-03-27 at 3 49 35 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/47c14904-180c-4c52-a644-223d0936c9fa">
|
||||||
|
|
||||||
**🦄v2.0 Two-way sync with local folder, select model install path, cloud sync workflows**
|
**🦄v2.0 Two-way sync with local folder, models manager support external models path like Automatic1111, cloud sync workflows**
|
||||||
|
|
||||||
- Two-way sync with selected local folder so you can move files using File Explore or Finder to sync them with workspace!
|
v2.0 demo: https://youtu.be/7Hht_BMe844
|
||||||
- **Models manager support external models path like Automatic1111**! Choose models downloading folder from added external paths.
|
|
||||||
- Add models thumbnails image by putting a image file with the same name in models folder like A1111
|
|
||||||
- One click sharing to cloud to backup your workflows to cloud
|
|
||||||
v2.0 demo: https://youtu.be/7Hht_BMe844
|
|
||||||
|
|
||||||
**v1.7 ComfyUI model manager, view your installed models, install models from civit ai, open workflow in multiple browser tabs**
|
**v1.7 ComfyUI model manager, view your installed models, install models from civit ai, open workflow in multiple browser tabs**
|
||||||
|
|
||||||
@@ -97,7 +82,7 @@ Control+P quick spotlight search (like VSCode)
|
|||||||
|
|
||||||
<img width="200" alt="Screenshot 2024-01-23 at 12 26 56 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/d29d0831-a15a-4597-acf6-facee4dbc76e">
|
<img width="200" alt="Screenshot 2024-01-23 at 12 26 56 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/d29d0831-a15a-4597-acf6-facee4dbc76e">
|
||||||
|
|
||||||
<img width="460" alt="gallery" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/2b7886c9-2d4a-46c6-adac-429720f74f11">
|
## Usage
|
||||||
|
|
||||||
switch between flows:
|
switch between flows:
|
||||||
|
|
||||||
@@ -111,10 +96,6 @@ Bulk import flows:
|
|||||||
|
|
||||||
<img width="400" alt="Screenshot 2024-01-02 at 10 22 45 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/884c8852-73c7-41d6-9179-7fb1fcea9d04">
|
<img width="400" alt="Screenshot 2024-01-02 at 10 22 45 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/884c8852-73c7-41d6-9179-7fb1fcea9d04">
|
||||||
|
|
||||||
add tags to flow:
|
|
||||||
|
|
||||||
<img width="400" alt="Screenshot 2023-12-13 at 1 54 04 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/6bf4d22b-f52a-4d23-8882-4a31162616f3">
|
|
||||||
|
|
||||||
Workflow operations: Manual Save, Download, Discard unsaved changes:
|
Workflow operations: Manual Save, Download, Discard unsaved changes:
|
||||||
|
|
||||||
<img width="500" alt="Screenshot 2023-12-29 at 9 59 57 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/88c82672-ebf2-4feb-bf5b-74b447843c2c">
|
<img width="500" alt="Screenshot 2023-12-29 at 9 59 57 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/88c82672-ebf2-4feb-bf5b-74b447843c2c">
|
||||||
|
|||||||
+13
-15
@@ -5,17 +5,18 @@ import folder_paths
|
|||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess # don't remove this
|
import subprocess
|
||||||
from urllib.parse import urlparse
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from .service.model_manager.model_installer import download_url_with_wget
|
|
||||||
from .service.model_manager.model_list import get_model_list
|
|
||||||
from .service.media_service import *
|
from .service.media_service import *
|
||||||
from .service.file_sync_service import *
|
from .service.file_sync_service import *
|
||||||
from .service.db_service import get_my_workflows_dir
|
from .service.db_service import get_my_workflows_dir
|
||||||
from .service.node_service import *
|
from .service.node_service import *
|
||||||
|
try:
|
||||||
|
from send2trash import send2trash
|
||||||
|
except ImportError:
|
||||||
|
send2trash = None
|
||||||
|
|
||||||
WEB_DIRECTORY = "entry"
|
WEB_DIRECTORY = "entry"
|
||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
@@ -112,31 +113,28 @@ async def update_file(request):
|
|||||||
await asyncio.to_thread(write_json_to_file, json_str)
|
await asyncio.to_thread(write_json_to_file, json_str)
|
||||||
return web.Response(text="File updated successfully")
|
return web.Response(text="File updated successfully")
|
||||||
|
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.post("/workspace/delete_file")
|
@server.PromptServer.instance.routes.post("/workspace/delete_file")
|
||||||
async def delete_file(request):
|
async def delete_file(request):
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
file_path = data['file_path']
|
file_path = data['file_path']
|
||||||
delete_empty_folder = data['deleteEmptyFolder']
|
|
||||||
|
|
||||||
def sync_delete_file(file_path, delete_empty_folder):
|
def sync_delete_file(file_path):
|
||||||
my_workflows_dir = get_my_workflows_dir()
|
my_workflows_dir = get_my_workflows_dir()
|
||||||
full_path = os.path.join(my_workflows_dir, file_path)
|
full_path = os.path.join(my_workflows_dir, file_path)
|
||||||
|
|
||||||
if os.path.exists(full_path):
|
if os.path.exists(full_path):
|
||||||
os.remove(full_path)
|
if send2trash:
|
||||||
directory = os.path.dirname(full_path)
|
send2trash(full_path)
|
||||||
if delete_empty_folder and not os.listdir(directory):
|
|
||||||
# If the directory is empty, remove the directory
|
|
||||||
os.rmdir(directory)
|
|
||||||
return "File and empty directory deleted successfully"
|
|
||||||
else:
|
else:
|
||||||
return "File deleted successfully"
|
os.remove(full_path)
|
||||||
|
print("❌⛔️send2trash is not available. Deleting file permanently. Please `pip install send2trash`")
|
||||||
|
|
||||||
|
return "File deleted successfully"
|
||||||
else:
|
else:
|
||||||
return "File not found"
|
return "File not found"
|
||||||
|
|
||||||
# Run the synchronous file operation in a separate thread
|
# Run the synchronous file operation in a separate thread
|
||||||
response_text = await asyncio.to_thread(sync_delete_file, file_path, delete_empty_folder)
|
response_text = await asyncio.to_thread(sync_delete_file, file_path)
|
||||||
|
|
||||||
if response_text == "File not found":
|
if response_text == "File not found":
|
||||||
return web.Response(text=response_text, status=404)
|
return web.Response(text=response_text, status=404)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-workspace-manager"
|
||||||
|
description = "A ComfyUI custom node for project management to centralize the management of all your workflows in one place. Seamlessly switch between workflows, create and update them within a single workspace, like Google Docs."
|
||||||
|
version = "1.0.0"
|
||||||
|
license = "LICENSE"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/11cafe/comfyui-workspace-manager"
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "weixuan11"
|
||||||
|
DisplayName = "comfyui-workspace-manager"
|
||||||
|
Icon = ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
send2trash
|
||||||
@@ -136,7 +136,11 @@ def read_workflow_file(path, id):
|
|||||||
|
|
||||||
def check_and_update_workflow_id(file_path, seen_ids):
|
def check_and_update_workflow_id(file_path, seen_ids):
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
json_data = json.load(f)
|
try:
|
||||||
|
json_data = json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Error reading JSON from {file_path}: {e}")
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
"extra" in json_data
|
"extra" in json_data
|
||||||
and "workspace_info" in json_data["extra"]
|
and "workspace_info" in json_data["extra"]
|
||||||
@@ -172,7 +176,10 @@ def dedupe_workflow_ids():
|
|||||||
if not file.endswith(".json"):
|
if not file.endswith(".json"):
|
||||||
continue
|
continue
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
check_and_update_workflow_id(file_path, seen_ids)
|
try:
|
||||||
|
check_and_update_workflow_id(file_path, seen_ids)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while processing file {file_path}: {e}")
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get('/workspace/deduplicate_workflow_ids')
|
@server.PromptServer.instance.routes.get('/workspace/deduplicate_workflow_ids')
|
||||||
async def deduplicate_workflow_ids(request):
|
async def deduplicate_workflow_ids(request):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from .model_manager.model_preview import get_thumbnail_for_image_file
|
|||||||
|
|
||||||
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
|
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
|
||||||
video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv']
|
video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv']
|
||||||
API_URL = 'https://www.nodecafe.org/api/image/upload'
|
API_URL = 'https://www.nodecafe.co/api/image/upload'
|
||||||
def view_media(filename, isPreview = False, isInput = False):
|
def view_media(filename, isPreview = False, isInput = False):
|
||||||
if not filename:
|
if not filename:
|
||||||
return web.Response(status=404)
|
return web.Response(status=404)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if not os.path.exists(FILE_HASH_DICT_FOLDER_PATH):
|
|||||||
os.makedirs(FILE_HASH_DICT_FOLDER_PATH)
|
os.makedirs(FILE_HASH_DICT_FOLDER_PATH)
|
||||||
|
|
||||||
# Early open shelve for better performance
|
# Early open shelve for better performance
|
||||||
with shelve.open(FILE_HASH_DICT_PATH) as file_hash_dict:
|
with shelve.open(FILE_HASH_DICT_PATH) as file_hash_dict_early_open:
|
||||||
print(f"Workspace manager - Openning file hash dict")
|
print(f"Workspace manager - Openning file hash dict")
|
||||||
|
|
||||||
file_list = []
|
file_list = []
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from aiohttp import web
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import glob
|
import traceback
|
||||||
|
import logging
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
import server
|
import server
|
||||||
import uuid
|
import uuid
|
||||||
@@ -41,7 +42,7 @@ def folder_handle(path, recursive, metaInfoOnly, fileList=None):
|
|||||||
if recursive:
|
if recursive:
|
||||||
folder_handle(item_path, recursive, metaInfoOnly, fileList)
|
folder_handle(item_path, recursive, metaInfoOnly, fileList)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error scanning item {item}: {e}")
|
logging.error(f"Error scan workflow {item_path}: {e}, {traceback.format_exc()}")
|
||||||
return fileList
|
return fileList
|
||||||
|
|
||||||
def file_handle(name, fileList, file_path, metaInfoOnly):
|
def file_handle(name, fileList, file_path, metaInfoOnly):
|
||||||
@@ -51,6 +52,9 @@ def file_handle(name, fileList, file_path, metaInfoOnly):
|
|||||||
createTime, updateTime = getFileCreateTime(file_path)
|
createTime, updateTime = getFileCreateTime(file_path)
|
||||||
workspace_info = json_data.get('extra', {}).get('workspace_info', {})
|
workspace_info = json_data.get('extra', {}).get('workspace_info', {})
|
||||||
workflow_id = workspace_info.get('id', str(uuid.uuid4()))
|
workflow_id = workspace_info.get('id', str(uuid.uuid4()))
|
||||||
|
saveLock = workspace_info.get('saveLock', False)
|
||||||
|
cloudID = workspace_info.get('cloudID', None)
|
||||||
|
coverMediaPath = workspace_info.get('coverMediaPath', None)
|
||||||
# Update JSON data with new ID if needed and write back to file
|
# Update JSON data with new ID if needed and write back to file
|
||||||
if 'id' not in workspace_info:
|
if 'id' not in workspace_info:
|
||||||
if 'extra' not in json_data:
|
if 'extra' not in json_data:
|
||||||
@@ -65,6 +69,9 @@ def file_handle(name, fileList, file_path, metaInfoOnly):
|
|||||||
'name': name,
|
'name': name,
|
||||||
'type': "workflow",
|
'type': "workflow",
|
||||||
'id': workflow_id,
|
'id': workflow_id,
|
||||||
|
'saveLock': saveLock,
|
||||||
|
'cloudID': cloudID,
|
||||||
|
'coverMediaPath': coverMediaPath,
|
||||||
'createTime': createTime,
|
'createTime': createTime,
|
||||||
'updateTime': updateTime
|
'updateTime': updateTime
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import server
|
import server
|
||||||
from .db_service import get_my_workflows_dir
|
from .db_service import get_my_workflows_dir
|
||||||
|
try:
|
||||||
|
from send2trash import send2trash
|
||||||
|
except ImportError:
|
||||||
|
send2trash = None
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.post('/workspace/folder/create')
|
@server.PromptServer.instance.routes.post('/workspace/folder/create')
|
||||||
async def create_folder(request):
|
async def create_folder(request):
|
||||||
@@ -31,7 +35,11 @@ def delete_folder_sync(reqJson):
|
|||||||
folder_path = reqJson.get('path')
|
folder_path = reqJson.get('path')
|
||||||
folder_path = os.path.join(get_my_workflows_dir(), folder_path)
|
folder_path = os.path.join(get_my_workflows_dir(), folder_path)
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(folder_path)
|
if send2trash:
|
||||||
|
send2trash(folder_path)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(folder_path)
|
||||||
|
print("❌⛔️send2trash is not available. Deleting file permanently. Please `pip install send2trash`")
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"setupGithooks": "node ../scripts/setupGithooks"
|
"setupGithooks": "node ../scripts/setupGitHooks"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
|||||||
+25
-13
@@ -1,10 +1,22 @@
|
|||||||
import { Table } from "./db-tables/WorkspaceDB";
|
import { Table } from "./db-tables/WorkspaceDB";
|
||||||
import type { ModelsListRespItem } from "./model-manager/types";
|
import type { ModelsListRespItem } from "./model-manager/types";
|
||||||
|
import { api } from "./utils/comfyapp";
|
||||||
|
|
||||||
|
export function fetchApi(
|
||||||
|
route: string,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<Response> {
|
||||||
|
if (api == null) {
|
||||||
|
console.error("api is null!");
|
||||||
|
throw new Error("api is null!");
|
||||||
|
}
|
||||||
|
return api.fetchApi(route, options);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDB(table: Table): Promise<string | undefined> {
|
export async function getDB(table: Table): Promise<string | undefined> {
|
||||||
console.warn("[workspace deprecated] getDB is deprecated", table);
|
console.warn("[workspace deprecated] getDB is deprecated", table);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/workspace/get_db?table=${table}`);
|
const response = await fetchApi(`/workspace/get_db?table=${table}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -18,7 +30,7 @@ export async function getDB(table: Table): Promise<string | undefined> {
|
|||||||
|
|
||||||
export async function saveDB(table: Table, jsonData: string) {
|
export async function saveDB(table: Table, jsonData: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/save_db", {
|
const response = await fetchApi("/workspace/save_db", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -34,7 +46,7 @@ export async function saveDB(table: Table, jsonData: string) {
|
|||||||
|
|
||||||
export async function updateFile(file_path: string, jsonData: string) {
|
export async function updateFile(file_path: string, jsonData: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/update_file", {
|
const response = await fetchApi("/workspace/update_file", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -54,7 +66,7 @@ export async function updateFile(file_path: string, jsonData: string) {
|
|||||||
|
|
||||||
export async function deleteFile(file_path: string, deleteEmptyFolder = false) {
|
export async function deleteFile(file_path: string, deleteEmptyFolder = false) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/delete_file", {
|
const response = await fetchApi("/workspace/delete_file", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -73,7 +85,7 @@ export async function deleteFile(file_path: string, deleteEmptyFolder = false) {
|
|||||||
|
|
||||||
export async function listBackup(dir: string) {
|
export async function listBackup(dir: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/list_backup", {
|
const response = await fetchApi("/workspace/list_backup", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -91,7 +103,7 @@ export async function listBackup(dir: string) {
|
|||||||
|
|
||||||
export async function getSystemDir(root?: string) {
|
export async function getSystemDir(root?: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/get_system_dir", {
|
const response = await fetchApi("/workspace/get_system_dir", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -109,7 +121,7 @@ export async function getSystemDir(root?: string) {
|
|||||||
|
|
||||||
export async function openWorkflowsFolder() {
|
export async function openWorkflowsFolder() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/open_workflow_file_browser", {
|
const response = await fetchApi("/workspace/open_workflow_file_browser", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -123,7 +135,7 @@ export async function openWorkflowsFolder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMyWorkflowsDir() {
|
export async function fetchMyWorkflowsDir() {
|
||||||
const resp = await fetch("/workspace/get_my_workflows_dir");
|
const resp = await fetchApi("/workspace/get_my_workflows_dir");
|
||||||
const res = (await resp.json()) as {
|
const res = (await resp.json()) as {
|
||||||
path?: string;
|
path?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -137,7 +149,7 @@ export async function fetchMyWorkflowsDir() {
|
|||||||
|
|
||||||
export async function getAllModelsList() {
|
export async function getAllModelsList() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/model_manager/get_model_list", {
|
const response = await fetchApi("/model_manager/get_model_list", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -152,7 +164,7 @@ export async function getAllModelsList() {
|
|||||||
|
|
||||||
export async function getAllFoldersList() {
|
export async function getAllFoldersList() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/model_manager/get_folder_list", {
|
const response = await fetchApi("/model_manager/get_folder_list", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -167,7 +179,7 @@ export async function getAllFoldersList() {
|
|||||||
|
|
||||||
export async function deleteLocalDiskFolder(folderPath: string) {
|
export async function deleteLocalDiskFolder(folderPath: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/delete_folder", {
|
const response = await fetchApi("/workspace/delete_folder", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -185,7 +197,7 @@ export async function deleteLocalDiskFolder(folderPath: string) {
|
|||||||
|
|
||||||
export async function cancelDownload(savePath: string) {
|
export async function cancelDownload(savePath: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/model_manager/cancel_installation", {
|
const response = await fetchApi("/model_manager/cancel_installation", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -206,7 +218,7 @@ export async function copyFlowsToNewDirectory(
|
|||||||
dstDir: string,
|
dstDir: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/copy_json_files", {
|
const response = await fetchApi("/workspace/copy_json_files", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
+67
-140
@@ -1,15 +1,4 @@
|
|||||||
import {
|
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||||
Suspense,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
DragEvent,
|
|
||||||
} from "react";
|
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
// @ts-ignore
|
|
||||||
import { api } from "/scripts/api.js";
|
|
||||||
import { Box, Portal, useToast } from "@chakra-ui/react";
|
import { Box, Portal, useToast } from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
loadDBs,
|
loadDBs,
|
||||||
@@ -36,14 +25,18 @@ const RecentFilesDrawer = React.lazy(
|
|||||||
);
|
);
|
||||||
const GalleryModal = React.lazy(() => import("./gallery/GalleryModal"));
|
const GalleryModal = React.lazy(() => import("./gallery/GalleryModal"));
|
||||||
import { IconExternalLink } from "@tabler/icons-react";
|
import { IconExternalLink } from "@tabler/icons-react";
|
||||||
import { DRAWER_Z_INDEX, UPGRADE_TO_2WAY_SYNC_KEY } from "./const";
|
import {
|
||||||
|
COMFYSPACE_TRACKING_FIELD_NAME,
|
||||||
|
DRAWER_Z_INDEX,
|
||||||
|
UPGRADE_TO_2WAY_SYNC_KEY,
|
||||||
|
} from "./const";
|
||||||
import ServerEventListener from "./model-manager/hooks/ServerEventListener";
|
import ServerEventListener from "./model-manager/hooks/ServerEventListener";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { WorkspaceRoute } from "./types/types";
|
import { WorkspaceRoute } from "./types/types";
|
||||||
import { useStateRef } from "./customHooks/useStateRef";
|
import { useStateRef } from "./customHooks/useStateRef";
|
||||||
import { indexdb } from "./db-tables/indexdb";
|
import { indexdb } from "./db-tables/indexdb";
|
||||||
import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm";
|
import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm";
|
||||||
import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck";
|
import { api, app } from "./utils/comfyapp";
|
||||||
|
import { fetchApi } from "./Api";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [curFlowName, setCurFlowName] = useState<string | null>(null);
|
const [curFlowName, setCurFlowName] = useState<string | null>(null);
|
||||||
@@ -60,7 +53,8 @@ export default function App() {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [curVersion, setCurVersion] = useStateRef<WorkflowVersion | null>(null);
|
const [curVersion, setCurVersion] = useStateRef<WorkflowVersion | null>(null);
|
||||||
const saveCurWorkflow = useCallback(async (force = false) => {
|
const saveCurWorkflow = useCallback(async (force = false) => {
|
||||||
if (curFlowID.current) {
|
const curWorkflow = workflowsTable?.curWorkflow;
|
||||||
|
if (curWorkflow) {
|
||||||
if (!force && workflowsTable?.curWorkflow?.saveLock) {
|
if (!force && workflowsTable?.curWorkflow?.saveLock) {
|
||||||
toast({
|
toast({
|
||||||
title: "The workflow is locked and cannot be saved",
|
title: "The workflow is locked and cannot be saved",
|
||||||
@@ -71,11 +65,11 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
const graphJson = JSON.stringify(app.graph.serialize());
|
const graphJson = JSON.stringify(app.graph.serialize());
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
workflowsTable?.updateFlow(curFlowID.current, {
|
workflowsTable?.updateFlow(curWorkflow.id, {
|
||||||
json: graphJson,
|
json: graphJson,
|
||||||
}),
|
}),
|
||||||
changelogsTable?.create({
|
changelogsTable?.create({
|
||||||
workflowID: curFlowID.current,
|
workflowID: curWorkflow.id,
|
||||||
json: graphJson,
|
json: graphJson,
|
||||||
isAutoSave: false,
|
isAutoSave: false,
|
||||||
}),
|
}),
|
||||||
@@ -154,73 +148,64 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
setLoadingDB(false);
|
setLoadingDB(false);
|
||||||
|
|
||||||
let latestWfID = null;
|
const urlWorkflowID = getWorkflowIdInUrlHash();
|
||||||
latestWfID = getWorkflowIdInUrlHash();
|
if (urlWorkflowID == null) {
|
||||||
if (latestWfID == null) {
|
const latestWf = localStorage.getItem("workflow");
|
||||||
latestWfID = localStorage.getItem("curFlowID");
|
if (latestWf) {
|
||||||
}
|
const workflow = JSON.parse(latestWf);
|
||||||
if (latestWfID) {
|
const id = workflow.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.id;
|
||||||
loadWorkflowIDImpl(latestWfID);
|
if (id) {
|
||||||
}
|
const flow = await workflowsTable?.get(id);
|
||||||
fetch("/workspace/deduplicate_workflow_ids");
|
flow && setCurFlowIDAndName(flow);
|
||||||
const twoway = await userSettingsTable?.getSetting("twoWaySync");
|
if (!flow?.saveLock && flow && flow.json != latestWf) {
|
||||||
!twoway &&
|
setIsDirty(true);
|
||||||
indexdb.cache.get(UPGRADE_TO_2WAY_SYNC_KEY).then(async (value) => {
|
|
||||||
if (value?.value !== "true") {
|
|
||||||
const workflowsCount = await indexdb.workflows.count();
|
|
||||||
if (workflowsCount == 0) {
|
|
||||||
// empty workflows, enable 2way sync
|
|
||||||
await userSettingsTable?.upsert({ twoWaySync: true });
|
|
||||||
await indexdb.cache.put({
|
|
||||||
id: UPGRADE_TO_2WAY_SYNC_KEY,
|
|
||||||
value: "true",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const myWorkflowsDir =
|
|
||||||
await userSettingsTable?.getSetting("myWorkflowsDir");
|
|
||||||
showDialog(
|
|
||||||
<EnableTwowaySyncConfirm
|
|
||||||
myWorkflowsDir={myWorkflowsDir ?? "undefined"}
|
|
||||||
/>,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
label: "I have downloaded all my workflows and ready to enable",
|
|
||||||
onClick: async () => {
|
|
||||||
await userSettingsTable?.upsert({ twoWaySync: true });
|
|
||||||
if (await userSettingsTable?.getSetting("twoWaySync")) {
|
|
||||||
indexdb.cache.put({
|
|
||||||
id: UPGRADE_TO_2WAY_SYNC_KEY,
|
|
||||||
value: "true",
|
|
||||||
});
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colorScheme: "red",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
closeOnOverlayClick: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
} else {
|
||||||
|
loadWorkflowIDImpl(urlWorkflowID);
|
||||||
|
}
|
||||||
|
fetchApi("/workspace/deduplicate_workflow_ids");
|
||||||
|
const twoway = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
|
|
||||||
const checkIsDirty = () => {
|
if (!twoway) {
|
||||||
if (curFlowID.current == null) return false;
|
const workflowsCount = await indexdb.workflows.count();
|
||||||
const curflow = workflowsTable?.curWorkflow;
|
if (workflowsCount == 0) {
|
||||||
if (curflow == null) return false;
|
// empty workflows, enable 2way sync
|
||||||
if (curflow.json == null) return true;
|
await userSettingsTable?.upsert({ twoWaySync: true });
|
||||||
const graphJson = app.graph.serialize() ?? {};
|
await indexdb.cache.put({
|
||||||
let lastSaved = {};
|
id: UPGRADE_TO_2WAY_SYNC_KEY,
|
||||||
try {
|
value: "true",
|
||||||
lastSaved = curflow?.json ? JSON.parse(curflow?.json) : {};
|
});
|
||||||
} catch (e) {
|
return;
|
||||||
console.error("error parsing json", e);
|
}
|
||||||
|
const myWorkflowsDir =
|
||||||
|
await userSettingsTable?.getSetting("myWorkflowsDir");
|
||||||
|
showDialog(
|
||||||
|
<EnableTwowaySyncConfirm
|
||||||
|
myWorkflowsDir={myWorkflowsDir ?? "undefined"}
|
||||||
|
/>,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: "I have downloaded all my workflows and ready to enable",
|
||||||
|
onClick: async () => {
|
||||||
|
await userSettingsTable?.upsert({ twoWaySync: true });
|
||||||
|
if (await userSettingsTable?.getSetting("twoWaySync")) {
|
||||||
|
indexdb.cache.put({
|
||||||
|
id: UPGRADE_TO_2WAY_SYNC_KEY,
|
||||||
|
value: "true",
|
||||||
|
});
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colorScheme: "red",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
closeOnOverlayClick: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const equal = deepJsonDiffCheck(graphJson, lastSaved);
|
|
||||||
return !equal;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => {
|
const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => {
|
||||||
@@ -352,7 +337,6 @@ export default function App() {
|
|||||||
json: workflow.json,
|
json: workflow.json,
|
||||||
name: newFlowName || workflow.name,
|
name: newFlowName || workflow.name,
|
||||||
parentFolderID: workflow.parentFolderID,
|
parentFolderID: workflow.parentFolderID,
|
||||||
tags: workflow.tags,
|
|
||||||
});
|
});
|
||||||
flow && (await loadWorkflowID(flow.id));
|
flow && (await loadWorkflowID(flow.id));
|
||||||
};
|
};
|
||||||
@@ -390,37 +374,6 @@ export default function App() {
|
|||||||
graphAppSetup();
|
graphAppSetup();
|
||||||
setLoadChild(true);
|
setLoadChild(true);
|
||||||
|
|
||||||
const fileInput = document.getElementById(
|
|
||||||
"comfy-file-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const fileInputListener = async () => {
|
|
||||||
if (fileInput && fileInput.files && fileInput.files.length > 0) {
|
|
||||||
const newFlow: Workflow = {
|
|
||||||
id: nanoid(),
|
|
||||||
name: fileInput.files[0].name,
|
|
||||||
json: JSON.stringify(defaultGraph),
|
|
||||||
updateTime: Date.now(),
|
|
||||||
createTime: Date.now(),
|
|
||||||
};
|
|
||||||
setCurFlowIDAndName(newFlow);
|
|
||||||
await workflowsTable?.createFlow(newFlow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fileInput?.addEventListener("change", fileInputListener);
|
|
||||||
|
|
||||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
||||||
const autoSaveEnabled = userSettingsTable?.settings?.autoSave ?? true;
|
|
||||||
if (workflowsTable?.curWorkflow?.saveLock) return;
|
|
||||||
const isDirty = checkIsDirty();
|
|
||||||
|
|
||||||
if (!autoSaveEnabled && isDirty) {
|
|
||||||
e.preventDefault(); // For modern browsers
|
|
||||||
e.returnValue = "You have unsaved changes!"; // For older browsers
|
|
||||||
showSaveOrDiscardCurWorkflowDialog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
||||||
|
|
||||||
const handleExecuted = (e: any) => {
|
const handleExecuted = (e: any) => {
|
||||||
e.detail?.output?.images?.forEach(
|
e.detail?.output?.images?.forEach(
|
||||||
(im: { filename: string; subfolder: string; type: string }) => {
|
(im: { filename: string; subfolder: string; type: string }) => {
|
||||||
@@ -439,35 +392,8 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
api.addEventListener("executed", handleExecuted);
|
api.addEventListener("executed", handleExecuted);
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent) => {
|
|
||||||
const fileName = event.dataTransfer?.files[0]?.name;
|
|
||||||
const n = app.dragOverNode;
|
|
||||||
if (n && n.onDragDrop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const overwriteFlow =
|
|
||||||
(await userSettingsTable?.getSetting(
|
|
||||||
"overwriteCurWorkflowWhenDroppingFileToCanvas",
|
|
||||||
)) ?? false;
|
|
||||||
if (!overwriteFlow && fileName) {
|
|
||||||
const newFlow: Workflow = {
|
|
||||||
id: nanoid(),
|
|
||||||
name: fileName,
|
|
||||||
json: JSON.stringify(defaultGraph),
|
|
||||||
updateTime: Date.now(),
|
|
||||||
createTime: Date.now(),
|
|
||||||
};
|
|
||||||
setCurFlowIDAndName(newFlow);
|
|
||||||
await workflowsTable?.createFlow(newFlow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
app.canvasEl.addEventListener("drop", handleDrop);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("change", fileInputListener);
|
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
||||||
window.removeEventListener("executed", handleExecuted);
|
window.removeEventListener("executed", handleExecuted);
|
||||||
app.canvasEl.removeEventListener("drop", handleDrop);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -491,6 +417,7 @@ export default function App() {
|
|||||||
route: route,
|
route: route,
|
||||||
curVersion: curVersion,
|
curVersion: curVersion,
|
||||||
setCurVersion: setCurVersion,
|
setCurVersion: setCurVersion,
|
||||||
|
setCurFlowIDAndName: setCurFlowIDAndName,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={workspaceContainerRef} className="workspace_manager">
|
<div ref={workspaceContainerRef} className="workspace_manager">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Workflow } from "../types/dbTypes";
|
|||||||
import { mediaTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
import { mediaTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||||
import { WorkspaceContext } from "../WorkspaceContext";
|
import { WorkspaceContext } from "../WorkspaceContext";
|
||||||
import { openWorkflowInNewTab } from "../utils";
|
import { openWorkflowInNewTab } from "../utils";
|
||||||
|
import { fetchApi } from "../Api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
@@ -55,7 +56,7 @@ export default function MoreActionMenu({ workflow }: Props) {
|
|||||||
body.append(`images[${index}]`, file, file.name);
|
body.append(`images[${index}]`, file, file.name);
|
||||||
});
|
});
|
||||||
body.append("subfolder", "workspace_manager");
|
body.append("subfolder", "workspace_manager");
|
||||||
await fetch("/workspace/images/save", {
|
await fetchApi("/workspace/images/save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu";
|
|||||||
import { insertWorkflowToCanvas, sortFileItem } from "../utils";
|
import { insertWorkflowToCanvas, sortFileItem } from "../utils";
|
||||||
import MultipleSelectionOperation from "./MultipleSelectionOperation";
|
import MultipleSelectionOperation from "./MultipleSelectionOperation";
|
||||||
import { ESortTypes, sortTypeLocalStorageKey } from "./types";
|
import { ESortTypes, sortTypeLocalStorageKey } from "./types";
|
||||||
// @ts-expect-error ComfyUI import
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import { useDebounce } from "../customHooks/useDebounce";
|
import { useDebounce } from "../customHooks/useDebounce";
|
||||||
import SearchInput from "../components/SearchInput";
|
import SearchInput from "../components/SearchInput";
|
||||||
import { openWorkflowsFolder } from "../Api";
|
import { openWorkflowsFolder } from "../Api";
|
||||||
@@ -40,6 +38,7 @@ import MyTagsRow from "./MyTagsRow";
|
|||||||
import ImportFlowsFileInput from "./ImportFlowsFileInput";
|
import ImportFlowsFileInput from "./ImportFlowsFileInput";
|
||||||
import ItemsList from "./ItemsList";
|
import ItemsList from "./ItemsList";
|
||||||
import { DRAWER_Z_INDEX } from "../const";
|
import { DRAWER_Z_INDEX } from "../const";
|
||||||
|
import { app } from "../utils/comfyapp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -258,22 +257,18 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
|||||||
/>
|
/>
|
||||||
<HStack mb={2} mt={2} p={0} justifyContent="space-between">
|
<HStack mb={2} mt={2} p={0} justifyContent="space-between">
|
||||||
<HStack>
|
<HStack>
|
||||||
{allFlowsRef.current.length > 0 ? (
|
<MultipleSelectionOperation
|
||||||
<MultipleSelectionOperation
|
multipleState={multipleState}
|
||||||
multipleState={multipleState}
|
changeMultipleState={(state) => {
|
||||||
changeMultipleState={(state) => {
|
setMultipleState(state);
|
||||||
setMultipleState(state);
|
!state && setSelectedKeys([]);
|
||||||
!state && setSelectedKeys([]);
|
}}
|
||||||
}}
|
selectedKeys={selectedKeys}
|
||||||
selectedKeys={selectedKeys}
|
isSelectedAll={
|
||||||
isSelectedAll={
|
selectedKeys.length === allFlowsRef.current.length
|
||||||
selectedKeys.length === allFlowsRef.current.length
|
}
|
||||||
}
|
batchOperationCallback={batchOperationCallback}
|
||||||
batchOperationCallback={batchOperationCallback}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Box />
|
|
||||||
)}
|
|
||||||
{!multipleState && (
|
{!multipleState && (
|
||||||
<Tooltip hasArrow label="New folder" placement="bottom-start">
|
<Tooltip hasArrow label="New folder" placement="bottom-start">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
|
|||||||
w={"100%"}
|
w={"100%"}
|
||||||
justify={"space-between"}
|
justify={"space-between"}
|
||||||
borderBottom={"1px solid"}
|
borderBottom={"1px solid"}
|
||||||
borderColor={colorMode == "dark" ? "gray.900" : "gray.200"}
|
borderColor={colorMode == "dark" ? "gray.700" : "gray.200"}
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
>
|
>
|
||||||
@@ -111,7 +111,7 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
|
|||||||
{isHovering ? (
|
{isHovering ? (
|
||||||
<WorkflowListItemActionButtons workflow={workflow} />
|
<WorkflowListItemActionButtons workflow={workflow} />
|
||||||
) : (
|
) : (
|
||||||
<Text color={"GrayText"} ml={"2px"} fontSize={"sm"}>
|
<Text color={"GrayText"} fontSize={"sm"}>
|
||||||
{formatTimestamp(workflow.updateTime, false, false, "/")}
|
{formatTimestamp(workflow.updateTime, false, false, "/")}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import CommonCheckboxSettings from "../settings/CommonCheckboxSettings";
|
|||||||
import SelectMyWorkflowsDir from "../settings/SelectMyWorkflowsDir";
|
import SelectMyWorkflowsDir from "../settings/SelectMyWorkflowsDir";
|
||||||
import { CommonNumberSetting } from "../settings/CommonNumberSetting";
|
import { CommonNumberSetting } from "../settings/CommonNumberSetting";
|
||||||
import AutosaveSetting from "../settings/AutosaveSetting";
|
import AutosaveSetting from "../settings/AutosaveSetting";
|
||||||
|
import CloudHostSetting from "../settings/CloudHostSetting";
|
||||||
|
|
||||||
export default function WorkspaceSettingsModal({
|
export default function WorkspaceSettingsModal({
|
||||||
onClose,
|
onClose,
|
||||||
@@ -56,6 +57,7 @@ export default function WorkspaceSettingsModal({
|
|||||||
settingKey="hideCoverImage"
|
settingKey="hideCoverImage"
|
||||||
text="Hide thumbnail image in workflow list"
|
text="Hide thumbnail image in workflow list"
|
||||||
/>
|
/>
|
||||||
|
<CloudHostSetting />
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const WorkspaceContext = createContext<{
|
|||||||
route: WorkspaceRoute;
|
route: WorkspaceRoute;
|
||||||
curVersion: WorkflowVersion | null;
|
curVersion: WorkflowVersion | null;
|
||||||
setCurVersion: (version: WorkflowVersion | null) => void;
|
setCurVersion: (version: WorkflowVersion | null) => void;
|
||||||
|
setCurFlowIDAndName: (workflow: Workflow | null) => void;
|
||||||
}>({
|
}>({
|
||||||
curFlowID: null,
|
curFlowID: null,
|
||||||
loadWorkflowID: () => {},
|
loadWorkflowID: () => {},
|
||||||
@@ -37,6 +38,7 @@ export const WorkspaceContext = createContext<{
|
|||||||
curVersion: null,
|
curVersion: null,
|
||||||
setIsDirty: () => {},
|
setIsDirty: () => {},
|
||||||
setCurVersion: () => {},
|
setCurVersion: () => {},
|
||||||
|
setCurFlowIDAndName: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RecentFilesContext = createContext<{
|
export const RecentFilesContext = createContext<{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fetchApi } from "../Api";
|
||||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
||||||
import { indexdb } from "../db-tables/indexdb";
|
import { indexdb } from "../db-tables/indexdb";
|
||||||
import { Workflow } from "../types/dbTypes";
|
import { Workflow } from "../types/dbTypes";
|
||||||
@@ -9,17 +10,17 @@ import {
|
|||||||
|
|
||||||
export namespace TwowaySyncAPI {
|
export namespace TwowaySyncAPI {
|
||||||
export async function moveWorkflow(
|
export async function moveWorkflow(
|
||||||
workflow: Workflow,
|
curRelPath: string,
|
||||||
newParentFolderRelPath: string,
|
newParentFolderRelPath: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/file/move", {
|
const response = await fetchApi("/workspace/file/move", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
path: getWorkflowRelPath(workflow),
|
path: curRelPath,
|
||||||
newParentPath: newParentFolderRelPath,
|
newParentPath: newParentFolderRelPath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -39,7 +40,7 @@ export namespace TwowaySyncAPI {
|
|||||||
parentFolderID: string | null,
|
parentFolderID: string | null,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/file/gen_unique_name", {
|
const response = await fetchApi("/workspace/file/gen_unique_name", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -61,17 +62,17 @@ export namespace TwowaySyncAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function renameWorkflow(
|
export async function renameWorkflow(
|
||||||
{ parentFolderID, name }: Pick<Workflow, "name" | "parentFolderID">,
|
{ relPath, id }: { relPath: string; id: string },
|
||||||
newName: string,
|
newName: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/file/rename", {
|
const response = await fetchApi("/workspace/file/rename", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
path: joinRelPath(parentFolderID ?? "", name + ".json"),
|
path: relPath,
|
||||||
newName: newName,
|
newName: newName,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -102,7 +103,12 @@ export namespace TwowaySyncAPI {
|
|||||||
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {};
|
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {};
|
||||||
}
|
}
|
||||||
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME].id = workflow.id;
|
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME].id = workflow.id;
|
||||||
const response = await fetch("/workspace/file/save", {
|
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME].saveLock = workflow.saveLock;
|
||||||
|
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME].cloudID = workflow.cloudID;
|
||||||
|
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME].coverMediaPath =
|
||||||
|
workflow.coverMediaPath;
|
||||||
|
|
||||||
|
const response = await fetchApi("/workspace/file/save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -125,7 +131,7 @@ export namespace TwowaySyncAPI {
|
|||||||
}
|
}
|
||||||
export async function deleteWorkflow(workflow: Workflow) {
|
export async function deleteWorkflow(workflow: Workflow) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/delete_file", {
|
const response = await fetchApi("/workspace/delete_file", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -142,12 +148,8 @@ export namespace TwowaySyncAPI {
|
|||||||
console.error("Error deleting file:", error);
|
console.error("Error deleting file:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function createWorkflow({
|
export async function createWorkflow(workflow: Workflow) {
|
||||||
parentFolderID,
|
const { parentFolderID, name, json, id } = workflow;
|
||||||
name,
|
|
||||||
json,
|
|
||||||
id,
|
|
||||||
}: Workflow) {
|
|
||||||
let jsonObj: any = JSON.parse(json);
|
let jsonObj: any = JSON.parse(json);
|
||||||
if (jsonObj.extra == null) {
|
if (jsonObj.extra == null) {
|
||||||
jsonObj.extra = {};
|
jsonObj.extra = {};
|
||||||
@@ -156,6 +158,10 @@ export namespace TwowaySyncAPI {
|
|||||||
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {};
|
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {};
|
||||||
}
|
}
|
||||||
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME].id = id; // to avoid overwriting deps
|
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME].id = id; // to avoid overwriting deps
|
||||||
|
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME].saveLock = workflow.saveLock;
|
||||||
|
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME].cloudID = workflow.cloudID;
|
||||||
|
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME].coverMediaPath =
|
||||||
|
workflow.coverMediaPath;
|
||||||
const input: { parentFolderPath: string; name: string; json: string } = {
|
const input: { parentFolderPath: string; name: string; json: string } = {
|
||||||
parentFolderPath: sanitizeRelPath(parentFolderID ?? ""),
|
parentFolderPath: sanitizeRelPath(parentFolderID ?? ""),
|
||||||
name: name,
|
name: name,
|
||||||
@@ -163,7 +169,7 @@ export namespace TwowaySyncAPI {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/create_workflow_file", {
|
const response = await fetchApi("/workspace/create_workflow_file", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -187,20 +193,28 @@ export namespace TwowaySyncAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function getFile({
|
export async function getFile({
|
||||||
parentFolderID,
|
relPath,
|
||||||
name,
|
|
||||||
id,
|
id,
|
||||||
}: {
|
}: {
|
||||||
parentFolderID: string | null;
|
relPath: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
json?: Object;
|
json?: {
|
||||||
|
extra: {
|
||||||
|
[COMFYSPACE_TRACKING_FIELD_NAME]: {
|
||||||
|
id: string;
|
||||||
|
saveLock: boolean;
|
||||||
|
cloudID: string | null;
|
||||||
|
coverMediaPath: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createTime?: number;
|
||||||
|
updateTime?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
const relPath = joinRelPath(parentFolderID ?? "", name + ".json");
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/get_workflow_file", {
|
const response = await fetchApi("/workspace/get_workflow_file", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -235,6 +249,9 @@ export type ScanLocalFile = {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
json: string;
|
json: string;
|
||||||
|
saveLock: boolean;
|
||||||
|
cloudID: string | null;
|
||||||
|
coverMediaPath: string | null;
|
||||||
createTime: number;
|
createTime: number;
|
||||||
updateTime: number;
|
updateTime: number;
|
||||||
lastOpenedTime?: number;
|
lastOpenedTime?: number;
|
||||||
@@ -251,17 +268,20 @@ export async function scanLocalFiles(
|
|||||||
metaInfoOnly = false,
|
metaInfoOnly = false,
|
||||||
): Promise<Array<ScanLocalFile | ScanLocalFolder>> {
|
): Promise<Array<ScanLocalFile | ScanLocalFolder>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/file/scan_my_workflows_folder", {
|
const response = await fetchApi(
|
||||||
method: "POST",
|
"/workspace/file/scan_my_workflows_folder",
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
path: sanitizeRelPath(path),
|
||||||
|
recursive,
|
||||||
|
metaInfoOnly,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
);
|
||||||
path: sanitizeRelPath(path),
|
|
||||||
recursive,
|
|
||||||
metaInfoOnly,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fetchApi } from "../Api";
|
||||||
import { Folder } from "../types/dbTypes";
|
import { Folder } from "../types/dbTypes";
|
||||||
import { sanitizeRelPath } from "../utils/OsPathUtils";
|
import { sanitizeRelPath } from "../utils/OsPathUtils";
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export namespace TwowayFolderSyncAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createFolder(folder: Folder) {
|
export async function createFolder(folder: Folder) {
|
||||||
await fetch("/workspace/folder/create", {
|
await fetchApi("/workspace/folder/create", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -27,7 +28,7 @@ export namespace TwowayFolderSyncAPI {
|
|||||||
folderToBeMoved: string,
|
folderToBeMoved: string,
|
||||||
newParentPath: string,
|
newParentPath: string,
|
||||||
) {
|
) {
|
||||||
const resp = await fetch("/workspace/folder/move", {
|
const resp = await fetchApi("/workspace/folder/move", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -45,7 +46,7 @@ export namespace TwowayFolderSyncAPI {
|
|||||||
}
|
}
|
||||||
export async function deleteFolder(relPath: string) {
|
export async function deleteFolder(relPath: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/folder/delete", {
|
const response = await fetchApi("/workspace/folder/delete", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -65,7 +66,7 @@ export namespace TwowayFolderSyncAPI {
|
|||||||
folderRelPath: string,
|
folderRelPath: string,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/file/count_files", {
|
const response = await fetchApi("/workspace/file/count_files", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -90,7 +91,7 @@ export namespace TwowayFolderSyncAPI {
|
|||||||
newName: string,
|
newName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/workspace/folder/rename", {
|
const response = await fetchApi("/workspace/folder/rename", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { workflowVersionsTable } from "../db-tables/WorkspaceDB";
|
import { workflowVersionsTable } from "../db-tables/WorkspaceDB";
|
||||||
import { useState, ChangeEvent } from "react";
|
import { useState, ChangeEvent } from "react";
|
||||||
// @ts-ignore
|
import { app } from "../utils/comfyapp";
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
|||||||
@@ -43,11 +43,10 @@ import { Workflow } from "../types/dbTypes";
|
|||||||
import CreateVersionDialog from "./CreateVersionDialog";
|
import CreateVersionDialog from "./CreateVersionDialog";
|
||||||
import HoverMenu from "./HoverMenu";
|
import HoverMenu from "./HoverMenu";
|
||||||
const ShareDialog = lazy(() => import("../share/ShareDialog"));
|
const ShareDialog = lazy(() => import("../share/ShareDialog"));
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
||||||
import DownloadSpaceJsonDialog from "../spacejson/DownloadSpaceJsonDialog";
|
|
||||||
import { downloadJsonFile } from "../utils/downloadJsonFile";
|
import { downloadJsonFile } from "../utils/downloadJsonFile";
|
||||||
|
import { SharedTopbarButton } from "../share/SharedTopbarButton";
|
||||||
|
import { app } from "../utils/comfyapp";
|
||||||
|
|
||||||
export default function DropdownTitle() {
|
export default function DropdownTitle() {
|
||||||
const {
|
const {
|
||||||
@@ -186,21 +185,15 @@ export default function DropdownTitle() {
|
|||||||
>
|
>
|
||||||
Versions History
|
Versions History
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/* <MenuItem
|
|
||||||
onClick={() => setRoute("downloadSpaceJson")}
|
|
||||||
icon={<IconDownload size={20} />}
|
|
||||||
iconSpacing={1}
|
|
||||||
>
|
|
||||||
Save runnable workflow🧪
|
|
||||||
</MenuItem> */}
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setRoute("downloadSpaceJson")}
|
onClick={() => setRoute("share")}
|
||||||
icon={<IconShare2 size={20} />}
|
icon={<IconShare2 size={20} />}
|
||||||
iconSpacing={1}
|
iconSpacing={1}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
>
|
>
|
||||||
<HStack>
|
<HStack>
|
||||||
<p>Share</p> <Tag size={"sm"}>🧪beta</Tag>
|
<p>Share</p> <Tag size={"sm"}>🧪beta</Tag>
|
||||||
|
<SharedTopbarButton />
|
||||||
</HStack>
|
</HStack>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
@@ -211,7 +204,6 @@ export default function DropdownTitle() {
|
|||||||
{route == "versionHistory" && (
|
{route == "versionHistory" && (
|
||||||
<VersionHistoryDrawer onClose={() => setRoute("root")} />
|
<VersionHistoryDrawer onClose={() => setRoute("root")} />
|
||||||
)}
|
)}
|
||||||
{route == "downloadSpaceJson" && <DownloadSpaceJsonDialog />}
|
|
||||||
{route === "saveAsModal" && (
|
{route === "saveAsModal" && (
|
||||||
<Modal isOpen={true} onClose={handleOnCloseModal}>
|
<Modal isOpen={true} onClose={handleOnCloseModal}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function EditFlowName({
|
|||||||
as="div"
|
as="div"
|
||||||
color="white"
|
color="white"
|
||||||
onClick={startEdit}
|
onClick={startEdit}
|
||||||
maxW={200}
|
maxW={160}
|
||||||
fontWeight={500}
|
fontWeight={500}
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ import {
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { WorkspaceContext } from "../WorkspaceContext";
|
import { WorkspaceContext } from "../WorkspaceContext";
|
||||||
import { formatTimestamp } from "../utils";
|
import { formatTimestamp } from "../utils";
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import { Changelog, WorkflowVersion } from "../types/dbTypes";
|
import { Changelog, WorkflowVersion } from "../types/dbTypes";
|
||||||
import DeleteConfirm from "./DeleteConfirm";
|
import DeleteConfirm from "./DeleteConfirm";
|
||||||
|
import { app } from "../utils/comfyapp";
|
||||||
|
|
||||||
export function VersionHistoryDrawer({ onClose }: { onClose: () => void }) {
|
export function VersionHistoryDrawer({ onClose }: { onClose: () => void }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|||||||
@@ -44,9 +44,8 @@ export class ChangelogsTable extends TableBase<Changelog> {
|
|||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
isAutoSave: input.isAutoSave,
|
isAutoSave: input.isAutoSave,
|
||||||
};
|
};
|
||||||
await this.deleteLogsExceedLimit(input.workflowID);
|
|
||||||
await indexdb.changelogs.add(change);
|
await indexdb.changelogs.add(change);
|
||||||
await this.saveDiskDB();
|
await this.deleteLogsExceedLimit(input.workflowID);
|
||||||
return change;
|
return change;
|
||||||
}
|
}
|
||||||
async deleteLogsExceedLimit(workflowID: string) {
|
async deleteLogsExceedLimit(workflowID: string) {
|
||||||
|
|||||||
@@ -1,59 +1,6 @@
|
|||||||
import { deleteFile, updateFile } from "../Api";
|
|
||||||
import { foldersTable, workflowsTable, userSettingsTable } from "./WorkspaceDB";
|
import { foldersTable, workflowsTable, userSettingsTable } from "./WorkspaceDB";
|
||||||
import {
|
|
||||||
COMFYSPACE_TRACKING_FIELD_NAME,
|
|
||||||
LEGACY_COMFYSPACE_TRACKING_FIELD_NAME,
|
|
||||||
} from "../const";
|
|
||||||
import { toFileNameFriendly } from "../utils";
|
|
||||||
import { Folder, Workflow } from "../types/dbTypes";
|
|
||||||
import { TwowayFolderSyncAPI } from "../apis/TwowaySyncFolderApi";
|
import { TwowayFolderSyncAPI } from "../apis/TwowaySyncFolderApi";
|
||||||
|
|
||||||
export async function saveJsonFileMyWorkflows(workflow: Workflow) {
|
|
||||||
const file_path = await generateFilePath(workflow);
|
|
||||||
if (file_path == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fullPath = await generateFilePathAbsolute(workflow);
|
|
||||||
await workflowsTable?.updateMetaInfo(workflow.id, {
|
|
||||||
filePath: fullPath ?? undefined,
|
|
||||||
});
|
|
||||||
const json = workflow.json;
|
|
||||||
const flow = JSON.parse(json);
|
|
||||||
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {
|
|
||||||
id: workflow.id,
|
|
||||||
name: workflow.name,
|
|
||||||
};
|
|
||||||
delete flow.extra[LEGACY_COMFYSPACE_TRACKING_FIELD_NAME];
|
|
||||||
|
|
||||||
await updateFile(file_path, JSON.stringify(flow));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteJsonFileMyWorkflows(workflow: Workflow) {
|
|
||||||
if (workflow.name == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file_path = await generateFilePath(workflow);
|
|
||||||
file_path != null && deleteFile(file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateFilePath(
|
|
||||||
workflow: Workflow,
|
|
||||||
): Promise<string | null> {
|
|
||||||
let filePath = toFileNameFriendly(workflow.name) + ".json";
|
|
||||||
let curFolderID = workflow.parentFolderID;
|
|
||||||
while (curFolderID != null) {
|
|
||||||
const folder = await foldersTable?.get(curFolderID);
|
|
||||||
if (folder == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const folderName = folder.name;
|
|
||||||
filePath = `${folderName}/${filePath}`;
|
|
||||||
curFolderID = folder.parentFolderID ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePath ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function genFolderRelPath(
|
export async function genFolderRelPath(
|
||||||
folderId: string | null,
|
folderId: string | null,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -71,46 +18,6 @@ export async function genFolderRelPath(
|
|||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
export async function generateFilePathAbsolute(
|
|
||||||
workflow: Workflow,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const subPath = await generateFilePath(workflow);
|
|
||||||
let myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir");
|
|
||||||
if (!myWorkflowsDir) {
|
|
||||||
console.error("myWorkflowsDir is not set");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!myWorkflowsDir.endsWith("/")) {
|
|
||||||
myWorkflowsDir = myWorkflowsDir + "/";
|
|
||||||
}
|
|
||||||
return myWorkflowsDir + subPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateFolderPath(id: string): Promise<string | null> {
|
|
||||||
const folder = await foldersTable?.get(id);
|
|
||||||
let parentFolderID = folder?.parentFolderID;
|
|
||||||
let folderPath = folder?.name;
|
|
||||||
while (parentFolderID) {
|
|
||||||
const folder = await foldersTable?.get(parentFolderID);
|
|
||||||
if (folder == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
folderPath = `${folder.name}/${folderPath}`;
|
|
||||||
parentFolderID = folder.parentFolderID ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir");
|
|
||||||
|
|
||||||
if (!myWorkflowsDir) {
|
|
||||||
console.error("myWorkflowsDir is not set");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!myWorkflowsDir.endsWith("/")) {
|
|
||||||
myWorkflowsDir = myWorkflowsDir + "/";
|
|
||||||
}
|
|
||||||
return myWorkflowsDir + folderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFileCountInFolder(folderId: string): Promise<number> {
|
export async function getFileCountInFolder(folderId: string): Promise<number> {
|
||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { userSettingsTable, workflowsTable } from "./WorkspaceDB";
|
import { userSettingsTable, workflowsTable } from "./WorkspaceDB";
|
||||||
import { EFlowOperationType, Folder } from "../types/dbTypes";
|
import { EFlowOperationType, Folder } from "../types/dbTypes";
|
||||||
import { validateOrSaveAllJsonFileMyWorkflows } from "../utils";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { TableBase } from "./TableBase";
|
import { TableBase } from "./TableBase";
|
||||||
import { indexdb } from "./indexdb";
|
import { indexdb } from "./indexdb";
|
||||||
@@ -84,11 +83,6 @@ export class FoldersTable extends TableBase<Folder> {
|
|||||||
}
|
}
|
||||||
await indexdb.folders.update(id, input);
|
await indexdb.folders.update(id, input);
|
||||||
this.saveDiskDB();
|
this.saveDiskDB();
|
||||||
|
|
||||||
// folder moved or renamed - move all workflows to the right directory(not required when folded state changes)
|
|
||||||
if (nameChanged || parentFolderChanged) {
|
|
||||||
validateOrSaveAllJsonFileMyWorkflows(true);
|
|
||||||
}
|
|
||||||
return newRecord;
|
return newRecord;
|
||||||
}
|
}
|
||||||
public async deleteFolder(
|
public async deleteFolder(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Table, workflowsTable } from "./WorkspaceDB";
|
|||||||
import { TableBase } from "./TableBase";
|
import { TableBase } from "./TableBase";
|
||||||
import { Media } from "../types/dbTypes";
|
import { Media } from "../types/dbTypes";
|
||||||
import { indexdb } from "./indexdb";
|
import { indexdb } from "./indexdb";
|
||||||
import { getMetadataFromUrl } from "../gallery/utils.ts";
|
|
||||||
|
|
||||||
export class MediaTable extends TableBase<Media> {
|
export class MediaTable extends TableBase<Media> {
|
||||||
static readonly TABLE_NAME: Table = "media";
|
static readonly TABLE_NAME: Table = "media";
|
||||||
@@ -25,20 +24,11 @@ export class MediaTable extends TableBase<Media> {
|
|||||||
const format = input.localPath.split(".").pop();
|
const format = input.localPath.split(".").pop();
|
||||||
if (format == null) return null;
|
if (format == null) return null;
|
||||||
|
|
||||||
const res = await getMetadataFromUrl(
|
|
||||||
`/workspace/view_media?filename=${input.localPath}`,
|
|
||||||
).catch((e) => {
|
|
||||||
console.error("get meta error:", e);
|
|
||||||
return { prompt: "" };
|
|
||||||
});
|
|
||||||
|
|
||||||
const md: Media = {
|
const md: Media = {
|
||||||
id: input.localPath,
|
id: input.localPath,
|
||||||
localPath: input.localPath,
|
localPath: input.localPath,
|
||||||
workflowID: input.workflowID,
|
workflowID: input.workflowID,
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
workflowJSON: JSON.stringify(res?.prompt),
|
|
||||||
format: format,
|
|
||||||
};
|
};
|
||||||
await workflowsTable?.updateMetaInfo(input.workflowID, {
|
await workflowsTable?.updateMetaInfo(input.workflowID, {
|
||||||
latestImage: md.localPath,
|
latestImage: md.localPath,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class UserSettingsTable extends TableBase<UserSettings> {
|
|||||||
autoSaveDuration: 3,
|
autoSaveDuration: 3,
|
||||||
twoWaySync: false,
|
twoWaySync: false,
|
||||||
foldersOnTop: false,
|
foldersOnTop: false,
|
||||||
cloudHost: "https://www.nodecafe.org",
|
cloudHost: "https://www.nodecafe.co",
|
||||||
overwriteCurWorkflowWhenDroppingFileToCanvas: false,
|
overwriteCurWorkflowWhenDroppingFileToCanvas: false,
|
||||||
maximumChangelogNumber: 80,
|
maximumChangelogNumber: 80,
|
||||||
hideCoverImage: false,
|
hideCoverImage: false,
|
||||||
@@ -93,10 +93,12 @@ export class UserSettingsTable extends TableBase<UserSettings> {
|
|||||||
...res,
|
...res,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
// overwrite legacy comfyspace.art
|
if (instance.defaultSettings.cloudHost.includes("comfyspace.art")) {
|
||||||
await instance.upsert({
|
// overwrite legacy comfyspace.art
|
||||||
cloudHost: instance.defaultSettings.cloudHost,
|
await instance.upsert({
|
||||||
});
|
cloudHost: instance.defaultSettings.cloudHost,
|
||||||
|
});
|
||||||
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ import { sortFileItem } from "../utils";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { TableBase } from "./TableBase";
|
import { TableBase } from "./TableBase";
|
||||||
import { indexdb } from "./indexdb";
|
import { indexdb } from "./indexdb";
|
||||||
import {
|
|
||||||
deleteJsonFileMyWorkflows,
|
|
||||||
saveJsonFileMyWorkflows,
|
|
||||||
} from "./DiskFileUtils";
|
|
||||||
import { ESortTypes, ImportWorkflow } from "../RecentFilesDrawer/types";
|
import { ESortTypes, ImportWorkflow } from "../RecentFilesDrawer/types";
|
||||||
import { defaultGraph } from "../defaultGraph";
|
import { defaultGraph } from "../defaultGraph";
|
||||||
import { scanMyWorkflowsDir } from "../utils/twowaySyncUtils";
|
import { scanMyWorkflowsDir } from "../utils/twowaySyncUtils";
|
||||||
@@ -25,6 +21,7 @@ import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
|||||||
export class WorkflowsTable extends TableBase<Workflow> {
|
export class WorkflowsTable extends TableBase<Workflow> {
|
||||||
static readonly TABLE_NAME = "workflows";
|
static readonly TABLE_NAME = "workflows";
|
||||||
private _curWorkflow: Workflow | null = null;
|
private _curWorkflow: Workflow | null = null;
|
||||||
|
workflowIDPathMap: Record<string, string> = {};
|
||||||
public get curWorkflow() {
|
public get curWorkflow() {
|
||||||
return this._curWorkflow;
|
return this._curWorkflow;
|
||||||
}
|
}
|
||||||
@@ -51,11 +48,7 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the currently opened workflow is the latest version and is consistent with the DB.
|
* Check whether the currently opened workflow is the latest version and is consistent with the DB
|
||||||
* Through updateTime comparison, if it is inconsistent, it means that a newer version already exists in the DB.
|
|
||||||
* If the automatic save function is turned on at this time, the latest version in the DB will be overwritten.
|
|
||||||
* Therefore, the user needs to confirm whether to continue to enable automatic saving.
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
*/
|
||||||
public async latestVersionCheck() {
|
public async latestVersionCheck() {
|
||||||
if (this._curWorkflow) {
|
if (this._curWorkflow) {
|
||||||
@@ -68,20 +61,8 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listByIndex(
|
public async listByIndex(): Promise<Workflow[]> {
|
||||||
index: keyof Workflow,
|
throw new Error("Method not allowed.");
|
||||||
value: string,
|
|
||||||
): Promise<Workflow[]> {
|
|
||||||
const list = await super.listByIndex(index, value);
|
|
||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
|
||||||
if (!twoWaySyncEnabled) {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
// for two way sync we need to varify if it really exists in disk
|
|
||||||
const results = await Promise.all(
|
|
||||||
list.map(async (f) => await this.get(f.id)),
|
|
||||||
);
|
|
||||||
return results.filter((f) => f != null) as Workflow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateUniqueName(name?: string, parentFolderID?: string) {
|
async generateUniqueName(name?: string, parentFolderID?: string) {
|
||||||
@@ -139,34 +120,20 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
};
|
};
|
||||||
//add to IndexDB
|
//add to IndexDB
|
||||||
await indexdb.workflows.add(newWorkflow);
|
await indexdb.workflows.add(newWorkflow);
|
||||||
// add to disk file db
|
|
||||||
this.saveDiskDB();
|
|
||||||
// add to my_workflows/
|
// add to my_workflows/
|
||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
if (twoWaySyncEnabled) {
|
if (twoWaySyncEnabled) {
|
||||||
await TwowaySyncAPI.createWorkflow(newWorkflow);
|
await TwowaySyncAPI.createWorkflow(newWorkflow);
|
||||||
return await this.get(newWorkflow.id);
|
return await this.get(newWorkflow.id);
|
||||||
} else {
|
|
||||||
saveJsonFileMyWorkflows(newWorkflow);
|
|
||||||
}
|
}
|
||||||
return newWorkflow;
|
return newWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
// disallow TableBase.update()
|
// disallow TableBase.update()
|
||||||
async update(
|
async update(): Promise<Workflow | null> {
|
||||||
_id: string,
|
|
||||||
_change: Partial<Workflow>,
|
|
||||||
): Promise<Workflow | null> {
|
|
||||||
throw new Error("Method not allowed.");
|
throw new Error("Method not allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateTopFields(
|
|
||||||
id: string,
|
|
||||||
change: Pick<Partial<Workflow>, "topFieldsConfig">,
|
|
||||||
): Promise<Workflow | null> {
|
|
||||||
return this._update(id, change);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateMetaInfo(
|
public async updateMetaInfo(
|
||||||
id: string,
|
id: string,
|
||||||
change: Omit<Partial<Workflow>, "id" | "name" | "parentFolderID" | "json">,
|
change: Omit<Partial<Workflow>, "id" | "name" | "parentFolderID" | "json">,
|
||||||
@@ -177,35 +144,33 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
id: string,
|
id: string,
|
||||||
change: Partial<Workflow>,
|
change: Partial<Workflow>,
|
||||||
): Promise<Workflow | null> {
|
): Promise<Workflow | null> {
|
||||||
//update indexdb
|
const cur = await this.get(id);
|
||||||
await indexdb.workflows.update(id, change);
|
if (!cur) {
|
||||||
const newWorkflow = (await indexdb.workflows.get(id)) ?? null;
|
return null;
|
||||||
|
}
|
||||||
|
const newWorkflow = {
|
||||||
|
...cur,
|
||||||
|
...change,
|
||||||
|
};
|
||||||
|
await TwowaySyncAPI.saveWorkflow(newWorkflow);
|
||||||
|
|
||||||
//update curWorkflow RAM
|
//update curWorkflow RAM
|
||||||
if (this._curWorkflow && this._curWorkflow.id === id) {
|
if (this._curWorkflow && this._curWorkflow.id === id) {
|
||||||
this._curWorkflow = newWorkflow;
|
this._curWorkflow = newWorkflow;
|
||||||
}
|
}
|
||||||
this.saveDiskDB();
|
|
||||||
return newWorkflow;
|
return newWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFolder(id: string, change: Pick<Workflow, "parentFolderID">) {
|
async updateFolder(id: string, change: Pick<Workflow, "parentFolderID">) {
|
||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
const oldWorkflow = await this.get(id);
|
|
||||||
const newWorkflow = await this.updateMetaInfo(id, change as any);
|
|
||||||
if (!newWorkflow || !oldWorkflow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (twoWaySyncEnabled) {
|
if (twoWaySyncEnabled) {
|
||||||
await TwowaySyncAPI.moveWorkflow(oldWorkflow, change.parentFolderID!);
|
const relPath = this.workflowIDPathMap[id];
|
||||||
} else {
|
if (!relPath) {
|
||||||
await saveJsonFileMyWorkflows(newWorkflow);
|
alert("Error: workflow not found. Cannot move workflow.");
|
||||||
await deleteJsonFileMyWorkflows(oldWorkflow);
|
return;
|
||||||
// update parent folder updateTime
|
|
||||||
if (newWorkflow?.parentFolderID != null) {
|
|
||||||
await indexdb.folders?.update(newWorkflow.parentFolderID, {
|
|
||||||
updateTime: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
await TwowaySyncAPI.moveWorkflow(relPath, change.parentFolderID!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async genLastManualSavedJson(id: string) {
|
public async genLastManualSavedJson(id: string) {
|
||||||
@@ -213,25 +178,31 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
return changelog?.json;
|
return changelog?.json;
|
||||||
}
|
}
|
||||||
public async updateName(id: string, change: Pick<Workflow, "name">) {
|
public async updateName(id: string, change: Pick<Workflow, "name">) {
|
||||||
const before = await this.get(id);
|
const path = this.workflowIDPathMap[id];
|
||||||
|
if (!path) {
|
||||||
|
alert("Error: workflow not found. Cannot rename workflow.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
if (twoWaySyncEnabled) {
|
if (twoWaySyncEnabled) {
|
||||||
before &&
|
const success = await TwowaySyncAPI.renameWorkflow(
|
||||||
(await TwowaySyncAPI.renameWorkflow(before, change.name + ".json"));
|
{
|
||||||
|
relPath: path,
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
change.name + ".json",
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
this.workflowIDPathMap[id] =
|
||||||
|
`${path.split("/").slice(0, -1).join("/")}/${change.name}.json`
|
||||||
|
.split("/")
|
||||||
|
.filter((part) => part !== "")
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return await this.updateMetaInfo(id, change as any);
|
return await this.updateMetaInfo(id, change as any);
|
||||||
}
|
}
|
||||||
public async updateFlow(id: string, input: Pick<Workflow, "json">) {
|
public async updateFlow(id: string, input: Pick<Workflow, "json">) {
|
||||||
const before = await this.get(id, false);
|
|
||||||
if (before == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const beforeStr = JSON.stringify(before.json);
|
|
||||||
const afterStr = JSON.stringify(input.json);
|
|
||||||
if (beforeStr === afterStr) {
|
|
||||||
// no change detected
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const after = await this._update(id, {
|
const after = await this._update(id, {
|
||||||
updateTime: Date.now(),
|
updateTime: Date.now(),
|
||||||
json: input.json,
|
json: input.json,
|
||||||
@@ -240,8 +211,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
if (twoWaySyncEnabled) {
|
if (twoWaySyncEnabled) {
|
||||||
after && (await TwowaySyncAPI.saveWorkflow(after));
|
after && (await TwowaySyncAPI.saveWorkflow(after));
|
||||||
} else {
|
|
||||||
after && (await saveJsonFileMyWorkflows(after));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,33 +263,19 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("batchCreateFlows error", e);
|
console.error("batchCreateFlows error", e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
await saveJsonFileMyWorkflows(newWorkflow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await indexdb.workflows.bulkAdd(newWorkflows);
|
|
||||||
this.saveDiskDB();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("batchCreateFlows error", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteFlow(id: string) {
|
public async deleteFlow(id: string) {
|
||||||
const workflow = await this.get(id);
|
const workflow = await this.get(id);
|
||||||
|
|
||||||
if (workflow) {
|
if (workflow) {
|
||||||
const twoWaySyncEnabled =
|
const twoWaySyncEnabled =
|
||||||
await userSettingsTable?.getSetting("twoWaySync");
|
await userSettingsTable?.getSetting("twoWaySync");
|
||||||
if (twoWaySyncEnabled) {
|
if (twoWaySyncEnabled) {
|
||||||
await TwowaySyncAPI.deleteWorkflow(workflow);
|
await TwowaySyncAPI.deleteWorkflow(workflow);
|
||||||
} else {
|
|
||||||
deleteJsonFileMyWorkflows({ ...workflow });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//add to IndexDB
|
|
||||||
await indexdb.workflows.delete(id);
|
|
||||||
this.saveDiskDB();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async batchDeleteFlow(ids: string[]) {
|
public async batchDeleteFlow(ids: string[]) {
|
||||||
@@ -331,12 +286,8 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
await userSettingsTable?.getSetting("twoWaySync");
|
await userSettingsTable?.getSetting("twoWaySync");
|
||||||
if (twoWaySyncEnabled) {
|
if (twoWaySyncEnabled) {
|
||||||
await TwowaySyncAPI.deleteWorkflow(flow);
|
await TwowaySyncAPI.deleteWorkflow(flow);
|
||||||
} else {
|
|
||||||
deleteJsonFileMyWorkflows({ ...flow });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await indexdb.workflows.bulkDelete(ids);
|
|
||||||
this.saveDiskDB();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listFolderContent(
|
public async listFolderContent(
|
||||||
@@ -366,28 +317,55 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
|||||||
public async get(
|
public async get(
|
||||||
id: string,
|
id: string,
|
||||||
fetchJsonContentOnDisk = true, //if false, only get the meta info from indexdb
|
fetchJsonContentOnDisk = true, //if false, only get the meta info from indexdb
|
||||||
): Promise<Workflow | undefined> {
|
): Promise<
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
parentFolderID?: string | null;
|
||||||
|
saveLock?: boolean;
|
||||||
|
cloudID?: string | null;
|
||||||
|
coverMediaPath?: string | null;
|
||||||
|
json: string;
|
||||||
|
updateTime: number;
|
||||||
|
createTime: number;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||||
const wf = await indexdb.workflows.get(id);
|
if (!twoWaySyncEnabled) {
|
||||||
if (!twoWaySyncEnabled || !fetchJsonContentOnDisk) {
|
return await indexdb.workflows.get(id);
|
||||||
return wf;
|
|
||||||
}
|
}
|
||||||
// two way sync mode
|
// two way sync mode
|
||||||
if (wf == null) {
|
const rel = this.workflowIDPathMap[id];
|
||||||
|
if (!rel) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const data = await TwowaySyncAPI.getFile({
|
const data = await TwowaySyncAPI.getFile({
|
||||||
parentFolderID: wf.parentFolderID ?? null,
|
relPath: rel,
|
||||||
name: wf.name,
|
id: id,
|
||||||
id: wf.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data.json) {
|
if (!data.json) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const workspaceInfo = data.json?.extra?.[COMFYSPACE_TRACKING_FIELD_NAME];
|
||||||
|
if (!workspaceInfo || workspaceInfo.id !== id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...wf,
|
id: workspaceInfo.id,
|
||||||
|
name:
|
||||||
|
rel
|
||||||
|
.split("/")
|
||||||
|
.pop()
|
||||||
|
?.replace(/\.json$/, "") ?? "Untitled Flow",
|
||||||
|
parentFolderID: rel.split("/").slice(0, -1).join("/"),
|
||||||
|
saveLock: workspaceInfo.saveLock,
|
||||||
|
cloudID: workspaceInfo.cloudID,
|
||||||
|
coverMediaPath: workspaceInfo.coverMediaPath,
|
||||||
json: JSON.stringify(data.json),
|
json: JSON.stringify(data.json),
|
||||||
|
updateTime: data.updateTime ?? Date.now(),
|
||||||
|
createTime: data.createTime ?? Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
// @ts-expect-error ComfyUI import
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Flex,
|
|
||||||
Heading,
|
Heading,
|
||||||
HStack,
|
HStack,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
import { MetaBoxContext } from "./metaBoxContext.ts";
|
import { MetaBoxContext } from "./metaBoxContext.ts";
|
||||||
import { workflowsTable } from "../../../db-tables/WorkspaceDB.ts";
|
import { workflowsTable } from "../../../db-tables/WorkspaceDB.ts";
|
||||||
import { WorkspaceContext } from "../../../WorkspaceContext.ts";
|
import { WorkspaceContext } from "../../../WorkspaceContext.ts";
|
||||||
// @ts-ignore
|
import { app } from "../../../utils/comfyapp.ts";
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
export type TopFieldType = {
|
export type TopFieldType = {
|
||||||
promptKey: string | number;
|
promptKey: string | number;
|
||||||
class_type?: string;
|
class_type?: string;
|
||||||
@@ -64,18 +63,6 @@ export default function MetadataForm({ media }: { media: Media | null }) {
|
|||||||
|
|
||||||
const [topFields, setTopFields] = useState<TopFieldType[]>([]);
|
const [topFields, setTopFields] = useState<TopFieldType[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (curFlowID) {
|
|
||||||
const topFieldsConfig = (await workflowsTable?.get(curFlowID))
|
|
||||||
?.topFieldsConfig;
|
|
||||||
if (topFieldsConfig) {
|
|
||||||
setTopFields(topFieldsConfig);
|
|
||||||
setShowAllInputs(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [curFlowID]);
|
|
||||||
const updateTopField = (field: TopFieldType) => {
|
const updateTopField = (field: TopFieldType) => {
|
||||||
if (
|
if (
|
||||||
isInTopField(topFields, {
|
isInTopField(topFields, {
|
||||||
@@ -88,19 +75,9 @@ export default function MetadataForm({ media }: { media: Media | null }) {
|
|||||||
(v) => v.name !== field.name || v.promptKey !== field.promptKey,
|
(v) => v.name !== field.name || v.promptKey !== field.promptKey,
|
||||||
);
|
);
|
||||||
setTopFields(topFieldsConfig);
|
setTopFields(topFieldsConfig);
|
||||||
if (curFlowID) {
|
|
||||||
workflowsTable?.updateTopFields(curFlowID, {
|
|
||||||
topFieldsConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const topFieldsConfig = [...topFields, field];
|
const topFieldsConfig = [...topFields, field];
|
||||||
setTopFields(topFieldsConfig);
|
setTopFields(topFieldsConfig);
|
||||||
if (curFlowID) {
|
|
||||||
workflowsTable?.updateTopFields(curFlowID, {
|
|
||||||
topFieldsConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,6 @@
|
|||||||
import React, { Suspense } from "react";
|
import React, { Suspense } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { ColorModeScript } from "@chakra-ui/react";
|
import { ColorModeScript, type StyleFunctionProps } from "@chakra-ui/react";
|
||||||
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
|
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
|
||||||
import { AlertDialogProvider } from "./components/AlertDialogProvider.tsx";
|
import { AlertDialogProvider } from "./components/AlertDialogProvider.tsx";
|
||||||
import CSSReset from "./MyCSSReset.tsx";
|
import CSSReset from "./MyCSSReset.tsx";
|
||||||
@@ -35,7 +35,20 @@ const zIndices = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const theme = extendTheme({
|
const theme = extendTheme({
|
||||||
|
config: {
|
||||||
|
initialColorMode: "dark",
|
||||||
|
useSystemColorMode: false,
|
||||||
|
},
|
||||||
zIndices,
|
zIndices,
|
||||||
|
components: {
|
||||||
|
Card: {
|
||||||
|
baseStyle: (props: StyleFunctionProps) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: props.colorMode === "dark" ? "#171B21" : "#F6F8FA",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ReactDOM.createRoot(topbar).render(
|
ReactDOM.createRoot(topbar).render(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default function ServerEventListener() {
|
|||||||
name: version.name,
|
name: version.name,
|
||||||
authorID: version.authorID,
|
authorID: version.authorID,
|
||||||
json: version.json,
|
json: version.json,
|
||||||
|
api_prompt: null,
|
||||||
});
|
});
|
||||||
newversion && loadWorkflowID(existingWorkflow.id, newversion.id);
|
newversion && loadWorkflowID(existingWorkflow.id, newversion.id);
|
||||||
}
|
}
|
||||||
@@ -56,6 +57,7 @@ export default function ServerEventListener() {
|
|||||||
name: version.name,
|
name: version.name,
|
||||||
json: version.json,
|
json: version.json,
|
||||||
authorID: version.authorID,
|
authorID: version.authorID,
|
||||||
|
api_prompt: null,
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
});
|
});
|
||||||
await loadWorkflowID(newWorkflow.id, newverison?.id);
|
await loadWorkflowID(newWorkflow.id, newverison?.id);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { api } from "/scripts/api.js";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
HStack,
|
HStack,
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { cancelDownload } from "../../Api";
|
import { cancelDownload } from "../../Api";
|
||||||
|
import { api } from "../../utils/comfyapp";
|
||||||
|
|
||||||
type Queue = { save_path: string; progress: number };
|
type Queue = { save_path: string; progress: number };
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function getModelFromSearch(
|
|||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
q: q,
|
q: q,
|
||||||
indexUid: "models_v8",
|
indexUid: "models_v9",
|
||||||
facets: [
|
facets: [
|
||||||
"category.name",
|
"category.name",
|
||||||
"checkpointType",
|
"checkpointType",
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ import {
|
|||||||
import { IconExternalLink } from "@tabler/icons-react";
|
import { IconExternalLink } from "@tabler/icons-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
|
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import type {
|
import type {
|
||||||
DepsResult,
|
DepsResult,
|
||||||
ModelFile,
|
ModelDep,
|
||||||
} from "../../spacejson/handleDownloadSpaceJson";
|
} from "../../spacejson/handleDownloadSpaceJson";
|
||||||
import { indexdb } from "../../db-tables/indexdb";
|
import { indexdb } from "../../db-tables/indexdb";
|
||||||
|
import { app } from "../../utils/comfyapp";
|
||||||
|
|
||||||
const HF_LOGO =
|
const HF_LOGO =
|
||||||
"https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png";
|
"https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png";
|
||||||
@@ -33,8 +32,8 @@ export interface MissingModel {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: MissingModel;
|
model: MissingModel;
|
||||||
setSelectedModelDeps: (deps: ModelFile[]) => void;
|
setSelectedModelDeps: (deps: ModelDep[]) => void;
|
||||||
selectedModelDeps: ModelFile[];
|
selectedModelDeps: ModelDep[];
|
||||||
foldersList: Record<string, string[]>;
|
foldersList: Record<string, string[]>;
|
||||||
setFoldersList: (folders: Record<string, string[]>) => void;
|
setFoldersList: (folders: Record<string, string[]>) => void;
|
||||||
}
|
}
|
||||||
@@ -47,7 +46,7 @@ export default function MissingModelItem({
|
|||||||
setFoldersList,
|
setFoldersList,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [suggestedModel, setSuggestedModel] = useState<
|
const [suggestedModel, setSuggestedModel] = useState<
|
||||||
ModelFile | { existingModel: string }
|
ModelDep | { existingModel: string }
|
||||||
>();
|
>();
|
||||||
const modelDeps: DepsResult["models"] =
|
const modelDeps: DepsResult["models"] =
|
||||||
app.graph.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.deps?.models;
|
app.graph.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.deps?.models;
|
||||||
@@ -77,7 +76,7 @@ export default function MissingModelItem({
|
|||||||
|
|
||||||
const selectedModelNames = selectedModelDeps.map((dep) => dep.filename);
|
const selectedModelNames = selectedModelDeps.map((dep) => dep.filename);
|
||||||
function isExistingModel(
|
function isExistingModel(
|
||||||
model: ModelFile | { existingModel: string },
|
model: ModelDep | { existingModel: string },
|
||||||
): model is { existingModel: string } {
|
): model is { existingModel: string } {
|
||||||
return (model as { existingModel: string }).existingModel != null;
|
return (model as { existingModel: string }).existingModel != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,14 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import MissingModelItem from "./MissingModelItem";
|
import MissingModelItem from "./MissingModelItem";
|
||||||
import { DRAWER_Z_INDEX } from "../../const";
|
import { DRAWER_Z_INDEX } from "../../const";
|
||||||
import type { ModelFile } from "../../spacejson/handleDownloadSpaceJson";
|
import type { ModelDep } from "../../spacejson/handleDownloadSpaceJson";
|
||||||
import { getAllFoldersList } from "../../Api";
|
import { getAllFoldersList } from "../../Api";
|
||||||
import { installModelsApi } from "../api/modelsApi";
|
import { installModelsApi } from "../api/modelsApi";
|
||||||
import { getCivitApiKey } from "../../utils/civitUtils";
|
import { getCivitApiKey } from "../../utils/civitUtils";
|
||||||
import { WorkspaceContext } from "../../WorkspaceContext";
|
import { WorkspaceContext } from "../../WorkspaceContext";
|
||||||
|
import { app } from "../../utils/comfyapp";
|
||||||
|
|
||||||
export interface MissingModel {
|
export interface MissingModel {
|
||||||
class_type: string;
|
class_type: string;
|
||||||
@@ -41,7 +40,7 @@ export default function MissingModelsListDrawer({
|
|||||||
app.canvasEl.removeEventListener("click", onClose);
|
app.canvasEl.removeEventListener("click", onClose);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const [selectedModelDeps, setSelectedModelDeps] = useState<ModelFile[]>([]);
|
const [selectedModelDeps, setSelectedModelDeps] = useState<ModelDep[]>([]);
|
||||||
const [foldersList, setFoldersList] = useState<Record<string, string[]>>({});
|
const [foldersList, setFoldersList] = useState<Record<string, string[]>>({});
|
||||||
const { setRoute } = useContext(WorkspaceContext);
|
const { setRoute } = useContext(WorkspaceContext);
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
@@ -78,7 +77,7 @@ export default function MissingModelsListDrawer({
|
|||||||
}
|
}
|
||||||
installModelsApi({
|
installModelsApi({
|
||||||
file_hash: model.fileHash ?? undefined,
|
file_hash: model.fileHash ?? undefined,
|
||||||
filename: model.filename,
|
filename: model.name,
|
||||||
save_path: foldersList[model.fileFolder][0],
|
save_path: foldersList[model.fileFolder][0],
|
||||||
url,
|
url,
|
||||||
force_filename: true,
|
force_filename: true,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import Fuse from "fuse.js/min-basic";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ModelsTags } from "./ModelsTags";
|
import { ModelsTags } from "./ModelsTags";
|
||||||
import { ModelsList } from "./ModelsList";
|
import { ModelsList } from "./ModelsList";
|
||||||
// @ts-expect-error ComfyUI imports
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import InstallModelsButton from "../install-models/InstallModelsButton";
|
import InstallModelsButton from "../install-models/InstallModelsButton";
|
||||||
import { type ModelsListRespItem } from "../types";
|
import { type ModelsListRespItem } from "../types";
|
||||||
import { useUpdateModels } from "../hooks/useUpdateModels";
|
import { useUpdateModels } from "../hooks/useUpdateModels";
|
||||||
@@ -23,6 +21,7 @@ import ShowNsfwModelThumbnailSettings from "../../settings/ShowNsfwModelThumbnai
|
|||||||
import { indexdb } from "../../db-tables/indexdb";
|
import { indexdb } from "../../db-tables/indexdb";
|
||||||
import { type Model } from "../../types/dbTypes";
|
import { type Model } from "../../types/dbTypes";
|
||||||
import ModelDropEventListener from "../topbar/ModelDropEventListener";
|
import ModelDropEventListener from "../topbar/ModelDropEventListener";
|
||||||
|
import { app } from "../../utils/comfyapp";
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Button } from "@chakra-ui/react";
|
import { Button } from "@chakra-ui/react";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
// @ts-ignore
|
|
||||||
import { api } from "/scripts/api.js";
|
import { api } from "/scripts/api.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import MissingModelsListDrawer, {
|
import MissingModelsListDrawer, {
|
||||||
MissingModel,
|
MissingModel,
|
||||||
} from "../missing-models-drawer/MissingModelsListDrawer";
|
} from "../missing-models-drawer/MissingModelsListDrawer";
|
||||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
|
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
|
||||||
|
import { app } from "../../utils/comfyapp";
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
interface NodeError {
|
interface NodeError {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, DragEvent } from "react";
|
import { useEffect, DragEvent } from "react";
|
||||||
// @ts-ignore
|
import { app } from "../../utils/comfyapp";
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
|
|
||||||
export default function ModelDropEventListener() {
|
export default function ModelDropEventListener() {
|
||||||
const handleModelDrop = async (
|
const handleModelDrop = async (
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Button, DarkMode, Stack } from "@chakra-ui/react";
|
import { Button, DarkMode, Stack } from "@chakra-ui/react";
|
||||||
import { lazy, useEffect, DragEvent, useContext, Suspense } from "react";
|
import { lazy, useEffect, useContext, Suspense } from "react";
|
||||||
import ModelsListDrawer from "../models-list-drawer/ModelsListDrawer";
|
import ModelsListDrawer from "../models-list-drawer/ModelsListDrawer";
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { WorkspaceContext } from "../../WorkspaceContext";
|
import { WorkspaceContext } from "../../WorkspaceContext";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -15,30 +13,10 @@ import type { ModelsListRespItemFromApi } from "../types";
|
|||||||
import InatallModelsModal from "../install-models/InstallModelsModal";
|
import InatallModelsModal from "../install-models/InstallModelsModal";
|
||||||
import { TOPBAR_BUTTON_HEIGHT } from "../../const";
|
import { TOPBAR_BUTTON_HEIGHT } from "../../const";
|
||||||
|
|
||||||
const AddMissingModelsButton = lazy(
|
|
||||||
() => import("./InstallMissingModelsButton"),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function ModelManagerTopbar() {
|
export default function ModelManagerTopbar() {
|
||||||
const { setRoute, route } = useContext(WorkspaceContext);
|
const { setRoute, route } = useContext(WorkspaceContext);
|
||||||
const handleModelDrop = async (
|
|
||||||
e: DragEvent<HTMLCanvasElement> & { canvasX: number; canvasY: number },
|
|
||||||
) => {
|
|
||||||
const eventName = e.dataTransfer.getData("eventName");
|
|
||||||
if (eventName !== "WorkspaceManagerAddNode") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modelRelativePath = e.dataTransfer.getData("modelRelativePath");
|
|
||||||
const nodeType = e.dataTransfer.getData("nodeType");
|
|
||||||
// @ts-ignore
|
|
||||||
const node = LiteGraph.createNode(nodeType);
|
|
||||||
node.pos = [e.canvasX, e.canvasY];
|
|
||||||
node.configure({ widgets_values: [modelRelativePath] });
|
|
||||||
app.graph.add(node);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
app.canvasEl.addEventListener("drop", handleModelDrop);
|
|
||||||
api.addEventListener(
|
api.addEventListener(
|
||||||
"model_list",
|
"model_list",
|
||||||
async (e: { detail: ModelsListRespItemFromApi[] }) => {
|
async (e: { detail: ModelsListRespItemFromApi[] }) => {
|
||||||
@@ -77,30 +55,22 @@ export default function ModelManagerTopbar() {
|
|||||||
window.dispatchEvent(new CustomEvent("model_list_updated"));
|
window.dispatchEvent(new CustomEvent("model_list_updated"));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return () => {
|
|
||||||
app.canvasEl.removeEventListener("drop", handleModelDrop);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ position: "relative" }}>
|
<Stack style={{ position: "relative" }}>
|
||||||
<DarkMode>
|
<Button
|
||||||
<Button
|
size={"sm"}
|
||||||
size={"sm"}
|
backgroundColor={"#434554"}
|
||||||
backgroundColor={"#434554"}
|
color={"white"}
|
||||||
aria-label="My models"
|
colorScheme="blue"
|
||||||
onClick={() => setRoute("modelList")}
|
aria-label="My models"
|
||||||
px={1}
|
onClick={() => setRoute("modelList")}
|
||||||
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
px={1}
|
||||||
>
|
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
||||||
Models
|
>
|
||||||
</Button>
|
Models
|
||||||
</DarkMode>
|
</Button>
|
||||||
<div style={{ position: "absolute", top: "38px", left: "0px" }}>
|
|
||||||
<Suspense>
|
|
||||||
<AddMissingModelsButton />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
{route === "modelList" && (
|
{route === "modelList" && (
|
||||||
<ModelsListDrawer onClose={() => setRoute("root")} />
|
<ModelsListDrawer onClose={() => setRoute("root")} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { deleteFile } from "./Api";
|
import { deleteFile } from "./Api";
|
||||||
// @ts-ignore
|
|
||||||
import { app, ComfyApp } from "/scripts/app.js";
|
|
||||||
|
|
||||||
export function formatTimestamp(
|
export function formatTimestamp(
|
||||||
unixTimestamp: number,
|
unixTimestamp: number,
|
||||||
showSec: boolean = false,
|
showSec: boolean = false,
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Input, Stack, useToast, Text, Flex, Button } from "@chakra-ui/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { userSettingsTable } from "../db-tables/WorkspaceDB";
|
||||||
|
|
||||||
|
export default function CloudHostSetting() {
|
||||||
|
const toast = useToast();
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
|
||||||
|
const getSettings = () => {
|
||||||
|
userSettingsTable?.getSetting("cloudHost").then((res) => {
|
||||||
|
setText(res ?? "");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSettings();
|
||||||
|
}, []);
|
||||||
|
const submitChange = async (text: string) => {
|
||||||
|
await userSettingsTable?.upsert({ cloudHost: text });
|
||||||
|
getSettings();
|
||||||
|
toast({
|
||||||
|
title: "Setting saved. Please refresh to see the changes.",
|
||||||
|
status: "success",
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Text>
|
||||||
|
For enterprise paid user only. Hosting site of the shared workflow.
|
||||||
|
</Text>
|
||||||
|
<Flex gap={2} alignItems={"center"}>
|
||||||
|
<Input
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onBlur={() => submitChange(text)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size={"sm"}
|
||||||
|
onClick={() => {
|
||||||
|
const cloudHost = userSettingsTable?.defaultSettings.cloudHost!;
|
||||||
|
setText(cloudHost);
|
||||||
|
submitChange(cloudHost);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
+134
-88
@@ -10,26 +10,20 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
Radio,
|
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Stack,
|
Stack,
|
||||||
|
Switch,
|
||||||
Tag,
|
Tag,
|
||||||
Text,
|
Text,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Workflow, WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes";
|
import { WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import CustomSelector from "../components/CustomSelector";
|
import CustomSelector from "../components/CustomSelector";
|
||||||
import { IconCloud, IconCopy, IconExternalLink } from "@tabler/icons-react";
|
import { IconCopy, IconExternalLink } from "@tabler/icons-react";
|
||||||
import {
|
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||||
userSettingsTable,
|
|
||||||
workflowVersionsTable,
|
|
||||||
workflowsTable,
|
|
||||||
} from "../db-tables/WorkspaceDB";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { api } from "/scripts/api.js";
|
import { api } from "/scripts/api.js";
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import {
|
import {
|
||||||
PrivacyLabel,
|
PrivacyLabel,
|
||||||
fetchCloudWorkflowPrivacy,
|
fetchCloudWorkflowPrivacy,
|
||||||
@@ -38,35 +32,50 @@ import {
|
|||||||
privacyOptions,
|
privacyOptions,
|
||||||
} from "./shareUtils";
|
} from "./shareUtils";
|
||||||
|
|
||||||
import ShareDialogWorkflowVersionRadio from "./ShareDialogWorkflowVersionRadio";
|
import ResourceDepsForm from "../spacejson/ResourceDepsForm";
|
||||||
|
import { app } from "../utils/comfyapp";
|
||||||
|
import { useStateRef } from "../customHooks/useStateRef";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { WorkspaceContext } from "../WorkspaceContext";
|
||||||
|
import {
|
||||||
|
DepsResult,
|
||||||
|
WorkspaceInfoDeps,
|
||||||
|
extractAndFetchFileNames,
|
||||||
|
} from "../spacejson/handleDownloadSpaceJson";
|
||||||
|
import { fetchApi } from "../Api";
|
||||||
|
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShareDialog({ onClose }: Props) {
|
export default function ShareDialog({ onClose }: Props) {
|
||||||
const [versionName, setVersionName] = useState("v" + getCurDateString());
|
const [versionName, setVersionName, versionNameRef] = useStateRef(
|
||||||
const [localVersions, setLocalVersions] = useState<WorkflowVersion[]>([]);
|
"v" + getCurDateString(),
|
||||||
|
);
|
||||||
|
const { saveCurWorkflow } = useContext(WorkspaceContext);
|
||||||
|
const [deps, setDeps] = useState<DepsResult>();
|
||||||
|
const [uploadingImage, setUploadingImage] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [cloudHost, setCloudHost] = useState("");
|
const cloudHost = userSettingsTable?.settings?.cloudHost;
|
||||||
const [privacy, setPrivacy] = useState<WorkflowPrivacy>("PRIVATE");
|
const [privacy, setPrivacy, privacyRef] =
|
||||||
|
useStateRef<WorkflowPrivacy>("PRIVATE");
|
||||||
const [selectedVersion, setSelectedVersion] = useState<
|
const [selectedVersion, setSelectedVersion] = useState<
|
||||||
string | "new_version"
|
string | "new_version"
|
||||||
>("new_version");
|
>("new_version");
|
||||||
const cloudHostRef = useRef("");
|
const workflow = workflowsTable?.curWorkflow;
|
||||||
const [workflow, setWorkflow] = useState<Workflow>();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const [enableDeps, setEnableDeps] = useState(true);
|
||||||
const handleShareWorkflowSuccess = async (event: MessageEvent) => {
|
const handleShareWorkflowSuccess = async (event: MessageEvent) => {
|
||||||
const detail = event.data;
|
const detail = event.data;
|
||||||
if (
|
if (
|
||||||
event.origin !== cloudHostRef.current ||
|
// event.origin !== cloudHostRef.current ||
|
||||||
detail.type !== "share_workflow_success"
|
detail.type !== "share_workflow_success"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localID = detail.localWorkflowID;
|
const localID = detail.localWorkflowID;
|
||||||
const localVerID = detail.localVersionID;
|
|
||||||
const cloudVersionID = detail.version.id;
|
const cloudVersionID = detail.version.id;
|
||||||
const cloudID = detail.version.workflowID;
|
const cloudID = detail.version.workflowID;
|
||||||
|
|
||||||
@@ -74,24 +83,22 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
localID &&
|
localID &&
|
||||||
(await workflowsTable?.updateMetaInfo(localID, {
|
(await workflowsTable?.updateMetaInfo(localID, {
|
||||||
cloudID: cloudID,
|
cloudID: cloudID,
|
||||||
cloudOrigin: event.origin,
|
privacy: privacyRef.current,
|
||||||
privacy: privacy,
|
|
||||||
}));
|
|
||||||
localVerID &&
|
|
||||||
cloudVersionID &&
|
|
||||||
(await workflowVersionsTable?.update(localVerID, {
|
|
||||||
cloudID: cloudVersionID,
|
|
||||||
cloudOrigin: event.origin,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
window.open(
|
window.open(
|
||||||
cloudHostRef.current + "/workflow/" + cloudID + "/" + cloudVersionID,
|
cloudHost + "/workflow/" + cloudID + "/" + cloudVersionID,
|
||||||
"_blank",
|
"_blank",
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
const graph = app.graph.serialize();
|
||||||
|
extractAndFetchFileNames(graph.nodes ?? []).then((depsRes) => {
|
||||||
|
setDeps(depsRes);
|
||||||
|
});
|
||||||
window.addEventListener("message", handleShareWorkflowSuccess);
|
window.addEventListener("message", handleShareWorkflowSuccess);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("message", handleShareWorkflowSuccess);
|
window.removeEventListener("message", handleShareWorkflowSuccess);
|
||||||
@@ -99,24 +106,11 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const host =
|
|
||||||
import.meta.env.MODE === "production"
|
|
||||||
? await userSettingsTable?.getSetting("cloudHost")
|
|
||||||
: "http://localhost:3000";
|
|
||||||
if (host) {
|
|
||||||
setCloudHost(host);
|
|
||||||
cloudHostRef.current = host;
|
|
||||||
}
|
|
||||||
const workflow = workflowsTable?.curWorkflow ?? undefined;
|
|
||||||
setWorkflow(workflow);
|
|
||||||
if (workflow?.cloudID && workflow.cloudOrigin) {
|
if (workflow?.cloudID && workflow.cloudOrigin) {
|
||||||
fetchCloudWorkflowPrivacy(workflow).then((privacy) => {
|
fetchCloudWorkflowPrivacy(workflow).then((privacy) => {
|
||||||
setPrivacy(privacy);
|
setPrivacy(privacy);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
workflowVersionsTable?.listByWorkflowID(workflow!.id).then((vers) => {
|
|
||||||
setLocalVersions(vers);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
const copyTextToClipboard = (text: string) => {
|
const copyTextToClipboard = (text: string) => {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@@ -133,7 +127,24 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
console.error("Failed to copy text: ", err);
|
console.error("Failed to copy text: ", err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const onShare = async () => {
|
const pushToCloud = async (imageDeps?: DepsResult["images"]) => {
|
||||||
|
const graph = app.graph.serialize();
|
||||||
|
(graph.extra ||= {})[COMFYSPACE_TRACKING_FIELD_NAME] ||= {};
|
||||||
|
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].deps = {
|
||||||
|
models: deps?.models ?? {},
|
||||||
|
images: imageDeps ?? {},
|
||||||
|
nodeRepos: deps?.nodeRepos ?? [],
|
||||||
|
} as WorkspaceInfoDeps;
|
||||||
|
|
||||||
|
await saveCurWorkflow();
|
||||||
|
|
||||||
|
const apiPrompt = await app
|
||||||
|
.graphToPrompt(app.graph)
|
||||||
|
.then((data: { output: any; workflow: any }) => {
|
||||||
|
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].apiPrompt = data.output;
|
||||||
|
return data.output;
|
||||||
|
});
|
||||||
|
const jsonToShare = JSON.stringify(graph);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const secretKey = generateRandomKey(32);
|
const secretKey = generateRandomKey(32);
|
||||||
const host =
|
const host =
|
||||||
@@ -141,30 +152,15 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
? ((await userSettingsTable?.getSetting("cloudHost")) as string)
|
? ((await userSettingsTable?.getSetting("cloudHost")) as string)
|
||||||
: "http://localhost:3000";
|
: "http://localhost:3000";
|
||||||
let nodeDefs: Object;
|
let nodeDefs: Object;
|
||||||
let version: WorkflowVersion | undefined;
|
nodeDefs = getNodeDefs();
|
||||||
try {
|
const version: WorkflowVersion = {
|
||||||
nodeDefs = getNodeDefs();
|
id: nanoid(),
|
||||||
if (selectedVersion === "new_version") {
|
workflowID: workflow!.id,
|
||||||
version = await workflowVersionsTable?.add({
|
name: versionNameRef.current,
|
||||||
workflowID: workflow!.id,
|
createTime: Date.now(),
|
||||||
name: versionName,
|
api_prompt: JSON.stringify(apiPrompt),
|
||||||
createTime: Date.now(),
|
json: jsonToShare ?? JSON.stringify(app.graph.serialize()),
|
||||||
json: JSON.stringify(app.graph.serialize()),
|
};
|
||||||
});
|
|
||||||
} else {
|
|
||||||
version = await workflowVersionsTable?.get(selectedVersion);
|
|
||||||
}
|
|
||||||
if (!version) {
|
|
||||||
alert(`Failed to find version: ${selectedVersion}, please try again.`);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setLoading(false);
|
|
||||||
alert("Failed to share workflow, please try again.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const protocol = window.location.protocol;
|
const protocol = window.location.protocol;
|
||||||
const baseHost = window.location.host;
|
const baseHost = window.location.host;
|
||||||
const baseUrl = `${protocol}//${baseHost}`;
|
const baseUrl = `${protocol}//${baseHost}`;
|
||||||
@@ -184,7 +180,7 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
workflow: curWorkflow,
|
workflow: curWorkflow,
|
||||||
version: version,
|
version: version,
|
||||||
nodeDefs: nodeDefs,
|
nodeDefs: nodeDefs,
|
||||||
privacy: privacy,
|
privacy: privacyRef.current,
|
||||||
},
|
},
|
||||||
host,
|
host,
|
||||||
);
|
);
|
||||||
@@ -194,12 +190,57 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
window.addEventListener("message", handleChildReady);
|
window.addEventListener("message", handleChildReady);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onShare = async () => {
|
||||||
|
const imageDeps = deps?.images ?? {};
|
||||||
|
if (enableDeps) {
|
||||||
|
// upload images
|
||||||
|
setUploadingImage(true);
|
||||||
|
const imageDepsToUpload = Object.values(deps?.images ?? {})
|
||||||
|
// .filter((i) => !i.url)
|
||||||
|
.map((i) => i.filename);
|
||||||
|
if (imageDepsToUpload.length) {
|
||||||
|
const uploadResp = await fetchApi("/workspace/upload_image", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
images: imageDepsToUpload,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = (await uploadResp.json()) as Record<string, string>;
|
||||||
|
Object.keys(json).forEach((key) => {
|
||||||
|
if (imageDeps?.[key]) {
|
||||||
|
imageDeps[key].url = json[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setUploadingImage(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToCloud(imageDeps);
|
||||||
|
};
|
||||||
|
|
||||||
const cloudWorkflowID = workflowsTable?.curWorkflow?.cloudID;
|
const cloudWorkflowID = workflowsTable?.curWorkflow?.cloudID;
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={true} onClose={onClose} size={"lg"}>
|
<Modal isOpen={true} onClose={onClose} size={"lg"}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent width={["98%", "90%", "50%"]} maxWidth={"600px"}>
|
<ModalContent width={["98%", "90%", "50%"]} maxWidth={"600px"}>
|
||||||
<ModalHeader>Share "{workflow?.name}"</ModalHeader>
|
<ModalHeader>
|
||||||
|
<Flex justifyContent={"space-between"}>
|
||||||
|
<p>Share "{workflow?.name}"</p>
|
||||||
|
<Button
|
||||||
|
colorScheme="teal"
|
||||||
|
onClick={onShare}
|
||||||
|
size={"sm"}
|
||||||
|
isDisabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Sharing" : "Share"}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
{cloudHost && (
|
||||||
|
<Text fontSize={16} color={"GrayText"} fontWeight={400}>
|
||||||
|
Share to {new URL(cloudHost).host}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody pb={10}>
|
<ModalBody pb={10}>
|
||||||
<Stack gap={5}>
|
<Stack gap={5}>
|
||||||
{cloudWorkflowID == null ? (
|
{cloudWorkflowID == null ? (
|
||||||
@@ -248,7 +289,6 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
<Text>Choose a version to share:</Text>
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
gap={4}
|
gap={4}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -258,7 +298,6 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack mb={5} alignItems={"center"}>
|
<HStack mb={5} alignItems={"center"}>
|
||||||
<Radio key={"new_version"} value="new_version" />
|
|
||||||
<Input
|
<Input
|
||||||
value={versionName}
|
value={versionName}
|
||||||
width={"60%"}
|
width={"60%"}
|
||||||
@@ -270,28 +309,35 @@ export default function ShareDialog({ onClose }: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Flex color="green">
|
<Flex color="green">
|
||||||
<Badge colorScheme="purple">New version</Badge>
|
<Badge colorScheme="purple">Version Name</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{localVersions.slice(0, 4).map((ver) => {
|
|
||||||
return (
|
|
||||||
<ShareDialogWorkflowVersionRadio
|
|
||||||
key={ver.id}
|
|
||||||
version={ver}
|
|
||||||
cloudHost={cloudHost}
|
|
||||||
cloudWorkflowID={cloudWorkflowID ?? null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
{Object.keys(deps?.images ?? {}).length > 0 && (
|
||||||
|
<Stack borderRadius={6} borderWidth={"1px"} p={2}>
|
||||||
|
<Switch
|
||||||
|
isChecked={enableDeps}
|
||||||
|
onChange={() => setEnableDeps(!enableDeps)}
|
||||||
|
fontWeight={"600"}
|
||||||
|
>
|
||||||
|
Share input images
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<span style={{ color: "GrayText" }}>
|
||||||
|
If enabled your input images will be uploaded and shared to{" "}
|
||||||
|
{cloudHost}
|
||||||
|
</span>
|
||||||
|
{enableDeps && (
|
||||||
|
<ResourceDepsForm
|
||||||
|
deps={deps ?? null}
|
||||||
|
setDeps={setDeps}
|
||||||
|
uploadingImage={uploadingImage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<HStack justifyContent={"flex-end"} mt={16}>
|
|
||||||
<Button colorScheme="teal" onClick={onShare} isDisabled={loading}>
|
|
||||||
{loading ? "Sharing" : "Share"}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { IconLink, IconLock, IconWorld } from "@tabler/icons-react";
|
import { IconLink, IconLock, IconWorld } from "@tabler/icons-react";
|
||||||
import { CustomSelectorOption } from "../components/CustomSelector";
|
import { CustomSelectorOption } from "../components/CustomSelector";
|
||||||
import { Workflow, WorkflowPrivacy } from "../types/dbTypes";
|
import { Workflow, WorkflowPrivacy } from "../types/dbTypes";
|
||||||
// @ts-expect-error
|
import { app } from "../utils/comfyapp";
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
|
|
||||||
export function generateRandomKey(length: number) {
|
export function generateRandomKey(length: number) {
|
||||||
// Generate a random array of bytes
|
// Generate a random array of bytes
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useContext, useRef } from "react";
|
|
||||||
import { WorkspaceContext } from "../WorkspaceContext";
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
HStack,
|
|
||||||
Heading,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalContent,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
|
||||||
import ResourceDepsForm from "./ResourceDepsForm";
|
|
||||||
|
|
||||||
export default function DownloadSpaceJsonDialog() {
|
|
||||||
const { setRoute } = useContext(WorkspaceContext);
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
|
||||||
return (
|
|
||||||
<Modal isOpen={true} onClose={() => setRoute("root")} size={"xl"}>
|
|
||||||
<ModalContent>
|
|
||||||
<ModalBody p={5} gap={5}>
|
|
||||||
<HStack>
|
|
||||||
<Heading size={"md"}>Add Resouce Dependencies</Heading>
|
|
||||||
<Tooltip
|
|
||||||
label="runnable workflows include all resource dependecy download urls
|
|
||||||
(models, cursom nodes, input images) and can be one-click
|
|
||||||
installed and run in any ComfyUI with workpace-manager installed."
|
|
||||||
>
|
|
||||||
<Badge borderRadius={10}>
|
|
||||||
<IconInfoCircle size={20} />
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>
|
|
||||||
<Text color={"GrayText"}>Export runnable workflow</Text>
|
|
||||||
<ResourceDepsForm ref={formRef} />
|
|
||||||
<HStack justifyContent={"flex-end"}>
|
|
||||||
<Button
|
|
||||||
colorScheme="teal"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => formRef.current?.requestSubmit()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={() => setRoute("share")}>
|
|
||||||
Skip
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,332 +1,44 @@
|
|||||||
import {
|
import { HStack, Heading, Image, Spinner, Stack } from "@chakra-ui/react";
|
||||||
forwardRef,
|
import { DepsResult } from "./handleDownloadSpaceJson";
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { WorkspaceContext } from "../WorkspaceContext";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
HStack,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Image,
|
|
||||||
Input,
|
|
||||||
Spinner,
|
|
||||||
Stack,
|
|
||||||
Tag,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import {
|
|
||||||
DepsResult,
|
|
||||||
ModelFile,
|
|
||||||
WorkspaceInfoDeps,
|
|
||||||
extractAndFetchFileNames,
|
|
||||||
} from "./handleDownloadSpaceJson";
|
|
||||||
// @ts-ignore
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import { IconExternalLink, IconPencil, IconRefresh } from "@tabler/icons-react";
|
|
||||||
import { getHgModelInfoUrlFromDownloadUrl } from "../utils/civitUtils";
|
|
||||||
import { getAllModelsList } from "../Api";
|
|
||||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
|
||||||
import { indexdb } from "../db-tables/indexdb";
|
|
||||||
|
|
||||||
export default forwardRef<HTMLFormElement>(
|
export default function ResourceDepsForm({
|
||||||
function ResourceDepsForm(_props, ref) {
|
deps,
|
||||||
const { saveCurWorkflow, setRoute } = useContext(WorkspaceContext);
|
uploadingImage,
|
||||||
const [deps, setDeps] = useState<DepsResult>();
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
const [uploadingImage, setUploadingImage] = useState(false);
|
|
||||||
|
|
||||||
const init = useCallback(async () => {
|
|
||||||
const graph = app.graph.serialize();
|
|
||||||
extractAndFetchFileNames(graph.nodes ?? []).then((depsRes) => {
|
|
||||||
setDeps(depsRes);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onRefreshModelsList = useCallback(async () => {
|
|
||||||
setDeps(undefined);
|
|
||||||
await getAllModelsList();
|
|
||||||
window.addEventListener("model_list_updated", () => {
|
|
||||||
init();
|
|
||||||
window.removeEventListener("model_list_updated", () => {});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
init();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const validateURL = (url: string) => {
|
|
||||||
const pattern = new RegExp(
|
|
||||||
"^(https?:\\/\\/)?" +
|
|
||||||
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" +
|
|
||||||
"((\\d{1,3}\\.){3}\\d{1,3}))" +
|
|
||||||
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" +
|
|
||||||
"(\\?[;&a-z\\d%_.~+=-]*)?" +
|
|
||||||
"(\\#[-a-z\\d_]*)?$",
|
|
||||||
"i",
|
|
||||||
);
|
|
||||||
return !!pattern.test(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event: any) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const formData = new FormData(event.target);
|
|
||||||
// validate model deps and save model urls to db
|
|
||||||
const saveModelUrlDBPromises: Promise<ModelFile>[] =
|
|
||||||
Object.values(deps?.models ?? {}).map(async (model) => {
|
|
||||||
const inputDownloadUrl = formData.get(model.filename)?.toString();
|
|
||||||
if (inputDownloadUrl?.length) {
|
|
||||||
// because the models table is storing filename without extension as id, which should be migrated
|
|
||||||
const baseName = getBaseFileName(model.filename);
|
|
||||||
await indexdb.models.update(baseName + "@" + model.fileFolder, {
|
|
||||||
downloadUrl: inputDownloadUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const infoUrl = model.infoUrl?.length
|
|
||||||
? model.infoUrl
|
|
||||||
: inputDownloadUrl
|
|
||||||
? getHgModelInfoUrlFromDownloadUrl(inputDownloadUrl)
|
|
||||||
: null;
|
|
||||||
return {
|
|
||||||
filename: model.filename,
|
|
||||||
nodeType: model.nodeType,
|
|
||||||
fileHash: model.fileHash,
|
|
||||||
fileFolder: model.fileFolder,
|
|
||||||
downloadUrl: inputDownloadUrl ?? model.downloadUrl ?? null,
|
|
||||||
infoUrl: infoUrl ?? null,
|
|
||||||
inputName: model.inputName,
|
|
||||||
};
|
|
||||||
}) ?? [];
|
|
||||||
const modelDepsArr = await Promise.all(saveModelUrlDBPromises);
|
|
||||||
if (!validateModelDeps(modelDepsArr)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setErrors({});
|
|
||||||
|
|
||||||
// upload images
|
|
||||||
setUploadingImage(true);
|
|
||||||
const imageDepsToUpload = Object.values(deps?.images ?? {})
|
|
||||||
.filter((i) => !i.url)
|
|
||||||
.map((i) => i.filename);
|
|
||||||
const imageDeps = deps?.images ?? {};
|
|
||||||
|
|
||||||
if (imageDepsToUpload.length) {
|
|
||||||
const uploadResp = await fetch("/workspace/upload_image", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
images: imageDepsToUpload,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = (await uploadResp.json()) as Record<string, string>;
|
|
||||||
Object.keys(json).forEach((key) => {
|
|
||||||
if (imageDeps?.[key]) {
|
|
||||||
imageDeps[key].url = json[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setUploadingImage(false);
|
|
||||||
|
|
||||||
// save deps to graph
|
|
||||||
const modelDeps: WorkspaceInfoDeps["models"] = {};
|
|
||||||
modelDepsArr.forEach((model) => {
|
|
||||||
modelDeps[model.filename] = model;
|
|
||||||
});
|
|
||||||
const graph = app.graph.serialize();
|
|
||||||
(graph.extra ||= {})[COMFYSPACE_TRACKING_FIELD_NAME] ||= {};
|
|
||||||
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].deps = {
|
|
||||||
models: modelDeps,
|
|
||||||
images: imageDeps ?? {},
|
|
||||||
nodeRepos: deps?.nodeRepos ?? [],
|
|
||||||
} as WorkspaceInfoDeps;
|
|
||||||
await app
|
|
||||||
.graphToPrompt(app.graph)
|
|
||||||
.then((data: { output: any; workflow: any }) => {
|
|
||||||
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].apiPrompt = data.output;
|
|
||||||
});
|
|
||||||
|
|
||||||
await saveCurWorkflow();
|
|
||||||
console.log("graph", graph);
|
|
||||||
setRoute("share");
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateModelDeps = useCallback((modelDeps: ModelFile[]) => {
|
|
||||||
let isValid = true;
|
|
||||||
modelDeps.forEach((model) => {
|
|
||||||
const key = model.filename;
|
|
||||||
if (!model.downloadUrl) {
|
|
||||||
setErrors((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[key]: "Invalid URL",
|
|
||||||
}));
|
|
||||||
isValid = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!validateURL(model.downloadUrl)) {
|
|
||||||
setErrors((prev) => ({ ...prev, [key]: "Invalid URL" }));
|
|
||||||
isValid = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!model.fileFolder) {
|
|
||||||
setErrors((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[key]: "Model file not found, please re-scan models folder",
|
|
||||||
}));
|
|
||||||
isValid = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return isValid;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageDepsArr = Object.values(deps?.images ?? {});
|
|
||||||
const modelDepsArr = Object.values(deps?.models ?? []);
|
|
||||||
if (!deps) {
|
|
||||||
return <Spinner size="md" color="teal.400" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} ref={ref}>
|
|
||||||
<Stack gap={5} mt={5}>
|
|
||||||
<Stack>
|
|
||||||
<Heading size={"sm"}>Models ({modelDepsArr.length})</Heading>
|
|
||||||
{modelDepsArr.map((modelFile, index) => (
|
|
||||||
<ModelDepsItem
|
|
||||||
modelFile={modelFile}
|
|
||||||
key={index}
|
|
||||||
errors={errors}
|
|
||||||
onClickRefetchModelList={onRefreshModelsList}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{imageDepsArr.length > 0 && (
|
|
||||||
<Stack>
|
|
||||||
<HStack>
|
|
||||||
<Heading size={"sm"}>Images ({imageDepsArr.length})</Heading>
|
|
||||||
{/* <Tag colorScheme="yellow">Will be uploaded as url</Tag> */}
|
|
||||||
<p style={{ color: "GrayText" }}>Will be uploaded as url</p>
|
|
||||||
</HStack>
|
|
||||||
{uploadingImage && (
|
|
||||||
<span>
|
|
||||||
<Spinner size="md" color="teal.400" /> Uploading
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{imageDepsArr.map((image) => (
|
|
||||||
<Stack key={image.filename}>
|
|
||||||
<p>{image.filename}</p>
|
|
||||||
<Image
|
|
||||||
width={250}
|
|
||||||
src={`/workspace/view_media?filename=${image.filename}&isPreview=true&isInput=true`}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function getBaseFileName(filename: string) {
|
|
||||||
const parts = filename.split(".");
|
|
||||||
if (parts.length > 1) {
|
|
||||||
parts.pop();
|
|
||||||
return parts.join(".");
|
|
||||||
} else {
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModelDepsItem({
|
|
||||||
modelFile,
|
|
||||||
errors,
|
|
||||||
onClickRefetchModelList,
|
|
||||||
}: {
|
}: {
|
||||||
modelFile: DepsResult["models"][0];
|
deps: DepsResult | null;
|
||||||
errors: Record<string, string>;
|
setDeps: (deps: DepsResult) => void;
|
||||||
onClickRefetchModelList: () => void;
|
uploadingImage: boolean;
|
||||||
}) {
|
}) {
|
||||||
const inputKey = modelFile.filename;
|
const imageDepsArr = Object.values(deps?.images ?? {});
|
||||||
const [isEditing, setIsEditing] = useState(!modelFile.downloadUrl);
|
if (!deps) {
|
||||||
|
return <Spinner size="md" color="teal.400" />;
|
||||||
if (!modelFile.fileFolder) {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Flex gap={2}>
|
|
||||||
<p>{modelFile.filename}</p>
|
|
||||||
<Tag size={"sm"} colorScheme={"red"}>
|
|
||||||
Model file not found
|
|
||||||
</Tag>
|
|
||||||
</Flex>
|
|
||||||
<Button
|
|
||||||
size={"sm"}
|
|
||||||
leftIcon={<IconRefresh />}
|
|
||||||
width={"fit-content"}
|
|
||||||
borderColor={"red"}
|
|
||||||
borderWidth={errors[inputKey] != null ? 2 : 0}
|
|
||||||
onClick={onClickRefetchModelList}
|
|
||||||
>
|
|
||||||
Fetch model file
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack ml={2}>
|
<Stack gap={5}>
|
||||||
<Flex key={modelFile.filename + modelFile.nodeType} gap={2}>
|
{imageDepsArr.length > 0 && (
|
||||||
<Tag height={"fit-content"} colorScheme={"teal"}>
|
<Stack>
|
||||||
{modelFile.fileFolder}
|
|
||||||
</Tag>
|
|
||||||
|
|
||||||
{modelFile.downloadUrl ? (
|
|
||||||
<HStack>
|
<HStack>
|
||||||
<a
|
<Heading size={"sm"}>Images ({imageDepsArr.length})</Heading>
|
||||||
href={modelFile.infoUrl ?? modelFile.downloadUrl}
|
{/* <Tag colorScheme="yellow">Will be uploaded as url</Tag> */}
|
||||||
target="_blank"
|
<p style={{ color: "GrayText" }}>Will be uploaded as url</p>
|
||||||
>
|
|
||||||
{modelFile.filename}
|
|
||||||
</a>
|
|
||||||
<IconExternalLink size={18} />
|
|
||||||
<IconButton
|
|
||||||
ml={3}
|
|
||||||
aria-label="edit"
|
|
||||||
icon={<IconPencil size={18} />}
|
|
||||||
size={"xs"}
|
|
||||||
onClick={() => setIsEditing(!isEditing)}
|
|
||||||
/>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
) : (
|
{uploadingImage && (
|
||||||
<Stack>
|
|
||||||
<span>
|
<span>
|
||||||
{modelFile.filename} <span>❓</span>
|
<Spinner size="md" color="teal.400" /> Uploading
|
||||||
</span>
|
</span>
|
||||||
</Stack>
|
)}
|
||||||
)}
|
{imageDepsArr.map((image) => (
|
||||||
</Flex>
|
<Stack key={image.filename}>
|
||||||
|
<p>{image.filename}</p>
|
||||||
<Stack style={{ display: isEditing ? "block" : "none" }}>
|
<Image
|
||||||
<Tooltip
|
width={250}
|
||||||
label={"Model download link, supports Huggingface and Civitai"}
|
src={`/workspace/view_media?filename=${image.filename}&isPreview=true&isInput=true`}
|
||||||
>
|
/>
|
||||||
<Input
|
</Stack>
|
||||||
name={inputKey}
|
))}
|
||||||
defaultValue={modelFile.downloadUrl ?? ""}
|
</Stack>
|
||||||
size={"sm"}
|
)}
|
||||||
isInvalid={errors[inputKey] != null}
|
|
||||||
placeholder={"https://civitai.com/api/download/models/324524"}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
<Text color={"red.400"}>{errors[inputKey]}</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import {
|
|||||||
getCivitModelPageUrl,
|
getCivitModelPageUrl,
|
||||||
getHgModelInfoUrlFromDownloadUrl,
|
getHgModelInfoUrlFromDownloadUrl,
|
||||||
} from "../utils/civitUtils";
|
} from "../utils/civitUtils";
|
||||||
// @ts-ignore
|
import { app } from "../utils/comfyapp";
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
|
|
||||||
type LiteNode = {
|
type LiteNode = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -14,13 +13,18 @@ type LiteNode = {
|
|||||||
widgets_values?: Array<string | number>;
|
widgets_values?: Array<string | number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelFile = {
|
export type ModelDep = {
|
||||||
filename: string;
|
name: string;
|
||||||
fileFolder: string | null;
|
folder: string | null;
|
||||||
fileHash: string | null;
|
hash: string | null;
|
||||||
downloadUrl: string | null;
|
url: string | null;
|
||||||
|
// TODO: will be deperecated
|
||||||
|
filename?: string;
|
||||||
|
fileFolder?: string | null;
|
||||||
|
fileHash?: string | null;
|
||||||
|
downloadUrl?: string | null;
|
||||||
// optional info
|
// optional info
|
||||||
infoUrl: string | null;
|
infoUrl?: string | null;
|
||||||
nodeType?: string;
|
nodeType?: string;
|
||||||
inputName?: string;
|
inputName?: string;
|
||||||
};
|
};
|
||||||
@@ -41,12 +45,12 @@ export type DepsResult = {
|
|||||||
nodeRepos: NodeRepo[];
|
nodeRepos: NodeRepo[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DepsResultModel = ModelFile & {
|
export type DepsResultModel = ModelDep & {
|
||||||
length: number;
|
length: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceInfoDeps = {
|
export type WorkspaceInfoDeps = {
|
||||||
models: Record<string, ModelFile>;
|
models: Record<string, ModelDep>;
|
||||||
images: DepsResult["images"];
|
images: DepsResult["images"];
|
||||||
nodeRepos: DepsResult["nodeRepos"];
|
nodeRepos: DepsResult["nodeRepos"];
|
||||||
};
|
};
|
||||||
@@ -58,20 +62,18 @@ async function fetchModelData(
|
|||||||
): Promise<DepsResultModel> {
|
): Promise<DepsResultModel> {
|
||||||
const res =
|
const res =
|
||||||
(await indexdb.models.where("fileName").equals(filename).toArray()) ?? [];
|
(await indexdb.models.where("fileName").equals(filename).toArray()) ?? [];
|
||||||
|
console.log("fetch modeldata", res);
|
||||||
const first = res.at(0);
|
const first = res.at(0);
|
||||||
const modelFile: DepsResultModel = {
|
const modelFile: DepsResultModel = {
|
||||||
filename,
|
name: filename,
|
||||||
nodeType,
|
nodeType,
|
||||||
fileHash: first?.fileHash ?? null,
|
hash: first?.fileHash ?? null,
|
||||||
fileFolder: first?.fileFolder ?? null,
|
folder: first?.fileFolder ?? null,
|
||||||
downloadUrl: first?.civitModelVersionID
|
url: first?.civitModelVersionID
|
||||||
? getCivitModelDownloadUrl(first.civitModelVersionID)
|
? getCivitModelDownloadUrl(first.civitModelVersionID)
|
||||||
: first?.downloadUrl ?? null,
|
: first?.downloadUrl ?? null,
|
||||||
inputName: inputName,
|
inputName: inputName,
|
||||||
length: res.length,
|
length: res.length,
|
||||||
infoUrl: first?.civitModelID
|
|
||||||
? getCivitModelPageUrl(first.civitModelID, first.civitModelVersionID)
|
|
||||||
: getHgModelInfoUrlFromDownloadUrl(first?.downloadUrl ?? "") ?? null,
|
|
||||||
};
|
};
|
||||||
return modelFile;
|
return modelFile;
|
||||||
}
|
}
|
||||||
@@ -95,13 +97,9 @@ export async function extractAndFetchFileNames(
|
|||||||
if (typeof value != "string") return;
|
if (typeof value != "string") return;
|
||||||
// Check if it's a model file
|
// Check if it's a model file
|
||||||
if (modelFileExtensions.some((ext) => value.endsWith(ext))) {
|
if (modelFileExtensions.some((ext) => value.endsWith(ext))) {
|
||||||
// if (curDeps?.models?.[value]) {
|
|
||||||
// modelPromises.push(Promise.resolve(curDeps.models[value]));
|
|
||||||
// } else {
|
|
||||||
modelPromises.push(
|
modelPromises.push(
|
||||||
fetchModelData(value, node.class_type, inputName),
|
fetchModelData(value, node.class_type, inputName),
|
||||||
);
|
);
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
// Check if it's an image file
|
// Check if it's an image file
|
||||||
if (imageFileExtensions.some((ext) => value.endsWith(ext))) {
|
if (imageFileExtensions.some((ext) => value.endsWith(ext))) {
|
||||||
@@ -135,10 +133,7 @@ export async function extractAndFetchFileNames(
|
|||||||
const models = await Promise.all(modelPromises);
|
const models = await Promise.all(modelPromises);
|
||||||
const modelsMap: Record<string, DepsResultModel> = {};
|
const modelsMap: Record<string, DepsResultModel> = {};
|
||||||
models.forEach((model) => {
|
models.forEach((model) => {
|
||||||
// if (model.nodeType) {
|
modelsMap[model.name] = model;
|
||||||
// model.gitRepo = reposMapping[model.nodeType];
|
|
||||||
// }
|
|
||||||
modelsMap[model.filename] = model;
|
|
||||||
});
|
});
|
||||||
const nodeRepos: Record<string, NodeRepo> = {};
|
const nodeRepos: Record<string, NodeRepo> = {};
|
||||||
Object.values(reposMapping).forEach((repo) => {
|
Object.values(reposMapping).forEach((repo) => {
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
// @ts-expect-error
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import { WorkspaceContext } from "../WorkspaceContext";
|
import { WorkspaceContext } from "../WorkspaceContext";
|
||||||
import {
|
import {
|
||||||
changelogsTable,
|
changelogsTable,
|
||||||
userSettingsTable,
|
userSettingsTable,
|
||||||
workflowsTable,
|
workflowsTable,
|
||||||
} from "../db-tables/WorkspaceDB";
|
} from "../db-tables/WorkspaceDB";
|
||||||
import { Tag, Tooltip, useToast } from "@chakra-ui/react";
|
import { Tag, Tooltip } from "@chakra-ui/react";
|
||||||
import { matchShortcut } from "../utils";
|
import { matchShortcut } from "../utils";
|
||||||
import { EShortcutKeys } from "../types/dbTypes";
|
import { EShortcutKeys } from "../types/dbTypes";
|
||||||
import useDebounceFn from "../customHooks/useDebounceFn";
|
import useDebounceFn from "../customHooks/useDebounceFn";
|
||||||
import { deepJsonDiffCheck } from "../utils/deepJsonDiffCheck";
|
import {
|
||||||
import { SHORTCUT_TRIGGER_EVENT } from "../const";
|
COMFYSPACE_TRACKING_FIELD_NAME,
|
||||||
|
SHORTCUT_TRIGGER_EVENT,
|
||||||
|
} from "../const";
|
||||||
|
import { app } from "../utils/comfyapp";
|
||||||
|
|
||||||
export default function AppIsDirtyEventListener() {
|
export default function AppIsDirtyEventListener() {
|
||||||
const { isDirty, setIsDirty, setRoute, saveCurWorkflow } =
|
const {
|
||||||
useContext(WorkspaceContext);
|
isDirty,
|
||||||
|
setIsDirty,
|
||||||
|
setRoute,
|
||||||
|
saveCurWorkflow,
|
||||||
|
setCurFlowIDAndName,
|
||||||
|
} = useContext(WorkspaceContext);
|
||||||
const isDirtRef = useRef(isDirty);
|
const isDirtRef = useRef(isDirty);
|
||||||
const [isOutdated, setIsOutdated] = useState(false);
|
const [isOutdated, setIsOutdated] = useState(false);
|
||||||
const toast = useToast();
|
|
||||||
const autoSaveTimer = useRef<number | null>(null);
|
const autoSaveTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,7 +32,7 @@ export default function AppIsDirtyEventListener() {
|
|||||||
}, [isDirty]);
|
}, [isDirty]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shortcutListener = (event: KeyboardEvent) => {
|
const keydownListener = (event: KeyboardEvent) => {
|
||||||
if (document.visibilityState === "hidden") return;
|
if (document.visibilityState === "hidden") return;
|
||||||
const matchResult = matchShortcut(event);
|
const matchResult = matchShortcut(event);
|
||||||
if (matchResult) {
|
if (matchResult) {
|
||||||
@@ -60,26 +65,17 @@ export default function AppIsDirtyEventListener() {
|
|||||||
debounceOnIsDirty();
|
debounceOnIsDirty();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const originalOnAfterChange = app.canvas.onAfterChange;
|
|
||||||
app.graph.onAfterChange = function () {
|
|
||||||
// Call the original onAfterChange method
|
|
||||||
originalOnAfterChange?.apply(this, arguments);
|
|
||||||
debounceOnIsDirty();
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalOnConfigure = app.graph.onConfigure;
|
const originalOnConfigure = app.graph.onConfigure;
|
||||||
app.graph.onConfigure = function () {
|
app.graph.onConfigure = function () {
|
||||||
originalOnConfigure?.apply(this, arguments);
|
originalOnConfigure?.apply(this, arguments);
|
||||||
// delay check diff json cuz it takes time for graph.serialize() to settle up and return the correct json
|
// delay check diff json cuz it takes time for graph.serialize() to settle up and return the correct json
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const deepCheck = deepJsonDiffCheck(
|
const id = app.graph.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.id;
|
||||||
app.graph.serialize(),
|
if (id == null || id != workflowsTable?.curWorkflow?.id) {
|
||||||
JSON.parse(workflowsTable?.curWorkflow?.json ?? "{}"),
|
setCurFlowIDAndName(null);
|
||||||
);
|
|
||||||
if (!deepCheck) {
|
|
||||||
debounceOnIsDirty();
|
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
@@ -92,7 +88,7 @@ export default function AppIsDirtyEventListener() {
|
|||||||
debounceOnIsDirty();
|
debounceOnIsDirty();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", shortcutListener);
|
document.addEventListener("keydown", keydownListener);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userSettingsTable?.settings?.autoSave &&
|
userSettingsTable?.settings?.autoSave &&
|
||||||
@@ -105,7 +101,7 @@ export default function AppIsDirtyEventListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", shortcutListener);
|
document.removeEventListener("keydown", keydownListener);
|
||||||
autoSaveTimer.current && clearInterval(autoSaveTimer.current);
|
autoSaveTimer.current && clearInterval(autoSaveTimer.current);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -132,6 +128,10 @@ export default function AppIsDirtyEventListener() {
|
|||||||
|
|
||||||
const onIsDirty = async () => {
|
const onIsDirty = async () => {
|
||||||
if (workflowsTable?.curWorkflow?.saveLock || isDirtRef.current) return;
|
if (workflowsTable?.curWorkflow?.saveLock || isDirtRef.current) return;
|
||||||
|
const graph = app.graph.serialize();
|
||||||
|
if (JSON.stringify(graph) === workflowsTable?.curWorkflow?.json) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsDirty(true);
|
setIsDirty(true);
|
||||||
if (
|
if (
|
||||||
userSettingsTable?.settings?.autoSave &&
|
userSettingsTable?.settings?.autoSave &&
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
IconFolder,
|
IconFolder,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
IconPlus,
|
|
||||||
IconTriangleInvertedFilled,
|
IconTriangleInvertedFilled,
|
||||||
IconLock,
|
IconLock,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
@@ -22,10 +21,10 @@ import EditFlowName from "../components/EditFlowName";
|
|||||||
import { WorkspaceContext } from "../WorkspaceContext";
|
import { WorkspaceContext } from "../WorkspaceContext";
|
||||||
import { PanelPosition } from "../types/dbTypes";
|
import { PanelPosition } from "../types/dbTypes";
|
||||||
import "./Topbar.css";
|
import "./Topbar.css";
|
||||||
import { SharedTopbarButton } from "../share/SharedTopbarButton";
|
|
||||||
import VersionNameTopbar from "./VersionNameTopbar";
|
import VersionNameTopbar from "./VersionNameTopbar";
|
||||||
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||||
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
||||||
|
import TopbarNewWorkflowButton from "./TopbarNewWorkflowButton";
|
||||||
const AppIsDirtyEventListener = lazy(() => import("./AppIsDirtyEventListener"));
|
const AppIsDirtyEventListener = lazy(() => import("./AppIsDirtyEventListener"));
|
||||||
const ModelManagerTopbar = lazy(
|
const ModelManagerTopbar = lazy(
|
||||||
() => import("../model-manager/topbar/ModelManagerTopbar"),
|
() => import("../model-manager/topbar/ModelManagerTopbar"),
|
||||||
@@ -37,14 +36,8 @@ interface Props {
|
|||||||
setCurFlowName: (newName: string) => void;
|
setCurFlowName: (newName: string) => void;
|
||||||
}
|
}
|
||||||
export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
||||||
const {
|
const { isDirty, saveCurWorkflow, setRoute, curFlowID, route } =
|
||||||
isDirty,
|
useContext(WorkspaceContext);
|
||||||
loadNewWorkflow,
|
|
||||||
saveCurWorkflow,
|
|
||||||
setRoute,
|
|
||||||
curFlowID,
|
|
||||||
route,
|
|
||||||
} = useContext(WorkspaceContext);
|
|
||||||
const [positionStyle, setPositionStyle] = useState<PanelPosition>();
|
const [positionStyle, setPositionStyle] = useState<PanelPosition>();
|
||||||
const updatePanelPosition: (
|
const updatePanelPosition: (
|
||||||
position?: PanelPosition,
|
position?: PanelPosition,
|
||||||
@@ -116,21 +109,7 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
|||||||
<Suspense fallback={<div style={{ width: "60px" }} />}>
|
<Suspense fallback={<div style={{ width: "60px" }} />}>
|
||||||
<ModelManagerTopbar />
|
<ModelManagerTopbar />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Tooltip label="New workflow">
|
<TopbarNewWorkflowButton />
|
||||||
<Button
|
|
||||||
size={"sm"}
|
|
||||||
variant={"outline"}
|
|
||||||
colorScheme="teal"
|
|
||||||
aria-label="new workflow"
|
|
||||||
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
|
||||||
onClick={() => loadNewWorkflow()}
|
|
||||||
px={1}
|
|
||||||
>
|
|
||||||
<HStack gap={0}>
|
|
||||||
<IconPlus size={16} color={"white"} />
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<EditFlowName
|
<EditFlowName
|
||||||
isDirty={isDirty}
|
isDirty={isDirty}
|
||||||
displayName={curFlowName ?? ""}
|
displayName={curFlowName ?? ""}
|
||||||
@@ -172,7 +151,6 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div style={{ width: 1 }} />
|
<div style={{ width: 1 }} />
|
||||||
)}
|
)}
|
||||||
<SharedTopbarButton />
|
|
||||||
<VersionNameTopbar />
|
<VersionNameTopbar />
|
||||||
<AppIsDirtyEventListener />
|
<AppIsDirtyEventListener />
|
||||||
<IconGripVertical
|
<IconGripVertical
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Tooltip, Button } from "@chakra-ui/react";
|
||||||
|
import { IconDeviceFloppy, IconPlus } from "@tabler/icons-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { WorkspaceContext } from "../WorkspaceContext";
|
||||||
|
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
||||||
|
import { app } from "../utils/comfyapp";
|
||||||
|
import { workflowsTable } from "../db-tables/WorkspaceDB";
|
||||||
|
|
||||||
|
export default function TopbarNewWorkflowButton() {
|
||||||
|
const { loadNewWorkflow, setCurFlowIDAndName, curFlowID } =
|
||||||
|
useContext(WorkspaceContext);
|
||||||
|
const saveNewWorkflow = async () => {
|
||||||
|
const graph = JSON.stringify(app.graph.serialize());
|
||||||
|
const workflowName = prompt("Enter workflow name", "Untitled");
|
||||||
|
if (!workflowName) return;
|
||||||
|
const flow = await workflowsTable?.createFlow({
|
||||||
|
json: graph,
|
||||||
|
name: workflowName,
|
||||||
|
});
|
||||||
|
flow && setCurFlowIDAndName(flow);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Tooltip label={curFlowID ? "New workflow" : "Save workflow"}>
|
||||||
|
<Button
|
||||||
|
size={"sm"}
|
||||||
|
variant={"outline"}
|
||||||
|
colorScheme="teal"
|
||||||
|
aria-label="new workflow"
|
||||||
|
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
||||||
|
onClick={() => (curFlowID ? loadNewWorkflow() : saveNewWorkflow())}
|
||||||
|
px={1}
|
||||||
|
>
|
||||||
|
{curFlowID ? (
|
||||||
|
<IconPlus size={16} color={"white"} />
|
||||||
|
) : (
|
||||||
|
<IconDeviceFloppy size={16} color={"white"} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,17 +10,17 @@ export interface SortableItem {
|
|||||||
export interface Workflow extends SortableItem {
|
export interface Workflow extends SortableItem {
|
||||||
id: string;
|
id: string;
|
||||||
json: string;
|
json: string;
|
||||||
lastSavedJson?: string; // TODO will be deprecated
|
|
||||||
name: string;
|
name: string;
|
||||||
|
coverMediaPath?: string | null;
|
||||||
|
saveLock?: boolean;
|
||||||
|
cloudID?: string | null;
|
||||||
createTime: number;
|
createTime: number;
|
||||||
filePath?: string; // TODO will be deprecated
|
|
||||||
|
// will be deprecated
|
||||||
privacy?: WorkflowPrivacy;
|
privacy?: WorkflowPrivacy;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
parentFolderID?: string | null; //TODO remove undefined, use null only
|
parentFolderID?: string | null; //TODO remove undefined, use null only
|
||||||
coverMediaPath?: string;
|
|
||||||
cloudID?: string; // TODO will be deprecated
|
|
||||||
cloudOrigin?: string;
|
cloudOrigin?: string;
|
||||||
saveLock?: boolean;
|
|
||||||
latestImage?: string;
|
latestImage?: string;
|
||||||
topFieldsConfig?: TopFieldType[];
|
topFieldsConfig?: TopFieldType[];
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,7 @@ export type WorkflowVersion = {
|
|||||||
workflowID: string;
|
workflowID: string;
|
||||||
json: string;
|
json: string;
|
||||||
createTime: number;
|
createTime: number;
|
||||||
|
api_prompt: string | null;
|
||||||
cloudID?: string;
|
cloudID?: string;
|
||||||
cloudOrigin?: string;
|
cloudOrigin?: string;
|
||||||
authorID?: string;
|
authorID?: string;
|
||||||
@@ -131,7 +132,6 @@ export type Media = {
|
|||||||
workflowJSON?: string;
|
workflowJSON?: string;
|
||||||
createTime: number;
|
createTime: number;
|
||||||
localPath: string;
|
localPath: string;
|
||||||
format: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-52
@@ -1,22 +1,7 @@
|
|||||||
import { deleteFile, updateFile } from "./Api";
|
|
||||||
import { ESortTypes } from "./RecentFilesDrawer/types";
|
import { ESortTypes } from "./RecentFilesDrawer/types";
|
||||||
import { workflowsTable, userSettingsTable } from "./db-tables/WorkspaceDB";
|
import { userSettingsTable } from "./db-tables/WorkspaceDB";
|
||||||
import {
|
import { Workflow, EShortcutKeys, SortableItem } from "./types/dbTypes";
|
||||||
generateFilePathAbsolute,
|
import { app } from "./utils/comfyapp";
|
||||||
saveJsonFileMyWorkflows,
|
|
||||||
} from "./db-tables/DiskFileUtils";
|
|
||||||
import {
|
|
||||||
Workflow,
|
|
||||||
EShortcutKeys,
|
|
||||||
EOtherKeys,
|
|
||||||
SortableItem,
|
|
||||||
} from "./types/dbTypes";
|
|
||||||
// @ts-expect-error ComfyUI import
|
|
||||||
import { app } from "/scripts/app.js";
|
|
||||||
import {
|
|
||||||
COMFYSPACE_TRACKING_FIELD_NAME,
|
|
||||||
LEGACY_COMFYSPACE_TRACKING_FIELD_NAME,
|
|
||||||
} from "./const";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
declare let LiteGraph: any, LGraph: any, LGraphCanvas: any;
|
declare let LiteGraph: any, LGraph: any, LGraphCanvas: any;
|
||||||
@@ -156,22 +141,6 @@ export function sortFlows(
|
|||||||
|
|
||||||
return copyFlows;
|
return copyFlows;
|
||||||
}
|
}
|
||||||
export async function validateOrSaveAllJsonFileMyWorkflows(
|
|
||||||
deleteEmptyFolder = false,
|
|
||||||
) {
|
|
||||||
const twoWaySync = await userSettingsTable?.getSetting("twoWaySync");
|
|
||||||
if (twoWaySync) return; // disable this function in two way sync mode
|
|
||||||
const flowList = (await workflowsTable?.listAll()) ?? [];
|
|
||||||
for (const workflow of flowList) {
|
|
||||||
const fullPath = await generateFilePathAbsolute(workflow);
|
|
||||||
if (workflow.filePath != fullPath) {
|
|
||||||
// file path changed
|
|
||||||
workflow.filePath != null &&
|
|
||||||
(await deleteFile(workflow.filePath, deleteEmptyFolder));
|
|
||||||
await saveJsonFileMyWorkflows(workflow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sortFileItem = <T extends SortableItem>(
|
export const sortFileItem = <T extends SortableItem>(
|
||||||
items: T[],
|
items: T[],
|
||||||
@@ -341,21 +310,3 @@ export const openWorkflowInNewTab = (workflowID: string) => {
|
|||||||
const newHash = generateUrlHashWithFlowId(workflowID);
|
const newHash = generateUrlHashWithFlowId(workflowID);
|
||||||
window.open(`${window.location.origin}/#${newHash}`);
|
window.open(`${window.location.origin}/#${newHash}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function rewriteAllLocalFiles() {
|
|
||||||
const flowList = (await workflowsTable?.listAll()) ?? [];
|
|
||||||
for (const workflow of flowList) {
|
|
||||||
try {
|
|
||||||
const fullPath = await generateFilePathAbsolute(workflow);
|
|
||||||
const flow = JSON.parse(workflow.json);
|
|
||||||
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {
|
|
||||||
id: workflow.id,
|
|
||||||
name: workflow.name,
|
|
||||||
};
|
|
||||||
delete flow.extra[LEGACY_COMFYSPACE_TRACKING_FIELD_NAME];
|
|
||||||
fullPath && (await updateFile(fullPath, JSON.stringify(flow)));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const app = window.app;
|
export let app: any | null = null;
|
||||||
const api_base = location.pathname.split("/").slice(0, -1).join("/");
|
const api_base = location.pathname.split("/").slice(0, -1).join("/");
|
||||||
|
|
||||||
export const api = import(api_base + "/scripts/app.js").then((api) => {
|
export let api: any | null = null;
|
||||||
return api;
|
|
||||||
|
import(api_base + "/scripts/api.js").then((apiJs) => {
|
||||||
|
api = apiJs?.api;
|
||||||
|
});
|
||||||
|
|
||||||
|
import(api_base + "/scripts/app.js").then((appJs) => {
|
||||||
|
app = appJs?.app;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
ScanLocalFolder,
|
ScanLocalFolder,
|
||||||
scanLocalFiles,
|
scanLocalFiles,
|
||||||
} from "../apis/TwowaySyncApi";
|
} from "../apis/TwowaySyncApi";
|
||||||
import { isFolder, userSettingsTable } from "../db-tables/WorkspaceDB";
|
import { isFolder, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||||
import { indexdb } from "../db-tables/indexdb";
|
import { indexdb } from "../db-tables/indexdb";
|
||||||
import { Folder, Workflow } from "../types/dbTypes";
|
import { Folder, Workflow } from "../types/dbTypes";
|
||||||
import { sortFileItem } from "../utils";
|
import { sortFileItem } from "../utils";
|
||||||
@@ -34,21 +34,33 @@ export async function scanMyWorkflowsDir(
|
|||||||
return folder;
|
return folder;
|
||||||
} else {
|
} else {
|
||||||
// is workflow
|
// is workflow
|
||||||
const existingWorkflow = (await indexdb.workflows?.get(file.id)) ?? {
|
// const existingWorkflow = (await indexdb.workflows
|
||||||
createTime: Date.now(),
|
// ?.get(file.id)
|
||||||
updateTime: Date.now(),
|
// .catch((err) => {
|
||||||
};
|
// console.error("Error getting workflow from indexdb", err);
|
||||||
|
// return null;
|
||||||
|
// })) ?? {
|
||||||
|
// createTime: Date.now(),
|
||||||
|
// updateTime: Date.now(),
|
||||||
|
// };
|
||||||
|
|
||||||
|
if (workflowsTable) {
|
||||||
|
workflowsTable.workflowIDPathMap[file.id] = relPath;
|
||||||
|
}
|
||||||
|
|
||||||
const newWorkflow: Workflow = {
|
const newWorkflow: Workflow = {
|
||||||
...existingWorkflow,
|
// ...existingWorkflow,
|
||||||
id: file.id,
|
id: file.id,
|
||||||
json: file.json ?? "{}",
|
json: file.json ?? "{}",
|
||||||
|
cloudID: file.cloudID,
|
||||||
|
coverMediaPath: file.coverMediaPath,
|
||||||
|
saveLock: file.saveLock,
|
||||||
name: fileName.replace(/\.json$/, ""),
|
name: fileName.replace(/\.json$/, ""),
|
||||||
parentFolderID: parentRelPath,
|
parentFolderID: parentRelPath,
|
||||||
createTime: file.createTime,
|
createTime: file.createTime,
|
||||||
// setting updateTime will result latestVersionChcek() always fail if in
|
// setting updateTime will result latestVersionChcek() always fail if in
|
||||||
// workflow.get() not pull updateTime from file
|
// workflow.get() not pull updateTime from file
|
||||||
// updateTime: file.updateTime,
|
updateTime: file.updateTime,
|
||||||
};
|
};
|
||||||
return newWorkflow;
|
return newWorkflow;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user