Merge branch 'dev' into feat/upload-thumbnail

# Conflicts:
#	service/file_sync_service.py
This commit is contained in:
ss707494
2024-03-11 21:06:26 +08:00
11 changed files with 160 additions and 38 deletions
+39 -1
View File
@@ -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')
+22
View File
@@ -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
View File
@@ -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
+9 -3
View File
@@ -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>
+6 -6
View File
@@ -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 />
) : (
+1 -1
View File
@@ -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;
+19 -6
View File
@@ -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}
+46 -17
View File
@@ -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