mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
Further linear fixes (#8074)
┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8074-Further-linear-fixes-2e96d73d365081efb74bf150982c7a66) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
||||
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
||||
<SidebarSettingsButton :is-small="isSmall" />
|
||||
<ModeToggle v-if="showLinearToggle" />
|
||||
<ModeToggle v-if="menuItemStore.hasSeenLinear || linearFeatureFlag" />
|
||||
</div>
|
||||
</div>
|
||||
<HelpCenterPopups :is-small="isSmall" />
|
||||
@@ -52,7 +52,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useResizeObserver, whenever } from '@vueuse/core'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
@@ -68,6 +68,7 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
@@ -83,14 +84,14 @@ const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const menuItemStore = useMenuItemStore()
|
||||
const sideToolbarRef = ref<HTMLElement>()
|
||||
const topToolbarRef = ref<HTMLElement>()
|
||||
const bottomToolbarRef = ref<HTMLElement>()
|
||||
|
||||
const showLinearToggle = ref(useFeatureFlags().flags.linearToggleEnabled)
|
||||
whenever(
|
||||
() => canvasStore.linearMode,
|
||||
() => (showLinearToggle.value = true)
|
||||
const linearFeatureFlag = useFeatureFlags().featureFlag(
|
||||
'linearToggleEnabled',
|
||||
false
|
||||
)
|
||||
|
||||
const isSmall = computed(
|
||||
|
||||
@@ -54,6 +54,10 @@ interface IWidgetKnobOptions extends IWidgetOptions<number[]> {
|
||||
gradient_stops?: string
|
||||
}
|
||||
|
||||
export interface IWidgetAssetOptions extends IWidgetOptions {
|
||||
openModal: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for a node.
|
||||
* All types are based on IBaseWidget - additions can be made there or directly on individual types.
|
||||
@@ -249,7 +253,7 @@ export interface ITextareaWidget extends IBaseWidget<string, 'textarea'> {
|
||||
export interface IAssetWidget extends IBaseWidget<
|
||||
string,
|
||||
'asset',
|
||||
IWidgetOptions<string[]>
|
||||
IWidgetAssetOptions
|
||||
> {
|
||||
type: 'asset'
|
||||
value: string
|
||||
|
||||
@@ -53,6 +53,6 @@ export class AssetWidget
|
||||
|
||||
override onClick() {
|
||||
//Open Modal
|
||||
this.callback?.(this.value)
|
||||
this.options.openModal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ defineProps<{
|
||||
onDragOver?: (e: DragEvent) => boolean
|
||||
onDragDrop?: (e: DragEvent) => Promise<boolean> | boolean
|
||||
dropIndicator?: {
|
||||
label?: string
|
||||
iconClass?: string
|
||||
imageUrl?: string
|
||||
label?: string
|
||||
onClick?: (e: MouseEvent) => void
|
||||
}
|
||||
}>()
|
||||
@@ -44,8 +45,15 @@ const canAcceptDrop = ref(false)
|
||||
"
|
||||
@click.prevent="dropIndicator?.onClick?.($event)"
|
||||
>
|
||||
<span v-if="dropIndicator.label" v-text="dropIndicator.label" />
|
||||
<i v-if="dropIndicator.iconClass" :class="dropIndicator.iconClass" />
|
||||
<img
|
||||
v-if="dropIndicator?.imageUrl"
|
||||
class="h-23"
|
||||
:src="dropIndicator?.imageUrl"
|
||||
/>
|
||||
<template v-else>
|
||||
<span v-if="dropIndicator.label" v-text="dropIndicator.label" />
|
||||
<i v-if="dropIndicator.iconClass" :class="dropIndicator.iconClass" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-else />
|
||||
|
||||
@@ -18,6 +18,7 @@ import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -50,15 +51,26 @@ useEventListener(
|
||||
() => (graphNodes.value = app.rootGraph.nodes)
|
||||
)
|
||||
|
||||
function getDropIndicator(node: LGraphNode) {
|
||||
if (node.type !== 'LoadImage') return undefined
|
||||
|
||||
const filename = node.widgets?.[0]?.value
|
||||
const resultItem = { type: 'input', filename: `${filename}` }
|
||||
|
||||
return {
|
||||
iconClass: 'icon-[lucide--image]',
|
||||
imageUrl: filename
|
||||
? api.apiURL(
|
||||
`/view?${new URLSearchParams(resultItem)}${app.getPreviewFormatParam()}`
|
||||
)
|
||||
: undefined,
|
||||
label: t('linearMode.dragAndDropImage'),
|
||||
onClick: () => node.widgets?.[1]?.callback?.(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function nodeToNodeData(node: LGraphNode) {
|
||||
const dropIndicator =
|
||||
node.type !== 'LoadImage'
|
||||
? undefined
|
||||
: {
|
||||
iconClass: 'icon-[lucide--image]',
|
||||
label: t('linearMode.dragAndDropImage'),
|
||||
onClick: () => node.widgets?.[1]?.callback?.(undefined)
|
||||
}
|
||||
const dropIndicator = getDropIndicator(node)
|
||||
const nodeData = extractVueNodeData(node)
|
||||
for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined
|
||||
|
||||
@@ -107,7 +119,7 @@ async function runButtonClick(e: Event) {
|
||||
: 'Comfy.QueuePrompt'
|
||||
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_run_linear'
|
||||
button_id: props.mobile ? 'queue_run_linear_mobile' : 'queue_run_linear'
|
||||
})
|
||||
if (batchCount.value > 1) {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
@@ -196,7 +208,7 @@ defineExpose({ runButtonClick })
|
||||
<NodeWidgets
|
||||
:node-data
|
||||
:style="{ background: applyLightThemeColor(nodeData.bgcolor) }"
|
||||
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg"
|
||||
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg max-w-100"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ async function rerun(e: Event) {
|
||||
<VideoPreview
|
||||
v-else-if="getMediaType(selectedOutput) === 'video'"
|
||||
:src="selectedOutput!.url"
|
||||
class="object-contain flex-1 contain-size"
|
||||
class="object-contain flex-1 md:contain-size"
|
||||
/>
|
||||
<audio
|
||||
v-else-if="getMediaType(selectedOutput) === 'audio'"
|
||||
@@ -178,7 +178,7 @@ async function rerun(e: Event) {
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
class="pointer-events-none object-contain flex-1 max-h-full brightness-50 opacity-10"
|
||||
class="pointer-events-none object-contain flex-1 max-h-full md:contain-size brightness-50 opacity-10"
|
||||
src="/assets/images/comfy-logo-mono.svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -217,7 +217,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div class="border-border-subtle md:border-r" />
|
||||
<WorkflowsSidebarTab v-if="displayWorkflows" class="min-w-50" />
|
||||
<WorkflowsSidebarTab v-if="displayWorkflows" class="min-w-50 grow-1" />
|
||||
<article
|
||||
v-else
|
||||
ref="outputsRef"
|
||||
|
||||
@@ -215,7 +215,8 @@ describe('useComboWidget', () => {
|
||||
'asset',
|
||||
'ckpt_name',
|
||||
'model1.safetensors',
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
||||
expect(vi.mocked(assetService.isAssetBrowserEligible)).toHaveBeenCalledWith(
|
||||
@@ -250,7 +251,8 @@ describe('useComboWidget', () => {
|
||||
'asset',
|
||||
'ckpt_name',
|
||||
'fallback.safetensors',
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
||||
expect(widget).toBe(mockWidget)
|
||||
@@ -280,7 +282,8 @@ describe('useComboWidget', () => {
|
||||
'asset',
|
||||
'ckpt_name',
|
||||
'Select model', // Should fallback to this instead of undefined
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
||||
expect(widget).toBe(mockWidget)
|
||||
|
||||
@@ -4,7 +4,10 @@ import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isAssetWidget, isComboWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetAssetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import {
|
||||
assetFilenameSchema,
|
||||
@@ -91,55 +94,59 @@ const createAssetBrowserWidget = (
|
||||
const displayLabel = currentValue ?? t('widgets.selectModel')
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
|
||||
async function openModal(this: IBaseWidget) {
|
||||
if (!isAssetWidget(widget)) {
|
||||
throw new Error(`Expected asset widget but received ${widget.type}`)
|
||||
}
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: node.comfyClass || '',
|
||||
inputName: inputSpec.name,
|
||||
currentValue: widget.value,
|
||||
onAssetSelected: (asset) => {
|
||||
const validatedAsset = assetItemSchema.safeParse(asset)
|
||||
|
||||
if (!validatedAsset.success) {
|
||||
console.error(
|
||||
'Invalid asset item:',
|
||||
validatedAsset.error.errors,
|
||||
'Received:',
|
||||
asset
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const filename = validatedAsset.data.user_metadata?.filename
|
||||
const validatedFilename = assetFilenameSchema.safeParse(filename)
|
||||
|
||||
if (!validatedFilename.success) {
|
||||
console.error(
|
||||
'Invalid asset filename:',
|
||||
validatedFilename.error.errors,
|
||||
'for asset:',
|
||||
validatedAsset.data.id
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const oldValue = widget.value
|
||||
this.value = validatedFilename.data
|
||||
node.onWidgetChanged?.(
|
||||
widget.name,
|
||||
validatedFilename.data,
|
||||
oldValue,
|
||||
widget
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
const options: IWidgetAssetOptions = { openModal }
|
||||
|
||||
const widget = node.addWidget(
|
||||
'asset',
|
||||
inputSpec.name,
|
||||
displayLabel,
|
||||
async function (this: IBaseWidget) {
|
||||
if (!isAssetWidget(widget)) {
|
||||
throw new Error(`Expected asset widget but received ${widget.type}`)
|
||||
}
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: node.comfyClass || '',
|
||||
inputName: inputSpec.name,
|
||||
currentValue: widget.value,
|
||||
onAssetSelected: (asset) => {
|
||||
const validatedAsset = assetItemSchema.safeParse(asset)
|
||||
|
||||
if (!validatedAsset.success) {
|
||||
console.error(
|
||||
'Invalid asset item:',
|
||||
validatedAsset.error.errors,
|
||||
'Received:',
|
||||
asset
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const filename = validatedAsset.data.user_metadata?.filename
|
||||
const validatedFilename = assetFilenameSchema.safeParse(filename)
|
||||
|
||||
if (!validatedFilename.success) {
|
||||
console.error(
|
||||
'Invalid asset filename:',
|
||||
validatedFilename.error.errors,
|
||||
'for asset:',
|
||||
validatedAsset.data.id
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const oldValue = widget.value
|
||||
this.value = validatedFilename.data
|
||||
node.onWidgetChanged?.(
|
||||
widget.name,
|
||||
validatedFilename.data,
|
||||
oldValue,
|
||||
widget
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
() => undefined,
|
||||
options
|
||||
)
|
||||
|
||||
return widget
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { CORE_MENU_COMMANDS } from '@/constants/coreMenuCommands'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
import { useCommandStore } from './commandStore'
|
||||
|
||||
export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const commandStore = useCommandStore()
|
||||
const menuItems = ref<MenuItem[]>([])
|
||||
const menuItemHasActiveStateChildren = ref<Record<string, boolean>>({})
|
||||
const hasSeenLinear = ref(false)
|
||||
|
||||
whenever(
|
||||
() => canvasStore.linearMode,
|
||||
() => (hasSeenLinear.value = true),
|
||||
{ immediate: true, once: true }
|
||||
)
|
||||
|
||||
const registerMenuGroup = (path: string[], items: MenuItem[]) => {
|
||||
let currentLevel = menuItems.value
|
||||
@@ -103,6 +113,7 @@ export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
registerCommands,
|
||||
loadExtensionMenuCommands,
|
||||
registerCoreMenuCommands,
|
||||
menuItemHasActiveStateChildren
|
||||
menuItemHasActiveStateChildren,
|
||||
hasSeenLinear
|
||||
}
|
||||
})
|
||||
|
||||
@@ -139,8 +139,8 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
:selected-item
|
||||
:selected-output
|
||||
/>
|
||||
<div ref="topLeftRef" class="absolute z-20 top-4 left-4" />
|
||||
<div ref="topRightRef" class="absolute z-20 top-4 right-4" />
|
||||
<div ref="topLeftRef" class="absolute z-21 top-4 left-4" />
|
||||
<div ref="topRightRef" class="absolute z-21 top-4 right-4" />
|
||||
<div ref="bottomLeftRef" class="absolute z-20 bottom-4 left-4" />
|
||||
<div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" />
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user