mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Filter cached/canceled results (#1586)
* Filter cached/canceled results * Highlight if on * Update setting
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
|
||||
<template #tool-buttons>
|
||||
<Popover ref="outputFilterPopup">
|
||||
<OutputFilters />
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
icon="pi pi-filter"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="outputFilterPopup.toggle($event)"
|
||||
v-tooltip="$t(`sideToolbar.queueTab.filter`)"
|
||||
:class="{ 'text-yellow-500': anyFilter }"
|
||||
/>
|
||||
<Button
|
||||
:icon="
|
||||
imageFit === 'cover'
|
||||
@@ -99,6 +111,7 @@ import Button from 'primevue/button'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Popover from 'primevue/popover'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TaskItem from './queue/TaskItem.vue'
|
||||
import ResultGallery from './queue/ResultGallery.vue'
|
||||
@@ -111,7 +124,9 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const IMAGE_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FLAT = 'Comfy.Queue.ShowFlatList'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -120,7 +135,7 @@ const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
const isExpanded = ref(false)
|
||||
const isExpanded = computed<boolean>(() => settingStore.get(SETTING_FLAT))
|
||||
const visibleTasks = ref<TaskItemImpl[]>([])
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||
@@ -128,7 +143,23 @@ const galleryActiveIndex = ref(-1)
|
||||
// Folder view: only show outputs from a single selected task.
|
||||
const folderTask = ref<TaskItemImpl | null>(null)
|
||||
const isInFolderView = computed(() => folderTask.value !== null)
|
||||
const imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
|
||||
const imageFit = computed<string>(() => settingStore.get(SETTING_FIT))
|
||||
const hideCached = computed<boolean>(
|
||||
() => settingStore.get(SETTING_FILTER)?.hideCached
|
||||
)
|
||||
const hideCanceled = computed<boolean>(
|
||||
() => settingStore.get(SETTING_FILTER)?.hideCanceled
|
||||
)
|
||||
const anyFilter = computed(() => hideCanceled.value || hideCached.value)
|
||||
|
||||
watch(hideCached, () => {
|
||||
updateVisibleTasks()
|
||||
})
|
||||
watch(hideCanceled, () => {
|
||||
updateVisibleTasks()
|
||||
})
|
||||
|
||||
const outputFilterPopup = ref(null)
|
||||
|
||||
const ITEMS_PER_PAGE = 8
|
||||
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
||||
@@ -149,9 +180,31 @@ const allGalleryItems = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
const filterTasks = (tasks: TaskItemImpl[]) =>
|
||||
tasks
|
||||
.filter((t) => {
|
||||
if (
|
||||
hideCanceled.value &&
|
||||
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
hideCached.value &&
|
||||
t.flatOutputs?.length &&
|
||||
t.flatOutputs.every((o) => o.cached)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.slice(0, ITEMS_PER_PAGE)
|
||||
|
||||
const loadMoreItems = () => {
|
||||
const currentLength = visibleTasks.value.length
|
||||
const newTasks = allTasks.value.slice(
|
||||
const newTasks = filterTasks(allTasks.value).slice(
|
||||
currentLength,
|
||||
currentLength + ITEMS_PER_PAGE
|
||||
)
|
||||
@@ -186,11 +239,11 @@ useResizeObserver(scrollContainer, () => {
|
||||
})
|
||||
|
||||
const updateVisibleTasks = () => {
|
||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
||||
visibleTasks.value = filterTasks(allTasks.value)
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
settingStore.set(SETTING_FLAT, !isExpanded.value)
|
||||
updateVisibleTasks()
|
||||
}
|
||||
|
||||
@@ -291,7 +344,10 @@ const exitFolderView = () => {
|
||||
}
|
||||
|
||||
const toggleImageFit = () => {
|
||||
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
|
||||
settingStore.set(
|
||||
SETTING_FIT,
|
||||
imageFit.value === 'cover' ? 'contain' : 'cover'
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
{{ $t('sideToolbar.queueTab.filters.hideCached') }}
|
||||
<ToggleSwitch v-model="hideCached" />
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
{{ $t('sideToolbar.queueTab.filters.hideCanceled') }}
|
||||
<ToggleSwitch v-model="hideCanceled" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const filter = settingStore.get(SETTING_FILTER) ?? {}
|
||||
|
||||
const createCompute = (k: string) =>
|
||||
computed({
|
||||
get() {
|
||||
return filter[k]
|
||||
},
|
||||
set(value) {
|
||||
filter[k] = value
|
||||
settingStore.set(SETTING_FILTER, filter)
|
||||
}
|
||||
})
|
||||
|
||||
const hideCached = createCompute('hideCached')
|
||||
const hideCanceled = createCompute('hideCanceled')
|
||||
</script>
|
||||
@@ -31,16 +31,20 @@
|
||||
|
||||
<div class="task-item-details">
|
||||
<div class="tag-wrapper status-tag-group">
|
||||
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
|
||||
<Tag v-if="isFlatTask && task.isHistory && node" class="node-name-tag">
|
||||
<Button
|
||||
class="task-node-link"
|
||||
:label="`${node?.type} (#${node?.id})`"
|
||||
:label="`${node.type} (#${node.id})`"
|
||||
link
|
||||
size="small"
|
||||
@click="app.goToNode(node?.id)"
|
||||
/>
|
||||
</Tag>
|
||||
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
||||
<Tag
|
||||
:severity="taskTagSeverity(task.displayStatus)"
|
||||
class="task-duration relative"
|
||||
>
|
||||
<i v-if="isCachedResult" class="pi pi-server task-cached-icon"></i>
|
||||
<span v-html="taskStatusText(task.displayStatus)"></span>
|
||||
<span v-if="task.isHistory" class="task-time">
|
||||
{{ formatTime(task.executionTimeInSeconds) }}
|
||||
@@ -90,6 +94,7 @@ const node: ComfyNode | null =
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
const isCachedResult = props.isFlatTask && coverResult?.cached
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
@@ -142,7 +147,7 @@ const taskStatusText = (status: TaskItemDisplayStatus) => {
|
||||
case TaskItemDisplayStatus.Running:
|
||||
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
|
||||
case TaskItemDisplayStatus.Completed:
|
||||
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
||||
return `<i class="pi pi-check${isCachedResult ? ' cached' : ''}" style="font-weight: bold"></i>`
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return 'Failed'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
@@ -226,4 +231,15 @@ are floating on top of images. */
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.task-cached-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.pi-check.cached) {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -126,7 +126,12 @@ const messages = {
|
||||
backToAllTasks: 'Back to All Tasks',
|
||||
containImagePreview: 'Fill Image Preview',
|
||||
coverImagePreview: 'Fit Image Preview',
|
||||
clearPendingTasks: 'Clear Pending Tasks'
|
||||
clearPendingTasks: 'Clear Pending Tasks',
|
||||
filter: 'Filter Outputs',
|
||||
filters: {
|
||||
hideCached: 'Hide Cached',
|
||||
hideCanceled: 'Hide Canceled'
|
||||
}
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
|
||||
@@ -198,6 +198,25 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'hidden',
|
||||
defaultValue: 'cover'
|
||||
},
|
||||
// Hidden setting used by the queue for if the results should be grouped by prompt
|
||||
{
|
||||
id: 'Comfy.Queue.ShowFlatList',
|
||||
name: 'Queue show flat list',
|
||||
type: 'hidden',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// Hidden setting used by the queue to filter certain results
|
||||
{
|
||||
id: 'Comfy.Queue.Filter',
|
||||
name: 'Queue output filters',
|
||||
type: 'hidden',
|
||||
defaultValue: {
|
||||
hideCanceled: false,
|
||||
hideCached: false
|
||||
},
|
||||
versionAdded: '1.4.3'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupSelectedNodes.Padding',
|
||||
category: ['LiteGraph', 'Group', 'Padding'],
|
||||
|
||||
@@ -30,6 +30,7 @@ export class ResultItemImpl {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
cached: boolean
|
||||
|
||||
nodeId: NodeId
|
||||
// 'audio' | 'images' | ...
|
||||
@@ -43,6 +44,7 @@ export class ResultItemImpl {
|
||||
this.filename = obj.filename ?? ''
|
||||
this.subfolder = obj.subfolder ?? ''
|
||||
this.type = obj.type ?? ''
|
||||
this.cached = obj.cached
|
||||
|
||||
this.nodeId = obj.nodeId
|
||||
this.mediaType = obj.mediaType
|
||||
@@ -149,6 +151,13 @@ export class TaskItemImpl {
|
||||
if (!this.outputs) {
|
||||
return []
|
||||
}
|
||||
|
||||
const cachedEntries = new Set(
|
||||
this.status?.messages?.find((message) => {
|
||||
return message[0] === 'execution_cached'
|
||||
})?.[1].nodes
|
||||
)
|
||||
|
||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as ResultItem[]).map(
|
||||
@@ -156,7 +165,8 @@ export class TaskItemImpl {
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
mediaType,
|
||||
cached: cachedEntries.has(nodeId)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -454,6 +454,11 @@ const zNodeBadgeMode = z.enum(
|
||||
Object.values(NodeBadgeMode) as [string, ...string[]]
|
||||
)
|
||||
|
||||
const zQueueFilter = z.object({
|
||||
hideCached: z.boolean(),
|
||||
hideCanceled: z.boolean()
|
||||
})
|
||||
|
||||
const zSettings = z.record(z.any()).and(
|
||||
z
|
||||
.object({
|
||||
@@ -506,6 +511,8 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.Validation.Workflows': z.boolean(),
|
||||
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
|
||||
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
||||
'Comfy.Queue.ShowFlatList': z.boolean(),
|
||||
'Comfy.Queue.Filter': zQueueFilter.passthrough(),
|
||||
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
|
||||
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
||||
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user