mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-21 04:47:34 +00:00
Compare commits
22 Commits
core/1.13
...
create_fol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6e34cfd3 | ||
|
|
2a445f3f94 | ||
|
|
f8dcb915aa | ||
|
|
11925ce345 | ||
|
|
90053058ba | ||
|
|
b36f748a78 | ||
|
|
d57d12b426 | ||
|
|
bd1be28478 | ||
|
|
891e18af8e | ||
|
|
1610d06cd1 | ||
|
|
e3c7bbf966 | ||
|
|
0bfbbe838f | ||
|
|
c82fe80716 | ||
|
|
ad98bcb87c | ||
|
|
652ea15e8b | ||
|
|
a7a8cc633b | ||
|
|
d23aec4ceb | ||
|
|
8db088b27a | ||
|
|
7ef6e52f38 | ||
|
|
edeefe0883 | ||
|
|
c6046e47d2 | ||
|
|
e8bcccc276 |
Binary file not shown.
@@ -471,7 +471,6 @@ export class ComfyPage {
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
|
||||
@@ -95,18 +95,6 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
return this.page.locator('.workflows-sidebar-tab')
|
||||
}
|
||||
|
||||
get browseGalleryButton() {
|
||||
return this.root.locator('.browse-templates-button')
|
||||
}
|
||||
|
||||
get newBlankWorkflowButton() {
|
||||
return this.root.locator('.new-blank-workflow-button')
|
||||
}
|
||||
|
||||
get openWorkflowButton() {
|
||||
return this.root.locator('.open-workflow-button')
|
||||
}
|
||||
|
||||
async getOpenedWorkflowNames() {
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-open .node-label')
|
||||
|
||||
@@ -673,7 +673,7 @@ test.describe('Load duplicate workflow', () => {
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
|
||||
@@ -8,8 +8,7 @@ test.describe('Load Workflow in Media', () => {
|
||||
'edited_workflow.webp',
|
||||
'no_workflow.webp',
|
||||
'large_workflow.webp',
|
||||
'workflow.webm',
|
||||
'workflow.glb'
|
||||
'workflow.webm'
|
||||
].forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName}`, async ({ comfyPage }) => {
|
||||
await comfyPage.dragAndDropFile(fileName)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB |
@@ -119,7 +119,10 @@ test.describe('Menu', () => {
|
||||
test('Can add new bookmark folder', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.newFolderButton.click()
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
const textInput = comfyPage.page.locator('.editable-text input')
|
||||
await textInput.waitFor({ state: 'visible' })
|
||||
await textInput.fill('New Folder')
|
||||
await textInput.press('Enter')
|
||||
expect(await tab.getFolder('New Folder').count()).toBe(1)
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
|
||||
@@ -132,8 +135,10 @@ test.describe('Menu', () => {
|
||||
|
||||
await tab.getFolder('foo').click({ button: 'right' })
|
||||
await comfyPage.page.getByLabel('New Folder').click()
|
||||
await comfyPage.page.keyboard.type('bar')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
const textInput = comfyPage.page.locator('.editable-text input')
|
||||
await textInput.waitFor({ state: 'visible' })
|
||||
await textInput.fill('bar')
|
||||
await textInput.press('Enter')
|
||||
|
||||
expect(await tab.getFolder('bar').count()).toBe(1)
|
||||
expect(
|
||||
@@ -397,7 +402,7 @@ test.describe('Menu', () => {
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
|
||||
await tab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*Unsaved Workflow (2).json'
|
||||
@@ -496,7 +501,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Can save workflow as', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
@@ -554,6 +559,8 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
// Compare the exported workflow with the original
|
||||
delete downloadedContent.id
|
||||
delete downloadedContentZh.id
|
||||
expect(downloadedContent).toBeDefined()
|
||||
expect(downloadedContent).toEqual(downloadedContentZh)
|
||||
})
|
||||
@@ -618,7 +625,7 @@ test.describe('Menu', () => {
|
||||
|
||||
// Load blank workflow
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
|
||||
// Switch back to the missing_nodes workflow
|
||||
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
|
||||
|
||||
@@ -57,13 +57,13 @@ test.describe('Templates', () => {
|
||||
test('Can load template workflows', async ({ comfyPage }) => {
|
||||
// Clear the workflow
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(0)
|
||||
}).toPass({ timeout: 250 })
|
||||
|
||||
// Load a template
|
||||
await comfyPage.menu.workflowsTab.browseGalleryButton.click()
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.loadTemplate('default')
|
||||
await expect(comfyPage.templates.content).toBeHidden()
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.13.10",
|
||||
"version": "1.14.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.13.10",
|
||||
"version": "1.14.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.10.7",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.20",
|
||||
"@comfyorg/litegraph": "^0.10.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -460,15 +460,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.31.tgz",
|
||||
"integrity": "sha512-6tdUfrRyJ9mLlGhNxKqao0kdO+nKRLzQIbENmTK1EtJ1zhMmCp43a+pG7+kecjgp0pbfzxWKhTdCarS9A9fkqw==",
|
||||
"version": "0.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.20.tgz",
|
||||
"integrity": "sha512-JFKGk9wSx7CcYh9MRNo7bqTLJwQzVc+1Xg8V2Ghn9BS3RzpmkfktaWHi+waU7/CRQMzvjF+mnDPP58xk1xbVhA==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.10.7.tgz",
|
||||
"integrity": "sha512-Cts4FBAk+e5RWPfmXUiBkjYzHkpy1eiW+DRCAYjGBGkobRhPZIBBHkk85vAnhMva704jSS9nsHG8c7r1/LUGyQ==",
|
||||
"version": "0.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.10.9.tgz",
|
||||
"integrity": "sha512-ubGozxdDIVNL/MYvfCAXgiaqBfIODtp0jZeN9uzWrdHwqUy9ZkLt/7/q7G4nGpNcEoShbMu7EK4VPH3WRmNQ7A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.13.10",
|
||||
"version": "1.14.0",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -72,8 +72,8 @@
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.10.7",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.20",
|
||||
"@comfyorg/litegraph": "^0.10.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -105,6 +105,5 @@
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.0"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0+sha512.837566d24eec14ec0f5f1411adb544e892b3454255e61fdef8fd05f3429480102806bac7446bc9daff3896b01ae4b62d00096c7e989f1596f2af10b927532f39"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:class="props.class"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
:value="renderedRoots"
|
||||
:value="renderedRoot.children"
|
||||
selectionMode="single"
|
||||
:pt="{
|
||||
nodeLabel: 'tree-explorer-node-label',
|
||||
@@ -43,21 +43,24 @@ import { computed, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import { useTreeFolderOperations } from '@/composables/tree/useTreeFolderOperations'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
import {
|
||||
InjectKeyExpandedKeys,
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
type RenderedTreeExplorerNode,
|
||||
type TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import { combineTrees, findNodeByKey } from '@/utils/treeUtil'
|
||||
|
||||
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
|
||||
provide('expandedKeys', expandedKeys)
|
||||
provide(InjectKeyExpandedKeys, expandedKeys)
|
||||
const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
|
||||
provide('selectionKeys', selectionKeys)
|
||||
// Tracks whether the caller has set the selectionKeys model.
|
||||
const storeSelectionKeys = selectionKeys.value !== undefined
|
||||
|
||||
const props = defineProps<{
|
||||
roots: TreeExplorerNode[]
|
||||
root: TreeExplorerNode
|
||||
class?: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
@@ -65,8 +68,23 @@ const emit = defineEmits<{
|
||||
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
|
||||
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
|
||||
}>()
|
||||
const renderedRoots = computed<RenderedTreeExplorerNode[]>(() => {
|
||||
return props.roots.map(fillNodeInfo)
|
||||
|
||||
const {
|
||||
newFolderNode,
|
||||
getAddFolderMenuItem,
|
||||
handleFolderCreation,
|
||||
addFolderCommand
|
||||
} = useTreeFolderOperations(
|
||||
/* expandNode */ (node: TreeExplorerNode) => {
|
||||
expandedKeys.value[node.key] = true
|
||||
}
|
||||
)
|
||||
|
||||
const renderedRoot = computed<RenderedTreeExplorerNode>(() => {
|
||||
const renderedRoot = fillNodeInfo(props.root)
|
||||
return newFolderNode.value
|
||||
? combineTrees(renderedRoot, newFolderNode.value)
|
||||
: renderedRoot
|
||||
})
|
||||
const getTreeNodeIcon = (node: TreeExplorerNode) => {
|
||||
if (node.getIcon) {
|
||||
@@ -81,7 +99,7 @@ const getTreeNodeIcon = (node: TreeExplorerNode) => {
|
||||
if (node.leaf) {
|
||||
return 'pi pi-file'
|
||||
}
|
||||
const isExpanded = expandedKeys.value[node.key]
|
||||
const isExpanded = expandedKeys.value?.[node.key] ?? false
|
||||
return isExpanded ? 'pi pi-folder-open' : 'pi pi-folder'
|
||||
}
|
||||
const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
@@ -95,7 +113,8 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
children,
|
||||
type: node.leaf ? 'node' : 'folder',
|
||||
totalLeaves,
|
||||
badgeText: node.getBadgeText ? node.getBadgeText() : null
|
||||
badgeText: node.getBadgeText ? node.getBadgeText() : null,
|
||||
isEditingLabel: node.key === renameEditingNode.value?.key
|
||||
}
|
||||
}
|
||||
const onNodeContentClick = async (
|
||||
@@ -112,7 +131,6 @@ const onNodeContentClick = async (
|
||||
}
|
||||
const menu = ref(null)
|
||||
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('menuTargetNode', menuTargetNode)
|
||||
const extraMenuItems = computed(() => {
|
||||
return menuTargetNode.value?.contextMenuItems
|
||||
? typeof menuTargetNode.value.contextMenuItems === 'function'
|
||||
@@ -121,7 +139,26 @@ const extraMenuItems = computed(() => {
|
||||
: []
|
||||
})
|
||||
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('renameEditingNode', renameEditingNode)
|
||||
const errorHandling = useErrorHandling()
|
||||
const handleNodeLabelEdit = async (
|
||||
node: RenderedTreeExplorerNode,
|
||||
newName: string
|
||||
) => {
|
||||
await errorHandling.wrapWithErrorHandlingAsync(
|
||||
async () => {
|
||||
if (node.key === newFolderNode.value?.key) {
|
||||
await handleFolderCreation(newName)
|
||||
} else {
|
||||
await node.handleRename(newName)
|
||||
}
|
||||
},
|
||||
node.handleError,
|
||||
() => {
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
)()
|
||||
}
|
||||
provide(InjectKeyHandleEditLabelFunction, handleNodeLabelEdit)
|
||||
|
||||
const { t } = useI18n()
|
||||
const renameCommand = (node: RenderedTreeExplorerNode) => {
|
||||
@@ -133,6 +170,7 @@ const deleteCommand = async (node: RenderedTreeExplorerNode) => {
|
||||
}
|
||||
const menuItems = computed<MenuItem[]>(() =>
|
||||
[
|
||||
getAddFolderMenuItem(menuTargetNode.value),
|
||||
{
|
||||
label: t('g.rename'),
|
||||
icon: 'pi pi-file-edit',
|
||||
@@ -163,7 +201,6 @@ const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandling = useErrorHandling()
|
||||
const wrapCommandWithErrorHandler = (
|
||||
command: (event: MenuItemCommandEvent) => void,
|
||||
{ isAsync = false }: { isAsync: boolean }
|
||||
@@ -181,7 +218,14 @@ const wrapCommandWithErrorHandler = (
|
||||
|
||||
defineExpose({
|
||||
renameCommand,
|
||||
deleteCommand
|
||||
deleteCommand,
|
||||
/**
|
||||
* The command to add a folder to a node via the context menu
|
||||
* @param targetNodeKey - The key of the node where the folder will be added under
|
||||
*/
|
||||
addFolderCommand: (targetNodeKey: string) => {
|
||||
addFolderCommand(findNodeByKey(renderedRoot.value, targetNodeKey))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -38,18 +38,17 @@
|
||||
<script setup lang="ts">
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
|
||||
import Badge from 'primevue/badge'
|
||||
import { Ref, computed, inject, ref } from 'vue'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import {
|
||||
usePragmaticDraggable,
|
||||
usePragmaticDroppable
|
||||
} from '@/composables/usePragmaticDragAndDrop'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerDragAndDropData,
|
||||
TreeExplorerNode
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
type RenderedTreeExplorerNode,
|
||||
type TreeExplorerDragAndDropData
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -77,22 +76,12 @@ const nodeBadgeText = computed<string>(() => {
|
||||
})
|
||||
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
|
||||
|
||||
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
|
||||
const renameEditingNode =
|
||||
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
|
||||
const isEditing = computed(
|
||||
() => labelEditable.value && renameEditingNode.value?.key === props.node.key
|
||||
)
|
||||
const errorHandling = useErrorHandling()
|
||||
const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
async (newName: string) => {
|
||||
await props.node.handleRename(newName)
|
||||
},
|
||||
props.node.handleError,
|
||||
() => {
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
)
|
||||
const isEditing = computed<boolean>(() => props.node.isEditingLabel)
|
||||
const handleEditLabel = inject(InjectKeyHandleEditLabelFunction)
|
||||
const handleRename = (newName: string) => {
|
||||
handleEditLabel(props.node, newName)
|
||||
}
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="container" class="scroll-container">
|
||||
<div :style="{ height: `${(state.start / cols) * itemSize}px` }" />
|
||||
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
|
||||
<div :style="gridStyle">
|
||||
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
|
||||
<slot name="item" :item="item"> </slot>
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div
|
||||
:style="{
|
||||
height: `${((props.items.length - state.end) / cols) * itemSize}px`
|
||||
height: `${((items.length - state.end) / cols) * itemHeight}px`
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@@ -19,22 +19,25 @@ import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { clamp, debounce } from 'lodash'
|
||||
import { type CSSProperties, computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
const {
|
||||
items,
|
||||
bufferRows = 1,
|
||||
scrollThrottle = 64,
|
||||
resizeDebounce = 64,
|
||||
defaultItemHeight = 200,
|
||||
defaultItemWidth = 200
|
||||
} = defineProps<{
|
||||
items: (T & { key: string })[]
|
||||
gridStyle: Partial<CSSProperties>
|
||||
bufferRows?: number
|
||||
scrollThrottle?: number
|
||||
resizeDebounce?: number
|
||||
defaultItemSize?: number
|
||||
defaultItemHeight?: number
|
||||
defaultItemWidth?: number
|
||||
}>()
|
||||
const {
|
||||
bufferRows = 1,
|
||||
scrollThrottle = 64,
|
||||
resizeDebounce = 64,
|
||||
defaultItemSize = 200
|
||||
} = props
|
||||
|
||||
const itemSize = ref(defaultItemSize)
|
||||
const itemHeight = ref(defaultItemHeight)
|
||||
const itemWidth = ref(defaultItemWidth)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const { width, height } = useElementSize(container)
|
||||
const { y: scrollY } = useScroll(container, {
|
||||
@@ -42,12 +45,10 @@ const { y: scrollY } = useScroll(container, {
|
||||
eventListenerOptions: { passive: true }
|
||||
})
|
||||
|
||||
const cols = computed(() => Math.floor(width.value / itemSize.value) || 1)
|
||||
const viewRows = computed(() => Math.ceil(height.value / itemSize.value))
|
||||
const offsetRows = computed(() => Math.floor(scrollY.value / itemSize.value))
|
||||
const isValidGrid = computed(
|
||||
() => height.value && width.value && props.items?.length
|
||||
)
|
||||
const cols = computed(() => Math.floor(width.value / itemWidth.value) || 1)
|
||||
const viewRows = computed(() => Math.ceil(height.value / itemHeight.value))
|
||||
const offsetRows = computed(() => Math.floor(scrollY.value / itemHeight.value))
|
||||
const isValidGrid = computed(() => height.value && width.value && items?.length)
|
||||
|
||||
const state = computed<{ start: number; end: number }>(() => {
|
||||
const fromRow = offsetRows.value - bufferRows
|
||||
@@ -57,18 +58,19 @@ const state = computed<{ start: number; end: number }>(() => {
|
||||
const toCol = toRow * cols.value
|
||||
|
||||
return {
|
||||
start: clamp(fromCol, 0, props.items?.length),
|
||||
end: clamp(toCol, fromCol, props.items?.length)
|
||||
start: clamp(fromCol, 0, items?.length),
|
||||
end: clamp(toCol, fromCol, items?.length)
|
||||
}
|
||||
})
|
||||
const renderedItems = computed(() =>
|
||||
isValidGrid.value ? props.items.slice(state.value.start, state.value.end) : []
|
||||
isValidGrid.value ? items.slice(state.value.start, state.value.end) : []
|
||||
)
|
||||
|
||||
const updateItemSize = () => {
|
||||
if (container.value) {
|
||||
const firstItem = container.value.querySelector('[data-virtual-grid-item]')
|
||||
itemSize.value = firstItem?.clientHeight || defaultItemSize
|
||||
itemHeight.value = firstItem?.clientHeight || defaultItemHeight
|
||||
itemWidth.value = firstItem?.clientWidth || defaultItemWidth
|
||||
}
|
||||
}
|
||||
const onResize = debounce(updateItemSize, resizeDebounce)
|
||||
|
||||
@@ -10,8 +10,10 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
RenderedTreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
// Create a mock i18n instance
|
||||
const i18n = createI18n({
|
||||
@@ -47,7 +49,6 @@ describe('TreeExplorerTreeNode', () => {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge },
|
||||
provide: { renameEditingNode: { value: null } },
|
||||
plugins: [createTestingPinia(), i18n]
|
||||
}
|
||||
})
|
||||
@@ -63,10 +64,14 @@ describe('TreeExplorerTreeNode', () => {
|
||||
|
||||
it('makes node label editable when renamingEditingNode matches', async () => {
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: mockNode },
|
||||
props: {
|
||||
node: {
|
||||
...mockNode,
|
||||
isEditingLabel: true
|
||||
}
|
||||
},
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
@@ -75,62 +80,25 @@ describe('TreeExplorerTreeNode', () => {
|
||||
expect(editableText.props('isEditing')).toBe(true)
|
||||
})
|
||||
|
||||
it('triggers handleRename callback when editing is finished', async () => {
|
||||
const handleRenameMock = vi.fn()
|
||||
const nodeWithMockRename = {
|
||||
...mockNode,
|
||||
handleRename: handleRenameMock
|
||||
}
|
||||
it('triggers handleEditLabel callback when editing is finished', async () => {
|
||||
const handleEditLabelMock = vi.fn()
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: nodeWithMockRename },
|
||||
props: {
|
||||
node: {
|
||||
...mockNode,
|
||||
isEditingLabel: true
|
||||
}
|
||||
},
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
provide: { [InjectKeyHandleEditLabelFunction]: handleEditLabelMock },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows error toast when handleRename promise rejects', async () => {
|
||||
const handleRenameMock = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Rename failed'))
|
||||
const nodeWithMockRename = {
|
||||
...mockNode,
|
||||
handleRename: handleRenameMock
|
||||
}
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: nodeWithMockRename },
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const toastStore = useToastStore()
|
||||
const addToastSpy = vi.spyOn(toastStore, 'add')
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
|
||||
// Wait for the promise to reject and the toast to be added
|
||||
vi.runAllTimers()
|
||||
|
||||
// Wait for any pending promises to resolve
|
||||
await new Promise(process.nextTick)
|
||||
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
expect(addToastSpy).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Rename failed'
|
||||
})
|
||||
expect(handleEditLabelMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
v-bind="item.contentProps"
|
||||
:maximized="item.dialogComponentProps.maximized"
|
||||
/>
|
||||
|
||||
<template #footer v-if="item.footerComponent">
|
||||
<component :is="item.footerComponent" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
|
||||
114
src/components/dialog/content/ManagerProgressDialogContent.vue
Normal file
114
src/components/dialog/content/ManagerProgressDialogContent.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden transition-all duration-300"
|
||||
:class="{
|
||||
'max-h-[500px]': isExpanded,
|
||||
'max-h-0 p-0 m-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="sectionsContainerRef"
|
||||
class="px-6 py-4 overflow-y-auto max-h-[450px] scroll-container"
|
||||
:style="{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(156, 163, 175, 0.5) transparent'
|
||||
}"
|
||||
:class="{
|
||||
'max-h-[450px]': isExpanded,
|
||||
'max-h-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div v-for="(panel, index) in taskPanels" :key="index">
|
||||
<Panel
|
||||
:expanded="expandedPanels[index] || false"
|
||||
toggleable
|
||||
class="shadow-elevation-1 rounded-lg mt-2 dark-theme:bg-black dark-theme:border-black"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full py-2">
|
||||
<div class="flex flex-col text-sm font-medium leading-normal">
|
||||
<span>{{ panel.taskName }}</span>
|
||||
<span v-show="expandedPanels[index]" class="text-muted">
|
||||
{{
|
||||
index === taskPanels.length - 1
|
||||
? 'In progress'
|
||||
: 'Completed ✓'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #toggleicon>
|
||||
<Button
|
||||
:icon="
|
||||
expandedPanels[index]
|
||||
? 'pi pi-chevron-down'
|
||||
: 'pi pi-chevron-right'
|
||||
"
|
||||
text
|
||||
class="text-neutral-300"
|
||||
@click="togglePanel(index)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="overflow-y-auto h-64 rounded-lg bg-black"
|
||||
:class="{
|
||||
'h-64': index !== taskPanels.length - 1,
|
||||
'flex-grow': index === taskPanels.length - 1
|
||||
}"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-for="(log, logIndex) in panel.logs"
|
||||
:key="logIndex"
|
||||
class="text-neutral-400 dark-theme:text-muted"
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useScroll, whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const { taskLogs } = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
|
||||
const taskPanels = computed(() => taskLogs)
|
||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
||||
|
||||
const expandedPanels = ref<Record<number, boolean>>({})
|
||||
const togglePanel = (index: number) => {
|
||||
expandedPanels.value[index] = !expandedPanels.value[index]
|
||||
}
|
||||
|
||||
const sectionsContainerRef = ref<HTMLElement | null>(null)
|
||||
const { y: scrollY } = useScroll(sectionsContainerRef)
|
||||
|
||||
const scrollToBottom = () => {
|
||||
scrollY.value = sectionsContainerRef.value?.scrollHeight ?? 0
|
||||
}
|
||||
|
||||
whenever(() => isExpanded.value, scrollToBottom)
|
||||
onMounted(() => {
|
||||
expandedPanels.value = {}
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
progressDialogContent.collapse()
|
||||
})
|
||||
</script>
|
||||
@@ -124,7 +124,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_CARD_SIZE = 512
|
||||
const DEFAULT_CARD_SIZE = 349
|
||||
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
option-value="value"
|
||||
:options="allVersionOptions"
|
||||
:highlight-on-select="false"
|
||||
class="my-3 w-full max-h-[50vh] border-none"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex justify-between items-center w-full p-1">
|
||||
|
||||
@@ -5,62 +5,65 @@
|
||||
'outline outline-[6px] outline-[var(--p-primary-color)]': isSelected
|
||||
}"
|
||||
:pt="{
|
||||
body: { class: 'p-0 flex flex-col h-full rounded-2xl' },
|
||||
body: { class: 'p-0 flex flex-col h-full rounded-2xl gap-0' },
|
||||
content: { class: 'flex-1 flex flex-col rounded-2xl' },
|
||||
title: { class: 'p-0 m-0' },
|
||||
title: {
|
||||
class:
|
||||
'self-stretch px-4 py-3 inline-flex justify-start items-center gap-6'
|
||||
},
|
||||
footer: { class: 'p-0 m-0' }
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex justify-between p-5 pb-1 align-middle text-sm">
|
||||
<span class="flex items-start mt-2">
|
||||
<i
|
||||
class="pi pi-box text-muted text-2xl ml-1 mr-5"
|
||||
style="opacity: 0.5"
|
||||
/>
|
||||
<span class="text-lg relative top-[.25rem]">{{
|
||||
$t('manager.nodePack')
|
||||
}}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isPackInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<PackCardHeader :node-pack="nodePack" />
|
||||
</template>
|
||||
<template #content>
|
||||
<ContentDivider />
|
||||
<div class="flex flex-1 p-5 mt-3 cursor-pointer">
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<PackIcon :node-pack="nodePack" />
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<div
|
||||
class="self-stretch px-4 py-3 inline-flex justify-start items-start cursor-pointer"
|
||||
>
|
||||
<PackIcon :node-pack="nodePack" />
|
||||
<div
|
||||
class="px-4 inline-flex flex-col justify-start items-start overflow-hidden"
|
||||
>
|
||||
<span
|
||||
class="text-lg font-bold pb-4 truncate overflow-hidden text-ellipsis"
|
||||
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
|
||||
:title="nodePack.name"
|
||||
>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-center items-center gap-2.5"
|
||||
>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="text-sm text-color-secondary m-0 line-clamp-3 overflow-hidden"
|
||||
class="flex-1 justify-start text-muted text-sm font-medium leading-3 break-words overflow-hidden min-h-12 line-clamp-3"
|
||||
:title="nodePack.description"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2"
|
||||
>
|
||||
<div
|
||||
v-if="nodesCount"
|
||||
class="px-2 py-1 flex justify-center text-sm items-center gap-1"
|
||||
>
|
||||
<div class="text-center justify-center font-medium leading-3">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 py-1 flex justify-center items-center gap-1">
|
||||
<div
|
||||
v-if="isUpdateAvailable"
|
||||
class="w-4 h-4 relative overflow-hidden"
|
||||
>
|
||||
<i class="pi pi-arrow-circle-up text-blue-600" />
|
||||
</div>
|
||||
<PackVersionBadge :node-pack="nodePack" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,21 +79,35 @@ import Card from 'primevue/card'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack, isSelected = false } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
isSelected?: boolean
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { isPackInstalled, getInstalledPackVersion } = useComfyManagerStore()
|
||||
|
||||
const isPackInstalled = computed(() =>
|
||||
managerStore.isPackInstalled(nodePack?.id)
|
||||
)
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isUpdateAvailable = computed(() => {
|
||||
if (!isInstalled.value) return false
|
||||
|
||||
const latestVersion = nodePack.latest_version?.version
|
||||
if (!latestVersion) return false
|
||||
|
||||
const installedVersion = getInstalledPackVersion(nodePack.id)
|
||||
|
||||
// Don't attempt to show update available for nightly GitHub packs
|
||||
if (installedVersion && !isSemVer(installedVersion)) return false
|
||||
|
||||
return compareVersions(latestVersion, installedVersion) > 0
|
||||
})
|
||||
|
||||
// TODO: remove type assertion once comfy_nodes is added to node (pack) info type in backend
|
||||
const nodesCount = computed(() => (nodePack as any).comfy_nodes?.length)
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="flex justify-between p-5 text-xs text-muted">
|
||||
<div
|
||||
class="flex justify-between px-5 py-4 text-xs text-muted font-medium leading-3"
|
||||
>
|
||||
<div class="flex items-center gap-2 cursor-pointer">
|
||||
<span v-if="nodePack.publisher?.name">
|
||||
{{ nodePack.publisher.name }}
|
||||
<span v-if="publisherName" class="max-w-40 truncate">
|
||||
{{ publisherName }}
|
||||
</span>
|
||||
<PackVersionBadge v-if="isInstalled" :node-pack="nodePack" />
|
||||
<span v-else-if="nodePack.latest_version">
|
||||
{{ nodePack.latest_version.version }}
|
||||
</span>
|
||||
@@ -26,14 +27,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const publisherName = computed(() => {
|
||||
if (!nodePack) return null
|
||||
|
||||
const { publisher, author } = nodePack
|
||||
return publisher?.name ?? publisher?.id ?? author
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="w-[100%] flex justify-between items-center">
|
||||
<div class="flex justify-start items-center">
|
||||
<div class="w-1 h-6 rounded-md" />
|
||||
<div class="w-6 h-6 relative overflow-hidden">
|
||||
<i class="pi pi-box text-xl text-muted" style="opacity: 0.6" />
|
||||
</div>
|
||||
<div class="px-3 py-2 rounded-md flex justify-start items-start gap-2.5">
|
||||
<div class="text-right justify-start text-sm font-bold leading-none">
|
||||
{{ $t('manager.nodePack') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex justify-start items-center gap-3">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
</script>
|
||||
107
src/components/dialog/footer/ManagerProgressFooter.vue
Normal file
107
src/components/dialog/footer/ManagerProgressFooter.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isInProgress">
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} of
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress"
|
||||
rounded
|
||||
outlined
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('g.restart') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
? 'pi pi-chevron-up'
|
||||
: 'pi pi-chevron-right'
|
||||
"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const currentTaskName = computed(() => {
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
return task?.taskName ?? fallbackTaskName
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
|
||||
const onReconnect = () => {
|
||||
useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
}
|
||||
</script>
|
||||
29
src/components/dialog/header/ManagerProgressHeader.vue
Normal file
29
src/components/dialog/header/ManagerProgressHeader.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="progressDialogContent.isExpanded"
|
||||
class="px-4 py-2 flex items-center"
|
||||
>
|
||||
<TabMenu
|
||||
v-model:activeIndex="activeTabIndex"
|
||||
:model="tabs"
|
||||
class="w-full border-none"
|
||||
:pt="{
|
||||
menu: { class: 'border-none' },
|
||||
menuitem: { class: 'font-medium' },
|
||||
action: { class: 'px-4 py-2' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const tabs = [{ label: 'Installation Queue' }, { label: 'Failed (0)' }]
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@ const widgets = computed(() =>
|
||||
)
|
||||
)
|
||||
|
||||
const MARGIN = 10
|
||||
const DEFAULT_MARGIN = 10
|
||||
const updateWidgets = () => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
if (!lgCanvas) return
|
||||
@@ -49,10 +49,11 @@ const updateWidgets = () => {
|
||||
|
||||
widgetState.visible = visible
|
||||
if (visible) {
|
||||
widgetState.pos = [node.pos[0] + MARGIN, node.pos[1] + MARGIN + widget.y]
|
||||
const margin = widget.options.margin ?? DEFAULT_MARGIN
|
||||
widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]
|
||||
widgetState.size = [
|
||||
(widget.width ?? node.width) - MARGIN * 2,
|
||||
(widget.computedHeight ?? 50) - MARGIN * 2
|
||||
(widget.width ?? node.width) - margin * 2,
|
||||
(widget.computedHeight ?? 50) - margin * 2
|
||||
]
|
||||
// TODO: optimize this logic as it's O(n), where n is the number of nodes
|
||||
widgetState.zIndex = lgCanvas.graph.nodes.indexOf(node)
|
||||
|
||||
@@ -33,9 +33,6 @@
|
||||
<Message v-if="pathExists" severity="warn">
|
||||
{{ $t('install.pathExists') }}
|
||||
</Message>
|
||||
<Message v-if="nonDefaultDrive" severity="warn">
|
||||
{{ $t('install.nonDefaultDrive') }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<!-- System Paths Info -->
|
||||
@@ -83,7 +80,6 @@ const { t } = useI18n()
|
||||
const installPath = defineModel<string>('installPath', { required: true })
|
||||
const pathError = defineModel<string>('pathError', { required: true })
|
||||
const pathExists = ref(false)
|
||||
const nonDefaultDrive = ref(false)
|
||||
const appData = ref('')
|
||||
const appPath = ref('')
|
||||
const inputTouched = ref(false)
|
||||
@@ -104,7 +100,6 @@ const validatePath = async (path: string) => {
|
||||
try {
|
||||
pathError.value = ''
|
||||
pathExists.value = false
|
||||
nonDefaultDrive.value = false
|
||||
const validation = await electron.validateInstallPath(path)
|
||||
|
||||
// Create a pre-formatted list of errors
|
||||
@@ -116,14 +111,12 @@ const validatePath = async (path: string) => {
|
||||
errors.push(`${t('install.insufficientFreeSpace')}: ${requiredGB} GB`)
|
||||
}
|
||||
if (validation.parentMissing) errors.push(t('install.parentMissing'))
|
||||
if (validation.isOneDrive) errors.push(t('install.isOneDrive'))
|
||||
|
||||
if (validation.error)
|
||||
errors.push(`${t('install.unhandledError')}: ${validation.error}`)
|
||||
pathError.value = errors.join('\n')
|
||||
}
|
||||
|
||||
if (validation.isNonDefaultDrive) nonDefaultDrive.value = true
|
||||
// Display the path exists warning
|
||||
if (validation.exists) pathExists.value = true
|
||||
} catch (error) {
|
||||
pathError.value = t('install.pathValidationFailed')
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
:root="renderedRoot"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
:root="renderedRoot"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
|
||||
@@ -5,28 +5,18 @@
|
||||
>
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
icon="pi pi-th-large"
|
||||
icon="pi pi-folder-plus"
|
||||
@click="addNewFolder"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.browseTemplates')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.BrowseTemplates')"
|
||||
v-tooltip.bottom="$t('g.newFolder')"
|
||||
/>
|
||||
<Button
|
||||
class="open-workflow-button"
|
||||
icon="pi pi-folder-open"
|
||||
icon="pi pi-refresh"
|
||||
@click="workflowStore.syncWorkflows()"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.openWorkflow')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.OpenWorkflow')"
|
||||
/>
|
||||
<Button
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.newBlankWorkflow')"
|
||||
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
|
||||
text
|
||||
v-tooltip.bottom="$t('g.refresh')"
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
@@ -49,9 +39,7 @@
|
||||
class="ml-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open).children
|
||||
"
|
||||
:root="renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open)"
|
||||
:selectionKeys="selectionKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
@@ -87,11 +75,11 @@
|
||||
class="ml-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
:root="
|
||||
renderTreeNode(
|
||||
bookmarkedWorkflowsTree,
|
||||
WorkflowTreeType.Bookmarks
|
||||
).children
|
||||
)
|
||||
"
|
||||
:selectionKeys="selectionKeys"
|
||||
>
|
||||
@@ -107,11 +95,10 @@
|
||||
class="ml-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
renderTreeNode(workflowsTree, WorkflowTreeType.Browse).children
|
||||
"
|
||||
:root="renderTreeNode(workflowsTree, WorkflowTreeType.Browse)"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:selectionKeys="selectionKeys"
|
||||
ref="persistedWorkflowsTreeExplorerRef"
|
||||
v-if="workflowStore.persistedWorkflows.length > 0"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
@@ -128,9 +115,7 @@
|
||||
</div>
|
||||
<div class="comfyui-workflows-search-panel" v-else>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
renderTreeNode(filteredRoot, WorkflowTreeType.Browse).children
|
||||
"
|
||||
:root="renderTreeNode(filteredRoot, WorkflowTreeType.Browse)"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
@@ -159,7 +144,6 @@ import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue
|
||||
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
|
||||
import { useTreeExpansion } from '@/composables/useTreeExpansion'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import {
|
||||
useWorkflowBookmarkStore,
|
||||
@@ -197,7 +181,6 @@ const handleSearch = (query: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -244,7 +227,6 @@ const renderTreeNode = (
|
||||
type: WorkflowTreeType
|
||||
): TreeExplorerNode<ComfyWorkflow> => {
|
||||
const children = node.children?.map((child) => renderTreeNode(child, type))
|
||||
|
||||
const workflow: ComfyWorkflow = node.data
|
||||
|
||||
function handleClick(this: TreeExplorerNode<ComfyWorkflow>, e: MouseEvent) {
|
||||
@@ -255,7 +237,7 @@ const renderTreeNode = (
|
||||
}
|
||||
}
|
||||
|
||||
const actions = node.leaf
|
||||
const actions: Partial<TreeExplorerNode<ComfyWorkflow>> = node.leaf
|
||||
? {
|
||||
handleClick,
|
||||
async handleRename(newName: string) {
|
||||
@@ -285,7 +267,16 @@ const renderTreeNode = (
|
||||
},
|
||||
draggable: true
|
||||
}
|
||||
: { handleClick }
|
||||
: {
|
||||
handleClick,
|
||||
async handleAddFolder(folderName: string) {
|
||||
if (folderName === '') return
|
||||
|
||||
const parentPath = this.key.replace(/^root\/?/, '')
|
||||
const folderPath = parentPath + '/' + folderName
|
||||
await workflowStore.createFolder(folderPath)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
@@ -305,4 +296,11 @@ const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||
onMounted(async () => {
|
||||
await workflowBookmarkStore.loadBookmarks()
|
||||
})
|
||||
|
||||
const persistedWorkflowsTreeExplorerRef = ref<InstanceType<
|
||||
typeof TreeExplorer
|
||||
> | null>(null)
|
||||
const addNewFolder = () => {
|
||||
persistedWorkflowsTreeExplorerRef.value?.addFolderCommand?.('root')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:root="renderedBookmarkedRoot"
|
||||
:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
@@ -40,7 +40,6 @@ import type {
|
||||
TreeExplorerDragAndDropData,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import { findNodeByKey } from '@/utils/treeUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
filteredNodeDefs: ComfyNodeDefImpl[]
|
||||
@@ -94,14 +93,6 @@ const { t } = useI18n()
|
||||
const extraMenuItems = (
|
||||
menuTargetNode: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
) => [
|
||||
{
|
||||
label: t('g.newFolder'),
|
||||
icon: 'pi pi-folder-plus',
|
||||
command: () => {
|
||||
addNewBookmarkFolder(menuTargetNode)
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
},
|
||||
{
|
||||
label: t('g.customize'),
|
||||
icon: 'pi pi-palette',
|
||||
@@ -152,6 +143,11 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
},
|
||||
children: sortedChildren,
|
||||
draggable: node.leaf,
|
||||
handleAddFolder(newName: string) {
|
||||
if (newName !== '') {
|
||||
nodeBookmarkStore.addNewBookmarkFolder(this.data, newName)
|
||||
}
|
||||
},
|
||||
renderDragPreview(container) {
|
||||
const vnode = h(NodePreview, { nodeDef: node.data })
|
||||
render(vnode, container)
|
||||
@@ -197,25 +193,8 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
)
|
||||
|
||||
const treeExplorerRef = ref<InstanceType<typeof TreeExplorer> | null>(null)
|
||||
const addNewBookmarkFolder = (
|
||||
parent?: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
) => {
|
||||
const newFolderKey =
|
||||
'root/' + nodeBookmarkStore.addNewBookmarkFolder(parent?.data).slice(0, -1)
|
||||
nextTick(() => {
|
||||
treeExplorerRef.value?.renameCommand(
|
||||
findNodeByKey(
|
||||
renderedBookmarkedRoot.value,
|
||||
newFolderKey
|
||||
) as RenderedTreeExplorerNode
|
||||
)
|
||||
if (parent) {
|
||||
expandedKeys.value[parent.key] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
addNewBookmarkFolder
|
||||
addNewBookmarkFolder: () => treeExplorerRef.value?.addFolderCommand('root')
|
||||
})
|
||||
|
||||
const showCustomizationDialog = ref(false)
|
||||
|
||||
@@ -8,21 +8,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import type { BookmarkCustomization } from '@/schemas/apiSchema'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
InjectKeyExpandedKeys,
|
||||
type RenderedTreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
const { node } = defineProps<{
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
}>()
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const customization = computed<BookmarkCustomization | undefined>(() => {
|
||||
return nodeBookmarkStore.bookmarksCustomization[props.node.data.nodePath]
|
||||
const nodeDef = node.data
|
||||
return nodeDef
|
||||
? nodeBookmarkStore.bookmarksCustomization[nodeDef.nodePath]
|
||||
: undefined
|
||||
})
|
||||
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
@@ -56,7 +62,7 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const expandedKeys = inject<Ref<Record<string, boolean>>>('expandedKeys')
|
||||
const expandedKeys = inject(InjectKeyExpandedKeys)
|
||||
const handleItemDrop = (node: RenderedTreeExplorerNode) => {
|
||||
expandedKeys.value[node.key] = true
|
||||
}
|
||||
|
||||
76
src/composables/tree/useTreeFolderOperations.ts
Normal file
76
src/composables/tree/useTreeFolderOperations.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
/**
|
||||
* Use this to handle folder operations in a tree.
|
||||
* @param expandNode - The function to expand a node.
|
||||
*/
|
||||
export function useTreeFolderOperations(
|
||||
expandNode: (node: RenderedTreeExplorerNode) => void
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const newFolderNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
const addFolderTargetNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
|
||||
// Generate a unique temporary key for the new folder
|
||||
const generateTempKey = (parentKey: string) => {
|
||||
return `${parentKey}/new_folder_${Date.now()}`
|
||||
}
|
||||
|
||||
// Handle folder creation after name is confirmed
|
||||
const handleFolderCreation = async (newName: string) => {
|
||||
if (!newFolderNode.value || !addFolderTargetNode.value) return
|
||||
|
||||
try {
|
||||
// Call the handleAddFolder method with the new folder name
|
||||
await addFolderTargetNode.value?.handleAddFolder?.(newName)
|
||||
} finally {
|
||||
newFolderNode.value = null
|
||||
addFolderTargetNode.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The command to add a folder to a node via the context menu
|
||||
* @param targetNode - The node where the folder will be added under
|
||||
*/
|
||||
const addFolderCommand = (targetNode: RenderedTreeExplorerNode) => {
|
||||
expandNode(targetNode)
|
||||
newFolderNode.value = {
|
||||
key: generateTempKey(targetNode.key),
|
||||
label: '',
|
||||
leaf: false,
|
||||
children: [],
|
||||
icon: 'pi pi-folder',
|
||||
type: 'folder',
|
||||
totalLeaves: 0,
|
||||
badgeText: '',
|
||||
isEditingLabel: true
|
||||
}
|
||||
addFolderTargetNode.value = targetNode
|
||||
}
|
||||
|
||||
// Generate the "Add Folder" menu item
|
||||
const getAddFolderMenuItem = (
|
||||
targetNode: RenderedTreeExplorerNode | null
|
||||
) => {
|
||||
return {
|
||||
label: t('g.newFolder'),
|
||||
icon: 'pi pi-folder-plus',
|
||||
command: () => {
|
||||
if (targetNode) addFolderCommand(targetNode)
|
||||
},
|
||||
visible: targetNode && !targetNode.leaf && !!targetNode.handleAddFolder,
|
||||
isAsync: false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newFolderNode,
|
||||
addFolderCommand,
|
||||
getAddFolderMenuItem,
|
||||
handleFolderCreation
|
||||
}
|
||||
}
|
||||
@@ -590,6 +590,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ToggleManagerProgressDialog',
|
||||
icon: 'pi pi-spinner',
|
||||
label: 'Toggle Progress Dialog',
|
||||
versionAdded: '1.13.9',
|
||||
function: () => {
|
||||
dialogService.showManagerProgressDialog()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
|
||||
import { PackField } from '@/types/comfyManagerTypes'
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 16
|
||||
const SEARCH_DEBOUNCE_TIME = 256
|
||||
const DEFAULT_PAGE_SIZE = 64
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,30 +23,20 @@ export function useTreeExpansion(expandedKeys: Ref<Record<string, boolean>>) {
|
||||
}
|
||||
|
||||
const expandNode = (node: TreeNode) => {
|
||||
if (
|
||||
node.key &&
|
||||
typeof node.key === 'string' &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
if (node.key && typeof node.key === 'string' && !node.leaf) {
|
||||
expandedKeys.value[node.key] = true
|
||||
|
||||
for (const child of node.children) {
|
||||
for (const child of node.children ?? []) {
|
||||
expandNode(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const collapseNode = (node: TreeNode) => {
|
||||
if (
|
||||
node.key &&
|
||||
typeof node.key === 'string' &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
if (node.key && typeof node.key === 'string' && !node.leaf) {
|
||||
delete expandedKeys.value[node.key]
|
||||
|
||||
for (const child of node.children) {
|
||||
for (const child of node.children ?? []) {
|
||||
collapseNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ export const useImageUploadWidget = () => {
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
fileComboWidget.callback = function () {
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value)
|
||||
node.graph?.setDirtyCanvas(true)
|
||||
}
|
||||
|
||||
// On load if we have a value then render the image
|
||||
|
||||
@@ -12,7 +12,6 @@ import './nodeTemplates'
|
||||
import './noteNode'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './saveMesh'
|
||||
import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
import './uploadAudio'
|
||||
|
||||
@@ -8,11 +8,6 @@ import { api } from '@/scripts/api'
|
||||
class Load3DConfiguration {
|
||||
constructor(private load3d: Load3d) {}
|
||||
|
||||
configureForSaveMesh(loadFolder: 'input' | 'output', filePath: string) {
|
||||
this.setupModelHandlingForSaveMesh(filePath, loadFolder)
|
||||
this.setupDefaultProperties()
|
||||
}
|
||||
|
||||
configure(
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
@@ -39,17 +34,6 @@ class Load3DConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
private setupModelHandlingForSaveMesh(
|
||||
filePath: string,
|
||||
loadFolder: 'input' | 'output'
|
||||
) {
|
||||
const onModelWidgetUpdate = this.createModelUpdateHandler(loadFolder)
|
||||
|
||||
if (filePath) {
|
||||
onModelWidgetUpdate(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
private setupModelHandling(
|
||||
modelWidget: IWidget,
|
||||
loadFolder: 'input' | 'output',
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.SaveGLB',
|
||||
|
||||
async beforeRegisterNodeDef(_nodeType, nodeData) {
|
||||
if ('SaveGLB' === nodeData.name) {
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
nodeData.input.required.image = ['PREVIEW_3D']
|
||||
}
|
||||
},
|
||||
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
PREVIEW_3D(node) {
|
||||
const inputSpec: CustomInputSpec = {
|
||||
name: 'image',
|
||||
type: 'Preview3D'
|
||||
}
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: Load3D,
|
||||
inputSpec,
|
||||
options: {}
|
||||
})
|
||||
|
||||
addWidget(node, widget)
|
||||
|
||||
return { widget }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.constructor.comfyClass !== 'SaveGLB') return
|
||||
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
node.setSize([Math.max(oldWidth, 400), Math.max(oldHeight, 550)])
|
||||
|
||||
await nextTick()
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
onExecuted?.apply(this, arguments as any)
|
||||
|
||||
const fileInfo = message['3d'][0]
|
||||
|
||||
const load3d = useLoad3dService().getLoad3d(node)
|
||||
|
||||
const modelWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
|
||||
|
||||
if (load3d && modelWidget) {
|
||||
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']
|
||||
|
||||
console.log(filePath)
|
||||
|
||||
modelWidget.value = filePath
|
||||
|
||||
const config = new Load3DConfiguration(load3d)
|
||||
|
||||
config.configureForSaveMesh(fileInfo['type'], filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle Progress Dialog"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -94,10 +94,12 @@
|
||||
"filter": "Filter",
|
||||
"apply": "Apply",
|
||||
"enabled": "Enabled",
|
||||
"installed": "Installed"
|
||||
"installed": "Installed",
|
||||
"restart": "Restart"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
|
||||
"loadingVersions": "Loading versions...",
|
||||
"selectVersion": "Select Version",
|
||||
"downloads": "Downloads",
|
||||
@@ -258,11 +260,9 @@
|
||||
"pathExists": "Directory already exists - please ensure you have backed up all data",
|
||||
"cannotWrite": "Unable to write to the selected path",
|
||||
"insufficientFreeSpace": "Insufficient space - minimum free space",
|
||||
"isOneDrive": "OneDrive is not supported. Please install ComfyUI in another location.",
|
||||
"nonDefaultDrive": "Please install ComfyUI on your system drive (eg. C:\\). Drives with different file systems may cause unpredicable issues. Models and other files can be stored on other drives after installation.",
|
||||
"parentMissing": "Path does not exist - create the containing directory first",
|
||||
"unhandledError": "Unknown error",
|
||||
"installLocationDescription": "Select the directory for ComfyUI's user data. A python environment will be installed to the selected location.",
|
||||
"installLocationDescription": "Select the directory for ComfyUI's user data. A python environment will be installed to the selected location. Please make sure the selected disk has enough space (~15GB) left.",
|
||||
"installLocationTooltip": "ComfyUI's user data directory. Stores:\n- Python Environment\n- Models\n- Custom nodes\n",
|
||||
"appDataLocationTooltip": "ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
|
||||
"appPathLocationTooltip": "ComfyUI's app asset directory. Stores the ComfyUI code and assets",
|
||||
@@ -508,10 +508,7 @@
|
||||
"area_composition_square_area_for_subject": "Area Composition Square Area for Subject"
|
||||
},
|
||||
"3D": {
|
||||
"stable_zero123_example": "Stable Zero123",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0",
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 2.0 MV",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 2.0 MV Turbo"
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Stable Audio"
|
||||
@@ -620,6 +617,7 @@
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Toggle Progress Dialog": "Toggle Progress Dialog",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Open": "Open",
|
||||
@@ -851,7 +849,6 @@
|
||||
"latent": "latent",
|
||||
"video": "video",
|
||||
"audio": "audio",
|
||||
"3d": "3d",
|
||||
"ltxv": "ltxv",
|
||||
"sd3": "sd3",
|
||||
"sigmas": "sigmas",
|
||||
@@ -867,6 +864,7 @@
|
||||
"compositing": "compositing",
|
||||
"samplers": "samplers",
|
||||
"operations": "operations",
|
||||
"3d": "3d",
|
||||
"debug": "debug",
|
||||
"model": "model",
|
||||
"model_specific": "model_specific",
|
||||
@@ -903,7 +901,6 @@
|
||||
"LOAD_3D": "LOAD_3D",
|
||||
"LOAD_3D_ANIMATION": "LOAD_3D_ANIMATION",
|
||||
"MASK": "MASK",
|
||||
"MESH": "MESH",
|
||||
"MODEL": "MODEL",
|
||||
"NOISE": "NOISE",
|
||||
"PHOTOMAKER": "PHOTOMAKER",
|
||||
@@ -914,7 +911,6 @@
|
||||
"TIMESTEPS_RANGE": "TIMESTEPS_RANGE",
|
||||
"UPSCALE_MODEL": "UPSCALE_MODEL",
|
||||
"VAE": "VAE",
|
||||
"VOXEL": "VOXEL",
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"maintenance": {
|
||||
|
||||
@@ -1234,18 +1234,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentHunyuan3Dv2": {
|
||||
"display_name": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "batch_size",
|
||||
"tooltip": "The number of latent images in the batch."
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
"display_name": "Empty Latent Image",
|
||||
"description": "Create a new batch of empty latent images to be denoised via sampling.",
|
||||
@@ -1484,47 +1472,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2Conditioning": {
|
||||
"display_name": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {
|
||||
"clip_vision_output": {
|
||||
"name": "clip_vision_output"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positive"
|
||||
},
|
||||
"1": {
|
||||
"name": "negative"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2ConditioningMultiView": {
|
||||
"display_name": "Hunyuan3Dv2ConditioningMultiView",
|
||||
"inputs": {
|
||||
"front": {
|
||||
"name": "front"
|
||||
},
|
||||
"left": {
|
||||
"name": "left"
|
||||
},
|
||||
"back": {
|
||||
"name": "back"
|
||||
},
|
||||
"right": {
|
||||
"name": "right"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positive"
|
||||
},
|
||||
"1": {
|
||||
"name": "negative"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HunyuanImageToVideo": {
|
||||
"display_name": "HunyuanImageToVideo",
|
||||
"inputs": {
|
||||
@@ -2417,9 +2364,6 @@
|
||||
"audio": {
|
||||
"name": "audio"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"upload": {
|
||||
"name": "choose file to upload"
|
||||
}
|
||||
@@ -4665,9 +4609,6 @@
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "audio"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5025,23 +4966,6 @@
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "filename_prefix"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveGLB": {
|
||||
"display_name": "SaveGLB",
|
||||
"inputs": {
|
||||
"mesh": {
|
||||
"name": "mesh"
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "filename_prefix"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5801,23 +5725,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"samples": {
|
||||
"name": "samples"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"num_chunks": {
|
||||
"name": "num_chunks"
|
||||
},
|
||||
"octree_resolution": {
|
||||
"name": "octree_resolution"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
"display_name": "VAE Decode (Tiled)",
|
||||
"inputs": {
|
||||
@@ -5948,17 +5855,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"voxel": {
|
||||
"name": "voxel"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "threshold"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VPScheduler": {
|
||||
"display_name": "VPScheduler",
|
||||
"inputs": {
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
"LOAD_3D": "CHARGER_3D",
|
||||
"LOAD_3D_ANIMATION": "CHARGER_ANIMATION_3D",
|
||||
"MASK": "MASQUE",
|
||||
"MESH": "MAILLAGE",
|
||||
"MODEL": "MODÈLE",
|
||||
"NOISE": "BRUIT",
|
||||
"PHOTOMAKER": "PHOTOMAKER",
|
||||
@@ -85,7 +84,6 @@
|
||||
"TIMESTEPS_RANGE": "PLAGE_DES_ÉTAPES_TEMPORELLES",
|
||||
"UPSCALE_MODEL": "MODÈLE_DE_MISE_À_L'ÉCHELLE",
|
||||
"VAE": "VAE",
|
||||
"VOXEL": "VOXEL",
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"desktopMenu": {
|
||||
@@ -190,6 +188,7 @@
|
||||
"reportSent": "Rapport soumis",
|
||||
"reset": "Réinitialiser",
|
||||
"resetKeybindingsTooltip": "Réinitialiser les raccourcis clavier par défaut",
|
||||
"restart": "Redémarrer",
|
||||
"resultsCount": "{count} Résultats Trouvés",
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement",
|
||||
@@ -268,7 +267,6 @@
|
||||
"installLocationDescription": "Sélectionnez le répertoire pour les données utilisateur de ComfyUI. Un environnement python sera installé à l'emplacement sélectionné. Veuillez vous assurer que le disque sélectionné a suffisamment d'espace (~15GB) restant.",
|
||||
"installLocationTooltip": "Répertoire des données utilisateur de ComfyUI. Stocke :\n- Environnement Python\n- Modèles\n- Nœuds personnalisés\n",
|
||||
"insufficientFreeSpace": "Espace insuffisant - espace libre minimum",
|
||||
"isOneDrive": "L'installation dans OneDrive peut causer des problèmes. Nous recommandons fortement d'installer dans un emplacement non-OneDrive.",
|
||||
"manualConfiguration": {
|
||||
"createVenv": "Vous devrez créer un environnement virtuel dans le répertoire suivant",
|
||||
"requirements": "Exigences",
|
||||
@@ -283,7 +281,6 @@
|
||||
"migrationOptional": "La migration est facultative. Si vous n'avez pas d'installation existante, vous pouvez sauter cette étape.",
|
||||
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation. Votre installation existante de ComfyUI ne sera pas affectée.",
|
||||
"moreInfo": "Pour plus d'informations, veuillez lire notre",
|
||||
"nonDefaultDrive": "Veuillez installer ComfyUI sur le disque système de votre ordinateur (par exemple C:\\). Les disques avec des systèmes de fichiers différents peuvent causer des problèmes imprévisibles. Les modèles et autres fichiers peuvent être stockés sur d'autres disques après l'installation.",
|
||||
"parentMissing": "Le chemin n'existe pas - créez d'abord le répertoire contenant",
|
||||
"pathExists": "Le répertoire existe déjà - veuillez vous assurer que vous avez sauvegardé toutes les données",
|
||||
"pathValidationFailed": "Échec de la validation du chemin",
|
||||
@@ -401,6 +398,7 @@
|
||||
"nodePack": "Pack de Nœuds",
|
||||
"packsSelected": "Packs sélectionnés",
|
||||
"repository": "Référentiel",
|
||||
"restartToApplyChanges": "Pour appliquer les modifications, veuillez redémarrer ComfyUI",
|
||||
"searchPlaceholder": "Recherche",
|
||||
"selectVersion": "Sélectionner la version",
|
||||
"sort": {
|
||||
@@ -534,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Basculer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Basculer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Progress Dialog": "Basculer la boîte de dialogue de progression",
|
||||
"Toggle Queue Sidebar": "Basculer la barre latérale de la file d'attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
@@ -884,9 +883,6 @@
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D Multivue",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D Turbo",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"Area Composition": {
|
||||
|
||||
@@ -1251,18 +1251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentHunyuan3Dv2": {
|
||||
"display_name": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "taille_du_lot",
|
||||
"tooltip": "Le nombre d'images latentes dans le lot."
|
||||
},
|
||||
"resolution": {
|
||||
"name": "résolution"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
"description": "Créez un nouveau lot d'images latentes vides à débruiter via l'échantillonnage.",
|
||||
"display_name": "Image Latente Vide",
|
||||
@@ -1484,47 +1472,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2Conditioning": {
|
||||
"display_name": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {
|
||||
"clip_vision_output": {
|
||||
"name": "sortie_vision_clip"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positif"
|
||||
},
|
||||
"1": {
|
||||
"name": "négatif"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2ConditioningMultiView": {
|
||||
"display_name": "Hunyuan3Dv2ConditioningMultiView",
|
||||
"inputs": {
|
||||
"back": {
|
||||
"name": "arrière"
|
||||
},
|
||||
"front": {
|
||||
"name": "avant"
|
||||
},
|
||||
"left": {
|
||||
"name": "gauche"
|
||||
},
|
||||
"right": {
|
||||
"name": "droite"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positif"
|
||||
},
|
||||
"1": {
|
||||
"name": "négatif"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HunyuanImageToVideo": {
|
||||
"display_name": "HunyuanImageToVideo",
|
||||
"inputs": {
|
||||
@@ -2589,9 +2536,6 @@
|
||||
"audio": {
|
||||
"name": "audio"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"upload": {
|
||||
"name": "choisissez le fichier à télécharger"
|
||||
}
|
||||
@@ -4670,9 +4614,6 @@
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "audio"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5153,28 +5094,11 @@
|
||||
"audio": {
|
||||
"name": "audio"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "préfixe_du_nom_de_fichier"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveGLB": {
|
||||
"display_name": "SaveGLB",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "préfixe_du_nom_de_fichier"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"mesh": {
|
||||
"name": "maillage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveImage": {
|
||||
"description": "Enregistre les images d'entrée dans votre répertoire de sortie ComfyUI.",
|
||||
"display_name": "Enregistrer Image",
|
||||
@@ -5781,23 +5705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"num_chunks": {
|
||||
"name": "nombre_de_morceaux"
|
||||
},
|
||||
"octree_resolution": {
|
||||
"name": "résolution_octree"
|
||||
},
|
||||
"samples": {
|
||||
"name": "échantillons"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
"display_name": "VAE Decode (Tiled)",
|
||||
"inputs": {
|
||||
@@ -5945,17 +5852,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
"name": "seuil"
|
||||
},
|
||||
"voxel": {
|
||||
"name": "voxel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanImageToVideo": {
|
||||
"display_name": "WanImageVersVidéo",
|
||||
"inputs": {
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
"LOAD_3D": "3Dをロード",
|
||||
"LOAD_3D_ANIMATION": "3Dアニメーションをロード",
|
||||
"MASK": "マスク",
|
||||
"MESH": "メッシュ",
|
||||
"MODEL": "モデル",
|
||||
"NOISE": "ノイズ",
|
||||
"PHOTOMAKER": "PHOTOMAKER",
|
||||
@@ -85,7 +84,6 @@
|
||||
"TIMESTEPS_RANGE": "タイムステップの範囲",
|
||||
"UPSCALE_MODEL": "アップスケールモデル",
|
||||
"VAE": "VAE",
|
||||
"VOXEL": "ボクセル",
|
||||
"WEBCAM": "ウェブカメラ"
|
||||
},
|
||||
"desktopMenu": {
|
||||
@@ -190,6 +188,7 @@
|
||||
"reportSent": "レポートが送信されました",
|
||||
"reset": "リセット",
|
||||
"resetKeybindingsTooltip": "キーバインディングをデフォルトにリセット",
|
||||
"restart": "再起動",
|
||||
"resultsCount": "{count}件の結果が見つかりました",
|
||||
"save": "保存",
|
||||
"saving": "保存中",
|
||||
@@ -268,7 +267,6 @@
|
||||
"installLocationDescription": "ComfyUIのユーザーデータを保存するディレクトリを選択してください。Python環境が選択した場所にインストールされます。選択したディスクに約15GBの空き容量が必要です。",
|
||||
"installLocationTooltip": "ComfyUIのユーザーデータディレクトリ。保存内容:\n- Python環境\n- モデル\n- カスタムノード\n",
|
||||
"insufficientFreeSpace": "空き容量が不足しています - 最低限の空き容量",
|
||||
"isOneDrive": "OneDriveにインストールすると問題が発生する可能性があります。非OneDriveの場所にインストールすることを強くお勧めします。",
|
||||
"manualConfiguration": {
|
||||
"createVenv": "次のディレクトリに仮想環境を作成する必要があります",
|
||||
"requirements": "要件",
|
||||
@@ -283,7 +281,6 @@
|
||||
"migrationOptional": "移行は任意です。既存のインストールがない場合、このステップをスキップできます。",
|
||||
"migrationSourcePathDescription": "既存のComfyUIインストールがある場合、既存のユーザーファイルとモデルを新しいインストールにコピー/リンクすることができます。既存のComfyUIインストールは影響を受けません。",
|
||||
"moreInfo": "詳しくはこちらをご覧ください",
|
||||
"nonDefaultDrive": "ComfyUIをシステムドライブ(例:C:\\)にインストールしてください。異なるファイルシステムを持つドライブでは、予測不能な問題が発生する可能性があります。インストール後にモデルやその他のファイルを他のドライブに保存することができます。",
|
||||
"parentMissing": "パスが存在しません - 最初に含まれるディレクトリを作成してください",
|
||||
"pathExists": "ディレクトリはすでに存在します - すべてのデータをバックアップしたことを確認してください",
|
||||
"pathValidationFailed": "パスの検証に失敗しました",
|
||||
@@ -401,6 +398,7 @@
|
||||
"nodePack": "ノードパック",
|
||||
"packsSelected": "選択したパック",
|
||||
"repository": "リポジトリ",
|
||||
"restartToApplyChanges": "変更を適用するには、ComfyUIを再起動してください",
|
||||
"searchPlaceholder": "検索",
|
||||
"selectVersion": "バージョンを選択",
|
||||
"sort": {
|
||||
@@ -534,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
|
||||
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
|
||||
"Toggle Progress Dialog": "進行状況ダイアログの切り替え",
|
||||
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
|
||||
"Toggle Search Box": "検索ボックスの切り替え",
|
||||
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
|
||||
@@ -884,9 +883,6 @@
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D マルチビュー",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D ターボ",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"Area Composition": {
|
||||
|
||||
@@ -1251,18 +1251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentHunyuan3Dv2": {
|
||||
"display_name": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "バッチサイズ",
|
||||
"tooltip": "バッチ内の潜在画像の数。"
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
"description": "サンプリングを通じてノイズを除去するための空の潜在画像の新しいバッチを作成します。",
|
||||
"display_name": "空の潜在画像",
|
||||
@@ -1484,47 +1472,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2Conditioning": {
|
||||
"display_name": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {
|
||||
"clip_vision_output": {
|
||||
"name": "clip_vision_output"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "ポジティブ"
|
||||
},
|
||||
"1": {
|
||||
"name": "ネガティブ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2ConditioningMultiView": {
|
||||
"display_name": "Hunyuan3Dv2ConditioningMultiView",
|
||||
"inputs": {
|
||||
"back": {
|
||||
"name": "バック"
|
||||
},
|
||||
"front": {
|
||||
"name": "フロント"
|
||||
},
|
||||
"left": {
|
||||
"name": "左"
|
||||
},
|
||||
"right": {
|
||||
"name": "右"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "ポジティブ"
|
||||
},
|
||||
"1": {
|
||||
"name": "ネガティブ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HunyuanImageToVideo": {
|
||||
"display_name": "HunyuanImageToVideo",
|
||||
"inputs": {
|
||||
@@ -2589,9 +2536,6 @@
|
||||
"audio": {
|
||||
"name": "オーディオ"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"upload": {
|
||||
"name": "アップロードするファイルを選択"
|
||||
}
|
||||
@@ -4670,9 +4614,6 @@
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "オーディオ"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5153,28 +5094,11 @@
|
||||
"audio": {
|
||||
"name": "オーディオ"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "ファイル名_プレフィックス"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveGLB": {
|
||||
"display_name": "SaveGLB",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "ファイル名のプレフィックス"
|
||||
},
|
||||
"image": {
|
||||
"name": "画像"
|
||||
},
|
||||
"mesh": {
|
||||
"name": "メッシュ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveImage": {
|
||||
"description": "入力画像をComfyUI出力ディレクトリに保存します。",
|
||||
"display_name": "画像を保存",
|
||||
@@ -5781,23 +5705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"num_chunks": {
|
||||
"name": "num_chunks"
|
||||
},
|
||||
"octree_resolution": {
|
||||
"name": "octree_resolution"
|
||||
},
|
||||
"samples": {
|
||||
"name": "サンプル"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
"display_name": "VAEデコード(タイル)",
|
||||
"inputs": {
|
||||
@@ -5945,17 +5852,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
"name": "閾値"
|
||||
},
|
||||
"voxel": {
|
||||
"name": "ボクセル"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanImageToVideo": {
|
||||
"display_name": "Wan画像からビデオへ",
|
||||
"inputs": {
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "프로그레스 대화 상자 전환"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
"LOAD_3D": "3D 로드",
|
||||
"LOAD_3D_ANIMATION": "3D 애니메이션 로드",
|
||||
"MASK": "마스크",
|
||||
"MESH": "메시",
|
||||
"MODEL": "모델",
|
||||
"NOISE": "노이즈",
|
||||
"PHOTOMAKER": "PHOTOMAKER",
|
||||
@@ -85,7 +84,6 @@
|
||||
"TIMESTEPS_RANGE": "타임스텝 범위",
|
||||
"UPSCALE_MODEL": "업스케일 모델",
|
||||
"VAE": "VAE",
|
||||
"VOXEL": "복셀",
|
||||
"WEBCAM": "웹캠"
|
||||
},
|
||||
"desktopMenu": {
|
||||
@@ -190,6 +188,7 @@
|
||||
"reportSent": "보고서 제출됨",
|
||||
"reset": "재설정",
|
||||
"resetKeybindingsTooltip": "키 바인딩을 기본값으로 재설정",
|
||||
"restart": "재시작",
|
||||
"resultsCount": "{count} 개의 결과를 찾았습니다",
|
||||
"save": "저장",
|
||||
"saving": "저장 중",
|
||||
@@ -268,7 +267,6 @@
|
||||
"installLocationDescription": "ComfyUI의 사용자 데이터 디렉토리를 선택하십시오. 선택한 위치에 Python 환경이 설치됩니다. 선택한 디스크에 충분한 공간(~15GB)이 남아 있는지 확인하십시오.",
|
||||
"installLocationTooltip": "ComfyUI의 사용자 데이터 디렉토리. 저장소:\n- Python 환경\n- 모델\n- 사용자 정의 노드\n",
|
||||
"insufficientFreeSpace": "공간이 부족합니다 - 최소한의 여유 공간",
|
||||
"isOneDrive": "OneDrive에 설치하면 문제가 발생할 수 있습니다. OneDrive가 아닌 위치에 설치하는 것을 강력히 권장합니다.",
|
||||
"manualConfiguration": {
|
||||
"createVenv": "다음 디렉토리에 가상 환경을 생성해야 합니다",
|
||||
"requirements": "요구 사항",
|
||||
@@ -283,7 +281,6 @@
|
||||
"migrationOptional": "마이그레이션은 선택 사항입니다. 기존에 설치된 것이 없다면, 이 단계를 건너뛸 수 있습니다.",
|
||||
"migrationSourcePathDescription": "기존에 설치된 ComfyUI가 있으면, 기존 사용자 파일과 모델을 새 설치본으로 복사하거나 링크 할 수 있습니다. 기존의 ComfyUI 설치는 영향을 받지 않습니다.",
|
||||
"moreInfo": "더 많은 정보를 원하시면, 다음을 읽어주세요",
|
||||
"nonDefaultDrive": "ComfyUI를 시스템 드라이브(예: C:\\)에 설치하십시오. 다른 파일 시스템을 가진 드라이브는 예측할 수 없는 문제를 일으킬 수 있습니다. 설치 후에는 모델 및 기타 파일을 다른 드라이브에 저장할 수 있습니다.",
|
||||
"parentMissing": "경로가 존재하지 않습니다 - 먼저 포함하는 디렉토리를 생성하세요",
|
||||
"pathExists": "디렉토리가 이미 존재합니다 - 모든 데이터를 백업했는지 확인해 주세요",
|
||||
"pathValidationFailed": "경로 유효성 검사 실패",
|
||||
@@ -401,6 +398,7 @@
|
||||
"nodePack": "노드 팩",
|
||||
"packsSelected": "선택한 팩",
|
||||
"repository": "저장소",
|
||||
"restartToApplyChanges": "변경 사항을 적용하려면 ComfyUI를 재시작해 주세요",
|
||||
"searchPlaceholder": "검색",
|
||||
"selectVersion": "버전 선택",
|
||||
"sort": {
|
||||
@@ -534,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Progress Dialog": "진행 상황 대화 상자 전환",
|
||||
"Toggle Queue Sidebar": "실행 큐 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
@@ -884,9 +883,6 @@
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 다중뷰",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 터보",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"stable_zero123_example": "스테이블 제로123"
|
||||
},
|
||||
"Area Composition": {
|
||||
|
||||
@@ -1251,18 +1251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentHunyuan3Dv2": {
|
||||
"display_name": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "배치 크기",
|
||||
"tooltip": "배치에 있는 잠재 이미지의 수입니다."
|
||||
},
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
"description": "샘플링을 통해 디노이즈할 빈 잠재 이미지의 새 배치를 생성합니다.",
|
||||
"display_name": "빈 잠재 이미지",
|
||||
@@ -1484,47 +1472,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2Conditioning": {
|
||||
"display_name": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {
|
||||
"clip_vision_output": {
|
||||
"name": "clip_vision_output"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "긍정적"
|
||||
},
|
||||
"1": {
|
||||
"name": "부정적"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2ConditioningMultiView": {
|
||||
"display_name": "Hunyuan3Dv2ConditioningMultiView",
|
||||
"inputs": {
|
||||
"back": {
|
||||
"name": "뒤"
|
||||
},
|
||||
"front": {
|
||||
"name": "앞"
|
||||
},
|
||||
"left": {
|
||||
"name": "왼쪽"
|
||||
},
|
||||
"right": {
|
||||
"name": "오른쪽"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "긍정적"
|
||||
},
|
||||
"1": {
|
||||
"name": "부정적"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HunyuanImageToVideo": {
|
||||
"display_name": "HunyuanImageToVideo",
|
||||
"inputs": {
|
||||
@@ -2589,9 +2536,6 @@
|
||||
"audio": {
|
||||
"name": "오디오"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "오디오UI"
|
||||
},
|
||||
"upload": {
|
||||
"name": "업로드할 파일 선택"
|
||||
}
|
||||
@@ -4670,9 +4614,6 @@
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "오디오"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "오디오UI"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5153,28 +5094,11 @@
|
||||
"audio": {
|
||||
"name": "오디오"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "오디오UI"
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "파일명 접두사"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveGLB": {
|
||||
"display_name": "SaveGLB",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "파일명 접두사"
|
||||
},
|
||||
"image": {
|
||||
"name": "이미지"
|
||||
},
|
||||
"mesh": {
|
||||
"name": "메시"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveImage": {
|
||||
"description": "입력 이미지를 ComfyUI 출력 디렉토리에 저장합니다.",
|
||||
"display_name": "이미지 저장",
|
||||
@@ -5781,23 +5705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"num_chunks": {
|
||||
"name": "num_chunks"
|
||||
},
|
||||
"octree_resolution": {
|
||||
"name": "옥트리 해상도"
|
||||
},
|
||||
"samples": {
|
||||
"name": "샘플"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
"display_name": "VAE 디코드 (타일)",
|
||||
"inputs": {
|
||||
@@ -5945,17 +5852,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
"name": "임계값"
|
||||
},
|
||||
"voxel": {
|
||||
"name": "복셀"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanImageToVideo": {
|
||||
"display_name": "Wan 이미지를 비디오로",
|
||||
"inputs": {
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
"LOAD_3D": "ЗАГРУЗИТЬ_3D",
|
||||
"LOAD_3D_ANIMATION": "ЗАГРУЗИТЬ_3D_АНИМАЦИЮ",
|
||||
"MASK": "МАСКА",
|
||||
"MESH": "СЕТКА",
|
||||
"MODEL": "МОДЕЛЬ",
|
||||
"NOISE": "ШУМ",
|
||||
"PHOTOMAKER": "PHOTOMAKER",
|
||||
@@ -85,7 +84,6 @@
|
||||
"TIMESTEPS_RANGE": "ДИАПАЗОН_ВРЕМЕННЫХ_ШАГОВ",
|
||||
"UPSCALE_MODEL": "МОДЕЛЬ_АПСКЕЙЛА",
|
||||
"VAE": "VAE",
|
||||
"VOXEL": "ВОКСЕЛ",
|
||||
"WEBCAM": "ВЕБ-КАМЕРА"
|
||||
},
|
||||
"desktopMenu": {
|
||||
@@ -190,6 +188,7 @@
|
||||
"reportSent": "Отчёт отправлен",
|
||||
"reset": "Сбросить",
|
||||
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
|
||||
"restart": "Перезапустить",
|
||||
"resultsCount": "Найдено {count} результатов",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение",
|
||||
@@ -268,7 +267,6 @@
|
||||
"installLocationDescription": "Выберите директорию для пользовательских данных ComfyUI. В выбранном месте будет установлена среда Python. Пожалуйста, убедитесь, что на выбранном диске достаточно места (~15 ГБ).",
|
||||
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские ноды\n",
|
||||
"insufficientFreeSpace": "Недостаточно места — минимально необходимое свободное место",
|
||||
"isOneDrive": "Установка в OneDrive может вызвать проблемы. Настоятельно рекомендуем устанавливать в месте, не связанном с OneDrive.",
|
||||
"manualConfiguration": {
|
||||
"createVenv": "Вам потребуется создать виртуальное окружение в следующем каталоге",
|
||||
"requirements": "Требования",
|
||||
@@ -283,7 +281,6 @@
|
||||
"migrationOptional": "Миграция является необязательной. Если у вас нет существующей установки, вы можете пропустить этот шаг.",
|
||||
"migrationSourcePathDescription": "Если у вас уже есть установленный ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели с новой установкой. Ваша существующая установка ComfyUI не будет затронута.",
|
||||
"moreInfo": "Для получения дополнительной информации, пожалуйста, прочтите нашу",
|
||||
"nonDefaultDrive": "Пожалуйста, установите ComfyUI на системный диск (например, C:\\). Диски с другими файловыми системами могут вызвать непредсказуемые проблемы. Модели и другие файлы можно хранить на других дисках после установки.",
|
||||
"parentMissing": "Путь не существует — сначала создайте родительский каталог",
|
||||
"pathExists": "Директория уже существует — пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
|
||||
"pathValidationFailed": "Не удалось проверить путь",
|
||||
@@ -401,6 +398,7 @@
|
||||
"nodePack": "Пакет Узлов",
|
||||
"packsSelected": "Выбрано пакетов",
|
||||
"repository": "Репозиторий",
|
||||
"restartToApplyChanges": "Чтобы применить изменения, пожалуйста, перезапустите ComfyUI",
|
||||
"searchPlaceholder": "Поиск",
|
||||
"selectVersion": "Выберите версию",
|
||||
"sort": {
|
||||
@@ -534,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
|
||||
"Toggle Progress Dialog": "Переключить диалоговое окно прогресса",
|
||||
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
|
||||
@@ -884,9 +883,6 @@
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D Многовидовой",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D Турбо",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"Area Composition": {
|
||||
|
||||
@@ -1251,18 +1251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentHunyuan3Dv2": {
|
||||
"display_name": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "размер_пакета",
|
||||
"tooltip": "Количество скрытых изображений в пакете."
|
||||
},
|
||||
"resolution": {
|
||||
"name": "разрешение"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
"description": "Создаёт новую партию пустых латентных изображений для удаления шума через выборку.",
|
||||
"display_name": "Пустое латентное изображение",
|
||||
@@ -1484,47 +1472,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2Conditioning": {
|
||||
"display_name": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {
|
||||
"clip_vision_output": {
|
||||
"name": "выход_clip_vision"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "положительный"
|
||||
},
|
||||
"1": {
|
||||
"name": "отрицательный"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2ConditioningMultiView": {
|
||||
"display_name": "Hunyuan3Dv2ConditioningMultiView",
|
||||
"inputs": {
|
||||
"back": {
|
||||
"name": "сзади"
|
||||
},
|
||||
"front": {
|
||||
"name": "фронт"
|
||||
},
|
||||
"left": {
|
||||
"name": "слева"
|
||||
},
|
||||
"right": {
|
||||
"name": "справа"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "положительный"
|
||||
},
|
||||
"1": {
|
||||
"name": "отрицательный"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HunyuanImageToVideo": {
|
||||
"display_name": "HunyuanImageToVideo",
|
||||
"inputs": {
|
||||
@@ -2589,9 +2536,6 @@
|
||||
"audio": {
|
||||
"name": "аудио"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"upload": {
|
||||
"name": "выберите файл для загрузки"
|
||||
}
|
||||
@@ -4670,9 +4614,6 @@
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "аудио"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5153,28 +5094,11 @@
|
||||
"audio": {
|
||||
"name": "аудио"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "audioUI"
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "префикс_названия_файла"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveGLB": {
|
||||
"display_name": "SaveGLB",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "префикс_имени_файла"
|
||||
},
|
||||
"image": {
|
||||
"name": "изображение"
|
||||
},
|
||||
"mesh": {
|
||||
"name": "сетка"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveImage": {
|
||||
"description": "Сохраняет входные изображения в вашу директорию вывода ComfyUI.",
|
||||
"display_name": "Сохранить изображение",
|
||||
@@ -5781,23 +5705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"num_chunks": {
|
||||
"name": "количество_частей"
|
||||
},
|
||||
"octree_resolution": {
|
||||
"name": "разрешение_октодерева"
|
||||
},
|
||||
"samples": {
|
||||
"name": "образцы"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
"display_name": "Декодировать VAE (плитками)",
|
||||
"inputs": {
|
||||
@@ -5945,17 +5852,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
"name": "порог"
|
||||
},
|
||||
"voxel": {
|
||||
"name": "воксель"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanImageToVideo": {
|
||||
"display_name": "WanИзображениеВВидео",
|
||||
"inputs": {
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
"LOAD_3D": "加载3D",
|
||||
"LOAD_3D_ANIMATION": "加载3D动画",
|
||||
"MASK": "遮罩",
|
||||
"MESH": "网格",
|
||||
"MODEL": "模型",
|
||||
"NOISE": "噪波",
|
||||
"PHOTOMAKER": "PhotoMaker",
|
||||
@@ -85,7 +84,6 @@
|
||||
"TIMESTEPS_RANGE": "时间间隔范围",
|
||||
"UPSCALE_MODEL": "放大模型",
|
||||
"VAE": "VAE",
|
||||
"VOXEL": "体素",
|
||||
"WEBCAM": "摄像头"
|
||||
},
|
||||
"desktopMenu": {
|
||||
@@ -190,6 +188,7 @@
|
||||
"reportSent": "报告已提交",
|
||||
"reset": "重置",
|
||||
"resetKeybindingsTooltip": "将快捷键重置为默认",
|
||||
"restart": "重新启动",
|
||||
"resultsCount": "找到 {count} 个结果",
|
||||
"save": "保存",
|
||||
"saving": "正在保存",
|
||||
@@ -268,7 +267,6 @@
|
||||
"installLocationDescription": "选择 ComfyUI 用户数据的存放目录。将安装一个 Python 环境到所选位置。请确保所选磁盘有足够的空间(约 15GB)。",
|
||||
"installLocationTooltip": "ComfyUI 的用户数据目录。存储:\n- Python 环境\n- 模型\n- 自定义节点\n",
|
||||
"insufficientFreeSpace": "空间不足 - 最小可用空间",
|
||||
"isOneDrive": "在OneDrive中安装可能会导致问题。强烈建议在非OneDrive位置安装。",
|
||||
"manualConfiguration": {
|
||||
"createVenv": "您需要在以下目录中创建虚拟环境",
|
||||
"requirements": "依赖项",
|
||||
@@ -283,7 +281,6 @@
|
||||
"migrationOptional": "迁移是可选的。如果您之前没有安装过 ComfyUI,可以跳过此步骤。",
|
||||
"migrationSourcePathDescription": "如果您已有现有的ComfyUI安装,我们可以复制/链接您现有的用户文件和模型到新的安装。您现有的ComfyUI安装将不会受到影响。",
|
||||
"moreInfo": "有关更多信息,请阅读我们的",
|
||||
"nonDefaultDrive": "请在您的系统驱动器上安装ComfyUI(例如C:\\)。具有不同文件系统的驱动器可能会导致不可预测的问题。安装后,模型和其他文件可以存储在其他驱动器上。",
|
||||
"parentMissing": "路径不存在 - 请先创建包含该路径的目录",
|
||||
"pathExists": "目录已存在 - 请确保您已备份全部数据",
|
||||
"pathValidationFailed": "路径验证失败",
|
||||
@@ -401,6 +398,7 @@
|
||||
"nodePack": "节点包",
|
||||
"packsSelected": "选定的包",
|
||||
"repository": "仓库",
|
||||
"restartToApplyChanges": "要应用更改,请重新启动ComfyUI",
|
||||
"searchPlaceholder": "搜索",
|
||||
"selectVersion": "选择版本",
|
||||
"sort": {
|
||||
@@ -534,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切换模型库侧边栏",
|
||||
"Toggle Node Library Sidebar": "切换节点库侧边栏",
|
||||
"Toggle Progress Dialog": "切换进度对话框",
|
||||
"Toggle Queue Sidebar": "切换队列侧边栏",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
@@ -884,9 +883,6 @@
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D多视图",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D Turbo",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"stable_zero123_example": "稳定Zero123"
|
||||
},
|
||||
"Area Composition": {
|
||||
|
||||
@@ -1251,18 +1251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentHunyuan3Dv2": {
|
||||
"display_name": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "批量大小",
|
||||
"tooltip": "批量中的潜在图像数量。"
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
"description": "创建一批新的空Latent图像,以通过采样进行降噪。",
|
||||
"display_name": "空Latent图像",
|
||||
@@ -1484,47 +1472,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2Conditioning": {
|
||||
"display_name": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {
|
||||
"clip_vision_output": {
|
||||
"name": "clip视觉输出"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "正向"
|
||||
},
|
||||
"1": {
|
||||
"name": "反向"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hunyuan3Dv2ConditioningMultiView": {
|
||||
"display_name": "Hunyuan3Dv2ConditioningMultiView",
|
||||
"inputs": {
|
||||
"back": {
|
||||
"name": "后"
|
||||
},
|
||||
"front": {
|
||||
"name": "前"
|
||||
},
|
||||
"left": {
|
||||
"name": "左"
|
||||
},
|
||||
"right": {
|
||||
"name": "右"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "正向"
|
||||
},
|
||||
"1": {
|
||||
"name": "反向"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HunyuanImageToVideo": {
|
||||
"display_name": "Hunyuan图像到视频",
|
||||
"inputs": {
|
||||
@@ -2589,9 +2536,6 @@
|
||||
"audio": {
|
||||
"name": "音频"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "音频UI"
|
||||
},
|
||||
"upload": {
|
||||
"name": "选择文件上传"
|
||||
}
|
||||
@@ -4670,9 +4614,6 @@
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "音频"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "音频UI"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5153,28 +5094,11 @@
|
||||
"audio": {
|
||||
"name": "音频"
|
||||
},
|
||||
"audioUI": {
|
||||
"name": "音频UI"
|
||||
},
|
||||
"filename_prefix": {
|
||||
"name": "文件名前缀"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveGLB": {
|
||||
"display_name": "SaveGLB",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "文件名前缀"
|
||||
},
|
||||
"image": {
|
||||
"name": "图像"
|
||||
},
|
||||
"mesh": {
|
||||
"name": "网格"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveImage": {
|
||||
"description": "将输入图像保存到您的ComfyUI输出目录。",
|
||||
"display_name": "保存图像",
|
||||
@@ -5781,23 +5705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"num_chunks": {
|
||||
"name": "块数"
|
||||
},
|
||||
"octree_resolution": {
|
||||
"name": "八叉树分辨率"
|
||||
},
|
||||
"samples": {
|
||||
"name": "样本"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
"display_name": "VAE解码(分块)",
|
||||
"inputs": {
|
||||
@@ -5945,17 +5852,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
"name": "阈值"
|
||||
},
|
||||
"voxel": {
|
||||
"name": "体素"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanImageToVideo": {
|
||||
"display_name": "Wan图像到视频",
|
||||
"inputs": {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { type ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { getFromWebmFile } from '@/scripts/metadata/ebml'
|
||||
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
@@ -1392,18 +1391,6 @@ export class ComfyApp {
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'model/gltf-binary' ||
|
||||
file.name?.endsWith('.glb')
|
||||
) {
|
||||
const gltfInfo = await getGltfBinaryMetadata(file)
|
||||
if (gltfInfo.workflow) {
|
||||
this.loadGraphData(gltfInfo.workflow, true, true, fileName)
|
||||
} else if (gltfInfo.prompt) {
|
||||
this.loadApiJson(gltfInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'application/json' ||
|
||||
file.name?.endsWith('.json')
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface DOMWidgetOptions<V extends object | string>
|
||||
getMaxHeight?: () => number
|
||||
getHeight?: () => string | number
|
||||
onDraw?: (widget: BaseDOMWidget<V>) => void
|
||||
margin?: number
|
||||
/**
|
||||
* @deprecated Use `afterResize` instead. This callback is a legacy API
|
||||
* that fires before resize happens, but it is no longer supported. Now it
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import {
|
||||
ComfyApiWorkflow,
|
||||
ComfyWorkflowJSON
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import {
|
||||
ASCII,
|
||||
ComfyMetadata,
|
||||
ComfyMetadataTags,
|
||||
GltfChunkHeader,
|
||||
GltfHeader,
|
||||
GltfJsonData,
|
||||
GltfSizeBytes
|
||||
} from '@/types/metadataTypes'
|
||||
|
||||
const MAX_READ_BYTES = 1 << 20
|
||||
|
||||
const isJsonChunk = (chunk: GltfChunkHeader | null): boolean =>
|
||||
!!chunk && chunk.chunkTypeIdentifier === ASCII.JSON
|
||||
|
||||
const isValidChunkRange = (
|
||||
start: number,
|
||||
length: number,
|
||||
bufferSize: number
|
||||
): boolean => start + length <= bufferSize
|
||||
|
||||
const byteArrayToString = (bytes: Uint8Array): string =>
|
||||
new TextDecoder().decode(bytes)
|
||||
|
||||
const parseGltfBinaryHeader = (dataView: DataView): GltfHeader | null => {
|
||||
if (dataView.byteLength < GltfSizeBytes.HEADER) return null
|
||||
|
||||
const magicNumber = dataView.getUint32(0, true)
|
||||
if (magicNumber !== ASCII.GLTF) return null
|
||||
|
||||
return {
|
||||
magicNumber,
|
||||
gltfFormatVersion: dataView.getUint32(4, true),
|
||||
totalLengthBytes: dataView.getUint32(8, true)
|
||||
}
|
||||
}
|
||||
|
||||
const parseChunkHeaderAtOffset = (
|
||||
dataView: DataView,
|
||||
offset: number
|
||||
): GltfChunkHeader | null => {
|
||||
if (offset + GltfSizeBytes.CHUNK_HEADER > dataView.byteLength) return null
|
||||
|
||||
return {
|
||||
chunkLengthBytes: dataView.getUint32(offset, true),
|
||||
chunkTypeIdentifier: dataView.getUint32(offset + 4, true)
|
||||
}
|
||||
}
|
||||
|
||||
const extractJsonChunk = (
|
||||
buffer: ArrayBuffer
|
||||
): { start: number; length: number } | null => {
|
||||
const dataView = new DataView(buffer)
|
||||
|
||||
const header = parseGltfBinaryHeader(dataView)
|
||||
if (!header) return null
|
||||
|
||||
const chunkOffset = GltfSizeBytes.HEADER
|
||||
const firstChunk = parseChunkHeaderAtOffset(dataView, chunkOffset)
|
||||
if (!firstChunk || !isJsonChunk(firstChunk)) return null
|
||||
|
||||
const jsonStart = chunkOffset + GltfSizeBytes.CHUNK_HEADER
|
||||
const isValid = isValidChunkRange(
|
||||
jsonStart,
|
||||
firstChunk.chunkLengthBytes,
|
||||
dataView.byteLength
|
||||
)
|
||||
if (!isValid) return null
|
||||
|
||||
return { start: jsonStart, length: firstChunk.chunkLengthBytes }
|
||||
}
|
||||
|
||||
const extractJsonChunkData = (buffer: ArrayBuffer): Uint8Array | null => {
|
||||
const chunkLocation = extractJsonChunk(buffer)
|
||||
if (!chunkLocation) return null
|
||||
|
||||
return new Uint8Array(buffer, chunkLocation.start, chunkLocation.length)
|
||||
}
|
||||
|
||||
const parseJson = (text: string): ReturnType<typeof JSON.parse> | null => {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const parseJsonBytes = (
|
||||
bytes: Uint8Array
|
||||
): ReturnType<typeof JSON.parse> | null => {
|
||||
const jsonString = byteArrayToString(bytes)
|
||||
return parseJson(jsonString)
|
||||
}
|
||||
|
||||
const parseMetadataValue = (
|
||||
value: string | object
|
||||
): ComfyWorkflowJSON | ComfyApiWorkflow | undefined => {
|
||||
if (typeof value !== 'string')
|
||||
return value as ComfyWorkflowJSON | ComfyApiWorkflow
|
||||
|
||||
const parsed = parseJson(value)
|
||||
if (!parsed) return undefined
|
||||
|
||||
return parsed as ComfyWorkflowJSON | ComfyApiWorkflow
|
||||
}
|
||||
|
||||
const extractComfyMetadata = (jsonData: GltfJsonData): ComfyMetadata => {
|
||||
const metadata: ComfyMetadata = {}
|
||||
|
||||
if (!jsonData?.asset?.extras) return metadata
|
||||
|
||||
const { extras } = jsonData.asset
|
||||
|
||||
if (extras.workflow) {
|
||||
const parsedValue = parseMetadataValue(extras.workflow)
|
||||
if (parsedValue) {
|
||||
metadata[ComfyMetadataTags.WORKFLOW.toLowerCase()] = parsedValue
|
||||
}
|
||||
}
|
||||
|
||||
if (extras.prompt) {
|
||||
const parsedValue = parseMetadataValue(extras.prompt)
|
||||
if (parsedValue) {
|
||||
metadata[ComfyMetadataTags.PROMPT.toLowerCase()] = parsedValue
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
const processGltfFileBuffer = (buffer: ArrayBuffer): ComfyMetadata => {
|
||||
const jsonChunk = extractJsonChunkData(buffer)
|
||||
if (!jsonChunk) return {}
|
||||
|
||||
const parsedJson = parseJsonBytes(jsonChunk)
|
||||
if (!parsedJson) return {}
|
||||
|
||||
return extractComfyMetadata(parsedJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ComfyUI metadata from a GLTF binary file (GLB)
|
||||
*/
|
||||
export function getGltfBinaryMetadata(file: File): Promise<ComfyMetadata> {
|
||||
return new Promise<ComfyMetadata>((resolve) => {
|
||||
if (!file) return Promise.resolve({})
|
||||
|
||||
const bytesToRead = Math.min(file.size, MAX_READ_BYTES)
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
if (!event.target?.result) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
|
||||
resolve(processGltfFileBuffer(event.target.result as ArrayBuffer))
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
}
|
||||
reader.onerror = () => resolve({})
|
||||
reader.readAsArrayBuffer(file.slice(0, bytesToRead))
|
||||
})
|
||||
}
|
||||
@@ -131,7 +131,9 @@ export const useAlgoliaSearchService = () => {
|
||||
status: algoliaNode.status,
|
||||
icon: algoliaNode.icon_url,
|
||||
latest_version: toRegistryLatestVersion(algoliaNode),
|
||||
publisher: toRegistryPublisher(algoliaNode)
|
||||
publisher: toRegistryPublisher(algoliaNode),
|
||||
// @ts-expect-error remove when comfy_nodes is added to node (pack) info
|
||||
comfy_nodes: algoliaNode.comfy_nodes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationD
|
||||
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import ManagerProgressHeader from '@/components/dialog/header/ManagerProgressHeader.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
|
||||
@@ -121,6 +124,29 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showManagerProgressDialog(options?: {
|
||||
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
|
||||
}) {
|
||||
return dialogStore.showDialog({
|
||||
key: 'global-manager-progress-dialog',
|
||||
component: ManagerProgressDialogContent,
|
||||
headerComponent: ManagerProgressHeader,
|
||||
footerComponent: ManagerProgressFooter,
|
||||
props: options?.props,
|
||||
dialogComponentProps: {
|
||||
closable: false,
|
||||
modal: false,
|
||||
position: 'bottom',
|
||||
pt: {
|
||||
root: { class: 'w-[80%] max-w-2xl mx-auto border-none' },
|
||||
content: { class: '!p-0' },
|
||||
header: { class: '!p-0 border-none' },
|
||||
footer: { class: '!p-0 border-none' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function prompt({
|
||||
title,
|
||||
message,
|
||||
@@ -203,6 +229,7 @@ export const useDialogService = () => {
|
||||
showTemplateWorkflowsDialog,
|
||||
showIssueReportDialog,
|
||||
showManagerDialog,
|
||||
showManagerProgressDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { useServerLogs } from '@/composables/useServerLogs'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
*/
|
||||
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const managerService = useComfyManagerService()
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
const installedPacks = ref<InstalledPacksResponse>({})
|
||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||
const disabledPacksIds = ref<Set<string>>(new Set())
|
||||
@@ -110,6 +113,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
@@ -178,6 +182,11 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
enqueueTask(withLogs(task, `Disabling ${params.id}`))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
const pack = installedPacks.value[packId]
|
||||
return pack?.ver
|
||||
}
|
||||
|
||||
const clearLogs = () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
@@ -197,6 +206,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installedPacksIds,
|
||||
isPackInstalled: isInstalledPackId,
|
||||
isPackEnabled: isEnabledPackId,
|
||||
getInstalledPackVersion,
|
||||
|
||||
// Pack actions
|
||||
installPack,
|
||||
@@ -207,3 +217,33 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
enablePack: installPack // Enable is done via install endpoint with a disabled pack
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Store for state of the manager progress dialog content.
|
||||
* The dialog itself is managed by the dialog store. This store is used to
|
||||
* manage the visibility of the dialog's content, header, footer.
|
||||
*/
|
||||
export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const collapse = () => {
|
||||
isExpanded.value = false
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
isExpanded.value = true
|
||||
}
|
||||
return {
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,11 +7,24 @@ import { type Component, markRaw, ref } from 'vue'
|
||||
|
||||
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
|
||||
type DialogPosition =
|
||||
| 'center'
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'topleft'
|
||||
| 'topright'
|
||||
| 'bottomleft'
|
||||
| 'bottomright'
|
||||
|
||||
interface CustomDialogComponentProps {
|
||||
maximizable?: boolean
|
||||
maximized?: boolean
|
||||
onClose?: () => void
|
||||
closable?: boolean
|
||||
modal?: boolean
|
||||
position?: DialogPosition
|
||||
pt?: DialogPassThroughOptions
|
||||
}
|
||||
|
||||
@@ -25,6 +38,7 @@ interface DialogInstance {
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
footerComponent?: Component
|
||||
dialogComponentProps: DialogComponentProps
|
||||
}
|
||||
|
||||
@@ -32,6 +46,7 @@ export interface ShowDialogOptions {
|
||||
key?: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
@@ -66,6 +81,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
key: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
@@ -81,6 +97,9 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
headerComponent: options.headerComponent
|
||||
? markRaw(options.headerComponent)
|
||||
: undefined,
|
||||
footerComponent: options.footerComponent
|
||||
? markRaw(options.footerComponent)
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
contentProps: { ...options.props },
|
||||
dialogComponentProps: {
|
||||
|
||||
@@ -73,14 +73,12 @@ export const useNodeBookmarkStore = defineStore('nodeBookmark', () => {
|
||||
)
|
||||
}
|
||||
|
||||
const addNewBookmarkFolder = (parent?: ComfyNodeDefImpl) => {
|
||||
const addNewBookmarkFolder = (
|
||||
parent: ComfyNodeDefImpl | undefined,
|
||||
folderName: string
|
||||
) => {
|
||||
const parentPath = parent ? parent.nodePath : ''
|
||||
let newFolderPath = parentPath + 'New Folder/'
|
||||
let suffix = 1
|
||||
while (bookmarks.value.some((b: string) => b.startsWith(newFolderPath))) {
|
||||
newFolderPath = parentPath + `New Folder ${suffix}/`
|
||||
suffix++
|
||||
}
|
||||
const newFolderPath = parentPath + folderName + '/'
|
||||
addBookmark(newFolderPath)
|
||||
return newFolderPath
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { UserFile } from './userFileStore'
|
||||
|
||||
export class ComfyWorkflow extends UserFile {
|
||||
static readonly basePath = 'workflows/'
|
||||
static readonly folderPlaceholderFilename = 'folder.index'
|
||||
|
||||
/**
|
||||
* The change tracker for the workflow. Non-reactive raw object.
|
||||
@@ -32,7 +33,9 @@ export class ComfyWorkflow extends UserFile {
|
||||
}
|
||||
|
||||
get key() {
|
||||
return this.path.substring(ComfyWorkflow.basePath.length)
|
||||
const key = this.isFolderPlaceholder ? this.directory + '/' : this.path
|
||||
|
||||
return key.substring(ComfyWorkflow.basePath.length)
|
||||
}
|
||||
|
||||
get activeState(): ComfyWorkflowJSON | null {
|
||||
@@ -55,6 +58,13 @@ export class ComfyWorkflow extends UserFile {
|
||||
this._isModified = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the workflow is a folder placeholder.
|
||||
*/
|
||||
get isFolderPlaceholder(): boolean {
|
||||
return this.fullFilename === ComfyWorkflow.folderPlaceholderFilename
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the workflow content from remote storage. Directly returns the loaded
|
||||
* workflow if the content is already loaded.
|
||||
@@ -153,6 +163,8 @@ export interface WorkflowStore {
|
||||
getWorkflowByPath: (path: string) => ComfyWorkflow | null
|
||||
syncWorkflows: (dir?: string) => Promise<void>
|
||||
reorderWorkflows: (from: number, to: number) => void
|
||||
|
||||
createFolder: (folderPath: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
@@ -418,6 +430,40 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new folder in the workflows directory
|
||||
* @param folderPath The path of the folder to create (relative to workflows/)
|
||||
* @returns Promise that resolves when the folder is created
|
||||
*/
|
||||
const createFolder = async (folderPath: string): Promise<void> => {
|
||||
isBusy.value = true
|
||||
try {
|
||||
// Ensure the path is properly formatted
|
||||
const normalizedPath = folderPath.endsWith('/')
|
||||
? folderPath.slice(0, -1)
|
||||
: folderPath
|
||||
|
||||
// Create the full path including the reserved index file
|
||||
const indexFilePath = `${ComfyWorkflow.basePath}${normalizedPath}/${ComfyWorkflow.folderPlaceholderFilename}`
|
||||
|
||||
// Create an empty file to represent the folder
|
||||
const resp = await api.storeUserData(indexFilePath, '', {
|
||||
overwrite: false,
|
||||
throwOnError: true,
|
||||
full_info: true
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error('Failed to create folder')
|
||||
}
|
||||
|
||||
// Sync workflows to update the file tree
|
||||
await syncWorkflows()
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
isActive,
|
||||
@@ -439,7 +485,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
persistedWorkflows,
|
||||
modifiedWorkflows,
|
||||
getWorkflowByPath,
|
||||
syncWorkflows
|
||||
syncWorkflows,
|
||||
|
||||
createFolder
|
||||
}
|
||||
}) as unknown as () => WorkflowStore
|
||||
|
||||
|
||||
@@ -43,38 +43,3 @@ export type TextRange = {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export enum ASCII {
|
||||
GLTF = 0x46546c67,
|
||||
JSON = 0x4e4f534a
|
||||
}
|
||||
|
||||
export enum GltfSizeBytes {
|
||||
HEADER = 12,
|
||||
CHUNK_HEADER = 8
|
||||
}
|
||||
|
||||
export type GltfHeader = {
|
||||
magicNumber: number
|
||||
gltfFormatVersion: number
|
||||
totalLengthBytes: number
|
||||
}
|
||||
|
||||
export type GltfChunkHeader = {
|
||||
chunkLengthBytes: number
|
||||
chunkTypeIdentifier: number
|
||||
}
|
||||
|
||||
export type GltfExtras = {
|
||||
workflow?: string | object
|
||||
prompt?: string | object
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type GltfJsonData = {
|
||||
asset?: {
|
||||
extras?: GltfExtras
|
||||
[key: string]: any
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
export interface TreeExplorerNode<T = any> {
|
||||
key: string
|
||||
@@ -18,10 +19,10 @@ export interface TreeExplorerNode<T = any> {
|
||||
) => void | Promise<void>
|
||||
/** Function to handle deleting the node */
|
||||
handleDelete?: (this: TreeExplorerNode<T>) => void | Promise<void>
|
||||
/** Function to handle adding a child node */
|
||||
handleAddChild?: (
|
||||
/** Function to handle adding a folder */
|
||||
handleAddFolder?: (
|
||||
this: TreeExplorerNode<T>,
|
||||
child: TreeExplorerNode<T>
|
||||
folderName: string
|
||||
) => void | Promise<void>
|
||||
/** Whether the node is draggable */
|
||||
draggable?: boolean
|
||||
@@ -58,9 +59,18 @@ export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
|
||||
totalLeaves: number
|
||||
/** Text to display on the leaf-count badge. Empty string means no badge. */
|
||||
badgeText?: string
|
||||
/** Whether the node label is currently being edited */
|
||||
isEditingLabel?: boolean
|
||||
}
|
||||
|
||||
export type TreeExplorerDragAndDropData<T = any> = {
|
||||
type: 'tree-explorer-node'
|
||||
data: RenderedTreeExplorerNode<T>
|
||||
}
|
||||
|
||||
export const InjectKeyHandleEditLabelFunction: InjectionKey<
|
||||
(node: RenderedTreeExplorerNode, newName: string) => void
|
||||
> = Symbol()
|
||||
|
||||
export const InjectKeyExpandedKeys: InjectionKey<Ref<Record<string, boolean>>> =
|
||||
Symbol()
|
||||
|
||||
@@ -384,3 +384,31 @@ export const downloadUrlToHfRepoUrl = (url: string): string => {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export const isSemVer = (version: string) => {
|
||||
const regex = /^(\d+)\.(\d+)\.(\d+)$/
|
||||
return regex.test(version)
|
||||
}
|
||||
|
||||
const normalizeVersion = (version: string) =>
|
||||
version
|
||||
.split(/[+.-]/)
|
||||
.map(Number)
|
||||
.filter((part) => !Number.isNaN(part))
|
||||
|
||||
export function compareVersions(versionA: string, versionB: string): number {
|
||||
versionA ??= '0.0.0'
|
||||
versionB ??= '0.0.0'
|
||||
|
||||
const aParts = normalizeVersion(versionA)
|
||||
const bParts = normalizeVersion(versionB)
|
||||
|
||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||
const aPart = aParts[i] ?? 0
|
||||
const bPart = bParts[i] ?? 0
|
||||
if (aPart < bPart) return -1
|
||||
if (aPart > bPart) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -105,7 +105,10 @@ export function sortedTree(
|
||||
return newNode
|
||||
}
|
||||
|
||||
export const findNodeByKey = (root: TreeNode, key: string): TreeNode | null => {
|
||||
export const findNodeByKey = <T extends TreeNode>(
|
||||
root: T,
|
||||
key: string
|
||||
): T | null => {
|
||||
if (root.key === key) {
|
||||
return root
|
||||
}
|
||||
@@ -113,10 +116,46 @@ export const findNodeByKey = (root: TreeNode, key: string): TreeNode | null => {
|
||||
return null
|
||||
}
|
||||
for (const child of root.children) {
|
||||
const result = findNodeByKey(child, key)
|
||||
const result = findNodeByKey(child as T, key)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone a tree node and its children.
|
||||
* @param node - The node to clone.
|
||||
* @returns A deep clone of the node.
|
||||
*/
|
||||
export function cloneTree<T extends TreeNode>(node: T): T {
|
||||
const clone: T = { ...node } as T
|
||||
|
||||
// Clone children recursively
|
||||
if (node.children && node.children.length > 0) {
|
||||
clone.children = node.children.map((child) => cloneTree(child as T))
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a subtree into the tree.
|
||||
* @param root - The root of the tree.
|
||||
* @param subtree - The subtree to merge.
|
||||
* @returns A new tree with the subtree merged.
|
||||
*/
|
||||
export const combineTrees = <T extends TreeNode>(root: T, subtree: T): T => {
|
||||
const newRoot = cloneTree(root)
|
||||
|
||||
const parentKey = subtree.key.slice(0, subtree.key.lastIndexOf('/'))
|
||||
const parent = findNodeByKey(newRoot, parentKey)
|
||||
|
||||
if (parent) {
|
||||
parent.children ??= []
|
||||
parent.children.push(cloneTree(subtree))
|
||||
}
|
||||
|
||||
return newRoot
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { ASCII, GltfSizeBytes } from '@/types/metadataTypes'
|
||||
|
||||
import { getGltfBinaryMetadata } from '../../../../src/scripts/metadata/gltf'
|
||||
|
||||
describe('GLTF binary metadata parser', () => {
|
||||
const createGLTFFileStructure = () => {
|
||||
const header = new ArrayBuffer(GltfSizeBytes.HEADER)
|
||||
const headerView = new DataView(header)
|
||||
return { header, headerView }
|
||||
}
|
||||
|
||||
const jsonToBinary = (json: object) => {
|
||||
const jsonString = JSON.stringify(json)
|
||||
const jsonData = new TextEncoder().encode(jsonString)
|
||||
return jsonData
|
||||
}
|
||||
|
||||
const createJSONChunk = (jsonData: ArrayBuffer) => {
|
||||
const chunkHeader = new ArrayBuffer(GltfSizeBytes.CHUNK_HEADER)
|
||||
const chunkView = new DataView(chunkHeader)
|
||||
chunkView.setUint32(0, jsonData.byteLength, true)
|
||||
chunkView.setUint32(4, ASCII.JSON, true)
|
||||
return chunkHeader
|
||||
}
|
||||
|
||||
const setVersionHeader = (headerView: DataView, version: number) => {
|
||||
headerView.setUint32(4, version, true)
|
||||
}
|
||||
|
||||
const setTypeHeader = (headerView: DataView, type: number) => {
|
||||
headerView.setUint32(0, type, true)
|
||||
}
|
||||
|
||||
const setTotalLengthHeader = (headerView: DataView, length: number) => {
|
||||
headerView.setUint32(8, length, true)
|
||||
}
|
||||
|
||||
const setHeaders = (headerView: DataView, jsonData: ArrayBuffer) => {
|
||||
setTypeHeader(headerView, ASCII.GLTF)
|
||||
setVersionHeader(headerView, 2)
|
||||
setTotalLengthHeader(
|
||||
headerView,
|
||||
GltfSizeBytes.HEADER + GltfSizeBytes.CHUNK_HEADER + jsonData.byteLength
|
||||
)
|
||||
}
|
||||
|
||||
function createMockGltfFile(jsonContent: object): File {
|
||||
const jsonData = jsonToBinary(jsonContent)
|
||||
const { header, headerView } = createGLTFFileStructure()
|
||||
|
||||
setHeaders(headerView, jsonData)
|
||||
|
||||
const chunkHeader = createJSONChunk(jsonData)
|
||||
|
||||
const fileContent = new Uint8Array(
|
||||
header.byteLength + chunkHeader.byteLength + jsonData.byteLength
|
||||
)
|
||||
fileContent.set(new Uint8Array(header), 0)
|
||||
fileContent.set(new Uint8Array(chunkHeader), header.byteLength)
|
||||
fileContent.set(jsonData, header.byteLength + chunkHeader.byteLength)
|
||||
|
||||
return new File([fileContent], 'test.glb', { type: 'model/gltf-binary' })
|
||||
}
|
||||
|
||||
it('should extract workflow metadata from GLTF binary file', async () => {
|
||||
const testWorkflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'TestNode',
|
||||
pos: [100, 100]
|
||||
}
|
||||
],
|
||||
links: []
|
||||
}
|
||||
|
||||
const mockFile = createMockGltfFile({
|
||||
asset: {
|
||||
version: '2.0',
|
||||
generator: 'ComfyUI GLTF Test',
|
||||
extras: {
|
||||
workflow: testWorkflow
|
||||
}
|
||||
},
|
||||
scenes: []
|
||||
})
|
||||
|
||||
const metadata = await getGltfBinaryMetadata(mockFile)
|
||||
|
||||
expect(metadata).toBeDefined()
|
||||
expect(metadata.workflow).toBeDefined()
|
||||
|
||||
const workflow = metadata.workflow as {
|
||||
nodes: Array<{ id: number; type: string }>
|
||||
}
|
||||
expect(workflow.nodes[0].id).toBe(1)
|
||||
expect(workflow.nodes[0].type).toBe('TestNode')
|
||||
})
|
||||
|
||||
it('should extract prompt metadata from GLTF binary file', async () => {
|
||||
const testPrompt = {
|
||||
node1: {
|
||||
class_type: 'TestNode',
|
||||
inputs: {
|
||||
seed: 123456
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockFile = createMockGltfFile({
|
||||
asset: {
|
||||
version: '2.0',
|
||||
generator: 'ComfyUI GLTF Test',
|
||||
extras: {
|
||||
prompt: testPrompt
|
||||
}
|
||||
},
|
||||
scenes: []
|
||||
})
|
||||
|
||||
const metadata = await getGltfBinaryMetadata(mockFile)
|
||||
expect(metadata).toBeDefined()
|
||||
expect(metadata.prompt).toBeDefined()
|
||||
|
||||
const prompt = metadata.prompt as Record<string, any>
|
||||
expect(prompt.node1.class_type).toBe('TestNode')
|
||||
expect(prompt.node1.inputs.seed).toBe(123456)
|
||||
})
|
||||
|
||||
it('should handle string JSON content', async () => {
|
||||
const workflowStr = JSON.stringify({
|
||||
nodes: [{ id: 1, type: 'StringifiedNode' }],
|
||||
links: []
|
||||
})
|
||||
|
||||
const mockFile = createMockGltfFile({
|
||||
asset: {
|
||||
version: '2.0',
|
||||
extras: {
|
||||
workflow: workflowStr // As string instead of object
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const metadata = await getGltfBinaryMetadata(mockFile)
|
||||
|
||||
expect(metadata).toBeDefined()
|
||||
expect(metadata.workflow).toBeDefined()
|
||||
|
||||
const workflow = metadata.workflow as {
|
||||
nodes: Array<{ id: number; type: string }>
|
||||
}
|
||||
expect(workflow.nodes[0].type).toBe('StringifiedNode')
|
||||
})
|
||||
|
||||
it('should handle invalid GLTF binary files gracefully', async () => {
|
||||
const invalidEmptyFile = new File([], 'invalid.glb')
|
||||
const metadata = await getGltfBinaryMetadata(invalidEmptyFile)
|
||||
expect(metadata).toEqual({})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user