mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 08:30:08 +00:00
merge main into rh-test
This commit is contained in:
@@ -100,57 +100,64 @@ The following diagram illustrates the store architecture and data flow:
|
||||
|
||||
## Core Stores
|
||||
|
||||
The following table lists ALL stores in the system as of 2025-01-30:
|
||||
The following table lists ALL 46 store instances in the system as of 2025-09-01:
|
||||
|
||||
### Main Stores
|
||||
|
||||
| Store | Description | Category |
|
||||
|-------|-------------|----------|
|
||||
| aboutPanelStore.ts | Manages the About panel state and badges | UI |
|
||||
| apiKeyAuthStore.ts | Handles API key authentication | Auth |
|
||||
| comfyManagerStore.ts | Manages ComfyUI application state | Core |
|
||||
| comfyRegistryStore.ts | Handles extensions registry | Registry |
|
||||
| commandStore.ts | Manages commands and command execution | Core |
|
||||
| dialogStore.ts | Controls dialog/modal display and state | UI |
|
||||
| domWidgetStore.ts | Manages DOM widget state | Widgets |
|
||||
| electronDownloadStore.ts | Handles Electron-specific download operations | Platform |
|
||||
| executionStore.ts | Tracks workflow execution state | Execution |
|
||||
| extensionStore.ts | Manages extension registration and state | Extensions |
|
||||
| firebaseAuthStore.ts | Handles Firebase authentication | Auth |
|
||||
| graphStore.ts | Manages the graph canvas state | Core |
|
||||
| imagePreviewStore.ts | Controls image preview functionality | Media |
|
||||
| keybindingStore.ts | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | Handles menu items and their state | UI |
|
||||
| modelStore.ts | Manages AI models information | Models |
|
||||
| modelToNodeStore.ts | Maps models to compatible nodes | Models |
|
||||
| nodeBookmarkStore.ts | Manages node bookmarks and favorites | Nodes |
|
||||
| nodeDefStore.ts | Manages node definitions | Nodes |
|
||||
| queueStore.ts | Handles the execution queue | Execution |
|
||||
| releaseStore.ts | Manages application release information | System |
|
||||
| serverConfigStore.ts | Handles server configuration | Config |
|
||||
| settingStore.ts | Manages application settings | Config |
|
||||
| subgraphNavigationStore.ts | Handles subgraph navigation state | Navigation |
|
||||
| systemStatsStore.ts | Tracks system performance statistics | System |
|
||||
| toastStore.ts | Manages toast notifications | UI |
|
||||
| userFileStore.ts | Manages user file operations | Files |
|
||||
| userStore.ts | Manages user data and preferences | User |
|
||||
| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core |
|
||||
| widgetStore.ts | Manages widget configurations | Widgets |
|
||||
| workflowStore.ts | Handles workflow data and operations | Workflows |
|
||||
| workflowTemplatesStore.ts | Manages workflow templates | Workflows |
|
||||
| workspaceStore.ts | Manages overall workspace state | Workspace |
|
||||
| File | Store | Description | Category |
|
||||
|------|-------|-------------|----------|
|
||||
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
|
||||
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
|
||||
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
|
||||
| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI |
|
||||
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
|
||||
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
|
||||
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
|
||||
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
|
||||
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
|
||||
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
|
||||
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
|
||||
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
|
||||
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
|
||||
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
|
||||
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
|
||||
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
|
||||
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
|
||||
| modelStore.ts | useModelStore | Manages AI models information | Models |
|
||||
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
|
||||
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
|
||||
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
|
||||
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
|
||||
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
|
||||
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
|
||||
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
|
||||
| releaseStore.ts | useReleaseStore | Manages application release information | System |
|
||||
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
|
||||
| settingStore.ts | useSettingStore | Manages application settings | Config |
|
||||
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
|
||||
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
|
||||
| toastStore.ts | useToastStore | Manages toast notifications | UI |
|
||||
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
|
||||
| userStore.ts | useUserStore | Manages user data and preferences | User |
|
||||
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
|
||||
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
|
||||
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
|
||||
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
|
||||
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
|
||||
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
|
||||
|
||||
### Workspace Stores
|
||||
Located in `stores/workspace/`:
|
||||
|
||||
| Store | Description |
|
||||
|-------|-------------|
|
||||
| bottomPanelStore.ts | Controls bottom panel visibility and state |
|
||||
| colorPaletteStore.ts | Manages color palette configurations |
|
||||
| nodeHelpStore.ts | Handles node help and documentation display |
|
||||
| searchBoxStore.ts | Manages search box functionality |
|
||||
| sidebarTabStore.ts | Controls sidebar tab states and navigation |
|
||||
| File | Store | Description | Category |
|
||||
|------|-------|-------------|----------|
|
||||
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
|
||||
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
|
||||
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
|
||||
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
|
||||
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
|
||||
|
||||
## Store Development Guidelines
|
||||
|
||||
@@ -189,7 +196,7 @@ export const useExampleStore = defineStore('example', () => {
|
||||
async function fetchItems() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/items')
|
||||
const data = await response.json()
|
||||
@@ -207,11 +214,11 @@ export const useExampleStore = defineStore('example', () => {
|
||||
items,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
|
||||
// Getters
|
||||
itemCount,
|
||||
hasError,
|
||||
|
||||
|
||||
// Actions
|
||||
addItem,
|
||||
fetchItems
|
||||
@@ -238,7 +245,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await api.getData()
|
||||
const result = await api.getExtensions()
|
||||
data.value = result
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
@@ -266,21 +273,21 @@ import { useOtherStore } from './otherStore'
|
||||
export const useComposedStore = defineStore('composed', () => {
|
||||
const otherStore = useOtherStore()
|
||||
const { someData } = storeToRefs(otherStore)
|
||||
|
||||
|
||||
// Local state
|
||||
const localState = ref(0)
|
||||
|
||||
|
||||
// Computed value based on other store
|
||||
const derivedValue = computed(() => {
|
||||
return computeFromOtherData(someData.value, localState.value)
|
||||
})
|
||||
|
||||
|
||||
// Action that uses another store
|
||||
async function complexAction() {
|
||||
await otherStore.someAction()
|
||||
localState.value += 1
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
localState,
|
||||
derivedValue,
|
||||
@@ -299,20 +306,20 @@ export const usePreferencesStore = defineStore('preferences', () => {
|
||||
// Load from localStorage if available
|
||||
const theme = ref(localStorage.getItem('theme') || 'light')
|
||||
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
|
||||
|
||||
|
||||
// Save to localStorage when changed
|
||||
watch(theme, (newTheme) => {
|
||||
localStorage.setItem('theme', newTheme)
|
||||
})
|
||||
|
||||
|
||||
watch(fontSize, (newSize) => {
|
||||
localStorage.setItem('fontSize', newSize.toString())
|
||||
})
|
||||
|
||||
|
||||
function setTheme(newTheme) {
|
||||
theme.value = newTheme
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
theme,
|
||||
fontSize,
|
||||
@@ -347,7 +354,7 @@ describe('useExampleStore', () => {
|
||||
// Create a fresh pinia instance and make it active
|
||||
setActivePinia(createPinia())
|
||||
store = useExampleStore()
|
||||
|
||||
|
||||
// Clear all mocks
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
@@ -363,14 +370,14 @@ describe('useExampleStore', () => {
|
||||
expect(store.items).toEqual(['test'])
|
||||
expect(store.itemCount).toBe(1)
|
||||
})
|
||||
|
||||
|
||||
it('should fetch items', async () => {
|
||||
// Setup mock response
|
||||
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
|
||||
|
||||
|
||||
// Call the action
|
||||
await store.fetchItems()
|
||||
|
||||
|
||||
// Verify state changes
|
||||
expect(store.isLoading).toBe(false)
|
||||
expect(store.items).toEqual(['item1', 'item2'])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { AboutPageBadge } from '@/types/comfy'
|
||||
import type { AboutPageBadge } from '@/types/comfy'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
import { useExtensionStore } from './extensionStore'
|
||||
|
||||
@@ -4,10 +4,10 @@ import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { ApiKeyAuthHeader } from '@/types/authTypes'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
import type { ApiKeyAuthHeader } from '@/types/authTypes'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type ComfyApiUser =
|
||||
operations['createCustomer']['responses']['201']['content']['application/json']
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { useServerLogs } from '@/composables/useServerLogs'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInfo,
|
||||
ManagerPackInstalled,
|
||||
TaskLog,
|
||||
UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
|
||||
/**
|
||||
* Store for state of installed node packs
|
||||
*/
|
||||
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const { t } = useI18n()
|
||||
const managerService = useComfyManagerService()
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
const installedPacks = ref<InstalledPacksResponse>({})
|
||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||
const disabledPacksIds = ref<Set<string>>(new Set())
|
||||
const installedPacksIds = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
|
||||
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } =
|
||||
useManagerQueue()
|
||||
|
||||
const setStale = () => {
|
||||
isStale.value = true
|
||||
}
|
||||
|
||||
const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id
|
||||
|
||||
const isInstalledPackId = (packName: string | undefined): boolean =>
|
||||
!!packName && installedPacksIds.value.has(packName)
|
||||
|
||||
const isEnabledPackId = (packName: string | undefined): boolean =>
|
||||
!!packName &&
|
||||
isInstalledPackId(packName) &&
|
||||
enabledPacksIds.value.has(packName)
|
||||
|
||||
const packsToIdSet = (packs: ManagerPackInstalled[]) =>
|
||||
packs.reduce((acc, pack) => {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
if (id) acc.add(id)
|
||||
return acc
|
||||
}, new Set<string>())
|
||||
|
||||
/**
|
||||
* A pack is disabled if there is a disabled entry and no corresponding
|
||||
* enabled entry. If `packname@1.0.2` is disabled, but `packname@1.0.3` is
|
||||
* enabled, then `packname` is considered enabled.
|
||||
*
|
||||
* @example
|
||||
* installedPacks = {
|
||||
* "packname@1_0_2": { enabled: false, cnr_id: "packname" },
|
||||
* "packname": { enabled: true, cnr_id: "packname" }
|
||||
* }
|
||||
* isDisabled("packname") // false
|
||||
*
|
||||
* installedPacks = {
|
||||
* "packname@1_0_2": { enabled: false, cnr_id: "packname" },
|
||||
* }
|
||||
* isDisabled("packname") // true
|
||||
*/
|
||||
const updateDisabledIds = (packs: ManagerPackInstalled[]) => {
|
||||
// Use temporary variables to avoid triggering reactivity
|
||||
const enabledIds = new Set<string>()
|
||||
const disabledIds = new Set<string>()
|
||||
|
||||
for (const pack of packs) {
|
||||
const id = getPackId(pack)
|
||||
if (!id) continue
|
||||
|
||||
const { enabled } = pack
|
||||
|
||||
if (enabled === true) enabledIds.add(id)
|
||||
else if (enabled === false) disabledIds.add(id)
|
||||
|
||||
// If pack in both (has a disabled and enabled version), remove from disabled
|
||||
const inBothSets = enabledIds.has(id) && disabledIds.has(id)
|
||||
if (inBothSets) disabledIds.delete(id)
|
||||
}
|
||||
|
||||
enabledPacksIds.value = enabledIds
|
||||
disabledPacksIds.value = disabledIds
|
||||
}
|
||||
|
||||
const updateInstalledIds = (packs: ManagerPackInstalled[]) => {
|
||||
installedPacksIds.value = packsToIdSet(packs)
|
||||
}
|
||||
|
||||
const onPacksChanged = () => {
|
||||
const packs = Object.values(installedPacks.value)
|
||||
updateDisabledIds(packs)
|
||||
updateInstalledIds(packs)
|
||||
}
|
||||
|
||||
watch(installedPacks, onPacksChanged, { deep: true })
|
||||
|
||||
const refreshInstalledList = async () => {
|
||||
const packs = await managerService.listInstalledPacks()
|
||||
if (packs) installedPacks.value = packs
|
||||
isStale.value = false
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
|
||||
const loggedTask = async () => {
|
||||
taskLogs.value.push({ taskName, logs: logs.value })
|
||||
await startListening()
|
||||
return task()
|
||||
}
|
||||
|
||||
const onComplete = async () => {
|
||||
await stopListening()
|
||||
setStale()
|
||||
}
|
||||
|
||||
return { task: loggedTask, onComplete }
|
||||
}
|
||||
|
||||
const installPack = useCachedRequest<InstallPackParams, void>(
|
||||
async (params: InstallPackParams, signal?: AbortSignal) => {
|
||||
if (!params.id) return
|
||||
|
||||
let actionDescription = t('g.installing')
|
||||
if (installedPacksIds.value.has(params.id)) {
|
||||
const installedPack = installedPacks.value[params.id]
|
||||
|
||||
if (installedPack && installedPack.ver !== params.selected_version) {
|
||||
actionDescription = t('manager.changingVersion', {
|
||||
from: installedPack.ver,
|
||||
to: params.selected_version
|
||||
})
|
||||
} else {
|
||||
actionDescription = t('g.enabling')
|
||||
}
|
||||
}
|
||||
|
||||
const task = () => managerService.installPack(params, signal)
|
||||
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const uninstallPack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
|
||||
async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
|
||||
const task = () => managerService.updateAllPacks(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.updatingAllPacks')))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
const pack = installedPacks.value[packId]
|
||||
return pack?.ver
|
||||
}
|
||||
|
||||
const clearLogs = () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
// Manager state
|
||||
isLoading: managerService.isLoading,
|
||||
error: managerService.error,
|
||||
statusMessage,
|
||||
allTasksDone,
|
||||
uncompletedCount,
|
||||
taskLogs,
|
||||
clearLogs,
|
||||
setStale,
|
||||
|
||||
// Installed packs state
|
||||
installedPacks,
|
||||
installedPacksIds,
|
||||
isPackInstalled: isInstalledPackId,
|
||||
isPackEnabled: isEnabledPackId,
|
||||
getInstalledPackVersion,
|
||||
refreshInstalledList,
|
||||
|
||||
// Pack actions
|
||||
installPack,
|
||||
uninstallPack,
|
||||
updatePack,
|
||||
updateAllPacks,
|
||||
disablePack,
|
||||
enablePack: installPack // Enable is done via install endpoint with a disabled pack
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Store for state of the manager progress dialog content.
|
||||
* The dialog itself is managed by the dialog store. This store is used to
|
||||
* manage the visibility of the dialog's content, header, footer.
|
||||
*/
|
||||
export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const collapse = () => {
|
||||
isExpanded.value = false
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
isExpanded.value = true
|
||||
}
|
||||
return {
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -43,6 +43,7 @@ interface DialogInstance {
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
footerComponent?: Component
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps: DialogComponentProps
|
||||
priority: number
|
||||
}
|
||||
@@ -54,6 +55,7 @@ export interface ShowDialogOptions {
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
/**
|
||||
* Optional priority for dialog stacking.
|
||||
@@ -127,6 +129,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
priority?: number
|
||||
}) {
|
||||
@@ -146,6 +149,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
contentProps: { ...options.props },
|
||||
footerProps: { ...options.footerProps },
|
||||
priority: options.priority ?? 1,
|
||||
dialogComponentProps: {
|
||||
maximizable: false,
|
||||
|
||||
@@ -57,6 +57,13 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||
if (state) state.active = false
|
||||
}
|
||||
|
||||
const setWidget = (widget: BaseDOMWidget) => {
|
||||
const state = widgetStates.value.get(widget.id)
|
||||
if (!state) return
|
||||
state.active = true
|
||||
state.widget = widget
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
widgetStates.value.clear()
|
||||
}
|
||||
@@ -69,6 +76,7 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||
unregisterWidget,
|
||||
activateWidget,
|
||||
deactivateWidget,
|
||||
setWidget,
|
||||
clear
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,14 @@ import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget
|
||||
import { useNodeChatHistory } from '@/composables/node/useNodeChatHistory'
|
||||
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
|
||||
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type {
|
||||
DisplayComponentWsMessage,
|
||||
ExecutedWsMessage,
|
||||
@@ -17,21 +25,13 @@ import type {
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { useCanvasStore } from './graphStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
|
||||
|
||||
export interface QueuedPrompt {
|
||||
interface QueuedPrompt {
|
||||
/**
|
||||
* The nodes that are queued to be executed. The key is the node id and the
|
||||
* value is a boolean indicating if the node has been executed.
|
||||
@@ -43,6 +43,57 @@ export interface QueuedPrompt {
|
||||
workflow?: ComfyWorkflow
|
||||
}
|
||||
|
||||
const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => {
|
||||
const node = graph.getNodeById(id)
|
||||
if (node?.isSubgraphNode()) return node.subgraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get the subgraph objects for the given subgraph instance IDs
|
||||
* @param currentGraph The current graph
|
||||
* @param subgraphNodeIds The instance IDs
|
||||
* @param subgraphs The subgraphs
|
||||
* @returns The subgraphs that correspond to each of the instance IDs.
|
||||
*/
|
||||
function getSubgraphsFromInstanceIds(
|
||||
currentGraph: LGraph | Subgraph,
|
||||
subgraphNodeIds: string[],
|
||||
subgraphs: Subgraph[] = []
|
||||
): Subgraph[] {
|
||||
// Last segment is the node portion; nothing to do.
|
||||
if (subgraphNodeIds.length === 1) return subgraphs
|
||||
|
||||
const currentPart = subgraphNodeIds.shift()
|
||||
if (currentPart === undefined) return subgraphs
|
||||
|
||||
const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph)
|
||||
if (!subgraph) throw new Error(`Subgraph not found: ${currentPart}`)
|
||||
|
||||
subgraphs.push(subgraph)
|
||||
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert execution context node IDs to NodeLocatorIds
|
||||
* @param nodeId The node ID from execution context (could be execution ID)
|
||||
* @returns The NodeLocatorId
|
||||
*/
|
||||
function executionIdToNodeLocatorId(nodeId: string | number): NodeLocatorId {
|
||||
const nodeIdStr = String(nodeId)
|
||||
|
||||
if (!nodeIdStr.includes(':')) {
|
||||
// It's a top-level node ID
|
||||
return nodeIdStr
|
||||
}
|
||||
|
||||
// It's an execution node ID
|
||||
const parts = nodeIdStr.split(':')
|
||||
const localNodeId = parts[parts.length - 1]
|
||||
const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts)
|
||||
const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId)
|
||||
return nodeLocatorId
|
||||
}
|
||||
|
||||
export const useExecutionStore = defineStore('execution', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -55,29 +106,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
// This is the progress of all nodes in the currently executing workflow
|
||||
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
|
||||
|
||||
/**
|
||||
* Convert execution context node IDs to NodeLocatorIds
|
||||
* @param nodeId The node ID from execution context (could be execution ID)
|
||||
* @returns The NodeLocatorId
|
||||
*/
|
||||
const executionIdToNodeLocatorId = (
|
||||
nodeId: string | number
|
||||
): NodeLocatorId => {
|
||||
const nodeIdStr = String(nodeId)
|
||||
|
||||
if (!nodeIdStr.includes(':')) {
|
||||
// It's a top-level node ID
|
||||
return nodeIdStr
|
||||
}
|
||||
|
||||
// It's an execution node ID
|
||||
const parts = nodeIdStr.split(':')
|
||||
const localNodeId = parts[parts.length - 1]
|
||||
const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts)
|
||||
const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId)
|
||||
return nodeLocatorId
|
||||
}
|
||||
|
||||
const mergeExecutionProgressStates = (
|
||||
currentState: NodeProgressState | undefined,
|
||||
newState: NodeProgressState
|
||||
@@ -132,16 +160,20 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
// Easily access all currently executing node IDs
|
||||
const executingNodeIds = computed<NodeId[]>(() => {
|
||||
return Object.entries(nodeProgressStates)
|
||||
return Object.entries(nodeProgressStates.value)
|
||||
.filter(([_, state]) => state.state === 'running')
|
||||
.map(([nodeId, _]) => nodeId)
|
||||
})
|
||||
|
||||
// @deprecated For backward compatibility - stores the primary executing node ID
|
||||
const executingNodeId = computed<NodeId | null>(() => {
|
||||
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
|
||||
return executingNodeIds.value[0] ?? null
|
||||
})
|
||||
|
||||
const uniqueExecutingNodeIdStrings = computed(
|
||||
() => new Set(executingNodeIds.value.map(String))
|
||||
)
|
||||
|
||||
// For backward compatibility - returns the primary executing node
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
@@ -159,36 +191,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
)
|
||||
})
|
||||
|
||||
const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => {
|
||||
const node = graph.getNodeById(id)
|
||||
if (node?.isSubgraphNode()) return node.subgraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get the subgraph objects for the given subgraph instance IDs
|
||||
* @param currentGraph The current graph
|
||||
* @param subgraphNodeIds The instance IDs
|
||||
* @param subgraphs The subgraphs
|
||||
* @returns The subgraphs that correspond to each of the instance IDs.
|
||||
*/
|
||||
const getSubgraphsFromInstanceIds = (
|
||||
currentGraph: LGraph | Subgraph,
|
||||
subgraphNodeIds: string[],
|
||||
subgraphs: Subgraph[] = []
|
||||
): Subgraph[] => {
|
||||
// Last segment is the node portion; nothing to do.
|
||||
if (subgraphNodeIds.length === 1) return subgraphs
|
||||
|
||||
const currentPart = subgraphNodeIds.shift()
|
||||
if (currentPart === undefined) return subgraphs
|
||||
|
||||
const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph)
|
||||
if (!subgraph) throw new Error(`Subgraph not found: ${currentPart}`)
|
||||
|
||||
subgraphs.push(subgraph)
|
||||
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||
}
|
||||
|
||||
// This is the progress of the currently executing node (for backward compatibility)
|
||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
@@ -220,6 +222,19 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
return total > 0 ? done / total : 0
|
||||
})
|
||||
|
||||
const lastExecutionErrorNodeLocatorId = computed(() => {
|
||||
const err = lastExecutionError.value
|
||||
if (!err) return null
|
||||
return executionIdToNodeLocatorId(String(err.node_id))
|
||||
})
|
||||
|
||||
const lastExecutionErrorNodeId = computed(() => {
|
||||
const locator = lastExecutionErrorNodeLocatorId.value
|
||||
if (!locator) return null
|
||||
const localId = workflowStore.nodeLocatorIdToNodeId(locator)
|
||||
return localId != null ? String(localId) : null
|
||||
})
|
||||
|
||||
function bindExecutionEvents() {
|
||||
api.addEventListener('execution_start', handleExecutionStart)
|
||||
api.addEventListener('execution_cached', handleExecutionCached)
|
||||
@@ -410,62 +425,25 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
return {
|
||||
isIdle,
|
||||
clientId,
|
||||
/**
|
||||
* The id of the prompt that is currently being executed
|
||||
*/
|
||||
activePromptId,
|
||||
/**
|
||||
* The queued prompts
|
||||
*/
|
||||
queuedPrompts,
|
||||
/**
|
||||
* The node errors from the previous execution.
|
||||
*/
|
||||
lastNodeErrors,
|
||||
/**
|
||||
* The error from the previous execution.
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
lastExecutionErrorNodeId,
|
||||
executingNodeId,
|
||||
/**
|
||||
* The list of all nodes that are currently executing
|
||||
*/
|
||||
executingNodeIds,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
activePrompt,
|
||||
/**
|
||||
* The total number of nodes to execute
|
||||
*/
|
||||
totalNodesToExecute,
|
||||
/**
|
||||
* The number of nodes that have been executed
|
||||
*/
|
||||
nodesExecuted,
|
||||
/**
|
||||
* The progress of the execution
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (backward compatibility)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
/**
|
||||
* All node progress states from progress_state events
|
||||
*/
|
||||
nodeProgressStates,
|
||||
nodeLocationProgressStates,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt,
|
||||
uniqueExecutingNodeIdStrings,
|
||||
// Raw executing progress data for backward compatibility in ComfyApp.
|
||||
_executingNodeProgress,
|
||||
// NodeLocatorId conversion helpers
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { ComfyExtension } from '@/types/comfy'
|
||||
/**
|
||||
* These extensions are always active, even if they are disabled in the setting.
|
||||
*/
|
||||
export const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
|
||||
const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
|
||||
|
||||
export const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
|
||||
const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
|
||||
// pysssss.Locking is replaced by pin/unpin in ComfyUI core.
|
||||
// https://github.com/Comfy-Org/litegraph.js/pull/117
|
||||
'pysssss.Locking',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type UserCredential,
|
||||
browserLocalPersistence,
|
||||
createUserWithEmailAndPassword,
|
||||
deleteUser,
|
||||
onAuthStateChanged,
|
||||
sendEmailVerification,
|
||||
sendPasswordResetEmail,
|
||||
@@ -25,8 +26,8 @@ import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { type AuthHeader } from '@/types/authTypes'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
import type { AuthHeader } from '@/types/authTypes'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type CreditPurchaseResponse =
|
||||
operations['InitiateCreditPurchase']['responses']['201']['content']['application/json']
|
||||
@@ -41,7 +42,7 @@ type AccessBillingPortalResponse =
|
||||
type AccessBillingPortalReqBody =
|
||||
operations['AccessBillingPortal']['requestBody']
|
||||
|
||||
export class FirebaseAuthStoreError extends Error {
|
||||
class FirebaseAuthStoreError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FirebaseAuthStoreError'
|
||||
@@ -301,6 +302,14 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
await sendEmailVerification(currentUser.value)
|
||||
}
|
||||
|
||||
/** Delete the current user account */
|
||||
const _deleteAccount = async (): Promise<void> => {
|
||||
if (!currentUser.value) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
await deleteUser(currentUser.value)
|
||||
}
|
||||
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
@@ -401,6 +410,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
sendPasswordReset,
|
||||
updatePassword: _updatePassword,
|
||||
getAuthHeader,
|
||||
verifyEmail
|
||||
verifyEmail,
|
||||
deleteAccount: _deleteAccount
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
|
||||
|
||||
import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { isLGraphGroup, isLGraphNode, isReroute } from '@/utils/litegraphUtil'
|
||||
|
||||
export const useTitleEditorStore = defineStore('titleEditor', () => {
|
||||
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
|
||||
|
||||
return {
|
||||
titleEditorTarget
|
||||
}
|
||||
})
|
||||
|
||||
export const useCanvasStore = defineStore('canvas', () => {
|
||||
/**
|
||||
* The LGraphCanvas instance.
|
||||
*
|
||||
* The root LGraphCanvas object is a shallow ref.
|
||||
*/
|
||||
const canvas = shallowRef<LGraphCanvas | null>(null)
|
||||
/**
|
||||
* The selected items on the canvas. All stored items are raw.
|
||||
*/
|
||||
const selectedItems = ref<Raw<Positionable>[]>([])
|
||||
const updateSelectedItems = () => {
|
||||
const items = Array.from(canvas.value?.selectedItems ?? [])
|
||||
selectedItems.value = items.map((item) => markRaw(item))
|
||||
}
|
||||
|
||||
// Reactive scale percentage that syncs with app.canvas.ds.scale
|
||||
const appScalePercentage = ref(100)
|
||||
|
||||
// Set up scale synchronization when canvas is available
|
||||
let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
|
||||
undefined
|
||||
const initScaleSync = () => {
|
||||
if (app.canvas?.ds) {
|
||||
// Initial sync
|
||||
originalOnChanged = app.canvas.ds.onChanged
|
||||
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
|
||||
|
||||
// Set up continuous sync
|
||||
app.canvas.ds.onChanged = () => {
|
||||
if (app.canvas?.ds?.scale) {
|
||||
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
|
||||
}
|
||||
// Call original handler if exists
|
||||
originalOnChanged?.(app.canvas.ds.scale, app.canvas.ds.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupScaleSync = () => {
|
||||
if (app.canvas?.ds) {
|
||||
app.canvas.ds.onChanged = originalOnChanged
|
||||
originalOnChanged = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
|
||||
const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
|
||||
const rerouteSelected = computed(() => selectedItems.value.some(isReroute))
|
||||
|
||||
const getCanvas = () => {
|
||||
if (!canvas.value) throw new Error('getCanvas: canvas is null')
|
||||
return canvas.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the canvas zoom level from a percentage value
|
||||
* @param percentage - Zoom percentage value (1-1000, where 1000 = 1000% zoom)
|
||||
*/
|
||||
const setAppZoomFromPercentage = (percentage: number) => {
|
||||
if (!app.canvas?.ds || percentage <= 0) return
|
||||
|
||||
// Convert percentage to scale (1000% = 10.0 scale)
|
||||
const newScale = percentage / 100
|
||||
const ds = app.canvas.ds
|
||||
|
||||
ds.changeScale(
|
||||
newScale,
|
||||
ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
|
||||
)
|
||||
app.canvas.setDirty(true, true)
|
||||
|
||||
// Update reactive value immediately for UI consistency
|
||||
appScalePercentage.value = Math.round(newScale * 100)
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
selectedItems,
|
||||
nodeSelected,
|
||||
groupSelected,
|
||||
rerouteSelected,
|
||||
appScalePercentage,
|
||||
updateSelectedItems,
|
||||
getCanvas,
|
||||
setAppZoomFromPercentage,
|
||||
initScaleSync,
|
||||
cleanupScaleSync
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
import type { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type {
|
||||
ExecutedWsMessage,
|
||||
ResultItem,
|
||||
ResultItemType
|
||||
@@ -9,11 +13,12 @@ import {
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
import { isVideoNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const PREVIEW_REVOKE_DELAY_MS = 400
|
||||
|
||||
const createOutputs = (
|
||||
filenames: string[],
|
||||
type: ResultItemType,
|
||||
@@ -35,6 +40,25 @@ interface SetOutputOptions {
|
||||
export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const { nodeIdToNodeLocatorId } = useWorkflowStore()
|
||||
const { executionIdToNodeLocatorId } = useExecutionStore()
|
||||
const scheduledRevoke: Record<NodeLocatorId, { stop: () => void }> = {}
|
||||
|
||||
function scheduleRevoke(locator: NodeLocatorId, cb: () => void) {
|
||||
scheduledRevoke[locator]?.stop()
|
||||
|
||||
const { stop } = useTimeoutFn(() => {
|
||||
delete scheduledRevoke[locator]
|
||||
cb()
|
||||
}, PREVIEW_REVOKE_DELAY_MS)
|
||||
|
||||
scheduledRevoke[locator] = { stop }
|
||||
}
|
||||
|
||||
const nodeOutputs = ref<Record<string, ExecutedWsMessage['output']>>({})
|
||||
|
||||
// Reactive state for node preview images - mirrors app.nodePreviewImages
|
||||
const nodePreviewImages = ref<Record<string, string[]>>(
|
||||
app.nodePreviewImages || {}
|
||||
)
|
||||
|
||||
function getNodeOutputs(
|
||||
node: LGraphNode
|
||||
@@ -127,6 +151,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
}
|
||||
|
||||
app.nodeOutputs[nodeLocatorId] = outputs
|
||||
nodeOutputs.value[nodeLocatorId] = outputs
|
||||
}
|
||||
|
||||
function setNodeOutputs(
|
||||
@@ -139,17 +164,22 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
if (!filenames || !node) return
|
||||
|
||||
const locatorId =
|
||||
node.graph instanceof Subgraph
|
||||
? nodeIdToNodeLocatorId(node.id, node.graph ?? undefined)
|
||||
: `${node.id}`
|
||||
if (!locatorId) return
|
||||
if (typeof filenames === 'string') {
|
||||
setNodeOutputsByNodeId(
|
||||
node.id,
|
||||
setOutputsByLocatorId(
|
||||
locatorId,
|
||||
createOutputs([filenames], folder, isAnimated)
|
||||
)
|
||||
} else if (!Array.isArray(filenames)) {
|
||||
setNodeOutputsByNodeId(node.id, filenames)
|
||||
setOutputsByLocatorId(locatorId, filenames)
|
||||
} else {
|
||||
const resultItems = createOutputs(filenames, folder, isAnimated)
|
||||
if (!resultItems?.images?.length) return
|
||||
setNodeOutputsByNodeId(node.id, resultItems)
|
||||
setOutputsByLocatorId(locatorId, resultItems)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,26 +203,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node outputs by node ID.
|
||||
* Uses the current graph context to create the appropriate NodeLocatorId.
|
||||
*
|
||||
* @param nodeId - The node ID
|
||||
* @param outputs - The outputs to store
|
||||
* @param options - Options for setting outputs
|
||||
* @param options.merge - If true, merge with existing outputs (arrays are concatenated)
|
||||
*/
|
||||
function setNodeOutputsByNodeId(
|
||||
nodeId: string | number,
|
||||
outputs: ExecutedWsMessage['output'] | ResultItem,
|
||||
options: SetOutputOptions = {}
|
||||
) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node preview images by execution ID (hierarchical ID from backend).
|
||||
* Converts the execution ID to a NodeLocatorId before storing.
|
||||
@@ -206,8 +216,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
if (scheduledRevoke[nodeLocatorId]) {
|
||||
scheduledRevoke[nodeLocatorId].stop()
|
||||
delete scheduledRevoke[nodeLocatorId]
|
||||
}
|
||||
app.nodePreviewImages[nodeLocatorId] = previewImages
|
||||
nodePreviewImages.value[nodeLocatorId] = previewImages
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +236,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
previewImages: string[]
|
||||
) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
if (scheduledRevoke[nodeLocatorId]) {
|
||||
scheduledRevoke[nodeLocatorId].stop()
|
||||
delete scheduledRevoke[nodeLocatorId]
|
||||
}
|
||||
app.nodePreviewImages[nodeLocatorId] = previewImages
|
||||
nodePreviewImages.value[nodeLocatorId] = previewImages
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,8 +253,9 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
function revokePreviewsByExecutionId(executionId: string) {
|
||||
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
revokePreviewsByLocatorId(nodeLocatorId)
|
||||
scheduleRevoke(nodeLocatorId, () =>
|
||||
revokePreviewsByLocatorId(nodeLocatorId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,6 +273,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
}
|
||||
|
||||
delete app.nodePreviewImages[nodeLocatorId]
|
||||
delete nodePreviewImages.value[nodeLocatorId]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,6 +290,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
}
|
||||
}
|
||||
app.nodePreviewImages = {}
|
||||
nodePreviewImages.value = {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,18 +307,66 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove node outputs for a specific node
|
||||
* Clears both outputs and preview images
|
||||
*/
|
||||
function removeNodeOutputs(nodeId: number | string) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(Number(nodeId))
|
||||
if (!nodeLocatorId) return false
|
||||
|
||||
// Clear from app.nodeOutputs
|
||||
const hadOutputs = !!app.nodeOutputs[nodeLocatorId]
|
||||
delete app.nodeOutputs[nodeLocatorId]
|
||||
|
||||
// Clear from reactive state
|
||||
delete nodeOutputs.value[nodeLocatorId]
|
||||
|
||||
// Clear preview images
|
||||
if (app.nodePreviewImages[nodeLocatorId]) {
|
||||
delete app.nodePreviewImages[nodeLocatorId]
|
||||
delete nodePreviewImages.value[nodeLocatorId]
|
||||
}
|
||||
|
||||
return hadOutputs
|
||||
}
|
||||
|
||||
function restoreOutputs(
|
||||
outputs: Record<string, ExecutedWsMessage['output']>
|
||||
) {
|
||||
app.nodeOutputs = outputs
|
||||
nodeOutputs.value = outputs
|
||||
}
|
||||
|
||||
function resetAllOutputsAndPreviews() {
|
||||
app.nodeOutputs = {}
|
||||
nodeOutputs.value = {}
|
||||
revokeAllPreviews()
|
||||
}
|
||||
|
||||
return {
|
||||
// Getters
|
||||
getNodeOutputs,
|
||||
getNodeImageUrls,
|
||||
getNodePreviews,
|
||||
getPreviewParam,
|
||||
|
||||
// Setters
|
||||
setNodeOutputs,
|
||||
setNodeOutputsByExecutionId,
|
||||
setNodeOutputsByNodeId,
|
||||
setNodePreviewsByExecutionId,
|
||||
setNodePreviewsByNodeId,
|
||||
|
||||
// Cleanup
|
||||
revokePreviewsByExecutionId,
|
||||
revokeAllPreviews,
|
||||
revokeSubgraphPreviews,
|
||||
getPreviewParam
|
||||
removeNodeOutputs,
|
||||
restoreOutputs,
|
||||
resetAllOutputsAndPreviews,
|
||||
|
||||
// State
|
||||
nodeOutputs,
|
||||
nodePreviewImages
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Ref, computed, ref, toRaw } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
|
||||
import { RESERVED_BY_TEXT_INPUT } from '@/constants/reservedKeyCombos'
|
||||
import { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
|
||||
|
||||
export class KeybindingImpl implements Keybinding {
|
||||
commandId: string
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MenuItem } from 'primevue/menuitem'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { CORE_MENU_COMMANDS } from '@/constants/coreMenuCommands'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
import { useCommandStore } from './commandStore'
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ModelFile } from '@/platform/assets/schemas/assetSchema'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
/** (Internal helper) finds a value in a metadata object from any of a list of keys. */
|
||||
@@ -153,7 +156,10 @@ export class ModelFolder {
|
||||
models: Record<string, ComfyModelDef> = {}
|
||||
state: ResourceState = ResourceState.Uninitialized
|
||||
|
||||
constructor(public directory: string) {}
|
||||
constructor(
|
||||
public directory: string,
|
||||
private getModelsFunc: (folder: string) => Promise<ModelFile[]>
|
||||
) {}
|
||||
|
||||
get key(): string {
|
||||
return this.directory + '/'
|
||||
@@ -167,7 +173,7 @@ export class ModelFolder {
|
||||
return this
|
||||
}
|
||||
this.state = ResourceState.Loading
|
||||
const models = await api.getModels(this.directory)
|
||||
const models = await this.getModelsFunc(this.directory)
|
||||
for (const model of models) {
|
||||
this.models[`${model.pathIndex}/${model.name}`] = new ComfyModelDef(
|
||||
model.name,
|
||||
@@ -182,6 +188,7 @@ export class ModelFolder {
|
||||
|
||||
/** Model store handler, wraps individual per-folder model stores */
|
||||
export const useModelStore = defineStore('models', () => {
|
||||
const settingStore = useSettingStore()
|
||||
const modelFolderNames = ref<string[]>([])
|
||||
const modelFolderByName = ref<Record<string, ModelFolder>>({})
|
||||
const modelFolders = computed<ModelFolder[]>(() =>
|
||||
@@ -197,11 +204,22 @@ export const useModelStore = defineStore('models', () => {
|
||||
* Loads the model folders from the server
|
||||
*/
|
||||
async function loadModelFolders() {
|
||||
const resData = await api.getModelFolders()
|
||||
const useAssetAPI: boolean = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
|
||||
const resData = useAssetAPI
|
||||
? await assetService.getAssetModelFolders()
|
||||
: await api.getModelFolders()
|
||||
modelFolderNames.value = resData.map((folder) => folder.name)
|
||||
modelFolderByName.value = {}
|
||||
for (const folderName of modelFolderNames.value) {
|
||||
modelFolderByName.value[folderName] = new ModelFolder(folderName)
|
||||
const getModelsFunc = useAssetAPI
|
||||
? (folder: string) => assetService.getAssetModels(folder)
|
||||
: (folder: string) => api.getModels(folder)
|
||||
|
||||
modelFolderByName.value[folderName] = new ModelFolder(
|
||||
folderName,
|
||||
getModelsFunc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
/** Helper class that defines how to construct a node from a model. */
|
||||
export class ModelNodeProvider {
|
||||
@@ -22,6 +23,53 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
const modelToNodeMap = ref<Record<string, ModelNodeProvider[]>>({})
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const haveDefaultsLoaded = ref(false)
|
||||
|
||||
/** Internal computed for reactive caching of registered node types */
|
||||
const registeredNodeTypes = computed(() => {
|
||||
return new Set(
|
||||
Object.values(modelToNodeMap.value)
|
||||
.flat()
|
||||
.map((provider) => provider.nodeDef.name)
|
||||
)
|
||||
})
|
||||
|
||||
/** Internal computed for efficient reverse lookup: nodeType -> category */
|
||||
const nodeTypeToCategory = computed(() => {
|
||||
const lookup: Record<string, string> = {}
|
||||
for (const [category, providers] of Object.entries(modelToNodeMap.value)) {
|
||||
for (const provider of providers) {
|
||||
// Only store the first category for each node type (matches current assetService behavior)
|
||||
if (!lookup[provider.nodeDef.name]) {
|
||||
lookup[provider.nodeDef.name] = category
|
||||
}
|
||||
}
|
||||
}
|
||||
return lookup
|
||||
})
|
||||
|
||||
/** Get set of all registered node types for efficient lookup */
|
||||
function getRegisteredNodeTypes(): Set<string> {
|
||||
registerDefaults()
|
||||
return registeredNodeTypes.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category for a given node type.
|
||||
* Performs efficient O(1) lookup using cached reverse map.
|
||||
* @param nodeType The node type name to find the category for
|
||||
* @returns The category name, or undefined if not found
|
||||
*/
|
||||
function getCategoryForNodeType(nodeType: string): string | undefined {
|
||||
registerDefaults()
|
||||
|
||||
// Handle invalid input gracefully
|
||||
if (!nodeType || typeof nodeType !== 'string') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return nodeTypeToCategory.value[nodeType]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node provider for the given model type name.
|
||||
* @param modelType The name of the model type to get the node provider for.
|
||||
@@ -83,10 +131,16 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
quickRegister('loras', 'LoraLoaderModelOnly', 'lora_name')
|
||||
quickRegister('vae', 'VAELoader', 'vae_name')
|
||||
quickRegister('controlnet', 'ControlNetLoader', 'control_net_name')
|
||||
quickRegister('unet', 'UNETLoader', 'unet_name')
|
||||
quickRegister('upscale_models', 'UpscaleModelLoader', 'model_name')
|
||||
quickRegister('style_models', 'StyleModelLoader', 'style_model')
|
||||
quickRegister('gligen', 'GLIGENLoader', 'gligen_name')
|
||||
}
|
||||
|
||||
return {
|
||||
modelToNodeMap,
|
||||
getRegisteredNodeTypes,
|
||||
getCategoryForNodeType,
|
||||
getNodeProvider,
|
||||
getAllNodeProviders,
|
||||
registerNodeProvider,
|
||||
|
||||
@@ -2,15 +2,16 @@ import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { BookmarkCustomization } from '@/schemas/apiSchema'
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
import { useNodeDefStore } from './nodeDefStore'
|
||||
import { ComfyNodeDefImpl, createDummyFolderNodeDef } from './nodeDefStore'
|
||||
import type { ComfyNodeDefImpl } from './nodeDefStore'
|
||||
import { createDummyFolderNodeDef } from './nodeDefStore'
|
||||
import { buildNodeDefTree } from './nodeDefStore'
|
||||
import { useSettingStore } from './settingStore'
|
||||
|
||||
export const BOOKMARK_SETTING_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
|
||||
const BOOKMARK_SETTING_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
|
||||
|
||||
export const useNodeBookmarkStore = defineStore('nodeBookmark', () => {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ComfyOutputTypesSpec as ComfyOutputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import {
|
||||
type NodeSource,
|
||||
NodeSourceType,
|
||||
@@ -223,7 +224,7 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDefV1> = {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BuildNodeDefTreeOptions {
|
||||
interface BuildNodeDefTreeOptions {
|
||||
/**
|
||||
* Custom function to extract the tree path from a node definition.
|
||||
* If not provided, uses the default path based on nodeDef.nodePath.
|
||||
@@ -292,7 +293,13 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const showExperimental = ref(false)
|
||||
const nodeDefFilters = ref<NodeDefFilter[]>([])
|
||||
|
||||
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
||||
const nodeDefs = computed(() => {
|
||||
const subgraphStore = useSubgraphStore()
|
||||
return [
|
||||
...Object.values(nodeDefsByName.value),
|
||||
...subgraphStore.subgraphBlueprints
|
||||
]
|
||||
})
|
||||
const nodeDataTypes = computed(() => {
|
||||
const types = new Set<string>()
|
||||
for (const nodeDef of nodeDefs.value) {
|
||||
@@ -345,6 +352,16 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
return nodeDef
|
||||
}
|
||||
|
||||
function getInputSpecForWidget(
|
||||
node: LGraphNode,
|
||||
widgetName: string
|
||||
): InputSpecV2 | undefined {
|
||||
const nodeDef = fromLGraphNode(node)
|
||||
if (!nodeDef) return undefined
|
||||
|
||||
return nodeDef.inputs[widgetName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a node definition filter.
|
||||
* @param filter - The filter to register
|
||||
@@ -383,7 +400,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
})
|
||||
|
||||
// Subgraph nodes filter
|
||||
// @todo Remove this filter when subgraph v2 is released
|
||||
// Filter out litegraph typed subgraphs, saved blueprints are added in separately
|
||||
registerNodeDefFilter({
|
||||
id: 'core.subgraph',
|
||||
name: 'Hide Subgraph Nodes',
|
||||
@@ -417,6 +434,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
updateNodeDefs,
|
||||
addNodeDef,
|
||||
fromLGraphNode,
|
||||
getInputSpecForWidget,
|
||||
registerNodeDefFilter,
|
||||
unregisterNodeDefFilter
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
|
||||
import type {
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
ResultItem,
|
||||
StatusWsMessageStatus,
|
||||
@@ -11,14 +15,13 @@ import type {
|
||||
TaskStatus,
|
||||
TaskType
|
||||
} from '@/schemas/apiSchema'
|
||||
import type { ComfyWorkflowJSON, NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
|
||||
// Task type used in the API.
|
||||
export type APITaskType = 'queue' | 'history'
|
||||
type APITaskType = 'queue' | 'history'
|
||||
|
||||
export enum TaskItemDisplayStatus {
|
||||
Running = 'Running',
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { type ReleaseNote, useReleaseService } from '@/services/releaseService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { compareVersions, stringToLocale } from '@/utils/formatUtil'
|
||||
|
||||
// Store for managing release notes
|
||||
export const useReleaseStore = defineStore('release', () => {
|
||||
// State
|
||||
const releases = ref<ReleaseNote[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Services
|
||||
const releaseService = useReleaseService()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// Current ComfyUI version
|
||||
const currentComfyUIVersion = computed(
|
||||
() => systemStatsStore?.systemStats?.system?.comfyui_version ?? ''
|
||||
)
|
||||
|
||||
// Release data from settings
|
||||
const locale = computed(() => settingStore.get('Comfy.Locale'))
|
||||
const releaseVersion = computed(() =>
|
||||
settingStore.get('Comfy.Release.Version')
|
||||
)
|
||||
const releaseStatus = computed(() => settingStore.get('Comfy.Release.Status'))
|
||||
const releaseTimestamp = computed(() =>
|
||||
settingStore.get('Comfy.Release.Timestamp')
|
||||
)
|
||||
const showVersionUpdates = computed(() =>
|
||||
settingStore.get('Comfy.Notification.ShowVersionUpdates')
|
||||
)
|
||||
|
||||
// Most recent release
|
||||
const recentRelease = computed(() => {
|
||||
return releases.value[0] ?? null
|
||||
})
|
||||
|
||||
// 3 most recent releases
|
||||
const recentReleases = computed(() => {
|
||||
return releases.value.slice(0, 3)
|
||||
})
|
||||
|
||||
// Helper constants
|
||||
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000 // 3 days
|
||||
|
||||
// New version available?
|
||||
const isNewVersionAvailable = computed(
|
||||
() =>
|
||||
!!recentRelease.value &&
|
||||
compareVersions(
|
||||
recentRelease.value.version,
|
||||
currentComfyUIVersion.value
|
||||
) > 0
|
||||
)
|
||||
|
||||
const isLatestVersion = computed(
|
||||
() =>
|
||||
!!recentRelease.value &&
|
||||
!compareVersions(recentRelease.value.version, currentComfyUIVersion.value)
|
||||
)
|
||||
|
||||
const hasMediumOrHighAttention = computed(() =>
|
||||
recentReleases.value
|
||||
.slice(0, -1)
|
||||
.some(
|
||||
(release) =>
|
||||
release.attention === 'medium' || release.attention === 'high'
|
||||
)
|
||||
)
|
||||
|
||||
// Show toast if needed
|
||||
const shouldShowToast = computed(() => {
|
||||
// Only show on desktop version
|
||||
if (!isElectron()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isNewVersionAvailable.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip if low attention
|
||||
if (!hasMediumOrHighAttention.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip if user already skipped or changelog seen
|
||||
if (
|
||||
releaseVersion.value === recentRelease.value?.version &&
|
||||
['skipped', 'changelog seen'].includes(releaseStatus.value)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Show red-dot indicator
|
||||
const shouldShowRedDot = computed(() => {
|
||||
// Only show on desktop version
|
||||
if (!isElectron()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Already latest → no dot
|
||||
if (!isNewVersionAvailable.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { version } = recentRelease.value
|
||||
|
||||
// Changelog seen → clear dot
|
||||
if (
|
||||
releaseVersion.value === version &&
|
||||
releaseStatus.value === 'changelog seen'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Attention medium / high (levels 2 & 3)
|
||||
if (hasMediumOrHighAttention.value) {
|
||||
// Persist until changelog is opened
|
||||
return true
|
||||
}
|
||||
|
||||
// Attention low (level 1) and skipped → keep up to 3 d
|
||||
if (
|
||||
releaseVersion.value === version &&
|
||||
releaseStatus.value === 'skipped' &&
|
||||
releaseTimestamp.value &&
|
||||
Date.now() - releaseTimestamp.value >= THREE_DAYS_MS
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Not skipped → show
|
||||
return true
|
||||
})
|
||||
|
||||
// Show "What's New" popup
|
||||
const shouldShowPopup = computed(() => {
|
||||
// Only show on desktop version
|
||||
if (!isElectron()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isLatestVersion.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Hide if already seen
|
||||
if (
|
||||
releaseVersion.value === recentRelease.value.version &&
|
||||
releaseStatus.value === "what's new seen"
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Action handlers for user interactions
|
||||
async function handleSkipRelease(version: string): Promise<void> {
|
||||
if (
|
||||
version !== recentRelease.value?.version ||
|
||||
releaseStatus.value === 'changelog seen'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
await settingStore.set('Comfy.Release.Version', version)
|
||||
await settingStore.set('Comfy.Release.Status', 'skipped')
|
||||
await settingStore.set('Comfy.Release.Timestamp', Date.now())
|
||||
}
|
||||
|
||||
async function handleShowChangelog(version: string): Promise<void> {
|
||||
if (version !== recentRelease.value?.version) {
|
||||
return
|
||||
}
|
||||
|
||||
await settingStore.set('Comfy.Release.Version', version)
|
||||
await settingStore.set('Comfy.Release.Status', 'changelog seen')
|
||||
await settingStore.set('Comfy.Release.Timestamp', Date.now())
|
||||
}
|
||||
|
||||
async function handleWhatsNewSeen(version: string): Promise<void> {
|
||||
if (version !== recentRelease.value?.version) {
|
||||
return
|
||||
}
|
||||
|
||||
await settingStore.set('Comfy.Release.Version', version)
|
||||
await settingStore.set('Comfy.Release.Status', "what's new seen")
|
||||
await settingStore.set('Comfy.Release.Timestamp', Date.now())
|
||||
}
|
||||
|
||||
// Fetch releases from API
|
||||
async function fetchReleases(): Promise<void> {
|
||||
if (isLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip fetching if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip fetching if API nodes are disabled via argv
|
||||
if (
|
||||
systemStatsStore.systemStats?.system?.argv?.includes(
|
||||
'--disable-api-nodes'
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// Ensure system stats are loaded
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
|
||||
const fetchedReleases = await releaseService.getReleases({
|
||||
project: 'comfyui',
|
||||
current_version: currentComfyUIVersion.value,
|
||||
form_factor: systemStatsStore.getFormFactor(),
|
||||
locale: stringToLocale(locale.value)
|
||||
})
|
||||
|
||||
if (fetchedReleases !== null) {
|
||||
releases.value = fetchedReleases
|
||||
} else if (releaseService.error.value) {
|
||||
error.value = releaseService.error.value
|
||||
}
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize store
|
||||
async function initialize(): Promise<void> {
|
||||
await fetchReleases()
|
||||
}
|
||||
|
||||
return {
|
||||
releases,
|
||||
isLoading,
|
||||
error,
|
||||
recentRelease,
|
||||
recentReleases,
|
||||
shouldShowToast,
|
||||
shouldShowRedDot,
|
||||
shouldShowPopup,
|
||||
shouldShowUpdateButton: isNewVersionAvailable,
|
||||
handleSkipRelease,
|
||||
handleShowChangelog,
|
||||
handleWhatsNewSeen,
|
||||
fetchReleases,
|
||||
initialize
|
||||
}
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
|
||||
import type { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
|
||||
|
||||
export type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
/**
|
||||
* Current value.
|
||||
*/
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { Settings } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
export const getSettingInfo = (setting: SettingParams) => {
|
||||
const parts = setting.category || setting.id.split('.')
|
||||
return {
|
||||
category: parts[0] ?? 'Other',
|
||||
subCategory: parts[1] ?? 'Other'
|
||||
}
|
||||
}
|
||||
|
||||
export interface SettingTreeNode extends TreeNode {
|
||||
data?: SettingParams
|
||||
}
|
||||
|
||||
function tryMigrateDeprecatedValue(
|
||||
setting: SettingParams | undefined,
|
||||
value: unknown
|
||||
) {
|
||||
return setting?.migrateDeprecatedValue?.(value) ?? value
|
||||
}
|
||||
|
||||
function onChange(
|
||||
setting: SettingParams | undefined,
|
||||
newValue: unknown,
|
||||
oldValue: unknown
|
||||
) {
|
||||
if (setting?.onChange) {
|
||||
setting.onChange(newValue, oldValue)
|
||||
}
|
||||
// Backward compatibility with old settings dialog.
|
||||
// Some extensions still listens event emitted by the old settings dialog.
|
||||
// @ts-expect-error 'setting' is possibly 'undefined'.ts(18048)
|
||||
app.ui.settings.dispatchChange(setting.id, newValue, oldValue)
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore('setting', () => {
|
||||
const settingValues = ref<Record<string, any>>({})
|
||||
const settingsById = ref<Record<string, SettingParams>>({})
|
||||
|
||||
/**
|
||||
* Check if a setting's value exists, i.e. if the user has set it manually.
|
||||
* @param key - The key of the setting to check.
|
||||
* @returns Whether the setting exists.
|
||||
*/
|
||||
function exists(key: string) {
|
||||
return settingValues.value[key] !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting value.
|
||||
* @param key - The key of the setting to set.
|
||||
* @param value - The value to set.
|
||||
*/
|
||||
async function set<K extends keyof Settings>(key: K, value: Settings[K]) {
|
||||
// Clone the incoming value to prevent external mutations
|
||||
const clonedValue = _.cloneDeep(value)
|
||||
const newValue = tryMigrateDeprecatedValue(
|
||||
settingsById.value[key],
|
||||
clonedValue
|
||||
)
|
||||
const oldValue = get(key)
|
||||
if (newValue === oldValue) return
|
||||
|
||||
onChange(settingsById.value[key], newValue, oldValue)
|
||||
settingValues.value[key] = newValue
|
||||
await api.storeSetting(key, newValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting value.
|
||||
* @param key - The key of the setting to get.
|
||||
* @returns The value of the setting.
|
||||
*/
|
||||
function get<K extends keyof Settings>(key: K): Settings[K] {
|
||||
// Clone the value when returning to prevent external mutations
|
||||
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the setting params, asserting the type that is intentionally left off
|
||||
* of {@link settingsById}.
|
||||
* @param key The key of the setting to get.
|
||||
* @returns The setting.
|
||||
*/
|
||||
function getSettingById<K extends keyof Settings>(
|
||||
key: K
|
||||
): SettingParams<Settings[K]> | undefined {
|
||||
return settingsById.value[key] as SettingParams<Settings[K]> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value of a setting.
|
||||
* @param key - The key of the setting to get.
|
||||
* @returns The default value of the setting.
|
||||
*/
|
||||
function getDefaultValue<K extends keyof Settings>(
|
||||
key: K
|
||||
): Settings[K] | undefined {
|
||||
// Assertion: settingsById is not typed.
|
||||
const param = getSettingById(key)
|
||||
|
||||
if (param === undefined) return
|
||||
|
||||
const versionedDefault = getVersionedDefaultValue(key, param)
|
||||
|
||||
if (versionedDefault) {
|
||||
return versionedDefault
|
||||
}
|
||||
|
||||
return typeof param.defaultValue === 'function'
|
||||
? param.defaultValue()
|
||||
: param.defaultValue
|
||||
}
|
||||
|
||||
function getVersionedDefaultValue<
|
||||
K extends keyof Settings,
|
||||
TValue = Settings[K]
|
||||
>(key: K, param: SettingParams<TValue> | undefined): TValue | null {
|
||||
// get default versioned value, skipping if the key is 'Comfy.InstalledVersion' to prevent infinite loop
|
||||
const defaultsByInstallVersion = param?.defaultsByInstallVersion
|
||||
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
|
||||
const installedVersion = get('Comfy.InstalledVersion')
|
||||
|
||||
if (installedVersion) {
|
||||
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
|
||||
(a, b) => compareVersions(b, a)
|
||||
)
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
// Ensure the version is in a valid format before comparing
|
||||
if (!isSemVer(version)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (compareVersions(installedVersion, version) >= 0) {
|
||||
const versionedDefault = defaultsByInstallVersion[version]
|
||||
return typeof versionedDefault === 'function'
|
||||
? versionedDefault()
|
||||
: versionedDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a setting.
|
||||
* @param setting - The setting to register.
|
||||
*/
|
||||
function addSetting(setting: SettingParams) {
|
||||
if (!setting.id) {
|
||||
throw new Error('Settings must have an ID')
|
||||
}
|
||||
if (setting.id in settingsById.value) {
|
||||
throw new Error(`Setting ${setting.id} must have a unique ID.`)
|
||||
}
|
||||
|
||||
settingsById.value[setting.id] = setting
|
||||
|
||||
if (settingValues.value[setting.id] !== undefined) {
|
||||
settingValues.value[setting.id] = tryMigrateDeprecatedValue(
|
||||
setting,
|
||||
settingValues.value[setting.id]
|
||||
)
|
||||
}
|
||||
onChange(setting, get(setting.id), undefined)
|
||||
}
|
||||
|
||||
/*
|
||||
* Load setting values from server.
|
||||
* This needs to be called before any setting is registered.
|
||||
*/
|
||||
async function loadSettingValues() {
|
||||
if (Object.keys(settingsById.value).length) {
|
||||
throw new Error(
|
||||
'Setting values must be loaded before any setting is registered.'
|
||||
)
|
||||
}
|
||||
settingValues.value = await api.getSettings()
|
||||
}
|
||||
|
||||
return {
|
||||
settingValues,
|
||||
settingsById,
|
||||
addSetting,
|
||||
loadSettingValues,
|
||||
set,
|
||||
get,
|
||||
exists,
|
||||
getDefaultValue
|
||||
}
|
||||
})
|
||||
@@ -4,13 +4,12 @@ import { computed, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import type { DragAndScaleState } from '@/lib/litegraph/src/DragAndScale'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { findSubgraphPathById } from '@/utils/graphTraversalUtil'
|
||||
import { isNonNullish } from '@/utils/typeGuardUtil'
|
||||
|
||||
import { useCanvasStore } from './graphStore'
|
||||
import { useWorkflowStore } from './workflowStore'
|
||||
|
||||
/**
|
||||
* Stores the current subgraph navigation state; a stack representing subgraph
|
||||
* navigation history from the root graph to the subgraph that is currently
|
||||
|
||||
321
src/stores/subgraphStore.ts
Normal file
321
src/stores/subgraphStore.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
import type {
|
||||
ComfyNodeDef as ComfyNodeDefV1,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type { UserFile } from '@/stores/userFileStore'
|
||||
|
||||
async function confirmOverwrite(name: string): Promise<boolean | null> {
|
||||
return await useDialogService().confirm({
|
||||
title: t('subgraphStore.overwriteBlueprintTitle'),
|
||||
type: 'overwriteBlueprint',
|
||||
message: t('subgraphStore.overwriteBlueprint'),
|
||||
itemList: [name]
|
||||
})
|
||||
}
|
||||
|
||||
export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
class SubgraphBlueprint extends ComfyWorkflow {
|
||||
static override readonly basePath = 'subgraphs/'
|
||||
override readonly tintCanvasBg = '#22227740'
|
||||
|
||||
hasPromptedSave: boolean = false
|
||||
|
||||
constructor(
|
||||
options: { path: string; modified: number; size: number },
|
||||
confirmFirstSave: boolean = false
|
||||
) {
|
||||
super(options)
|
||||
this.hasPromptedSave = !confirmFirstSave
|
||||
}
|
||||
|
||||
validateSubgraph() {
|
||||
if (!this.activeState?.definitions)
|
||||
throw new Error(
|
||||
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
|
||||
)
|
||||
const { subgraphs } = this.activeState.definitions
|
||||
const { nodes } = this.activeState
|
||||
//Instanceof doesn't function as nodes are serialized
|
||||
function isSubgraphNode(node: ComfyNode) {
|
||||
return node && subgraphs.some((s) => s.id === node.type)
|
||||
}
|
||||
if (nodes.length == 1 && isSubgraphNode(nodes[0])) return
|
||||
const errors: Record<NodeId, NodeError> = {}
|
||||
//mark errors for all but first subgraph node
|
||||
let firstSubgraphFound = false
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (!firstSubgraphFound && isSubgraphNode(nodes[i])) {
|
||||
firstSubgraphFound = true
|
||||
continue
|
||||
}
|
||||
errors[nodes[i].id] = {
|
||||
errors: [],
|
||||
class_type: nodes[i].type,
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
useExecutionStore().lastNodeErrors = errors
|
||||
useCanvasStore().getCanvas().draw(true, true)
|
||||
throw new Error(
|
||||
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
|
||||
)
|
||||
}
|
||||
|
||||
override async save(): Promise<UserFile> {
|
||||
this.validateSubgraph()
|
||||
if (
|
||||
!this.hasPromptedSave &&
|
||||
useSettingStore().get('Comfy.Workflow.WarnBlueprintOverwrite')
|
||||
) {
|
||||
if (!(await confirmOverwrite(this.filename))) return this
|
||||
this.hasPromptedSave = true
|
||||
}
|
||||
const ret = await super.save()
|
||||
useSubgraphStore().updateDef(await this.load())
|
||||
return ret
|
||||
}
|
||||
|
||||
override async saveAs(path: string) {
|
||||
this.validateSubgraph()
|
||||
this.hasPromptedSave = true
|
||||
const ret = await super.saveAs(path)
|
||||
useSubgraphStore().updateDef(await this.load())
|
||||
return ret
|
||||
}
|
||||
override async load({
|
||||
force = false
|
||||
}: { force?: boolean } = {}): Promise<LoadedComfyWorkflow> {
|
||||
if (!force && this.isLoaded) return await super.load({ force })
|
||||
const loaded = await super.load({ force })
|
||||
const st = loaded.activeState
|
||||
const sg = (st.definitions?.subgraphs ?? []).find(
|
||||
(sg) => sg.id == st.nodes[0].type
|
||||
)
|
||||
if (!sg)
|
||||
throw new Error(
|
||||
'Loaded subgraph blueprint does not contain valid subgraph'
|
||||
)
|
||||
sg.name = st.nodes[0].title = this.filename
|
||||
return loaded
|
||||
}
|
||||
override async promptSave(): Promise<string | null> {
|
||||
return await useDialogService().prompt({
|
||||
title: t('subgraphStore.saveBlueprint'),
|
||||
message: t('subgraphStore.blueprintName') + ':',
|
||||
defaultValue: this.filename
|
||||
})
|
||||
}
|
||||
override unload(): void {
|
||||
//Skip unloading. Even if a workflow is closed after editing,
|
||||
//it must remain loaded in order to be added to the graph
|
||||
}
|
||||
}
|
||||
const subgraphCache: Record<string, LoadedComfyWorkflow> = {}
|
||||
const typePrefix = 'SubgraphBlueprint.'
|
||||
const subgraphDefCache = ref<Map<string, ComfyNodeDefImpl>>(new Map())
|
||||
const canvasStore = useCanvasStore()
|
||||
const subgraphBlueprints = computed(() => [
|
||||
...subgraphDefCache.value.values()
|
||||
])
|
||||
async function fetchSubgraphs() {
|
||||
async function loadBlueprint(options: {
|
||||
path: string
|
||||
modified: number
|
||||
size: number
|
||||
}): Promise<void> {
|
||||
const name = options.path.slice(0, -'.json'.length)
|
||||
options.path = SubgraphBlueprint.basePath + options.path
|
||||
const bp = await new SubgraphBlueprint(options, true).load()
|
||||
useWorkflowStore().attachWorkflow(bp)
|
||||
const nodeDef = convertToNodeDef(bp)
|
||||
|
||||
subgraphDefCache.value.set(name, nodeDef)
|
||||
subgraphCache[name] = bp
|
||||
}
|
||||
|
||||
const res = (
|
||||
await api.listUserDataFullInfo(SubgraphBlueprint.basePath)
|
||||
).filter((f) => f.path.endsWith('.json'))
|
||||
const settled = await Promise.allSettled(res.map(loadBlueprint))
|
||||
const errors = settled.filter((i) => 'reason' in i).map((i) => i.reason)
|
||||
errors.forEach((e) => console.error('Failed to load subgraph blueprint', e))
|
||||
if (errors.length > 0) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('subgraphStore.loadFailure'),
|
||||
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`,
|
||||
life: 6000
|
||||
})
|
||||
}
|
||||
}
|
||||
function convertToNodeDef(workflow: LoadedComfyWorkflow): ComfyNodeDefImpl {
|
||||
const name = workflow.filename
|
||||
const subgraphNode = workflow.changeTracker.initialState.nodes[0]
|
||||
if (!subgraphNode) throw new Error('Invalid Subgraph Blueprint')
|
||||
subgraphNode.inputs ??= []
|
||||
subgraphNode.outputs ??= []
|
||||
//NOTE: Types are cast to string. This is only used for input coloring on previews
|
||||
const inputs = Object.fromEntries(
|
||||
subgraphNode.inputs.map((i) => [
|
||||
i.name,
|
||||
[`${i.type}`, undefined] satisfies InputSpec
|
||||
])
|
||||
)
|
||||
let description = 'User generated subgraph blueprint'
|
||||
if (workflow.initialState.extra?.BlueprintDescription)
|
||||
description = `${workflow.initialState.extra.BlueprintDescription}`
|
||||
const nodedefv1: ComfyNodeDefV1 = {
|
||||
input: { required: inputs },
|
||||
output: subgraphNode.outputs.map((o) => `${o.type}`),
|
||||
output_name: subgraphNode.outputs.map((o) => o.name),
|
||||
name: typePrefix + name,
|
||||
display_name: name,
|
||||
description,
|
||||
category: 'Subgraph Blueprints',
|
||||
output_node: false,
|
||||
python_module: 'blueprint'
|
||||
}
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodedefv1)
|
||||
return nodeDefImpl
|
||||
}
|
||||
async function publishSubgraph() {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const subgraphNode = [...canvas.selectedItems][0]
|
||||
if (
|
||||
canvas.selectedItems.size !== 1 ||
|
||||
!(subgraphNode instanceof SubgraphNode)
|
||||
)
|
||||
throw new TypeError('Must have single SubgraphNode selected to publish')
|
||||
|
||||
const { nodes = [], subgraphs = [] } = canvas._serializeItems([
|
||||
subgraphNode
|
||||
])
|
||||
if (nodes.length != 1) {
|
||||
throw new TypeError('Must have single SubgraphNode selected to publish')
|
||||
}
|
||||
//create minimal workflow
|
||||
const workflowData = {
|
||||
revision: 0,
|
||||
last_node_id: subgraphNode.id,
|
||||
last_link_id: 0,
|
||||
nodes,
|
||||
links: [],
|
||||
version: 0.4,
|
||||
definitions: { subgraphs }
|
||||
}
|
||||
//prompt name
|
||||
const name = await useDialogService().prompt({
|
||||
title: t('subgraphStore.saveBlueprint'),
|
||||
message: t('subgraphStore.blueprintName') + ':',
|
||||
defaultValue: subgraphNode.title
|
||||
})
|
||||
if (!name) return
|
||||
if (subgraphDefCache.value.has(name) && !(await confirmOverwrite(name)))
|
||||
//User has chosen not to overwrite.
|
||||
return
|
||||
|
||||
//upload file
|
||||
const path = SubgraphBlueprint.basePath + name + '.json'
|
||||
const workflow = new SubgraphBlueprint({
|
||||
path,
|
||||
size: -1,
|
||||
modified: Date.now()
|
||||
})
|
||||
workflow.originalContent = JSON.stringify(workflowData)
|
||||
const loadedWorkflow = await workflow.load()
|
||||
//Mark non-temporary
|
||||
workflow.size = 1
|
||||
await workflow.save()
|
||||
//add to files list?
|
||||
useWorkflowStore().attachWorkflow(loadedWorkflow)
|
||||
subgraphDefCache.value.set(name, convertToNodeDef(loadedWorkflow))
|
||||
subgraphCache[name] = loadedWorkflow
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('subgraphStore.publishSuccess'),
|
||||
detail: t('subgraphStore.publishSuccessMessage'),
|
||||
life: 4000
|
||||
})
|
||||
}
|
||||
function updateDef(blueprint: LoadedComfyWorkflow) {
|
||||
subgraphDefCache.value.set(blueprint.filename, convertToNodeDef(blueprint))
|
||||
}
|
||||
async function editBlueprint(nodeType: string) {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
useWorkflowStore().attachWorkflow(subgraphCache[name])
|
||||
await useWorkflowService().openWorkflow(subgraphCache[name])
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (canvas.graph && 'subgraph' in canvas.graph.nodes[0])
|
||||
canvas.setGraph(canvas.graph.nodes[0].subgraph)
|
||||
}
|
||||
function getBlueprint(nodeType: string): ComfyWorkflowJSON {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
return subgraphCache[name].changeTracker.initialState
|
||||
}
|
||||
async function deleteBlueprint(nodeType: string) {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
if (
|
||||
!(await useDialogService().confirm({
|
||||
title: t('subgraphStore.confirmDeleteTitle'),
|
||||
type: 'delete',
|
||||
message: t('subgraphStore.confirmDelete'),
|
||||
itemList: [name]
|
||||
}))
|
||||
)
|
||||
return
|
||||
|
||||
await subgraphCache[name].delete()
|
||||
delete subgraphCache[name]
|
||||
subgraphDefCache.value.delete(name)
|
||||
}
|
||||
function isSubgraphBlueprint(
|
||||
workflow: unknown
|
||||
): workflow is SubgraphBlueprint {
|
||||
return workflow instanceof SubgraphBlueprint
|
||||
}
|
||||
|
||||
return {
|
||||
deleteBlueprint,
|
||||
editBlueprint,
|
||||
fetchSubgraphs,
|
||||
getBlueprint,
|
||||
isSubgraphBlueprint,
|
||||
publishSubgraph,
|
||||
subgraphBlueprints,
|
||||
typePrefix,
|
||||
updateDef
|
||||
}
|
||||
})
|
||||
@@ -1,32 +1,34 @@
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
export const useSystemStatsStore = defineStore('systemStats', () => {
|
||||
const systemStats = ref<SystemStats | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchSystemStats() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
const fetchSystemStatsData = async () => {
|
||||
try {
|
||||
systemStats.value = await api.getSystemStats()
|
||||
return await api.getSystemStats()
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'An error occurred while fetching system stats'
|
||||
console.error('Error fetching system stats:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
state: systemStats,
|
||||
isLoading,
|
||||
error,
|
||||
isReady: isInitialized,
|
||||
execute: refetchSystemStats
|
||||
} = useAsyncState<SystemStats | null>(
|
||||
fetchSystemStatsData,
|
||||
null, // initial value
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function getFormFactor(): string {
|
||||
if (!systemStats.value?.system?.os) {
|
||||
return 'other'
|
||||
@@ -62,7 +64,8 @@ export const useSystemStatsStore = defineStore('systemStats', () => {
|
||||
systemStats,
|
||||
isLoading,
|
||||
error,
|
||||
fetchSystemStats,
|
||||
isInitialized,
|
||||
refetchSystemStats,
|
||||
getFormFactor
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Within Vue component context, you can directly call useToast().add()
|
||||
// instead of going through the store.
|
||||
// The store is useful when you need to call it from outside the Vue component context.
|
||||
import { defineStore } from 'pinia'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useToastStore = defineStore('toast', () => {
|
||||
const messagesToAdd = ref<ToastMessageOptions[]>([])
|
||||
const messagesToRemove = ref<ToastMessageOptions[]>([])
|
||||
const removeAllRequested = ref(false)
|
||||
|
||||
function add(message: ToastMessageOptions) {
|
||||
messagesToAdd.value = [...messagesToAdd.value, message]
|
||||
}
|
||||
|
||||
function remove(message: ToastMessageOptions) {
|
||||
messagesToRemove.value = [...messagesToRemove.value, message]
|
||||
}
|
||||
|
||||
function removeAll() {
|
||||
removeAllRequested.value = true
|
||||
}
|
||||
|
||||
function addAlert(message: string) {
|
||||
add({ severity: 'warn', summary: 'Alert', detail: message })
|
||||
}
|
||||
|
||||
return {
|
||||
messagesToAdd,
|
||||
messagesToRemove,
|
||||
removeAllRequested,
|
||||
|
||||
add,
|
||||
remove,
|
||||
removeAll,
|
||||
addAlert
|
||||
}
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { UserDataFullInfo } from '@/schemas/apiSchema'
|
||||
import type { UserDataFullInfo } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
@@ -182,7 +182,7 @@ export class UserFile {
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadedUserFile extends UserFile {
|
||||
interface LoadedUserFile extends UserFile {
|
||||
isLoaded: true
|
||||
originalContent: string
|
||||
content: string
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import * as semver from 'semver'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import config from '@/config'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
const DISMISSAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
|
||||
export const useVersionCompatibilityStore = defineStore(
|
||||
'versionCompatibility',
|
||||
() => {
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const frontendVersion = computed(() => config.app_version)
|
||||
const backendVersion = computed(
|
||||
() => systemStatsStore.systemStats?.system?.comfyui_version ?? ''
|
||||
)
|
||||
const requiredFrontendVersion = computed(
|
||||
() =>
|
||||
systemStatsStore.systemStats?.system?.required_frontend_version ?? ''
|
||||
)
|
||||
|
||||
const isFrontendOutdated = computed(() => {
|
||||
if (
|
||||
!frontendVersion.value ||
|
||||
!requiredFrontendVersion.value ||
|
||||
!semver.valid(frontendVersion.value) ||
|
||||
!semver.valid(requiredFrontendVersion.value)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// Returns true if required version is greater than frontend version
|
||||
return semver.gt(requiredFrontendVersion.value, frontendVersion.value)
|
||||
})
|
||||
|
||||
const isFrontendNewer = computed(() => {
|
||||
// We don't warn about frontend being newer than backend
|
||||
// Only warn when frontend is outdated (behind required version)
|
||||
return false
|
||||
})
|
||||
|
||||
const hasVersionMismatch = computed(() => {
|
||||
return isFrontendOutdated.value
|
||||
})
|
||||
|
||||
const versionKey = computed(() => {
|
||||
if (
|
||||
!frontendVersion.value ||
|
||||
!backendVersion.value ||
|
||||
!requiredFrontendVersion.value
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}`
|
||||
})
|
||||
|
||||
// Use reactive storage for dismissals - creates a reactive ref that syncs with localStorage
|
||||
// All version mismatch dismissals are stored in a single object for clean localStorage organization
|
||||
const dismissalStorage = useStorage(
|
||||
'comfy.versionMismatch.dismissals',
|
||||
{} as Record<string, number>,
|
||||
localStorage,
|
||||
{
|
||||
serializer: {
|
||||
read: (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
write: (value: Record<string, number>) => JSON.stringify(value)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const isDismissed = computed(() => {
|
||||
if (!versionKey.value) return false
|
||||
|
||||
const dismissedUntil = dismissalStorage.value[versionKey.value]
|
||||
if (!dismissedUntil) return false
|
||||
|
||||
// Check if dismissal has expired
|
||||
return Date.now() < dismissedUntil
|
||||
})
|
||||
|
||||
const shouldShowWarning = computed(() => {
|
||||
return hasVersionMismatch.value && !isDismissed.value
|
||||
})
|
||||
|
||||
const warningMessage = computed(() => {
|
||||
if (isFrontendOutdated.value) {
|
||||
return {
|
||||
type: 'outdated' as const,
|
||||
frontendVersion: frontendVersion.value,
|
||||
requiredVersion: requiredFrontendVersion.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
async function checkVersionCompatibility() {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
}
|
||||
|
||||
function dismissWarning() {
|
||||
if (!versionKey.value) return
|
||||
|
||||
const dismissUntil = Date.now() + DISMISSAL_DURATION_MS
|
||||
dismissalStorage.value = {
|
||||
...dismissalStorage.value,
|
||||
[versionKey.value]: dismissUntil
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
await checkVersionCompatibility()
|
||||
}
|
||||
|
||||
return {
|
||||
frontendVersion,
|
||||
backendVersion,
|
||||
requiredFrontendVersion,
|
||||
hasVersionMismatch,
|
||||
shouldShowWarning,
|
||||
warningMessage,
|
||||
isFrontendOutdated,
|
||||
isFrontendNewer,
|
||||
checkVersionCompatibility,
|
||||
dismissWarning,
|
||||
initialize
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
type InputSpec as InputSpecV1,
|
||||
getInputSpecType
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { ComfyWidgetConstructor, ComfyWidgets } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import { ComfyWidgets } from '@/scripts/widgets'
|
||||
|
||||
export const useWidgetStore = defineStore('widget', () => {
|
||||
const coreWidgets = ComfyWidgets
|
||||
|
||||
@@ -1,722 +0,0 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
import { UserFile } from './userFileStore'
|
||||
|
||||
export class ComfyWorkflow extends UserFile {
|
||||
static readonly basePath = 'workflows/'
|
||||
|
||||
/**
|
||||
* The change tracker for the workflow. Non-reactive raw object.
|
||||
*/
|
||||
changeTracker: ChangeTracker | null = null
|
||||
/**
|
||||
* Whether the workflow has been modified comparing to the initial state.
|
||||
*/
|
||||
_isModified: boolean = false
|
||||
|
||||
/**
|
||||
* @param options The path, modified, and size of the workflow.
|
||||
* Note: path is the full path, including the 'workflows/' prefix.
|
||||
*/
|
||||
constructor(options: { path: string; modified: number; size: number }) {
|
||||
super(options.path, options.modified, options.size)
|
||||
}
|
||||
|
||||
override get key() {
|
||||
return this.path.substring(ComfyWorkflow.basePath.length)
|
||||
}
|
||||
|
||||
get activeState(): ComfyWorkflowJSON | null {
|
||||
return this.changeTracker?.activeState ?? null
|
||||
}
|
||||
|
||||
get initialState(): ComfyWorkflowJSON | null {
|
||||
return this.changeTracker?.initialState ?? null
|
||||
}
|
||||
|
||||
override get isLoaded(): boolean {
|
||||
return this.changeTracker !== null
|
||||
}
|
||||
|
||||
override get isModified(): boolean {
|
||||
return this._isModified
|
||||
}
|
||||
|
||||
override set isModified(value: boolean) {
|
||||
this._isModified = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the workflow content from remote storage. Directly returns the loaded
|
||||
* workflow if the content is already loaded.
|
||||
*
|
||||
* @param force Whether to force loading the content even if it is already loaded.
|
||||
* @returns this
|
||||
*/
|
||||
override async load({
|
||||
force = false
|
||||
}: { force?: boolean } = {}): Promise<LoadedComfyWorkflow> {
|
||||
await super.load({ force })
|
||||
if (!force && this.isLoaded) return this as LoadedComfyWorkflow
|
||||
|
||||
if (!this.originalContent) {
|
||||
throw new Error('[ASSERT] Workflow content should be loaded')
|
||||
}
|
||||
|
||||
// Note: originalContent is populated by super.load()
|
||||
console.debug('load and start tracking of workflow', this.path)
|
||||
this.changeTracker = markRaw(
|
||||
new ChangeTracker(
|
||||
this,
|
||||
/* initialState= */ JSON.parse(this.originalContent)
|
||||
)
|
||||
)
|
||||
return this as LoadedComfyWorkflow
|
||||
}
|
||||
|
||||
override unload(): void {
|
||||
console.debug('unload workflow', this.path)
|
||||
this.changeTracker = null
|
||||
super.unload()
|
||||
}
|
||||
|
||||
override async save() {
|
||||
this.content = JSON.stringify(this.activeState)
|
||||
// Force save to ensure the content is updated in remote storage incase
|
||||
// the isModified state is screwed by changeTracker.
|
||||
const ret = await super.save({ force: true })
|
||||
this.changeTracker?.reset()
|
||||
this.isModified = false
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the workflow as a new file.
|
||||
* @param path The path to save the workflow to. Note: with 'workflows/' prefix.
|
||||
* @returns this
|
||||
*/
|
||||
override async saveAs(path: string) {
|
||||
this.content = JSON.stringify(this.activeState)
|
||||
return await super.saveAs(path)
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadedComfyWorkflow extends ComfyWorkflow {
|
||||
isLoaded: true
|
||||
originalContent: string
|
||||
content: string
|
||||
changeTracker: ChangeTracker
|
||||
initialState: ComfyWorkflowJSON
|
||||
activeState: ComfyWorkflowJSON
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed store interface for the workflow store.
|
||||
* Explicitly typed to avoid trigger following error:
|
||||
* error TS7056: The inferred type of this node exceeds the maximum length the
|
||||
* compiler will serialize. An explicit type annotation is needed.
|
||||
*/
|
||||
export interface WorkflowStore {
|
||||
activeWorkflow: LoadedComfyWorkflow | null
|
||||
isActive: (workflow: ComfyWorkflow) => boolean
|
||||
openWorkflows: ComfyWorkflow[]
|
||||
openedWorkflowIndexShift: (shift: number) => ComfyWorkflow | null
|
||||
openWorkflow: (workflow: ComfyWorkflow) => Promise<LoadedComfyWorkflow>
|
||||
openWorkflowsInBackground: (paths: {
|
||||
left?: string[]
|
||||
right?: string[]
|
||||
}) => void
|
||||
isOpen: (workflow: ComfyWorkflow) => boolean
|
||||
isBusy: boolean
|
||||
closeWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
createTemporary: (
|
||||
path?: string,
|
||||
workflowData?: ComfyWorkflowJSON
|
||||
) => ComfyWorkflow
|
||||
renameWorkflow: (workflow: ComfyWorkflow, newPath: string) => Promise<void>
|
||||
deleteWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
saveWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
|
||||
workflows: ComfyWorkflow[]
|
||||
bookmarkedWorkflows: ComfyWorkflow[]
|
||||
persistedWorkflows: ComfyWorkflow[]
|
||||
modifiedWorkflows: ComfyWorkflow[]
|
||||
getWorkflowByPath: (path: string) => ComfyWorkflow | null
|
||||
syncWorkflows: (dir?: string) => Promise<void>
|
||||
reorderWorkflows: (from: number, to: number) => void
|
||||
|
||||
/** `true` if any subgraph is currently being viewed. */
|
||||
isSubgraphActive: boolean
|
||||
activeSubgraph: Subgraph | undefined
|
||||
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
||||
updateActiveGraph: () => void
|
||||
executionIdToCurrentId: (id: string) => any
|
||||
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
|
||||
nodeExecutionIdToNodeLocatorId: (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
) => NodeLocatorId | null
|
||||
nodeLocatorIdToNodeId: (locatorId: NodeLocatorId | string) => NodeId | null
|
||||
nodeLocatorIdToNodeExecutionId: (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
) => NodeExecutionId | null
|
||||
}
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
/**
|
||||
* Detach the workflow from the store. lightweight helper function.
|
||||
* @param workflow The workflow to detach.
|
||||
* @returns The index of the workflow in the openWorkflowPaths array, or -1 if the workflow was not open.
|
||||
*/
|
||||
const detachWorkflow = (workflow: ComfyWorkflow) => {
|
||||
delete workflowLookup.value[workflow.path]
|
||||
const index = openWorkflowPaths.value.indexOf(workflow.path)
|
||||
if (index !== -1) {
|
||||
openWorkflowPaths.value = openWorkflowPaths.value.filter(
|
||||
(path) => path !== workflow.path
|
||||
)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the workflow to the store. lightweight helper function.
|
||||
* @param workflow The workflow to attach.
|
||||
* @param openIndex The index to open the workflow at.
|
||||
*/
|
||||
const attachWorkflow = (workflow: ComfyWorkflow, openIndex: number = -1) => {
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
|
||||
if (openIndex !== -1) {
|
||||
openWorkflowPaths.value.splice(openIndex, 0, workflow.path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The active workflow currently being edited.
|
||||
*/
|
||||
const activeWorkflow = ref<LoadedComfyWorkflow | null>(null)
|
||||
const isActive = (workflow: ComfyWorkflow) =>
|
||||
activeWorkflow.value?.path === workflow.path
|
||||
/**
|
||||
* All workflows.
|
||||
*/
|
||||
const workflowLookup = ref<Record<string, ComfyWorkflow>>({})
|
||||
const workflows = computed<ComfyWorkflow[]>(() =>
|
||||
Object.values(workflowLookup.value)
|
||||
)
|
||||
const getWorkflowByPath = (path: string): ComfyWorkflow | null =>
|
||||
workflowLookup.value[path] ?? null
|
||||
|
||||
/**
|
||||
* The paths of the open workflows. It is setup as a ref to allow user
|
||||
* to reorder the workflows opened.
|
||||
*/
|
||||
const openWorkflowPaths = ref<string[]>([])
|
||||
const openWorkflowPathSet = computed(() => new Set(openWorkflowPaths.value))
|
||||
const openWorkflows = computed(() =>
|
||||
openWorkflowPaths.value.map((path) => workflowLookup.value[path])
|
||||
)
|
||||
const reorderWorkflows = (from: number, to: number) => {
|
||||
const movedTab = openWorkflowPaths.value[from]
|
||||
openWorkflowPaths.value.splice(from, 1)
|
||||
openWorkflowPaths.value.splice(to, 0, movedTab)
|
||||
}
|
||||
const isOpen = (workflow: ComfyWorkflow) =>
|
||||
openWorkflowPathSet.value.has(workflow.path)
|
||||
|
||||
/**
|
||||
* Add paths to the list of open workflow paths without loading the files
|
||||
* or changing the active workflow.
|
||||
*
|
||||
* @param paths - The workflows to open, specified as:
|
||||
* - `left`: Workflows to be added to the left.
|
||||
* - `right`: Workflows to be added to the right.
|
||||
*
|
||||
* Invalid paths (non-strings or paths not found in `workflowLookup.value`)
|
||||
* will be ignored. Duplicate paths are automatically removed.
|
||||
*/
|
||||
const openWorkflowsInBackground = (paths: {
|
||||
left?: string[]
|
||||
right?: string[]
|
||||
}) => {
|
||||
const { left = [], right = [] } = paths
|
||||
if (!left.length && !right.length) return
|
||||
|
||||
const isValidPath = (
|
||||
path: unknown
|
||||
): path is keyof typeof workflowLookup.value =>
|
||||
typeof path === 'string' && path in workflowLookup.value
|
||||
|
||||
openWorkflowPaths.value = _.union(
|
||||
left,
|
||||
openWorkflowPaths.value,
|
||||
right
|
||||
).filter(isValidPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the workflow as the active workflow.
|
||||
* @param workflow The workflow to open.
|
||||
*/
|
||||
const openWorkflow = async (
|
||||
workflow: ComfyWorkflow
|
||||
): Promise<LoadedComfyWorkflow> => {
|
||||
if (isActive(workflow)) return workflow as LoadedComfyWorkflow
|
||||
|
||||
if (!openWorkflowPaths.value.includes(workflow.path)) {
|
||||
openWorkflowPaths.value.push(workflow.path)
|
||||
}
|
||||
const loadedWorkflow = await workflow.load()
|
||||
activeWorkflow.value = loadedWorkflow
|
||||
console.debug('[workflowStore] open workflow', workflow.path)
|
||||
return loadedWorkflow
|
||||
}
|
||||
|
||||
const getUnconflictedPath = (basePath: string): string => {
|
||||
const { directory, filename, suffix } = getPathDetails(basePath)
|
||||
let counter = 2
|
||||
let newPath = basePath
|
||||
while (workflowLookup.value[newPath]) {
|
||||
newPath = `${directory}/${filename} (${counter}).${suffix}`
|
||||
counter++
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
const createTemporary = (path?: string, workflowData?: ComfyWorkflowJSON) => {
|
||||
const fullPath = getUnconflictedPath(
|
||||
ComfyWorkflow.basePath + (path ?? 'Unsaved Workflow.json')
|
||||
)
|
||||
const workflow = new ComfyWorkflow({
|
||||
path: fullPath,
|
||||
modified: Date.now(),
|
||||
size: -1
|
||||
})
|
||||
|
||||
workflow.originalContent = workflow.content = workflowData
|
||||
? JSON.stringify(workflowData)
|
||||
: defaultGraphJSON
|
||||
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
return workflow
|
||||
}
|
||||
|
||||
const closeWorkflow = async (workflow: ComfyWorkflow) => {
|
||||
openWorkflowPaths.value = openWorkflowPaths.value.filter(
|
||||
(path) => path !== workflow.path
|
||||
)
|
||||
if (workflow.isTemporary) {
|
||||
// Clear thumbnail when temporary workflow is closed
|
||||
clearThumbnail(workflow.key)
|
||||
delete workflowLookup.value[workflow.path]
|
||||
} else {
|
||||
workflow.unload()
|
||||
}
|
||||
console.debug('[workflowStore] close workflow', workflow.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workflow at the given index shift from the active workflow.
|
||||
* @param shift The shift to the next workflow. Positive for next, negative for previous.
|
||||
* @returns The next workflow or null if the shift is out of bounds.
|
||||
*/
|
||||
const openedWorkflowIndexShift = (shift: number): ComfyWorkflow | null => {
|
||||
const index = openWorkflowPaths.value.indexOf(
|
||||
activeWorkflow.value?.path ?? ''
|
||||
)
|
||||
|
||||
if (index !== -1) {
|
||||
const length = openWorkflows.value.length
|
||||
const nextIndex = (index + shift + length) % length
|
||||
const nextWorkflow = openWorkflows.value[nextIndex]
|
||||
return nextWorkflow ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const persistedWorkflows = computed(() =>
|
||||
Array.from(workflows.value).filter((workflow) => workflow.isPersisted)
|
||||
)
|
||||
const syncWorkflows = async (dir: string = '') => {
|
||||
await syncEntities(
|
||||
dir ? 'workflows/' + dir : 'workflows',
|
||||
workflowLookup.value,
|
||||
(file) =>
|
||||
new ComfyWorkflow({
|
||||
path: file.path,
|
||||
modified: file.modified,
|
||||
size: file.size
|
||||
}),
|
||||
(existingWorkflow, file) => {
|
||||
existingWorkflow.lastModified = file.modified
|
||||
existingWorkflow.size = file.size
|
||||
existingWorkflow.unload()
|
||||
},
|
||||
/* exclude */ (workflow) => workflow.isTemporary
|
||||
)
|
||||
}
|
||||
|
||||
const bookmarkStore = useWorkflowBookmarkStore()
|
||||
const bookmarkedWorkflows = computed(() =>
|
||||
workflows.value.filter((workflow) =>
|
||||
bookmarkStore.isBookmarked(workflow.path)
|
||||
)
|
||||
)
|
||||
const modifiedWorkflows = computed(() =>
|
||||
workflows.value.filter((workflow) => workflow.isModified)
|
||||
)
|
||||
|
||||
/** A filesystem operation is currently in progress (e.g. save, rename, delete) */
|
||||
const isBusy = ref<boolean>(false)
|
||||
const { moveWorkflowThumbnail, clearThumbnail } = useWorkflowThumbnail()
|
||||
|
||||
const renameWorkflow = async (workflow: ComfyWorkflow, newPath: string) => {
|
||||
isBusy.value = true
|
||||
try {
|
||||
// Capture all needed values upfront
|
||||
const oldPath = workflow.path
|
||||
const oldKey = workflow.key
|
||||
const wasBookmarked = bookmarkStore.isBookmarked(oldPath)
|
||||
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
// Perform the actual rename operation first
|
||||
try {
|
||||
await workflow.rename(newPath)
|
||||
} finally {
|
||||
attachWorkflow(workflow, openIndex)
|
||||
}
|
||||
|
||||
// Move thumbnail from old key to new key (using workflow keys, not full paths)
|
||||
const newKey = workflow.key
|
||||
moveWorkflowThumbnail(oldKey, newKey)
|
||||
// Update bookmarks
|
||||
if (wasBookmarked) {
|
||||
await bookmarkStore.setBookmarked(oldPath, false)
|
||||
await bookmarkStore.setBookmarked(newPath, true)
|
||||
}
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteWorkflow = async (workflow: ComfyWorkflow) => {
|
||||
isBusy.value = true
|
||||
try {
|
||||
await workflow.delete()
|
||||
if (bookmarkStore.isBookmarked(workflow.path)) {
|
||||
await bookmarkStore.setBookmarked(workflow.path, false)
|
||||
}
|
||||
// Clear thumbnail when workflow is deleted
|
||||
clearThumbnail(workflow.key)
|
||||
delete workflowLookup.value[workflow.path]
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a workflow.
|
||||
* @param workflow The workflow to save.
|
||||
*/
|
||||
const saveWorkflow = async (workflow: ComfyWorkflow) => {
|
||||
isBusy.value = true
|
||||
try {
|
||||
// Detach the workflow and re-attach to force refresh the tree objects.
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
try {
|
||||
await workflow.save()
|
||||
} finally {
|
||||
attachWorkflow(workflow, openIndex)
|
||||
}
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** @see WorkflowStore.isSubgraphActive */
|
||||
const isSubgraphActive = ref(false)
|
||||
|
||||
/** @see WorkflowStore.activeSubgraph */
|
||||
const activeSubgraph = shallowRef<Raw<Subgraph>>()
|
||||
|
||||
/** @see WorkflowStore.updateActiveGraph */
|
||||
const updateActiveGraph = () => {
|
||||
const subgraph = comfyApp.canvas?.subgraph
|
||||
activeSubgraph.value = subgraph ? markRaw(subgraph) : undefined
|
||||
if (!comfyApp.canvas) return
|
||||
|
||||
isSubgraphActive.value = isSubgraph(subgraph)
|
||||
}
|
||||
|
||||
const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => {
|
||||
const node = graph.getNodeById(id)
|
||||
if (node?.isSubgraphNode()) return node.subgraph
|
||||
}
|
||||
|
||||
const getSubgraphsFromInstanceIds = (
|
||||
currentGraph: LGraph | Subgraph,
|
||||
subgraphNodeIds: string[],
|
||||
subgraphs: Subgraph[] = []
|
||||
): Subgraph[] => {
|
||||
const currentPart = subgraphNodeIds.shift()
|
||||
if (currentPart === undefined) return subgraphs
|
||||
|
||||
const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph)
|
||||
if (subgraph === undefined) throw new Error('Subgraph not found')
|
||||
|
||||
subgraphs.push(subgraph)
|
||||
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||
}
|
||||
|
||||
const executionIdToCurrentId = (id: string) => {
|
||||
const subgraph = activeSubgraph.value
|
||||
|
||||
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||
if (!id.includes(':')) {
|
||||
return !subgraph ? id : undefined
|
||||
} else if (!subgraph) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the execution ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// Start from the root graph
|
||||
const { graph } = comfyApp
|
||||
|
||||
// If the last subgraph is the active subgraph, return the node ID
|
||||
const subgraphs = getSubgraphsFromInstanceIds(graph, subgraphNodeIds)
|
||||
if (subgraphs.at(-1) === subgraph) {
|
||||
return subgraphNodeIds.at(-1)
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeWorkflow, updateActiveGraph)
|
||||
|
||||
/**
|
||||
* Convert a node ID to a NodeLocatorId
|
||||
* @param nodeId The local node ID
|
||||
* @param subgraph The subgraph containing the node (defaults to active subgraph)
|
||||
* @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is)
|
||||
*/
|
||||
const nodeIdToNodeLocatorId = (
|
||||
nodeId: NodeId,
|
||||
subgraph?: Subgraph
|
||||
): NodeLocatorId => {
|
||||
const targetSubgraph = subgraph ?? activeSubgraph.value
|
||||
if (!targetSubgraph) {
|
||||
// Node is in the root graph, return the node ID as-is
|
||||
return String(nodeId)
|
||||
}
|
||||
|
||||
return createNodeLocatorId(targetSubgraph.id, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an execution ID to a NodeLocatorId
|
||||
* @param nodeExecutionId The execution node ID (e.g., "123:456:789")
|
||||
* @returns The NodeLocatorId or null if conversion fails
|
||||
*/
|
||||
const nodeExecutionIdToNodeLocatorId = (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
): NodeLocatorId | null => {
|
||||
// Handle simple node IDs (root graph - no colons)
|
||||
if (!nodeExecutionId.includes(':')) {
|
||||
return nodeExecutionId
|
||||
}
|
||||
|
||||
const parts = parseNodeExecutionId(nodeExecutionId)
|
||||
if (!parts || parts.length === 0) return null
|
||||
|
||||
const nodeId = parts[parts.length - 1]
|
||||
const subgraphNodeIds = parts.slice(0, -1)
|
||||
|
||||
if (subgraphNodeIds.length === 0) {
|
||||
// Node is in root graph, return the node ID as-is
|
||||
return String(nodeId)
|
||||
}
|
||||
|
||||
try {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
subgraphNodeIds.map((id) => String(id))
|
||||
)
|
||||
const immediateSubgraph = subgraphs[subgraphs.length - 1]
|
||||
return createNodeLocatorId(immediateSubgraph.id, nodeId)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the node ID from a NodeLocatorId
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The local node ID or null if invalid
|
||||
*/
|
||||
const nodeLocatorIdToNodeId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): NodeId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
return parsed?.localNodeId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution ID for a specific context
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @param targetSubgraph The subgraph context (defaults to active subgraph)
|
||||
* @returns The execution ID or null if the node is not accessible from the target context
|
||||
*/
|
||||
const nodeLocatorIdToNodeExecutionId = (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
): NodeExecutionId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
if (!parsed) return null
|
||||
|
||||
const { subgraphUuid, localNodeId } = parsed
|
||||
|
||||
// If no subgraph UUID, this is a root graph node
|
||||
if (!subgraphUuid) {
|
||||
return String(localNodeId)
|
||||
}
|
||||
|
||||
// Find the path from root to the subgraph with this UUID
|
||||
const findSubgraphPath = (
|
||||
graph: LGraph | Subgraph,
|
||||
targetUuid: string,
|
||||
path: NodeId[] = []
|
||||
): NodeId[] | null => {
|
||||
if (isSubgraph(graph) && graph.id === targetUuid) {
|
||||
return path
|
||||
}
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (node.isSubgraphNode() && node.subgraph) {
|
||||
const result = findSubgraphPath(node.subgraph, targetUuid, [
|
||||
...path,
|
||||
node.id
|
||||
])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const path = findSubgraphPath(comfyApp.graph, subgraphUuid)
|
||||
if (!path) return null
|
||||
|
||||
// If we have a target subgraph, check if the path goes through it
|
||||
if (
|
||||
targetSubgraph &&
|
||||
!path.some((_, idx) => {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
path.slice(0, idx + 1).map((id) => String(id))
|
||||
)
|
||||
return subgraphs[subgraphs.length - 1] === targetSubgraph
|
||||
})
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createNodeExecutionId([...path, localNodeId])
|
||||
}
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
isActive,
|
||||
openWorkflows,
|
||||
openedWorkflowIndexShift,
|
||||
openWorkflow,
|
||||
openWorkflowsInBackground,
|
||||
isOpen,
|
||||
isBusy,
|
||||
closeWorkflow,
|
||||
createTemporary,
|
||||
renameWorkflow,
|
||||
deleteWorkflow,
|
||||
saveWorkflow,
|
||||
reorderWorkflows,
|
||||
|
||||
workflows,
|
||||
bookmarkedWorkflows,
|
||||
persistedWorkflows,
|
||||
modifiedWorkflows,
|
||||
getWorkflowByPath,
|
||||
syncWorkflows,
|
||||
|
||||
isSubgraphActive,
|
||||
activeSubgraph,
|
||||
updateActiveGraph,
|
||||
executionIdToCurrentId,
|
||||
nodeIdToNodeLocatorId,
|
||||
nodeExecutionIdToNodeLocatorId,
|
||||
nodeLocatorIdToNodeId,
|
||||
nodeLocatorIdToNodeExecutionId
|
||||
}
|
||||
}) satisfies () => WorkflowStore
|
||||
|
||||
export const useWorkflowBookmarkStore = defineStore('workflowBookmark', () => {
|
||||
const bookmarks = ref<Set<string>>(new Set())
|
||||
|
||||
const isBookmarked = (path: string) => bookmarks.value.has(path)
|
||||
|
||||
const loadBookmarks = async () => {
|
||||
const resp = await api.getUserData('workflows/.index.json')
|
||||
if (resp.status === 200) {
|
||||
const info = await resp.json()
|
||||
bookmarks.value = new Set(info?.favorites ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
const saveBookmarks = async () => {
|
||||
await api.storeUserData('workflows/.index.json', {
|
||||
favorites: Array.from(bookmarks.value)
|
||||
})
|
||||
}
|
||||
|
||||
const setBookmarked = async (path: string, value: boolean) => {
|
||||
if (bookmarks.value.has(path) === value) return
|
||||
if (value) {
|
||||
bookmarks.value.add(path)
|
||||
} else {
|
||||
bookmarks.value.delete(path)
|
||||
}
|
||||
await saveBookmarks()
|
||||
}
|
||||
|
||||
const toggleBookmarked = async (path: string) => {
|
||||
await setBookmarked(path, !bookmarks.value.has(path))
|
||||
}
|
||||
|
||||
return {
|
||||
isBookmarked,
|
||||
loadBookmarks,
|
||||
saveBookmarks,
|
||||
setBookmarked,
|
||||
toggleBookmarked
|
||||
}
|
||||
})
|
||||
@@ -1,224 +0,0 @@
|
||||
import axios, { type AxiosError } from 'axios'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import type {
|
||||
TemplateGroup,
|
||||
TemplateInfo,
|
||||
WorkflowTemplates
|
||||
} from '@/types/workflowTemplateTypes'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const SHOULD_SORT_CATEGORIES = new Set([
|
||||
// API Node templates should be strictly sorted by name to avoid any
|
||||
// favoritism or bias towards a particular API. Other categories can
|
||||
// have their ordering specified in index.json freely.
|
||||
'Image API',
|
||||
'Video API'
|
||||
])
|
||||
|
||||
export const useWorkflowTemplatesStore = defineStore(
|
||||
'workflowTemplates',
|
||||
() => {
|
||||
const customTemplates = shallowRef<{ [moduleName: string]: string[] }>({})
|
||||
const coreTemplates = shallowRef<WorkflowTemplates[]>([])
|
||||
const isLoaded = ref(false)
|
||||
|
||||
/**
|
||||
* Sort a list of templates in alphabetical order by localized display name.
|
||||
*/
|
||||
const sortTemplateList = (templates: TemplateInfo[]) =>
|
||||
templates.sort((a, b) => {
|
||||
const aName = st(
|
||||
`templateWorkflows.name.${normalizeI18nKey(a.name)}`,
|
||||
a.title ?? a.name
|
||||
)
|
||||
const bName = st(
|
||||
`templateWorkflows.name.${normalizeI18nKey(b.name)}`,
|
||||
b.name
|
||||
)
|
||||
return aName.localeCompare(bName)
|
||||
})
|
||||
|
||||
/**
|
||||
* Sort any template categories (grouped templates) that should be sorted.
|
||||
* Leave other categories' templates in their original order specified in index.json
|
||||
*/
|
||||
const sortCategoryTemplates = (categories: WorkflowTemplates[]) =>
|
||||
categories.map((category) => {
|
||||
if (SHOULD_SORT_CATEGORIES.has(category.title)) {
|
||||
return {
|
||||
...category,
|
||||
templates: sortTemplateList(category.templates)
|
||||
}
|
||||
}
|
||||
return category
|
||||
})
|
||||
|
||||
/**
|
||||
* Add localization fields to a template.
|
||||
*/
|
||||
const addLocalizedFieldsToTemplate = (
|
||||
template: TemplateInfo,
|
||||
categoryTitle: string
|
||||
) => ({
|
||||
...template,
|
||||
localizedTitle: st(
|
||||
`templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
|
||||
template.title ?? template.name
|
||||
),
|
||||
localizedDescription: st(
|
||||
`templateWorkflows.templateDescription.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
|
||||
template.description
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Add localization fields to all templates in a list of templates.
|
||||
*/
|
||||
const localizeTemplateList = (
|
||||
templates: TemplateInfo[],
|
||||
categoryTitle: string
|
||||
) =>
|
||||
templates.map((template) =>
|
||||
addLocalizedFieldsToTemplate(template, categoryTitle)
|
||||
)
|
||||
|
||||
/**
|
||||
* Add localization fields to a template category and all its constituent templates.
|
||||
*/
|
||||
const localizeTemplateCategory = (templateCategory: WorkflowTemplates) => ({
|
||||
...templateCategory,
|
||||
localizedTitle: st(
|
||||
`templateWorkflows.category.${normalizeI18nKey(templateCategory.title)}`,
|
||||
templateCategory.title ?? templateCategory.moduleName
|
||||
),
|
||||
templates: localizeTemplateList(
|
||||
templateCategory.templates,
|
||||
templateCategory.title
|
||||
)
|
||||
})
|
||||
|
||||
// Create an "All" category that combines all templates
|
||||
const createAllCategory = () => {
|
||||
// First, get core templates with source module added
|
||||
const coreTemplatesWithSourceModule = coreTemplates.value.flatMap(
|
||||
(category) =>
|
||||
// For each template in each category, add the sourceModule and pass through any localized fields
|
||||
category.templates.map((template) => {
|
||||
// Get localized template with its original category title for i18n lookup
|
||||
const localizedTemplate = addLocalizedFieldsToTemplate(
|
||||
template,
|
||||
category.title
|
||||
)
|
||||
return {
|
||||
...localizedTemplate,
|
||||
sourceModule: category.moduleName
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Now handle custom templates
|
||||
const customTemplatesWithSourceModule = Object.entries(
|
||||
customTemplates.value
|
||||
).flatMap(([moduleName, templates]) =>
|
||||
templates.map((name) => ({
|
||||
name,
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'jpg',
|
||||
description: name,
|
||||
sourceModule: moduleName
|
||||
}))
|
||||
)
|
||||
|
||||
return {
|
||||
moduleName: 'all',
|
||||
title: 'All',
|
||||
localizedTitle: st('templateWorkflows.category.All', 'All Templates'),
|
||||
templates: [
|
||||
...coreTemplatesWithSourceModule,
|
||||
...customTemplatesWithSourceModule
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const groupedTemplates = computed<TemplateGroup[]>(() => {
|
||||
// Get regular categories
|
||||
const allTemplates = [
|
||||
...sortCategoryTemplates(coreTemplates.value).map(
|
||||
localizeTemplateCategory
|
||||
),
|
||||
...Object.entries(customTemplates.value).map(
|
||||
([moduleName, templates]) => ({
|
||||
moduleName,
|
||||
title: moduleName,
|
||||
localizedTitle: st(
|
||||
`templateWorkflows.category.${normalizeI18nKey(moduleName)}`,
|
||||
moduleName
|
||||
),
|
||||
templates: templates.map((name) => ({
|
||||
name,
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'jpg',
|
||||
description: name
|
||||
}))
|
||||
})
|
||||
)
|
||||
]
|
||||
|
||||
// Group templates by their main category
|
||||
const groupedByCategory = Object.entries(
|
||||
groupBy(allTemplates, (template) =>
|
||||
template.moduleName === 'default'
|
||||
? st(
|
||||
'templateWorkflows.category.ComfyUI Examples',
|
||||
'ComfyUI Examples'
|
||||
)
|
||||
: st('templateWorkflows.category.Custom Nodes', 'Custom Nodes')
|
||||
)
|
||||
).map(([label, modules]) => ({ label, modules }))
|
||||
|
||||
// Insert the "All" category at the top of the "ComfyUI Examples" group
|
||||
const comfyExamplesGroupIndex = groupedByCategory.findIndex(
|
||||
(group) =>
|
||||
group.label ===
|
||||
st('templateWorkflows.category.ComfyUI Examples', 'ComfyUI Examples')
|
||||
)
|
||||
|
||||
if (comfyExamplesGroupIndex !== -1) {
|
||||
groupedByCategory[comfyExamplesGroupIndex].modules.unshift(
|
||||
createAllCategory()
|
||||
)
|
||||
}
|
||||
|
||||
return groupedByCategory
|
||||
})
|
||||
|
||||
async function loadWorkflowTemplates() {
|
||||
try {
|
||||
if (!isLoaded.value) {
|
||||
customTemplates.value = await api.getWorkflowTemplates()
|
||||
coreTemplates.value = await api.getCoreWorkflowTemplates()
|
||||
isLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflow templates:', error)
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError
|
||||
if (axiosError.response?.data) {
|
||||
console.error('Template error details:', axiosError.response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
groupedTemplates,
|
||||
isLoaded,
|
||||
loadWorkflowTemplates
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useLogsTerminalTab
|
||||
} from '@/composables/bottomPanelTabs/useTerminalTabs'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
import type NodeSearchBoxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
export const useSearchBoxStore = defineStore('searchBox', () => {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -4,11 +4,11 @@ import { computed, ref } from 'vue'
|
||||
import { useModelLibrarySidebarTab } from '@/composables/sidebarTabs/useModelLibrarySidebarTab'
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import { useQueueSidebarTab } from '@/composables/sidebarTabs/useQueueSidebarTab'
|
||||
import { useWorkflowsSidebarTab } from '@/composables/sidebarTabs/useWorkflowsSidebarTab'
|
||||
import { t, te } from '@/i18n'
|
||||
import { useWorkflowsSidebarTab } from '@/platform/workflow/management/composables/useWorkflowsSidebarTab'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
const sidebarTabs = ref<SidebarTabExtension[]>([])
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { Settings } from '@/schemas/apiSchema'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -10,9 +13,6 @@ import { useApiKeyAuthStore } from './apiKeyAuthStore'
|
||||
import { useCommandStore } from './commandStore'
|
||||
import { useFirebaseAuthStore } from './firebaseAuthStore'
|
||||
import { useQueueSettingsStore } from './queueStore'
|
||||
import { useSettingStore } from './settingStore'
|
||||
import { useToastStore } from './toastStore'
|
||||
import { useWorkflowStore } from './workflowStore'
|
||||
import { useBottomPanelStore } from './workspace/bottomPanelStore'
|
||||
import { useSidebarTabStore } from './workspace/sidebarTabStore'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user