diff --git a/service/file_sync_service.py b/service/file_sync_service.py index fef3b1c..459dbe6 100644 --- a/service/file_sync_service.py +++ b/service/file_sync_service.py @@ -242,6 +242,44 @@ def count_files_sync(reqJson): if file.is_file(): file_count += 1 return {"success": True, "count": file_count} +# Begin: Copy all files to the new directory when switching the save directory +@server.PromptServer.instance.routes.post('/workspace/copy_json_files') +async def copy_json_files(request): + reqJson = await request.json() + src_dir = reqJson['src'] + dest_dir = reqJson['dst'] + try: + for item in os.listdir(src_dir): + src_item_path = os.path.join(src_dir, item) + dest_item_path = os.path.join(dest_dir, item) + + if os.path.isdir(src_item_path): + dest_item_path = await get_unique_dir_name(dest_item_path) + await asyncio.to_thread(shutil.copytree, src_item_path, dest_item_path) + elif item.endswith('.json'): + dest_item_path = await get_unique_file_name(dest_item_path) + await asyncio.to_thread(shutil.copy2, src_item_path, dest_item_path) + + return web.json_response({"success": True}, content_type='application/json') + except Exception as e: + return web.json_response({"success": False, "error": str(e)}, content_type='application/json') + +async def get_unique_file_name(path): + base, extension = os.path.splitext(path) + counter = 1 + while os.path.exists(path): + path = f"{base}_{counter}{extension}" + counter += 1 + return path + +async def get_unique_dir_name(path): + original_path = path + counter = 1 + while os.path.exists(path): + path = f"{original_path}_{counter}" + counter += 1 + return path +# End: Copy all files to the new directory when switching the save directory @server.PromptServer.instance.routes.post("/images/save") async def save_images(request): @@ -270,4 +308,4 @@ async def save_images(request): break await loop.run_in_executor(None, f.write, chunk) files_saved.append(str(file_path)) - return web.Response(text=json.dumps({"imgPaths": files_saved}), content_type='application/json') \ No newline at end of file + return web.Response(text=json.dumps({"imgPaths": files_saved}), content_type='application/json') diff --git a/ui/src/Api.ts b/ui/src/Api.ts index 09edb5c..c803fbb 100644 --- a/ui/src/Api.ts +++ b/ui/src/Api.ts @@ -200,3 +200,25 @@ export async function cancelDownload(savePath: string) { console.error("Error move file:", error); } } + +export async function copyFlowsToNewDirectory( + sourceDir: string, + dstDir: string, +) { + try { + const response = await fetch("/workspace/copy_json_files", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + src: sourceDir, + dst: dstDir, + }), + }); + const result = await response.json(); + return result; + } catch (error) { + console.error("Error copy flows to new directory:", error); + } +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 009f337..0342196 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -226,6 +226,9 @@ export default function App() { colorScheme: "red", }, ], + { + closeOnOverlayClick: false, + }, ); } }); @@ -482,7 +485,6 @@ export default function App() { }, }, ], - true, ); } else { // autosave workflow if enabled diff --git a/ui/src/components/AlertDialogProvider.tsx b/ui/src/components/AlertDialogProvider.tsx index c5ef867..3b0caa4 100644 --- a/ui/src/components/AlertDialogProvider.tsx +++ b/ui/src/components/AlertDialogProvider.tsx @@ -28,7 +28,10 @@ interface DialogContextType { showDialog: ( message: string | React.ReactNode, buttons?: (DialogButton | null)[], - hideCloseIcon?: boolean, + options?: { + hideCloseIcon?: boolean; + closeOnOverlayClick?: boolean; + }, ) => void; } @@ -49,14 +52,16 @@ export const AlertDialogProvider: React.FC<{ children: ReactNode }> = ({ const [message, setMessage] = useState(""); const [buttons, setButtons] = useState<(DialogButton | null)[]>([]); const [hideCloseIcon, setHideCloseIcon] = useState(false); + const [closeOnOverlayClick, setCloseOnOverlayClick] = useState(true); const cancelRef = React.useRef(null); const showDialog: DialogContextType["showDialog"] = useCallback( - (message, buttons, hideCloseIcon = false) => { + (message, buttons, options) => { setMessage(message); setButtons(buttons ?? []); setIsOpen(true); - setHideCloseIcon(hideCloseIcon); + setHideCloseIcon(options?.hideCloseIcon ?? false); + setCloseOnOverlayClick(options?.closeOnOverlayClick ?? true); }, [], ); @@ -74,6 +79,7 @@ export const AlertDialogProvider: React.FC<{ children: ReactNode }> = ({ leastDestructiveRef={cancelRef} onClose={handleClose} size={"xl"} + closeOnOverlayClick={closeOnOverlayClick} > diff --git a/ui/src/db-tables/WorkspaceDB.ts b/ui/src/db-tables/WorkspaceDB.ts index 8e30ddd..2dc0575 100644 --- a/ui/src/db-tables/WorkspaceDB.ts +++ b/ui/src/db-tables/WorkspaceDB.ts @@ -71,7 +71,7 @@ export async function backfillIndexdb() { const backfillWorkflows = async () => { try { const all = await workflowsTable?.getRecords(); - all && (await indexdb.workflows.bulkAdd(Object.values(all))); + all && (await indexdb.workflows.bulkPut(Object.values(all))); } catch (error) { console.error(error); } @@ -79,7 +79,7 @@ export async function backfillIndexdb() { const backfillFolders = async () => { try { const all = await foldersTable?.getRecords(); - all && (await indexdb.folders.bulkAdd(Object.values(all))); + all && (await indexdb.folders.bulkPut(Object.values(all))); } catch (error) { console.error(error); } @@ -87,7 +87,7 @@ export async function backfillIndexdb() { const backfillMedia = async () => { try { const all = await mediaTable?.getRecords(); - all && (await indexdb.media.bulkAdd(Object.values(all))); + all && (await indexdb.media.bulkPut(Object.values(all))); } catch (error) { console.error(error); } @@ -95,7 +95,7 @@ export async function backfillIndexdb() { const backfillChangelogs = async () => { try { const all = await changelogsTable?.getRecords(); - all && (await indexdb.changelogs.bulkAdd(Object.values(all))); + all && (await indexdb.changelogs.bulkPut(Object.values(all))); } catch (error) { console.error(error); } @@ -103,7 +103,7 @@ export async function backfillIndexdb() { const backfillTags = async () => { try { const all = await tagsTable?.getRecords(); - all && (await indexdb.tags.bulkAdd(Object.values(all))); + all && (await indexdb.tags.bulkPut(Object.values(all))); } catch (error) { console.error(error); } @@ -129,7 +129,7 @@ export async function backfillIndexdb() { const backfillWorkflowVersions = async () => { try { const all = await workflowVersionsTable?.getRecords(); - all && (await indexdb.workflowVersions.bulkAdd(Object.values(all))); + all && (await indexdb.workflowVersions.bulkPut(Object.values(all))); } catch (error) { console.error(error); } diff --git a/ui/src/gallery/components/AllPromptForm/AllPromptForm.tsx b/ui/src/gallery/components/AllPromptForm/AllPromptForm.tsx index 8ecbb8f..fc9255f 100644 --- a/ui/src/gallery/components/AllPromptForm/AllPromptForm.tsx +++ b/ui/src/gallery/components/AllPromptForm/AllPromptForm.tsx @@ -11,6 +11,7 @@ import { FormItemComponent } from "../FormItem/FormItemComponent.tsx"; import { MetaData } from "../../utils.ts"; import { FormItem } from "../FormItem/types.ts"; import { isInTopField, TopFieldType } from "../MetaBox/MetaBox.tsx"; +import { useEffect, useState } from "react"; export default function AllPromptForm({ metaData, @@ -24,8 +25,16 @@ export default function AllPromptForm({ updateTopField?: (field: TopFieldType) => void; }) { const prompt = metaData.prompt; + const [defaultIndex, setDefaultIndex] = useState([]); + useEffect(() => { + setDefaultIndex(Object.keys(prompt).map((_, i) => i)); + }, [prompt]); return ( - + setDefaultIndex(val as number[])} + allowMultiple + > {Object.keys(prompt).map((promptKey) => { const promptElement = prompt[promptKey]; const promptInputs = promptElement.inputs; @@ -35,6 +44,7 @@ export default function AllPromptForm({ !isInTopField(topFields, { name: v, promptKey: promptKey, + classType: promptElement.class_type, }), ); if (inputsKeyList.length === 0) return null; @@ -59,6 +69,7 @@ export default function AllPromptForm({ isInTopField(topFields, { name: inputsKey, promptKey, + classType: promptElement.class_type, }) ) { return null; diff --git a/ui/src/gallery/components/FormItem/FormItemComponent.tsx b/ui/src/gallery/components/FormItem/FormItemComponent.tsx index 66c9e7c..61ef3db 100644 --- a/ui/src/gallery/components/FormItem/FormItemComponent.tsx +++ b/ui/src/gallery/components/FormItem/FormItemComponent.tsx @@ -88,6 +88,7 @@ export const FormItemComponent: FC = (props) => { isInTopField(props.topFields, { name: props.name, promptKey: props.promptKey, + classType: props.classType, }) ? ( ) : ( diff --git a/ui/src/gallery/components/FormItem/types.ts b/ui/src/gallery/components/FormItem/types.ts index c35bbbe..c55f1a1 100644 --- a/ui/src/gallery/components/FormItem/types.ts +++ b/ui/src/gallery/components/FormItem/types.ts @@ -20,7 +20,7 @@ export type FormItem = { topFields: TopFieldType[]; updateTopField?: (field: TopFieldType) => void; updateMetaData?: (data: { - promptKey: string; + promptKey: string | number; name: string; value: any; }) => void; diff --git a/ui/src/gallery/components/MetaBox/MetaBox.tsx b/ui/src/gallery/components/MetaBox/MetaBox.tsx index 766acda..74024b8 100644 --- a/ui/src/gallery/components/MetaBox/MetaBox.tsx +++ b/ui/src/gallery/components/MetaBox/MetaBox.tsx @@ -11,46 +11,59 @@ export type TopFieldType = { class_type?: string; name: string; }; + +// comfyui default workflow export const DEFAULT_TOP_FIELDS: TopFieldType[] = [ { promptKey: "4", name: "ckpt_name", + class_type: "CheckpointLoaderSimple", }, { promptKey: "6", name: "text", + class_type: "CLIPTextEncode", }, { promptKey: "7", name: "text", + class_type: "CLIPTextEncode", }, { promptKey: "5", name: "width", + class_type: "EmptyLatentImage", }, { promptKey: "5", name: "height", + class_type: "EmptyLatentImage", }, { promptKey: "3", name: "steps", + class_type: "KSampler", }, { promptKey: "3", name: "sampler_name", + class_type: "KSampler", }, { promptKey: "3", name: "cfg", + class_type: "KSampler", }, ]; export const isInTopField = ( topFields: TopFieldType[], - item: Pick, + item: Pick, ) => { return topFields?.some( - (top) => top.promptKey === item?.promptKey && top.name === item?.name, + (top) => + top.promptKey === item?.promptKey && + top.name === item?.name && + top.class_type === item.classType, ); }; @@ -60,16 +73,15 @@ export default function MetaBox({ metaData: MetaData; media: Media; }) { + const _metaData = JSON.parse(JSON.stringify(oriMetaData)); const [topFields, setTopFields] = useState(DEFAULT_TOP_FIELDS); - const [metaData, setMetaData] = useState( - JSON.parse(JSON.stringify(oriMetaData)), - ); + const [metaData, setMetaData] = useState(_metaData); const updateMetaData = ({ promptKey, name, value, }: { - promptKey: string; + promptKey: string | number; name: string; value: any; }) => { @@ -93,6 +105,7 @@ export default function MetaBox({ isInTopField(topFields, { name: field.name, promptKey: field?.promptKey, + classType: field?.class_type ?? "", }) ) { setTopFields((pre) => diff --git a/ui/src/gallery/components/TopForm/TopForm.tsx b/ui/src/gallery/components/TopForm/TopForm.tsx index 0d20eb1..18186d0 100644 --- a/ui/src/gallery/components/TopForm/TopForm.tsx +++ b/ui/src/gallery/components/TopForm/TopForm.tsx @@ -20,12 +20,12 @@ export default function TopForm({ <> {topFields?.length > 0 && ( - {topFields?.map((field) => { + {topFields?.map((field, i) => { if (!prompt?.[field.promptKey]?.inputs) return null; const promptValue = prompt?.[field.promptKey]?.inputs?.[field.name]; return ( 0; @@ -36,6 +40,7 @@ export default function SelectMyWorkflowsDir() { const [noPermission, setNoPermission] = useState(false); const manualEntryRef = useRef(null); const toast = useToast(); + const { showDialog } = useDialog(); useEffect(() => { fetchMyWorkflowsDir().then((dir) => setCurrentDirectory(dir ?? "")); @@ -83,7 +88,26 @@ export default function SelectMyWorkflowsDir() { setDirPathList([]); }; - const onSaveDirectory = async (manualEntry?: string) => { + const saveDirectory = async ( + newPath: string, + needCopy: boolean = false, + isManualEntry: boolean = false, + ) => { + if (needCopy) { + const sourcePath = await fetchMyWorkflowsDir(); + sourcePath && (await copyFlowsToNewDirectory(sourcePath, newPath)); + } + await userSettingsTable?.upsert({ + myWorkflowsDir: newPath, + }); + const getNewPath = + (await userSettingsTable?.getSetting("myWorkflowsDir")) ?? ""; + setCurrentDirectory(getNewPath); + isManualEntry && setNoPermission(false); + onCloseEditDirectory(); + }; + + const onSubmit = async (manualEntry?: string) => { if (manualEntry) { const { error = "" } = await getSystemDir(manualEntry); if (error.includes("Not a directory")) { @@ -100,16 +124,21 @@ export default function SelectMyWorkflowsDir() { } const newPath = manualEntry ?? `${isWindows ? "" : slash}${dirPathList.join(slash)}`; - await userSettingsTable?.upsert({ - myWorkflowsDir: newPath, - }); - const getNewPath = - (await userSettingsTable?.getSetting("myWorkflowsDir")) ?? ""; - setCurrentDirectory(getNewPath); - manualEntry && setNoPermission(false); - onCloseEditDirectory(); - // to update /my_workflows files in disk to new location - validateOrSaveAllJsonFileMyWorkflows(); + + showDialog("Do you want to copy all workflows to new directory?", [ + { + label: "Yes", + onClick: () => { + saveDirectory(newPath, true, !!manualEntry); + }, + }, + { + label: "No", + onClick: () => { + saveDirectory(newPath, false, !!manualEntry); + }, + }, + ]); }; const onReset = async () => { @@ -150,7 +179,7 @@ export default function SelectMyWorkflowsDir() { size="sm" onClick={() => { manualEntryRef?.current?.value && - onSaveDirectory(manualEntryRef?.current?.value); + onSubmit(manualEntryRef?.current?.value); }} > Save @@ -220,7 +249,7 @@ export default function SelectMyWorkflowsDir() { {dirPathList.map((dir, index) => ( - <> + {index > 0 && slash} - + ))} { - onSaveDirectory(); + onSubmit(); }} > Save