Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dc3aa557c | ||
|
|
316f5c1def | ||
|
|
d80e791547 | ||
|
|
b37e9b6449 | ||
|
|
87b82d6f2d | ||
|
|
ce56822a69 | ||
|
|
1cf711e4fa | ||
|
|
b08c4e0079 | ||
|
|
b2052ff205 | ||
|
|
fb5fac0621 | ||
|
|
7b64353aba |
@@ -1,20 +0,0 @@
|
||||
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,12 +4,13 @@ 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)!
|
||||
|
||||
[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
|
||||
[New!] we're building a comfyui custom nodes library wiki like Pypi: https://www.nodecafe.org/
|
||||
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
|
||||
|
||||
[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">
|
||||
|
||||
## Installation
|
||||
@@ -20,14 +21,15 @@ Simply search for “ComfyUI Workspace Manager”, install, and restart ComfyUI.
|
||||
|
||||
### [Option 2] via git
|
||||
|
||||
Like any other custom node installation, in your ComfyUI root folder run command and restart your ComfyUI.
|
||||
Like any other custom node installation, in your ComfyUI root folder
|
||||
|
||||
```
|
||||
cd custom_nodes && git clone https://github.com/11cafe/comfyui-workspace-manager.git
|
||||
```
|
||||
|
||||
‼️ 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.
|
||||
and restart your ComfyUI.
|
||||
|
||||
It is recommended to do git clone than downloading zip folder, because you can get latest updates by `git pull`!
|
||||
|
||||
## Features
|
||||
|
||||
@@ -39,7 +41,7 @@ cd custom_nodes && git clone https://github.com/11cafe/comfyui-workspace-manager
|
||||
- 📂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).
|
||||
- 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 slowing)
|
||||
- **Enable/disable autosave workflow** (please disable autosave if you are experiencing UI lagging)
|
||||
- You can move files using OS File Explorer and it will be **two-way synced** with workspace.
|
||||
- 🕛**Version control** of workflow, create/switch versions
|
||||
- Bulk import workflows into your workspace
|
||||
@@ -61,8 +63,17 @@ 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)
|
||||
|
||||
## Updates
|
||||
**🦩v2.1 New gallery! Inspect image metadata, search image by prompt, model/lora used**
|
||||
# <<<<<<< HEAD
|
||||
|
||||
🚧🔴 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">
|
||||
|
||||
@@ -70,9 +81,13 @@ 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">
|
||||
|
||||
**🦄v2.0 Two-way sync with local folder, models manager support external models path like Automatic1111, cloud sync workflows**
|
||||
|
||||
v2.0 demo: https://youtu.be/7Hht_BMe844
|
||||
**🦄v2.0 Two-way sync with local folder, select model install path, 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!
|
||||
- **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**
|
||||
|
||||
@@ -82,7 +97,7 @@ v2.0 demo: https://youtu.be/7Hht_BMe844
|
||||
|
||||
<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">
|
||||
|
||||
## Usage
|
||||
<img width="460" alt="gallery" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/2b7886c9-2d4a-46c6-adac-429720f74f11">
|
||||
|
||||
switch between flows:
|
||||
|
||||
@@ -96,6 +111,10 @@ 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">
|
||||
|
||||
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:
|
||||
|
||||
<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">
|
||||
|
||||
+15
-13
@@ -5,18 +5,17 @@ import folder_paths
|
||||
import shutil
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import subprocess # don't remove this
|
||||
from urllib.parse import urlparse
|
||||
import subprocess
|
||||
import os
|
||||
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.file_sync_service import *
|
||||
from .service.db_service import get_my_workflows_dir
|
||||
from .service.node_service import *
|
||||
try:
|
||||
from send2trash import send2trash
|
||||
except ImportError:
|
||||
send2trash = None
|
||||
|
||||
WEB_DIRECTORY = "entry"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
@@ -113,28 +112,31 @@ async def update_file(request):
|
||||
await asyncio.to_thread(write_json_to_file, json_str)
|
||||
return web.Response(text="File updated successfully")
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/workspace/delete_file")
|
||||
async def delete_file(request):
|
||||
data = await request.json()
|
||||
file_path = data['file_path']
|
||||
delete_empty_folder = data['deleteEmptyFolder']
|
||||
|
||||
def sync_delete_file(file_path):
|
||||
def sync_delete_file(file_path, delete_empty_folder):
|
||||
my_workflows_dir = get_my_workflows_dir()
|
||||
full_path = os.path.join(my_workflows_dir, file_path)
|
||||
|
||||
if os.path.exists(full_path):
|
||||
if send2trash:
|
||||
send2trash(full_path)
|
||||
os.remove(full_path)
|
||||
directory = os.path.dirname(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:
|
||||
os.remove(full_path)
|
||||
print("❌⛔️send2trash is not available. Deleting file permanently. Please `pip install send2trash`")
|
||||
|
||||
return "File deleted successfully"
|
||||
return "File deleted successfully"
|
||||
else:
|
||||
return "File not found"
|
||||
|
||||
# Run the synchronous file operation in a separate thread
|
||||
response_text = await asyncio.to_thread(sync_delete_file, file_path)
|
||||
response_text = await asyncio.to_thread(sync_delete_file, file_path, delete_empty_folder)
|
||||
|
||||
if response_text == "File not found":
|
||||
return web.Response(text=response_text, status=404)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[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 = ""
|
||||
@@ -1 +0,0 @@
|
||||
send2trash
|
||||
@@ -136,11 +136,7 @@ def read_workflow_file(path, id):
|
||||
|
||||
def check_and_update_workflow_id(file_path, seen_ids):
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
try:
|
||||
json_data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error reading JSON from {file_path}: {e}")
|
||||
return
|
||||
json_data = json.load(f)
|
||||
if (
|
||||
"extra" in json_data
|
||||
and "workspace_info" in json_data["extra"]
|
||||
@@ -176,10 +172,7 @@ def dedupe_workflow_ids():
|
||||
if not file.endswith(".json"):
|
||||
continue
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
check_and_update_workflow_id(file_path, seen_ids)
|
||||
except Exception as e:
|
||||
print(f"Error while processing file {file_path}: {e}")
|
||||
check_and_update_workflow_id(file_path, seen_ids)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/workspace/deduplicate_workflow_ids')
|
||||
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']
|
||||
video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv']
|
||||
API_URL = 'https://www.nodecafe.co/api/image/upload'
|
||||
API_URL = 'https://www.nodecafe.org/api/image/upload'
|
||||
def view_media(filename, isPreview = False, isInput = False):
|
||||
if not filename:
|
||||
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)
|
||||
|
||||
# Early open shelve for better performance
|
||||
with shelve.open(FILE_HASH_DICT_PATH) as file_hash_dict_early_open:
|
||||
with shelve.open(FILE_HASH_DICT_PATH) as file_hash_dict:
|
||||
print(f"Workspace manager - Openning file hash dict")
|
||||
|
||||
file_list = []
|
||||
|
||||
@@ -3,8 +3,7 @@ from aiohttp import web
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
import glob
|
||||
from threading import Lock
|
||||
import server
|
||||
import uuid
|
||||
@@ -42,7 +41,7 @@ def folder_handle(path, recursive, metaInfoOnly, fileList=None):
|
||||
if recursive:
|
||||
folder_handle(item_path, recursive, metaInfoOnly, fileList)
|
||||
except Exception as e:
|
||||
logging.error(f"Error scan workflow {item_path}: {e}, {traceback.format_exc()}")
|
||||
print(f"Error scanning item {item}: {e}")
|
||||
return fileList
|
||||
|
||||
def file_handle(name, fileList, file_path, metaInfoOnly):
|
||||
@@ -52,9 +51,6 @@ def file_handle(name, fileList, file_path, metaInfoOnly):
|
||||
createTime, updateTime = getFileCreateTime(file_path)
|
||||
workspace_info = json_data.get('extra', {}).get('workspace_info', {})
|
||||
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
|
||||
if 'id' not in workspace_info:
|
||||
if 'extra' not in json_data:
|
||||
@@ -69,9 +65,6 @@ def file_handle(name, fileList, file_path, metaInfoOnly):
|
||||
'name': name,
|
||||
'type': "workflow",
|
||||
'id': workflow_id,
|
||||
'saveLock': saveLock,
|
||||
'cloudID': cloudID,
|
||||
'coverMediaPath': coverMediaPath,
|
||||
'createTime': createTime,
|
||||
'updateTime': updateTime
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import os
|
||||
from pathlib import Path
|
||||
import server
|
||||
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')
|
||||
async def create_folder(request):
|
||||
@@ -35,11 +31,7 @@ def delete_folder_sync(reqJson):
|
||||
folder_path = reqJson.get('path')
|
||||
folder_path = os.path.join(get_my_workflows_dir(), folder_path)
|
||||
try:
|
||||
if send2trash:
|
||||
send2trash(folder_path)
|
||||
else:
|
||||
shutil.rmtree(folder_path)
|
||||
print("❌⛔️send2trash is not available. Deleting file permanently. Please `pip install send2trash`")
|
||||
shutil.rmtree(folder_path)
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"setupGithooks": "node ../scripts/setupGitHooks"
|
||||
"setupGithooks": "node ../scripts/setupGithooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
|
||||
+13
-25
@@ -1,22 +1,10 @@
|
||||
import { Table } from "./db-tables/WorkspaceDB";
|
||||
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> {
|
||||
console.warn("[workspace deprecated] getDB is deprecated", table);
|
||||
try {
|
||||
const response = await fetchApi(`/workspace/get_db?table=${table}`);
|
||||
const response = await fetch(`/workspace/get_db?table=${table}`);
|
||||
if (!response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -30,7 +18,7 @@ export async function getDB(table: Table): Promise<string | undefined> {
|
||||
|
||||
export async function saveDB(table: Table, jsonData: string) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/save_db", {
|
||||
const response = await fetch("/workspace/save_db", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -46,7 +34,7 @@ export async function saveDB(table: Table, jsonData: string) {
|
||||
|
||||
export async function updateFile(file_path: string, jsonData: string) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/update_file", {
|
||||
const response = await fetch("/workspace/update_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -66,7 +54,7 @@ export async function updateFile(file_path: string, jsonData: string) {
|
||||
|
||||
export async function deleteFile(file_path: string, deleteEmptyFolder = false) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/delete_file", {
|
||||
const response = await fetch("/workspace/delete_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -85,7 +73,7 @@ export async function deleteFile(file_path: string, deleteEmptyFolder = false) {
|
||||
|
||||
export async function listBackup(dir: string) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/list_backup", {
|
||||
const response = await fetch("/workspace/list_backup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -103,7 +91,7 @@ export async function listBackup(dir: string) {
|
||||
|
||||
export async function getSystemDir(root?: string) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/get_system_dir", {
|
||||
const response = await fetch("/workspace/get_system_dir", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -121,7 +109,7 @@ export async function getSystemDir(root?: string) {
|
||||
|
||||
export async function openWorkflowsFolder() {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/open_workflow_file_browser", {
|
||||
const response = await fetch("/workspace/open_workflow_file_browser", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -135,7 +123,7 @@ export async function openWorkflowsFolder() {
|
||||
}
|
||||
|
||||
export async function fetchMyWorkflowsDir() {
|
||||
const resp = await fetchApi("/workspace/get_my_workflows_dir");
|
||||
const resp = await fetch("/workspace/get_my_workflows_dir");
|
||||
const res = (await resp.json()) as {
|
||||
path?: string;
|
||||
error?: string;
|
||||
@@ -149,7 +137,7 @@ export async function fetchMyWorkflowsDir() {
|
||||
|
||||
export async function getAllModelsList() {
|
||||
try {
|
||||
const response = await fetchApi("/model_manager/get_model_list", {
|
||||
const response = await fetch("/model_manager/get_model_list", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -164,7 +152,7 @@ export async function getAllModelsList() {
|
||||
|
||||
export async function getAllFoldersList() {
|
||||
try {
|
||||
const response = await fetchApi("/model_manager/get_folder_list", {
|
||||
const response = await fetch("/model_manager/get_folder_list", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -179,7 +167,7 @@ export async function getAllFoldersList() {
|
||||
|
||||
export async function deleteLocalDiskFolder(folderPath: string) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/delete_folder", {
|
||||
const response = await fetch("/workspace/delete_folder", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -197,7 +185,7 @@ export async function deleteLocalDiskFolder(folderPath: string) {
|
||||
|
||||
export async function cancelDownload(savePath: string) {
|
||||
try {
|
||||
const response = await fetchApi("/model_manager/cancel_installation", {
|
||||
const response = await fetch("/model_manager/cancel_installation", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -218,7 +206,7 @@ export async function copyFlowsToNewDirectory(
|
||||
dstDir: string,
|
||||
) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/copy_json_files", {
|
||||
const response = await fetch("/workspace/copy_json_files", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
+212
-103
@@ -1,4 +1,15 @@
|
||||
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
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 {
|
||||
loadDBs,
|
||||
@@ -25,25 +36,23 @@ const RecentFilesDrawer = React.lazy(
|
||||
);
|
||||
const GalleryModal = React.lazy(() => import("./gallery/GalleryModal"));
|
||||
import { IconExternalLink } from "@tabler/icons-react";
|
||||
import {
|
||||
COMFYSPACE_TRACKING_FIELD_NAME,
|
||||
DRAWER_Z_INDEX,
|
||||
UPGRADE_TO_2WAY_SYNC_KEY,
|
||||
} from "./const";
|
||||
import { DRAWER_Z_INDEX, UPGRADE_TO_2WAY_SYNC_KEY } from "./const";
|
||||
import ServerEventListener from "./model-manager/hooks/ServerEventListener";
|
||||
import { nanoid } from "nanoid";
|
||||
import { WorkspaceRoute } from "./types/types";
|
||||
import { useStateRef } from "./customHooks/useStateRef";
|
||||
import { indexdb } from "./db-tables/indexdb";
|
||||
import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm";
|
||||
import { api, app } from "./utils/comfyapp";
|
||||
import { fetchApi } from "./Api";
|
||||
import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck";
|
||||
import {
|
||||
OPEN_TAB_EVENT,
|
||||
tabDataManager,
|
||||
} from "./topbar/multipleTabs/TabDataManager";
|
||||
|
||||
export default function App() {
|
||||
const [curFlowName, setCurFlowName] = useState<string | null>(null);
|
||||
const [route, setRoute] = useState<WorkspaceRoute>("root");
|
||||
const [loadingDB, setLoadingDB] = useState(true);
|
||||
const [flowID, setFlowID] = useState<string | null>(null);
|
||||
const curFlowID = useRef<string | null>(null);
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const workspaceContainerRef = useRef(null);
|
||||
@@ -52,10 +61,10 @@ export default function App() {
|
||||
const developmentEnvLoadFirst = useRef(false);
|
||||
const toast = useToast();
|
||||
const [curVersion, setCurVersion] = useStateRef<WorkflowVersion | null>(null);
|
||||
const saveCurWorkflow = useCallback(async (force = false) => {
|
||||
const curWorkflow = workflowsTable?.curWorkflow;
|
||||
if (curWorkflow) {
|
||||
if (!force && workflowsTable?.curWorkflow?.saveLock) {
|
||||
const saveCurWorkflow = useCallback(async (newGraphJson?: string) => {
|
||||
const flowId = workflowsTable?.curWorkflow?.id;
|
||||
if (flowId) {
|
||||
if (workflowsTable?.curWorkflow?.saveLock) {
|
||||
toast({
|
||||
title: "The workflow is locked and cannot be saved",
|
||||
status: "warning",
|
||||
@@ -63,13 +72,14 @@ export default function App() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const graphJson = JSON.stringify(app.graph.serialize());
|
||||
!newGraphJson && setIsDirty(false);
|
||||
const graphJson = newGraphJson || JSON.stringify(app.graph.serialize());
|
||||
await Promise.all([
|
||||
workflowsTable?.updateFlow(curWorkflow.id, {
|
||||
workflowsTable?.updateFlow(flowId, {
|
||||
json: graphJson,
|
||||
}),
|
||||
changelogsTable?.create({
|
||||
workflowID: curWorkflow.id,
|
||||
workflowID: flowId,
|
||||
json: graphJson,
|
||||
isAutoSave: false,
|
||||
}),
|
||||
@@ -81,7 +91,6 @@ export default function App() {
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
setIsDirty(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -116,9 +125,7 @@ export default function App() {
|
||||
// curID null is when you deleted current workflow
|
||||
const id = workflow?.id ?? null;
|
||||
workflowsTable?.updateCurWorkflow(workflow);
|
||||
curFlowID.current = id;
|
||||
setFlowID(id);
|
||||
setCurFlowName(workflow?.name ?? "");
|
||||
if (id == null) {
|
||||
document.title = "ComfyUI";
|
||||
window.location.hash = "";
|
||||
@@ -148,67 +155,80 @@ export default function App() {
|
||||
}
|
||||
setLoadingDB(false);
|
||||
|
||||
const urlWorkflowID = getWorkflowIdInUrlHash();
|
||||
if (urlWorkflowID == null) {
|
||||
const latestWf = localStorage.getItem("workflow");
|
||||
if (latestWf) {
|
||||
const workflow = JSON.parse(latestWf);
|
||||
const id = workflow.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.id;
|
||||
if (id) {
|
||||
const flow = await workflowsTable?.get(id);
|
||||
flow && setCurFlowIDAndName(flow);
|
||||
if (!flow?.saveLock && flow && flow.json != latestWf) {
|
||||
setIsDirty(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadWorkflowIDImpl(urlWorkflowID);
|
||||
let latestWfID = null;
|
||||
latestWfID = getWorkflowIdInUrlHash();
|
||||
if (latestWfID == null) {
|
||||
latestWfID = localStorage.getItem("curFlowID");
|
||||
}
|
||||
fetchApi("/workspace/deduplicate_workflow_ids");
|
||||
if (latestWfID) {
|
||||
loadWorkflowIDImpl(latestWfID!, null, true);
|
||||
}
|
||||
fetch("/workspace/deduplicate_workflow_ids");
|
||||
const twoway = await userSettingsTable?.getSetting("twoWaySync");
|
||||
|
||||
if (!twoway) {
|
||||
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();
|
||||
}
|
||||
!twoway &&
|
||||
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,
|
||||
},
|
||||
colorScheme: "red",
|
||||
},
|
||||
],
|
||||
{
|
||||
closeOnOverlayClick: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => {
|
||||
const checkIsDirty = () => {
|
||||
if (workflowsTable?.curWorkflow?.id == null) return false;
|
||||
const curflow = workflowsTable?.curWorkflow;
|
||||
if (curflow == null) return false;
|
||||
if (curflow.json == null) return true;
|
||||
const graphJson = app.graph.serialize() ?? {};
|
||||
let lastSaved = {};
|
||||
try {
|
||||
lastSaved = curflow?.json ? JSON.parse(curflow?.json) : {};
|
||||
} catch (e) {
|
||||
console.error("error parsing json", e);
|
||||
}
|
||||
const equal = deepJsonDiffCheck(graphJson, lastSaved);
|
||||
return !equal;
|
||||
};
|
||||
|
||||
const loadWorkflowIDImpl = async (
|
||||
id: string,
|
||||
versionID?: string | null,
|
||||
isInit?: boolean,
|
||||
) => {
|
||||
if (app.graph == null) {
|
||||
return;
|
||||
}
|
||||
@@ -223,27 +243,55 @@ export default function App() {
|
||||
? await workflowVersionsTable?.get(versionID)
|
||||
: null;
|
||||
|
||||
app.ui.dialog.close();
|
||||
if (version) {
|
||||
setCurVersion(version);
|
||||
setCurFlowIDAndName({
|
||||
...flow,
|
||||
json: version.json,
|
||||
});
|
||||
app.loadGraphData(JSON.parse(version.json));
|
||||
} else {
|
||||
setCurFlowIDAndName(flow);
|
||||
setCurVersion(null);
|
||||
app.loadGraphData(JSON.parse(flow.json));
|
||||
setCurVersion(version ?? null);
|
||||
|
||||
const tabIndex = tabDataManager.tabs.findIndex((tab) => tab.id === id);
|
||||
if (tabIndex !== -1) {
|
||||
tabDataManager.changeActiveTab(tabIndex);
|
||||
if (version) {
|
||||
tabDataManager.updateTabData(tabIndex, { json: version.json });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
app.ui.dialog.close();
|
||||
setRoute("root");
|
||||
isDirty && setIsDirty(false);
|
||||
|
||||
const newTabInfo = {
|
||||
id: flow.id,
|
||||
name: flow.name,
|
||||
json: version ? version.json : flow.json,
|
||||
isDirty: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* During initialization,
|
||||
* because the event listener of OPEN_TAB_EVENT has not been registered yet,
|
||||
* the newly added tab will be invalid,
|
||||
* so the tab is added manually during initialization.
|
||||
*/
|
||||
if (isInit) {
|
||||
tabDataManager.addTabData(newTabInfo);
|
||||
app.loadGraphData(JSON.parse(newTabInfo.json));
|
||||
setCurFlowIDAndName({
|
||||
...flow,
|
||||
json: newTabInfo.json,
|
||||
});
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(OPEN_TAB_EVENT, {
|
||||
detail: {
|
||||
tabInfo: newTabInfo,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWorkflowID = async (
|
||||
id: string | null,
|
||||
versionID?: string | null,
|
||||
forceLoad: boolean = false,
|
||||
) => {
|
||||
// No current workflow, id is null when you deleted current workflow
|
||||
if (id === null) {
|
||||
@@ -251,18 +299,10 @@ export default function App() {
|
||||
app.graph.clear();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isDirty ||
|
||||
forceLoad ||
|
||||
userSettingsTable?.settings?.autoSave ||
|
||||
workflowsTable?.curWorkflow == null
|
||||
) {
|
||||
loadWorkflowIDImpl(id, versionID);
|
||||
return;
|
||||
}
|
||||
// prompt when auto save disabled and dirty
|
||||
showSaveOrDiscardCurWorkflowDialog(id);
|
||||
|
||||
loadWorkflowIDImpl(id, versionID);
|
||||
};
|
||||
|
||||
const showSaveOrDiscardCurWorkflowDialog = async (newIDToLoad?: string) => {
|
||||
const buttons = [
|
||||
newIDToLoad
|
||||
@@ -337,18 +377,19 @@ export default function App() {
|
||||
json: workflow.json,
|
||||
name: newFlowName || workflow.name,
|
||||
parentFolderID: workflow.parentFolderID,
|
||||
tags: workflow.tags,
|
||||
});
|
||||
flow && (await loadWorkflowID(flow.id));
|
||||
};
|
||||
|
||||
const onExecutedCreateMedia = useCallback((image: any) => {
|
||||
if (curFlowID.current == null) return;
|
||||
if (workflowsTable?.curWorkflow?.id == null) return;
|
||||
let path = image.filename;
|
||||
if (image.subfolder != null && image.subfolder !== "") {
|
||||
path = image.subfolder + "/" + path;
|
||||
}
|
||||
mediaTable?.create({
|
||||
workflowID: curFlowID.current,
|
||||
workflowID: workflowsTable?.curWorkflow?.id,
|
||||
localPath: path,
|
||||
});
|
||||
}, []);
|
||||
@@ -374,6 +415,42 @@ export default function App() {
|
||||
graphAppSetup();
|
||||
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);
|
||||
const res = await workflowsTable?.createFlow(newFlow);
|
||||
res &&
|
||||
tabDataManager.updateTabData(tabDataManager.activeIndex, {
|
||||
id: res.id,
|
||||
name: res.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
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) => {
|
||||
e.detail?.output?.images?.forEach(
|
||||
(im: { filename: string; subfolder: string; type: string }) => {
|
||||
@@ -392,8 +469,40 @@ export default function App() {
|
||||
};
|
||||
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);
|
||||
const res = await workflowsTable?.createFlow(newFlow);
|
||||
res &&
|
||||
tabDataManager.updateTabData(tabDataManager.activeIndex, {
|
||||
id: res.id,
|
||||
name: res.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
app.canvasEl.addEventListener("drop", handleDrop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("change", fileInputListener);
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
window.removeEventListener("executed", handleExecuted);
|
||||
app.canvasEl.removeEventListener("drop", handleDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -432,7 +541,7 @@ export default function App() {
|
||||
zIndex={DRAWER_Z_INDEX}
|
||||
draggable={false}
|
||||
>
|
||||
<Topbar curFlowName={curFlowName} setCurFlowName={setCurFlowName} />
|
||||
<Topbar />
|
||||
{loadChild && route === "recentFlows" && (
|
||||
<Suspense>
|
||||
<RecentFilesDrawer
|
||||
|
||||
@@ -21,7 +21,6 @@ import { Workflow } from "../types/dbTypes";
|
||||
import { mediaTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import { openWorkflowInNewTab } from "../utils";
|
||||
import { fetchApi } from "../Api";
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
@@ -56,7 +55,7 @@ export default function MoreActionMenu({ workflow }: Props) {
|
||||
body.append(`images[${index}]`, file, file.name);
|
||||
});
|
||||
body.append("subfolder", "workspace_manager");
|
||||
await fetchApi("/workspace/images/save", {
|
||||
await fetch("/workspace/images/save", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
@@ -30,6 +30,8 @@ import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu";
|
||||
import { insertWorkflowToCanvas, sortFileItem } from "../utils";
|
||||
import MultipleSelectionOperation from "./MultipleSelectionOperation";
|
||||
import { ESortTypes, sortTypeLocalStorageKey } from "./types";
|
||||
// @ts-expect-error ComfyUI import
|
||||
import { app } from "/scripts/app.js";
|
||||
import { useDebounce } from "../customHooks/useDebounce";
|
||||
import SearchInput from "../components/SearchInput";
|
||||
import { openWorkflowsFolder } from "../Api";
|
||||
@@ -38,7 +40,6 @@ import MyTagsRow from "./MyTagsRow";
|
||||
import ImportFlowsFileInput from "./ImportFlowsFileInput";
|
||||
import ItemsList from "./ItemsList";
|
||||
import { DRAWER_Z_INDEX } from "../const";
|
||||
import { app } from "../utils/comfyapp";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -50,7 +51,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
Array<Folder | Workflow>
|
||||
>([]);
|
||||
const allFlowsRef = useRef<Array<Workflow>>([]);
|
||||
const { loadWorkflowID, curFlowID } = useContext(WorkspaceContext);
|
||||
const [selectedTag, setSelectedTag] = useState<string>();
|
||||
const [multipleState, setMultipleState] = useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
@@ -112,9 +112,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
const onDelete = useCallback(
|
||||
async (id: string) => {
|
||||
await workflowsTable?.deleteFlow(id);
|
||||
if (workflowsTable?.curWorkflow?.id === id) {
|
||||
await loadWorkflowID?.(null);
|
||||
}
|
||||
await loadLatestWorkflows();
|
||||
},
|
||||
[selectedTag, debounceSearchValue],
|
||||
@@ -154,12 +151,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
const batchOperationCallback = (type: string, value: unknown) => {
|
||||
switch (type) {
|
||||
case "batchDelete":
|
||||
curFlowID &&
|
||||
workflowsTable?.get(curFlowID).then((res) => {
|
||||
if (!res) {
|
||||
loadWorkflowID?.(null);
|
||||
}
|
||||
});
|
||||
loadLatestWorkflows();
|
||||
setMultipleState(false);
|
||||
setSelectedKeys([]);
|
||||
@@ -257,18 +248,22 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
/>
|
||||
<HStack mb={2} mt={2} p={0} justifyContent="space-between">
|
||||
<HStack>
|
||||
<MultipleSelectionOperation
|
||||
multipleState={multipleState}
|
||||
changeMultipleState={(state) => {
|
||||
setMultipleState(state);
|
||||
!state && setSelectedKeys([]);
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
isSelectedAll={
|
||||
selectedKeys.length === allFlowsRef.current.length
|
||||
}
|
||||
batchOperationCallback={batchOperationCallback}
|
||||
/>
|
||||
{allFlowsRef.current.length > 0 ? (
|
||||
<MultipleSelectionOperation
|
||||
multipleState={multipleState}
|
||||
changeMultipleState={(state) => {
|
||||
setMultipleState(state);
|
||||
!state && setSelectedKeys([]);
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
isSelectedAll={
|
||||
selectedKeys.length === allFlowsRef.current.length
|
||||
}
|
||||
batchOperationCallback={batchOperationCallback}
|
||||
/>
|
||||
) : (
|
||||
<Box />
|
||||
)}
|
||||
{!multipleState && (
|
||||
<Tooltip hasArrow label="New folder" placement="bottom-start">
|
||||
<IconButton
|
||||
|
||||
@@ -89,7 +89,7 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
|
||||
w={"100%"}
|
||||
justify={"space-between"}
|
||||
borderBottom={"1px solid"}
|
||||
borderColor={colorMode == "dark" ? "gray.700" : "gray.200"}
|
||||
borderColor={colorMode == "dark" ? "gray.900" : "gray.200"}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
@@ -111,7 +111,7 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
|
||||
{isHovering ? (
|
||||
<WorkflowListItemActionButtons workflow={workflow} />
|
||||
) : (
|
||||
<Text color={"GrayText"} fontSize={"sm"}>
|
||||
<Text color={"GrayText"} ml={"2px"} fontSize={"sm"}>
|
||||
{formatTimestamp(workflow.updateTime, false, false, "/")}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -16,7 +16,6 @@ import CommonCheckboxSettings from "../settings/CommonCheckboxSettings";
|
||||
import SelectMyWorkflowsDir from "../settings/SelectMyWorkflowsDir";
|
||||
import { CommonNumberSetting } from "../settings/CommonNumberSetting";
|
||||
import AutosaveSetting from "../settings/AutosaveSetting";
|
||||
import CloudHostSetting from "../settings/CloudHostSetting";
|
||||
|
||||
export default function WorkspaceSettingsModal({
|
||||
onClose,
|
||||
@@ -57,7 +56,6 @@ export default function WorkspaceSettingsModal({
|
||||
settingKey="hideCoverImage"
|
||||
text="Hide thumbnail image in workflow list"
|
||||
/>
|
||||
<CloudHostSetting />
|
||||
</VStack>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
|
||||
@@ -9,13 +9,9 @@ export type JsonDiff = {
|
||||
export const WorkspaceContext = createContext<{
|
||||
curFlowID: string | null;
|
||||
onDuplicateWorkflow?: (flowID: string, newFlowName?: string) => void;
|
||||
loadWorkflowID: (
|
||||
id: string | null,
|
||||
versionID?: string | null,
|
||||
forceLoad?: boolean,
|
||||
) => void;
|
||||
loadWorkflowID: (id: string | null, versionID?: string | null) => void;
|
||||
setIsDirty: (dirty: boolean) => void;
|
||||
saveCurWorkflow: (force?: boolean) => Promise<void>;
|
||||
saveCurWorkflow: (saveCurWorkflow?: string) => Promise<void>;
|
||||
discardUnsavedChanges: () => Promise<void>;
|
||||
isDirty: boolean;
|
||||
loadNewWorkflow: (input?: { json: string; name?: string }) => void;
|
||||
@@ -24,7 +20,7 @@ export const WorkspaceContext = createContext<{
|
||||
route: WorkspaceRoute;
|
||||
curVersion: WorkflowVersion | null;
|
||||
setCurVersion: (version: WorkflowVersion | null) => void;
|
||||
setCurFlowIDAndName: (workflow: Workflow | null) => void;
|
||||
setCurFlowIDAndName: (workflow: Workflow) => void;
|
||||
}>({
|
||||
curFlowID: null,
|
||||
loadWorkflowID: () => {},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchApi } from "../Api";
|
||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
||||
import { indexdb } from "../db-tables/indexdb";
|
||||
import { Workflow } from "../types/dbTypes";
|
||||
@@ -10,17 +9,17 @@ import {
|
||||
|
||||
export namespace TwowaySyncAPI {
|
||||
export async function moveWorkflow(
|
||||
curRelPath: string,
|
||||
workflow: Workflow,
|
||||
newParentFolderRelPath: string,
|
||||
) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/file/move", {
|
||||
const response = await fetch("/workspace/file/move", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: curRelPath,
|
||||
path: getWorkflowRelPath(workflow),
|
||||
newParentPath: newParentFolderRelPath,
|
||||
}),
|
||||
});
|
||||
@@ -40,7 +39,7 @@ export namespace TwowaySyncAPI {
|
||||
parentFolderID: string | null,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/file/gen_unique_name", {
|
||||
const response = await fetch("/workspace/file/gen_unique_name", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -62,17 +61,17 @@ export namespace TwowaySyncAPI {
|
||||
}
|
||||
|
||||
export async function renameWorkflow(
|
||||
{ relPath, id }: { relPath: string; id: string },
|
||||
{ parentFolderID, name }: Pick<Workflow, "name" | "parentFolderID">,
|
||||
newName: string,
|
||||
) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/file/rename", {
|
||||
const response = await fetch("/workspace/file/rename", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: relPath,
|
||||
path: joinRelPath(parentFolderID ?? "", name + ".json"),
|
||||
newName: newName,
|
||||
}),
|
||||
});
|
||||
@@ -103,12 +102,7 @@ export namespace TwowaySyncAPI {
|
||||
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {};
|
||||
}
|
||||
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME].id = workflow.id;
|
||||
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", {
|
||||
const response = await fetch("/workspace/file/save", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -131,7 +125,7 @@ export namespace TwowaySyncAPI {
|
||||
}
|
||||
export async function deleteWorkflow(workflow: Workflow) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/delete_file", {
|
||||
const response = await fetch("/workspace/delete_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -148,8 +142,12 @@ export namespace TwowaySyncAPI {
|
||||
console.error("Error deleting file:", error);
|
||||
}
|
||||
}
|
||||
export async function createWorkflow(workflow: Workflow) {
|
||||
const { parentFolderID, name, json, id } = workflow;
|
||||
export async function createWorkflow({
|
||||
parentFolderID,
|
||||
name,
|
||||
json,
|
||||
id,
|
||||
}: Workflow) {
|
||||
let jsonObj: any = JSON.parse(json);
|
||||
if (jsonObj.extra == null) {
|
||||
jsonObj.extra = {};
|
||||
@@ -158,10 +156,6 @@ export namespace TwowaySyncAPI {
|
||||
jsonObj.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {};
|
||||
}
|
||||
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 } = {
|
||||
parentFolderPath: sanitizeRelPath(parentFolderID ?? ""),
|
||||
name: name,
|
||||
@@ -169,7 +163,7 @@ export namespace TwowaySyncAPI {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchApi("/workspace/create_workflow_file", {
|
||||
const response = await fetch("/workspace/create_workflow_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -193,28 +187,20 @@ export namespace TwowaySyncAPI {
|
||||
}
|
||||
}
|
||||
export async function getFile({
|
||||
relPath,
|
||||
parentFolderID,
|
||||
name,
|
||||
id,
|
||||
}: {
|
||||
relPath: string;
|
||||
parentFolderID: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
}): Promise<{
|
||||
json?: {
|
||||
extra: {
|
||||
[COMFYSPACE_TRACKING_FIELD_NAME]: {
|
||||
id: string;
|
||||
saveLock: boolean;
|
||||
cloudID: string | null;
|
||||
coverMediaPath: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
createTime?: number;
|
||||
updateTime?: number;
|
||||
json?: Object;
|
||||
error?: string;
|
||||
}> {
|
||||
const relPath = joinRelPath(parentFolderID ?? "", name + ".json");
|
||||
try {
|
||||
const response = await fetchApi("/workspace/get_workflow_file", {
|
||||
const response = await fetch("/workspace/get_workflow_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -249,9 +235,6 @@ export type ScanLocalFile = {
|
||||
name: string;
|
||||
id: string;
|
||||
json: string;
|
||||
saveLock: boolean;
|
||||
cloudID: string | null;
|
||||
coverMediaPath: string | null;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
lastOpenedTime?: number;
|
||||
@@ -268,20 +251,17 @@ export async function scanLocalFiles(
|
||||
metaInfoOnly = false,
|
||||
): Promise<Array<ScanLocalFile | ScanLocalFolder>> {
|
||||
try {
|
||||
const response = await fetchApi(
|
||||
"/workspace/file/scan_my_workflows_folder",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: sanitizeRelPath(path),
|
||||
recursive,
|
||||
metaInfoOnly,
|
||||
}),
|
||||
const response = await fetch("/workspace/file/scan_my_workflows_folder", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
path: sanitizeRelPath(path),
|
||||
recursive,
|
||||
metaInfoOnly,
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fetchApi } from "../Api";
|
||||
import { Folder } from "../types/dbTypes";
|
||||
import { sanitizeRelPath } from "../utils/OsPathUtils";
|
||||
|
||||
@@ -14,7 +13,7 @@ export namespace TwowayFolderSyncAPI {
|
||||
}
|
||||
|
||||
export async function createFolder(folder: Folder) {
|
||||
await fetchApi("/workspace/folder/create", {
|
||||
await fetch("/workspace/folder/create", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -28,7 +27,7 @@ export namespace TwowayFolderSyncAPI {
|
||||
folderToBeMoved: string,
|
||||
newParentPath: string,
|
||||
) {
|
||||
const resp = await fetchApi("/workspace/folder/move", {
|
||||
const resp = await fetch("/workspace/folder/move", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -46,7 +45,7 @@ export namespace TwowayFolderSyncAPI {
|
||||
}
|
||||
export async function deleteFolder(relPath: string) {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/folder/delete", {
|
||||
const response = await fetch("/workspace/folder/delete", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -66,7 +65,7 @@ export namespace TwowayFolderSyncAPI {
|
||||
folderRelPath: string,
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/file/count_files", {
|
||||
const response = await fetch("/workspace/file/count_files", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -91,7 +90,7 @@ export namespace TwowayFolderSyncAPI {
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetchApi("/workspace/folder/rename", {
|
||||
const response = await fetch("/workspace/folder/rename", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { workflowVersionsTable } from "../db-tables/WorkspaceDB";
|
||||
import { useState, ChangeEvent } from "react";
|
||||
import { app } from "../utils/comfyapp";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
interface Props {
|
||||
workflowId: string;
|
||||
|
||||
@@ -43,10 +43,11 @@ import { Workflow } from "../types/dbTypes";
|
||||
import CreateVersionDialog from "./CreateVersionDialog";
|
||||
import HoverMenu from "./HoverMenu";
|
||||
const ShareDialog = lazy(() => import("../share/ShareDialog"));
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
||||
import DownloadSpaceJsonDialog from "../spacejson/DownloadSpaceJsonDialog";
|
||||
import { downloadJsonFile } from "../utils/downloadJsonFile";
|
||||
import { SharedTopbarButton } from "../share/SharedTopbarButton";
|
||||
import { app } from "../utils/comfyapp";
|
||||
|
||||
export default function DropdownTitle() {
|
||||
const {
|
||||
@@ -97,7 +98,7 @@ export default function DropdownTitle() {
|
||||
parentFolderID: workflow?.parentFolderID,
|
||||
});
|
||||
|
||||
flow && (await loadWorkflowID(flow.id, null, true));
|
||||
flow && (await loadWorkflowID(flow.id, null));
|
||||
handleOnCloseModal();
|
||||
};
|
||||
|
||||
@@ -185,15 +186,21 @@ export default function DropdownTitle() {
|
||||
>
|
||||
Versions History
|
||||
</MenuItem>
|
||||
{/* <MenuItem
|
||||
onClick={() => setRoute("downloadSpaceJson")}
|
||||
icon={<IconDownload size={20} />}
|
||||
iconSpacing={1}
|
||||
>
|
||||
Save runnable workflow🧪
|
||||
</MenuItem> */}
|
||||
<MenuItem
|
||||
onClick={() => setRoute("share")}
|
||||
onClick={() => setRoute("downloadSpaceJson")}
|
||||
icon={<IconShare2 size={20} />}
|
||||
iconSpacing={1}
|
||||
alignItems={"center"}
|
||||
>
|
||||
<HStack>
|
||||
<p>Share</p> <Tag size={"sm"}>🧪beta</Tag>
|
||||
<SharedTopbarButton />
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
@@ -204,6 +211,7 @@ export default function DropdownTitle() {
|
||||
{route == "versionHistory" && (
|
||||
<VersionHistoryDrawer onClose={() => setRoute("root")} />
|
||||
)}
|
||||
{route == "downloadSpaceJson" && <DownloadSpaceJsonDialog />}
|
||||
{route === "saveAsModal" && (
|
||||
<Modal isOpen={true} onClose={handleOnCloseModal}>
|
||||
<ModalOverlay />
|
||||
|
||||
@@ -22,15 +22,10 @@ import { WorkspaceContext } from "../WorkspaceContext";
|
||||
|
||||
type Props = {
|
||||
displayName: string;
|
||||
updateFlowName: (newName: string) => void;
|
||||
isDirty: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export default function EditFlowName({
|
||||
displayName,
|
||||
updateFlowName,
|
||||
isDirty,
|
||||
}: Props) {
|
||||
export default function EditFlowName({ displayName, isActive }: Props) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { curFlowID } = useContext(WorkspaceContext);
|
||||
const [editName, setEditName] = useState(displayName);
|
||||
@@ -63,7 +58,6 @@ export default function EditFlowName({
|
||||
await workflowsTable?.updateName(curFlowID, {
|
||||
name: trimEditName,
|
||||
});
|
||||
updateFlowName(trimEditName);
|
||||
onCloseModal();
|
||||
}
|
||||
}
|
||||
@@ -79,17 +73,15 @@ export default function EditFlowName({
|
||||
<Tooltip label={displayName} placement="bottom">
|
||||
<Text
|
||||
as="div"
|
||||
color="white"
|
||||
onClick={startEdit}
|
||||
maxW={160}
|
||||
fontWeight={500}
|
||||
color={isActive ? "white" : "#9d9d9d"}
|
||||
onDoubleClick={startEdit}
|
||||
maxW="114px"
|
||||
marginRight="4px"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
userSelect="none"
|
||||
>
|
||||
<div style={{ width: 8, display: "inline-block" }}>
|
||||
<Text as="span">{isDirty && "* "}</Text>
|
||||
</div>
|
||||
{displayName}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
||||
@@ -219,12 +219,12 @@ export default function SpotlightSearch() {
|
||||
|
||||
document.addEventListener("keyup", handleKeyUp);
|
||||
document.addEventListener("keydown", keydownListener);
|
||||
window.addEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger);
|
||||
document.addEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keyup", handleKeyUp);
|
||||
document.removeEventListener("keydown", keydownListener);
|
||||
window.removeEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger);
|
||||
document.removeEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ import {
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import { formatTimestamp } from "../utils";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import { Changelog, WorkflowVersion } from "../types/dbTypes";
|
||||
import DeleteConfirm from "./DeleteConfirm";
|
||||
import { app } from "../utils/comfyapp";
|
||||
|
||||
export function VersionHistoryDrawer({ onClose }: { onClose: () => void }) {
|
||||
const toast = useToast();
|
||||
|
||||
@@ -44,8 +44,9 @@ export class ChangelogsTable extends TableBase<Changelog> {
|
||||
createTime: Date.now(),
|
||||
isAutoSave: input.isAutoSave,
|
||||
};
|
||||
await indexdb.changelogs.add(change);
|
||||
await this.deleteLogsExceedLimit(input.workflowID);
|
||||
await indexdb.changelogs.add(change);
|
||||
await this.saveDiskDB();
|
||||
return change;
|
||||
}
|
||||
async deleteLogsExceedLimit(workflowID: string) {
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
import { deleteFile, updateFile } from "../Api";
|
||||
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";
|
||||
|
||||
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(
|
||||
folderId: string | null,
|
||||
): Promise<string> {
|
||||
@@ -18,6 +71,46 @@ export async function genFolderRelPath(
|
||||
|
||||
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> {
|
||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { userSettingsTable, workflowsTable } from "./WorkspaceDB";
|
||||
import { EFlowOperationType, Folder } from "../types/dbTypes";
|
||||
import { validateOrSaveAllJsonFileMyWorkflows } from "../utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import { TableBase } from "./TableBase";
|
||||
import { indexdb } from "./indexdb";
|
||||
@@ -83,6 +84,11 @@ export class FoldersTable extends TableBase<Folder> {
|
||||
}
|
||||
await indexdb.folders.update(id, input);
|
||||
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;
|
||||
}
|
||||
public async deleteFolder(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Table, workflowsTable } from "./WorkspaceDB";
|
||||
import { TableBase } from "./TableBase";
|
||||
import { Media } from "../types/dbTypes";
|
||||
import { indexdb } from "./indexdb";
|
||||
import { getMetadataFromUrl } from "../gallery/utils.ts";
|
||||
|
||||
export class MediaTable extends TableBase<Media> {
|
||||
static readonly TABLE_NAME: Table = "media";
|
||||
@@ -24,11 +25,20 @@ export class MediaTable extends TableBase<Media> {
|
||||
const format = input.localPath.split(".").pop();
|
||||
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 = {
|
||||
id: input.localPath,
|
||||
localPath: input.localPath,
|
||||
workflowID: input.workflowID,
|
||||
createTime: Date.now(),
|
||||
workflowJSON: JSON.stringify(res?.prompt),
|
||||
format: format,
|
||||
};
|
||||
await workflowsTable?.updateMetaInfo(input.workflowID, {
|
||||
latestImage: md.localPath,
|
||||
|
||||
@@ -43,7 +43,7 @@ export class UserSettingsTable extends TableBase<UserSettings> {
|
||||
autoSaveDuration: 3,
|
||||
twoWaySync: false,
|
||||
foldersOnTop: false,
|
||||
cloudHost: "https://www.nodecafe.co",
|
||||
cloudHost: "https://www.nodecafe.org",
|
||||
overwriteCurWorkflowWhenDroppingFileToCanvas: false,
|
||||
maximumChangelogNumber: 80,
|
||||
hideCoverImage: false,
|
||||
@@ -93,12 +93,10 @@ export class UserSettingsTable extends TableBase<UserSettings> {
|
||||
...res,
|
||||
};
|
||||
});
|
||||
if (instance.defaultSettings.cloudHost.includes("comfyspace.art")) {
|
||||
// overwrite legacy comfyspace.art
|
||||
await instance.upsert({
|
||||
cloudHost: instance.defaultSettings.cloudHost,
|
||||
});
|
||||
}
|
||||
// overwrite legacy comfyspace.art
|
||||
await instance.upsert({
|
||||
cloudHost: instance.defaultSettings.cloudHost,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import { sortFileItem } from "../utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import { TableBase } from "./TableBase";
|
||||
import { indexdb } from "./indexdb";
|
||||
import {
|
||||
deleteJsonFileMyWorkflows,
|
||||
saveJsonFileMyWorkflows,
|
||||
} from "./DiskFileUtils";
|
||||
import { ESortTypes, ImportWorkflow } from "../RecentFilesDrawer/types";
|
||||
import { defaultGraph } from "../defaultGraph";
|
||||
import { scanMyWorkflowsDir } from "../utils/twowaySyncUtils";
|
||||
@@ -17,11 +21,11 @@ import {
|
||||
scanLocalFiles,
|
||||
} from "../apis/TwowaySyncApi";
|
||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
|
||||
import { tabDataManager } from "../topbar/multipleTabs/TabDataManager";
|
||||
|
||||
export class WorkflowsTable extends TableBase<Workflow> {
|
||||
static readonly TABLE_NAME = "workflows";
|
||||
private _curWorkflow: Workflow | null = null;
|
||||
workflowIDPathMap: Record<string, string> = {};
|
||||
public get curWorkflow() {
|
||||
return this._curWorkflow;
|
||||
}
|
||||
@@ -48,7 +52,11 @@ 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() {
|
||||
if (this._curWorkflow) {
|
||||
@@ -61,8 +69,20 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listByIndex(): Promise<Workflow[]> {
|
||||
throw new Error("Method not allowed.");
|
||||
public async listByIndex(
|
||||
index: keyof Workflow,
|
||||
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) {
|
||||
@@ -120,20 +140,34 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
};
|
||||
//add to IndexDB
|
||||
await indexdb.workflows.add(newWorkflow);
|
||||
// add to disk file db
|
||||
this.saveDiskDB();
|
||||
// add to my_workflows/
|
||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||
if (twoWaySyncEnabled) {
|
||||
await TwowaySyncAPI.createWorkflow(newWorkflow);
|
||||
return await this.get(newWorkflow.id);
|
||||
} else {
|
||||
saveJsonFileMyWorkflows(newWorkflow);
|
||||
}
|
||||
return newWorkflow;
|
||||
}
|
||||
|
||||
// disallow TableBase.update()
|
||||
async update(): Promise<Workflow | null> {
|
||||
async update(
|
||||
_id: string,
|
||||
_change: Partial<Workflow>,
|
||||
): Promise<Workflow | null> {
|
||||
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(
|
||||
id: string,
|
||||
change: Omit<Partial<Workflow>, "id" | "name" | "parentFolderID" | "json">,
|
||||
@@ -144,33 +178,35 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
id: string,
|
||||
change: Partial<Workflow>,
|
||||
): Promise<Workflow | null> {
|
||||
const cur = await this.get(id);
|
||||
if (!cur) {
|
||||
return null;
|
||||
}
|
||||
const newWorkflow = {
|
||||
...cur,
|
||||
...change,
|
||||
};
|
||||
await TwowaySyncAPI.saveWorkflow(newWorkflow);
|
||||
|
||||
//update indexdb
|
||||
await indexdb.workflows.update(id, change);
|
||||
const newWorkflow = (await indexdb.workflows.get(id)) ?? null;
|
||||
//update curWorkflow RAM
|
||||
if (this._curWorkflow && this._curWorkflow.id === id) {
|
||||
this._curWorkflow = newWorkflow;
|
||||
}
|
||||
this.saveDiskDB();
|
||||
return newWorkflow;
|
||||
}
|
||||
|
||||
async updateFolder(id: string, change: Pick<Workflow, "parentFolderID">) {
|
||||
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) {
|
||||
const relPath = this.workflowIDPathMap[id];
|
||||
if (!relPath) {
|
||||
alert("Error: workflow not found. Cannot move workflow.");
|
||||
return;
|
||||
await TwowaySyncAPI.moveWorkflow(oldWorkflow, change.parentFolderID!);
|
||||
} else {
|
||||
await saveJsonFileMyWorkflows(newWorkflow);
|
||||
await deleteJsonFileMyWorkflows(oldWorkflow);
|
||||
// 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) {
|
||||
@@ -178,31 +214,26 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
return changelog?.json;
|
||||
}
|
||||
public async updateName(id: string, change: Pick<Workflow, "name">) {
|
||||
const path = this.workflowIDPathMap[id];
|
||||
if (!path) {
|
||||
alert("Error: workflow not found. Cannot rename workflow.");
|
||||
return;
|
||||
}
|
||||
const before = await this.get(id);
|
||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||
if (twoWaySyncEnabled) {
|
||||
const success = await TwowaySyncAPI.renameWorkflow(
|
||||
{
|
||||
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("/");
|
||||
}
|
||||
before &&
|
||||
(await TwowaySyncAPI.renameWorkflow(before, change.name + ".json"));
|
||||
}
|
||||
tabDataManager.updateTabData(tabDataManager.activeIndex, change);
|
||||
return await this.updateMetaInfo(id, change as any);
|
||||
}
|
||||
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, {
|
||||
updateTime: Date.now(),
|
||||
json: input.json,
|
||||
@@ -211,6 +242,8 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||
if (twoWaySyncEnabled) {
|
||||
after && (await TwowaySyncAPI.saveWorkflow(after));
|
||||
} else {
|
||||
after && (await saveJsonFileMyWorkflows(after));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,19 +296,39 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
} catch (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) {
|
||||
const workflow = await this.get(id);
|
||||
|
||||
if (workflow) {
|
||||
const twoWaySyncEnabled =
|
||||
await userSettingsTable?.getSetting("twoWaySync");
|
||||
if (twoWaySyncEnabled) {
|
||||
await TwowaySyncAPI.deleteWorkflow(workflow);
|
||||
} else {
|
||||
deleteJsonFileMyWorkflows({ ...workflow });
|
||||
}
|
||||
}
|
||||
|
||||
const tabIndex = tabDataManager.tabs.findIndex((tab) => tab.id === id);
|
||||
if (tabIndex !== -1) {
|
||||
tabDataManager?.deleteTabData(tabIndex);
|
||||
}
|
||||
|
||||
//add to IndexDB
|
||||
await indexdb.workflows.delete(id);
|
||||
this.saveDiskDB();
|
||||
}
|
||||
|
||||
public async batchDeleteFlow(ids: string[]) {
|
||||
@@ -286,8 +339,13 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
await userSettingsTable?.getSetting("twoWaySync");
|
||||
if (twoWaySyncEnabled) {
|
||||
await TwowaySyncAPI.deleteWorkflow(flow);
|
||||
} else {
|
||||
deleteJsonFileMyWorkflows({ ...flow });
|
||||
}
|
||||
}
|
||||
await indexdb.workflows.bulkDelete(ids);
|
||||
tabDataManager.batchDeleteTabData(ids);
|
||||
this.saveDiskDB();
|
||||
}
|
||||
|
||||
public async listFolderContent(
|
||||
@@ -317,55 +375,28 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
public async get(
|
||||
id: string,
|
||||
fetchJsonContentOnDisk = true, //if false, only get the meta info from indexdb
|
||||
): Promise<
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
parentFolderID?: string | null;
|
||||
saveLock?: boolean;
|
||||
cloudID?: string | null;
|
||||
coverMediaPath?: string | null;
|
||||
json: string;
|
||||
updateTime: number;
|
||||
createTime: number;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
): Promise<Workflow | undefined> {
|
||||
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
|
||||
if (!twoWaySyncEnabled) {
|
||||
return await indexdb.workflows.get(id);
|
||||
const wf = await indexdb.workflows.get(id);
|
||||
if (!twoWaySyncEnabled || !fetchJsonContentOnDisk) {
|
||||
return wf;
|
||||
}
|
||||
// two way sync mode
|
||||
const rel = this.workflowIDPathMap[id];
|
||||
if (!rel) {
|
||||
if (wf == null) {
|
||||
return undefined;
|
||||
}
|
||||
const data = await TwowaySyncAPI.getFile({
|
||||
relPath: rel,
|
||||
id: id,
|
||||
parentFolderID: wf.parentFolderID ?? null,
|
||||
name: wf.name,
|
||||
id: wf.id,
|
||||
});
|
||||
|
||||
if (!data.json) {
|
||||
return undefined;
|
||||
}
|
||||
const workspaceInfo = data.json?.extra?.[COMFYSPACE_TRACKING_FIELD_NAME];
|
||||
if (!workspaceInfo || workspaceInfo.id !== id) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
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,
|
||||
...wf,
|
||||
json: JSON.stringify(data.json),
|
||||
updateTime: data.updateTime ?? Date.now(),
|
||||
createTime: data.createTime ?? Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// @ts-expect-error ComfyUI import
|
||||
import { app } from "/scripts/app.js";
|
||||
import {
|
||||
Checkbox,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
IconButton,
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
import { MetaBoxContext } from "./metaBoxContext.ts";
|
||||
import { workflowsTable } from "../../../db-tables/WorkspaceDB.ts";
|
||||
import { WorkspaceContext } from "../../../WorkspaceContext.ts";
|
||||
import { app } from "../../../utils/comfyapp.ts";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
export type TopFieldType = {
|
||||
promptKey: string | number;
|
||||
class_type?: string;
|
||||
@@ -63,6 +64,18 @@ export default function MetadataForm({ media }: { media: Media | null }) {
|
||||
|
||||
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) => {
|
||||
if (
|
||||
isInTopField(topFields, {
|
||||
@@ -75,9 +88,19 @@ export default function MetadataForm({ media }: { media: Media | null }) {
|
||||
(v) => v.name !== field.name || v.promptKey !== field.promptKey,
|
||||
);
|
||||
setTopFields(topFieldsConfig);
|
||||
if (curFlowID) {
|
||||
workflowsTable?.updateTopFields(curFlowID, {
|
||||
topFieldsConfig,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const topFieldsConfig = [...topFields, field];
|
||||
setTopFields(topFieldsConfig);
|
||||
if (curFlowID) {
|
||||
workflowsTable?.updateTopFields(curFlowID, {
|
||||
topFieldsConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-14
@@ -1,6 +1,6 @@
|
||||
import React, { Suspense } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { ColorModeScript, type StyleFunctionProps } from "@chakra-ui/react";
|
||||
import { ColorModeScript } from "@chakra-ui/react";
|
||||
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
|
||||
import { AlertDialogProvider } from "./components/AlertDialogProvider.tsx";
|
||||
import CSSReset from "./MyCSSReset.tsx";
|
||||
@@ -35,20 +35,7 @@ const zIndices = {
|
||||
};
|
||||
|
||||
const theme = extendTheme({
|
||||
config: {
|
||||
initialColorMode: "dark",
|
||||
useSystemColorMode: false,
|
||||
},
|
||||
zIndices,
|
||||
components: {
|
||||
Card: {
|
||||
baseStyle: (props: StyleFunctionProps) => ({
|
||||
container: {
|
||||
backgroundColor: props.colorMode === "dark" ? "#171B21" : "#F6F8FA",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(topbar).render(
|
||||
|
||||
@@ -32,7 +32,6 @@ export default function ServerEventListener() {
|
||||
name: version.name,
|
||||
authorID: version.authorID,
|
||||
json: version.json,
|
||||
api_prompt: null,
|
||||
});
|
||||
newversion && loadWorkflowID(existingWorkflow.id, newversion.id);
|
||||
}
|
||||
@@ -57,7 +56,6 @@ export default function ServerEventListener() {
|
||||
name: version.name,
|
||||
json: version.json,
|
||||
authorID: version.authorID,
|
||||
api_prompt: null,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
await loadWorkflowID(newWorkflow.id, newverison?.id);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { cancelDownload } from "../../Api";
|
||||
import { api } from "../../utils/comfyapp";
|
||||
|
||||
type Queue = { save_path: string; progress: number };
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function getModelFromSearch(
|
||||
queries: [
|
||||
{
|
||||
q: q,
|
||||
indexUid: "models_v9",
|
||||
indexUid: "models_v8",
|
||||
facets: [
|
||||
"category.name",
|
||||
"checkpointType",
|
||||
|
||||
@@ -12,12 +12,13 @@ import {
|
||||
import { IconExternalLink } from "@tabler/icons-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import type {
|
||||
DepsResult,
|
||||
ModelDep,
|
||||
ModelFile,
|
||||
} from "../../spacejson/handleDownloadSpaceJson";
|
||||
import { indexdb } from "../../db-tables/indexdb";
|
||||
import { app } from "../../utils/comfyapp";
|
||||
|
||||
const HF_LOGO =
|
||||
"https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png";
|
||||
@@ -32,8 +33,8 @@ export interface MissingModel {
|
||||
|
||||
interface Props {
|
||||
model: MissingModel;
|
||||
setSelectedModelDeps: (deps: ModelDep[]) => void;
|
||||
selectedModelDeps: ModelDep[];
|
||||
setSelectedModelDeps: (deps: ModelFile[]) => void;
|
||||
selectedModelDeps: ModelFile[];
|
||||
foldersList: Record<string, string[]>;
|
||||
setFoldersList: (folders: Record<string, string[]>) => void;
|
||||
}
|
||||
@@ -46,7 +47,7 @@ export default function MissingModelItem({
|
||||
setFoldersList,
|
||||
}: Props) {
|
||||
const [suggestedModel, setSuggestedModel] = useState<
|
||||
ModelDep | { existingModel: string }
|
||||
ModelFile | { existingModel: string }
|
||||
>();
|
||||
const modelDeps: DepsResult["models"] =
|
||||
app.graph.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.deps?.models;
|
||||
@@ -76,7 +77,7 @@ export default function MissingModelItem({
|
||||
|
||||
const selectedModelNames = selectedModelDeps.map((dep) => dep.filename);
|
||||
function isExistingModel(
|
||||
model: ModelDep | { existingModel: string },
|
||||
model: ModelFile | { existingModel: string },
|
||||
): model is { existingModel: string } {
|
||||
return (model as { existingModel: string }).existingModel != null;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,15 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import MissingModelItem from "./MissingModelItem";
|
||||
import { DRAWER_Z_INDEX } from "../../const";
|
||||
import type { ModelDep } from "../../spacejson/handleDownloadSpaceJson";
|
||||
import type { ModelFile } from "../../spacejson/handleDownloadSpaceJson";
|
||||
import { getAllFoldersList } from "../../Api";
|
||||
import { installModelsApi } from "../api/modelsApi";
|
||||
import { getCivitApiKey } from "../../utils/civitUtils";
|
||||
import { WorkspaceContext } from "../../WorkspaceContext";
|
||||
import { app } from "../../utils/comfyapp";
|
||||
|
||||
export interface MissingModel {
|
||||
class_type: string;
|
||||
@@ -40,7 +41,7 @@ export default function MissingModelsListDrawer({
|
||||
app.canvasEl.removeEventListener("click", onClose);
|
||||
};
|
||||
}, []);
|
||||
const [selectedModelDeps, setSelectedModelDeps] = useState<ModelDep[]>([]);
|
||||
const [selectedModelDeps, setSelectedModelDeps] = useState<ModelFile[]>([]);
|
||||
const [foldersList, setFoldersList] = useState<Record<string, string[]>>({});
|
||||
const { setRoute } = useContext(WorkspaceContext);
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -77,7 +78,7 @@ export default function MissingModelsListDrawer({
|
||||
}
|
||||
installModelsApi({
|
||||
file_hash: model.fileHash ?? undefined,
|
||||
filename: model.name,
|
||||
filename: model.filename,
|
||||
save_path: foldersList[model.fileFolder][0],
|
||||
url,
|
||||
force_filename: true,
|
||||
|
||||
@@ -13,6 +13,8 @@ import Fuse from "fuse.js/min-basic";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ModelsTags } from "./ModelsTags";
|
||||
import { ModelsList } from "./ModelsList";
|
||||
// @ts-expect-error ComfyUI imports
|
||||
import { app } from "/scripts/app.js";
|
||||
import InstallModelsButton from "../install-models/InstallModelsButton";
|
||||
import { type ModelsListRespItem } from "../types";
|
||||
import { useUpdateModels } from "../hooks/useUpdateModels";
|
||||
@@ -21,7 +23,6 @@ import ShowNsfwModelThumbnailSettings from "../../settings/ShowNsfwModelThumbnai
|
||||
import { indexdb } from "../../db-tables/indexdb";
|
||||
import { type Model } from "../../types/dbTypes";
|
||||
import ModelDropEventListener from "../topbar/ModelDropEventListener";
|
||||
import { app } from "../../utils/comfyapp";
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Button } from "@chakra-ui/react";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import MissingModelsListDrawer, {
|
||||
MissingModel,
|
||||
} from "../missing-models-drawer/MissingModelsListDrawer";
|
||||
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
|
||||
import { app } from "../../utils/comfyapp";
|
||||
interface Props {}
|
||||
|
||||
interface NodeError {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, DragEvent } from "react";
|
||||
import { app } from "../../utils/comfyapp";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
export default function ModelDropEventListener() {
|
||||
const handleModelDrop = async (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Button, DarkMode, Stack } from "@chakra-ui/react";
|
||||
import { lazy, useEffect, useContext, Suspense } from "react";
|
||||
import { lazy, useEffect, DragEvent, useContext, Suspense } from "react";
|
||||
import ModelsListDrawer from "../models-list-drawer/ModelsListDrawer";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import "./index.css";
|
||||
import { WorkspaceContext } from "../../WorkspaceContext";
|
||||
// @ts-ignore
|
||||
@@ -13,10 +15,30 @@ import type { ModelsListRespItemFromApi } from "../types";
|
||||
import InatallModelsModal from "../install-models/InstallModelsModal";
|
||||
import { TOPBAR_BUTTON_HEIGHT } from "../../const";
|
||||
|
||||
const AddMissingModelsButton = lazy(
|
||||
() => import("./InstallMissingModelsButton"),
|
||||
);
|
||||
|
||||
export default function ModelManagerTopbar() {
|
||||
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(() => {
|
||||
app.canvasEl.addEventListener("drop", handleModelDrop);
|
||||
api.addEventListener(
|
||||
"model_list",
|
||||
async (e: { detail: ModelsListRespItemFromApi[] }) => {
|
||||
@@ -55,22 +77,30 @@ export default function ModelManagerTopbar() {
|
||||
window.dispatchEvent(new CustomEvent("model_list_updated"));
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
app.canvasEl.removeEventListener("drop", handleModelDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack style={{ position: "relative" }}>
|
||||
<Button
|
||||
size={"sm"}
|
||||
backgroundColor={"#434554"}
|
||||
color={"white"}
|
||||
colorScheme="blue"
|
||||
aria-label="My models"
|
||||
onClick={() => setRoute("modelList")}
|
||||
px={1}
|
||||
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
||||
>
|
||||
Models
|
||||
</Button>
|
||||
<DarkMode>
|
||||
<Button
|
||||
size={"sm"}
|
||||
backgroundColor={"#434554"}
|
||||
aria-label="My models"
|
||||
onClick={() => setRoute("modelList")}
|
||||
px={1}
|
||||
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
||||
>
|
||||
Models
|
||||
</Button>
|
||||
</DarkMode>
|
||||
<div style={{ position: "absolute", top: "38px", left: "0px" }}>
|
||||
<Suspense>
|
||||
<AddMissingModelsButton />
|
||||
</Suspense>
|
||||
</div>
|
||||
{route === "modelList" && (
|
||||
<ModelsListDrawer onClose={() => setRoute("root")} />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// @ts-ignore
|
||||
import { deleteFile } from "./Api";
|
||||
// @ts-ignore
|
||||
import { app, ComfyApp } from "/scripts/app.js";
|
||||
|
||||
export function formatTimestamp(
|
||||
unixTimestamp: number,
|
||||
showSec: boolean = false,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
+88
-134
@@ -10,20 +10,26 @@ import {
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Stack,
|
||||
Switch,
|
||||
Tag,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Workflow, WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import CustomSelector from "../components/CustomSelector";
|
||||
import { IconCopy, IconExternalLink } from "@tabler/icons-react";
|
||||
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||
import { IconCloud, IconCopy, IconExternalLink } from "@tabler/icons-react";
|
||||
import {
|
||||
userSettingsTable,
|
||||
workflowVersionsTable,
|
||||
workflowsTable,
|
||||
} from "../db-tables/WorkspaceDB";
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import {
|
||||
PrivacyLabel,
|
||||
fetchCloudWorkflowPrivacy,
|
||||
@@ -32,50 +38,35 @@ import {
|
||||
privacyOptions,
|
||||
} from "./shareUtils";
|
||||
|
||||
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";
|
||||
import ShareDialogWorkflowVersionRadio from "./ShareDialogWorkflowVersionRadio";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ShareDialog({ onClose }: Props) {
|
||||
const [versionName, setVersionName, versionNameRef] = useStateRef(
|
||||
"v" + getCurDateString(),
|
||||
);
|
||||
const { saveCurWorkflow } = useContext(WorkspaceContext);
|
||||
const [deps, setDeps] = useState<DepsResult>();
|
||||
const [uploadingImage, setUploadingImage] = useState(false);
|
||||
const [versionName, setVersionName] = useState("v" + getCurDateString());
|
||||
const [localVersions, setLocalVersions] = useState<WorkflowVersion[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const cloudHost = userSettingsTable?.settings?.cloudHost;
|
||||
const [privacy, setPrivacy, privacyRef] =
|
||||
useStateRef<WorkflowPrivacy>("PRIVATE");
|
||||
const [cloudHost, setCloudHost] = useState("");
|
||||
const [privacy, setPrivacy] = useState<WorkflowPrivacy>("PRIVATE");
|
||||
const [selectedVersion, setSelectedVersion] = useState<
|
||||
string | "new_version"
|
||||
>("new_version");
|
||||
const workflow = workflowsTable?.curWorkflow;
|
||||
const cloudHostRef = useRef("");
|
||||
const [workflow, setWorkflow] = useState<Workflow>();
|
||||
const toast = useToast();
|
||||
const [enableDeps, setEnableDeps] = useState(true);
|
||||
const handleShareWorkflowSuccess = async (event: MessageEvent) => {
|
||||
const detail = event.data;
|
||||
if (
|
||||
// event.origin !== cloudHostRef.current ||
|
||||
event.origin !== cloudHostRef.current ||
|
||||
detail.type !== "share_workflow_success"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localID = detail.localWorkflowID;
|
||||
const localVerID = detail.localVersionID;
|
||||
const cloudVersionID = detail.version.id;
|
||||
const cloudID = detail.version.workflowID;
|
||||
|
||||
@@ -83,22 +74,24 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
localID &&
|
||||
(await workflowsTable?.updateMetaInfo(localID, {
|
||||
cloudID: cloudID,
|
||||
privacy: privacyRef.current,
|
||||
cloudOrigin: event.origin,
|
||||
privacy: privacy,
|
||||
}));
|
||||
localVerID &&
|
||||
cloudVersionID &&
|
||||
(await workflowVersionsTable?.update(localVerID, {
|
||||
cloudID: cloudVersionID,
|
||||
cloudOrigin: event.origin,
|
||||
}));
|
||||
|
||||
loadData();
|
||||
window.open(
|
||||
cloudHost + "/workflow/" + cloudID + "/" + cloudVersionID,
|
||||
cloudHostRef.current + "/workflow/" + cloudID + "/" + cloudVersionID,
|
||||
"_blank",
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
const graph = app.graph.serialize();
|
||||
extractAndFetchFileNames(graph.nodes ?? []).then((depsRes) => {
|
||||
setDeps(depsRes);
|
||||
});
|
||||
window.addEventListener("message", handleShareWorkflowSuccess);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleShareWorkflowSuccess);
|
||||
@@ -106,11 +99,24 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
}, []);
|
||||
|
||||
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) {
|
||||
fetchCloudWorkflowPrivacy(workflow).then((privacy) => {
|
||||
setPrivacy(privacy);
|
||||
});
|
||||
}
|
||||
workflowVersionsTable?.listByWorkflowID(workflow!.id).then((vers) => {
|
||||
setLocalVersions(vers);
|
||||
});
|
||||
};
|
||||
const copyTextToClipboard = (text: string) => {
|
||||
navigator.clipboard
|
||||
@@ -127,24 +133,7 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
});
|
||||
};
|
||||
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);
|
||||
const onShare = async () => {
|
||||
setLoading(true);
|
||||
const secretKey = generateRandomKey(32);
|
||||
const host =
|
||||
@@ -152,15 +141,30 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
? ((await userSettingsTable?.getSetting("cloudHost")) as string)
|
||||
: "http://localhost:3000";
|
||||
let nodeDefs: Object;
|
||||
nodeDefs = getNodeDefs();
|
||||
const version: WorkflowVersion = {
|
||||
id: nanoid(),
|
||||
workflowID: workflow!.id,
|
||||
name: versionNameRef.current,
|
||||
createTime: Date.now(),
|
||||
api_prompt: JSON.stringify(apiPrompt),
|
||||
json: jsonToShare ?? JSON.stringify(app.graph.serialize()),
|
||||
};
|
||||
let version: WorkflowVersion | undefined;
|
||||
try {
|
||||
nodeDefs = getNodeDefs();
|
||||
if (selectedVersion === "new_version") {
|
||||
version = await workflowVersionsTable?.add({
|
||||
workflowID: workflow!.id,
|
||||
name: versionName,
|
||||
createTime: Date.now(),
|
||||
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 baseHost = window.location.host;
|
||||
const baseUrl = `${protocol}//${baseHost}`;
|
||||
@@ -180,7 +184,7 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
workflow: curWorkflow,
|
||||
version: version,
|
||||
nodeDefs: nodeDefs,
|
||||
privacy: privacyRef.current,
|
||||
privacy: privacy,
|
||||
},
|
||||
host,
|
||||
);
|
||||
@@ -190,57 +194,12 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
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;
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose} size={"lg"}>
|
||||
<ModalOverlay />
|
||||
<ModalContent width={["98%", "90%", "50%"]} maxWidth={"600px"}>
|
||||
<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>
|
||||
<ModalHeader>Share "{workflow?.name}"</ModalHeader>
|
||||
<ModalBody pb={10}>
|
||||
<Stack gap={5}>
|
||||
{cloudWorkflowID == null ? (
|
||||
@@ -289,6 +248,7 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
<Text>Choose a version to share:</Text>
|
||||
<RadioGroup
|
||||
gap={4}
|
||||
onChange={(val) => {
|
||||
@@ -298,6 +258,7 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
>
|
||||
<Stack>
|
||||
<HStack mb={5} alignItems={"center"}>
|
||||
<Radio key={"new_version"} value="new_version" />
|
||||
<Input
|
||||
value={versionName}
|
||||
width={"60%"}
|
||||
@@ -309,35 +270,28 @@ export default function ShareDialog({ onClose }: Props) {
|
||||
}}
|
||||
/>
|
||||
<Flex color="green">
|
||||
<Badge colorScheme="purple">Version Name</Badge>
|
||||
<Badge colorScheme="purple">New version</Badge>
|
||||
</Flex>
|
||||
</HStack>
|
||||
|
||||
{localVersions.slice(0, 4).map((ver) => {
|
||||
return (
|
||||
<ShareDialogWorkflowVersionRadio
|
||||
key={ver.id}
|
||||
version={ver}
|
||||
cloudHost={cloudHost}
|
||||
cloudWorkflowID={cloudWorkflowID ?? null}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</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>
|
||||
<HStack justifyContent={"flex-end"} mt={16}>
|
||||
<Button colorScheme="teal" onClick={onShare} isDisabled={loading}>
|
||||
{loading ? "Sharing" : "Share"}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IconLink, IconLock, IconWorld } from "@tabler/icons-react";
|
||||
import { CustomSelectorOption } from "../components/CustomSelector";
|
||||
import { Workflow, WorkflowPrivacy } from "../types/dbTypes";
|
||||
import { app } from "../utils/comfyapp";
|
||||
// @ts-expect-error
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
export function generateRandomKey(length: number) {
|
||||
// Generate a random array of bytes
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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,44 +1,332 @@
|
||||
import { HStack, Heading, Image, Spinner, Stack } from "@chakra-ui/react";
|
||||
import { DepsResult } from "./handleDownloadSpaceJson";
|
||||
import {
|
||||
forwardRef,
|
||||
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 function ResourceDepsForm({
|
||||
deps,
|
||||
uploadingImage,
|
||||
}: {
|
||||
deps: DepsResult | null;
|
||||
setDeps: (deps: DepsResult) => void;
|
||||
uploadingImage: boolean;
|
||||
}) {
|
||||
const imageDepsArr = Object.values(deps?.images ?? {});
|
||||
if (!deps) {
|
||||
return <Spinner size="md" color="teal.400" />;
|
||||
}
|
||||
export default forwardRef<HTMLFormElement>(
|
||||
function ResourceDepsForm(_props, ref) {
|
||||
const { saveCurWorkflow, setRoute } = useContext(WorkspaceContext);
|
||||
const [deps, setDeps] = useState<DepsResult>();
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [uploadingImage, setUploadingImage] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack gap={5}>
|
||||
{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`}
|
||||
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];
|
||||
errors: Record<string, string>;
|
||||
onClickRefetchModelList: () => void;
|
||||
}) {
|
||||
const inputKey = modelFile.filename;
|
||||
const [isEditing, setIsEditing] = useState(!modelFile.downloadUrl);
|
||||
|
||||
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 (
|
||||
<Stack ml={2}>
|
||||
<Flex key={modelFile.filename + modelFile.nodeType} gap={2}>
|
||||
<Tag height={"fit-content"} colorScheme={"teal"}>
|
||||
{modelFile.fileFolder}
|
||||
</Tag>
|
||||
|
||||
{modelFile.downloadUrl ? (
|
||||
<HStack>
|
||||
<a
|
||||
href={modelFile.infoUrl ?? modelFile.downloadUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{modelFile.filename}
|
||||
</a>
|
||||
<IconExternalLink size={18} />
|
||||
<IconButton
|
||||
ml={3}
|
||||
aria-label="edit"
|
||||
icon={<IconPencil size={18} />}
|
||||
size={"xs"}
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
/>
|
||||
</HStack>
|
||||
) : (
|
||||
<Stack>
|
||||
<span>
|
||||
{modelFile.filename} <span>❓</span>
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Stack style={{ display: isEditing ? "block" : "none" }}>
|
||||
<Tooltip
|
||||
label={"Model download link, supports Huggingface and Civitai"}
|
||||
>
|
||||
<Input
|
||||
name={inputKey}
|
||||
defaultValue={modelFile.downloadUrl ?? ""}
|
||||
size={"sm"}
|
||||
isInvalid={errors[inputKey] != null}
|
||||
placeholder={"https://civitai.com/api/download/models/324524"}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Text color={"red.400"}>{errors[inputKey]}</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
getCivitModelPageUrl,
|
||||
getHgModelInfoUrlFromDownloadUrl,
|
||||
} from "../utils/civitUtils";
|
||||
import { app } from "../utils/comfyapp";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
type LiteNode = {
|
||||
id: number;
|
||||
@@ -13,18 +14,13 @@ type LiteNode = {
|
||||
widgets_values?: Array<string | number>;
|
||||
};
|
||||
|
||||
export type ModelDep = {
|
||||
name: string;
|
||||
folder: string | null;
|
||||
hash: string | null;
|
||||
url: string | null;
|
||||
// TODO: will be deperecated
|
||||
filename?: string;
|
||||
fileFolder?: string | null;
|
||||
fileHash?: string | null;
|
||||
downloadUrl?: string | null;
|
||||
export type ModelFile = {
|
||||
filename: string;
|
||||
fileFolder: string | null;
|
||||
fileHash: string | null;
|
||||
downloadUrl: string | null;
|
||||
// optional info
|
||||
infoUrl?: string | null;
|
||||
infoUrl: string | null;
|
||||
nodeType?: string;
|
||||
inputName?: string;
|
||||
};
|
||||
@@ -45,12 +41,12 @@ export type DepsResult = {
|
||||
nodeRepos: NodeRepo[];
|
||||
};
|
||||
|
||||
export type DepsResultModel = ModelDep & {
|
||||
export type DepsResultModel = ModelFile & {
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type WorkspaceInfoDeps = {
|
||||
models: Record<string, ModelDep>;
|
||||
models: Record<string, ModelFile>;
|
||||
images: DepsResult["images"];
|
||||
nodeRepos: DepsResult["nodeRepos"];
|
||||
};
|
||||
@@ -62,18 +58,20 @@ async function fetchModelData(
|
||||
): Promise<DepsResultModel> {
|
||||
const res =
|
||||
(await indexdb.models.where("fileName").equals(filename).toArray()) ?? [];
|
||||
console.log("fetch modeldata", res);
|
||||
const first = res.at(0);
|
||||
const modelFile: DepsResultModel = {
|
||||
name: filename,
|
||||
filename,
|
||||
nodeType,
|
||||
hash: first?.fileHash ?? null,
|
||||
folder: first?.fileFolder ?? null,
|
||||
url: first?.civitModelVersionID
|
||||
fileHash: first?.fileHash ?? null,
|
||||
fileFolder: first?.fileFolder ?? null,
|
||||
downloadUrl: first?.civitModelVersionID
|
||||
? getCivitModelDownloadUrl(first.civitModelVersionID)
|
||||
: first?.downloadUrl ?? null,
|
||||
inputName: inputName,
|
||||
length: res.length,
|
||||
infoUrl: first?.civitModelID
|
||||
? getCivitModelPageUrl(first.civitModelID, first.civitModelVersionID)
|
||||
: getHgModelInfoUrlFromDownloadUrl(first?.downloadUrl ?? "") ?? null,
|
||||
};
|
||||
return modelFile;
|
||||
}
|
||||
@@ -97,9 +95,13 @@ export async function extractAndFetchFileNames(
|
||||
if (typeof value != "string") return;
|
||||
// Check if it's a model file
|
||||
if (modelFileExtensions.some((ext) => value.endsWith(ext))) {
|
||||
// if (curDeps?.models?.[value]) {
|
||||
// modelPromises.push(Promise.resolve(curDeps.models[value]));
|
||||
// } else {
|
||||
modelPromises.push(
|
||||
fetchModelData(value, node.class_type, inputName),
|
||||
);
|
||||
// }
|
||||
}
|
||||
// Check if it's an image file
|
||||
if (imageFileExtensions.some((ext) => value.endsWith(ext))) {
|
||||
@@ -133,7 +135,10 @@ export async function extractAndFetchFileNames(
|
||||
const models = await Promise.all(modelPromises);
|
||||
const modelsMap: Record<string, DepsResultModel> = {};
|
||||
models.forEach((model) => {
|
||||
modelsMap[model.name] = model;
|
||||
// if (model.nodeType) {
|
||||
// model.gitRepo = reposMapping[model.nodeType];
|
||||
// }
|
||||
modelsMap[model.filename] = model;
|
||||
});
|
||||
const nodeRepos: Record<string, NodeRepo> = {};
|
||||
Object.values(reposMapping).forEach((repo) => {
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
// @ts-expect-error
|
||||
import { app } from "/scripts/app.js";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import {
|
||||
changelogsTable,
|
||||
userSettingsTable,
|
||||
workflowsTable,
|
||||
} from "../db-tables/WorkspaceDB";
|
||||
import { Tag, Tooltip } from "@chakra-ui/react";
|
||||
import { Tag, Tooltip, useToast } from "@chakra-ui/react";
|
||||
import { matchShortcut } from "../utils";
|
||||
import { EShortcutKeys } from "../types/dbTypes";
|
||||
import useDebounceFn from "../customHooks/useDebounceFn";
|
||||
import {
|
||||
COMFYSPACE_TRACKING_FIELD_NAME,
|
||||
SHORTCUT_TRIGGER_EVENT,
|
||||
} from "../const";
|
||||
import { app } from "../utils/comfyapp";
|
||||
import { deepJsonDiffCheck } from "../utils/deepJsonDiffCheck";
|
||||
import { SHORTCUT_TRIGGER_EVENT } from "../const";
|
||||
|
||||
export default function AppIsDirtyEventListener() {
|
||||
const {
|
||||
isDirty,
|
||||
setIsDirty,
|
||||
setRoute,
|
||||
saveCurWorkflow,
|
||||
setCurFlowIDAndName,
|
||||
} = useContext(WorkspaceContext);
|
||||
const { isDirty, setIsDirty, setRoute, saveCurWorkflow } =
|
||||
useContext(WorkspaceContext);
|
||||
const isDirtRef = useRef(isDirty);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const toast = useToast();
|
||||
const autoSaveTimer = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,7 +27,7 @@ export default function AppIsDirtyEventListener() {
|
||||
}, [isDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
const keydownListener = (event: KeyboardEvent) => {
|
||||
const shortcutListener = (event: KeyboardEvent) => {
|
||||
if (document.visibilityState === "hidden") return;
|
||||
const matchResult = matchShortcut(event);
|
||||
if (matchResult) {
|
||||
@@ -65,17 +60,26 @@ export default function AppIsDirtyEventListener() {
|
||||
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;
|
||||
app.graph.onConfigure = function () {
|
||||
originalOnConfigure?.apply(this, arguments);
|
||||
// delay check diff json cuz it takes time for graph.serialize() to settle up and return the correct json
|
||||
setTimeout(() => {
|
||||
const id = app.graph.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.id;
|
||||
if (id == null || id != workflowsTable?.curWorkflow?.id) {
|
||||
setCurFlowIDAndName(null);
|
||||
const deepCheck = deepJsonDiffCheck(
|
||||
app.graph.serialize(),
|
||||
JSON.parse(workflowsTable?.curWorkflow?.json ?? "{}"),
|
||||
);
|
||||
if (!deepCheck) {
|
||||
debounceOnIsDirty();
|
||||
}
|
||||
}, 500);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
@@ -88,7 +92,7 @@ export default function AppIsDirtyEventListener() {
|
||||
debounceOnIsDirty();
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", keydownListener);
|
||||
document.addEventListener("keydown", shortcutListener);
|
||||
|
||||
if (
|
||||
userSettingsTable?.settings?.autoSave &&
|
||||
@@ -101,7 +105,7 @@ export default function AppIsDirtyEventListener() {
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", keydownListener);
|
||||
document.removeEventListener("keydown", shortcutListener);
|
||||
autoSaveTimer.current && clearInterval(autoSaveTimer.current);
|
||||
};
|
||||
}, []);
|
||||
@@ -128,10 +132,6 @@ export default function AppIsDirtyEventListener() {
|
||||
|
||||
const onIsDirty = async () => {
|
||||
if (workflowsTable?.curWorkflow?.saveLock || isDirtRef.current) return;
|
||||
const graph = app.graph.serialize();
|
||||
if (JSON.stringify(graph) === workflowsTable?.curWorkflow?.json) {
|
||||
return;
|
||||
}
|
||||
setIsDirty(true);
|
||||
if (
|
||||
userSettingsTable?.settings?.autoSave &&
|
||||
|
||||
+30
-25
@@ -5,8 +5,8 @@ import {
|
||||
IconFolder,
|
||||
IconGripVertical,
|
||||
IconPhoto,
|
||||
IconPlus,
|
||||
IconTriangleInvertedFilled,
|
||||
IconLock,
|
||||
} from "@tabler/icons-react";
|
||||
import DropdownTitle from "../components/DropdownTitle";
|
||||
import {
|
||||
@@ -17,27 +17,29 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import EditFlowName from "../components/EditFlowName";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import { PanelPosition } from "../types/dbTypes";
|
||||
import "./Topbar.css";
|
||||
import { SharedTopbarButton } from "../share/SharedTopbarButton";
|
||||
import VersionNameTopbar from "./VersionNameTopbar";
|
||||
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||
import { userSettingsTable } from "../db-tables/WorkspaceDB";
|
||||
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
||||
import TopbarNewWorkflowButton from "./TopbarNewWorkflowButton";
|
||||
const AppIsDirtyEventListener = lazy(() => import("./AppIsDirtyEventListener"));
|
||||
import MultipleTabs from "./multipleTabs";
|
||||
const ModelManagerTopbar = lazy(
|
||||
() => import("../model-manager/topbar/ModelManagerTopbar"),
|
||||
);
|
||||
const SpotlightSearch = lazy(() => import("../components/SpotlightSearch"));
|
||||
|
||||
interface Props {
|
||||
curFlowName: string | null;
|
||||
setCurFlowName: (newName: string) => void;
|
||||
}
|
||||
export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
||||
const { isDirty, saveCurWorkflow, setRoute, curFlowID, route } =
|
||||
useContext(WorkspaceContext);
|
||||
export function Topbar() {
|
||||
const {
|
||||
isDirty,
|
||||
loadNewWorkflow,
|
||||
saveCurWorkflow,
|
||||
setRoute,
|
||||
curFlowID,
|
||||
route,
|
||||
} = useContext(WorkspaceContext);
|
||||
const [positionStyle, setPositionStyle] = useState<PanelPosition>();
|
||||
const updatePanelPosition: (
|
||||
position?: PanelPosition,
|
||||
@@ -109,20 +111,21 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
||||
<Suspense fallback={<div style={{ width: "60px" }} />}>
|
||||
<ModelManagerTopbar />
|
||||
</Suspense>
|
||||
<TopbarNewWorkflowButton />
|
||||
<EditFlowName
|
||||
isDirty={isDirty}
|
||||
displayName={curFlowName ?? ""}
|
||||
updateFlowName={(newName) => {
|
||||
setCurFlowName(newName);
|
||||
requestAnimationFrame(() => {
|
||||
updatePanelPosition();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{workflowsTable?.curWorkflow?.saveLock && (
|
||||
<IconLock color="#FFF" size={20} />
|
||||
)}
|
||||
<Tooltip label="New workflow">
|
||||
<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>
|
||||
{curFlowID && (
|
||||
<HStack gap={"4px"}>
|
||||
<Tooltip label="Open gallery">
|
||||
@@ -151,6 +154,8 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
||||
) : (
|
||||
<div style={{ width: 1 }} />
|
||||
)}
|
||||
<MultipleTabs />
|
||||
<SharedTopbarButton />
|
||||
<VersionNameTopbar />
|
||||
<AppIsDirtyEventListener />
|
||||
<IconGripVertical
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
export const RE_RENDER_MULTIPLE_TABS_EVENT = "RE_RENDER_MULTIPLE_TABS_EVENT";
|
||||
export const OPEN_TAB_EVENT = "OPEN_TAB_EVENT";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
export type TabData = {
|
||||
id: string;
|
||||
name: string;
|
||||
json: string;
|
||||
isDirty: boolean;
|
||||
};
|
||||
type TabUpdateInput = Partial<TabData>;
|
||||
|
||||
class TabDataManager {
|
||||
tabs: TabData[] = [];
|
||||
activeIndex: number = 0;
|
||||
activeTab: TabData | null = null;
|
||||
|
||||
changeActiveTab(index: number, needLoadNewFlow: boolean = true) {
|
||||
this.saveCurTabJson();
|
||||
this.activeIndex = index;
|
||||
this.activeTab = this.tabs[index];
|
||||
this.notifyChanges(needLoadNewFlow ? "loadNewFlow" : "");
|
||||
}
|
||||
|
||||
updateTabData(index: number, updateInput: TabUpdateInput) {
|
||||
if (this.tabs[index]) {
|
||||
this.tabs[index] = { ...this.tabs[index], ...updateInput };
|
||||
this.activeTab = this.tabs[index];
|
||||
this.notifyChanges(updateInput.json ? "loadNewFlow" : "");
|
||||
}
|
||||
}
|
||||
|
||||
private saveCurTabJson() {
|
||||
const graphJson = JSON.stringify(app.graph.serialize());
|
||||
this.tabs[this.activeIndex].json = graphJson;
|
||||
}
|
||||
|
||||
addTabData(newTab: TabData) {
|
||||
const currentFlowIsDirty = this.activeTab?.isDirty;
|
||||
const existingIndex = this.tabs.findIndex((tab) => tab.id === newTab.id);
|
||||
|
||||
if (this.tabs.length === 0) {
|
||||
this.tabs.push(newTab);
|
||||
this.activeTab = newTab;
|
||||
} else {
|
||||
let nextActiveIndex = -1;
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
nextActiveIndex = existingIndex;
|
||||
} else if (currentFlowIsDirty) {
|
||||
nextActiveIndex = this.activeIndex + 1;
|
||||
this.tabs.splice(nextActiveIndex, 0, newTab);
|
||||
}
|
||||
|
||||
if (nextActiveIndex >= 0) {
|
||||
this.saveCurTabJson();
|
||||
this.activeIndex = nextActiveIndex;
|
||||
this.activeTab = this.tabs[nextActiveIndex];
|
||||
} else {
|
||||
this.updateTabData(this.activeIndex, newTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.notifyChanges("loadNewFlow");
|
||||
}
|
||||
|
||||
deleteTabData(index: number) {
|
||||
if (index < 0 || index >= this.tabs.length) return;
|
||||
|
||||
if (this.tabs.length === 1) {
|
||||
this.tabs = [];
|
||||
this.activeIndex = 0;
|
||||
this.activeTab = null;
|
||||
this.notifyChanges("clearCanvas");
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabs.splice(index, 1);
|
||||
|
||||
if (index <= this.activeIndex) {
|
||||
const isDeleteActiveTab = index === this.activeIndex;
|
||||
this.activeIndex = isDeleteActiveTab ? Math.min(this.tabs.length - 1, this.activeIndex) : this.activeIndex - 1;
|
||||
this.activeTab = this.tabs[this.activeIndex];
|
||||
this.notifyChanges(isDeleteActiveTab ? "loadNewFlow" : '');
|
||||
return;
|
||||
}
|
||||
this.notifyChanges('');
|
||||
}
|
||||
|
||||
batchDeleteTabData(ids: string[]) {
|
||||
if (this.tabs.length > 0) {
|
||||
this.tabs = this.tabs.filter(tab => !ids.includes(tab.id));
|
||||
if (this.tabs.length === 0) {
|
||||
this.activeIndex = 0;
|
||||
this.activeTab = null;
|
||||
this.notifyChanges("clearCanvas");
|
||||
return;
|
||||
} else {
|
||||
const activeIndex = this.tabs.findIndex(tab => tab.id === this.activeTab?.id);
|
||||
if (activeIndex === -1) {
|
||||
this.activeIndex = this.tabs.length - 1;
|
||||
this.activeTab = this.tabs[this.activeIndex];
|
||||
this.notifyChanges("loadNewFlow");
|
||||
return;
|
||||
} else if (activeIndex !== this.activeIndex) {
|
||||
this.activeIndex = activeIndex;
|
||||
this.activeTab = this.tabs[activeIndex];
|
||||
this.notifyChanges();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.notifyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private notifyChanges(otherAction?: string) {
|
||||
const event = new CustomEvent(RE_RENDER_MULTIPLE_TABS_EVENT, {
|
||||
detail: {
|
||||
tabs: this.tabs,
|
||||
activeIndex: this.activeIndex,
|
||||
activeTab: this.activeTab,
|
||||
otherAction,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
export const tabDataManager = new TabDataManager();
|
||||
@@ -0,0 +1,25 @@
|
||||
.workspace-multiple-tabs::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.workspace-multiple-tabs::-webkit-scrollbar-thumb {
|
||||
border-radius: 1em;
|
||||
background-color: rgba(130, 129, 129, 0.3);
|
||||
}
|
||||
|
||||
.workspace-multiple-tabs
|
||||
.workspace-multiple-tabs-item-action:hover
|
||||
.workspace-multiple-tabs-item-dirty {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.workspace-multiple-tabs-item-action:hover
|
||||
.workspace-multiple-tabs-item-dirty-close {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.workspace-multiple-tabs-item:hover .workspace-multiple-tabs-item-close {
|
||||
display: flex !important;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import { IconButton, HStack, Box } from "@chakra-ui/react";
|
||||
import { IconX, IconAsterisk, IconLock } from "@tabler/icons-react";
|
||||
import { useEffect, useContext, useReducer } from "react";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import { WorkspaceContext } from "../../WorkspaceContext";
|
||||
import {
|
||||
OPEN_TAB_EVENT,
|
||||
RE_RENDER_MULTIPLE_TABS_EVENT,
|
||||
} from "./TabDataManager";
|
||||
import { tabDataManager } from "./TabDataManager";
|
||||
import "./index.css";
|
||||
import { workflowsTable } from "../../db-tables/WorkspaceDB";
|
||||
import EditFlowName from "../../components/EditFlowName";
|
||||
import { useDialog } from "../../components/AlertDialogProvider";
|
||||
|
||||
export default function MultipleTabs() {
|
||||
const { showDialog } = useDialog();
|
||||
const {
|
||||
setCurFlowIDAndName,
|
||||
isDirty,
|
||||
setIsDirty,
|
||||
setCurVersion,
|
||||
curVersion,
|
||||
loadWorkflowID,
|
||||
saveCurWorkflow,
|
||||
} = useContext(WorkspaceContext);
|
||||
const [, reRenderDispatch] = useReducer((state) => state + 1, 0);
|
||||
|
||||
const onClose = (index: number) => {
|
||||
const deleteTabInfo = tabDataManager.tabs[index];
|
||||
if (deleteTabInfo.isDirty) {
|
||||
showDialog(
|
||||
`Please save or discard your changes to "${deleteTabInfo.name}" before leaving, or your changes will be lost.`,
|
||||
[
|
||||
{
|
||||
label: "Save",
|
||||
colorScheme: "teal",
|
||||
onClick: async () => {
|
||||
const deleteFlow = await workflowsTable?.get(deleteTabInfo.id);
|
||||
if (deleteFlow) {
|
||||
/**
|
||||
* Get the deleted flow information and update updateCurWorkflow,
|
||||
* Because workflowsTable.curWorkflow.id needs to be used when saving
|
||||
*/
|
||||
workflowsTable?.updateCurWorkflow({
|
||||
...deleteFlow,
|
||||
json: deleteTabInfo.json,
|
||||
});
|
||||
await saveCurWorkflow(deleteTabInfo.json);
|
||||
tabDataManager.deleteTabData(index);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Close",
|
||||
colorScheme: "red",
|
||||
onClick: async () => {
|
||||
tabDataManager.deleteTabData(index);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
tabDataManager.deleteTabData(index);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
tabDataManager.updateTabData(tabDataManager.activeIndex, { isDirty });
|
||||
}, [isDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
const reRender = (e: CustomEvent) => {
|
||||
const detail = e.detail;
|
||||
reRenderDispatch();
|
||||
switch (detail.otherAction) {
|
||||
case "clearCanvas":
|
||||
loadWorkflowID(null);
|
||||
break;
|
||||
case "loadNewFlow":
|
||||
app.loadGraphData(JSON.parse(detail.activeTab.json));
|
||||
workflowsTable?.get(detail.activeTab.id).then((flow) => {
|
||||
flow &&
|
||||
setCurFlowIDAndName({
|
||||
...flow,
|
||||
json: detail.activeTab.json,
|
||||
});
|
||||
});
|
||||
setIsDirty(detail.activeTab.isDirty);
|
||||
curVersion && setCurVersion(null);
|
||||
// Avoid scrolling when the corresponding element has not yet been rendered.
|
||||
setTimeout(() => {
|
||||
document
|
||||
.getElementById(
|
||||
`workspace-multiple-tabs-item-${detail.activeTab.id}`,
|
||||
)
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 0);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const openTab = (e: CustomEvent) => {
|
||||
const detail = e.detail;
|
||||
if (detail.tabInfo.id !== tabDataManager.activeTab?.id) {
|
||||
tabDataManager.addTabData(detail.tabInfo);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(RE_RENDER_MULTIPLE_TABS_EVENT, reRender);
|
||||
document.addEventListener(OPEN_TAB_EVENT, openTab);
|
||||
return () => {
|
||||
document.removeEventListener(RE_RENDER_MULTIPLE_TABS_EVENT, reRender);
|
||||
document.removeEventListener(OPEN_TAB_EVENT, reRender);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
spacing={0}
|
||||
className="workspace-multiple-tabs"
|
||||
overflowX="auto"
|
||||
maxW="calc(100vw - 380px)"
|
||||
>
|
||||
{tabDataManager.tabs.map((tab, index) => {
|
||||
const isActive = tabDataManager.activeIndex === index;
|
||||
const { id, name, isDirty } = tab;
|
||||
return (
|
||||
<Box
|
||||
key={`${id}-${name}`}
|
||||
className="workspace-multiple-tabs-item"
|
||||
id={`workspace-multiple-tabs-item-${id}`}
|
||||
cursor="pointer"
|
||||
border="1px"
|
||||
borderColor="#64646459"
|
||||
borderTopColor={isActive ? "#0078d4" : "#64646459"}
|
||||
borderRightWidth={0}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
h="32px"
|
||||
lineHeight="32px"
|
||||
padding="0 30px 0 10px"
|
||||
position="relative"
|
||||
bg={isActive ? "#1f1f1f" : "#181818"}
|
||||
_hover={{ bg: "#1f1f1f" }}
|
||||
_last={{ borderRightWidth: 1 }}
|
||||
onClick={() => {
|
||||
tabDataManager.activeIndex !== index &&
|
||||
tabDataManager.changeActiveTab(index);
|
||||
}}
|
||||
>
|
||||
<EditFlowName isActive={isActive} displayName={name ?? ""} />
|
||||
{isDirty ? (
|
||||
<Box
|
||||
className="workspace-multiple-tabs-item-action"
|
||||
position="absolute"
|
||||
right="4px"
|
||||
>
|
||||
<IconButton
|
||||
icon={<IconAsterisk color="white" width={14} height={14} />}
|
||||
size={"xs"}
|
||||
aria-label="un save flag"
|
||||
variant={"ghost"}
|
||||
display="flex"
|
||||
_hover={{ bg: "whiteAlpha.200" }}
|
||||
className="workspace-multiple-tabs-item-dirty"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(index);
|
||||
}}
|
||||
icon={<IconX color="white" width={14} height={14} />}
|
||||
size={"xs"}
|
||||
aria-label="close tab"
|
||||
variant={"ghost"}
|
||||
_hover={{ bg: "whiteAlpha.200" }}
|
||||
display="none"
|
||||
className="workspace-multiple-tabs-item-dirty-close"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(index);
|
||||
}}
|
||||
icon={<IconX color="white" width={14} height={14} />}
|
||||
size={"xs"}
|
||||
aria-label="close tab"
|
||||
variant={"ghost"}
|
||||
_hover={{ bg: "whiteAlpha.200" }}
|
||||
display={isActive ? "flex" : "none"}
|
||||
position="absolute"
|
||||
right="4px"
|
||||
className={isActive ? "" : "workspace-multiple-tabs-item-close"}
|
||||
/>
|
||||
)}
|
||||
{isActive && workflowsTable?.curWorkflow?.saveLock && (
|
||||
<IconLock color="#FFF" size={20} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
@@ -10,17 +10,17 @@ export interface SortableItem {
|
||||
export interface Workflow extends SortableItem {
|
||||
id: string;
|
||||
json: string;
|
||||
lastSavedJson?: string; // TODO will be deprecated
|
||||
name: string;
|
||||
coverMediaPath?: string | null;
|
||||
saveLock?: boolean;
|
||||
cloudID?: string | null;
|
||||
createTime: number;
|
||||
|
||||
// will be deprecated
|
||||
filePath?: string; // TODO will be deprecated
|
||||
privacy?: WorkflowPrivacy;
|
||||
tags?: string[];
|
||||
parentFolderID?: string | null; //TODO remove undefined, use null only
|
||||
coverMediaPath?: string;
|
||||
cloudID?: string; // TODO will be deprecated
|
||||
cloudOrigin?: string;
|
||||
saveLock?: boolean;
|
||||
latestImage?: string;
|
||||
topFieldsConfig?: TopFieldType[];
|
||||
}
|
||||
@@ -67,7 +67,6 @@ export type WorkflowVersion = {
|
||||
workflowID: string;
|
||||
json: string;
|
||||
createTime: number;
|
||||
api_prompt: string | null;
|
||||
cloudID?: string;
|
||||
cloudOrigin?: string;
|
||||
authorID?: string;
|
||||
@@ -132,6 +131,7 @@ export type Media = {
|
||||
workflowJSON?: string;
|
||||
createTime: number;
|
||||
localPath: string;
|
||||
format: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+18
-1
@@ -1,12 +1,29 @@
|
||||
import { SHORTCUT_TRIGGER_EVENT } from "../const";
|
||||
import {
|
||||
OPEN_TAB_EVENT,
|
||||
RE_RENDER_MULTIPLE_TABS_EVENT,
|
||||
TabData,
|
||||
} from "../topbar/multipleTabs/TabDataManager";
|
||||
import { EOtherKeys, EShortcutKeys } from "./dbTypes";
|
||||
|
||||
interface ShortcutTriggerDetail {
|
||||
shortcutType: EShortcutKeys | EOtherKeys;
|
||||
}
|
||||
|
||||
interface reRenderTabsDetail {
|
||||
tabs: TabData;
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
interface reRenderTabsDetail {
|
||||
tabs: TabData;
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface WindowEventMap {
|
||||
interface DocumentEventMap {
|
||||
[SHORTCUT_TRIGGER_EVENT]: CustomEvent<ShortcutTriggerDetail>;
|
||||
[RE_RENDER_MULTIPLE_TABS_EVENT]: CustomEvent<reRenderTabsDetail>;
|
||||
[OPEN_TAB_EVENT]: CustomEvent;
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -1,7 +1,22 @@
|
||||
import { deleteFile, updateFile } from "./Api";
|
||||
import { ESortTypes } from "./RecentFilesDrawer/types";
|
||||
import { userSettingsTable } from "./db-tables/WorkspaceDB";
|
||||
import { Workflow, EShortcutKeys, SortableItem } from "./types/dbTypes";
|
||||
import { app } from "./utils/comfyapp";
|
||||
import { workflowsTable, userSettingsTable } from "./db-tables/WorkspaceDB";
|
||||
import {
|
||||
generateFilePathAbsolute,
|
||||
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
|
||||
declare let LiteGraph: any, LGraph: any, LGraphCanvas: any;
|
||||
@@ -141,6 +156,22 @@ export function sortFlows(
|
||||
|
||||
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>(
|
||||
items: T[],
|
||||
@@ -310,3 +341,21 @@ export const openWorkflowInNewTab = (workflowID: string) => {
|
||||
const newHash = generateUrlHashWithFlowId(workflowID);
|
||||
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,13 +1,7 @@
|
||||
// @ts-ignore
|
||||
export let app: any | null = null;
|
||||
export const app = window.app;
|
||||
const api_base = location.pathname.split("/").slice(0, -1).join("/");
|
||||
|
||||
export let api: any | null = null;
|
||||
|
||||
import(api_base + "/scripts/api.js").then((apiJs) => {
|
||||
api = apiJs?.api;
|
||||
});
|
||||
|
||||
import(api_base + "/scripts/app.js").then((appJs) => {
|
||||
app = appJs?.app;
|
||||
export const api = import(api_base + "/scripts/app.js").then((api) => {
|
||||
return api;
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ScanLocalFolder,
|
||||
scanLocalFiles,
|
||||
} from "../apis/TwowaySyncApi";
|
||||
import { isFolder, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||
import { isFolder, userSettingsTable } from "../db-tables/WorkspaceDB";
|
||||
import { indexdb } from "../db-tables/indexdb";
|
||||
import { Folder, Workflow } from "../types/dbTypes";
|
||||
import { sortFileItem } from "../utils";
|
||||
@@ -34,33 +34,21 @@ export async function scanMyWorkflowsDir(
|
||||
return folder;
|
||||
} else {
|
||||
// is workflow
|
||||
// const existingWorkflow = (await indexdb.workflows
|
||||
// ?.get(file.id)
|
||||
// .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 existingWorkflow = (await indexdb.workflows?.get(file.id)) ?? {
|
||||
createTime: Date.now(),
|
||||
updateTime: Date.now(),
|
||||
};
|
||||
|
||||
const newWorkflow: Workflow = {
|
||||
// ...existingWorkflow,
|
||||
...existingWorkflow,
|
||||
id: file.id,
|
||||
json: file.json ?? "{}",
|
||||
cloudID: file.cloudID,
|
||||
coverMediaPath: file.coverMediaPath,
|
||||
saveLock: file.saveLock,
|
||||
name: fileName.replace(/\.json$/, ""),
|
||||
parentFolderID: parentRelPath,
|
||||
createTime: file.createTime,
|
||||
// setting updateTime will result latestVersionChcek() always fail if in
|
||||
// workflow.get() not pull updateTime from file
|
||||
updateTime: file.updateTime,
|
||||
// updateTime: file.updateTime,
|
||||
};
|
||||
return newWorkflow;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user