fix: code quality improvements across codebase

- Fix STL export comma operator bug (originalURL was silently discarded)
- Remove redundant null checks, identical branches in CameraManager
- Simplify ViewHelperManager.visibleViewHelper boolean assignment
- Remove redundant inner length check in AnimationManager
- Extract calculateLetterbox helper to deduplicate aspect-ratio calcs
- Add explicit return types to ComfyApi public methods
- Extract registerAuthHook helper in extensionService
- Extract prepareConfigureData in litegraphService
- Rename NeverNever to OmitNeverProps for clarity
- Rename progressBarBackground.ts to useProgressBarBackground.ts
- Move tooltipConfig.ts to utils/ (not a composable)
- Remove restating comments and formulaic docstrings
- Add missing return type to getStorageValue
- Remove unnecessary runtime validation in deprecated gridUtil
- Simplify updateControlWidgetLabel and ControlsManager zoom narrowing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Brown
2026-03-06 13:57:05 -08:00
parent 376c32df05
commit 3956fa68a8
113 changed files with 559 additions and 929 deletions

View File

@@ -1,19 +1,7 @@
export interface UVMirror {
/**
* The setting id defined for the mirror.
*/
settingId: string
/**
* The default mirror to use.
*/
mirror: string
/**
* The fallback mirror to use.
*/
fallbackMirror: string
/**
* The path suffix to validate the mirror is reachable.
*/
validationPathSuffix?: string
}

View File

@@ -1,50 +1,33 @@
import type { PrimeVueSeverity } from '../primeVueTypes'
interface MaintenanceTaskButton {
/** The text to display on the button. */
text?: string
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
/** CSS classes, e.g. 'pi pi-external-link' */
icon?: string
}
/** A maintenance task, used by the maintenance page. */
export interface MaintenanceTask {
/** ID string used as i18n key */
/** Used as i18n key */
id: string
/** The display name of the task, e.g. Git */
name: string
/** Short description of the task. */
shortDescription?: string
/** Description of the task when it is in an error state. */
errorDescription?: string
/** Description of the task when it is in a warning state. */
warningDescription?: string
/** Full description of the task when it is in an OK state. */
description?: string
/** URL to the image to show in card mode. */
headerImg?: string
/** The button to display on the task card / list item. */
button?: MaintenanceTaskButton
/** Whether to show a confirmation dialog before running the task. */
requireConfirm?: boolean
/** The text to display in the confirmation dialog. */
confirmText?: string
/** Called by onClick to run the actual task. */
execute: (args?: unknown[]) => boolean | Promise<boolean>
/** Show the button with `severity="danger"` */
severity?: PrimeVueSeverity
/** Whether this task should display the terminal window when run. */
usesTerminal?: boolean
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
/** If true, successful completion refreshes install validation and auto-continues. */
isInstallationFix?: boolean
}
/** The filter options for the maintenance task list. */
export interface MaintenanceFilter {
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
/** CSS classes, e.g. 'pi pi-cross' */
icon: string
/** The text to display on the filter button. */
value: string
/** The tasks to display when this filter is selected. */
tasks: ReadonlyArray<MaintenanceTask>
}

View File

