mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Move workflow/widgets into component
This commit is contained in:
245
src/renderer/extensions/linearMode/LinearWorkflow.vue
Normal file
245
src/renderer/extensions/linearMode/LinearWorkflow.vue
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useEventListener, useTimeout } from '@vueuse/core'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
|
import { computed, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { safeWidgetMapper } from '@/composables/graph/useGraphNodeManager'
|
||||||
|
import { t } from '@/i18n'
|
||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||||
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
||||||
|
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||||
|
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
|
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||||
|
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
const executionStore = useExecutionStore()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { isActiveSubscription } = useSubscription()
|
||||||
|
|
||||||
|
const showNoteData = ref(false)
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
toastTo: string | HTMLElement
|
||||||
|
notesTo: string | HTMLElement
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const jobFinishedQueue = ref(true)
|
||||||
|
const {
|
||||||
|
ready: jobToastTimeout,
|
||||||
|
start: resetJobToastTimeout,
|
||||||
|
stop: stopJobTimeout
|
||||||
|
} = useTimeout(5000, { controls: true })
|
||||||
|
stopJobTimeout()
|
||||||
|
|
||||||
|
const graphNodes = shallowRef<LGraphNode[]>(app.rootGraph.nodes)
|
||||||
|
useEventListener(
|
||||||
|
app.rootGraph.events,
|
||||||
|
'configured',
|
||||||
|
() => (graphNodes.value = app.rootGraph.nodes)
|
||||||
|
)
|
||||||
|
|
||||||
|
function nodeToNodeData(node: LGraphNode) {
|
||||||
|
const mapper = safeWidgetMapper(node, new Map())
|
||||||
|
const widgets = node.widgets?.map(mapper) ?? []
|
||||||
|
const dropIndicator =
|
||||||
|
node.type !== 'LoadImage'
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
iconClass: 'icon-[lucide--image]',
|
||||||
|
label: t('linearMode.dragAndDropImage')
|
||||||
|
}
|
||||||
|
//of VueNodeData, only widgets is actually used
|
||||||
|
return {
|
||||||
|
executing: false,
|
||||||
|
id: `${node.id}`,
|
||||||
|
mode: 0,
|
||||||
|
selected: false,
|
||||||
|
title: node.title,
|
||||||
|
type: node.type,
|
||||||
|
widgets,
|
||||||
|
|
||||||
|
dropIndicator,
|
||||||
|
onDragDrop: node.onDragDrop,
|
||||||
|
onDragOver: node.onDragOver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeDatas = computed(() => {
|
||||||
|
return graphNodes.value
|
||||||
|
.filter(
|
||||||
|
(node) =>
|
||||||
|
node.mode === 0 &&
|
||||||
|
node.widgets?.length &&
|
||||||
|
!['MarkdownNote', 'Note'].includes(node.type)
|
||||||
|
)
|
||||||
|
.map(nodeToNodeData)
|
||||||
|
.reverse()
|
||||||
|
})
|
||||||
|
const noteDatas = computed(() => {
|
||||||
|
return graphNodes.value
|
||||||
|
.filter(
|
||||||
|
(node) => node.mode === 0 && ['MarkdownNote', 'Note'].includes(node.type)
|
||||||
|
)
|
||||||
|
.map(nodeToNodeData)
|
||||||
|
})
|
||||||
|
|
||||||
|
const batchCountWidget = {
|
||||||
|
options: { precision: 0, min: 1, max: 99 },
|
||||||
|
value: 1,
|
||||||
|
name: t('linearMode.runCount'),
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { batchCount } = storeToRefs(useQueueSettingsStore())
|
||||||
|
|
||||||
|
//TODO: refactor out of this file.
|
||||||
|
//code length is small, but changes should propagate
|
||||||
|
async function runButtonClick(e: Event) {
|
||||||
|
if (!jobFinishedQueue.value) return
|
||||||
|
try {
|
||||||
|
jobFinishedQueue.value = false
|
||||||
|
resetJobToastTimeout()
|
||||||
|
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
||||||
|
const commandId = isShiftPressed
|
||||||
|
? 'Comfy.QueuePromptFront'
|
||||||
|
: 'Comfy.QueuePrompt'
|
||||||
|
|
||||||
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
|
button_id: 'queue_run_linear'
|
||||||
|
})
|
||||||
|
if (batchCount.value > 1) {
|
||||||
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
|
button_id: 'queue_run_multiple_batches_submitted'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await commandStore.execute(commandId, {
|
||||||
|
metadata: {
|
||||||
|
subscribe_to_run: false,
|
||||||
|
trigger_source: 'button'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
//TODO: Error state indicator for failed queue?
|
||||||
|
jobFinishedQueue.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ runButtonClick })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-w-80 h-full">
|
||||||
|
<linear-workflow-info
|
||||||
|
class="h-12 border-x border-border-subtle py-2 px-4 gap-2 bg-comfy-menu-bg flex items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-bold truncate min-w-30"
|
||||||
|
v-text="workflowStore.activeWorkflow?.filename"
|
||||||
|
/>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<Button
|
||||||
|
v-if="noteDatas.length"
|
||||||
|
variant="muted-textonly"
|
||||||
|
@click="showNoteData = !showNoteData"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--info]" />
|
||||||
|
</Button>
|
||||||
|
<Button v-if="false"> {{ t('menuLabels.publish') }} </Button>
|
||||||
|
</linear-workflow-info>
|
||||||
|
<div
|
||||||
|
class="border gap-2 h-full border-[var(--interface-stroke)] bg-comfy-menu-bg flex flex-col px-2"
|
||||||
|
>
|
||||||
|
<linear-widgets class="grow-1 overflow-y-auto contain-size">
|
||||||
|
<template v-for="(nodeData, index) of nodeDatas" :key="nodeData.id">
|
||||||
|
<div
|
||||||
|
v-if="index !== 0"
|
||||||
|
class="w-full border-t-1 border-node-component-border"
|
||||||
|
/>
|
||||||
|
<DropZone
|
||||||
|
:on-drag-over="nodeData.onDragOver"
|
||||||
|
:on-drag-drop="nodeData.onDragDrop"
|
||||||
|
:drop-indicator="nodeData.dropIndicator"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
|
<NodeWidgets
|
||||||
|
:node-data
|
||||||
|
class="py-3 gap-y-4 **:[.col-span-2]:grid-cols-1 text-sm **:[.p-floatlabel]:h-35"
|
||||||
|
/>
|
||||||
|
</DropZone>
|
||||||
|
</template>
|
||||||
|
</linear-widgets>
|
||||||
|
<linear-run-button class="p-4 pb-6 border-t border-node-component-border">
|
||||||
|
<WidgetInputNumberInput
|
||||||
|
v-model="batchCount"
|
||||||
|
:widget="batchCountWidget"
|
||||||
|
class="*:[.min-w-0]:w-24 grid-cols-[auto_96px]!"
|
||||||
|
/>
|
||||||
|
<SubscribeToRunButton
|
||||||
|
v-if="!isActiveSubscription"
|
||||||
|
class="w-full mt-4"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex mt-4 gap-2">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
class="grow-1"
|
||||||
|
size="lg"
|
||||||
|
@click="runButtonClick"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--play]" />
|
||||||
|
{{ t('menu.run') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="!executionStore.isIdle"
|
||||||
|
variant="destructive"
|
||||||
|
size="lg"
|
||||||
|
class="w-10 p-2"
|
||||||
|
@click="commandStore.execute('Comfy.Interrupt')"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--x]" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</linear-run-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<teleport v-if="!jobToastTimeout || !jobFinishedQueue" defer :to="toastTo">
|
||||||
|
<div
|
||||||
|
class="bg-base-foreground text-base-background rounded-sm flex h-8 p-1 pr-2 gap-2 items-center"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="jobFinishedQueue"
|
||||||
|
class="icon-[lucide--check] size-5 bg-success-background"
|
||||||
|
/>
|
||||||
|
<ProgressSpinner v-else class="size-4" />
|
||||||
|
<span v-text="t('queue.jobAddedToQueue')" />
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
<teleport v-if="showNoteData" defer :to="notesTo">
|
||||||
|
<div
|
||||||
|
class="bg-base-background text-muted-foreground flex flex-col w-90 gap-2 rounded-2xl border-1 border-border-subtle py-3"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="muted-textonly"
|
||||||
|
size="icon"
|
||||||
|
class="self-end mr-3"
|
||||||
|
@click="showNoteData = false"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--x]" />
|
||||||
|
</Button>
|
||||||
|
<template v-for="nodeData in noteDatas" :key="nodeData.id">
|
||||||
|
<div class="w-full border-t border-border-subtle" />
|
||||||
|
<NodeWidgets
|
||||||
|
:node-data
|
||||||
|
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
@@ -43,15 +43,6 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
defineExpose({ onWheel })
|
defineExpose({ onWheel })
|
||||||
|
|
||||||
/*
|
|
||||||
Should be sole holder of all output state
|
|
||||||
Should emit selected item
|
|
||||||
Should take location of scroll to top as prop
|
|
||||||
|
|
||||||
Need way to expose handlewheel to parent
|
|
||||||
- expose is least bad option
|
|
||||||
*/
|
|
||||||
|
|
||||||
const selectedIndex = ref<[number, number]>([0, 0])
|
const selectedIndex = ref<[number, number]>([0, 0])
|
||||||
|
|
||||||
watch(selectedIndex, () => {
|
watch(selectedIndex, () => {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener, useTimeout, whenever } from '@vueuse/core'
|
import { whenever } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
|
||||||
import Splitter from 'primevue/splitter'
|
import Splitter from 'primevue/splitter'
|
||||||
import SplitterPanel from 'primevue/splitterpanel'
|
import SplitterPanel from 'primevue/splitterpanel'
|
||||||
import { computed, ref, shallowRef, useTemplateRef } from 'vue'
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import { downloadFile } from '@/base/common/downloadUtil'
|
import { downloadFile } from '@/base/common/downloadUtil'
|
||||||
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||||
@@ -12,19 +10,14 @@ import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
|||||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
import Popover from '@/components/ui/Popover.vue'
|
import Popover from '@/components/ui/Popover.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { safeWidgetMapper } from '@/composables/graph/useGraphNodeManager'
|
|
||||||
import { d, t } from '@/i18n'
|
import { d, t } from '@/i18n'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
|
||||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
|
||||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
|
||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
|
||||||
import ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'
|
import ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'
|
||||||
|
import LinearWorkflow from '@/renderer/extensions/linearMode/LinearWorkflow.vue'
|
||||||
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
|
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
|
||||||
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
|
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
|
||||||
import {
|
import {
|
||||||
@@ -32,159 +25,29 @@ import {
|
|||||||
mediaTypes
|
mediaTypes
|
||||||
} from '@/renderer/extensions/linearMode/mediaTypes'
|
} from '@/renderer/extensions/linearMode/mediaTypes'
|
||||||
import type { StatItem } from '@/renderer/extensions/linearMode/mediaTypes'
|
import type { StatItem } from '@/renderer/extensions/linearMode/mediaTypes'
|
||||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
|
||||||
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
|
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
|
||||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
|
||||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||||
import { executeWidgetsCallback } from '@/utils/litegraphUtil'
|
import { executeWidgetsCallback } from '@/utils/litegraphUtil'
|
||||||
|
|
||||||
const commandStore = useCommandStore()
|
|
||||||
const executionStore = useExecutionStore()
|
|
||||||
const mediaActions = useMediaAssetActions()
|
const mediaActions = useMediaAssetActions()
|
||||||
const nodeOutputStore = useNodeOutputStore()
|
const nodeOutputStore = useNodeOutputStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const { isActiveSubscription } = useSubscription()
|
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
|
|
||||||
const showNoteData = ref(false)
|
|
||||||
const hasPreview = ref(false)
|
const hasPreview = ref(false)
|
||||||
whenever(
|
whenever(
|
||||||
() => nodeOutputStore.latestPreview[0],
|
() => nodeOutputStore.latestPreview[0],
|
||||||
() => (hasPreview.value = true)
|
() => (hasPreview.value = true)
|
||||||
)
|
)
|
||||||
|
|
||||||
const graphNodes = shallowRef<LGraphNode[]>(app.rootGraph.nodes)
|
|
||||||
useEventListener(
|
|
||||||
app.rootGraph.events,
|
|
||||||
'configured',
|
|
||||||
() => (graphNodes.value = app.rootGraph.nodes)
|
|
||||||
)
|
|
||||||
|
|
||||||
function nodeToNodeData(node: LGraphNode) {
|
|
||||||
const mapper = safeWidgetMapper(node, new Map())
|
|
||||||
const widgets = node.widgets?.map(mapper) ?? []
|
|
||||||
const dropIndicator =
|
|
||||||
node.type !== 'LoadImage'
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
iconClass: 'icon-[lucide--image]',
|
|
||||||
label: t('linearMode.dragAndDropImage')
|
|
||||||
}
|
|
||||||
//of VueNodeData, only widgets is actually used
|
|
||||||
return {
|
|
||||||
executing: false,
|
|
||||||
id: `${node.id}`,
|
|
||||||
mode: 0,
|
|
||||||
selected: false,
|
|
||||||
title: node.title,
|
|
||||||
type: node.type,
|
|
||||||
widgets,
|
|
||||||
|
|
||||||
dropIndicator,
|
|
||||||
onDragDrop: node.onDragDrop,
|
|
||||||
onDragOver: node.onDragOver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeDatas = computed(() => {
|
|
||||||
return graphNodes.value
|
|
||||||
.filter(
|
|
||||||
(node) =>
|
|
||||||
node.mode === 0 &&
|
|
||||||
node.widgets?.length &&
|
|
||||||
!['MarkdownNote', 'Note'].includes(node.type)
|
|
||||||
)
|
|
||||||
.map(nodeToNodeData)
|
|
||||||
.reverse()
|
|
||||||
})
|
|
||||||
const noteDatas = computed(() => {
|
|
||||||
return graphNodes.value
|
|
||||||
.filter(
|
|
||||||
(node) => node.mode === 0 && ['MarkdownNote', 'Note'].includes(node.type)
|
|
||||||
)
|
|
||||||
.map(nodeToNodeData)
|
|
||||||
})
|
|
||||||
|
|
||||||
const batchCountWidget = {
|
|
||||||
options: { precision: 0, min: 1, max: 99 },
|
|
||||||
value: 1,
|
|
||||||
name: t('linearMode.runCount'),
|
|
||||||
type: 'number'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { batchCount } = storeToRefs(useQueueSettingsStore())
|
|
||||||
|
|
||||||
//TODO: refactor out of this file.
|
|
||||||
//code length is small, but changes should propagate
|
|
||||||
async function runButtonClick(e: Event) {
|
|
||||||
if (!jobFinishedQueue.value) return
|
|
||||||
try {
|
|
||||||
jobFinishedQueue.value = false
|
|
||||||
resetJobToastTimeout()
|
|
||||||
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
|
||||||
const commandId = isShiftPressed
|
|
||||||
? 'Comfy.QueuePromptFront'
|
|
||||||
: 'Comfy.QueuePrompt'
|
|
||||||
|
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
|
||||||
button_id: 'queue_run_linear'
|
|
||||||
})
|
|
||||||
if (batchCount.value > 1) {
|
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
|
||||||
button_id: 'queue_run_multiple_batches_submitted'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await commandStore.execute(commandId, {
|
|
||||||
metadata: {
|
|
||||||
subscribe_to_run: false,
|
|
||||||
trigger_source: 'button'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
//TODO: Error state indicator for failed queue?
|
|
||||||
jobFinishedQueue.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobFinishedQueue = ref(true)
|
|
||||||
const {
|
|
||||||
ready: jobToastTimeout,
|
|
||||||
start: resetJobToastTimeout,
|
|
||||||
stop: stopJobTimeout
|
|
||||||
} = useTimeout(5000, { controls: true })
|
|
||||||
stopJobTimeout()
|
|
||||||
|
|
||||||
function loadWorkflow(item: AssetItem | undefined, index: [number, number]) {
|
|
||||||
const workflow = getOutputAssetMetadata(item?.user_metadata)?.workflow
|
|
||||||
if (!workflow) return
|
|
||||||
selectedIndex.value = index
|
|
||||||
if (workflow.id !== app.rootGraph.id) return app.loadGraphData(workflow)
|
|
||||||
//update graph to new version, set old to top of undo queue
|
|
||||||
const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
|
|
||||||
if (!changeTracker) return app.loadGraphData(workflow)
|
|
||||||
changeTracker.redoQueue = []
|
|
||||||
changeTracker.updateState([workflow], changeTracker.undoQueue)
|
|
||||||
}
|
|
||||||
async function rerun(e: Event) {
|
|
||||||
loadWorkflow(selectedItem.value, selectedIndex.value)
|
|
||||||
//FIXME don't use timeouts here
|
|
||||||
//Currently seeds fail to properly update even with timeouts?
|
|
||||||
await new Promise((r) => setTimeout(r, 500))
|
|
||||||
executeWidgetsCallback(collectAllNodes(app.rootGraph), 'afterQueued')
|
|
||||||
selectedIndex.value = [0, 0]
|
|
||||||
|
|
||||||
runButtonClick(e)
|
|
||||||
}
|
|
||||||
const selectedItem = ref<AssetItem | undefined>()
|
const selectedItem = ref<AssetItem | undefined>()
|
||||||
const selectedOutput = ref<ResultItemImpl | undefined>()
|
const selectedOutput = ref<ResultItemImpl | undefined>()
|
||||||
const selectedIndex = ref<[number, number]>([0, 0])
|
const selectedIndex = ref<[number, number]>([0, 0])
|
||||||
const outputHistoryRef = useTemplateRef('outputHistoryRef')
|
const outputHistoryRef = useTemplateRef('outputHistoryRef')
|
||||||
|
|
||||||
|
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||||
|
|
||||||
const dateOptions = {
|
const dateOptions = {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -233,6 +96,31 @@ function downloadAsset(item?: AssetItem) {
|
|||||||
for (const output of user_metadata?.allOutputs ?? [])
|
for (const output of user_metadata?.allOutputs ?? [])
|
||||||
downloadFile(output.url, output.filename)
|
downloadFile(output.url, output.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadWorkflow(item: AssetItem | undefined, index: [number, number]) {
|
||||||
|
const workflow = getOutputAssetMetadata(item?.user_metadata)?.workflow
|
||||||
|
if (!workflow) return
|
||||||
|
selectedIndex.value = index
|
||||||
|
if (workflow.id !== app.rootGraph.id) return app.loadGraphData(workflow)
|
||||||
|
//update graph to new version, set old to top of undo queue
|
||||||
|
const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
|
||||||
|
if (!changeTracker) return app.loadGraphData(workflow)
|
||||||
|
changeTracker.redoQueue = []
|
||||||
|
changeTracker.updateState([workflow], changeTracker.undoQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rerun(e: Event) {
|
||||||
|
const runButtonClick = linearWorkflowRef.value?.runButtonClick
|
||||||
|
if (!runButtonClick) return
|
||||||
|
loadWorkflow(selectedItem.value, selectedIndex.value)
|
||||||
|
//FIXME don't use timeouts here
|
||||||
|
//Currently seeds fail to properly update even with timeouts?
|
||||||
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
executeWidgetsCallback(collectAllNodes(app.rootGraph), 'afterQueued')
|
||||||
|
selectedIndex.value = [0, 0]
|
||||||
|
|
||||||
|
runButtonClick(e)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute w-full h-full">
|
<div class="absolute w-full h-full">
|
||||||
@@ -263,6 +151,12 @@ function downloadAsset(item?: AssetItem) {
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<LinearWorkflow
|
||||||
|
v-else
|
||||||
|
ref="linearWorkflowRef"
|
||||||
|
toast-to="#linearDockBottomLeft"
|
||||||
|
notes-to="#linearDockTopLeft"
|
||||||
|
/>
|
||||||
<div />
|
<div />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
@@ -284,8 +178,14 @@ function downloadAsset(item?: AssetItem) {
|
|||||||
:size="1"
|
:size="1"
|
||||||
class="min-w-min outline-none"
|
class="min-w-min outline-none"
|
||||||
>
|
>
|
||||||
|
<LinearWorkflow
|
||||||
|
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
|
||||||
|
ref="linearWorkflowRef"
|
||||||
|
toast-to="#linearDockBottomRight"
|
||||||
|
notes-to="#linearDockTopRight"
|
||||||
|
/>
|
||||||
<OutputHistory
|
<OutputHistory
|
||||||
v-if="settingStore.get('Comfy.Sidebar.Location') === 'right'"
|
v-else
|
||||||
ref="outputHistoryRef"
|
ref="outputHistoryRef"
|
||||||
scroll-reset-button-to="#linearDockBottomRight"
|
scroll-reset-button-to="#linearDockBottomRight"
|
||||||
@update-selection="
|
@update-selection="
|
||||||
@@ -395,127 +295,5 @@ function downloadAsset(item?: AssetItem) {
|
|||||||
: '#linearLeftPanel'
|
: '#linearLeftPanel'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col min-w-80 h-full">
|
|
||||||
<linear-workflow-info
|
|
||||||
class="h-12 border-x border-border-subtle py-2 px-4 gap-2 bg-comfy-menu-bg flex items-center"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="font-bold truncate min-w-30"
|
|
||||||
v-text="workflowStore.activeWorkflow?.filename"
|
|
||||||
/>
|
|
||||||
<div class="flex-1" />
|
|
||||||
<Button
|
|
||||||
v-if="noteDatas.length"
|
|
||||||
variant="muted-textonly"
|
|
||||||
@click="showNoteData = !showNoteData"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--info]" />
|
|
||||||
</Button>
|
|
||||||
<Button v-if="false"> {{ t('menuLabels.publish') }} </Button>
|
|
||||||
</linear-workflow-info>
|
|
||||||
<div
|
|
||||||
class="border gap-2 h-full border-[var(--interface-stroke)] bg-comfy-menu-bg flex flex-col px-2"
|
|
||||||
>
|
|
||||||
<linear-widgets class="grow-1 overflow-y-auto contain-size">
|
|
||||||
<template v-for="(nodeData, index) of nodeDatas" :key="nodeData.id">
|
|
||||||
<div
|
|
||||||
v-if="index !== 0"
|
|
||||||
class="w-full border-t-1 border-node-component-border"
|
|
||||||
/>
|
|
||||||
<DropZone
|
|
||||||
:on-drag-over="nodeData.onDragOver"
|
|
||||||
:on-drag-drop="nodeData.onDragDrop"
|
|
||||||
:drop-indicator="nodeData.dropIndicator"
|
|
||||||
class="text-muted-foreground"
|
|
||||||
>
|
|
||||||
<NodeWidgets
|
|
||||||
:node-data
|
|
||||||
class="py-3 gap-y-4 **:[.col-span-2]:grid-cols-1 text-sm **:[.p-floatlabel]:h-35"
|
|
||||||
/>
|
|
||||||
</DropZone>
|
|
||||||
</template>
|
|
||||||
</linear-widgets>
|
|
||||||
<linear-run-button
|
|
||||||
class="p-4 pb-6 border-t border-node-component-border"
|
|
||||||
>
|
|
||||||
<WidgetInputNumberInput
|
|
||||||
v-model="batchCount"
|
|
||||||
:widget="batchCountWidget"
|
|
||||||
class="*:[.min-w-0]:w-24 grid-cols-[auto_96px]!"
|
|
||||||
/>
|
|
||||||
<SubscribeToRunButton
|
|
||||||
v-if="!isActiveSubscription"
|
|
||||||
class="w-full mt-4"
|
|
||||||
/>
|
|
||||||
<div v-else class="flex mt-4 gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
class="grow-1"
|
|
||||||
size="lg"
|
|
||||||
@click="runButtonClick"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--play]" />
|
|
||||||
{{ t('menu.run') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="!executionStore.isIdle"
|
|
||||||
variant="destructive"
|
|
||||||
size="lg"
|
|
||||||
class="w-10 p-2"
|
|
||||||
@click="commandStore.execute('Comfy.Interrupt')"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--x]" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</linear-run-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<teleport
|
|
||||||
:to="
|
|
||||||
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
|
||||||
? '#linearDockBottomRight'
|
|
||||||
: '#linearDockBottomLeft'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!jobToastTimeout || !jobFinishedQueue"
|
|
||||||
class="bg-base-foreground text-base-background rounded-sm flex h-8 p-1 pr-2 gap-2 items-center"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="jobFinishedQueue"
|
|
||||||
class="icon-[lucide--check] size-5 bg-success-background"
|
|
||||||
/>
|
|
||||||
<ProgressSpinner v-else class="size-4" />
|
|
||||||
<span v-text="t('queue.jobAddedToQueue')" />
|
|
||||||
</div>
|
|
||||||
</teleport>
|
|
||||||
<teleport
|
|
||||||
:to="
|
|
||||||
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
|
||||||
? '#linearDockTopRight'
|
|
||||||
: '#linearDockTopLeft'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="showNoteData"
|
|
||||||
class="bg-base-background text-muted-foreground flex flex-col w-90 gap-2 rounded-2xl border-1 border-border-subtle py-3"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="muted-textonly"
|
|
||||||
size="icon"
|
|
||||||
class="self-end mr-3"
|
|
||||||
@click="showNoteData = false"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--x]" />
|
|
||||||
</Button>
|
|
||||||
<template v-for="nodeData in noteDatas" :key="nodeData.id">
|
|
||||||
<div class="w-full border-t border-border-subtle" />
|
|
||||||
<NodeWidgets
|
|
||||||
:node-data
|
|
||||||
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</teleport>
|
|
||||||
</teleport>
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user