Merge branch 'main' into dev
This commit is contained in:
@@ -4,15 +4,11 @@ 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)!
|
||||
|
||||
<a href="https://discord.gg/HHvfEurv2Z">
|
||||
<img src="https://animeartmagazine.com/wp-content/uploads/2022/05/discord-button600.webp" alt="Join Discord" width="120"/>
|
||||
</a>
|
||||
<br/>
|
||||
[New!] we're building a comfyui custom nodes library wiki like Pypi: https://www.nodecafe.org/
|
||||
welcome any feedbacks!
|
||||
|
||||
<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
|
||||
|
||||
### [Option 1] via ComfyUI Manager (Preferred)
|
||||
@@ -33,14 +29,18 @@ It is recommended to do git clone than downloading zip folder, because you can g
|
||||
|
||||
## Features
|
||||
|
||||
🚧🔴 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.
|
||||
|
||||
- **Workflows manager**
|
||||
- 🔁Switch between different workflows easily. 1click open workflow in multiple browser tabs.
|
||||
- 🤏**Drag and drop** to insert subworkflows into current flow.
|
||||
- 🗂️Organize workflows with folders, 🏷️tags
|
||||
- 📂Save and sync all your workflows in a local folder (by default under `/ComfyUI/my_workflows` customize this location in Settings)
|
||||
- Save workflow by pressing the 💾 button, or ⌨Shift+S shortcut (customize this shortcut 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
|
||||
- Save workflow by pressing the 💾 button, or ⌨Shift+S shortcut (customize this shortcut 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
|
||||
- 🕛**Version control** of workflow, create/switch versions
|
||||
- Bulk import workflows into your workspace
|
||||
- 1click download all your workflows into a .zip
|
||||
@@ -56,24 +56,22 @@ It is recommended to do git clone than downloading zip folder, because you can g
|
||||
|
||||
Recommend using Chrome browser for the best experience. Safari doesn't work well. Edge or firefox should be fine too.
|
||||
|
||||
[中文版教程](https://www.uisdc.com/workspace-manager) [Roadmap](https://github.com/orgs/11cafe/projects/1/views/1)
|
||||
[中文版教程](https://www.uisdc.com/workspace-manager) [Roadmap](https://github.com/orgs/11cafe/projects/1/views/1)
|
||||
|
||||
### 🦄v2.0 beta: 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
|
||||
- models list sort by date/name
|
||||
v2.0 demo: https://youtu.be/7Hht_BMe844
|
||||
v2.0 demo: https://youtu.be/7Hht_BMe844
|
||||
|
||||
**Make sure you clicked these 2 buttons after updating to v2.0**
|
||||
**Make sure you clicked these 2 buttons after updating to v2.0**
|
||||
Once you have enabled two-way sync, DO NOT disable it again. We are migrating away from browser storage and embracing two-way sync with your local disk, so that everything is persistently stored in your disk.
|
||||
|
||||
<img width="541" alt="Screenshot 2024-03-11 at 12 24 35 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/4b0d44ba-b2e4-4077-bb8a-262829787c05">
|
||||
|
||||
|
||||
|
||||
|
||||
**v1.7 ComfyUI model manager, view your installed models, install models from civit ai, open workflow in multiple browser tabs**
|
||||
|
||||
<img width="300" alt="Screenshot 2024-02-02 at 5 51 31 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/a95dbf03-bd5a-43ce-af1f-b6ff246acf77">
|
||||
@@ -121,10 +119,9 @@ By default, We also enabled auto-saving your current workflow as a cache in case
|
||||
<img width="297" alt="Screenshot 2024-01-23 at 12 37 37 AM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/9ca0a63e-ddae-4436-a374-6ef0276c23fb">
|
||||
<img width="402" alt="Screenshot 2024-01-22 at 2 33 00 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/061a8463-f7e6-4985-9516-8117d7a5edd5">
|
||||
|
||||
|
||||
### Where are my workflow and workspace data stored?
|
||||
|
||||
🗂️All your workflows are saved in a folder in `/ComfyUI/my_workflows`, you can change this path in settings.
|
||||
🗂️All your workflows are saved in a folder in `/ComfyUI/my_workflows`, you can change this path in settings.
|
||||
|
||||
<img width="360" alt="Screenshot 2024-01-02 at 10 22 45 PM" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/4751b096-0886-4a32-95b1-87c7ce5b3787">
|
||||
|
||||
@@ -132,7 +129,6 @@ Other metadata (versions, gallery images) is primarily stored in your browser ca
|
||||
|
||||
<img width="607" alt="303892585-c04ca874-03f0-4569-9a40-d8453dd9b031" src="https://github.com/11cafe/comfyui-workspace-manager/assets/18367033/17097e17-743d-499f-8f86-0eb1193d0e46">
|
||||
|
||||
|
||||
**Looking for colaborators and coworkers** to develop this ComfyUI project management tool. If you know React, Python or ML model deployment, please reach out to weixuanfu01@gmail.com We want to make it easy to **manage, share and deploy** ComfyUI project.
|
||||
|
||||
## Upcoming Features
|
||||
|
||||
+12
-12
@@ -36,19 +36,19 @@ Current hooks include:
|
||||
3. Modify ComfyUI/web/index.html and add the following code. It should be noted that the port number in localhost:5173 needs to be consistent with the port number of the vite local service started by npm run dev.
|
||||
|
||||
```javascript
|
||||
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
import RefreshRuntime from "http://localhost:5173/@react-refresh";
|
||||
RefreshRuntime.injectIntoGlobalHook(window);
|
||||
window.$RefreshReg$ = () => {};
|
||||
window.$RefreshSig$ = () => (type) => type;
|
||||
window.__vite_plugin_react_preamble_installed__ = true;
|
||||
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const viteClientScript = document.createElement('script');
|
||||
viteClientScript.src = 'http://localhost:5173/@vite/client';
|
||||
viteClientScript.type = 'module';
|
||||
const head = document.getElementsByTagName("head")[0];
|
||||
const viteClientScript = document.createElement("script");
|
||||
viteClientScript.src = "http://localhost:5173/@vite/client";
|
||||
viteClientScript.type = "module";
|
||||
head.appendChild(viteClientScript);
|
||||
const workspaceMainScript = document.createElement('script');
|
||||
workspaceMainScript.src = 'http://localhost:5173/src/main.tsx';
|
||||
workspaceMainScript.type = 'module';
|
||||
const workspaceMainScript = document.createElement("script");
|
||||
workspaceMainScript.src = "http://localhost:5173/src/main.tsx";
|
||||
workspaceMainScript.type = "module";
|
||||
head.appendChild(workspaceMainScript);
|
||||
```
|
||||
|
||||
Vendored
+27
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{r as p}from"./input.js";import{app as n}from"/scripts/app.js";import{a5 as v,j as m,ao as S,o as y,E as w,A as s}from"./App-to8jIIgG.js";import"/scripts/api.js";function C(){const{setIsDirty:r,setRoute:h,saveCurWorkflow:g}=p.useContext(v),k=m();p.useEffect(()=>{const o=async t=>{switch(t){case w.SAVE:g();break;case w.SAVE_AS:h("saveAsModal");break}};document.addEventListener("click",t=>{(n.canvas.node_over!=null||n.canvas.node_capturing_input!=null||n.canvas.node_widget!=null)&&i()}),document.addEventListener("keydown",async function(t){var a;if(document.visibilityState==="hidden")return;const e=await S(t);e?o(e):(a=t.target)!=null&&a.matches("input, textarea")&&i()})},[]);const i=async()=>{var t,e,a,c,u,l,d;if((e=(t=s)==null?void 0:t.curWorkflow)!=null&&e.saveLock)return;const o=await((a=y)==null?void 0:a.getSetting("autoSave"));if(o||r(!0),(u=(c=s)==null?void 0:c.curWorkflow)!=null&&u.id&&o)if(!await((l=s)==null?void 0:l.latestVersionCheck()))k({title:"You are working on an outdated version",description:"This workflow is changed by another tab. Please close the page and reopen to get the latest version.",status:"warning",duration:5e3,isClosable:!0});else{const f=JSON.stringify(n.graph.serialize());f!=null&&await((d=s)==null?void 0:d.updateFlow(s.curWorkflow.id,{json:f})),r(!1)}};return null}export{C as default};
|
||||
+2
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
import{h as a}from"./App-to8jIIgG.js";var p=a("copy","IconCopy",[["path",{d:"M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z",key:"svg-0"}],["path",{d:"M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1",key:"svg-1"}]]);export{p as I};
|
||||
@@ -0,0 +1 @@
|
||||
import{r as c,j as e,h as d,P as f}from"./input.js";import{app as m}from"/scripts/app.js";import{G as x,g as j,I as v}from"./ModelManagerTopbar-hgv1rxNU.js";import{l as h,H as w,a9 as g,B as _,ab as M,ac as E,aJ as I,F as S,am as b}from"./App-to8jIIgG.js";import{V as k}from"./chunk-NTCQBYKE-m03PV2NY.js";import{I as y,G as C}from"./chunk-JARCRF6W-kavLurMU.js";import"/scripts/api.js";import"./chunk-3RSXBRAN-vYdmMhxP.js";import"./index-pz0vT7Or.js";function z({model:s}){const[a,t]=c.useState([]),n=D(s.received_value),p=async()=>{const l=await(await fetch(`https://huggingface.co/api/models?limit=3&search=${n}&full=true`)).json(),i=[];l.slice(0,1).forEach(({modelId:r,siblings:H})=>{i.push({name:r,downloadUrl:`https://huggingface.co/${r}/resolve/main/`,url:`https://huggingface.co/${r}/tree/main`,icon:"https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png"})}),t(r=>[...r,...i])},u=async()=>{const o=await j(n);t(l=>[...l,...o==null?void 0:o.slice(0,1).map(i=>({name:i.name,url:"https://civitai.com/models/"+i.id,icon:"https://images.crunchbase.com/image/upload/c_pad,h_170,w_170,f_auto,b_white,q_auto:eco,dpr_1/gtxrcmtsvpjjevozblfa"}))])};return c.useEffect(()=>{p(),u()},[]),e.jsx(x,{p:3,shadow:"md",borderWidth:"1px",children:e.jsxs(k,{align:"start",children:[e.jsx(h,{children:s.class_type}),e.jsxs(w,{ml:3,children:[e.jsxs(h,{children:[s.input_name,": "]}),e.jsx(h,{color:"red.400",children:s.received_value})]}),a.map(({url:o,name:l,icon:i})=>e.jsx(d,{size:"sm",onClick:r=>{r.stopPropagation(),window.open(o,"_blank")},leftIcon:e.jsx(y,{width:5,src:i}),rightIcon:e.jsx(g,{size:20}),children:l})),e.jsx(d,{colorScheme:"blue",mt:5,iconSpacing:1,leftIcon:e.jsx(g,{size:20}),size:"sm",onClick:()=>{window.open(`https://civitai.com/search/models?sortBy=models_v5&query=${n}`,"_blank")},children:"Search in CivitAI"})]})})}function D(s){return s.replace(/^.*(\\|\/|\:)/,"").replace(/\.[^/.]+$/,"").replaceAll("_"," ").replace(/([A-Z])/g," $1").trim()}function B({onClose:s,missingModels:a}){c.useEffect(()=>(m.canvasEl.addEventListener("click",s),()=>{m.canvasEl.removeEventListener("click",s)}),[]);const t=440;return e.jsx(f,{children:e.jsx(_,{style:{width:t},children:e.jsxs(M,{width:t,height:"100vh",p:4,gap:2,position:"fixed",top:0,left:0,shadow:"xl",zIndex:E,overflowY:"auto",children:[e.jsx(I,{children:e.jsxs(S,{justifyContent:"space-between",alignContent:"center",children:[e.jsxs(b,{size:"md",mr:2,children:["Missing Models (",a.length,")"]}),e.jsx(v,{})]})}),e.jsx(C,{templateColumns:"1",gap:1,marginTop:2,children:a.map(n=>e.jsx(z,{model:n},n.received_value))})]})})})}function T({}){const[s,a]=c.useState(!1),[t,n]=c.useState([]);return c.useEffect(()=>{},[]),t.length===0?null:e.jsxs(e.Fragment,{children:[e.jsxs(d,{size:"sm","aria-label":"missing models",px:2,onClick:()=>a(!0),children:["Install Missing (",t.length,")"]}),s&&e.jsx(B,{onClose:()=>a(!1),missingModels:t})]})}export{T as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{f as x,j as l,g as i,e as b,k as C,o as E,l as I,r as f,ah as P}from"./input.js";import{_ as z}from"./App-to8jIIgG.js";var _=x(function(o,s){const{children:t,placeholder:n,className:r,...a}=o;return l.jsxs(i.select,{...a,ref:s,className:b("chakra-select",r),children:[n&&l.jsx("option",{value:"",children:n}),t]})});_.displayName="SelectField";function F(e,o){const s={},t={};for(const[n,r]of Object.entries(e))o.includes(n)?s[n]=r:t[n]=r;return[s,t]}var H=x((e,o)=>{var s;const t=C("Select",e),{rootProps:n,placeholder:r,icon:a,color:c,height:S,h:d,minH:h,minHeight:y,iconColor:p,iconSize:u,...j}=E(e),[g,N]=F(j,P),m=z(N),k={width:"100%",height:"fit-content",position:"relative",color:c},w={paddingEnd:"2rem",...t.field,_focus:{zIndex:"unset",...(s=t.field)==null?void 0:s._focus}};return l.jsxs(i.div,{className:"chakra-select__wrapper",__css:k,...g,...n,children:[l.jsx(_,{ref:o,height:d??S,minH:h??y,placeholder:r,...m,__css:w,children:e.children}),l.jsx(v,{"data-disabled":I(m.disabled),...(p||c)&&{color:p||c},__css:t.icon,...u&&{fontSize:u},children:a})]})});H.displayName="Select";var M=e=>l.jsx("svg",{viewBox:"0 0 24 24",...e,children:l.jsx("path",{fill:"currentColor",d:"M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"})}),R=i("div",{baseStyle:{position:"absolute",display:"inline-flex",alignItems:"center",justifyContent:"center",pointerEvents:"none",top:"50%",transform:"translateY(-50%)"}}),v=e=>{const{children:o=l.jsx(M,{}),...s}=e,t=f.cloneElement(o,{role:"presentation",className:"chakra-select__icon",focusable:!1,"aria-hidden":!0,style:{width:"1em",height:"1em",color:"currentColor"}});return l.jsx(R,{...s,className:"chakra-select__icon-wrapper",children:f.isValidElement(o)?t:null})};v.displayName="SelectIcon";export{H as S};
|
||||
@@ -0,0 +1 @@
|
||||
import{f as o,n as t,o as c,j as l,g as k,e as m}from"./input.js";var f=o(function(s,e){const n=t("Link",s),{className:r,isExternal:a,...i}=c(s);return l.jsx(k.a,{target:a?"_blank":void 0,rel:a?"noopener":void 0,ref:e,className:m("chakra-link",r),...i,__css:n})});f.displayName="Link";export{f as L};
|
||||
@@ -4,36 +4,43 @@ import base64
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
MAX_IMAGE_SIZE = 400
|
||||
MAX_IMAGE_SIZE = 250
|
||||
|
||||
def preview_file(filename: str):
|
||||
preview_exts = [".jpg", ".png", ".jpeg", ".gif"]
|
||||
preview_exts = [*preview_exts, *[".preview" + x for x in preview_exts]]
|
||||
for ext in preview_exts:
|
||||
pathStr = os.path.splitext(filename)[0] + ext
|
||||
path = Path(pathStr).resolve()
|
||||
if os.path.exists(path):
|
||||
# because ComfyUI has extra model path feature
|
||||
# the path might not be relative to the ComfyUI root
|
||||
# so instead of returning the path, we return the image data directly, to avoid security issues
|
||||
bytes = get_thumbnail_for_image_file(path)
|
||||
# Get the base64 string
|
||||
img_base64 = base64.b64encode(bytes).decode()
|
||||
# Return the base64 string
|
||||
return f"data:image/jpeg;base64, {img_base64}"
|
||||
try:
|
||||
pathStr = os.path.splitext(filename)[0] + ext
|
||||
path = Path(pathStr).resolve()
|
||||
if os.path.exists(path):
|
||||
# because ComfyUI has extra model path feature
|
||||
# the path might not be relative to the ComfyUI root
|
||||
# so instead of returning the path, we return the image data directly, to avoid security issues
|
||||
bytes = get_thumbnail_for_image_file(path)
|
||||
# Get the base64 string
|
||||
img_base64 = base64.b64encode(bytes).decode()
|
||||
# Return the base64 string
|
||||
return f"data:image/jpeg;base64, {img_base64}"
|
||||
except Exception as e:
|
||||
print(f"Error opening image preview: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_thumbnail_for_image_file(file_path: Path):
|
||||
with Image.open(file_path) as img:
|
||||
# If the image is too large, resize it
|
||||
if img.width > MAX_IMAGE_SIZE and img.height > MAX_IMAGE_SIZE:
|
||||
# Calculate new width to maintain aspect ratio
|
||||
width = int(img.width * MAX_IMAGE_SIZE / img.height)
|
||||
# Resize the image
|
||||
img = img.resize((width, MAX_IMAGE_SIZE))
|
||||
img = img.convert("RGB")
|
||||
# Save the image to a BytesIO object
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="JPEG", quality=85)
|
||||
return buffer.getvalue()
|
||||
try:
|
||||
with Image.open(file_path) as img:
|
||||
# If the image is too large, resize it
|
||||
if img.width > MAX_IMAGE_SIZE and img.height > MAX_IMAGE_SIZE:
|
||||
# Calculate new width to maintain aspect ratio
|
||||
width = int(img.width * MAX_IMAGE_SIZE / img.height)
|
||||
# Resize the image
|
||||
img = img.resize((width, MAX_IMAGE_SIZE))
|
||||
img = img.convert("RGB")
|
||||
# Save the image to a BytesIO object
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="JPEG", quality=85)
|
||||
return buffer.getvalue()
|
||||
except Exception as e:
|
||||
print(f"Error opening image preview: {e}")
|
||||
return None
|
||||
|
||||
+13
-101
@@ -20,17 +20,16 @@ import {
|
||||
workflowVersionsTable,
|
||||
} from "./db-tables/WorkspaceDB";
|
||||
import { defaultGraph } from "./defaultGraph";
|
||||
import { JsonDiff, WorkspaceContext } from "./WorkspaceContext";
|
||||
import { WorkspaceContext } from "./WorkspaceContext";
|
||||
import {
|
||||
getFileUrl,
|
||||
matchShortcut,
|
||||
getWorkflowIdInUrlHash,
|
||||
generateUrlHashWithFlowId,
|
||||
openWorkflowInNewTab,
|
||||
validateOrSaveAllJsonFileMyWorkflows,
|
||||
} from "./utils";
|
||||
import { Topbar } from "./topbar/Topbar";
|
||||
import { EShortcutKeys, Workflow, WorkflowVersion } from "./types/dbTypes";
|
||||
import { Workflow, WorkflowVersion } from "./types/dbTypes";
|
||||
import { useDialog } from "./components/AlertDialogProvider";
|
||||
import React from "react";
|
||||
const RecentFilesDrawer = React.lazy(
|
||||
@@ -46,7 +45,7 @@ import { useStateRef } from "./customHooks/useStateRef";
|
||||
import { indexdb } from "./db-tables/indexdb";
|
||||
import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm";
|
||||
import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck";
|
||||
|
||||
const AppEventListener = React.lazy(() => import("./topbar/AppEventListener"));
|
||||
const ModelManagerTopbar = React.lazy(
|
||||
() => import("./model-manager/topbar/ModelManagerTopbar"),
|
||||
);
|
||||
@@ -70,12 +69,8 @@ export default function App() {
|
||||
const { showDialog } = useDialog();
|
||||
const [loadChild, setLoadChild] = useState(false);
|
||||
const developmentEnvLoadFirst = useRef(false);
|
||||
const autoSaveTimer = useRef(0);
|
||||
const toast = useToast();
|
||||
const workflowOverwriteNoticeStateRef = useRef("hide"); // disabled/hide/show;
|
||||
const [jsonDiff, setJsonDiff] = useState<JsonDiff>(null);
|
||||
const [curVersion, setCurVersion, curVersionRef] =
|
||||
useStateRef<WorkflowVersion | null>(null);
|
||||
const [curVersion, setCurVersion] = useStateRef<WorkflowVersion | null>(null);
|
||||
const saveCurWorkflow = useCallback(async () => {
|
||||
if (curFlowID.current) {
|
||||
if (workflowsTable?.curWorkflow?.saveLock) {
|
||||
@@ -120,6 +115,10 @@ export default function App() {
|
||||
};
|
||||
|
||||
const discardUnsavedChanges = async () => {
|
||||
if (userSettingsTable?.autoSave) {
|
||||
alert("You cannot discard unsaved changes when auto save is enabled");
|
||||
return;
|
||||
}
|
||||
const userInput = confirm(
|
||||
"Are you sure you want to discard unsaved changes? This will revert current workflow to your last saved version. You will lose all changes made since your last save.",
|
||||
);
|
||||
@@ -128,19 +127,14 @@ export default function App() {
|
||||
// User clicked OK
|
||||
const curID = workflowsTable?.curWorkflow?.id;
|
||||
if (curID == null) return;
|
||||
let lastSavedJson;
|
||||
if (userSettingsTable?.autoSave) {
|
||||
lastSavedJson = (await changelogsTable?.getLastestByWorkflowID(curID))
|
||||
?.json;
|
||||
} else {
|
||||
lastSavedJson = (await workflowsTable?.get(curID))?.json;
|
||||
}
|
||||
const lastSavedJson = (await workflowsTable?.get(curID))?.json;
|
||||
if (lastSavedJson) {
|
||||
workflowsTable?.updateCurWorkflow({
|
||||
...workflowsTable.curWorkflow!,
|
||||
json: lastSavedJson,
|
||||
});
|
||||
await app.loadGraphData(JSON.parse(lastSavedJson));
|
||||
setIsDirty(false);
|
||||
} else {
|
||||
alert("Error: No last saved version found");
|
||||
}
|
||||
@@ -189,7 +183,6 @@ export default function App() {
|
||||
if (latestWfID) {
|
||||
loadWorkflowIDImpl(latestWfID);
|
||||
}
|
||||
|
||||
await validateOrSaveAllJsonFileMyWorkflows();
|
||||
|
||||
indexdb.cache.get(UPGRADE_TO_2WAY_SYNC_KEY).then(async (value) => {
|
||||
@@ -258,10 +251,8 @@ export default function App() {
|
||||
|
||||
const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => {
|
||||
if (app.graph == null) {
|
||||
console.error("app.graph is null cannot load workflow");
|
||||
return;
|
||||
}
|
||||
|
||||
const flow = await workflowsTable?.get(id);
|
||||
// If the currently loaded flow does not exist, you need to clear the URL hash and localStorage to avoid popping up another prompt that the flow does not exist when refreshing the page.
|
||||
if (flow == null) {
|
||||
@@ -287,20 +278,9 @@ export default function App() {
|
||||
app.loadGraphData(JSON.parse(flow.json));
|
||||
}
|
||||
setRoute("root");
|
||||
/**
|
||||
* By an unlocked flow with isDirty true and unsaved,
|
||||
* When switching to a locked flow, isDirty is still true, and checkIsDirty() is not executed in autoSaveTimer.
|
||||
* causes * to be displayed in front of the topbar flow name,
|
||||
* So add this logic and reset isDirty every time you open a new process
|
||||
* In fact, it is reasonable to reset isDirty every time you open a new flow.
|
||||
*/
|
||||
isDirty && setIsDirty(false);
|
||||
};
|
||||
|
||||
const compareJsonDiff = (diff: { old: Object; new: Object } | null) => {
|
||||
setJsonDiff(diff);
|
||||
};
|
||||
|
||||
const loadWorkflowID = async (
|
||||
id: string | null,
|
||||
versionID?: string | null,
|
||||
@@ -447,72 +427,6 @@ export default function App() {
|
||||
}
|
||||
graphAppSetup();
|
||||
setLoadChild(true);
|
||||
autoSaveTimer.current = setInterval(async () => {
|
||||
const autoSaveEnabled = userSettingsTable?.autoSave;
|
||||
if (workflowsTable?.curWorkflow?.saveLock) return;
|
||||
const isDirty = checkIsDirty();
|
||||
setIsDirty(!!isDirty);
|
||||
if (!isDirty) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
curVersionRef.current != null &&
|
||||
curVersionRef.current.json !== JSON.stringify(app.graph.serialize())
|
||||
) {
|
||||
setCurVersion(null);
|
||||
}
|
||||
if (curFlowID.current != null && autoSaveEnabled) {
|
||||
const isLatest = await workflowsTable?.latestVersionCheck();
|
||||
if (
|
||||
workflowOverwriteNoticeStateRef.current !== "disabled" &&
|
||||
!isLatest
|
||||
) {
|
||||
workflowOverwriteNoticeStateRef.current = "show";
|
||||
showDialog(
|
||||
`This notification is to inform you that it appears you've opened the same workflow in multiple tabs. If all tabs are set to auto-save, this may lead to conflicts and potential data loss.\n\nYou might want to consider disabling auto-save to prevent this. We will remind you to save changes if you leave a page with unsaved work.`,
|
||||
[
|
||||
{
|
||||
label: "Do Not Remind Again",
|
||||
onClick: () => {
|
||||
workflowOverwriteNoticeStateRef.current = "disabled";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Disable Auto-Save",
|
||||
colorScheme: "teal",
|
||||
onClick: () => {
|
||||
userSettingsTable?.upsert({ autoSave: false });
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// autosave workflow if enabled
|
||||
const graphJson = JSON.stringify(app.graph.serialize());
|
||||
graphJson != null &&
|
||||
(await workflowsTable?.updateFlow(curFlowID.current!, {
|
||||
json: graphJson,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const shortcutListener = async (event: KeyboardEvent) => {
|
||||
if (document.visibilityState === "hidden") return;
|
||||
|
||||
const matchResult = await matchShortcut(event);
|
||||
|
||||
switch (matchResult) {
|
||||
case EShortcutKeys.SAVE:
|
||||
saveCurWorkflow();
|
||||
break;
|
||||
case EShortcutKeys.SAVE_AS:
|
||||
setRoute("saveAsModal");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", shortcutListener);
|
||||
|
||||
const fileInput = document.getElementById(
|
||||
"comfy-file-input",
|
||||
@@ -588,13 +502,10 @@ export default function App() {
|
||||
app.canvasEl.addEventListener("drop", handleDrop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", shortcutListener);
|
||||
window.removeEventListener("change", fileInputListener);
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
window.removeEventListener("executed", handleExecuted);
|
||||
app.canvasEl.removeEventListener("drop", handleDrop);
|
||||
|
||||
clearInterval(autoSaveTimer.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -611,13 +522,13 @@ export default function App() {
|
||||
discardUnsavedChanges: discardUnsavedChanges,
|
||||
saveCurWorkflow: saveCurWorkflow,
|
||||
isDirty: isDirty,
|
||||
setIsDirty: setIsDirty,
|
||||
loadNewWorkflow: loadNewWorkflow,
|
||||
loadFilePath: loadFilePath,
|
||||
setRoute: setRoute,
|
||||
route: route,
|
||||
jsonDiff: jsonDiff,
|
||||
compareJson: compareJsonDiff,
|
||||
curVersion: curVersion,
|
||||
setCurVersion: setCurVersion,
|
||||
}}
|
||||
>
|
||||
<div ref={workspaceContainerRef} className="workspace_manager">
|
||||
@@ -656,6 +567,7 @@ export default function App() {
|
||||
)}
|
||||
</Box>
|
||||
<ServerEventListener />
|
||||
<AppEventListener />
|
||||
</Portal>
|
||||
</div>
|
||||
</WorkspaceContext.Provider>
|
||||
|
||||
@@ -14,6 +14,7 @@ export const WorkspaceContext = createContext<{
|
||||
versionID?: string | null,
|
||||
forceLoad?: boolean,
|
||||
) => void;
|
||||
setIsDirty: (dirty: boolean) => void;
|
||||
saveCurWorkflow: () => void;
|
||||
discardUnsavedChanges: () => Promise<void>;
|
||||
isDirty: boolean;
|
||||
@@ -21,9 +22,8 @@ export const WorkspaceContext = createContext<{
|
||||
loadFilePath: (path: string, overwriteCurrent?: boolean) => void;
|
||||
setRoute: (route: WorkspaceRoute) => void;
|
||||
route: WorkspaceRoute;
|
||||
jsonDiff: JsonDiff;
|
||||
compareJson: (jsonDiff: JsonDiff) => void;
|
||||
curVersion: WorkflowVersion | null;
|
||||
setCurVersion: (version: WorkflowVersion | null) => void;
|
||||
}>({
|
||||
curFlowID: null,
|
||||
loadWorkflowID: () => {},
|
||||
@@ -34,9 +34,9 @@ export const WorkspaceContext = createContext<{
|
||||
loadFilePath: () => {},
|
||||
setRoute: () => {},
|
||||
route: "root",
|
||||
jsonDiff: null,
|
||||
compareJson: () => {},
|
||||
curVersion: null,
|
||||
setIsDirty: () => {},
|
||||
setCurVersion: () => {},
|
||||
});
|
||||
|
||||
export const RecentFilesContext = createContext<{
|
||||
|
||||
@@ -45,7 +45,6 @@ import HoverMenu from "./HoverMenu";
|
||||
const ShareDialog = lazy(() => import("../share/ShareDialog"));
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import JsonDiffCompareModal from "./JsonDiffCompareModal";
|
||||
|
||||
export default function DropdownTitle() {
|
||||
const {
|
||||
@@ -155,15 +154,17 @@ export default function DropdownTitle() {
|
||||
>
|
||||
Save
|
||||
</MenuItem>
|
||||
<Tooltip label="Revert workflow to your last saved version. You will lose all changes made since your last save.">
|
||||
<MenuItem
|
||||
onClick={discardUnsavedChanges}
|
||||
icon={<IconArrowBackUpDouble size={20} />}
|
||||
iconSpacing={1}
|
||||
>
|
||||
Discard unsaved changes
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
{!userSettingsTable?.autoSave && (
|
||||
<Tooltip label="Revert workflow to your last saved version. You will lose all changes made since your last save.">
|
||||
<MenuItem
|
||||
onClick={discardUnsavedChanges}
|
||||
icon={<IconArrowBackUpDouble size={20} />}
|
||||
iconSpacing={1}
|
||||
>
|
||||
Discard unsaved changes
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={handleDownload}
|
||||
icon={<IconDownload size={20} />}
|
||||
@@ -211,9 +212,6 @@ export default function DropdownTitle() {
|
||||
{route == "versionHistory" && (
|
||||
<VersionHistoryDrawer onClose={() => setRoute("root")} />
|
||||
)}
|
||||
|
||||
<JsonDiffCompareModal />
|
||||
|
||||
{route === "saveAsModal" && (
|
||||
<Modal isOpen={true} onClose={handleOnCloseModal}>
|
||||
<ModalOverlay />
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Box, Modal, ModalContent } from "@chakra-ui/react";
|
||||
import { useContext } from "react";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
|
||||
export default function JsonDiffCompareModal() {
|
||||
const { jsonDiff, compareJson } = useContext(WorkspaceContext);
|
||||
|
||||
if (jsonDiff == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => {
|
||||
compareJson(null);
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<Box height={"80vh"} width={"90%"} overflow={"scroll"}>
|
||||
{/* TODO: add json comparison ui view */}
|
||||
</Box>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,8 @@ export default function InstallMissingModelsButton({}: Props) {
|
||||
const [showMyModels, setShowMyModels] = useState(false);
|
||||
const [missingModels, setMissingModels] = useState<MissingModel[]>([]);
|
||||
useEffect(() => {
|
||||
// temporary disable missing model check
|
||||
return;
|
||||
// monkey patch queue prompt api to catch errors
|
||||
const queuePrompt = app.queuePrompt as Function;
|
||||
app.queuePrompt = async function () {
|
||||
|
||||
@@ -31,7 +31,7 @@ export function SharedTopbarButton({}) {
|
||||
<a href={cloudURL} style={{ textDecoration: "none" }} target="_blank">
|
||||
<DarkMode>
|
||||
<Tag>
|
||||
{privacy ? <PrivacyLabel privacy={privacy} showEmoji /> : "Shared"}
|
||||
{privacy === "PUBLIC" ? "🌐" : privacy === "UNLISTED" ? "🔗" : "🔒"}
|
||||
</Tag>
|
||||
</DarkMode>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
// @ts-expect-error
|
||||
import { app } from "/scripts/app.js";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
import { matchShortcut } from "../utils";
|
||||
import { EShortcutKeys } from "../types/dbTypes";
|
||||
|
||||
export default function AppEventListener() {
|
||||
const { setIsDirty, setRoute, saveCurWorkflow } =
|
||||
useContext(WorkspaceContext);
|
||||
const toast = useToast();
|
||||
useEffect(() => {
|
||||
const shortcutListener = async (matchResult: string) => {
|
||||
switch (matchResult) {
|
||||
case EShortcutKeys.SAVE:
|
||||
saveCurWorkflow();
|
||||
break;
|
||||
case EShortcutKeys.SAVE_AS:
|
||||
setRoute("saveAsModal");
|
||||
break;
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", (e) => {
|
||||
if (
|
||||
app.canvas.node_over != null ||
|
||||
app.canvas.node_capturing_input != null ||
|
||||
app.canvas.node_widget != null
|
||||
) {
|
||||
onIsDirty();
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", async function (event) {
|
||||
if (document.visibilityState === "hidden") return;
|
||||
|
||||
const matchResult = await matchShortcut(event);
|
||||
if (matchResult) {
|
||||
shortcutListener(matchResult);
|
||||
// @ts-expect-error
|
||||
} else if (event.target?.matches("input, textarea")) {
|
||||
onIsDirty();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const onIsDirty = async () => {
|
||||
if (workflowsTable?.curWorkflow?.saveLock) return;
|
||||
const autoSaveEnabled = await userSettingsTable?.getSetting("autoSave");
|
||||
if (!autoSaveEnabled) {
|
||||
setIsDirty(true);
|
||||
}
|
||||
if (workflowsTable?.curWorkflow?.id && autoSaveEnabled) {
|
||||
const isLatest = await workflowsTable?.latestVersionCheck();
|
||||
if (!isLatest) {
|
||||
toast({
|
||||
title: "You are working on an outdated version",
|
||||
description:
|
||||
"This workflow is changed by another tab. Please close the page and reopen to get the latest version.",
|
||||
status: "warning",
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
// autosave workflow if enabled
|
||||
const graphJson = JSON.stringify(app.graph.serialize());
|
||||
graphJson != null &&
|
||||
(await workflowsTable?.updateFlow(workflowsTable.curWorkflow.id, {
|
||||
json: graphJson,
|
||||
}));
|
||||
setIsDirty(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Button, ButtonGroup, Flex, IconButton } from "@chakra-ui/react";
|
||||
import { IconPlus, IconTriangleInvertedFilled } from "@tabler/icons-react";
|
||||
import { useContext } from "react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
|
||||
export default function VersionNameTopbar(
|
||||
{
|
||||
// onClick,
|
||||
}: {
|
||||
// onClick: () => void;
|
||||
},
|
||||
) {
|
||||
const { route, setRoute, curVersion } = useContext(WorkspaceContext);
|
||||
export default function VersionNameTopbar({}: {}) {
|
||||
const { setRoute, curVersion, isDirty, setCurVersion } =
|
||||
useContext(WorkspaceContext);
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
curVersion &&
|
||||
setCurVersion?.({ ...curVersion, name: curVersion.name + "*" });
|
||||
}
|
||||
}, [isDirty]);
|
||||
if (!curVersion) {
|
||||
return null;
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export default function VersionNameTopbar(
|
||||
rightIcon={<IconTriangleInvertedFilled size={10} />}
|
||||
onClick={() => setRoute("versionHistory")}
|
||||
>
|
||||
{curVersion?.name ?? "latest*"}
|
||||
{curVersion.name}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
Reference in New Issue
Block a user