@@ -2,11 +2,6 @@ import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
import { electronAPI } from './envUtil'
/**
* Check if a mirror is reachable from the electron App.
* @param mirror - The mirror to check.
* @returns True if the mirror is reachable, false otherwise.
*/
export const checkMirrorReachable = async (mirror: string) => {
return (
isValidUrl(mirror) && (await electronAPI().NetWork.canAccessUrl(mirror))

View File

@@ -8,7 +8,7 @@ export function isElectron() {
return 'electronAPI' in window && window.electronAPI !== undefined
}
export function electronAPI() {
export function electronAPI(): ElectronAPI {
return (window as ElectronWindow).electronAPI as ElectronAPI
}

View File

@@ -89,13 +89,11 @@
"chart.js": "^4.5.0",
"cva": "catalog:",
"dompurify": "^3.2.5",
"dotenv": "catalog:",
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"firebase": "catalog:",
"fuse.js": "^7.0.0",
"glob": "catalog:",
"jsonata": "catalog:",
"jsondiffpatch": "catalog:",
"loglevel": "^1.9.2",
@@ -145,6 +143,7 @@
"@vue/test-utils": "catalog:",
"@webgpu/types": "catalog:",
"cross-env": "catalog:",
"dotenv": "catalog:",
"eslint": "catalog:",
"eslint-config-prettier": "catalog:",
"eslint-import-resolver-typescript": "catalog:",
@@ -155,6 +154,7 @@
"eslint-plugin-unused-imports": "catalog:",
"eslint-plugin-vue": "catalog:",
"fs-extra": "^11.2.0",
"glob": "catalog:",
"globals": "catalog:",
"happy-dom": "catalog:",
"husky": "catalog:",

View File

@@ -51,10 +51,10 @@ export function downloadFile(url: string, filename?: string): void {
/**
* Download a Blob by creating a temporary object URL and anchor element
* @param filename - The filename to suggest to the browser
* @param blob - The Blob to download
* @param filename - The filename to suggest to the browser
*/
export function downloadBlob(filename: string, blob: Blob): void {
export function downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
triggerLinkDownload(url, filename)
@@ -138,7 +138,7 @@ async function downloadViaBlobFetch(
extractFilenameFromContentDisposition(contentDisposition)
const blob = await response.blob()
downloadBlob(headerFilename ?? fallbackFilename, blob)
downloadBlob(blob, headerFilename ?? fallbackFilename)
}
/**

View File

@@ -1,12 +1,3 @@
/**
* Utilities for pointer event handling
*/
/**
* Checks if a pointer or mouse event is a middle button input
* @param event - The pointer or mouse event to check
* @returns true if the event is from the middle button/wheel
*/
export function isMiddlePointerInput(
event: PointerEvent | MouseEvent
): boolean {

View File

@@ -141,7 +141,7 @@ import Button from '@/components/ui/button/Button.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'

View File

@@ -108,7 +108,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'

View File

@@ -41,7 +41,7 @@ const mockCommands: ComfyCommandImpl[] = [
icon: 'pi pi-test',
tooltip: 'Test tooltip',
menubarLabel: 'Other Command',
keybinding: null
keybinding: undefined
} as ComfyCommandImpl
]

View File

@@ -103,7 +103,7 @@ describe('ShortcutsList', () => {
id: 'No.Keybinding',
label: 'No Keybinding',
category: 'essentials',
keybinding: null
keybinding: undefined
} as ComfyCommandImpl
]

View File

@@ -149,7 +149,7 @@ import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useNodeBadge } from '@/composables/node/useNodeBadge'
import { useCanvasDrop } from '@/composables/useCanvasDrop'
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
import { useCopy } from '@/composables/useCopy'
import { useGraphCopyHandler } from '@/composables/useGraphCopyHandler'
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
import { usePaste } from '@/composables/usePaste'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
@@ -446,7 +446,7 @@ useNodeBadge()
useGlobalLitegraph()
useContextMenuTranslation()
useCopy()
useGraphCopyHandler()
usePaste()
useWorkflowAutoSave()

View File

@@ -1,12 +1,12 @@
<template>
<div>
<ZoomControlsModal :visible="isModalVisible" @close="hideModal" />
<ZoomControlsModal :visible="isPopoverOpen" @close="hidePopover" />
<!-- Backdrop -->
<div
v-if="hasActivePopup"
class="fixed inset-0 z-1200"
@click="hideModal"
@click="hidePopover"
></div>
<ButtonGroup
@@ -40,7 +40,7 @@
:aria-label="t('zoomControls.label')"
data-testid="zoom-controls-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="toggleModal"
@click="togglePopover"
>
<span class="inline-flex items-center gap-1 px-2 text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
@@ -110,7 +110,7 @@ const settingStore = useSettingStore()
const canvasInteractions = useCanvasInteractions()
const minimap = useMinimap()
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
const { isPopoverOpen, togglePopover, hidePopover, hasActivePopup } =
useZoomControls()
const stringifiedMinimapStyles = computed(() => {
@@ -157,7 +157,7 @@ const minimapCommandText = computed(() =>
// Computed properties for button classes and states
const zoomButtonClass = computed(() => [
'bg-comfy-menu-bg',
isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
isPopoverOpen.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
'hover:bg-interface-button-hover-surface!',
'p-0',
'h-8',

View File

@@ -76,7 +76,7 @@ import Load3DScene from '@/components/load3d/Load3DScene.vue'
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
import { useLoad3d } from '@/composables/useLoad3d'
import { useLoad3d } from '@/extensions/core/load3d/composables/useLoad3d'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'

View File

@@ -33,7 +33,7 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
const props = defineProps<{
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>

View File

@@ -103,8 +103,8 @@ import LightControls from '@/components/load3d/controls/viewer/ViewerLightContro
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
import Button from '@/components/ui/button/Button.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
import { useLoad3dViewer } from '@/extensions/core/load3d/composables/useLoad3dViewer'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useLoad3dService } from '@/services/load3dService'
import { useDialogStore } from '@/stores/dialogStore'

View File

@@ -95,7 +95,7 @@ import { useI18n } from 'vue-i18n'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'

View File

@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import QueueOverlayActive from './QueueOverlayActive.vue'
import * as tooltipConfig from '@/composables/useTooltipConfig'
import * as tooltipConfig from '@/utils/tooltipConfig'
const i18n = createI18n({
legacy: false,

View File

@@ -95,7 +95,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
defineProps<{
totalProgressStyle: Record<string, string>

View File

@@ -48,7 +48,7 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
}))
import QueueOverlayHeader from './QueueOverlayHeader.vue'
import * as tooltipConfig from '@/composables/useTooltipConfig'
import * as tooltipConfig from '@/utils/tooltipConfig'
const tooltipDirectiveStub = {
mounted: vi.fn(),

View File

@@ -32,7 +32,7 @@ import { useI18n } from 'vue-i18n'
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
defineProps<{
headerTitle: string

View File

@@ -121,7 +121,7 @@ import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { jobSortModes } from '@/composables/queue/useJobList'
import type { JobSortMode } from '@/composables/queue/useJobList'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
const {
hideShowAssetsAction = false,

View File

@@ -194,7 +194,7 @@ import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
import Button from '@/components/ui/button/Button.vue'
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { buildTooltipConfig } from '@/utils/tooltipConfig'
import type { JobState } from '@/types/queue'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'

View File

@@ -4,28 +4,13 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { collectFromNodes } from '@/utils/graphTraversalUtil'
/**
* Composable for handling selected LiteGraph items filtering and operations.
* This provides utilities for working with selected items on the canvas,
* including filtering out items that should not be included in selection operations.
*/
export function useSelectedLiteGraphItems() {
const canvasStore = useCanvasStore()
/**
* Items that should not show in the selection overlay are ignored.
* @param item - The item to check.
* @returns True if the item should be ignored, false otherwise.
*/
const isIgnoredItem = (item: Positionable): boolean => {
return item instanceof Reroute
}
/**
* Filter out items that should not show in the selection overlay.
* @param items - The Set of items to filter.
* @returns The filtered Set of items.
*/
const filterSelectableItems = (
items: Set<Positionable>
): Set<Positionable> => {
@@ -38,37 +23,20 @@ export function useSelectedLiteGraphItems() {
return result
}
/**
* Get the filtered selected items from the canvas.
* @returns The filtered Set of selected items.
*/
const getSelectableItems = (): Set<Positionable> => {
const { selectedItems } = canvasStore.getCanvas()
return filterSelectableItems(selectedItems)
}
/**
* Check if there are any selectable items.
* @returns True if there are selectable items, false otherwise.
*/
const hasSelectableItems = (): boolean => {
return getSelectableItems().size > 0
}
/**
* Check if there are multiple selectable items.
* @returns True if there are multiple selectable items, false otherwise.
*/
const hasMultipleSelectableItems = (): boolean => {
return getSelectableItems().size > 1
}
/**
* Get only the selected nodes (LGraphNode instances) from the canvas.
* This filters out other types of selected items like groups or reroutes.
* If a selected node is a subgraph, this also includes all nodes within it.
* @returns Array of selected LGraphNode instances and their descendants.
*/
/** Includes descendant nodes from any selected subgraphs. */
const getSelectedNodes = (): LGraphNode[] => {
const selectedNodes = app.canvas.selected_nodes
if (!selectedNodes) return []

View File

@@ -34,7 +34,7 @@ export function useSubgraphOperations() {
workflowStore.activeWorkflow?.changeTracker?.checkState()
}
const doUnpack = (
const unpackNodes = (
subgraphNodes: SubgraphNode[],
skipMissingNodes: boolean
) => {
@@ -65,7 +65,7 @@ export function useSubgraphOperations() {
if (subgraphNodes.length === 0) {
return
}
doUnpack(subgraphNodes, true)
unpackNodes(subgraphNodes, true)
}
const addSubgraphToLibrary = async () => {

View File

@@ -64,8 +64,8 @@ function useVueNodeLifecycleIndividual() {
try {
nodeManager.value.cleanup()
} catch {
/* empty */
} catch (error) {
console.warn('Node manager cleanup failed:', error)
}
nodeManager.value = null
}

View File

@@ -46,16 +46,13 @@ export const useComputedWithWidgetWatch = (
) => {
const { widgetNames, triggerCanvasRedraw = false } = options
// Create a reactive trigger based on widget values
const widgetValues = ref<Record<string, unknown>>({})
// Initialize widget observers
if (node.widgets) {
const widgetsToObserve = widgetNames
? node.widgets.filter((widget) => widgetNames.includes(widget.name))
: node.widgets
// Initialize current values
const currentValues: Record<string, unknown> = {}
widgetsToObserve.forEach((widget) => {
currentValues[widget.name] = widget.value
@@ -64,20 +61,17 @@ export const useComputedWithWidgetWatch = (
widgetsToObserve.forEach((widget) => {
widget.callback = useChainCallback(widget.callback, () => {
// Update the reactive widget values
widgetValues.value = {
...widgetValues.value,
[widget.name]: widget.value
}
// Optionally trigger a canvas redraw
if (triggerCanvasRedraw) {
node.graph?.setDirtyCanvas(true, true)
}
})
})
if (widgetNames && widgetNames.length > widgetsToObserve.length) {
//Inputs have been included
const indexesToObserve = widgetNames
.map((name) =>
widgetsToObserve.some((w) => w.name == name)
@@ -101,8 +95,6 @@ export const useComputedWithWidgetWatch = (
}
}
// Returns a function that creates a computed that responds to widget changes.
// The computed will be re-evaluated whenever any observed widget changes.
return <T>(computeFn: () => T): ComputedRef<T> => {
return computedWithControl(widgetValues, computeFn)
}

View File

@@ -79,8 +79,8 @@ vi.mock('@/scripts/api', () => ({
const downloadBlobMock = vi.fn()
vi.mock('@/scripts/utils', () => ({
downloadBlob: (filename: string, blob: Blob) =>
downloadBlobMock(filename, blob)
downloadBlob: (blob: Blob, filename: string) =>
downloadBlobMock(blob, filename)
}))
const dialogServiceMock = {
@@ -594,7 +594,7 @@ describe('useJobMenu', () => {
expect(dialogServiceMock.prompt).not.toHaveBeenCalled()
expect(downloadBlobMock).toHaveBeenCalledTimes(1)
const [filename, blob] = downloadBlobMock.mock.calls[0]
const [blob, filename] = downloadBlobMock.mock.calls[0]
expect(filename).toBe('Job 7.json')
await expect(blob.text()).resolves.toBe(
JSON.stringify({ foo: 'bar' }, null, 2)
@@ -621,7 +621,7 @@ describe('useJobMenu', () => {
message: expect.stringContaining('workflowService.enterFilename'),
defaultValue: 'Job job-1.json'
})
const [filename] = downloadBlobMock.mock.calls[0]
const [, filename] = downloadBlobMock.mock.calls[0]
expect(filename).toBe('custom-name.json')
})
@@ -642,7 +642,7 @@ describe('useJobMenu', () => {
await entry?.onClick?.()
expect(appendJsonExtMock).toHaveBeenCalledWith('existing.json')
const [filename] = downloadBlobMock.mock.calls[0]
const [, filename] = downloadBlobMock.mock.calls[0]
expect(filename).toBe('existing.json')
})

View File

@@ -200,7 +200,7 @@ export function useJobMenu(
const json = JSON.stringify(data, null, 2)
const blob = new Blob([json], { type: 'application/json' })
downloadBlob(filename, blob)
downloadBlob(blob, filename)
}
const deleteJobAsset = async () => {

View File

@@ -22,7 +22,7 @@ export const useBrowserTabTitle = () => {
: `[${Math.round(executionStore.executionProgress * 100)}%]`
)
const newMenuEnabled = computed(
const isMenuBarActive = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
@@ -86,7 +86,7 @@ export const useBrowserTabTitle = () => {
const workflowTitle = computed(
() =>
executionText.value +
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
(isMenuBarActive.value ? workflowNameText.value : DEFAULT_TITLE)
)
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)

View File

@@ -48,7 +48,7 @@ export function useCachedRequest<TParams, TResult>(
return result
} catch (err) {
// Set cache on error to prevent retrying bad requests
console.warn('Cached request failed, caching null result:', err)
cache.set(cacheKey, null)
return null
} finally {

View File

@@ -1163,7 +1163,29 @@ export function useCoreCommands(): ComfyCommand[] {
})
return
}
await api.freeMemory({ freeExecutionCache: false })
try {
const res = await api.freeMemory({ freeExecutionCache: false })
if (res.status === 200) {
useToastStore().add({
severity: 'success',
summary: 'Models have been unloaded.',
life: 3000
})
} else {
useToastStore().add({
severity: 'error',
summary:
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
life: 5000
})
}
} catch {
useToastStore().add({
severity: 'error',
summary: 'An error occurred while trying to unload models.',
life: 5000
})
}
}
},
{
@@ -1183,7 +1205,29 @@ export function useCoreCommands(): ComfyCommand[] {
})
return
}
await api.freeMemory({ freeExecutionCache: true })
try {
const res = await api.freeMemory({ freeExecutionCache: true })
if (res.status === 200) {
useToastStore().add({
severity: 'success',
summary: 'Models and Execution Cache have been cleared.',
life: 3000
})
} else {
useToastStore().add({
severity: 'error',
summary:
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
life: 5000
})
}
} catch {
useToastStore().add({
severity: 'error',
summary: 'An error occurred while trying to unload models.',
life: 5000
})
}
}
},
{

View File

@@ -5,23 +5,7 @@ import { electronAPI } from '@/utils/envUtil'
import { i18n } from '@/i18n'
/**
* Composable for building docs.comfy.org URLs with automatic locale and platform detection
*
* @example
* ```ts
* const { buildDocsUrl } = useExternalLink()
*
* // Simple usage
* const changelogUrl = buildDocsUrl('/changelog', { includeLocale: true })
* // => 'https://docs.comfy.org/zh-CN/changelog' (if Chinese)
*
* // With platform detection
* const desktopUrl = buildDocsUrl('/installation/desktop', {
* includeLocale: true,
* platform: true
* })
* // => 'https://docs.comfy.org/zh-CN/installation/desktop/macos' (if Chinese + macOS)
* ```
* Composable for building docs.comfy.org URLs with automatic locale and platform detection.
*/
export function useExternalLink() {
const locale = computed(() => String(i18n.global.locale.value))

View File

@@ -11,7 +11,7 @@ const clipboardHTMLWrapper = [
/**
* Adds a handler on copy that serializes selected nodes to JSON
*/
export const useCopy = () => {
export const useGraphCopyHandler = () => {
const canvasStore = useCanvasStore()
useEventListener(document, 'copy', (e) => {

View File

@@ -1,18 +0,0 @@
/**
* Build a tooltip configuration object compatible with v-tooltip.
* Consumers pass the translated text value.
*/
export const buildTooltipConfig = (value: string) => ({
value,
showDelay: 300,
hideDelay: 0,
pt: {
text: {
class:
'border-node-component-tooltip-border bg-node-component-tooltip-surface text-node-component-tooltip border rounded-md px-2 py-1 text-xs leading-none shadow-none'
},
arrow: {
class: 'border-t-node-component-tooltip-border'
}
}
})

View File

@@ -1,48 +1,8 @@
import type { HintedString } from '@primevue/core'
import { computed } from 'vue'
/**
* Options for configuring transform-compatible overlay props
* Props to keep PrimeVue overlays within CSS-transformed parent elements.
*/
interface TransformCompatOverlayOptions {
/**
* Where to append the overlay. 'self' keeps overlay within component
* for proper transform inheritance, 'body' teleports to document body
*/
appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement
// Future: other props needed for transform compatibility
// scrollTarget?: string | HTMLElement
// autoZIndex?: boolean
}
/**
* Composable that provides props to make PrimeVue overlay components
* compatible with CSS-transformed parent elements.
*
* Vue nodes use CSS transforms for positioning/scaling. PrimeVue overlay
* components (Select, MultiSelect, TreeSelect, etc.) teleport to document
* body by default, breaking transform inheritance. This composable provides
* the necessary props to keep overlays within their component elements.
*
* @param overrides - Optional overrides for specific use cases
* @returns Computed props object to spread on PrimeVue overlay components
*
* @example
* ```vue
* <template>
* <Select v-bind="overlayProps" />
* </template>
*
* <script setup>
* const overlayProps = useTransformCompatOverlayProps()
* </script>
* ```
*/
export function useTransformCompatOverlayProps(
overrides: TransformCompatOverlayOptions = {}
) {
return computed(() => ({
appendTo: 'self' as const,
...overrides
}))
export function useTransformCompatOverlayProps() {
return computed(() => ({ appendTo: 'self' as const }))
}

View File

@@ -1,27 +1,27 @@
import { computed, ref } from 'vue'
export function useZoomControls() {
const isModalVisible = ref(false)
const isPopoverOpen = ref(false)
const showModal = () => {
isModalVisible.value = true
const showPopover = () => {
isPopoverOpen.value = true
}
const hideModal = () => {
isModalVisible.value = false
const hidePopover = () => {
isPopoverOpen.value = false
}
const toggleModal = () => {
isModalVisible.value = !isModalVisible.value
const togglePopover = () => {
isPopoverOpen.value = !isPopoverOpen.value
}
const hasActivePopup = computed(() => isModalVisible.value)
const hasActivePopup = computed(() => isPopoverOpen.value)
return {
isModalVisible,
showModal,
hideModal,
toggleModal,
isPopoverOpen,
showPopover,
hidePopover,
togglePopover,
hasActivePopup
}
}

View File

@@ -1,11 +1,4 @@
/** Default panel size (%) for sidebar and builder panels */
export const SIDE_PANEL_SIZE = 20
/** Default panel size (%) for the center/main panel */
export const CENTER_PANEL_SIZE = 80
/** Minimum panel size (%) for the sidebar */
export const SIDEBAR_MIN_SIZE = 10
/** Minimum panel size (%) for the builder panel */
export const BUILDER_MIN_SIZE = 15

View File

@@ -1,19 +1,7 @@
interface UVMirror {
/**
* The setting id defined for the mirror.
*/
settingId: string
/**
* The default mirror to use.
*/
mirror: string
/**
* The fallback mirror to use.
*/
fallbackMirror: string
/**
* The path suffix to validate the mirror is reachable.
*/
validationPathSuffix?: string
}

View File

@@ -1,6 +1,6 @@
import { app } from '../../scripts/app'
import { ComfyApp } from '../../scripts/app'
import { $el, ComfyDialog } from '../../scripts/ui'
import { app } from '@/scripts/app'
import { ComfyApp } from '@/scripts/app'
import { $el, ComfyDialog } from '@/scripts/ui'
class ClipspaceDialog extends ComfyDialog {
static items: Array<

View File

@@ -4,7 +4,7 @@ import {
isComboWidget
} from '@/lib/litegraph/src/litegraph'
import { app } from '../../scripts/app'
import { app } from '@/scripts/app'
// Adds filtering to combo context menus

View File

@@ -1,4 +1,4 @@
import { app } from '../../scripts/app'
import { app } from '@/scripts/app'
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys

View File

@@ -27,8 +27,8 @@ import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO
import { GROUP } from '@/utils/executableGroupNodeDto'
import { deserialiseAndCreate, serialise } from '@/utils/vintageClipboard'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { ManageGroupDialog } from './groupNodeManage'
import { mergeIfValid } from './widgetInputs'

View File

@@ -8,10 +8,11 @@ import type {
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { type ComfyApp, app } from '../../scripts/app'
import { $el } from '../../scripts/ui'
import { ComfyDialog } from '../../scripts/ui/dialog'
import { DraggableList } from '../../scripts/ui/draggableList'
import type { ComfyApp } from '@/scripts/app'
import { app } from '@/scripts/app'
import { $el } from '@/scripts/ui'
import { ComfyDialog } from '@/scripts/ui/dialog'
import { DraggableList } from '@/scripts/ui/draggableList'
import type { GroupNodeConfig } from './groupNode'
// Lazy import to break circular dependency with groupNode.ts

View File

@@ -2,15 +2,12 @@ import type {
IContextMenuValue,
Positionable
} from '@/lib/litegraph/src/interfaces'
import {
LGraphCanvas,
LGraphGroup,
type LGraphNode
} from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas, LGraphGroup } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ComfyExtension } from '@/types/comfy'
import { app } from '../../scripts/app'
import { app } from '@/scripts/app'
function setNodeMode(node: LGraphNode, mode: number) {
node.mode = mode

View File

@@ -2,7 +2,10 @@ import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
import { nodeToLoad3dMap, useLoad3d } from '@/composables/useLoad3d'
import {
nodeToLoad3dMap,
useLoad3d
} from '@/extensions/core/load3d/composables/useLoad3d'
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
import type {
CameraConfig,

View File

@@ -64,9 +64,7 @@ export class AnimationManager implements AnimationManagerInterface {
this.currentAnimation = new THREE.AnimationMixer(model)
if (this.animationClips.length > 0) {
this.updateSelectedAnimation(0)
}
this.updateSelectedAnimation(0)
} else {
this.animationClips = []
}

View File

@@ -73,11 +73,9 @@ export class CameraManager implements CameraManagerInterface {
setControls(controls: OrbitControls): void {
this.controls = controls
if (this.controls) {
this.controls.addEventListener('end', () => {
this.eventManager.emitEvent('cameraChanged', this.getCameraState())
})
}
this.controls.addEventListener('end', () => {
this.eventManager.emitEvent('cameraChanged', this.getCameraState())
})
}
getCurrentCameraType(): CameraType {
@@ -117,13 +115,11 @@ export class CameraManager implements CameraManagerInterface {
this.activeCamera.position.copy(position)
this.activeCamera.rotation.copy(rotation)
if (this.activeCamera instanceof THREE.OrthographicCamera) {
this.activeCamera.zoom = oldZoom
this.activeCamera.updateProjectionMatrix()
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
this.activeCamera.zoom = oldZoom
this.activeCamera.updateProjectionMatrix()
}
const cam = this.activeCamera as
| THREE.PerspectiveCamera
| THREE.OrthographicCamera
cam.zoom = oldZoom
cam.updateProjectionMatrix()
if (this.controls) {
this.controls.object = this.activeCamera
@@ -160,13 +156,11 @@ export class CameraManager implements CameraManagerInterface {
this.controls?.target.copy(state.target)
if (this.activeCamera instanceof THREE.OrthographicCamera) {
this.activeCamera.zoom = state.zoom
this.activeCamera.updateProjectionMatrix()
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
this.activeCamera.zoom = state.zoom
this.activeCamera.updateProjectionMatrix()
}
const cam = this.activeCamera as
| THREE.PerspectiveCamera
| THREE.OrthographicCamera
cam.zoom = state.zoom
cam.updateProjectionMatrix()
this.controls?.update()
}

View File

@@ -29,10 +29,9 @@ export class ControlsManager implements ControlsManagerInterface {
const cameraState = {
position: this.camera.position.clone(),
target: this.controls.target.clone(),
zoom:
this.camera instanceof THREE.OrthographicCamera
? (this.camera as THREE.OrthographicCamera).zoom
: (this.camera as THREE.PerspectiveCamera).zoom,
zoom: (
this.camera as THREE.PerspectiveCamera | THREE.OrthographicCamera
).zoom,
cameraType:
this.camera instanceof THREE.PerspectiveCamera
? 'perspective'

View File

@@ -7,7 +7,6 @@ import type {
ModelConfig,
SceneConfig
} from '@/extensions/core/load3d/interfaces'
import type { Dictionary } from '@/lib/litegraph/src/interfaces'
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -25,7 +24,7 @@ type Load3DConfigurationSettings = {
class Load3DConfiguration {
constructor(
private load3d: Load3d,
private properties?: Dictionary<NodeProperty | undefined>
private properties?: Record<string, NodeProperty | undefined>
) {}
configureForSaveMesh(loadFolder: 'input' | 'output', filePath: string) {

View File

@@ -268,6 +268,36 @@ class Load3d {
return this.isViewerMode || (this.targetWidth > 0 && this.targetHeight > 0)
}
private calculateLetterbox(
containerWidth: number,
containerHeight: number
): {
renderWidth: number
renderHeight: number
offsetX: number
offsetY: number
} {
const containerAspectRatio = containerWidth / containerHeight
if (containerAspectRatio > this.targetAspectRatio) {
const renderHeight = containerHeight
const renderWidth = renderHeight * this.targetAspectRatio
return {
renderWidth,
renderHeight,
offsetX: (containerWidth - renderWidth) / 2,
offsetY: 0
}
}
const renderWidth = containerWidth
const renderHeight = renderWidth / this.targetAspectRatio
return {
renderWidth,
renderHeight,
offsetX: 0,
offsetY: (containerHeight - renderHeight) / 2
}
}
forceRender(): void {
const delta = this.clock.getDelta()
this.animationManager.update(delta)
@@ -299,22 +329,8 @@ class Load3d {
}
if (this.shouldMaintainAspectRatio()) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
let offsetX: number = 0
let offsetY: number = 0
if (containerAspectRatio > this.targetAspectRatio) {
renderHeight = containerHeight
renderWidth = renderHeight * this.targetAspectRatio
offsetX = (containerWidth - renderWidth) / 2
} else {
renderWidth = containerWidth
renderHeight = renderWidth / this.targetAspectRatio
offsetY = (containerHeight - renderHeight) / 2
}
const { renderWidth, renderHeight, offsetX, offsetY } =
this.calculateLetterbox(containerWidth, containerHeight)
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
@@ -325,8 +341,7 @@ class Load3d {
this.renderer.setViewport(offsetX, offsetY, renderWidth, renderHeight)
this.renderer.setScissor(offsetX, offsetY, renderWidth, renderHeight)
const renderAspectRatio = renderWidth / renderHeight
this.cameraManager.updateAspectRatio(renderAspectRatio)
this.cameraManager.updateAspectRatio(renderWidth / renderHeight)
} else {
// No aspect ratio constraint: fill the entire container
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
@@ -436,7 +451,7 @@ class Load3d {
await ModelExporter.exportOBJ(model, filename, originalURL)
break
case 'stl':
;(await ModelExporter.exportSTL(model, filename), originalURL)
await ModelExporter.exportSTL(model, filename, originalURL)
break
default:
throw new Error(`Unsupported export format: ${format}`)
@@ -468,18 +483,10 @@ class Load3d {
const containerHeight = this.renderer.domElement.clientHeight
if (this.shouldMaintainAspectRatio()) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
if (containerAspectRatio > this.targetAspectRatio) {
renderHeight = containerHeight
renderWidth = renderHeight * this.targetAspectRatio
} else {
renderWidth = containerWidth
renderHeight = renderWidth / this.targetAspectRatio
}
const { renderWidth, renderHeight } = this.calculateLetterbox(
containerWidth,
containerHeight
)
this.sceneManager.updateBackgroundSize(
this.sceneManager.backgroundTexture,
@@ -655,17 +662,10 @@ class Load3d {
}
if (this.shouldMaintainAspectRatio()) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
if (containerAspectRatio > this.targetAspectRatio) {
renderHeight = containerHeight
renderWidth = renderHeight * this.targetAspectRatio
} else {
renderWidth = containerWidth
renderHeight = renderWidth / this.targetAspectRatio
}
const { renderWidth, renderHeight } = this.calculateLetterbox(
containerWidth,
containerHeight
)
this.renderer.setSize(containerWidth, containerHeight)
this.cameraManager.handleResize(renderWidth, renderHeight)

View File

@@ -39,7 +39,7 @@ export class ModelExporter {
try {
const response = await fetch(url)
const blob = await response.blob()
downloadBlob(desiredFilename, blob)
downloadBlob(blob, desiredFilename)
} catch (error) {
console.error('Error downloading from URL:', error)
useToastStore().addAlert(t('toastMessages.failedToDownloadFile'))
@@ -147,11 +147,11 @@ export class ModelExporter {
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
const blob = new Blob([buffer], { type: 'application/octet-stream' })
downloadBlob(filename, blob)
downloadBlob(blob, filename)
}
private static saveString(text: string, filename: string): void {
const blob = new Blob([text], { type: 'text/plain' })
downloadBlob(filename, blob)
downloadBlob(blob, filename)
}
}

View File

@@ -225,7 +225,7 @@ export class RecordingManager {
try {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
downloadBlob(filename, blob)
downloadBlob(blob, filename)
this.eventManager.emitEvent('recordingExported', null)
} catch (error) {

View File

@@ -92,13 +92,8 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
handleResize(): void {}
visibleViewHelper(visible: boolean) {
if (visible) {
this.viewHelper.visible = true
this.viewHelperContainer.style.display = 'block'
} else {
this.viewHelper.visible = false
this.viewHelperContainer.style.display = 'none'
}
this.viewHelper.visible = visible
this.viewHelperContainer.style.display = visible ? 'block' : 'none'
}
recreateViewHelper(): void {

View File

@@ -1,7 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref, shallowRef } from 'vue'
import { nodeToLoad3dMap, useLoad3d } from '@/composables/useLoad3d'
import {
nodeToLoad3dMap,
useLoad3d
} from '@/extensions/core/load3d/composables/useLoad3d'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type { Size } from '@/lib/litegraph/src/interfaces'

View File

@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
import { SUPPORTED_EXTENSIONS } from '@/extensions/core/load3d/constants'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { createMockFileList } from '@/utils/__tests__/litegraphTestUtils'

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
import { useLoad3dViewer } from '@/extensions/core/load3d/composables/useLoad3dViewer'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type { LGraph } from '@/lib/litegraph/src/LGraph'

View File

@@ -7,9 +7,9 @@ import { useDialogService } from '@/services/dialogService'
import type { ComfyExtension } from '@/types/comfy'
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
import { $el, ComfyDialog } from '../../scripts/ui'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { $el, ComfyDialog } from '@/scripts/ui'
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
// Adds the ability to save and add multiple nodes as a template
@@ -115,8 +115,8 @@ class ManageTemplates extends ComfyDialog {
await api.storeUserData(file, templates, { stringify: false })
} catch (error) {
console.error(error)
// @ts-expect-error fixme ts strict error
useToastStore().addAlert(error.message)
const message = error instanceof Error ? error.message : String(error)
useToastStore().addAlert(message)
}
}
@@ -154,7 +154,7 @@ class ManageTemplates extends ComfyDialog {
const json = JSON.stringify({ templates: this.templates }, null, 2) // convert the data to a JSON string
const blob = new Blob([json], { type: 'application/json' })
downloadBlob('node_templates.json', blob)
downloadBlob(blob, 'node_templates.json')
}
override show() {
@@ -298,7 +298,7 @@ class ManageTemplates extends ComfyDialog {
})
// @ts-expect-error fixme ts strict error
const name = (nameInput.value || t.name) + '.json'
downloadBlob(name, blob)
downloadBlob(blob, name)
}
}),
$el('button', {

View File

@@ -1,8 +1,8 @@
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { app } from '../../scripts/app'
import { ComfyWidgets } from '../../scripts/widgets'
import { app } from '@/scripts/app'
import { ComfyWidgets } from '@/scripts/widgets'
// Node that add notes to your project

View File

@@ -6,7 +6,7 @@ import {
} from '@/lib/litegraph/src/litegraph'
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
import { app } from '../../scripts/app'
import { app } from '@/scripts/app'
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
// Node that allows you to redirect connections for cleaner graphs

View File

@@ -2,7 +2,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { applyTextReplacements } from '@/utils/searchAndReplace'
import { app } from '../../scripts/app'
import { app } from '@/scripts/app'
const saveNodeTypes = new Set([
'SaveImage',

View File

@@ -1,7 +1,7 @@
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import { useLoad3d } from '@/composables/useLoad3d'
import { useLoad3d } from '@/extensions/core/load3d/composables/useLoad3d'
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'

View File

@@ -1,8 +1,8 @@
import type { ComfyExtension } from '@/types/comfy'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { app } from '../../scripts/app'
import { ComfyWidgets } from '../../scripts/widgets'
import { app } from '@/scripts/app'
import { ComfyWidgets } from '@/scripts/widgets'
// Adds defaults for quickly adding nodes with middle click on the input/output

View File

@@ -22,8 +22,8 @@ import { useAudioService } from '@/services/audioService'
import { type NodeLocatorId } from '@/types'
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
function updateUIWidget(

View File

@@ -1,11 +1,8 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import {
type ComfyNodeDef,
type InputSpec,
isMediaUploadComboInput
} from '@/schemas/nodeDefSchema'
import type { ComfyNodeDef, InputSpec } from '@/schemas/nodeDefSchema'
import { isMediaUploadComboInput } from '@/schemas/nodeDefSchema'
import { app } from '../../scripts/app'
import { app } from '@/scripts/app'
// Adds an upload button to the nodes

View File

@@ -2,8 +2,8 @@ import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
const WEBCAM_READY = Symbol()

View File

@@ -28,7 +28,6 @@ import type { LGraphEventMap } from './infrastructure/LGraphEventMap'
import type { SubgraphEventMap } from './infrastructure/SubgraphEventMap'
import type {
DefaultConnectionColors,
Dictionary,
HasBoundingRect,
IContextMenuValue,
INodeInputSlot,
@@ -136,7 +135,7 @@ export interface GroupNodeWorkflowData {
config?: Record<number, GroupNodeConfigEntry>
}
export interface LGraphExtra extends Dictionary<unknown> {
export interface LGraphExtra extends Record<string, unknown> {
reroutes?: SerialisableReroute[]
linkExtensions?: { id: number; parentId: number | undefined }[]
ds?: DragAndScaleState
@@ -244,7 +243,7 @@ export class LGraph
filter?: string
/** Must contain serialisable values, e.g. primitive types */
config: LGraphConfig = {}
vars: Dictionary<unknown> = {}
vars: Record<string, unknown> = {}
nodes_executing: boolean[] = []
nodes_actioning: (string | boolean)[] = []
nodes_executedAction: string[] = []
@@ -636,7 +635,7 @@ export class LGraph
): LGraphNode[] {
const L: LGraphNode[] = []
const S: LGraphNode[] = []
const M: Dictionary<LGraphNode> = {}
const M: Record<string, LGraphNode> = {}
// to avoid repeating links
const visited_links: Record<NodeId, boolean> = {}
const remaining_links: Record<NodeId, number> = {}

View File

@@ -39,7 +39,6 @@ import type {
ConnectingLink,
ContextMenuDivElement,
DefaultConnectionColors,
Dictionary,
Direction,
IBoundaryNodes,
IColorable,
@@ -100,7 +99,7 @@ import type {
ISerialisedNode,
SubgraphIO
} from './types/serialisation'
import type { NeverNever, PickNevers } from './types/utility'
import type { OmitNeverProps, PickNevers } from './types/utility'
import type { IBaseWidget, TWidgetValue } from './types/widgets'
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
import { findFirstNode, getAllNestedItems } from './utils/collections'
@@ -275,7 +274,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
static DEFAULT_EVENT_LINK_COLOR = '#A86'
/** Link type to colour dictionary. */
static link_type_colors: Dictionary<string> = {
static link_type_colors: Record<string, string> = {
'-1': LGraphCanvas.DEFAULT_EVENT_LINK_COLOR,
number: '#AAA',
node: '#DCA'
@@ -345,7 +344,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
| undefined
/** Dispatches a custom event on the canvas. */
dispatch<T extends keyof NeverNever<LGraphCanvasEventMap>>(
dispatch<T extends keyof OmitNeverProps<LGraphCanvasEventMap>>(
type: T,
detail: LGraphCanvasEventMap[T]
): boolean
@@ -537,8 +536,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
output_on: string
}
default_connection_color_byType: Dictionary<CanvasColour>
default_connection_color_byTypeOff: Dictionary<CanvasColour>
default_connection_color_byType: Record<string, CanvasColour>
default_connection_color_byTypeOff: Record<string, CanvasColour>
/** Gets link colours. Extremely basic impl. until the legacy object dictionaries are removed. */
colourGetter: DefaultConnectionColors = {
@@ -653,7 +652,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
render_time = 0
fps = 0
/** @deprecated See {@link LGraphCanvas.selectedItems} */
selected_nodes: Dictionary<LGraphNode> = {}
selected_nodes: Record<string, LGraphNode> = {}
/** All selected nodes, groups, and reroutes */
selectedItems: Set<Positionable> = new Set()
/** The group currently being resized. */
@@ -669,7 +668,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
private _visible_node_ids: Set<NodeId> = new Set()
node_over?: LGraphNode
node_capturing_input?: LGraphNode | null
highlighted_links: Dictionary<boolean> = {}
highlighted_links: Record<string, boolean> = {}
private _visibleReroutes: Set<Reroute> = new Set()
@@ -766,7 +765,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
/** called after moving a node @deprecated Does not handle multi-node move, and can return the wrong node. */
onNodeMoved?: (node_dragged: LGraphNode | undefined) => void
/** @deprecated Called with the deprecated {@link selected_nodes} when the selection changes. Replacement not yet impl. */
onSelectionChange?: (selected: Dictionary<Positionable>) => void
onSelectionChange?: (selected: Record<string, Positionable>) => void
/** called when rendering a tooltip */
onDrawLinkTooltip?: (
ctx: CanvasRenderingContext2D,
@@ -1030,7 +1029,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @returns
*/
static getBoundaryNodes(
nodes: LGraphNode[] | Dictionary<LGraphNode>
nodes: LGraphNode[] | Record<string, LGraphNode>
): NullableProperties<IBoundaryNodes> {
const _nodes = Array.isArray(nodes) ? nodes : Object.values(nodes)
return (
@@ -1050,7 +1049,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param align_to Node to align to (if null, align to the furthest node in the given direction)
*/
static alignNodes(
nodes: Dictionary<LGraphNode>,
nodes: Record<string, LGraphNode>,
direction: Direction,
align_to?: LGraphNode
): void {

View File

@@ -34,7 +34,6 @@ import type {
ColorOption,
CompassCorners,
DefaultConnectionColors,
Dictionary,
IColorable,
IContextMenuValue,
IFoundSlot,
@@ -280,7 +279,7 @@ export class LGraphNode
private _concreteInputs: NodeInputSlot[] = []
private _concreteOutputs: NodeOutputSlot[] = []
properties: Dictionary<NodeProperty | undefined> = {}
properties: Record<string, NodeProperty | undefined> = {}
properties_info: INodePropertyInfo[] = []
flags: INodeFlags = {}
widgets?: IBaseWidget[]
@@ -2144,12 +2143,6 @@ export class LGraphNode
return false
}
/**
* Checks if the provided point is inside this node's collapse button area.
* @param x X co-ordinate to check
* @param y Y co-ordinate to check
* @returns true if the x,y point is in the collapse button area, otherwise false
*/
isPointInCollapse(x: number, y: number): boolean {
const squareLength = LiteGraph.NODE_TITLE_HEIGHT
return isInRectangle(

View File

@@ -5,11 +5,6 @@ import { LLink } from '@/lib/litegraph/src/litegraph'
import { test } from './__fixtures__/testExtensions'
describe('LLink', () => {
test('matches previous snapshot', () => {
const link = new LLink(1, 'float', 4, 2, 5, 3)
expect(link.serialize()).toMatchSnapshot('Basic')
})
test('serializes to the previous snapshot', () => {
const link = new LLink(1, 'float', 4, 2, 5, 3)
expect(link.serialize()).toMatchSnapshot('Basic')

View File

@@ -12,7 +12,6 @@ import { LabelPosition, SlotDirection, SlotShape, SlotType } from './draw'
import { Rectangle } from './infrastructure/Rectangle'
import type {
CreateNodeOptions,
Dictionary,
ISlotType,
Rect,
WhenNullish
@@ -167,7 +166,7 @@ export class LiteGraphGlobal {
Globals = {}
/** @deprecated Unused and will be deleted. */
searchbox_extras: Dictionary<unknown> = {}
searchbox_extras: Record<string, unknown> = {}
/** [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback */
node_box_coloured_when_on = false
@@ -611,7 +610,7 @@ export class LiteGraphGlobal {
* @returns array with all the names of the categories
*/
getNodeTypesCategories(filter?: string): string[] {
const categories: Dictionary<number> = { '': 1 }
const categories: Record<string, number> = { '': 1 }
for (const i in this.registered_node_types) {
const type = this.registered_node_types[i]
if (type.category && !type.skip_list) {

View File

@@ -1,16 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LLink > matches previous snapshot > Basic 1`] = `
[
1,
4,
2,
5,
3,
"float",
]
`;
exports[`LLink > serializes to the previous snapshot > Basic 1`] = `
[
1,

View File

@@ -18,7 +18,7 @@ export enum SlotShape {
Arrow = RenderShape.ARROW,
Grid = RenderShape.GRID,
Circle = RenderShape.CIRCLE,
HollowCircle = RenderShape.HollowCircle
HollowCircle = RenderShape.HOLLOW_CIRCLE
}
/** @see LinkDirection */

View File

@@ -1,4 +1,7 @@
import type { NeverNever, PickNevers } from '@/lib/litegraph/src/types/utility'
import type {
OmitNeverProps,
PickNevers
} from '@/lib/litegraph/src/types/utility'
type EventListeners<T> = {
readonly [K in keyof T]:
@@ -38,7 +41,7 @@ export interface CustomEventDispatcher<
EventMap extends Record<Keys, unknown>,
Keys extends keyof EventMap & string = keyof EventMap & string
> {
dispatch<T extends keyof NeverNever<EventMap>>(
dispatch<T extends keyof OmitNeverProps<EventMap>>(
type: T,
detail: EventMap[T]
): boolean
@@ -94,7 +97,7 @@ export class CustomEventTarget<
* @param detail A custom object to send with the event
* @returns `true` if the event was dispatched successfully, otherwise `false`.
*/
dispatch<T extends keyof NeverNever<EventMap>>(
dispatch<T extends keyof OmitNeverProps<EventMap>>(
type: T,
detail: EventMap[T]
): boolean

View File

@@ -1,6 +1,3 @@
/**
* Error thrown when infinite recursion is detected.
*/
export class RecursionError extends Error {
constructor(subject: string) {
super(subject)

View File

@@ -16,8 +16,6 @@ import type {
} from './types/globalEnums'
import type { IBaseWidget } from './types/widgets'
export type Dictionary<T> = { [key: string]: T }
/** Allows all properties to be null. The same as `Partial<T>`, but adds null instead of undefined. */
export type NullableProperties<T> = {
[P in keyof T]: T[P] | null
@@ -381,7 +379,7 @@ export interface INodeOutputSlot extends INodeSlot {
export interface CreateNodeOptions {
pos?: Point
size?: Size
properties?: Dictionary<NodeProperty | undefined>
properties?: Record<string, NodeProperty | undefined>
flags?: Partial<INodeFlags>
mode?: LGraphEventMode
color?: string

View File

@@ -19,7 +19,7 @@ export enum RenderShape {
/** Slot shape: Grid */
GRID = 6,
/** Slot shape: Hollow circle */
HollowCircle = 7
HOLLOW_CIRCLE = 7
}
/** Bit flags used to indicate what the pointer is currently hovering over. */
@@ -94,37 +94,27 @@ export enum EaseFunction {
/** Bit flags used to indicate what the pointer is currently hovering over. */
export enum Alignment {
/** No items / none */
None = 0,
/** Top */
Top = 1,
/** Bottom */
Bottom = 1 << 1,
/** Vertical middle */
Middle = 1 << 2,
/** Left */
Left = 1 << 3,
/** Right */
Right = 1 << 4,
/** Horizontal centre */
Centre = 1 << 5,
/** Top left */
TopLeft = Top | Left,
/** Top side, horizontally centred */
TopCentre = Top | Centre,
/** Top right */
TopRight = Top | Right,
/** Left side, vertically centred */
MidLeft = Left | Middle,
/** Middle centre */
MidCentre = Middle | Centre,
/** Right side, vertically centred */
MidRight = Right | Middle,
/** Bottom left */
BottomLeft = Bottom | Left,
/** Bottom side, horizontally centred */
BottomCentre = Bottom | Centre,
/** Bottom right */
BottomRight = Bottom | Right
}

View File

@@ -6,7 +6,6 @@ import type { NodeId, NodeProperty } from '../LGraphNode'
import type { LinkId, SerialisedLLinkArray } from '../LLink'
import type { FloatingRerouteSlot, RerouteId } from '../Reroute'
import type {
Dictionary,
INodeFlags,
INodeInputSlot,
INodeOutputSlot,
@@ -83,7 +82,7 @@ export interface ISerialisedNode {
mode: number
outputs?: ISerialisableNodeOutput[]
inputs?: ISerialisableNodeInput[]
properties?: Dictionary<NodeProperty | undefined>
properties?: Record<string, NodeProperty | undefined>
shape?: RenderShape
boxcolor?: string
color?: string
@@ -112,7 +111,7 @@ export interface ExportedSubgraphInstance extends NodeSubgraphSharedProps {
*/
type: UUID
/** Custom properties for this subgraph instance */
properties?: Dictionary<NodeProperty | undefined>
properties?: Record<string, NodeProperty | undefined>
}
/**

View File

@@ -8,6 +8,6 @@ export type PickNevers<T> = {
}
/** {@link Omit} all properties that evaluate to `never`. */
export type NeverNever<T> = {
export type OmitNeverProps<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K]
}

View File

@@ -38,11 +38,9 @@ export async function refreshRemoteConfig(
}
console.warn('Failed to load remote config:', response.statusText)
if (response.status === 401 || response.status === 403) {
window.__CONFIG__ = {}
remoteConfig.value = {}
remoteConfigState.value = 'error'
}
window.__CONFIG__ = {}
remoteConfig.value = {}
remoteConfigState.value = 'error'
} catch (error) {
console.error('Failed to fetch remote config:', error)
window.__CONFIG__ = {}

View File

@@ -20,27 +20,32 @@ export async function initTelemetry(): Promise<void> {
if (_initPromise) return _initPromise
_initPromise = (async () => {
const [
{ TelemetryRegistry },
{ MixpanelTelemetryProvider },
{ GtmTelemetryProvider },
{ ImpactTelemetryProvider },
{ PostHogTelemetryProvider }
] = await Promise.all([
import('./TelemetryRegistry'),
import('./providers/cloud/MixpanelTelemetryProvider'),
import('./providers/cloud/GtmTelemetryProvider'),
import('./providers/cloud/ImpactTelemetryProvider'),
import('./providers/cloud/PostHogTelemetryProvider')
])
try {
const [
{ TelemetryRegistry },
{ MixpanelTelemetryProvider },
{ GtmTelemetryProvider },
{ ImpactTelemetryProvider },
{ PostHogTelemetryProvider }
] = await Promise.all([
import('./TelemetryRegistry'),
import('./providers/cloud/MixpanelTelemetryProvider'),
import('./providers/cloud/GtmTelemetryProvider'),
import('./providers/cloud/ImpactTelemetryProvider'),
import('./providers/cloud/PostHogTelemetryProvider')
])
const registry = new TelemetryRegistry()
registry.registerProvider(new MixpanelTelemetryProvider())
registry.registerProvider(new GtmTelemetryProvider())
registry.registerProvider(new ImpactTelemetryProvider())
registry.registerProvider(new PostHogTelemetryProvider())
const registry = new TelemetryRegistry()
registry.registerProvider(new MixpanelTelemetryProvider())
registry.registerProvider(new GtmTelemetryProvider())
registry.registerProvider(new ImpactTelemetryProvider())
registry.registerProvider(new PostHogTelemetryProvider())
setTelemetryRegistry(registry)
setTelemetryRegistry(registry)
} catch (error) {
_initPromise = null
console.error('Failed to initialize telemetry:', error)
}
})()
return _initPromise

View File

@@ -1,7 +1,7 @@
import {
TOOLKIT_BLUEPRINT_MODULES,
TOOLKIT_NODE_NAMES
} from '@/constants/toolkitNodes'
TOOLKIT_NOVEL_NODE_NAMES as TOOLKIT_NODE_NAMES
} from '@/constants/essentialsNodes'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
import { app } from '@/scripts/app'

View File

@@ -66,7 +66,7 @@ export function useWorkflowActionsService() {
// Convert workflow to formatted JSON
const json = JSON.stringify(workflow, null, 2)
const blob = new Blob([json], { type: 'application/json' })
downloadBlob(filename, blob)
downloadBlob(blob, filename)
return { success: true }
} catch (error) {

View File

@@ -106,7 +106,7 @@ export const useWorkflowService = () => {
const blob = new Blob([json], { type: 'application/json' })
const file = await getFilename(filename)
if (!file) return
downloadBlob(file, blob)
downloadBlob(blob, file)
}
/**
* Save a workflow as a new file

View File

@@ -105,7 +105,8 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
}
scheduleNextPoll(opId)
} catch {
} catch (error) {
console.warn('Billing operation poll failed:', error)
if (Date.now() - operation.startedAt > TIMEOUT_MS) {
handleTimeout(opId)
return

View File

@@ -242,39 +242,8 @@ export class CanvasPathRenderer {
const innerB = { x: end.x, y: end.y }
// Apply directional offsets to create control points
switch (startDir) {
case 'left':
innerA.x -= l
break
case 'right':
innerA.x += l
break
case 'up':
innerA.y -= l
break
case 'down':
innerA.y += l
break
case 'none':
break
}
switch (endDir) {
case 'left':
innerB.x -= l
break
case 'right':
innerB.x += l
break
case 'up':
innerB.y -= l
break
case 'down':
innerB.y += l
break
case 'none':
break
}
this.applyDirectionOffset(innerA, startDir, l)
this.applyDirectionOffset(innerB, endDir, l)
// Draw 4-point path: start -> innerA -> innerB -> end
path.moveTo(start.x, start.y)
@@ -297,39 +266,8 @@ export class CanvasPathRenderer {
const innerB = { x: end.x, y: end.y }
// Apply directional offsets to match original behavior
switch (startDir) {
case 'left':
innerA.x -= l
break
case 'right':
innerA.x += l
break
case 'up':
innerA.y -= l
break
case 'down':
innerA.y += l
break
case 'none':
break
}
switch (endDir) {
case 'left':
innerB.x -= l
break
case 'right':
innerB.x += l
break
case 'up':
innerB.y -= l
break
case 'down':
innerB.y += l
break
case 'none':
break
}
this.applyDirectionOffset(innerA, startDir, l)
this.applyDirectionOffset(innerB, endDir, l)
// Calculate midpoint using innerA/innerB positions (matching original)
const midX = (innerA.x + innerB.x) * 0.5
@@ -398,18 +336,32 @@ export class CanvasPathRenderer {
}
private getDirectionOffset(direction: Direction, distance: number): Point {
const offset: Point = { x: 0, y: 0 }
this.applyDirectionOffset(offset, direction, distance)
return offset
}
/**
* Mutates {@link point} by adding an offset in the given {@link direction}.
*/
private applyDirectionOffset(
point: Point,
direction: Direction,
distance: number
): void {
switch (direction) {
case 'left':
return { x: -distance, y: 0 }
point.x -= distance
break
case 'right':
return { x: distance, y: 0 }
point.x += distance
break
case 'up':
return { x: 0, y: -distance }
point.y -= distance
break
case 'down':
return { x: 0, y: distance }
case 'none':
default:
return { x: 0, y: 0 }
point.y += distance
break
}
}
@@ -471,39 +423,8 @@ export class CanvasPathRenderer {
const pb = { x: endPoint.x, y: endPoint.y }
// Apply spline offsets based on direction
switch (startDirection) {
case 'left':
pa.x -= dist * factor
break
case 'right':
pa.x += dist * factor
break
case 'up':
pa.y -= dist * factor
break
case 'down':
pa.y += dist * factor
break
case 'none':
break
}
switch (endDirection) {
case 'left':
pb.x -= dist * factor
break
case 'right':
pb.x += dist * factor
break
case 'up':
pb.y -= dist * factor
break
case 'down':
pb.y += dist * factor
break
case 'none':
break
}
this.applyDirectionOffset(pa, startDirection, dist * factor)
this.applyDirectionOffset(pb, endDirection, dist * factor)
// Calculate bezier point (matching original computeConnectionPoint)
const c1 = (1 - t) * (1 - t) * (1 - t)
@@ -687,35 +608,8 @@ export class CanvasPathRenderer {
const innerB = { x: endPoint.x, y: endPoint.y }
// Apply same directional offsets as buildLinearPath
switch (link.startDirection) {
case 'left':
innerA.x -= l
break
case 'right':
innerA.x += l
break
case 'up':
innerA.y -= l
break
case 'down':
innerA.y += l
break
}
switch (link.endDirection) {
case 'left':
innerB.x -= l
break
case 'right':
innerB.x += l
break
case 'up':
innerB.y -= l
break
case 'down':
innerB.y += l
break
}
this.applyDirectionOffset(innerA, link.startDirection, l)
this.applyDirectionOffset(innerB, link.endDirection, l)
link.centerPos = {
x: (innerA.x + innerB.x) * 0.5,
@@ -732,35 +626,8 @@ export class CanvasPathRenderer {
const innerB = { x: endPoint.x, y: endPoint.y }
// Apply same directional offsets as buildStraightPath
switch (link.startDirection) {
case 'left':
innerA.x -= l
break
case 'right':
innerA.x += l
break
case 'up':
innerA.y -= l
break
case 'down':
innerA.y += l
break
}
switch (link.endDirection) {
case 'left':
innerB.x -= l
break
case 'right':
innerB.x += l
break
case 'up':
innerB.y -= l
break
case 'down':
innerB.y += l
break
}
this.applyDirectionOffset(innerA, link.startDirection, l)
this.applyDirectionOffset(innerB, link.endDirection, l)
// Calculate center using midX and average of innerA/innerB y positions
const midX = (innerA.x + innerB.x) * 0.5

View File

@@ -3,7 +3,7 @@ import { ref, useTemplateRef, watch } from 'vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
import { useLoad3dViewer } from '@/extensions/core/load3d/composables/useLoad3dViewer'
const { modelUrl } = defineProps<{
modelUrl: string

View File

@@ -83,7 +83,7 @@ const nodeData = computed<VueNodeData>(() => {
.map(([name, input]) => ({
name,
type: input.type,
shape: input.isOptional ? RenderShape.HollowCircle : undefined,
shape: input.isOptional ? RenderShape.HOLLOW_CIRCLE : undefined,
boundingRect: [0, 0, 0, 0],
link: null
}))

View File

@@ -87,7 +87,6 @@ export const useFloatWidget = () => {
widget,
'fixed',
undefined,
undefined,
transformInputSpecV2ToV1(inputSpec)
)
widget.linkedWidgets = [controlWidget]

View File

@@ -11,7 +11,7 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { app } from '@/scripts/app'
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { is_all_same_aspect_ratio } from '@/utils/imageUtil'
import { isAllSameAspectRatio } from '@/utils/imageUtil'
/**
* Workaround for Chrome GPU bug:
@@ -125,7 +125,7 @@ const renderPreview = (
let cell_padding: number
let cols: number
const compact_mode = is_all_same_aspect_ratio(imgs)
const compact_mode = isAllSameAspectRatio(imgs)
if (!compact_mode) {
// use rectangle cell style and border line
cell_padding = 2

View File

@@ -81,7 +81,6 @@ export const useIntWidget = () => {
widget,
defaultType,
undefined,
undefined,
transformInputSpecV2ToV1(inputSpec)
)
widget.linkedWidgets = [controlWidget]

View File

@@ -11,7 +11,6 @@ import type {
ModelFolderInfo
} from '@/platform/assets/schemas/assetSchema'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { ShareableAssetsResponse } from '@/schemas/apiSchema'
import { zShareableAssetsResponse } from '@/schemas/apiSchema'
import type { IFuseOptions } from 'fuse.js'
@@ -226,7 +225,7 @@ type ApiEventTypes = ApiToEventType<ApiCalls>
type ApiEvents = AsCustomEvents<ApiEventTypes>
/** {@link Omit} all properties that evaluate to `never`. */
type NeverNever<T> = {
type OmitNeverProps<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K]
}
@@ -238,7 +237,7 @@ type PickNevers<T> = {
/** Keys (names) of API events that _do not_ pass a {@link CustomEvent} `detail` object. */
type SimpleApiEvents = keyof PickNevers<ApiEventTypes>
/** Keys (names) of API events that pass a {@link CustomEvent} `detail` object. */
type ComplexApiEvents = keyof NeverNever<ApiEventTypes>
type ComplexApiEvents = keyof OmitNeverProps<ApiEventTypes>
export type GlobalSubgraphData = {
name: string
@@ -389,10 +388,12 @@ export class ComfyApi extends EventTarget {
/**
* Gets the Firebase auth store instance using cached composable function.
* Caches the composable function on first call, then reuses it.
* Returns null for non-cloud distributions.
* @returns The Firebase auth store instance, or null if not in cloud
* Returns undefined for non-cloud distributions.
* @returns The Firebase auth store instance, or undefined if not in cloud
*/
private async getAuthStore() {
private async getAuthStore(): Promise<
ReturnType<typeof useFirebaseAuthStore> | undefined
> {
if (isCloud) {
if (!this.authStoreComposable) {
const module = await import('@/stores/firebaseAuthStore')
@@ -425,7 +426,7 @@ export class ComfyApi extends EventTarget {
}
}
async fetchApi(route: string, options?: RequestInit) {
async fetchApi(route: string, options?: RequestInit): Promise<Response> {
const headers: HeadersInit = options?.headers ?? {}
if (isCloud) {
@@ -933,7 +934,10 @@ export class ComfyApi extends EventTarget {
* @param {string} model The model to get metadata for
* @returns The metadata for the model
*/
async viewMetadata(folder: string, model: string) {
async viewMetadata(
folder: string,
model: string
): Promise<Record<string, string | null> | null> {
const res = await this.fetchApi(
`/view_metadata/${folder}?filename=${encodeURIComponent(model)}`
)
@@ -960,7 +964,11 @@ export class ComfyApi extends EventTarget {
* @param {string} type The type of items to load, queue or history
* @returns The items of the specified type grouped by their status
*/
async getItems(type: 'queue' | 'history') {
async getItems(
type: 'queue' | 'history'
): Promise<
{ Running: JobListItem[]; Pending: JobListItem[] } | JobListItem[]
> {
if (type === 'queue') {
return this.getQueue()
}
@@ -1045,7 +1053,7 @@ export class ComfyApi extends EventTarget {
* @param {string} type The type of item to delete, queue or history
* @param {number} id The id of the item to delete
*/
async deleteItem(type: string, id: string) {
async deleteItem(type: string, id: string): Promise<void> {
await this._postItem(type, { delete: [id] })
}
@@ -1053,7 +1061,7 @@ export class ComfyApi extends EventTarget {
* Clears the specified list
* @param {string} type The type of list to clear, queue or history
*/
async clearItems(type: string) {
async clearItems(type: string): Promise<void> {
await this._postItem(type, { clear: true })
}
@@ -1062,7 +1070,7 @@ export class ComfyApi extends EventTarget {
* it is included in the payload as a helpful hint to the backend.
* @param {string | null} [runningJobId] Optional Running Job ID to interrupt
*/
async interrupt(runningJobId: string | null) {
async interrupt(runningJobId: string | null): Promise<void> {
await this._postItem(
'interrupt',
runningJobId ? { prompt_id: runningJobId } : undefined
@@ -1116,7 +1124,7 @@ export class ComfyApi extends EventTarget {
/**
* Stores a dictionary of settings for the current user
*/
async storeSettings(settings: Partial<Settings>) {
async storeSettings(settings: Partial<Settings>): Promise<Response> {
return this.fetchApi(`/settings`, {
method: 'POST',
body: JSON.stringify(settings)
@@ -1126,7 +1134,10 @@ export class ComfyApi extends EventTarget {
/**
* Stores a setting for the current user
*/
async storeSetting(id: keyof Settings, value: Settings[keyof Settings]) {
async storeSetting(
id: keyof Settings,
value: Settings[keyof Settings]
): Promise<Response> {
return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {
method: 'POST',
body: JSON.stringify(value)
@@ -1136,7 +1147,7 @@ export class ComfyApi extends EventTarget {
/**
* Gets a user data file for the current user
*/
async getUserData(file: string, options?: RequestInit) {
async getUserData(file: string, options?: RequestInit): Promise<Response> {
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options)
}
@@ -1183,7 +1194,7 @@ export class ComfyApi extends EventTarget {
* Deletes a user data file for the current user
* @param { string } file The name of the userdata file to delete
*/
async deleteUserData(file: string) {
async deleteUserData(file: string): Promise<Response> {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
method: 'DELETE'
})
@@ -1262,7 +1273,7 @@ export class ComfyApi extends EventTarget {
const url = isCloud
? this.apiURL('/logs/subscribe')
: this.internalURL('/logs/subscribe')
return await axios.patch(url, {
await axios.patch(url, {
enabled,
clientId: this.clientId
})
@@ -1278,54 +1289,23 @@ export class ComfyApi extends EventTarget {
return response.data
}
/* Frees memory by unloading models and optionally freeing execution cache
* @param {Object} options - The options object
* @param {boolean} options.freeExecutionCache - If true, also frees execution cache
/**
* Frees memory by unloading models and optionally freeing execution cache.
* @param options.freeExecutionCache - If true, also frees execution cache
* @returns The fetch response
*/
async freeMemory(options: { freeExecutionCache: boolean }) {
try {
let mode = ''
if (options.freeExecutionCache) {
mode = '{"unload_models": true, "free_memory": true}'
} else {
mode = '{"unload_models": true}'
}
async freeMemory(options: {
freeExecutionCache: boolean
}): Promise<Response> {
const body = options.freeExecutionCache
? { unload_models: true, free_memory: true }
: { unload_models: true }
const res = await this.fetchApi(`/free`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: mode
})
if (res.status === 200) {
if (options.freeExecutionCache) {
useToastStore().add({
severity: 'success',
summary: 'Models and Execution Cache have been cleared.',
life: 3000
})
} else {
useToastStore().add({
severity: 'success',
summary: 'Models have been unloaded.',
life: 3000
})
}
} else {
useToastStore().add({
severity: 'error',
summary:
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
life: 5000
})
}
} catch (error) {
useToastStore().add({
severity: 'error',
summary: 'An error occurred while trying to unload models.',
life: 5000
})
}
return this.fetchApi('/free', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
}
/**

View File

@@ -1,26 +1,24 @@
import { isObject } from 'es-toolkit/compat'
export function getDataFromJSON(
file: File
): Promise<Record<string, object> | undefined> {
return new Promise<Record<string, object> | undefined>((resolve) => {
export function getDataFromJSON(file: File): Promise<Record<string, object>> {
return new Promise<Record<string, object>>((resolve, reject) => {
const reader = new FileReader()
reader.onload = async () => {
const readerResult = reader.result as string
const jsonContent = JSON.parse(readerResult)
if (jsonContent?.templates) {
resolve({ templates: jsonContent.templates })
return
reader.onload = () => {
try {
const jsonContent = JSON.parse(reader.result as string)
if (jsonContent?.templates) {
resolve({ templates: jsonContent.templates })
} else if (isApiJson(jsonContent)) {
resolve({ prompt: jsonContent })
} else {
resolve({ workflow: jsonContent })
}
} catch (e) {
reject(e)
}
if (isApiJson(jsonContent)) {
resolve({ prompt: jsonContent })
return
}
resolve({ workflow: jsonContent })
return
}
reader.onerror = () => reject(reader.error)
reader.readAsText(file)
return
})
}

View File

@@ -21,9 +21,17 @@ export async function getMp3Metadata(file: File) {
if (page.match('\u00ff\u00fb')) break
}
let workflow, prompt
let prompt_s = header.match(/prompt\u0000(\{.*?\})\u0000/s)?.[1]
if (prompt_s) prompt = JSON.parse(prompt_s)
let workflow_s = header.match(/workflow\u0000(\{.*?\})\u0000/s)?.[1]
if (workflow_s) workflow = JSON.parse(workflow_s)
const prompt_s = header.match(/prompt\u0000(\{.*?\})\u0000/s)?.[1]
if (prompt_s) {
try {
prompt = JSON.parse(prompt_s)
} catch {}
}
const workflow_s = header.match(/workflow\u0000(\{.*?\})\u0000/s)?.[1]
if (workflow_s) {
try {
workflow = JSON.parse(workflow_s)
} catch {}
}
return { prompt, workflow }
}

View File

@@ -18,13 +18,21 @@ export async function getOggMetadata(file: File) {
if (oggs > 1) break
}
let workflow, prompt
let prompt_s = header
const prompt_s = header
.match(/prompt=(\{.*?(\}.*?\u0000))/s)?.[1]
?.match(/\{.*\}/)?.[0]
if (prompt_s) prompt = JSON.parse(prompt_s)
let workflow_s = header
if (prompt_s) {
try {
prompt = JSON.parse(prompt_s)
} catch {}
}
const workflow_s = header
.match(/workflow=(\{.*?(\}.*?\u0000))/s)?.[1]
?.match(/\{.*\}/)?.[0]
if (workflow_s) workflow = JSON.parse(workflow_s)
if (workflow_s) {
try {
workflow = JSON.parse(workflow_s)
} catch {}
}
return { prompt, workflow }
}

View File

@@ -94,7 +94,7 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
onShow: (el, v) => {
if (typeof v === 'string') {
el.textContent = v
} else {
} else if (v instanceof Node) {
el.replaceChildren(v)
}
}

View File

@@ -21,7 +21,7 @@ export function applyClasses(
}, '')
}
element.className = str
if (requiredClasses) {
if (requiredClasses.length) {
element.classList.add(...requiredClasses)
}
}
@@ -33,14 +33,12 @@ export function toggleElement(
onShow
}: {
onHide?: (el: HTMLElement) => void
// @ts-expect-error fixme ts strict error
onShow?: (el: HTMLElement, value) => void
onShow?: (el: HTMLElement, value: unknown) => void
} = {}
) {
let placeholder: HTMLElement | Comment
let hidden: boolean
// @ts-expect-error fixme ts strict error
return (value) => {
return (value: unknown) => {
if (value) {
if (hidden) {
hidden = false

Some files were not shown because too many files have changed in this diff Show More