Merge branch 'dev' into feat/upload-thumbnail
# Conflicts: # service/file_sync_service.py
This commit is contained in:
@@ -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')
|
||||
return web.Response(text=json.dumps({"imgPaths": files_saved}), content_type='application/json')
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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<string | React.ReactNode>("");
|
||||
const [buttons, setButtons] = useState<(DialogButton | null)[]>([]);
|
||||
const [hideCloseIcon, setHideCloseIcon] = useState(false);
|
||||
const [closeOnOverlayClick, setCloseOnOverlayClick] = useState(true);
|
||||
const cancelRef = React.useRef<HTMLButtonElement>(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}
|
||||
>
|
||||
<DarkMode>
|
||||
<AlertDialogOverlay>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<number[]>([]);
|
||||
useEffect(() => {
|
||||
setDefaultIndex(Object.keys(prompt).map((_, i) => i));
|
||||
}, [prompt]);
|
||||
return (
|
||||
<Accordion allowToggle>
|
||||
<Accordion
|
||||
index={defaultIndex}
|
||||
onChange={(val) => 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;
|
||||
|
||||
@@ -88,6 +88,7 @@ export const FormItemComponent: FC<FormItem> = (props) => {
|
||||
isInTopField(props.topFields, {
|
||||
name: props.name,
|
||||
promptKey: props.promptKey,
|
||||
classType: props.classType,
|
||||
}) ? (
|
||||
<IconPinFilled />
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<FormItem, "name" | "promptKey">,
|
||||
item: Pick<FormItem, "name" | "promptKey" | "classType">,
|
||||
) => {
|
||||
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<MetaData>(
|
||||
JSON.parse(JSON.stringify(oriMetaData)),
|
||||
);
|
||||
const [metaData, setMetaData] = useState<MetaData>(_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) =>
|
||||
|
||||
@@ -20,12 +20,12 @@ export default function TopForm({
|
||||
<>
|
||||
{topFields?.length > 0 && (
|
||||
<Flex px={2} gap={2} direction={"column"}>
|
||||
{topFields?.map((field) => {
|
||||
{topFields?.map((field, i) => {
|
||||
if (!prompt?.[field.promptKey]?.inputs) return null;
|
||||
const promptValue = prompt?.[field.promptKey]?.inputs?.[field.name];
|
||||
return (
|
||||
<FormItemComponent
|
||||
key={`formTop${field.name}`}
|
||||
key={`formTop${field.name}${i}`}
|
||||
promptKey={field.promptKey as string}
|
||||
classType={prompt?.[field.promptKey]?.class_type}
|
||||
name={field.name}
|
||||
|
||||
@@ -20,9 +20,13 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { fetchMyWorkflowsDir, getSystemDir } from "../Api";
|
||||
import {
|
||||
copyFlowsToNewDirectory,
|
||||
fetchMyWorkflowsDir,
|
||||
getSystemDir,
|
||||
} from "../Api";
|
||||
import { userSettingsTable } from "../db-tables/WorkspaceDB";
|
||||
import { validateOrSaveAllJsonFileMyWorkflows } from "../utils";
|
||||
import { useDialog } from "../components/AlertDialogProvider";
|
||||
|
||||
export default function SelectMyWorkflowsDir() {
|
||||
const isWindows = navigator.userAgent.indexOf("Window") > 0;
|
||||
@@ -36,6 +40,7 @@ export default function SelectMyWorkflowsDir() {
|
||||
const [noPermission, setNoPermission] = useState(false);
|
||||
const manualEntryRef = useRef<HTMLInputElement>(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() {
|
||||
<VStack spacing={2} alignItems="start">
|
||||
<Stack spacing="2px" direction="row" wrap="wrap">
|
||||
{dirPathList.map((dir, index) => (
|
||||
<>
|
||||
<span key={dir}>
|
||||
{index > 0 && slash}
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
@@ -233,7 +262,7 @@ export default function SelectMyWorkflowsDir() {
|
||||
>
|
||||
{dir}
|
||||
</Button>
|
||||
</>
|
||||
</span>
|
||||
))}
|
||||
</Stack>
|
||||
<SimpleGrid
|
||||
@@ -271,7 +300,7 @@ export default function SelectMyWorkflowsDir() {
|
||||
colorScheme="teal"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onSaveDirectory();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
|
||||
Reference in New Issue
Block a user