mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 16:30:57 +00:00
## Summary Refactors bootstrap and lifecycle management to parallelize initialization, use Vue best practices, and fix a logout state bug. ## Changes ### Bootstrap Store (`bootstrapStore.ts`) - Extract early bootstrap logic into a dedicated store using `useAsyncState` - Parallelize settings, i18n, and workflow sync loading (previously sequential) - Handle multi-user login scenarios by deferring settings/workflows until authenticated ### GraphCanvas Refactoring - Move non-DOM composables (`useGlobalLitegraph`, `useCopy`, `usePaste`, etc.) to script setup level for earlier initialization - Move `watch` and `whenever` declarations outside `onMounted` (Vue best practice) - Use `until()` from VueUse to await bootstrap store readiness instead of direct async calls ### GraphView Simplification - Replace manual `addEventListener`/`removeEventListener` with `useEventListener` - Replace `setInterval` with `useIntervalFn` for automatic cleanup - Move core command registration to script setup level ### Bug Fix Using `router.push()` for logout caused `isSettingsReady` to persist as `true`, making new users inherit the previous user's cached settings. Reverted to `window.location.reload()` with TODOs for future store reset implementation. ## Testing - Verified login/logout cycle clears settings correctly - Verified bootstrap sequence completes without errors --------- Co-authored-by: Amp <amp@ampcode.com>
225 lines
7.7 KiB
TypeScript
225 lines
7.7 KiB
TypeScript
import _ from 'es-toolkit/compat'
|
|
import { computed, onMounted, watch } from 'vue'
|
|
|
|
import { useNodePricing } from '@/composables/node/useNodePricing'
|
|
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
|
import { useComputedWithWidgetWatch } from '@/composables/node/useWatchWidget'
|
|
import { BadgePosition, LGraphBadge } from '@/lib/litegraph/src/litegraph'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { app } from '@/scripts/app'
|
|
import { useExtensionStore } from '@/stores/extensionStore'
|
|
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
|
import { NodeBadgeMode } from '@/types/nodeSource'
|
|
|
|
/**
|
|
* Add LGraphBadge to LGraphNode based on settings.
|
|
*
|
|
* Following badges are added:
|
|
* - Node ID badge
|
|
* - Node source badge
|
|
* - Node life cycle badge
|
|
* - API node credits badge
|
|
*/
|
|
export const useNodeBadge = () => {
|
|
const settingStore = useSettingStore()
|
|
const extensionStore = useExtensionStore()
|
|
const colorPaletteStore = useColorPaletteStore()
|
|
const priceBadge = usePriceBadge()
|
|
|
|
const nodeSourceBadgeMode = computed(
|
|
() =>
|
|
settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode
|
|
)
|
|
const nodeIdBadgeMode = computed(
|
|
() => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode
|
|
)
|
|
const nodeLifeCycleBadgeMode = computed(
|
|
() =>
|
|
settingStore.get(
|
|
'Comfy.NodeBadge.NodeLifeCycleBadgeMode'
|
|
) as NodeBadgeMode
|
|
)
|
|
|
|
const showApiPricingBadge = computed(() =>
|
|
settingStore.get('Comfy.NodeBadge.ShowApiPricing')
|
|
)
|
|
|
|
watch(
|
|
[
|
|
nodeSourceBadgeMode,
|
|
nodeIdBadgeMode,
|
|
nodeLifeCycleBadgeMode,
|
|
showApiPricingBadge
|
|
],
|
|
() => {
|
|
app.canvas?.setDirty(true, true)
|
|
}
|
|
)
|
|
|
|
const nodeDefStore = useNodeDefStore()
|
|
function badgeTextVisible(
|
|
nodeDef: ComfyNodeDefImpl | null,
|
|
badgeMode: NodeBadgeMode
|
|
): boolean {
|
|
return !(
|
|
badgeMode === NodeBadgeMode.None ||
|
|
(nodeDef?.isCoreNode && badgeMode === NodeBadgeMode.HideBuiltIn)
|
|
)
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (extensionStore.isExtensionInstalled('Comfy.NodeBadge')) return
|
|
|
|
// TODO: Fix the composables and watchers being setup in onMounted
|
|
const nodePricing = useNodePricing()
|
|
|
|
watch(
|
|
() => nodePricing.pricingRevision.value,
|
|
() => {
|
|
if (!showApiPricingBadge.value) return
|
|
app.canvas?.setDirty(true, true)
|
|
}
|
|
)
|
|
|
|
extensionStore.registerExtension({
|
|
name: 'Comfy.NodeBadge',
|
|
nodeCreated(node: LGraphNode) {
|
|
node.badgePosition = BadgePosition.TopRight
|
|
|
|
const badge = computed(() => {
|
|
const nodeDef = nodeDefStore.fromLGraphNode(node)
|
|
return new LGraphBadge({
|
|
text: _.truncate(
|
|
[
|
|
badgeTextVisible(nodeDef, nodeIdBadgeMode.value)
|
|
? `#${node.id}`
|
|
: '',
|
|
badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)
|
|
? (nodeDef?.nodeLifeCycleBadgeText ?? '')
|
|
: '',
|
|
badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)
|
|
? (nodeDef?.nodeSource?.badgeText ?? '')
|
|
: ''
|
|
]
|
|
.filter((s) => s.length > 0)
|
|
.join(' '),
|
|
{
|
|
length: 31
|
|
}
|
|
),
|
|
fgColor:
|
|
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
|
.BADGE_FG_COLOR,
|
|
bgColor:
|
|
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
|
.BADGE_BG_COLOR
|
|
})
|
|
})
|
|
|
|
node.badges.push(() => badge.value)
|
|
|
|
if (node.constructor.nodeData?.api_node && showApiPricingBadge.value) {
|
|
// JSONata rules are dynamic if they depend on any widgets/inputs/input_groups
|
|
const pricingConfig = nodePricing.getNodePricingConfig(node)
|
|
const hasDynamicPricing =
|
|
!!pricingConfig &&
|
|
((pricingConfig.depends_on?.widgets?.length ?? 0) > 0 ||
|
|
(pricingConfig.depends_on?.inputs?.length ?? 0) > 0 ||
|
|
(pricingConfig.depends_on?.input_groups?.length ?? 0) > 0)
|
|
|
|
// Keep the existing widget-watch wiring ONLY to trigger redraws on widget change.
|
|
// (We no longer rely on it to hold the current badge value.)
|
|
if (hasDynamicPricing) {
|
|
// For dynamic pricing nodes, use computed that watches widget changes
|
|
const relevantWidgetNames = nodePricing.getRelevantWidgetNames(
|
|
node.constructor.nodeData?.name
|
|
)
|
|
|
|
const computedWithWidgetWatch = useComputedWithWidgetWatch(node, {
|
|
widgetNames: relevantWidgetNames,
|
|
triggerCanvasRedraw: true
|
|
})
|
|
|
|
// Ensure watchers are installed; ignore the returned value.
|
|
// (This call is what registers the widget listeners in most implementations.)
|
|
computedWithWidgetWatch(() => 0)
|
|
|
|
// Hook into connection changes to trigger price recalculation
|
|
// This handles both connect and disconnect in VueNodes mode
|
|
const relevantInputs = pricingConfig?.depends_on?.inputs ?? []
|
|
const inputGroupPrefixes =
|
|
pricingConfig?.depends_on?.input_groups ?? []
|
|
const hasRelevantInputs =
|
|
relevantInputs.length > 0 || inputGroupPrefixes.length > 0
|
|
|
|
if (hasRelevantInputs) {
|
|
const originalOnConnectionsChange = node.onConnectionsChange
|
|
node.onConnectionsChange = function (
|
|
type,
|
|
slotIndex,
|
|
isConnected,
|
|
link,
|
|
ioSlot
|
|
) {
|
|
originalOnConnectionsChange?.call(
|
|
this,
|
|
type,
|
|
slotIndex,
|
|
isConnected,
|
|
link,
|
|
ioSlot
|
|
)
|
|
// Only trigger if this input affects pricing
|
|
const inputName = ioSlot?.name
|
|
if (!inputName) return
|
|
const isRelevantInput =
|
|
relevantInputs.includes(inputName) ||
|
|
inputGroupPrefixes.some((prefix) =>
|
|
inputName.startsWith(prefix + '.')
|
|
)
|
|
if (isRelevantInput) {
|
|
nodePricing.triggerPriceRecalculation(node)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let lastLabel = nodePricing.getNodeDisplayPrice(node)
|
|
let lastBadge = priceBadge.getCreditsBadge(lastLabel)
|
|
|
|
const creditsBadgeGetter: () => LGraphBadge = () => {
|
|
const label = nodePricing.getNodeDisplayPrice(node)
|
|
if (label !== lastLabel) {
|
|
lastLabel = label
|
|
lastBadge = priceBadge.getCreditsBadge(label)
|
|
}
|
|
return lastBadge
|
|
}
|
|
|
|
node.badges.push(creditsBadgeGetter)
|
|
}
|
|
},
|
|
init() {
|
|
app.canvas.canvas.addEventListener<'litegraph:set-graph'>(
|
|
'litegraph:set-graph',
|
|
() => {
|
|
for (const node of app.canvas.graph?.nodes ?? [])
|
|
priceBadge.updateSubgraphCredits(node)
|
|
}
|
|
)
|
|
app.canvas.canvas.addEventListener<'subgraph-converted'>(
|
|
'subgraph-converted',
|
|
(e) => priceBadge.updateSubgraphCredits(e.detail.subgraphNode)
|
|
)
|
|
},
|
|
afterConfigureGraph() {
|
|
for (const node of app.canvas.graph?.nodes ?? [])
|
|
priceBadge.updateSubgraphCredits(node)
|
|
}
|
|
})
|
|
})
|
|
}
|