Skip to content

Commit e6d2965

Browse files
authored
Queue media preview (#449)
* output url * Basic image previews * Split out task item component * Move task actions to context menu * simplify * Move spinner * Lift context menu to tab scope * Better tag * Fix placeholder style * nit * Correctly handle cancelled * nit * Split out result item as separate component * nit * Fix center crop * nit * Simplify task item * Flat list * Show prompt id * Make image draggable * Disable preview for dragging * Fix key * Correctly handle task in expanded view * Add preview
1 parent 9e3dffd commit e6d2965

File tree

8 files changed

+397
-125
lines changed

8 files changed

+397
-125
lines changed
Lines changed: 82 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,82 @@
11
<template>
2-
<DataTable
3-
v-if="tasks.length > 0"
4-
:value="tasks"
5-
dataKey="promptId"
6-
class="queue-table"
7-
>
8-
<Column header="STATUS">
9-
<template #body="{ data }">
10-
<Tag :severity="taskTagSeverity(data.displayStatus)">
11-
{{ data.displayStatus.toUpperCase() }}
12-
</Tag>
13-
</template>
14-
</Column>
15-
<Column header="TIME" :pt="{ root: { class: 'queue-time-cell' } }">
16-
<template #body="{ data }">
17-
<div v-if="data.isHistory" class="queue-time-cell-content">
18-
{{ formatTime(data.executionTimeInSeconds) }}
19-
</div>
20-
<div v-else-if="data.isRunning" class="queue-time-cell-content">
21-
<i class="pi pi-spin pi-spinner"></i>
22-
</div>
23-
<div v-else class="queue-time-cell-content">...</div>
24-
</template>
25-
</Column>
26-
<Column
27-
:pt="{
28-
headerCell: {
29-
class: 'queue-tool-header-cell'
30-
},
31-
bodyCell: {
32-
class: 'queue-tool-body-cell'
33-
}
34-
}"
35-
>
36-
<template #header>
37-
<Toast />
38-
<ConfirmPopup />
39-
<Button
40-
icon="pi pi-trash"
41-
text
42-
severity="primary"
43-
@click="confirmRemoveAll($event)"
2+
<SideBarTabTemplate :title="$t('sideToolbar.queue')">
3+
<template #tool-buttons>
4+
<Button
5+
:icon="isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
6+
text
7+
severity="secondary"
8+
@click="isExpanded = !isExpanded"
9+
class="toggle-expanded-button"
10+
v-tooltip="$t('sideToolbar.queueTab.showFlatList')"
11+
/>
12+
<Button
13+
icon="pi pi-trash"
14+
text
15+
severity="primary"
16+
@click="confirmRemoveAll($event)"
17+
class="clear-all-button"
18+
/>
19+
</template>
20+
<template #body>
21+
<div v-if="tasks.length > 0" class="queue-grid">
22+
<TaskItem
23+
v-for="task in tasks"
24+
:key="task.key"
25+
:task="task"
26+
:isFlatTask="isExpanded"
27+
@contextmenu="handleContextMenu"
4428
/>
45-
</template>
46-
<template #body="{ data }">
47-
<Button
48-
icon="pi pi-file-export"
49-
text
50-
severity="primary"
51-
@click="data.loadWorkflow()"
29+
</div>
30+
<div v-else>
31+
<NoResultsPlaceholder
32+
icon="pi pi-info-circle"
33+
:title="$t('noTasksFound')"
34+
:message="$t('noTasksFoundMessage')"
5235
/>
53-
<Button
54-
icon="pi pi-times"
55-
text
56-
severity="secondary"
57-
@click="removeTask(data)"
58-
/>
59-
</template>
60-
</Column>
61-
</DataTable>
62-
<div v-else>
63-
<NoResultsPlaceholder
64-
icon="pi pi-info-circle"
65-
:title="$t('noTasksFound')"
66-
:message="$t('noTasksFoundMessage')"
67-
/>
68-
</div>
36+
</div>
37+
</template>
38+
</SideBarTabTemplate>
39+
<Toast />
40+
<ConfirmPopup />
41+
<ContextMenu ref="menu" :model="menuItems" />
6942
</template>
7043

