More app fixes (#9432)

- Increased the z-index on app mode outputs so that they display above a
zoomed image
- The "view job" button on the job queued toast in mobile app mode will
take you to outputs instead of assets
- Image previews now have a minimum zoom of ~20% and a maximum zoom of
~50x
- The enter panel in linear mode now has a minimum size of ~1/5th screen
size
- In arrange mode, dragging to rearrange inputs will no longer cause a
horizontal scrollbar to appear.
- Videos will now display the first frame instead of a generic video
icon
- Muted/Bypassed nodes can no longer be selected as inputs/outputs, or
be displayed when in app mode.
- Linked input can no longer be selected or displayed
- Adds a share workflow button in app mode and wires up the existing
context menu

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9432-More-app-fixes-31a6d73d365081509cd0ea74bfdc9b95)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
AustinMroz
2026-03-06 13:41:52 -08:00
committed by GitHub
parent 3ddff9f7b6
commit 7a01be388f
14 changed files with 80 additions and 17 deletions

View File

@@ -3,9 +3,16 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue' import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
import { useAppMode } from '@/composables/useAppMode' import { useAppMode } from '@/composables/useAppMode'
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog' import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
import { isCloud } from '@/platform/distribution/types'
import {
openShareDialog,
prefetchShareDialog
} from '@/platform/workflow/sharing/composables/lazyShareDialog'
import { useAppModeStore } from '@/stores/appModeStore' import { useAppModeStore } from '@/stores/appModeStore'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -18,6 +25,8 @@ const workspaceStore = useWorkspaceStore()
const { enableAppBuilder } = useAppMode() const { enableAppBuilder } = useAppMode()
const appModeStore = useAppModeStore() const appModeStore = useAppModeStore()
const { enterBuilder } = appModeStore const { enterBuilder } = appModeStore
const { toastErrorHandler } = useErrorHandling()
const { flags } = useFeatureFlags()
const { hasNodes } = storeToRefs(appModeStore) const { hasNodes } = storeToRefs(appModeStore)
const tooltipOptions = { showDelay: 300, hideDelay: 300 } const tooltipOptions = { showDelay: 300, hideDelay: 300 }
@@ -60,6 +69,21 @@ function openTemplates() {
> >
<i class="icon-[lucide--hammer] size-4" /> <i class="icon-[lucide--hammer] size-4" />
</Button> </Button>
<Button
v-if="isCloud && flags.workflowSharingEnabled"
v-tooltip.right="{
value: t('actionbar.shareTooltip'),
...tooltipOptions
}"
variant="secondary"
size="unset"
:aria-label="t('actionbar.shareTooltip')"
class="size-10 rounded-lg"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
>
<i class="icon-[lucide--send] size-4" />
</Button>
<div <div
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background" class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"

View File

