mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
Merge remote-tracking branch 'origin/main' into feature/show_dropped_image_in_output_node
This commit is contained in:
@@ -15,8 +15,15 @@ import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
}
|
||||
useEventListener(window, 'keydown', handleKey)
|
||||
useEventListener(window, 'keyup', handleKey)
|
||||
|
||||
onMounted(() => {
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
|
||||
@@ -83,6 +83,7 @@ const setInitialPosition = () => {
|
||||
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
||||
x.value = storedPosition.value.x
|
||||
y.value = storedPosition.value.y
|
||||
captureLastDragState()
|
||||
return
|
||||
}
|
||||
if (panelRef.value) {
|
||||
@@ -97,6 +98,7 @@ const setInitialPosition = () => {
|
||||
|
||||
x.value = (screenWidth - menuWidth) / 2
|
||||
y.value = screenHeight - menuHeight - 10 // 10px margin from bottom
|
||||
captureLastDragState()
|
||||
}
|
||||
}
|
||||
onMounted(setInitialPosition)
|
||||
@@ -106,6 +108,31 @@ watch(visible, (newVisible) => {
|
||||
}
|
||||
})
|
||||
|
||||
const lastDragState = ref({
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight
|
||||
})
|
||||
const captureLastDragState = () => {
|
||||
lastDragState.value = {
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight
|
||||
}
|
||||
}
|
||||
watch(
|
||||
isDragging,
|
||||
(newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
captureLastDragState()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const adjustMenuPosition = () => {
|
||||
if (panelRef.value) {
|
||||
const screenWidth = window.innerWidth
|
||||
@@ -113,10 +140,34 @@ const adjustMenuPosition = () => {
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
|
||||
// Adjust x position if menu is off-screen horizontally
|
||||
x.value = clamp(x.value, 0, screenWidth - menuWidth)
|
||||
// Calculate the distance from each edge
|
||||
const distanceRight =
|
||||
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
|
||||
const distanceBottom =
|
||||
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
|
||||
|
||||
// Adjust y position if menu is off-screen vertically
|
||||
// Determine if the menu is closer to right/bottom or left/top
|
||||
const anchorRight = distanceRight < lastDragState.value.x
|
||||
const anchorBottom = distanceBottom < lastDragState.value.y
|
||||
|
||||
// Calculate new position
|
||||
if (anchorRight) {
|
||||
x.value =
|
||||
screenWidth - (lastDragState.value.windowWidth - lastDragState.value.x)
|
||||
} else {
|
||||
x.value = lastDragState.value.x
|
||||
}
|
||||
|
||||
if (anchorBottom) {
|
||||
y.value =
|
||||
screenHeight -
|
||||
(lastDragState.value.windowHeight - lastDragState.value.y)
|
||||
} else {
|
||||
y.value = lastDragState.value.y
|
||||
}
|
||||
|
||||
// Ensure the menu stays within the screen bounds
|
||||
x.value = clamp(x.value, 0, screenWidth - menuWidth)
|
||||
y.value = clamp(y.value, 0, screenHeight - menuHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,20 @@
|
||||
<SplitButton
|
||||
class="comfyui-queue-button"
|
||||
:label="activeQueueModeMenuItem.label"
|
||||
:icon="activeQueueModeMenuItem.icon"
|
||||
severity="primary"
|
||||
@click="queuePrompt"
|
||||
:model="queueModeMenuItems"
|
||||
data-testid="queue-button"
|
||||
v-tooltip.bottom="$t('menu.queueWorkflow')"
|
||||
v-tooltip.bottom="
|
||||
workspaceStore.shiftDown
|
||||
? $t('menu.queueWorkflowFront')
|
||||
: $t('menu.queueWorkflow')
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
|
||||
<i v-else :class="activeQueueModeMenuItem.icon" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
:label="item.label"
|
||||
@@ -58,7 +65,9 @@ import type { MenuItem } from 'primevue/menuitem'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<slot name="after-label" :node="props.node"></slot>
|
||||
</span>
|
||||
<Badge
|
||||
v-if="!props.node.leaf"
|
||||
:value="props.node.badgeText ?? props.node.totalLeaves"
|
||||
v-if="showNodeBadgeText"
|
||||
:value="nodeBadgeText"
|
||||
severity="secondary"
|
||||
class="leaf-count-badge"
|
||||
/>
|
||||
@@ -34,12 +34,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, inject, Ref, computed } from 'vue'
|
||||
import { ref, inject, Ref, computed } from 'vue'
|
||||
import Badge from 'primevue/badge'
|
||||
import {
|
||||
dropTargetForElements,
|
||||
draggable
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import type {
|
||||
TreeExplorerDragAndDropData,
|
||||
RenderedTreeExplorerNode,
|
||||
@@ -47,6 +43,7 @@ import type {
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { usePragmaticDraggable, usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
|
||||
const props = defineProps<{
|
||||
node: RenderedTreeExplorerNode
|
||||
@@ -62,6 +59,17 @@ const emit = defineEmits<{
|
||||
(e: 'dragEnd', node: RenderedTreeExplorerNode): void
|
||||
}>()
|
||||
|
||||
const nodeBadgeText = computed<string>(() => {
|
||||
if (props.node.leaf) {
|
||||
return ''
|
||||
}
|
||||
if (props.node.badgeText !== undefined && props.node.badgeText !== null) {
|
||||
return props.node.badgeText
|
||||
}
|
||||
return props.node.totalLeaves.toString()
|
||||
})
|
||||
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
|
||||
|
||||
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
|
||||
const renameEditingNode =
|
||||
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
|
||||
@@ -78,54 +86,44 @@ const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
let dropTargetCleanup = () => {}
|
||||
let draggableCleanup = () => {}
|
||||
onMounted(() => {
|
||||
treeNodeElement.value = container.value?.closest(
|
||||
'.p-tree-node-content'
|
||||
) as HTMLElement
|
||||
if (props.node.droppable) {
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: treeNodeElement.value,
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: () => {
|
||||
canDrop.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (props.node.draggable) {
|
||||
draggableCleanup = draggable({
|
||||
element: treeNodeElement.value,
|
||||
getInitialData() {
|
||||
return {
|
||||
type: 'tree-explorer-node',
|
||||
data: props.node
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
draggableCleanup()
|
||||
})
|
||||
const treeNodeElementGetter = () =>
|
||||
container.value?.closest('.p-tree-node-content') as HTMLElement
|
||||
|
||||
if (props.node.draggable) {
|
||||
usePragmaticDraggable(treeNodeElementGetter, {
|
||||
getInitialData: () => {
|
||||
return {
|
||||
type: 'tree-explorer-node',
|
||||
data: props.node
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
|
||||
if (props.node.droppable) {
|
||||
usePragmaticDroppable(treeNodeElementGetter, {
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: () => {
|
||||
canDrop.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeAll } from 'vitest'
|
||||
import EditableText from '../EditableText.vue'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
|
||||
@@ -12,13 +12,15 @@
|
||||
@maximize="onMaximize"
|
||||
@unmaximize="onUnmaximize"
|
||||
:pt="{ header: 'pb-0' }"
|
||||
:aria-labelledby="headerId"
|
||||
>
|
||||
<template #header>
|
||||
<component
|
||||
v-if="dialogStore.headerComponent"
|
||||
:is="dialogStore.headerComponent"
|
||||
:id="headerId"
|
||||
/>
|
||||
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
|
||||
<h3 v-else :id="headerId">{{ dialogStore.title || ' ' }}</h3>
|
||||
</template>
|
||||
|
||||
<component :is="dialogStore.component" v-bind="contentProps" />
|
||||
@@ -48,4 +50,6 @@ const contentProps = computed(() => ({
|
||||
...dialogStore.props,
|
||||
maximized: maximized.value
|
||||
}))
|
||||
|
||||
const headerId = `dialog-${Math.random().toString(36).substr(2, 9)}`
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="settings-sidebar">
|
||||
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-64">
|
||||
<SearchBox
|
||||
class="settings-search-box"
|
||||
class="settings-search-box w-full mb-2"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchSettings') + '...'"
|
||||
@@ -13,11 +13,11 @@
|
||||
optionLabel="label"
|
||||
scrollHeight="100%"
|
||||
:disabled="inSearch"
|
||||
:pt="{ root: { class: 'border-none' } }"
|
||||
class="border-none w-full"
|
||||
/>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
<Divider layout="vertical" />
|
||||
<div class="settings-content">
|
||||
<ScrollPanel class="settings-content flex-grow">
|
||||
<Tabs :value="tabValue">
|
||||
<TabPanels class="settings-tab-panels">
|
||||
<TabPanel key="search-results" value="Search Results">
|
||||
@@ -55,24 +55,35 @@
|
||||
<AboutPanel />
|
||||
</TabPanel>
|
||||
<TabPanel key="keybinding" value="Keybinding">
|
||||
<KeybindingPanel />
|
||||
<Suspense>
|
||||
<KeybindingPanel />
|
||||
<template #fallback>
|
||||
<div>Loading keybinding panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
<TabPanel key="extension" value="Extension">
|
||||
<ExtensionPanel />
|
||||
<Suspense>
|
||||
<ExtensionPanel />
|
||||
<template #fallback>
|
||||
<div>Loading extension panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, defineAsyncComponent } from 'vue'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import SettingGroup from './setting/SettingGroup.vue'
|
||||
@@ -80,8 +91,13 @@ import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
import KeybindingPanel from './setting/KeybindingPanel.vue'
|
||||
import ExtensionPanel from './setting/ExtensionPanel.vue'
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
)
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
@@ -193,44 +209,8 @@ const tabValue = computed(() =>
|
||||
display: flex;
|
||||
height: 70vh;
|
||||
width: 60vw;
|
||||
max-width: 1000px;
|
||||
max-width: 1024px;
|
||||
overflow: hidden;
|
||||
/* Prevents container from scrolling */
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
/* Prevents sidebar from shrinking */
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.settings-search-box {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
/* Allows vertical scrolling */
|
||||
}
|
||||
|
||||
/* Ensure the Listbox takes full width of the sidebar */
|
||||
.settings-sidebar :deep(.p-listbox) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Optional: Style scrollbars for webkit browsers */
|
||||
.settings-sidebar::-webkit-scrollbar,
|
||||
.settings-content::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar-thumb,
|
||||
.settings-content::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<canvas ref="canvasRef" id="graph-canvas" tabindex="1" />
|
||||
</teleport>
|
||||
<NodeSearchboxPopover />
|
||||
<NodeTooltip />
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -22,15 +22,10 @@ import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import { ref, computed, onUnmounted, onMounted, watchEffect } from 'vue'
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import {
|
||||
LiteGraph,
|
||||
@@ -44,7 +39,6 @@ import {
|
||||
LGraphBadge
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import {
|
||||
@@ -52,7 +46,7 @@ import {
|
||||
useModelToNodeStore
|
||||
} from '@/stores/modelToNodeStore'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -67,6 +61,7 @@ const betaMenuEnabled = computed(
|
||||
const canvasMenuEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Graph.CanvasMenu')
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
@@ -107,12 +102,12 @@ watchEffect(() => {
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
if (canvasStore.draggingCanvas) {
|
||||
if (canvasStore.canvas.dragging_canvas) {
|
||||
canvasStore.canvas.canvas.style.cursor = 'grabbing'
|
||||
return
|
||||
}
|
||||
|
||||
if (canvasStore.readOnly) {
|
||||
if (canvasStore.canvas.read_only) {
|
||||
canvasStore.canvas.canvas.style.cursor = 'grab'
|
||||
return
|
||||
}
|
||||
@@ -120,7 +115,60 @@ watchEffect(() => {
|
||||
canvasStore.canvas.canvas.style.cursor = 'default'
|
||||
})
|
||||
|
||||
let dropTargetCleanup = () => {}
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = comfyApp.addNodeOnGraph(provider.nodeDef, {
|
||||
pos
|
||||
})
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// Backward compatible
|
||||
@@ -145,78 +193,6 @@ onMounted(async () => {
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: canvasRef.value,
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = comfyApp.addNodeOnGraph(provider.nodeDef, {
|
||||
pos
|
||||
})
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Load keybindings. This must be done after comfyApp loads settings.
|
||||
useKeybindingStore().loadUserKeybindings()
|
||||
|
||||
// Migrate legacy bookmarks
|
||||
useNodeBookmarkStore().migrateLegacyBookmarks()
|
||||
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.endsWithFilterStartSequence('')
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
useNodeFrequencyStore().loadNodeFrequencies()
|
||||
emit('ready')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -26,13 +26,16 @@
|
||||
severity="secondary"
|
||||
v-tooltip.left="
|
||||
t(
|
||||
'graphCanvasMenu.' + (canvasStore.readOnly ? 'panMode' : 'selectMode')
|
||||
'graphCanvasMenu.' +
|
||||
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
|
||||
) + ' (Space)'
|
||||
"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-material-symbols:pan-tool-outline v-if="canvasStore.readOnly" />
|
||||
<i-material-symbols:pan-tool-outline
|
||||
v-if="canvasStore.canvas?.read_only"
|
||||
/>
|
||||
<i-simple-line-icons:cursor v-else />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingStore = useSettingStore()
|
||||
const tooltipRef = ref<HTMLDivElement>()
|
||||
const tooltipText = ref('')
|
||||
const left = ref<string>()
|
||||
@@ -129,24 +128,8 @@ const onMouseMove = (e: MouseEvent) => {
|
||||
idleTimeout = window.setTimeout(onIdle, 500)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => settingStore.get('Comfy.EnableTooltips'),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('click', hideTooltip)
|
||||
} else {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
})
|
||||
useEventListener(window, 'mousemove', onMouseMove)
|
||||
useEventListener(window, 'click', hideTooltip)
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:iconBadge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip="tab.tooltip + getTabTooltipSuffix(tab)"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
CustomSidebarTabExtension,
|
||||
SidebarTabExtension
|
||||
} from '@/types/extensionTypes'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -74,6 +75,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
const keybinding = keybindingStore.getKeybindingByCommandId(
|
||||
`Workspace.ToggleSidebarTab.${tab.id}`
|
||||
)
|
||||
return keybinding ? ` (${keybinding.combo.toString()})` : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -8,21 +8,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const previousDarkTheme = ref('dark')
|
||||
const currentTheme = computed(() => useSettingStore().get('Comfy.ColorPalette'))
|
||||
const isDarkMode = computed(() => currentTheme.value !== 'light')
|
||||
const icon = computed(() => (isDarkMode.value ? 'pi pi-moon' : 'pi pi-sun'))
|
||||
const settingStore = useSettingStore()
|
||||
const currentTheme = computed(() => settingStore.get('Comfy.ColorPalette'))
|
||||
const icon = computed(() =>
|
||||
currentTheme.value !== 'light' ? 'pi pi-moon' : 'pi pi-sun'
|
||||
)
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const toggleTheme = () => {
|
||||
if (isDarkMode.value) {
|
||||
previousDarkTheme.value = currentTheme.value
|
||||
useSettingStore().set('Comfy.ColorPalette', 'light')
|
||||
} else {
|
||||
useSettingStore().set('Comfy.ColorPalette', previousDarkTheme.value)
|
||||
}
|
||||
commandStore.execute('Comfy.ToggleTheme')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.modelLibrary')">
|
||||
<template #tool-buttons> </template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.modelLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="model-lib-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
:placeholder="$t('searchModels') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="model-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
:placeholder="$t('searchModels') + '...'"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<ModelTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<ModelTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="model-library-model-preview-container" />
|
||||
@@ -70,7 +68,8 @@ const root: ComputedRef<TreeNode> = computed(() => {
|
||||
if (Object.values(models.models).length) {
|
||||
modelList.push(...Object.values(models.models))
|
||||
} else {
|
||||
const fakeModel = new ComfyModelDef('(No Content)', folder)
|
||||
// ModelDef with key 'folder/a/b/c/' is treated as empty folder
|
||||
const fakeModel = new ComfyModelDef('', folder)
|
||||
fakeModel.is_fake_object = true
|
||||
modelList.push(fakeModel)
|
||||
}
|
||||
@@ -83,15 +82,12 @@ const root: ComputedRef<TreeNode> = computed(() => {
|
||||
if (searchQuery.value) {
|
||||
const search = searchQuery.value.toLocaleLowerCase()
|
||||
modelList = modelList.filter((model: ComfyModelDef) => {
|
||||
return model.file_name.toLocaleLowerCase().includes(search)
|
||||
return model.searchable.includes(search)
|
||||
})
|
||||
}
|
||||
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) => {
|
||||
return [
|
||||
model.directory,
|
||||
...model.file_name.replaceAll('\\', '/').split('/')
|
||||
]
|
||||
})
|
||||
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) =>
|
||||
model.key.split('/')
|
||||
)
|
||||
return tree
|
||||
})
|
||||
|
||||
@@ -102,18 +98,7 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
const model: ComfyModelDef | null =
|
||||
node.leaf && node.data ? node.data : null
|
||||
if (model?.is_fake_object) {
|
||||
if (model.file_name === '(No Content)') {
|
||||
return {
|
||||
key: node.key,
|
||||
label: t('noContent'),
|
||||
leaf: true,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
|
||||
return 'pi pi-file'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
if (model.file_name === 'Loading') {
|
||||
return {
|
||||
key: node.key,
|
||||
label: t('loading') + '...',
|
||||
@@ -151,10 +136,8 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
if (node.children?.length === 1) {
|
||||
const onlyChild = node.children[0]
|
||||
if (onlyChild.data?.is_fake_object) {
|
||||
if (onlyChild.data.file_name === '(No Content)') {
|
||||
return '0'
|
||||
} else if (onlyChild.data.file_name === 'Loading') {
|
||||
return '?'
|
||||
if (onlyChild.data.file_name === 'Loading') {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,15 +197,9 @@ watch(
|
||||
)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pi-fake-spacer {
|
||||
<style scoped>
|
||||
:deep(.pi-fake-spacer) {
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.nodeLibrary')">
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.nodeLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="new-folder-button"
|
||||
@@ -18,44 +21,41 @@
|
||||
v-tooltip="$t('sideToolbar.nodeLibraryTab.sortOrder')"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="node-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="node-lib-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
|
||||
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-if="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
<Popover ref="searchFilter" class="ml-[-13px]">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</template>
|
||||
<template #body>
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-show="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
class="m-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="node-library-node-preview-container" />
|
||||
@@ -193,23 +193,3 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
handleSearch(searchQuery.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.node-lib-filter-popup {
|
||||
margin-left: -13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
|
||||
:deep(.node-lib-bookmark-tree-explorer) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
:deep(.p-divider) {
|
||||
margin: var(--comfy-tree-explorer-item-padding) 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -249,7 +249,8 @@ const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: t('loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow()
|
||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
||||
disabled: !menuTargetTask.value?.workflow
|
||||
},
|
||||
{
|
||||
label: t('goToNode'),
|
||||
@@ -327,6 +328,14 @@ watch(
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.queue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
|
||||
@@ -1,66 +1,36 @@
|
||||
<template>
|
||||
<div class="comfy-vue-side-bar-container">
|
||||
<Toolbar class="comfy-vue-side-bar-header">
|
||||
<template #start>
|
||||
<span class="comfy-vue-side-bar-header-span">{{
|
||||
props.title.toUpperCase()
|
||||
}}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<div class="comfy-vue-side-bar-body">
|
||||
<slot name="body"></slot>
|
||||
<div
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full"
|
||||
:class="props.class"
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header">
|
||||
<Toolbar
|
||||
class="flex-shrink-0 border-x-0 border-t-0 rounded-none px-2 py-1 min-h-10"
|
||||
>
|
||||
<template #start>
|
||||
<span class="text-sm">{{ props.title.toUpperCase() }}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<!-- h-0 to force scrollpanel to flex-grow -->
|
||||
<ScrollPanel class="comfy-vue-side-bar-body flex-grow h-0">
|
||||
<div class="h-full">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Toolbar from 'primevue/toolbar'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
class?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-vue-side-bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header {
|
||||
flex-shrink: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
padding: 0.25rem 1rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header-span {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.workflows')">
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.workflows')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
@@ -23,13 +26,15 @@
|
||||
text
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="workflows-search-box mx-4 my-4"
|
||||
class="workflows-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchWorkflows') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="comfyui-workflows-panel" v-if="!isSearching">
|
||||
<div
|
||||
class="comfyui-workflows-open"
|
||||
@@ -214,9 +219,3 @@ const selectionKeys = computed(() => ({
|
||||
[`root/${workflowStore.activeWorkflow?.name}.json`]: true
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
class="node-lib-bookmark-tree-explorer py-0"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
:src="item.url"
|
||||
:contain="false"
|
||||
class="galleria-image"
|
||||
v-if="item.isImage"
|
||||
/>
|
||||
<ResultVideo v-else-if="item.isVideo" :result="item" />
|
||||
</template>
|
||||
</Galleria>
|
||||
</template>
|
||||
@@ -35,6 +37,7 @@ import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import Galleria from 'primevue/galleria'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const galleryVisible = ref(false)
|
||||
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
<template>
|
||||
<div class="result-container" ref="resultContainer">
|
||||
<template
|
||||
v-if="result.mediaType === 'images' || result.mediaType === 'gifs'"
|
||||
>
|
||||
<ComfyImage
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
/>
|
||||
<div class="image-preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TODO: handle more media types -->
|
||||
<ComfyImage
|
||||
v-if="result.isImage"
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
/>
|
||||
<ResultVideo v-else-if="result.isVideo" :result="result" />
|
||||
<div v-else class="task-result-preview">
|
||||
<i class="pi pi-file"></i>
|
||||
<span>{{ result.mediaType }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="result.supportsPreview" class="preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,6 +29,7 @@ import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
@@ -67,7 +66,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-preview-mask {
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
@@ -80,7 +79,7 @@ onMounted(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.result-container:hover .image-preview-mask {
|
||||
.result-container:hover .preview-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
30
src/components/sidebar/tabs/queue/ResultVideo.vue
Normal file
30
src/components/sidebar/tabs/queue/ResultVideo.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<video controls width="100%" height="100%">
|
||||
<source :src="url" :type="htmlVideoType" />
|
||||
{{ $t('videoFailedToLoad') }}
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const vhsAdvancedPreviews = computed(() =>
|
||||
settingStore.get('VHS.AdvancedPreviews')
|
||||
)
|
||||
|
||||
const url = computed(() =>
|
||||
vhsAdvancedPreviews.value
|
||||
? props.result.vhsAdvancedPreviewUrl
|
||||
: props.result.url
|
||||
)
|
||||
const htmlVideoType = computed(() =>
|
||||
vhsAdvancedPreviews.value ? 'video/webm' : props.result.htmlVideoType
|
||||
)
|
||||
</script>
|
||||
@@ -83,11 +83,12 @@ const coverResult = flatOutputs.length
|
||||
? props.task.previewOutput || flatOutputs[0]
|
||||
: null
|
||||
// Using `==` instead of `===` because NodeId can be a string or a number
|
||||
const node: ComfyNode | null = flatOutputs.length
|
||||
? props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult.nodeId
|
||||
) ?? null
|
||||
: null
|
||||
const node: ComfyNode | null =
|
||||
flatOutputs.length && props.task.workflow
|
||||
? props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult.nodeId
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
// Disabled because of https://github.com/Comfy-Org/ComfyUI_frontend/issues/1184
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { expect, describe, it } from 'vitest'
|
||||
import ResultGallery from '../ResultGallery.vue'
|
||||
@@ -12,15 +15,12 @@ describe('ResultGallery', () => {
|
||||
let mockResultItem: ResultItemImpl
|
||||
|
||||
beforeEach(() => {
|
||||
mockResultItem = {
|
||||
mockResultItem = new ResultItemImpl({
|
||||
filename: 'test.jpg',
|
||||
type: 'images',
|
||||
nodeId: 'test',
|
||||
mediaType: 'images',
|
||||
url: 'https://picsum.photos/200/300',
|
||||
urlWithTimestamp: 'https://picsum.photos/200/300?t=123456',
|
||||
supportsPreview: true
|
||||
}
|
||||
nodeId: 1,
|
||||
mediaType: 'images'
|
||||
})
|
||||
})
|
||||
|
||||
const mountResultGallery = (props: ResultGalleryProps, options = {}) => {
|
||||
@@ -9,7 +9,12 @@
|
||||
}"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<a class="p-menubar-item-link" v-bind="props.action">
|
||||
<a
|
||||
class="p-menubar-item-link"
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
>
|
||||
<span v-if="item.icon" class="p-menubar-item-icon" :class="item.icon" />
|
||||
<span class="p-menubar-item-label">{{ item.label }}</span>
|
||||
<span
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { app } from '../../scripts/app'
|
||||
import { $el } from '../../scripts/ui'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows for simple dynamic prompt replacement
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { mergeIfValid } from './widgetInputs'
|
||||
@@ -7,6 +8,7 @@ import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyLink, ComfyNode, ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
type GroupNodeWorkflowData = {
|
||||
external: ComfyLink[]
|
||||
@@ -52,7 +54,7 @@ class GroupNodeBuilder {
|
||||
nodes: LGraphNode[]
|
||||
nodeData: any
|
||||
|
||||
constructor(nodes) {
|
||||
constructor(nodes: LGraphNode[]) {
|
||||
this.nodes = nodes
|
||||
}
|
||||
|
||||
@@ -995,9 +997,7 @@ export class GroupNodeHandler {
|
||||
},
|
||||
{
|
||||
content: 'Manage Group Node',
|
||||
callback: () => {
|
||||
new ManageGroupDialog(app).show(this.type)
|
||||
}
|
||||
callback: manageGroupNodes
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1367,11 +1367,11 @@ export class GroupNodeHandler {
|
||||
return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP]
|
||||
}
|
||||
|
||||
static isGroupNode(node) {
|
||||
static isGroupNode(node: LGraphNode) {
|
||||
return !!node.constructor?.nodeData?.[GROUP]
|
||||
}
|
||||
|
||||
static async fromNodes(nodes) {
|
||||
static async fromNodes(nodes: LGraphNode[]) {
|
||||
// Process the nodes into the stored workflow group node data
|
||||
const builder = new GroupNodeBuilder(nodes)
|
||||
const res = builder.build()
|
||||
@@ -1404,9 +1404,7 @@ function addConvertToGroupOptions() {
|
||||
options.splice(index + 1, null, {
|
||||
content: `Convert to Group Node`,
|
||||
disabled,
|
||||
callback: async () => {
|
||||
return await GroupNodeHandler.fromNodes(selected)
|
||||
}
|
||||
callback: convertSelectedNodesToGroupNode
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1416,9 +1414,7 @@ function addConvertToGroupOptions() {
|
||||
options.splice(index + 1, null, {
|
||||
content: `Manage Group Nodes`,
|
||||
disabled,
|
||||
callback: () => {
|
||||
new ManageGroupDialog(app).show()
|
||||
}
|
||||
callback: manageGroupNodes
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1455,10 +1451,86 @@ const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert selected nodes to a group node
|
||||
* @throws {Error} if no nodes are selected
|
||||
* @throws {Error} if a group node is already selected
|
||||
* @throws {Error} if a group node is selected
|
||||
*
|
||||
* The context menu item should not be available if any of the above conditions are met.
|
||||
* The error is automatically handled by the commandStore when the command is executed.
|
||||
*/
|
||||
async function convertSelectedNodesToGroupNode() {
|
||||
const nodes = Object.values(app.canvas.selected_nodes ?? {})
|
||||
if (nodes.length === 0) {
|
||||
throw new Error('No nodes selected')
|
||||
}
|
||||
if (nodes.length === 1) {
|
||||
throw new Error('Please select multiple nodes to convert to group node')
|
||||
}
|
||||
if (nodes.some((n) => GroupNodeHandler.isGroupNode(n))) {
|
||||
throw new Error('Selected nodes contain a group node')
|
||||
}
|
||||
return await GroupNodeHandler.fromNodes(nodes)
|
||||
}
|
||||
|
||||
function ungroupSelectedGroupNodes() {
|
||||
const nodes = Object.values(app.canvas.selected_nodes ?? {})
|
||||
for (const node of nodes) {
|
||||
if (GroupNodeHandler.isGroupNode(node)) {
|
||||
node['convertToNodes']?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function manageGroupNodes() {
|
||||
new ManageGroupDialog(app).show()
|
||||
}
|
||||
|
||||
const id = 'Comfy.GroupNode'
|
||||
let globalDefs
|
||||
const ext = {
|
||||
const ext: ComfyExtension = {
|
||||
name: id,
|
||||
commands: [
|
||||
{
|
||||
id: 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode',
|
||||
label: 'Convert selected nodes to group node',
|
||||
icon: 'pi pi-sitemap',
|
||||
versionAdded: '1.3.17',
|
||||
function: convertSelectedNodesToGroupNode
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupNode.UngroupSelectedGroupNodes',
|
||||
label: 'Ungroup selected group nodes',
|
||||
icon: 'pi pi-sitemap',
|
||||
versionAdded: '1.3.17',
|
||||
function: ungroupSelectedGroupNodes
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupNode.ManageGroupNodes',
|
||||
label: 'Manage group nodes',
|
||||
icon: 'pi pi-cog',
|
||||
versionAdded: '1.3.17',
|
||||
function: manageGroupNodes
|
||||
}
|
||||
],
|
||||
keybindings: [
|
||||
{
|
||||
commandId: 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode',
|
||||
combo: {
|
||||
alt: true,
|
||||
key: 'g'
|
||||
}
|
||||
},
|
||||
{
|
||||
commandId: 'Comfy.GroupNode.UngroupSelectedGroupNodes',
|
||||
combo: {
|
||||
alt: true,
|
||||
shift: true,
|
||||
key: 'G'
|
||||
}
|
||||
}
|
||||
],
|
||||
setup() {
|
||||
addConvertToGroupOptions()
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el, ComfyDialog } from '../../scripts/ui'
|
||||
import { DraggableList } from '../../scripts/ui/draggableList'
|
||||
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LGraphGroup } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ app.registerExtension({
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding) {
|
||||
if (keybinding && keybinding.targetSelector !== '#graph-canvas') {
|
||||
await commandStore.execute(keybinding.commandId)
|
||||
event.preventDefault()
|
||||
return
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
const id = 'Comfy.LinkRenderMode'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app, type ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { IContextMenuValue } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { mergeIfValid, getWidgetConfig, setWidgetConfig } from './widgetInputs'
|
||||
@@ -159,7 +160,6 @@ app.registerExtension({
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l]
|
||||
if (link) {
|
||||
// @ts-expect-error Fix litegraph types
|
||||
link.color = color
|
||||
|
||||
if (app.configuringGraph) continue
|
||||
@@ -205,7 +205,6 @@ app.registerExtension({
|
||||
if (inputNode) {
|
||||
const link = app.graph.links[inputNode.inputs[0].link]
|
||||
if (link) {
|
||||
// @ts-expect-error Fix litegraph types
|
||||
link.color = color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { applyTextReplacements } from '../../scripts/utils'
|
||||
// Use widget values and dates in output filenames
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { app } from '../../scripts/app'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
@@ -37,13 +39,25 @@ app.registerExtension({
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value || 10
|
||||
}
|
||||
})
|
||||
// Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.
|
||||
// Using a new setting id can cause existing users to lose their existing settings.
|
||||
const alwaysSnapToGrid = app.ui.settings.addSetting({
|
||||
id: 'pysssss.SnapToGrid',
|
||||
category: ['Comfy', 'Graph', 'AlwaysSnapToGrid'],
|
||||
name: 'Always snap to grid',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.3.13'
|
||||
} as SettingParams)
|
||||
|
||||
const shouldSnapToGrid = () => app.shiftDown || alwaysSnapToGrid.value
|
||||
|
||||
// After moving a node, if the shift key is down align it to grid
|
||||
const onNodeMoved = app.canvas.onNodeMoved
|
||||
app.canvas.onNodeMoved = function (node) {
|
||||
const r = onNodeMoved?.apply(this, arguments)
|
||||
|
||||
if (app.shiftDown) {
|
||||
if (shouldSnapToGrid()) {
|
||||
// Ensure all selected nodes are realigned
|
||||
for (const id in this.selected_nodes) {
|
||||
this.selected_nodes[id].alignToGrid()
|
||||
@@ -58,7 +72,7 @@ app.registerExtension({
|
||||
app.graph.onNodeAdded = function (node) {
|
||||
const onResize = node.onResize
|
||||
node.onResize = function () {
|
||||
if (app.shiftDown) {
|
||||
if (shouldSnapToGrid()) {
|
||||
roundVectorToGrid(node.size)
|
||||
}
|
||||
return onResize?.apply(this, arguments)
|
||||
@@ -70,7 +84,7 @@ app.registerExtension({
|
||||
const origDrawNode = LGraphCanvas.prototype.drawNode
|
||||
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
||||
if (
|
||||
app.shiftDown &&
|
||||
shouldSnapToGrid() &&
|
||||
this.node_dragged &&
|
||||
node.id in this.selected_nodes
|
||||
) {
|
||||
@@ -132,8 +146,8 @@ app.registerExtension({
|
||||
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
|
||||
// has been set to `false`. Essentially, this check here is the equivalent to calling an
|
||||
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
|
||||
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
||||
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
||||
if (app.canvas.last_mouse_dragging === false && shouldSnapToGrid()) {
|
||||
// After moving a group (while shouldSnapToGrid()), snap all the child nodes and, finally,
|
||||
// align the group itself.
|
||||
this.recomputeInsideNodes()
|
||||
for (const node of this.nodes) {
|
||||
@@ -151,7 +165,7 @@ app.registerExtension({
|
||||
*/
|
||||
const drawGroups = LGraphCanvas.prototype.drawGroups
|
||||
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
||||
if (this.selected_group && app.shiftDown) {
|
||||
if (this.selected_group && shouldSnapToGrid()) {
|
||||
if (this.selected_group_resizing) {
|
||||
roundVectorToGrid(this.selected_group.size)
|
||||
} else if (selectedAndMovingGroup) {
|
||||
@@ -176,7 +190,7 @@ app.registerExtension({
|
||||
const onGroupAdd = LGraphCanvas.onGroupAdd
|
||||
LGraphCanvas.onGroupAdd = function () {
|
||||
const v = onGroupAdd.apply(app.canvas, arguments)
|
||||
if (app.shiftDown) {
|
||||
if (shouldSnapToGrid()) {
|
||||
const lastGroup = app.graph.groups[app.graph.groups.length - 1]
|
||||
if (lastGroup) {
|
||||
roundVectorToGrid(lastGroup.pos)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyWidgets, addValueControlWidgets } from '../../scripts/widgets'
|
||||
import { app } from '../../scripts/app'
|
||||
import { applyTextReplacements } from '../../scripts/utils'
|
||||
|
||||
59
src/hooks/dndHooks.ts
Normal file
59
src/hooks/dndHooks.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import {
|
||||
dropTargetForElements,
|
||||
draggable
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
|
||||
export function usePragmaticDroppable(
|
||||
dropTargetElement: HTMLElement | (() => HTMLElement),
|
||||
options: Omit<Parameters<typeof dropTargetForElements>[0], 'element'>
|
||||
) {
|
||||
let cleanup = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
const element =
|
||||
typeof dropTargetElement === 'function'
|
||||
? dropTargetElement()
|
||||
: dropTargetElement
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
cleanup = dropTargetForElements({
|
||||
element,
|
||||
...options
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup()
|
||||
})
|
||||
}
|
||||
|
||||
export function usePragmaticDraggable(
|
||||
draggableElement: HTMLElement | (() => HTMLElement),
|
||||
options: Omit<Parameters<typeof draggable>[0], 'element'>
|
||||
) {
|
||||
let cleanup = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
const element =
|
||||
typeof draggableElement === 'function'
|
||||
? draggableElement()
|
||||
: draggableElement
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
cleanup = draggable({
|
||||
element,
|
||||
...options
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup()
|
||||
})
|
||||
}
|
||||
16
src/hooks/sidebarTabs/modelLibrarySidebarTab.ts
Normal file
16
src/hooks/sidebarTabs/modelLibrarySidebarTab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
return {
|
||||
id: 'model-library',
|
||||
icon: 'pi pi-box',
|
||||
title: t('sideToolbar.modelLibrary'),
|
||||
tooltip: t('sideToolbar.modelLibrary'),
|
||||
component: markRaw(ModelLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
16
src/hooks/sidebarTabs/nodeLibrarySidebarTab.ts
Normal file
16
src/hooks/sidebarTabs/nodeLibrarySidebarTab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useNodeLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
return {
|
||||
id: 'node-library',
|
||||
icon: 'pi pi-book',
|
||||
title: t('sideToolbar.nodeLibrary'),
|
||||
tooltip: t('sideToolbar.nodeLibrary'),
|
||||
component: markRaw(NodeLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
22
src/hooks/sidebarTabs/queueSidebarTab.ts
Normal file
22
src/hooks/sidebarTabs/queueSidebarTab.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import QueueSidebarTab from '@/components/sidebar/tabs/QueueSidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useQueueSidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
return {
|
||||
id: 'queue',
|
||||
icon: 'pi pi-history',
|
||||
iconBadge: () => {
|
||||
const value = queuePendingTaskCountStore.count.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
title: t('sideToolbar.queue'),
|
||||
tooltip: t('sideToolbar.queue'),
|
||||
component: markRaw(QueueSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
30
src/hooks/sidebarTabs/workflowsSidebarTab.ts
Normal file
30
src/hooks/sidebarTabs/workflowsSidebarTab.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
return {
|
||||
id: 'workflows',
|
||||
icon: 'pi pi-folder-open',
|
||||
iconBadge: () => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const value = workflowStore.openWorkflows.length.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
title: t('sideToolbar.workflows'),
|
||||
tooltip: t('sideToolbar.workflows'),
|
||||
component: markRaw(WorkflowsSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
120
src/i18n.ts
120
src/i18n.ts
@@ -2,6 +2,7 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
videoFailedToLoad: 'Video failed to load',
|
||||
extensionName: 'Extension Name',
|
||||
reloadToApplyChanges: 'Reload to apply changes',
|
||||
insert: 'Insert',
|
||||
@@ -48,7 +49,6 @@ const messages = {
|
||||
noResultsFound: 'No Results Found',
|
||||
searchFailedMessage:
|
||||
"We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
noContent: '(No Content)',
|
||||
noTasksFound: 'No Tasks Found',
|
||||
noTasksFoundMessage: 'There are no tasks in the queue.',
|
||||
newFolder: 'New Folder',
|
||||
@@ -85,6 +85,7 @@ const messages = {
|
||||
change: 'On Change',
|
||||
changeTooltip: 'The workflow will be queued once a change is made',
|
||||
queueWorkflow: 'Queue workflow',
|
||||
queueWorkflowFront: 'Queue workflow (Insert at Front)',
|
||||
queue: 'Queue',
|
||||
interrupt: 'Cancel current run',
|
||||
refresh: 'Refresh node definitions',
|
||||
@@ -111,6 +112,7 @@ const messages = {
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
videoFailedToLoad: '视频加载失败',
|
||||
extensionName: '扩展名称',
|
||||
reloadToApplyChanges: '重新加载以应用更改',
|
||||
insert: '插入',
|
||||
@@ -144,7 +146,7 @@ const messages = {
|
||||
delete: '删除',
|
||||
rename: '重命名',
|
||||
customize: '定制',
|
||||
experimental: '测试',
|
||||
experimental: 'BETA',
|
||||
deprecated: '弃用',
|
||||
loadWorkflow: '加载工作流',
|
||||
goToNode: '前往节点',
|
||||
@@ -192,6 +194,7 @@ const messages = {
|
||||
change: '变动',
|
||||
changeTooltip: '工作流将会在改变后执行',
|
||||
queueWorkflow: '执行工作流',
|
||||
queueWorkflowFront: '执行工作流 (队列首)',
|
||||
queue: '队列',
|
||||
interrupt: '取消当前任务',
|
||||
refresh: '刷新节点',
|
||||
@@ -216,6 +219,119 @@ const messages = {
|
||||
panMode: '平移模式',
|
||||
toggleLinkVisibility: '切换链接可见性'
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
videoFailedToLoad: 'Видео не удалось загрузить',
|
||||
extensionName: 'Название расширения',
|
||||
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',
|
||||
insert: 'Вставить',
|
||||
systemInfo: 'Информация о системе',
|
||||
devices: 'Устройства',
|
||||
about: 'О',
|
||||
add: 'Добавить',
|
||||
confirm: 'Подтвердить',
|
||||
reset: 'Сбросить',
|
||||
resetKeybindingsTooltip: 'Сбросить сочетания клавиш по умолчанию',
|
||||
customizeFolder: 'Настроить папку',
|
||||
icon: 'Иконка',
|
||||
color: 'Цвет',
|
||||
bookmark: 'Закладка',
|
||||
folder: 'Папка',
|
||||
star: 'Звёздочка',
|
||||
heart: 'Сердце',
|
||||
file: 'Файл',
|
||||
inbox: 'Входящие',
|
||||
box: 'Ящик',
|
||||
briefcase: 'Чемодан',
|
||||
error: 'Ошибка',
|
||||
loading: 'Загрузка',
|
||||
findIssues: 'Найти Issue',
|
||||
copyToClipboard: 'Копировать в буфер обмена',
|
||||
openNewIssue: 'Открыть новый Issue',
|
||||
showReport: 'Показать отчёт',
|
||||
imageFailedToLoad: 'Изображение не удалось загрузить',
|
||||
reconnecting: 'Переподключение',
|
||||
reconnected: 'Переподключено',
|
||||
delete: 'Удалить',
|
||||
rename: 'Переименовать',
|
||||
customize: 'Настроить',
|
||||
experimental: 'БЕТА',
|
||||
deprecated: 'УСТАР',
|
||||
loadWorkflow: 'Загрузить рабочий процесс',
|
||||
goToNode: 'Перейти к узлу',
|
||||
settings: 'Настройки',
|
||||
searchWorkflows: 'Поиск рабочих процессов',
|
||||
searchSettings: 'Поиск настроек',
|
||||
searchNodes: 'Поиск узлов',
|
||||
searchModels: 'Поиск моделей',
|
||||
searchKeybindings: 'Поиск сочетаний клавиш',
|
||||
noResultsFound: 'Ничего не найдено',
|
||||
searchFailedMessage:
|
||||
'Не удалось найти ни одной настройки, соответствующей вашему запросу. Попробуйте скорректировать поисковый запрос.',
|
||||
noContent: '(Нет контента)',
|
||||
noTasksFound: 'Задачи не найдены',
|
||||
noTasksFoundMessage: 'В очереди нет задач.',
|
||||
newFolder: 'Новая папка',
|
||||
sideToolbar: {
|
||||
themeToggle: 'Переключить тему',
|
||||
queue: 'Очередь',
|
||||
nodeLibrary: 'Библиотека узлов',
|
||||
workflows: 'Рабочие процессы',
|
||||
browseTemplates: 'Просмотреть примеры шаблонов',
|
||||
openWorkflow: 'Открыть рабочий процесс в локальной файловой системе',
|
||||
newBlankWorkflow: 'Создайте новый пустой рабочий процесс',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Порядок сортировки'
|
||||
},
|
||||
modelLibrary: 'Библиотека моделей',
|
||||
queueTab: {
|
||||
showFlatList: 'Показать плоский список',
|
||||
backToAllTasks: 'Вернуться ко всем задачам',
|
||||
containImagePreview: 'Предпросмотр заливающего изображения',
|
||||
coverImagePreview: 'Предпросмотр подходящего изображения',
|
||||
clearPendingTasks: 'Очистить отложенные задачи'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
batchCount: 'Количество пакетов',
|
||||
batchCountTooltip:
|
||||
'Количество раз, когда генерация рабочего процесса должна быть помещена в очередь',
|
||||
autoQueue: 'Автоочередь',
|
||||
disabled: 'Отключено',
|
||||
disabledTooltip:
|
||||
'Рабочий процесс не будет автоматически помещён в очередь',
|
||||
instant: 'Мгновенно',
|
||||
instantTooltip:
|
||||
'Рабочий процесс будет помещён в очередь сразу же после завершения генерации',
|
||||
change: 'При изменении',
|
||||
changeTooltip:
|
||||
'Рабочий процесс будет поставлен в очередь после внесения изменений',
|
||||
queueWorkflow: 'Очередь рабочего процесса',
|
||||
queueWorkflowFront: 'Очередь рабочего процесса (Вставка спереди)',
|
||||
queue: 'Очередь',
|
||||
interrupt: 'Отменить текущее выполнение',
|
||||
refresh: 'Обновить определения узлов',
|
||||
clipspace: 'Открыть Clipspace',
|
||||
resetView: 'Сбросить вид холста',
|
||||
clear: 'Очистить рабочий процесс'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Начните работу с шаблона',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: 'Увеличить',
|
||||
zoomOut: 'Уменьшить',
|
||||
resetView: 'Сбросить вид',
|
||||
selectMode: 'Выбрать режим',
|
||||
panMode: 'Режим панорамирования',
|
||||
toggleLinkVisibility: 'Переключить видимость ссылок'
|
||||
}
|
||||
}
|
||||
// TODO: Add more languages
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import App from './App.vue'
|
||||
import router from '@/router'
|
||||
import { createApp } from 'vue'
|
||||
@@ -9,7 +10,6 @@ import Aura from '@primevue/themes/aura'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import 'reflect-metadata'
|
||||
|
||||
import '@comfyorg/litegraph/style.css'
|
||||
import '@/assets/css/style.css'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import {
|
||||
DownloadModelStatus,
|
||||
@@ -273,9 +274,15 @@ class ComfyApi extends EventTarget {
|
||||
* Loads node object definitions for the graph
|
||||
* @returns The node definitions
|
||||
*/
|
||||
async getNodeDefs(): Promise<Record<string, ComfyNodeDef>> {
|
||||
async getNodeDefs({ validate = false }: { validate?: boolean } = {}): Promise<
|
||||
Record<string, ComfyNodeDef>
|
||||
> {
|
||||
const resp = await this.fetchApi('/object_info', { cache: 'no-store' })
|
||||
const objectInfoUnsafe = await resp.json()
|
||||
if (!validate) {
|
||||
return objectInfoUnsafe
|
||||
}
|
||||
// Validate node definitions against zod schema. (slow)
|
||||
const objectInfo: Record<string, ComfyNodeDef> = {}
|
||||
for (const key in objectInfoUnsafe) {
|
||||
const validatedDef = validateComfyNodeDef(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyLogging } from './logging'
|
||||
import { ComfyWidgetConstructor, ComfyWidgets, initWidgets } from './widgets'
|
||||
import { ComfyUI, $el } from './ui'
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
SYSTEM_NODE_DEFS,
|
||||
useNodeDefStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { Vector2 } from '@comfyorg/litegraph'
|
||||
import { INodeInputSlot, Vector2 } from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
showExecutionErrorDialog,
|
||||
@@ -53,6 +54,9 @@ import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { shallowReactive } from 'vue'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
@@ -111,7 +115,6 @@ export class ComfyApp {
|
||||
extensionManager: ExtensionManager
|
||||
_nodeOutputs: Record<string, any>
|
||||
nodePreviewImages: Record<string, typeof Image>
|
||||
shiftDown: boolean
|
||||
graph: LGraph
|
||||
enableWorkflowViewRestore: any
|
||||
canvas: LGraphCanvas
|
||||
@@ -137,12 +140,20 @@ export class ComfyApp {
|
||||
menu: ComfyAppMenu
|
||||
bypassBgColor: string
|
||||
|
||||
// @deprecated
|
||||
// Use useExecutionStore().executingNodeId instead
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
*/
|
||||
get runningNodeId(): string | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use useWorkspaceStore().shiftDown instead
|
||||
*/
|
||||
get shiftDown(): boolean {
|
||||
return useWorkspaceStore().shiftDown
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.vueAppReady = false
|
||||
this.ui = new ComfyUI(this)
|
||||
@@ -175,12 +186,6 @@ export class ComfyApp {
|
||||
* @type {Record<string, Image>}
|
||||
*/
|
||||
this.nodePreviewImages = {}
|
||||
|
||||
/**
|
||||
* If the shift key on the keyboard is pressed
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.shiftDown = false
|
||||
}
|
||||
|
||||
get nodeOutputs() {
|
||||
@@ -1278,13 +1283,10 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* Handle keypress
|
||||
*
|
||||
* Ctrl + M mute/unmute selected nodes
|
||||
*/
|
||||
#addProcessKeyHandler() {
|
||||
const self = this
|
||||
const origProcessKey = LGraphCanvas.prototype.processKey
|
||||
LGraphCanvas.prototype.processKey = function (e) {
|
||||
LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
|
||||
if (!this.graph) {
|
||||
return
|
||||
}
|
||||
@@ -1296,54 +1298,11 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
if (e.type == 'keydown' && !e.repeat) {
|
||||
// Ctrl + M mute/unmute
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey)) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
if (this.selected_nodes[i].mode === 2) {
|
||||
// never
|
||||
this.selected_nodes[i].mode = 0 // always
|
||||
} else {
|
||||
this.selected_nodes[i].mode = 2 // never
|
||||
}
|
||||
}
|
||||
}
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// Ctrl + B bypass
|
||||
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
if (this.selected_nodes[i].mode === 4) {
|
||||
// never
|
||||
this.selected_nodes[i].mode = 0 // always
|
||||
} else {
|
||||
this.selected_nodes[i].mode = 4 // never
|
||||
}
|
||||
}
|
||||
}
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// p pin/unpin
|
||||
if (e.key === 'p') {
|
||||
if (this.selected_nodes) {
|
||||
for (const i in this.selected_nodes) {
|
||||
const node = this.selected_nodes[i]
|
||||
node.pin()
|
||||
}
|
||||
}
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// Alt + C collapse/uncollapse
|
||||
if (e.key === 'c' && e.altKey) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
this.selected_nodes[i].collapse()
|
||||
}
|
||||
}
|
||||
const keyCombo = KeyComboImpl.fromEvent(e)
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding && keybinding.targetSelector === '#graph-canvas') {
|
||||
useCommandStore().execute(keybinding.commandId)
|
||||
block_default = true
|
||||
}
|
||||
|
||||
@@ -1362,26 +1321,6 @@ export class ComfyApp {
|
||||
// Trigger onPaste
|
||||
return true
|
||||
}
|
||||
|
||||
if (e.key === '+' && e.altKey) {
|
||||
block_default = true
|
||||
let scale = this.ds.scale * 1.1
|
||||
this.ds.changeScale(scale, [
|
||||
this.ds.element.width / 2,
|
||||
this.ds.element.height / 2
|
||||
])
|
||||
this.graph.change()
|
||||
}
|
||||
|
||||
if (e.key === '-' && e.altKey) {
|
||||
block_default = true
|
||||
let scale = (this.ds.scale * 1) / 1.1
|
||||
this.ds.changeScale(scale, [
|
||||
this.ds.element.width / 2,
|
||||
this.ds.element.height / 2
|
||||
])
|
||||
this.graph.change()
|
||||
}
|
||||
}
|
||||
|
||||
this.graph.change()
|
||||
@@ -1681,15 +1620,6 @@ export class ComfyApp {
|
||||
api.init()
|
||||
}
|
||||
|
||||
#addKeyboardHandler() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
this.shiftDown = e.shiftKey
|
||||
})
|
||||
window.addEventListener('keyup', (e) => {
|
||||
this.shiftDown = e.shiftKey
|
||||
})
|
||||
}
|
||||
|
||||
#addConfigureHandler() {
|
||||
const app = this
|
||||
const configure = LGraph.prototype.configure
|
||||
@@ -1908,7 +1838,20 @@ export class ComfyApp {
|
||||
|
||||
this.#addAfterConfigureHandler()
|
||||
|
||||
this.canvas = new LGraphCanvas(canvasEl, this.graph)
|
||||
// Make LGraphCanvas shallow reactive so that any change on the root object
|
||||
// triggers reactivity.
|
||||
this.canvas = shallowReactive(
|
||||
new LGraphCanvas(canvasEl, this.graph, {
|
||||
skip_events: true,
|
||||
skip_render: true
|
||||
})
|
||||
)
|
||||
// Bind event/ start rendering later, so that event handlers get reactive canvas reference.
|
||||
this.canvas.options.skip_events = false
|
||||
this.canvas.options.skip_render = false
|
||||
this.canvas.bindEvents()
|
||||
this.canvas.startRendering()
|
||||
|
||||
this.ctx = canvasEl.getContext('2d')
|
||||
|
||||
LiteGraph.alt_drag_do_clone_nodes = true
|
||||
@@ -1970,7 +1913,6 @@ export class ComfyApp {
|
||||
this.#addDropHandler()
|
||||
this.#addCopyHandler()
|
||||
this.#addPasteHandler()
|
||||
this.#addKeyboardHandler()
|
||||
this.#addWidgetLinkHandling()
|
||||
|
||||
await this.#invokeExtensionsAsync('setup')
|
||||
@@ -2027,7 +1969,9 @@ export class ComfyApp {
|
||||
*/
|
||||
async registerNodes() {
|
||||
// Load node definitions from the backend
|
||||
const defs = await api.getNodeDefs()
|
||||
const defs = await api.getNodeDefs({
|
||||
validate: useSettingStore().get('Comfy.Validation.NodeDefs')
|
||||
})
|
||||
await this.registerNodesFromDefs(defs)
|
||||
await this.#invokeExtensionsAsync('registerCustomNodes')
|
||||
if (this.vueAppReady) {
|
||||
@@ -2150,6 +2094,11 @@ export class ComfyApp {
|
||||
incoming: Record<string, any>
|
||||
) => {
|
||||
const result = { ...incoming }
|
||||
if (current.widget === undefined && incoming.widget !== undefined) {
|
||||
// Field must be input as only inputs can be converted
|
||||
this.inputs.push(current as INodeInputSlot)
|
||||
return incoming
|
||||
}
|
||||
for (const key of ['name', 'type', 'shape']) {
|
||||
if (current[key] !== undefined) {
|
||||
result[key] = current[key]
|
||||
@@ -2231,7 +2180,7 @@ export class ComfyApp {
|
||||
localStorage.setItem('litegrapheditor_clipboard', old)
|
||||
}
|
||||
|
||||
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
#showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||
showLoadWorkflowWarning({
|
||||
missingNodeTypes,
|
||||
@@ -2244,7 +2193,7 @@ export class ComfyApp {
|
||||
})
|
||||
}
|
||||
|
||||
showMissingModelsError(missingModels, paths) {
|
||||
#showMissingModelsError(missingModels, paths) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
|
||||
showMissingModelsWarning({
|
||||
missingModels,
|
||||
@@ -2483,11 +2432,11 @@ export class ComfyApp {
|
||||
|
||||
// TODO: Properly handle if both nodes and models are missing (sequential dialogs?)
|
||||
if (missingNodeTypes.length && showMissingNodesDialog) {
|
||||
this.showMissingNodesError(missingNodeTypes)
|
||||
this.#showMissingNodesError(missingNodeTypes)
|
||||
}
|
||||
if (missingModels.length && showMissingModelsDialog) {
|
||||
const paths = await api.getFolderPaths()
|
||||
this.showMissingModelsError(missingModels, paths)
|
||||
this.#showMissingModelsError(missingModels, paths)
|
||||
}
|
||||
await this.#invokeExtensionsAsync('afterConfigureGraph', missingNodeTypes)
|
||||
requestAnimationFrame(() => {
|
||||
@@ -2874,7 +2823,7 @@ export class ComfyApp {
|
||||
(n) => !LiteGraph.registered_node_types[n.class_type]
|
||||
)
|
||||
if (missingNodeTypes.length) {
|
||||
this.showMissingNodesError(
|
||||
this.#showMissingNodesError(
|
||||
// @ts-expect-error
|
||||
missingNodeTypes.map((t) => t.class_type),
|
||||
false
|
||||
@@ -2989,7 +2938,9 @@ export class ComfyApp {
|
||||
useModelStore().clearCache()
|
||||
}
|
||||
|
||||
const defs = await api.getNodeDefs()
|
||||
const defs = await api.getNodeDefs({
|
||||
validate: useSettingStore().get('Comfy.Validation.NodeDefs')
|
||||
})
|
||||
|
||||
for (const nodeId in defs) {
|
||||
this.registerNodeDef(nodeId, defs[nodeId])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { ComfyApp } from './app'
|
||||
import { api } from './api'
|
||||
import { clone } from './utils'
|
||||
@@ -131,7 +132,11 @@ export class ChangeTracker {
|
||||
let keyIgnored = false
|
||||
window.addEventListener(
|
||||
'keydown',
|
||||
(e) => {
|
||||
(e: KeyboardEvent) => {
|
||||
// Do not trigger on repeat events (Holding down a key)
|
||||
// This can happen when user is holding down "Space" to pan the canvas.
|
||||
if (e.repeat) return
|
||||
|
||||
const activeEl = document.activeElement
|
||||
requestAnimationFrame(async () => {
|
||||
let bindInputEl
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app, ANIM_PREVIEW_WIDGET } from './app'
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { Vector4 } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el, ComfyDialog } from './ui'
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
|
||||
const dataView = new DataView(buffer)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
export function getFromPngBuffer(buffer: ArrayBuffer) {
|
||||
// Get the PNG data as a Uint8Array
|
||||
const pngData = new Uint8Array(buffer)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { api } from './api'
|
||||
import { getFromPngFile } from './metadata/png'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from './api'
|
||||
import { ComfyDialog as _ComfyDialog } from './ui/dialog'
|
||||
import { toggleSwitch } from './ui/toggleSwitch'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyDialog } from '../dialog'
|
||||
import { $el } from '../../ui'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList, toggleElement } from '../utils'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyButton } from './button'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { prop } from '../../utils'
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList } from '../utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { $el } from '../ui'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
/*
|
||||
Original implementation:
|
||||
https://github.com/TahaSh/drag-to-reorder
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../app'
|
||||
import { $el } from '../ui'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../ui'
|
||||
import { api } from '../api'
|
||||
import { ComfyDialog } from './dialog'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../ui'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from '../api'
|
||||
import { $el } from '../ui'
|
||||
import { createSpinner } from './spinner'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
export type ClassList = string | string[] | Record<string, boolean>
|
||||
|
||||
export function applyClasses(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
import { $el } from './ui'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from './api'
|
||||
import './domWidget'
|
||||
import type { ComfyApp } from './app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { ComfyApp } from './app'
|
||||
import { api } from './api'
|
||||
import { ChangeTracker } from './changeTracker'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import Fuse, { IFuseOptions, FuseSearchOptions } from 'fuse.js'
|
||||
import _ from 'lodash'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -17,6 +18,7 @@ import { useTitleEditorStore } from './graphStore'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { useWorkflowStore } from './workflowStore'
|
||||
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
export interface ComfyCommand {
|
||||
id: string
|
||||
@@ -75,6 +77,28 @@ export class ComfyCommandImpl implements ComfyCommand {
|
||||
const getTracker = () =>
|
||||
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
|
||||
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
const result: LGraphNode[] = []
|
||||
if (selectedNodes) {
|
||||
for (const i in selectedNodes) {
|
||||
const node = selectedNodes[i]
|
||||
result.push(node)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const toggleSelectedNodesMode = (mode: number) => {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
if (node.mode === mode) {
|
||||
node.mode = 0 // always
|
||||
} else {
|
||||
node.mode = mode
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useCommandStore = defineStore('command', () => {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -248,7 +272,11 @@ export const useCommandStore = defineStore('command', () => {
|
||||
icon: 'pi pi-plus',
|
||||
label: 'Zoom In',
|
||||
function: () => {
|
||||
app.canvas.ds.changeScale(app.canvas.ds.scale + 0.1)
|
||||
const ds = app.canvas.ds
|
||||
ds.changeScale(ds.scale * 1.1, [
|
||||
ds.element.width / 2,
|
||||
ds.element.height / 2
|
||||
])
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
@@ -257,7 +285,11 @@ export const useCommandStore = defineStore('command', () => {
|
||||
icon: 'pi pi-minus',
|
||||
label: 'Zoom Out',
|
||||
function: () => {
|
||||
app.canvas.ds.changeScale(app.canvas.ds.scale - 0.1)
|
||||
const ds = app.canvas.ds
|
||||
ds.changeScale(ds.scale / 1.1, [
|
||||
ds.element.width / 2,
|
||||
ds.element.height / 2
|
||||
])
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
@@ -365,6 +397,67 @@ export const useCommandStore = defineStore('command', () => {
|
||||
function: () => {
|
||||
useWorkflowStore().loadPreviousOpenedWorkflow()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||
icon: 'pi pi-volume-off',
|
||||
label: 'Mute/Unmute Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
toggleSelectedNodesMode(2) // muted
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
|
||||
icon: 'pi pi-shield',
|
||||
label: 'Bypass/Unbypass Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
toggleSelectedNodesMode(4) // bypassed
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Pin',
|
||||
icon: 'pi pi-pin',
|
||||
label: 'Pin/Unpin Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
node.pin(!node.pinned)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
|
||||
icon: 'pi pi-minus',
|
||||
label: 'Collapse/Expand Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
node.collapse()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleTheme',
|
||||
icon: 'pi pi-moon',
|
||||
label: 'Toggle Theme',
|
||||
versionAdded: '1.3.12',
|
||||
function: (() => {
|
||||
let previousDarkTheme: string = 'dark'
|
||||
|
||||
// Official light theme is the only light theme supported now.
|
||||
const isDarkMode = (themeId: string) => themeId !== 'light'
|
||||
return () => {
|
||||
const currentTheme = settingStore.get('Comfy.ColorPalette')
|
||||
if (isDarkMode(currentTheme)) {
|
||||
previousDarkTheme = currentTheme
|
||||
settingStore.set('Comfy.ColorPalette', 'light')
|
||||
} else {
|
||||
settingStore.set('Comfy.ColorPalette', previousDarkTheme)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -94,5 +94,71 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.ShowSettingsDialog'
|
||||
},
|
||||
// For '=' both holding shift and not holding shift
|
||||
{
|
||||
combo: {
|
||||
key: '=',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
alt: true,
|
||||
shift: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
// For number pad '+'
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '-',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomOut',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'p'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Pin',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'c',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'b',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||
targetSelector: '#graph-canvas'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -263,7 +263,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
options: ['en', 'zh'],
|
||||
options: ['en', 'zh', 'ru'],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
{
|
||||
@@ -435,5 +435,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: false,
|
||||
experimental: true,
|
||||
versionAdded: '1.3.11'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Validation.NodeDefs',
|
||||
name: 'Validate node definitions (slow)',
|
||||
type: 'boolean',
|
||||
tooltip:
|
||||
'Recommended for node developers. This will validate all node definitions on startup.',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.3.14'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -40,8 +40,10 @@ export const useDialogStore = defineStore('dialog', {
|
||||
}) {
|
||||
this.isVisible = true
|
||||
nextTick(() => {
|
||||
this.title = options.title
|
||||
this.headerComponent = markRaw(options.headerComponent)
|
||||
this.title = options.title ?? ''
|
||||
this.headerComponent = options.headerComponent
|
||||
? markRaw(options.headerComponent)
|
||||
: null
|
||||
this.component = markRaw(options.component)
|
||||
this.props = options.props || {}
|
||||
this.dialogComponentProps = options.dialogComponentProps || {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '../scripts/api'
|
||||
|
||||
@@ -62,6 +62,11 @@ export const useExtensionStore = defineStore('extension', () => {
|
||||
// pysssss.Locking is replaced by pin/unpin in ComfyUI core.
|
||||
// https://github.com/Comfy-Org/litegraph.js/pull/117
|
||||
disabledExtensionNames.value.add('pysssss.Locking')
|
||||
// pysssss.SnapToGrid is replaced by Comfy.Graph.AlwaysSnapToGrid in ComfyUI core.
|
||||
// pysssss.SnapToGrid tries to write global app.shiftDown state, which is no longer
|
||||
// allowed since v1.3.12.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/1176
|
||||
disabledExtensionNames.value.add('pysssss.SnapToGrid')
|
||||
}
|
||||
|
||||
// Some core extensions are registered before the store is initialized, e.g.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LGraphNode, LGraphGroup, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
export const useTitleEditorStore = defineStore('titleEditor', () => {
|
||||
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
|
||||
@@ -11,31 +11,14 @@ export const useTitleEditorStore = defineStore('titleEditor', () => {
|
||||
})
|
||||
|
||||
export const useCanvasStore = defineStore('canvas', () => {
|
||||
/**
|
||||
* The LGraphCanvas instance.
|
||||
*
|
||||
* The root LGraphCanvas object is shallow reactive.
|
||||
*/
|
||||
const canvas = shallowRef<LGraphCanvas | null>(null)
|
||||
const readOnly = ref(false)
|
||||
const draggingCanvas = ref(false)
|
||||
|
||||
document.addEventListener(
|
||||
'litegraph:canvas',
|
||||
(e: CustomEvent<{ subType: string; readOnly: boolean }>) => {
|
||||
if (e.detail?.subType === 'read-only') {
|
||||
readOnly.value = e.detail.readOnly
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'litegraph:canvas',
|
||||
(e: CustomEvent<{ subType: string; draggingCanvas: boolean }>) => {
|
||||
if (e.detail?.subType === 'dragging-canvas') {
|
||||
draggingCanvas.value = e.detail.draggingCanvas
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
canvas,
|
||||
readOnly,
|
||||
draggingCanvas
|
||||
canvas
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,16 +8,20 @@ import type { ComfyExtension } from '@/types/comfy'
|
||||
export class KeybindingImpl implements Keybinding {
|
||||
commandId: string
|
||||
combo: KeyComboImpl
|
||||
targetSelector?: string
|
||||
|
||||
constructor(obj: Keybinding) {
|
||||
this.commandId = obj.commandId
|
||||
this.combo = new KeyComboImpl(obj.combo)
|
||||
this.targetSelector = obj.targetSelector
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
if (toRaw(other) instanceof KeybindingImpl) {
|
||||
return (
|
||||
this.commandId === other.commandId && this.combo.equals(other.combo)
|
||||
this.commandId === other.commandId &&
|
||||
this.combo.equals(other.combo) &&
|
||||
this.targetSelector === other.targetSelector
|
||||
)
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -98,6 +98,27 @@ export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
registerCommands(['Edit'], ['Comfy.ClearWorkflow'])
|
||||
registerCommands(['Edit'], ['Comfy.OpenClipspace'])
|
||||
|
||||
registerMenuGroup(
|
||||
['Help'],
|
||||
[
|
||||
{
|
||||
icon: 'pi pi-github',
|
||||
label: 'ComfyUI Issues',
|
||||
url: 'https://github.com/comfyanonymous/ComfyUI/issues'
|
||||
},
|
||||
{
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'ComfyUI Docs',
|
||||
url: 'https://docs.comfy.org/'
|
||||
},
|
||||
{
|
||||
icon: 'pi pi-discord',
|
||||
label: 'Comfy-Org',
|
||||
url: 'https://www.comfy.org/discord'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
menuItems,
|
||||
registerMenuGroup,
|
||||
|
||||
@@ -19,13 +19,17 @@ function _findInMetadata(metadata: any, ...keys: string[]): string | null {
|
||||
/** Defines and holds metadata for a model */
|
||||
export class ComfyModelDef {
|
||||
/** Proper filename of the model */
|
||||
file_name: string = ''
|
||||
readonly file_name: string
|
||||
/** Normalized filename of the model, with all backslashes replaced with forward slashes */
|
||||
readonly normalized_file_name: string
|
||||
/** Directory containing the model, eg 'checkpoints' */
|
||||
directory: string = ''
|
||||
readonly directory: string
|
||||
/** Simplified copy of name, used as a default title. Excludes the directory and the '.safetensors' file extension */
|
||||
simplified_file_name: string = ''
|
||||
readonly simplified_file_name: string
|
||||
/** Key for the model, used to uniquely identify the model. */
|
||||
readonly key: string
|
||||
/** Title / display name of the model, sometimes same as the name but not always */
|
||||
title: string = ''
|
||||
title: string
|
||||
/** Metadata: architecture ID for the model, such as 'stable-diffusion-xl-v1-base' */
|
||||
architecture_id: string = ''
|
||||
/** Metadata: author of the model */
|
||||
@@ -48,10 +52,13 @@ export class ComfyModelDef {
|
||||
is_load_requested: boolean = false
|
||||
/** If true, this is a fake model object used as a placeholder for something (eg a loading icon) */
|
||||
is_fake_object: boolean = false
|
||||
/** A string full of auto-computed lowercase-only searchable text for this model */
|
||||
searchable: string = ''
|
||||
|
||||
constructor(name: string, directory: string) {
|
||||
this.file_name = name
|
||||
this.simplified_file_name = name.replaceAll('\\', '/').split('/').pop()
|
||||
this.normalized_file_name = name.replaceAll('\\', '/')
|
||||
this.simplified_file_name = this.normalized_file_name.split('/').pop() ?? ''
|
||||
if (this.simplified_file_name.endsWith('.safetensors')) {
|
||||
this.simplified_file_name = this.simplified_file_name.slice(
|
||||
0,
|
||||
@@ -60,6 +67,21 @@ export class ComfyModelDef {
|
||||
}
|
||||
this.title = this.simplified_file_name
|
||||
this.directory = directory
|
||||
this.key = `${directory}/${this.normalized_file_name}`
|
||||
this.updateSearchable()
|
||||
}
|
||||
|
||||
updateSearchable() {
|
||||
this.searchable = [
|
||||
this.file_name,
|
||||
this.title,
|
||||
this.author,
|
||||
this.description,
|
||||
this.trigger_phrase,
|
||||
this.tags.join(', ')
|
||||
]
|
||||
.join('\n')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
/** Loads the model metadata from the server, filling in this object if data is available */
|
||||
@@ -110,6 +132,7 @@ export class ComfyModelDef {
|
||||
_findInMetadata(metadata, 'modelspec.tags', 'tags') || ''
|
||||
this.tags = tagsCommaSeparated.split(',').map((tag) => tag.trim())
|
||||
this.has_loaded_metadata = true
|
||||
this.updateSearchable()
|
||||
} catch (error) {
|
||||
console.error('Error loading model metadata', this.file_name, this, error)
|
||||
}
|
||||
@@ -138,12 +161,12 @@ const folderBlacklist = ['configs', 'custom_nodes']
|
||||
/** Model store handler, wraps individual per-folder model stores */
|
||||
export const useModelStore = defineStore('modelStore', {
|
||||
state: () => ({
|
||||
modelStoreMap: {} as Record<string, ModelStore>,
|
||||
isLoading: {} as Record<string, Promise<ModelStore>>,
|
||||
modelStoreMap: {} as Record<string, ModelStore | null>,
|
||||
isLoading: {} as Record<string, Promise<ModelStore | null> | null>,
|
||||
modelFolders: [] as string[]
|
||||
}),
|
||||
actions: {
|
||||
async getModelsInFolderCached(folder: string): Promise<ModelStore> {
|
||||
async getModelsInFolderCached(folder: string): Promise<ModelStore | null> {
|
||||
if (folder in this.modelStoreMap) {
|
||||
return this.modelStoreMap[folder]
|
||||
}
|
||||
@@ -156,7 +179,7 @@ export const useModelStore = defineStore('modelStore', {
|
||||
}
|
||||
const store = new ModelStore(folder, models)
|
||||
this.modelStoreMap[folder] = store
|
||||
this.isLoading[folder] = false
|
||||
this.isLoading[folder] = null
|
||||
return store
|
||||
})
|
||||
this.isLoading[folder] = promise
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useSettingStore } from './settingStore'
|
||||
@@ -15,21 +16,24 @@ export const useNodeBookmarkStore = defineStore('nodeBookmark', () => {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
const migrateLegacyBookmarks = () => {
|
||||
settingStore
|
||||
.get('Comfy.NodeLibrary.Bookmarks')
|
||||
.forEach((bookmark: string) => {
|
||||
// If the bookmark is a folder, add it as a bookmark
|
||||
if (bookmark.endsWith('/')) {
|
||||
addBookmark(bookmark)
|
||||
return
|
||||
}
|
||||
const category = bookmark.split('/').slice(0, -1).join('/')
|
||||
const displayName = bookmark.split('/').pop()
|
||||
const nodeDef = nodeDefStore.nodeDefsByDisplayName[displayName]
|
||||
const legacyBookmarks = settingStore.get('Comfy.NodeLibrary.Bookmarks')
|
||||
if (!legacyBookmarks.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!nodeDef) return
|
||||
addBookmark(`${category === '' ? '' : category + '/'}${nodeDef.name}`)
|
||||
})
|
||||
legacyBookmarks.forEach((bookmark: string) => {
|
||||
// If the bookmark is a folder, add it as a bookmark
|
||||
if (bookmark.endsWith('/')) {
|
||||
addBookmark(bookmark)
|
||||
return
|
||||
}
|
||||
const category = bookmark.split('/').slice(0, -1).join('/')
|
||||
const displayName = bookmark.split('/').pop()
|
||||
const nodeDef = nodeDefStore.nodeDefsByDisplayName[displayName]
|
||||
|
||||
if (!nodeDef) return
|
||||
addBookmark(`${category === '' ? '' : category + '/'}${nodeDef.name}`)
|
||||
})
|
||||
settingStore.set('Comfy.NodeLibrary.Bookmarks', [])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
NodeSearchService,
|
||||
type SearchAuxScore
|
||||
} from '@/services/nodeSearchService'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import {
|
||||
type ComfyNodeDef,
|
||||
type ComfyInputsSpec as ComfyInputsSpecSchema
|
||||
} from '@/types/apiTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Type, Transform, plainToClass, Expose } from 'class-transformer'
|
||||
import { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import { TreeNode } from 'primevue/treenode'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
@@ -12,87 +15,60 @@ import { computed, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { type NodeSource, getNodeSource } from '@/types/nodeSource'
|
||||
|
||||
export class BaseInputSpec<T = any> {
|
||||
export interface BaseInputSpec<T = any> {
|
||||
name: string
|
||||
type: string
|
||||
tooltip?: string
|
||||
default?: T
|
||||
|
||||
@Type(() => Boolean)
|
||||
forceInput?: boolean
|
||||
|
||||
static isInputSpec(obj: any): boolean {
|
||||
return (
|
||||
Array.isArray(obj) &&
|
||||
obj.length >= 1 &&
|
||||
(typeof obj[0] === 'string' || Array.isArray(obj[0]))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class NumericInputSpec extends BaseInputSpec<number> {
|
||||
@Type(() => Number)
|
||||
export interface NumericInputSpec extends BaseInputSpec<number> {
|
||||
min?: number
|
||||
|
||||
@Type(() => Number)
|
||||
max?: number
|
||||
|
||||
@Type(() => Number)
|
||||
step?: number
|
||||
}
|
||||
|
||||
export class IntInputSpec extends NumericInputSpec {
|
||||
type: 'INT' = 'INT'
|
||||
export interface IntInputSpec extends NumericInputSpec {
|
||||
type: 'INT'
|
||||
}
|
||||
|
||||
export class FloatInputSpec extends NumericInputSpec {
|
||||
type: 'FLOAT' = 'FLOAT'
|
||||
|
||||
@Type(() => Number)
|
||||
export interface FloatInputSpec extends NumericInputSpec {
|
||||
type: 'FLOAT'
|
||||
round?: number
|
||||
}
|
||||
|
||||
export class BooleanInputSpec extends BaseInputSpec<boolean> {
|
||||
type: 'BOOLEAN' = 'BOOLEAN'
|
||||
|
||||
export interface BooleanInputSpec extends BaseInputSpec<boolean> {
|
||||
type: 'BOOLEAN'
|
||||
labelOn?: string
|
||||
labelOff?: string
|
||||
}
|
||||
|
||||
export class StringInputSpec extends BaseInputSpec<string> {
|
||||
type: 'STRING' = 'STRING'
|
||||
|
||||
@Type(() => Boolean)
|
||||
export interface StringInputSpec extends BaseInputSpec<string> {
|
||||
type: 'STRING'
|
||||
multiline?: boolean
|
||||
|
||||
@Type(() => Boolean)
|
||||
dynamicPrompts?: boolean
|
||||
}
|
||||
|
||||
export class ComboInputSpec extends BaseInputSpec<any> {
|
||||
type: string = 'COMBO'
|
||||
|
||||
@Transform(({ value }) => value[0])
|
||||
export interface ComboInputSpec extends BaseInputSpec<any> {
|
||||
type: 'COMBO'
|
||||
comboOptions: any[]
|
||||
|
||||
@Type(() => Boolean)
|
||||
controlAfterGenerate?: boolean
|
||||
|
||||
@Type(() => Boolean)
|
||||
imageUpload?: boolean
|
||||
}
|
||||
|
||||
export class CustomInputSpec extends BaseInputSpec {}
|
||||
|
||||
export class ComfyInputsSpec {
|
||||
@Transform(({ value }) => ComfyInputsSpec.transformInputSpecRecord(value))
|
||||
required: Record<string, BaseInputSpec> = {}
|
||||
|
||||
@Transform(({ value }) => ComfyInputsSpec.transformInputSpecRecord(value))
|
||||
optional: Record<string, BaseInputSpec> = {}
|
||||
|
||||
required: Record<string, BaseInputSpec>
|
||||
optional: Record<string, BaseInputSpec>
|
||||
hidden?: Record<string, any>
|
||||
|
||||
constructor(obj: ComfyInputsSpecSchema) {
|
||||
this.required = ComfyInputsSpec.transformInputSpecRecord(obj.required) ?? {}
|
||||
this.optional = ComfyInputsSpec.transformInputSpecRecord(obj.optional) ?? {}
|
||||
this.hidden = obj.hidden
|
||||
}
|
||||
|
||||
private static transformInputSpecRecord(
|
||||
record: Record<string, any>
|
||||
): Record<string, BaseInputSpec> {
|
||||
@@ -104,35 +80,39 @@ export class ComfyInputsSpec {
|
||||
return result
|
||||
}
|
||||
|
||||
private static isInputSpec(obj: any): boolean {
|
||||
return (
|
||||
Array.isArray(obj) &&
|
||||
obj.length >= 1 &&
|
||||
(typeof obj[0] === 'string' || Array.isArray(obj[0]))
|
||||
)
|
||||
}
|
||||
|
||||
private static transformSingleInputSpec(
|
||||
name: string,
|
||||
value: any
|
||||
): BaseInputSpec {
|
||||
if (!BaseInputSpec.isInputSpec(value)) return value
|
||||
if (!ComfyInputsSpec.isInputSpec(value)) return value
|
||||
|
||||
const [typeRaw, _spec] = value
|
||||
const spec = _spec ?? {}
|
||||
const type = Array.isArray(typeRaw) ? 'COMBO' : value[0]
|
||||
|
||||
switch (type) {
|
||||
case 'INT':
|
||||
return plainToClass(IntInputSpec, { name, type, ...spec })
|
||||
case 'FLOAT':
|
||||
return plainToClass(FloatInputSpec, { name, type, ...spec })
|
||||
case 'BOOLEAN':
|
||||
return plainToClass(BooleanInputSpec, { name, type, ...spec })
|
||||
case 'STRING':
|
||||
return plainToClass(StringInputSpec, { name, type, ...spec })
|
||||
case 'COMBO':
|
||||
return plainToClass(ComboInputSpec, {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
...spec,
|
||||
comboOptions: typeRaw,
|
||||
default: spec.default ?? typeRaw[0]
|
||||
})
|
||||
} as ComboInputSpec
|
||||
case 'INT':
|
||||
case 'FLOAT':
|
||||
case 'BOOLEAN':
|
||||
case 'STRING':
|
||||
default:
|
||||
return plainToClass(CustomInputSpec, { name, type, ...spec })
|
||||
return { name, type, ...spec } as BaseInputSpec
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,52 +145,46 @@ export class ComfyOutputsSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This class does not implement the ComfyNodeDef interface, as we are
|
||||
* using a custom output spec for output definitions.
|
||||
*/
|
||||
export class ComfyNodeDefImpl {
|
||||
name: string
|
||||
display_name: string
|
||||
category: string
|
||||
python_module: string
|
||||
description: string
|
||||
|
||||
@Transform(({ value, obj }) => value ?? obj.category === '', {
|
||||
toClassOnly: true
|
||||
})
|
||||
@Type(() => Boolean)
|
||||
@Expose()
|
||||
deprecated: boolean
|
||||
|
||||
@Transform(
|
||||
({ value, obj }) => value ?? obj.category.startsWith('_for_testing'),
|
||||
{
|
||||
toClassOnly: true
|
||||
}
|
||||
)
|
||||
@Type(() => Boolean)
|
||||
@Expose()
|
||||
experimental: boolean
|
||||
|
||||
@Type(() => ComfyInputsSpec)
|
||||
input: ComfyInputsSpec
|
||||
|
||||
@Transform(({ obj }) => ComfyNodeDefImpl.transformOutputSpec(obj))
|
||||
output: ComfyOutputsSpec
|
||||
|
||||
@Transform(({ obj }) => getNodeSource(obj.python_module), {
|
||||
toClassOnly: true
|
||||
})
|
||||
@Expose()
|
||||
nodeSource: NodeSource
|
||||
|
||||
constructor(obj: ComfyNodeDef) {
|
||||
this.name = obj.name
|
||||
this.display_name = obj.display_name
|
||||
this.category = obj.category
|
||||
this.python_module = obj.python_module
|
||||
this.description = obj.description
|
||||
this.deprecated = obj.deprecated ?? obj.category === ''
|
||||
this.experimental =
|
||||
obj.experimental ?? obj.category.startsWith('_for_testing')
|
||||
this.input = new ComfyInputsSpec(obj.input ?? {})
|
||||
this.output = ComfyNodeDefImpl.transformOutputSpec(obj)
|
||||
this.nodeSource = getNodeSource(obj.python_module)
|
||||
}
|
||||
|
||||
private static transformOutputSpec(obj: any): ComfyOutputsSpec {
|
||||
const { output, output_is_list, output_name, output_tooltips } = obj
|
||||
const result = output.map((type: string | any[], index: number) => {
|
||||
const result = (output ?? []).map((type: string | any[], index: number) => {
|
||||
const typeString = Array.isArray(type) ? 'COMBO' : type
|
||||
|
||||
return new ComfyOutputSpec(
|
||||
index,
|
||||
output_name[index],
|
||||
output_name?.[index],
|
||||
typeString,
|
||||
output_is_list[index],
|
||||
output_is_list?.[index],
|
||||
Array.isArray(type) ? type : undefined,
|
||||
output_tooltips?.[index]
|
||||
)
|
||||
@@ -276,13 +250,17 @@ export function buildNodeDefTree(nodeDefs: ComfyNodeDefImpl[]): TreeNode {
|
||||
}
|
||||
|
||||
export function createDummyFolderNodeDef(folderPath: string): ComfyNodeDefImpl {
|
||||
return plainToClass(ComfyNodeDefImpl, {
|
||||
return new ComfyNodeDefImpl({
|
||||
name: '',
|
||||
display_name: '',
|
||||
category: folderPath.endsWith('/') ? folderPath.slice(0, -1) : folderPath,
|
||||
python_module: 'nodes',
|
||||
description: 'Dummy Folder Node (User should never see this string)'
|
||||
})
|
||||
description: 'Dummy Folder Node (User should never see this string)',
|
||||
input: {},
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: []
|
||||
} as ComfyNodeDef)
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -325,7 +303,7 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
const newNodeDefsByName: { [key: string]: ComfyNodeDefImpl } = {}
|
||||
const nodeDefsByDisplayName: { [key: string]: ComfyNodeDefImpl } = {}
|
||||
for (const nodeDef of nodeDefs) {
|
||||
const nodeDefImpl = plainToClass(ComfyNodeDefImpl, nodeDef)
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
newNodeDefsByName[nodeDef.name] = nodeDefImpl
|
||||
nodeDefsByDisplayName[nodeDef.display_name] = nodeDefImpl
|
||||
}
|
||||
@@ -333,7 +311,7 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
this.nodeDefsByDisplayName = nodeDefsByDisplayName
|
||||
},
|
||||
addNodeDef(nodeDef: ComfyNodeDef) {
|
||||
const nodeDefImpl = plainToClass(ComfyNodeDefImpl, nodeDef)
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
this.nodeDefsByName[nodeDef.name] = nodeDefImpl
|
||||
this.nodeDefsByDisplayName[nodeDef.display_name] = nodeDefImpl
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import type {
|
||||
TaskItem,
|
||||
TaskType,
|
||||
@@ -9,8 +10,7 @@ import type {
|
||||
TaskOutput,
|
||||
ResultItem
|
||||
} from '@/types/apiTypes'
|
||||
import type { NodeId } from '@/types/comfyWorkflow'
|
||||
import { plainToClass } from 'class-transformer'
|
||||
import type { ComfyWorkflowJSON, NodeId } from '@/types/comfyWorkflow'
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { toRaw } from 'vue'
|
||||
@@ -35,17 +35,92 @@ export class ResultItemImpl {
|
||||
// 'audio' | 'images' | ...
|
||||
mediaType: string
|
||||
|
||||
// VHS output specific fields
|
||||
format?: string
|
||||
frame_rate?: number
|
||||
|
||||
constructor(obj: Record<string, any>) {
|
||||
this.filename = obj.filename
|
||||
this.subfolder = obj.subfolder
|
||||
this.type = obj.type
|
||||
|
||||
this.nodeId = obj.nodeId
|
||||
this.mediaType = obj.mediaType
|
||||
|
||||
this.format = obj.format
|
||||
this.frame_rate = obj.frame_rate
|
||||
}
|
||||
|
||||
private get urlParams(): URLSearchParams {
|
||||
const params = new URLSearchParams()
|
||||
params.set('filename', this.filename)
|
||||
params.set('type', this.type)
|
||||
params.set('subfolder', this.subfolder || '')
|
||||
|
||||
if (this.format) {
|
||||
params.set('format', this.format)
|
||||
}
|
||||
if (this.frame_rate) {
|
||||
params.set('frame_rate', this.frame_rate.toString())
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* VHS advanced preview URL. `/viewvideo` endpoint is provided by VHS node.
|
||||
*
|
||||
* `/viewvideo` always returns a webm file.
|
||||
*/
|
||||
get vhsAdvancedPreviewUrl(): string {
|
||||
return api.apiURL('/viewvideo?' + this.urlParams)
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return api.apiURL(`/view?filename=${encodeURIComponent(this.filename)}&type=${this.type}&
|
||||
subfolder=${encodeURIComponent(this.subfolder || '')}`)
|
||||
return api.apiURL('/view?' + this.urlParams)
|
||||
}
|
||||
|
||||
get urlWithTimestamp(): string {
|
||||
return `${this.url}&t=${+new Date()}`
|
||||
}
|
||||
|
||||
get isVhsFormat(): boolean {
|
||||
return !!this.format && !!this.frame_rate
|
||||
}
|
||||
|
||||
get htmlVideoType(): string | undefined {
|
||||
const defaultType = undefined
|
||||
|
||||
if (!this.isVhsFormat) {
|
||||
return defaultType
|
||||
}
|
||||
|
||||
if (this.format.endsWith('webm')) {
|
||||
return 'video/webm'
|
||||
}
|
||||
if (this.format.endsWith('mp4')) {
|
||||
return 'video/mp4'
|
||||
}
|
||||
return defaultType
|
||||
}
|
||||
|
||||
get isVideo(): boolean {
|
||||
return !this.isImage && this.format && this.format.startsWith('video/')
|
||||
}
|
||||
|
||||
get isGif(): boolean {
|
||||
return this.filename.endsWith('.gif')
|
||||
}
|
||||
|
||||
get isWebp(): boolean {
|
||||
return this.filename.endsWith('.webp')
|
||||
}
|
||||
|
||||
get isImage(): boolean {
|
||||
return this.mediaType === 'images' || this.isGif || this.isWebp
|
||||
}
|
||||
|
||||
get supportsPreview(): boolean {
|
||||
return ['images', 'gifs'].includes(this.mediaType)
|
||||
return this.isImage || this.isVideo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +151,13 @@ export class TaskItemImpl {
|
||||
}
|
||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as ResultItem[]).map((item: ResultItem) =>
|
||||
plainToClass(ResultItemImpl, {
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
(items as ResultItem[]).map(
|
||||
(item: ResultItem) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -138,8 +214,8 @@ export class TaskItemImpl {
|
||||
return this.extraData.client_id
|
||||
}
|
||||
|
||||
get workflow() {
|
||||
return this.extraPngInfo.workflow
|
||||
get workflow(): ComfyWorkflowJSON | undefined {
|
||||
return this.extraPngInfo?.workflow
|
||||
}
|
||||
|
||||
get messages() {
|
||||
@@ -213,7 +289,10 @@ export class TaskItemImpl {
|
||||
: undefined
|
||||
}
|
||||
|
||||
public async loadWorkflow() {
|
||||
public async loadWorkflow(app: ComfyApp) {
|
||||
if (!this.workflow) {
|
||||
return
|
||||
}
|
||||
await app.loadGraphData(toRaw(this.workflow))
|
||||
if (this.outputs) {
|
||||
app.nodeOutputs = toRaw(this.outputs)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
/**
|
||||
* TODO: Migrate scripts/ui/settings.ts here
|
||||
*
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useModelLibrarySidebarTab } from '@/hooks/sidebarTabs/modelLibrarySidebarTab'
|
||||
import { useNodeLibrarySidebarTab } from '@/hooks/sidebarTabs/nodeLibrarySidebarTab'
|
||||
import { useQueueSidebarTab } from '@/hooks/sidebarTabs/queueSidebarTab'
|
||||
import { useWorkflowsSidebarTab } from '@/hooks/sidebarTabs/workflowsSidebarTab'
|
||||
import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
const sidebarTabs = ref<SidebarTabExtension[]>([])
|
||||
@@ -19,6 +24,16 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
|
||||
const registerSidebarTab = (tab: SidebarTabExtension) => {
|
||||
sidebarTabs.value = [...sidebarTabs.value, tab]
|
||||
useCommandStore().registerCommand({
|
||||
id: `Workspace.ToggleSidebarTab.${tab.id}`,
|
||||
icon: tab.icon,
|
||||
label: tab.tooltip,
|
||||
tooltip: tab.tooltip,
|
||||
versionAdded: '1.3.9',
|
||||
function: () => {
|
||||
toggleSidebarTab(tab.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unregisterSidebarTab = (id: string) => {
|
||||
@@ -34,12 +49,23 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the core sidebar tabs.
|
||||
*/
|
||||
const registerCoreSidebarTabs = () => {
|
||||
registerSidebarTab(useQueueSidebarTab())
|
||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||
registerSidebarTab(useModelLibrarySidebarTab())
|
||||
registerSidebarTab(useWorkflowsSidebarTab())
|
||||
}
|
||||
|
||||
return {
|
||||
sidebarTabs,
|
||||
activeSidebarTabId,
|
||||
activeSidebarTab,
|
||||
toggleSidebarTab,
|
||||
registerSidebarTab,
|
||||
unregisterSidebarTab
|
||||
unregisterSidebarTab,
|
||||
registerCoreSidebarTabs
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,11 +7,14 @@ import { useSidebarTabStore } from './workspace/sidebarTabStore'
|
||||
|
||||
interface WorkspaceState {
|
||||
spinner: boolean
|
||||
// Whether the shift key is down globally
|
||||
shiftDown: boolean
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = defineStore('workspace', {
|
||||
state: (): WorkspaceState => ({
|
||||
spinner: false
|
||||
spinner: false,
|
||||
shiftDown: false
|
||||
}),
|
||||
getters: {
|
||||
toast(): ToastManager {
|
||||
@@ -32,16 +35,6 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
actions: {
|
||||
registerSidebarTab(tab: SidebarTabExtension) {
|
||||
this.sidebarTab.registerSidebarTab(tab)
|
||||
useCommandStore().registerCommand({
|
||||
id: `Workspace.ToggleSidebarTab.${tab.id}`,
|
||||
icon: tab.icon,
|
||||
label: tab.tooltip,
|
||||
tooltip: tab.tooltip,
|
||||
versionAdded: '1.3.9',
|
||||
function: () => {
|
||||
this.sidebarTab.toggleSidebarTab(tab.id)
|
||||
}
|
||||
})
|
||||
},
|
||||
unregisterSidebarTab(id: string) {
|
||||
this.sidebarTab.unregisterSidebarTab(id)
|
||||
|
||||
@@ -116,7 +116,8 @@ const zExtraPngInfo = z
|
||||
.passthrough()
|
||||
|
||||
const zExtraData = z.object({
|
||||
extra_pnginfo: zExtraPngInfo,
|
||||
/** extra_pnginfo can be missing is backend execution gets a validation error. */
|
||||
extra_pnginfo: zExtraPngInfo.optional(),
|
||||
client_id: z.string()
|
||||
})
|
||||
const zOutputsToExecute = z.array(zNodeId)
|
||||
@@ -346,10 +347,10 @@ const zComfyOutputTypesSpec = z.array(
|
||||
)
|
||||
|
||||
const zComfyNodeDef = z.object({
|
||||
input: zComfyInputsSpec,
|
||||
output: zComfyOutputTypesSpec,
|
||||
output_is_list: z.array(z.boolean()),
|
||||
output_name: z.array(z.string()),
|
||||
input: zComfyInputsSpec.optional(),
|
||||
output: zComfyOutputTypesSpec.optional(),
|
||||
output_is_list: z.array(z.boolean()).optional(),
|
||||
output_name: z.array(z.string()).optional(),
|
||||
output_tooltips: z.array(z.string()).optional(),
|
||||
name: z.string(),
|
||||
display_name: z.string(),
|
||||
|
||||
@@ -12,7 +12,12 @@ export const zKeyCombo = z.object({
|
||||
// Keybinding schema
|
||||
export const zKeybinding = z.object({
|
||||
commandId: z.string(),
|
||||
combo: zKeyCombo
|
||||
combo: zKeyCombo,
|
||||
// Optional target element CSS selector to limit keybinding to.
|
||||
// Note: Currently only used to distinguish between global keybindings
|
||||
// and litegraph canvas keybindings.
|
||||
// Do NOT use this field in extensions as it has no effect.
|
||||
targetSelector: z.string().optional()
|
||||
})
|
||||
|
||||
// Infer types from schemas
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type {
|
||||
ConnectingLink,
|
||||
LGraphNode,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user