7144
<script setup lang="ts">
72-
import DataTable from 'primevue/datatable'
73-
import Column from 'primevue/column'
74-
import Tag from 'primevue/tag'
7545
import Button from 'primevue/button'
7646
import ConfirmPopup from 'primevue/confirmpopup'
7747
import Toast from 'primevue/toast'
48+
import ContextMenu from 'primevue/contextmenu'
49+
import TaskItem from './queue/TaskItem.vue'
50+
import SideBarTabTemplate from './SidebarTabTemplate.vue'
7851
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
7952
import { useConfirm } from 'primevue/useconfirm'
8053
import { useToast } from 'primevue/usetoast'
81-
import {
82-
TaskItemDisplayStatus,
83-
TaskItemImpl,
84-
useQueueStore
85-
} from '@/stores/queueStore'
86-
import { computed, onMounted, onUnmounted } from 'vue'
54+
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
55+
import { computed, onMounted, onUnmounted, ref } from 'vue'
56+
import { useI18n } from 'vue-i18n'
57+
import { type MenuItem } from 'primevue/menuitem'
8758
import { api } from '@/scripts/api'
8859
8960
const confirm = useConfirm()
9061
const toast = useToast()
9162
const queueStore = useQueueStore()
63+
const { t } = useI18n()
64+
65+
const tasks = computed(() =>
66+
isExpanded.value ? queueStore.flatTasks : queueStore.tasks
67+
)
9268
93-
const tasks = computed(() => queueStore.tasks)
94-
const taskTagSeverity = (status: TaskItemDisplayStatus) => {
95-
switch (status) {
96-
case TaskItemDisplayStatus.Pending:
97-
return 'secondary'
98-
case TaskItemDisplayStatus.Running:
99-
return 'info'
100-
case TaskItemDisplayStatus.Completed:
101-
return 'success'
102-
case TaskItemDisplayStatus.Failed:
103-
return 'danger'
104-
case TaskItemDisplayStatus.Cancelled:
105-
return 'warning'
106-
}
107-
}
108-
const formatTime = (time?: number) => {
109-
if (time === undefined) {
110-
return ''
111-
}
112-
return `${time.toFixed(2)}s`
113-
}
11469
const removeTask = (task: TaskItemImpl) => {
11570
if (task.isRunning) {
11671
api.interrupt()
11772
}
11873
queueStore.delete(task)
11974
}
75+
12076
const removeAllTasks = async () => {
12177
await queueStore.clear()
12278
}
79+
12380
const confirmRemoveAll = (event) => {
12481
confirm.require({
12582
target: event.currentTarget,
@@ -147,30 +104,45 @@ const confirmRemoveAll = (event) => {
147104
}
148105
149106
const onStatus = () => queueStore.update()
107+
108+
const menu = ref(null)
109+
const menuTargetTask = ref<TaskItemImpl | null>(null)
110+
const menuItems = computed<MenuItem[]>(() => {
111+
return [
112+
{
113+
label: t('delete'),
114+
icon: 'pi pi-trash',
115+
command: () => removeTask(menuTargetTask.value!)
116+
},
117+
{
118+
label: t('loadWorkflow'),
119+
icon: 'pi pi-file-export',
120+
command: () => menuTargetTask.value?.loadWorkflow()
121+
}
122+
]
123+
})
124+
const handleContextMenu = ({ task, event }) => {
125+
menuTargetTask.value = task
126+
menu.value.show(event)
127+
}
128+
150129
onMounted(() => {
151130
api.addEventListener('status', onStatus)
152-
153131
queueStore.update()
154132
})
133+
155134
onUnmounted(() => {
156135
api.removeEventListener('status', onStatus)
157136
})
158-
</script>
159137
160-
<style>
161-
.queue-tool-header-cell {
162-
display: flex;
163-
justify-content: flex-end;
164-
}
165-
166-
.queue-tool-body-cell {
167-
display: table-cell;
168-
text-align: right !important;
169-
}
170-
</style>
138+
const isExpanded = ref(false)
139+
</script>
171140

172141
<style scoped>
173-
.queue-time-cell-content {
174-
width: fit-content;
142+
.queue-grid {
143+
display: grid;
144+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
145+
padding: 0.5rem;
146+
gap: 0.5rem;
175147
}
176148
</style>

src/components/sidebar/tabs/SidebarTabTemplate.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const props = defineProps({
4242
border-top: none;
4343
border-radius: 0;
4444
padding: 0.25rem 1rem;
45+
min-height: 2.5rem;
4546
}
4647
4748
.comfy-vue-side-bar-header-span {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<template>
2+
<div class="result-container" ref="resultContainer">
3+
<Image
4+
v-if="result.mediaType === 'images'"
5+
:src="result.url"
6+
alt="Task Output"
7+
width="100%"
8+
height="100%"
9+
preview
10+
:pt="{ previewMask: { class: 'image-preview-mask' } }"
11+
/>
12+
<!-- TODO: handle more media types -->
13+
<div v-else class="task-result-preview">
14+
<i class="pi pi-file"></i>
15+
<span>{{ result.mediaType }}</span>
16+
</div>
17+
</div>
18+
</template>
19+
20+
<script setup lang="ts">
21+
import { ResultItemImpl } from '@/stores/queueStore'
22+
import Image from 'primevue/image'
23+
import { onMounted, ref } from 'vue'
24+
25+
const props = defineProps<{
26+
result: ResultItemImpl
27+
}>()
28+
29+
const resultContainer = ref<HTMLElement | null>(null)
30+
31+
onMounted(() => {
32+
if (props.result.mediaType === 'images') {
33+
resultContainer.value.querySelectorAll('img').forEach((img) => {
34+
img.draggable = true
35+
})
36+
}
37+
})
38+
</script>
39+
40+
<style scoped>
41+
.result-container {
42+
aspect-ratio: 1 / 1;
43+
overflow: hidden;
44+
}
45+
46+
:deep(img) {
47+
position: absolute;
48+
top: 50%;
49+
left: 50%;
50+
transform: translate(-50%, -50%);
51+
width: 100%;
52+
height: 100%;
53+
object-fit: cover;
54+
}
55+
56+
.p-image-preview {
57+
position: static;
58+
display: contents;
59+
}
60+
61+
:deep(.image-preview-mask) {
62+
position: absolute;
63+
left: 50%;
64+
top: 50%;
65+
transform: translate(-50%, -50%);
66+
width: auto;
67+
height: auto;
68+
display: flex;
69+
align-items: center;
70+
justify-content: center;
71+
opacity: 0;
72+
padding: 10px;
73+
cursor: pointer;
74+
background: rgba(0, 0, 0, 0.5);
75+
color: var(--p-image-preview-mask-color);
76+
transition:
77+
opacity var(--p-image-transition-duration),
78+
background var(--p-image-transition-duration);
79+
border-radius: 50%;
80+
}
81+
</style>

0 commit comments

Comments
 (0)