@@ -12,7 +12,10 @@ import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces' import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums' import {
LGraphEventMode,
TitleMode
} from '@/lib/litegraph/src/types/globalEnums'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget' import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
@@ -159,7 +162,8 @@ function handleDown(e: MouseEvent) {
} }
function handleClick(e: MouseEvent) { function handleClick(e: MouseEvent) {
const [node, widget] = getHovered(e) ?? [] const [node, widget] = getHovered(e) ?? []
if (!node) return canvasInteractions.forwardEventToCanvas(e) if (node?.mode !== LGraphEventMode.ALWAYS)
return canvasInteractions.forwardEventToCanvas(e)
if (!widget) { if (!widget) {
if (!isSelectOutputsMode.value) return if (!isSelectOutputsMode.value) return
@@ -192,7 +196,10 @@ function nodeToDisplayTuple(
const renderedOutputs = computed(() => { const renderedOutputs = computed(() => {
void appModeStore.selectedOutputs.length void appModeStore.selectedOutputs.length
return canvas return canvas
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node) .graph!.nodes.filter(
(n) =>
n.constructor.nodeData?.output_node && n.mode === LGraphEventMode.ALWAYS
)
.map(nodeToDisplayTuple) .map(nodeToDisplayTuple)
}) })
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>( const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
@@ -215,6 +222,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
v-if="isArrangeMode" v-if="isArrangeMode"
v-slot="{ dragClass }" v-slot="{ dragClass }"
v-model="appModeStore.selectedInputs" v-model="appModeStore.selectedInputs"
class="overflow-x-clip"
> >
<div <div
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs" v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"

View File

@@ -9,7 +9,8 @@ const panY = ref(0.0)
function handleWheel(e: WheelEvent) { function handleWheel(e: WheelEvent) {
const zoomPaneEl = zoomPane.value const zoomPaneEl = zoomPane.value
if (!zoomPaneEl) return if (!zoomPaneEl || (e.deltaY < 0 ? zoom.value > 1200 : zoom.value < -500))
return
zoom.value -= e.deltaY zoom.value -= e.deltaY
const { x, y, width, height } = zoomPaneEl.getBoundingClientRect() const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()

View File

@@ -81,6 +81,8 @@ vi.mock('@/stores/appModeStore', () => ({
useAppModeStore: vi.fn(() => mockAppModeStore) useAppModeStore: vi.fn(() => mockAppModeStore)
})) }))
vi.mock('@/composables/useErrorHandling', () => ({}))
vi.mock('@/composables/useFeatureFlags', () => ({ vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: vi.fn(() => mockFeatureFlags) useFeatureFlags: vi.fn(() => mockFeatureFlags)
})) }))

View File

@@ -2,7 +2,10 @@ import type { ComputedRef, Ref } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useFeatureFlags } from '@/composables/useFeatureFlags' import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { openShareDialog } from '@/platform/workflow/sharing/composables/lazyShareDialog'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { import {
@@ -191,8 +194,9 @@ export function useWorkflowActionsMenu(
id: 'share', id: 'share',
label: t('breadcrumbsMenu.share'), label: t('breadcrumbsMenu.share'),
icon: 'icon-[comfy--send]', icon: 'icon-[comfy--send]',
command: async () => {}, command: () =>
visible: false openShareDialog().catch(useErrorHandling().toastErrorHandler),
visible: isCloud && flags.workflowSharingEnabled
}) })
addItem({ addItem({

View File

@@ -8,7 +8,7 @@ import { cn } from '@/utils/tailwindUtil'
const { id, name } = defineProps<{ const { id, name } = defineProps<{
id: string id: string
isSelectInputsMode: boolean enable: boolean
name: string name: string
}>() }>()
@@ -25,7 +25,7 @@ function togglePromotion() {
</script> </script>
<template> <template>
<div <div
v-if="isSelectInputsMode" v-if="enable"
class="pointer-events-auto relative col-span-2 flex cursor-pointer flex-row gap-1" class="pointer-events-auto relative col-span-2 flex cursor-pointer flex-row gap-1"
@pointerdown.capture.stop.prevent="togglePromotion" @pointerdown.capture.stop.prevent="togglePromotion"
@click.capture.stop.prevent @click.capture.stop.prevent

View File

@@ -11,6 +11,7 @@ import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager' import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import { useBillingContext } from '@/composables/billing/useBillingContext' import { useBillingContext } from '@/composables/billing/useBillingContext'
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil' import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue' import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
@@ -44,7 +45,7 @@ const props = defineProps<{
mobile?: boolean mobile?: boolean
}>() }>()
defineEmits<{ navigateAssets: [] }>() defineEmits<{ navigateOutputs: [] }>()
//NOTE: due to batching, will never be greater than 2 //NOTE: due to batching, will never be greater than 2
const pendingJobQueues = ref(0) const pendingJobQueues = ref(0)
@@ -72,7 +73,7 @@ const mappedSelections = computed(() => {
).map(([, widgetName]) => widgetName) ).map(([, widgetName]) => widgetName)
unprocessedInputs = unprocessedInputs.slice(inputGroup.length) unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
const node = resolveNode(nodeId) const node = resolveNode(nodeId)
if (!node) continue if (node?.mode !== LGraphEventMode.ALWAYS) continue
const nodeData = nodeToNodeData(node) const nodeData = nodeToNodeData(node)
remove(nodeData.widgets ?? [], (w) => !inputGroup.includes(w.name)) remove(nodeData.widgets ?? [], (w) => !inputGroup.includes(w.name))
@@ -105,6 +106,7 @@ function getDropIndicator(node: LGraphNode) {
function nodeToNodeData(node: LGraphNode) { function nodeToNodeData(node: LGraphNode) {
const dropIndicator = getDropIndicator(node) const dropIndicator = getDropIndicator(node)
const nodeData = extractVueNodeData(node) const nodeData = extractVueNodeData(node)
remove(nodeData.widgets ?? [], (w) => w.slotMetadata?.linked ?? false)
for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined
return { return {
@@ -261,7 +263,7 @@ defineExpose({ runButtonClick })
<Button <Button
v-if="mobile" v-if="mobile"
variant="inverted" variant="inverted"
@click="$emit('navigateAssets')" @click="$emit('navigateOutputs')"
> >
{{ t('linearMode.viewJob') }} {{ t('linearMode.viewJob') }}
</Button> </Button>

View File

@@ -159,7 +159,7 @@ async function rerun(e: Event) {
/> />
<OutputHistory <OutputHistory
v-if="!isBuilderMode" v-if="!isBuilderMode"
class="min-w-0" class="z-10 min-w-0"
@update-selection="handleSelection" @update-selection="handleSelection"
/> />
<LinearFeedback <LinearFeedback

View File

@@ -187,7 +187,7 @@ const menuEntries = computed<MenuItem[]>(() => [
:style="{ translate }" :style="{ translate }"
> >
<div class="absolute h-full w-screen overflow-y-auto contain-size"> <div class="absolute h-full w-screen overflow-y-auto contain-size">
<LinearControls mobile @navigate-assets="activeIndex = 2" /> <LinearControls mobile @navigate-outputs="activeIndex = 1" />
</div> </div>
<div <div
class="absolute top-0 left-[100vw] flex h-full w-screen flex-col bg-base-background" class="absolute top-0 left-[100vw] flex h-full w-screen flex-col bg-base-background"

View File

@@ -42,7 +42,7 @@ const queueCount = computed(
const itemClass = cn( const itemClass = cn(
'shrink-0 cursor-pointer rounded-lg border-2 border-transparent p-1 outline-none', 'shrink-0 cursor-pointer rounded-lg border-2 border-transparent p-1 outline-none',
'data-[state=checked]:border-interface-panel-job-progress-border' 'relative data-[state=checked]:border-interface-panel-job-progress-border'
) )
const hasActiveContent = computed( const hasActiveContent = computed(

View File

@@ -6,6 +6,8 @@ import {
import type { ResultItemImpl } from '@/stores/queueStore' import type { ResultItemImpl } from '@/stores/queueStore'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
import VideoPlayOverlay from '@/platform/assets/components/VideoPlayOverlay.vue'
const { output } = defineProps<{ const { output } = defineProps<{
output: ResultItemImpl output: ResultItemImpl
}>() }>()
@@ -19,6 +21,16 @@ const { output } = defineProps<{
height="40" height="40"
:src="output.url" :src="output.url"
/> />
<template v-else-if="getMediaType(output) === 'video'">
<video
class="pointer-events-none block size-10 rounded-sm bg-secondary-background object-cover"
preload="metadata"
width="40"
height="40"
:src="output.url"
/>
<VideoPlayOverlay size="sm" />
</template>
<i <i
v-else v-else
:class="cn(mediaTypes[getMediaType(output)]?.iconClass, 'block size-10')" :class="cn(mediaTypes[getMediaType(output)]?.iconClass, 'block size-10')"

View File

@@ -52,7 +52,9 @@
> >
<AppOutput <AppOutput
v-if=" v-if="
lgraphNode?.constructor?.nodeData?.output_node && isSelectOutputsMode lgraphNode?.constructor?.nodeData?.output_node &&
isSelectOutputsMode &&
nodeData.mode === LGraphEventMode.ALWAYS
" "
:id="nodeData.id" :id="nodeData.id"
/> />

View File

@@ -53,7 +53,11 @@
/> />
</div> </div>
<!-- Widget Component --> <!-- Widget Component -->
<AppInput :id="widget.id" :name="widget.name" :is-select-inputs-mode> <AppInput
:id="widget.id"
:name="widget.name"
:enable="canSelectInputs && !widget.simplified.options?.disabled"
>
<component <component
:is="widget.vueComponent" :is="widget.vueComponent"
v-model="widget.value" v-model="widget.value"
@@ -89,6 +93,7 @@ import { useAppMode } from '@/composables/useAppMode'
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu' import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
import { useErrorHandling } from '@/composables/useErrorHandling' import { useErrorHandling } from '@/composables/useErrorHandling'
import { st } from '@/i18n' import { st } from '@/i18n'
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -154,6 +159,9 @@ onErrorCaptured((error) => {
return false return false
}) })
const canSelectInputs = computed(
() => isSelectInputsMode.value && nodeData?.mode === LGraphEventMode.ALWAYS
)
const nodeType = computed(() => nodeData?.type || '') const nodeType = computed(() => nodeData?.type || '')
const settingStore = useSettingStore() const settingStore = useSettingStore()
const showAdvanced = computed( const showAdvanced = computed(

View File

@@ -142,7 +142,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
<SplitterPanel <SplitterPanel
id="linearCenterPanel" id="linearCenterPanel"
:size="CENTER_PANEL_SIZE" :size="CENTER_PANEL_SIZE"
class="relative flex min-w-0 flex-col gap-4 text-muted-foreground outline-none" class="relative flex min-w-[20vw] flex-col gap-4 text-muted-foreground outline-none"
> >
<LinearProgressBar <LinearProgressBar
class="absolute top-0 left-0 z-21 w-[calc(100%+16px)]" class="absolute top-0 left-0 z-21 w-[calc(100%+16px)]"