mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
Linear: progressbar, tooltips, and output fixes (#8250)
- Fixes only the first output being displayed in linear mode after the jobs migration - Fixes selected output no longer scrolling into view in history - Adds a progress bar indicator on running job <img width="113" height="102" alt="image" src="https://github.com/user-attachments/assets/ca684dbe-12c8-44aa-98f0-2985c0159156" /> - Moves linear toggle button to v-tooltip - Fixes placeholder sometimes continuing to display after a new output. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8250-Linear-progressbar-tooltips-and-output-fixes-2f06d73d365081ca9fa3ebf0e2516487) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -14,16 +14,24 @@ function toggleLinearMode() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-1 bg-secondary-background rounded-lg w-10">
|
<div class="p-1 bg-secondary-background rounded-lg w-10">
|
||||||
<Button
|
<Button
|
||||||
|
v-tooltip="{
|
||||||
|
value: t('linearMode.linearMode'),
|
||||||
|
showDelay: 300,
|
||||||
|
hideDelay: 300
|
||||||
|
}"
|
||||||
size="icon"
|
size="icon"
|
||||||
:title="t('linearMode.linearMode')"
|
|
||||||
:variant="canvasStore.linearMode ? 'inverted' : 'secondary'"
|
:variant="canvasStore.linearMode ? 'inverted' : 'secondary'"
|
||||||
@click="toggleLinearMode"
|
@click="toggleLinearMode"
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--panels-top-left]" />
|
<i class="icon-[lucide--panels-top-left]" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
v-tooltip="{
|
||||||
|
value: t('linearMode.graphMode'),
|
||||||
|
showDelay: 300,
|
||||||
|
hideDelay: 300
|
||||||
|
}"
|
||||||
size="icon"
|
size="icon"
|
||||||
:title="t('linearMode.graphMode')"
|
|
||||||
:variant="canvasStore.linearMode ? 'secondary' : 'inverted'"
|
:variant="canvasStore.linearMode ? 'secondary' : 'inverted'"
|
||||||
@click="toggleLinearMode"
|
@click="toggleLinearMode"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ async function rerun(e: Event) {
|
|||||||
<VideoPreview
|
<VideoPreview
|
||||||
v-else-if="getMediaType(selectedOutput) === 'video'"
|
v-else-if="getMediaType(selectedOutput) === 'video'"
|
||||||
:src="selectedOutput!.url"
|
:src="selectedOutput!.url"
|
||||||
class="object-contain flex-1 md:contain-size"
|
class="object-contain flex-1 md:contain-size md:p-3"
|
||||||
/>
|
/>
|
||||||
<audio
|
<audio
|
||||||
v-else-if="getMediaType(selectedOutput) === 'audio'"
|
v-else-if="getMediaType(selectedOutput) === 'audio'"
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener, useInfiniteScroll, useScroll } from '@vueuse/core'
|
import {
|
||||||
import { computed, ref, toRaw, useTemplateRef, watch } from 'vue'
|
useAsyncState,
|
||||||
|
useEventListener,
|
||||||
|
useInfiniteScroll,
|
||||||
|
useScroll
|
||||||
|
} from '@vueuse/core'
|
||||||
|
import { computed, ref, toRaw, toValue, useTemplateRef, watch } from 'vue'
|
||||||
|
import type { MaybeRef } from 'vue'
|
||||||
|
|
||||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||||
import SidebarIcon from '@/components/sidebar/SidebarIcon.vue'
|
import SidebarIcon from '@/components/sidebar/SidebarIcon.vue'
|
||||||
import SidebarTemplatesButton from '@/components/sidebar/SidebarTemplatesButton.vue'
|
import SidebarTemplatesButton from '@/components/sidebar/SidebarTemplatesButton.vue'
|
||||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||||
|
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||||
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||||
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
||||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||||
@@ -16,13 +24,21 @@ import {
|
|||||||
getMediaType,
|
getMediaType,
|
||||||
mediaTypes
|
mediaTypes
|
||||||
} from '@/renderer/extensions/linearMode/mediaTypes'
|
} from '@/renderer/extensions/linearMode/mediaTypes'
|
||||||
import { useQueueStore } from '@/stores/queueStore'
|
import type { NodeExecutionOutput, ResultItem } from '@/schemas/apiSchema'
|
||||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
import { getJobDetail } from '@/services/jobOutputCache'
|
||||||
|
import { useQueueStore, ResultItemImpl } from '@/stores/queueStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
const displayWorkflows = ref(false)
|
const displayWorkflows = ref(false)
|
||||||
const outputs = useMediaAssets('output')
|
const outputs = useMediaAssets('output')
|
||||||
|
const {
|
||||||
|
progressBarContainerClass,
|
||||||
|
progressBarPrimaryClass,
|
||||||
|
progressBarSecondaryClass,
|
||||||
|
progressPercentStyle
|
||||||
|
} = useProgressBarBackground()
|
||||||
|
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
@@ -46,14 +62,14 @@ defineExpose({ onWheel })
|
|||||||
|
|
||||||
const selectedIndex = ref<[number, number]>([-1, 0])
|
const selectedIndex = ref<[number, number]>([-1, 0])
|
||||||
|
|
||||||
watch(selectedIndex, () => {
|
function doEmit() {
|
||||||
const [index] = selectedIndex.value
|
const [index] = selectedIndex.value
|
||||||
emit('updateSelection', [
|
emit('updateSelection', [
|
||||||
outputs.media.value[index],
|
outputs.media.value[index],
|
||||||
selectedOutput.value,
|
selectedOutput.value,
|
||||||
selectedIndex.value[0] <= 0
|
selectedIndex.value[0] <= 0
|
||||||
])
|
])
|
||||||
})
|
}
|
||||||
|
|
||||||
const outputsRef = useTemplateRef('outputsRef')
|
const outputsRef = useTemplateRef('outputsRef')
|
||||||
const { reset: resetInfiniteScroll } = useInfiniteScroll(
|
const { reset: resetInfiniteScroll } = useInfiniteScroll(
|
||||||
@@ -72,36 +88,76 @@ watch(selectedIndex, () => {
|
|||||||
const [index, key] = selectedIndex.value
|
const [index, key] = selectedIndex.value
|
||||||
if (!outputsRef.value) return
|
if (!outputsRef.value) return
|
||||||
|
|
||||||
const outputElement = outputsRef.value?.children?.[index]?.children?.[key]
|
const outputElement = outputsRef.value?.querySelectorAll(
|
||||||
|
`[data-output-index="${index}"]`
|
||||||
|
)?.[key]
|
||||||
if (!outputElement) return
|
if (!outputElement) return
|
||||||
|
|
||||||
//container: 'nearest' is nice, but bleeding edge and chrome only
|
//container: 'nearest' is nice, but bleeding edge and chrome only
|
||||||
outputElement.scrollIntoView({ block: 'nearest' })
|
outputElement.scrollIntoView({ block: 'nearest' })
|
||||||
})
|
})
|
||||||
|
|
||||||
function allOutputs(item?: AssetItem) {
|
function outputCount(item?: AssetItem) {
|
||||||
const user_metadata = getOutputAssetMetadata(item?.user_metadata)
|
const user_metadata = getOutputAssetMetadata(item?.user_metadata)
|
||||||
if (!user_metadata?.allOutputs) return []
|
return user_metadata?.outputCount ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
return user_metadata.allOutputs
|
const outputsCache: Record<string, MaybeRef<ResultItemImpl[]>> = {}
|
||||||
|
|
||||||
|
function flattenNodeOutput([nodeId, nodeOutput]: [
|
||||||
|
string | number,
|
||||||
|
NodeExecutionOutput
|
||||||
|
]): ResultItemImpl[] {
|
||||||
|
const knownOutputs: Record<string, ResultItem[]> = {}
|
||||||
|
if (nodeOutput.audio) knownOutputs.audio = nodeOutput.audio
|
||||||
|
if (nodeOutput.images) knownOutputs.images = nodeOutput.images
|
||||||
|
if (nodeOutput.video) knownOutputs.video = nodeOutput.video
|
||||||
|
if (nodeOutput.gifs) knownOutputs.gifs = nodeOutput.gifs as ResultItem[]
|
||||||
|
if (nodeOutput['3d']) knownOutputs['3d'] = nodeOutput['3d'] as ResultItem[]
|
||||||
|
|
||||||
|
return Object.entries(knownOutputs).flatMap(([mediaType, outputs]) =>
|
||||||
|
outputs.map(
|
||||||
|
(output) => new ResultItemImpl({ ...output, mediaType, nodeId })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function allOutputs(item?: AssetItem): MaybeRef<ResultItemImpl[]> {
|
||||||
|
if (item?.id && outputsCache[item.id]) return outputsCache[item.id]
|
||||||
|
|
||||||
|
const user_metadata = getOutputAssetMetadata(item?.user_metadata)
|
||||||
|
if (!user_metadata) return []
|
||||||
|
if (
|
||||||
|
user_metadata.allOutputs &&
|
||||||
|
user_metadata.outputCount &&
|
||||||
|
user_metadata.outputCount < user_metadata.allOutputs.length
|
||||||
|
)
|
||||||
|
return user_metadata.allOutputs
|
||||||
|
|
||||||
|
const outputRef = useAsyncState(
|
||||||
|
getJobDetail(user_metadata.promptId).then((jobDetail) => {
|
||||||
|
if (!jobDetail?.outputs) return []
|
||||||
|
return Object.entries(jobDetail.outputs).flatMap(flattenNodeOutput)
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
).state
|
||||||
|
outputsCache[item!.id] = outputRef
|
||||||
|
return outputRef
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOutput = computed(() => {
|
const selectedOutput = computed(() => {
|
||||||
const [index, key] = selectedIndex.value
|
const [index, key] = selectedIndex.value
|
||||||
if (index < 0) return undefined
|
if (index < 0) return undefined
|
||||||
|
|
||||||
const output = allOutputs(outputs.media.value[index])[key]
|
return toValue(allOutputs(outputs.media.value[index]))[key]
|
||||||
if (output) return output
|
|
||||||
|
|
||||||
return allOutputs(outputs.media.value[0])[0]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch([selectedIndex, selectedOutput], doEmit)
|
||||||
watch(
|
watch(
|
||||||
() => outputs.media.value,
|
() => outputs.media.value,
|
||||||
(newAssets, oldAssets) => {
|
(newAssets, oldAssets) => {
|
||||||
if (newAssets.length === oldAssets.length || oldAssets.length === 0) return
|
if (newAssets.length === oldAssets.length || oldAssets.length === 0) return
|
||||||
if (selectedIndex.value[0] <= 0) {
|
if (selectedIndex.value[0] <= 0) {
|
||||||
//force update
|
|
||||||
selectedIndex.value = [0, 0]
|
selectedIndex.value = [0, 0]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -120,8 +176,7 @@ function gotoNextOutput() {
|
|||||||
selectedIndex.value = [0, 0]
|
selectedIndex.value = [0, 0]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const currentItem = outputs.media.value[index]
|
if (key + 1 < outputCount(outputs.media.value[index])) {
|
||||||
if (allOutputs(currentItem)[key + 1]) {
|
|
||||||
selectedIndex.value = [index, key + 1]
|
selectedIndex.value = [index, key + 1]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,8 +194,8 @@ function gotoPreviousOutput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const currentItem = outputs.media.value[index - 1]
|
const len = outputCount(outputs.media.value[index - 1])
|
||||||
selectedIndex.value = [index - 1, allOutputs(currentItem).length - 1]
|
selectedIndex.value = [index - 1, len - 1]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,12 +301,24 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
|||||||
queueStore.runningTasks.length + queueStore.pendingTasks.length
|
queueStore.runningTasks.length + queueStore.pendingTasks.length
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute -bottom-1 w-full h-3 rounded-sm overflow-clip">
|
||||||
|
<div :class="progressBarContainerClass">
|
||||||
|
<div
|
||||||
|
:class="progressBarPrimaryClass"
|
||||||
|
:style="progressPercentStyle(totalPercent)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
:class="progressBarSecondaryClass"
|
||||||
|
:style="progressPercentStyle(currentNodePercent)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<template v-for="(item, index) in outputs.media.value" :key="index">
|
<template v-for="(item, index) in outputs.media.value" :key="index">
|
||||||
<div
|
<div
|
||||||
class="border-border-subtle not-md:border-l md:border-t first:border-none not-md:h-21 md:w-full m-3"
|
class="border-border-subtle not-md:border-l md:border-t first:border-none not-md:h-21 md:w-full m-3"
|
||||||
/>
|
/>
|
||||||
<template v-for="(output, key) in allOutputs(item)" :key>
|
<template v-for="(output, key) in toValue(allOutputs(item))" :key>
|
||||||
<img
|
<img
|
||||||
v-if="getMediaType(output) === 'images'"
|
v-if="getMediaType(output) === 'images'"
|
||||||
:class="
|
:class="
|
||||||
@@ -262,6 +329,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
|||||||
'border-2'
|
'border-2'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:data-output-index="index"
|
||||||
:src="output.url"
|
:src="output.url"
|
||||||
@click="selectedIndex = [index, key]"
|
@click="selectedIndex = [index, key]"
|
||||||
/>
|
/>
|
||||||
@@ -275,6 +343,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
|||||||
'border-2'
|
'border-2'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:data-output-index="index"
|
||||||
@click="selectedIndex = [index, key]"
|
@click="selectedIndex = [index, key]"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
|
|||||||
Reference in New Issue
Block a user