Misc app mode fixes (#9368)

A working branch of smaller app mode fixes. Can be merged at any time
and I'll make a new branch.
- Selected inputs and outputs can now be re-ordered when clicking on
label text
- 3d outputs once again display correctly
- Some padding has been added to the side so that control buttons don't
overlap with the floating app sidebar controls
- A "Share" button placeholder has been added to the menu, but is
disabled
- Adds a workaround for canvas read_only state being disabled when
'space' is pressed.
  - This one is particularly hacky, and can be pulled out if problematic
- Fix download all only downloading the first output

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9368-Misc-app-mode-fixes-3196d73d365081eab02ad1e693784707)
by [Unito](https://www.unito.io)
This commit is contained in:
AustinMroz
2026-03-04 10:14:05 -08:00
committed by GitHub
parent c759fe517f
commit f084a60708
10 changed files with 52 additions and 21 deletions

View File

@@ -33,12 +33,12 @@ const entries = computed(() => {
</script>
<template>
<div class="p-2 my-2 rounded-lg flex items-center-safe gap-2">
<span
class="mr-auto flex-[4_1_0%] max-w-max min-w-0 truncate"
<div
class="mr-auto flex-[4_1_0%] max-w-max min-w-0 truncate drag-handle inline"
v-text="title"
/>
<span
class="flex-[2_1_0%] max-w-max min-w-0 truncate text-muted-foreground text-end"
<div
class="flex-[2_1_0%] max-w-max min-w-0 truncate text-muted-foreground text-end drag-handle inline"
v-text="subTitle"
/>
<Popover :entries>

View File

@@ -187,6 +187,15 @@ export function useWorkflowActionsMenu(
visible: isRoot
})
addItem({
id: 'share',
label: t('menuLabels.Share'),
icon: 'icon-[comfy--send]',
command: async () => {},
disabled: true,
visible: isRoot
})
addItem({
id: 'enter-app-mode',
label: t('breadcrumbsMenu.enterAppMode'),

View File

@@ -1280,6 +1280,7 @@
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",
"Share": "Share",
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
"Edit Subgraph Widgets": "Edit Subgraph Widgets",
"Exit Subgraph": "Exit Subgraph",

View File

@@ -45,7 +45,8 @@ const props = defineProps<{
defineEmits<{ navigateAssets: [] }>()
const jobFinishedQueue = ref(true)
//NOTE: due to batching, will never be greater than 2
const pendingJobQueues = ref(0)
const { ready: jobToastTimeout, start: resetJobToastTimeout } = useTimeout(
8000,
{ controls: true, immediate: false }
@@ -133,9 +134,8 @@ const partitionedNodes = computed(() => {
//TODO: refactor out of this file.
//code length is small, but changes should propagate
async function runButtonClick(e: Event) {
if (!jobFinishedQueue.value) return
try {
jobFinishedQueue.value = false
pendingJobQueues.value += 1
resetJobToastTimeout()
const isShiftPressed = 'shiftKey' in e && e.shiftKey
const commandId = isShiftPressed
@@ -155,7 +155,7 @@ async function runButtonClick(e: Event) {
})
} finally {
//TODO: Error state indicator for failed queue?
jobFinishedQueue.value = true
pendingJobQueues.value -= 1
}
}
@@ -244,7 +244,7 @@ defineExpose({ runButtonClick })
</template>
</section>
<Teleport
v-if="!jobToastTimeout || !jobFinishedQueue"
v-if="!jobToastTimeout || pendingJobQueues > 0"
defer
:disabled="mobile"
:to="toastTo"
@@ -252,7 +252,7 @@ defineExpose({ runButtonClick })
<div
class="bg-base-foreground md:bg-secondary-background text-base-background md:text-base-foreground rounded-lg flex h-10 md:h-8 p-1 pr-2 gap-2 items-center"
>
<template v-if="jobFinishedQueue">
<template v-if="pendingJobQueues === 0">
<i
class="icon-[lucide--check] size-5 not-md:bg-success-background"
/>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { defineAsyncComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { downloadFile } from '@/base/common/downloadUtil'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { useAppMode } from '@/composables/useAppMode'
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { extractWorkflowFromAsset } from '@/platform/workflow/utils/workflowExtractionUtil'
@@ -16,9 +16,8 @@ import LinearWelcome from '@/renderer/extensions/linearMode/LinearWelcome.vue'
import LinearArrange from '@/renderer/extensions/linearMode/LinearArrange.vue'
import LinearFeedback from '@/renderer/extensions/linearMode/LinearFeedback.vue'
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
import { useOutputHistory } from '@/renderer/extensions/linearMode/useOutputHistory'
import type { OutputSelection } from '@/renderer/extensions/linearMode/linearModeTypes'
// Lazy-loaded to avoid pulling THREE.js into the main bundle
const Preview3d = () => import('@/renderer/extensions/linearMode/Preview3d.vue')
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
import { getMediaType } from '@/renderer/extensions/linearMode/mediaTypes'
import { app } from '@/scripts/app'
@@ -28,13 +27,19 @@ import { useQueueStore } from '@/stores/queueStore'
import type { ResultItemImpl } from '@/stores/queueStore'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import { executeWidgetsCallback } from '@/utils/litegraphUtil'
import { useAppMode } from '@/composables/useAppMode'
// Lazy-loaded to avoid pulling THREE.js into the main bundle
const Preview3d = defineAsyncComponent(
() => import('@/renderer/extensions/linearMode/Preview3d.vue')
)
const { t } = useI18n()
const commandStore = useCommandStore()
const executionStore = useExecutionStore()
const mediaActions = useMediaAssetActions()
const queueStore = useQueueStore()
const { isBuilderMode, isArrangeMode } = useAppMode()
const { allOutputs } = useOutputHistory()
const { runButtonClick, mobile, typeformWidgetId } = defineProps<{
runButtonClick?: (e: Event) => void
mobile?: boolean
@@ -54,8 +59,7 @@ function handleSelection(sel: OutputSelection) {
}
function downloadAsset(item?: AssetItem) {
const user_metadata = getOutputAssetMetadata(item?.user_metadata)
for (const output of user_metadata?.allOutputs ?? [])
for (const output of allOutputs(item))
downloadFile(output.url, output.filename)
}
@@ -127,7 +131,7 @@ async function rerun(e: Event) {
{
icon: 'icon-[lucide--download]',
label: t('linearMode.downloadAll'),
command: () => downloadAsset(selectedItem!)
command: () => downloadAsset(selectedItem)
},
{ separator: true },
{

View File

@@ -24,7 +24,7 @@ watch([containerRef, () => modelUrl], async () => {
<template>
<div
ref="containerRef"
class="relative w-full h-full"
class="relative w-full md:w-[calc(100%-150px)] h-full self-center"
@mouseenter="viewer.handleMouseEnter"
@mouseleave="viewer.handleMouseLeave"
@resize="viewer.handleResize"

View File

@@ -29,5 +29,6 @@ export const mediaTypes: Record<string, StatItem> = {
export function getMediaType(output?: ResultItemImpl) {
if (!output) return ''
if (output.isVideo) return 'video'
if (output.isImage) return 'images'
return output.mediaType
}

View File

@@ -215,7 +215,9 @@
</template>
</Button>
</div>
<template v-if="!isCollapsed && nodeData.resizable !== false">
<template
v-if="!isCollapsed && nodeData.resizable !== false && !isSelectMode"
>
<div
v-for="handle in RESIZE_HANDLES"
:key="handle.corner"

View File

@@ -75,9 +75,20 @@ export const useAppModeStore = defineStore('appMode', () => {
{ deep: true }
)
let unwatch: () => void | undefined
watch(
() => mode.value === 'builder:select',
(inSelect) => (getCanvas().read_only = inSelect)
(inSelect) => {
const { state } = getCanvas()
if (!state) return
state.readOnly = inSelect
unwatch?.()
if (inSelect)
unwatch = watch(
() => state.readOnly,
() => (state.readOnly = true)
)
}
)
function enterBuilder() {