Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements to the queue image feed #646

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 61 additions & 11 deletions src/components/common/ComfyImage.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
<!-- A image with placeholder fallback on error -->
<template>
<img
:src="src"
@error="handleImageError"
:class="[{ 'broken-image': imageBroken }, ...classArray]"
/>
<span
v-if="!imageBroken"
class="comfy-image-wrap"
:class="[{ contain: contain }]"
>
<img
v-if="contain"
:src="src"
@error="handleImageError"
:data-test="src"
class="comfy-image-blur"
huchenlei marked this conversation as resolved.
Show resolved Hide resolved
:style="{ 'background-image': `url(${src})` }"
/>
<img
:src="src"
@error="handleImageError"
class="comfy-image-main"
:class="[...classArray]"
/>
</span>
<div v-if="imageBroken" class="broken-image-placeholder">
<i class="pi pi-image"></i>
<span>{{ $t('imageFailedToLoad') }}</span>
Expand All @@ -14,10 +29,16 @@
<script setup lang="ts">
import { computed, ref } from 'vue'

const props = defineProps<{
src: string
class?: string | string[] | object
}>()
const props = withDefaults(
defineProps<{
src: string
class?: string | string[] | object
contain: boolean
}>(),
{
contain: false
}
)

const imageBroken = ref(false)
const handleImageError = (e: Event) => {
Expand All @@ -37,8 +58,37 @@ const classArray = computed(() => {
</script>

<style scoped>
.broken-image {
display: none;
.comfy-image-wrap {
display: contents;
}

.comfy-image-blur {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}

.comfy-image-main {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
z-index: 1;
}

.contain .comfy-image-wrap {
position: relative;
width: 100%;
height: 100%;
}

.contain .comfy-image-main {
object-fit: contain;
backdrop-filter: blur(10px);
position: absolute;
}

.broken-image-placeholder {
Expand Down
42 changes: 40 additions & 2 deletions src/components/sidebar/tabs/QueueSidebarTab.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
<template>
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
<template #tool-buttons>
<Button
:icon="
imageFit === 'cover'
? 'pi pi-arrow-down-left-and-arrow-up-right-to-center'
: 'pi pi-arrow-up-right-and-arrow-down-left-from-center'
"
text
severity="secondary"
@click="toggleImageFit"
class="toggle-expanded-button"
v-tooltip="$t(`sideToolbar.queueTab.${imageFit}ImagePreview`)"
/>
<Button
v-if="isInFolderView"
icon="pi pi-arrow-left"
Expand All @@ -12,7 +24,7 @@
/>
<template v-else>
<Button
:icon="isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
:icon="isExpanded ? 'pi pi-images' : 'pi pi-image'"
text
severity="secondary"
@click="toggleExpanded"
Expand Down Expand Up @@ -47,6 +59,11 @@
</div>
<div ref="loadMoreTrigger" style="height: 1px" />
</div>
<div v-else-if="queueStore.isLoading">
<ProgressSpinner
style="width: 50px; left: 50%; transform: translateX(-50%)"
/>
</div>
<div v-else>
<NoResultsPlaceholder
icon="pi pi-info-circle"
Expand Down Expand Up @@ -74,16 +91,22 @@ import Button from 'primevue/button'
import ConfirmPopup from 'primevue/confirmpopup'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import ProgressSpinner from 'primevue/progressspinner'
import TaskItem from './queue/TaskItem.vue'
import ResultGallery from './queue/ResultGallery.vue'
import SidebarTabTemplate from './SidebarTabTemplate.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import { api } from '@/scripts/api'
import { ComfyNode } from '@/types/comfyWorkflow'
import { useSettingStore } from '@/stores/settingStore'
import { app } from '@/scripts/app'

const IMAGE_FIT = 'Comfy.Queue.ImageFit'
const confirm = useConfirm()
const toast = useToast()
const queueStore = useQueueStore()
const settingStore = useSettingStore()
const { t } = useI18n()

// Expanded view: show all outputs in a flat list.
Expand All @@ -95,6 +118,7 @@ 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 ITEMS_PER_PAGE = 8
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
Expand Down Expand Up @@ -204,6 +228,7 @@ const onStatus = async () => {

const menu = ref(null)
const menuTargetTask = ref<TaskItemImpl | null>(null)
const menuTargetNode = ref<ComfyNode | null>(null)
const menuItems = computed<MenuItem[]>(() => [
{
label: t('delete'),
Expand All @@ -215,17 +240,26 @@ const menuItems = computed<MenuItem[]>(() => [
label: t('loadWorkflow'),
icon: 'pi pi-file-export',
command: () => menuTargetTask.value?.loadWorkflow()
},
{
label: t('goToNode'),
icon: 'pi pi-arrow-circle-right',
command: () => app.goToNode(menuTargetNode.value?.id),
visible: !!menuTargetNode.value
}
])

const handleContextMenu = ({
task,
event
event,
node
}: {
task: TaskItemImpl
event: Event
node?: ComfyNode
}) => {
menuTargetTask.value = task
menuTargetNode.value = node
menu.value?.show(event)
}

Expand All @@ -245,6 +279,10 @@ const exitFolderView = () => {
updateVisibleTasks()
}

const toggleImageFit = () => {
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
}

onMounted(() => {
api.addEventListener('status', onStatus)
queueStore.update()
Expand Down
7 changes: 6 additions & 1 deletion src/components/sidebar/tabs/queue/ResultGallery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
:showThumbnails="false"
>
<template #item="{ item }">
<ComfyImage :key="item.url" :src="item.url" class="galleria-image" />
<ComfyImage
:key="item.url"
:src="item.url"
:contain="false"
class="galleria-image"
/>
</template>
</Galleria>
</template>
Expand Down
21 changes: 12 additions & 9 deletions src/components/sidebar/tabs/queue/ResultItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
<template
v-if="result.mediaType === 'images' || result.mediaType === 'gifs'"
>
<ComfyImage
:src="result.url"
class="task-output-image"
:contain="imageFit === 'contain'"
/>
<div class="image-preview-mask">
<Button
icon="pi pi-eye"
Expand All @@ -11,7 +16,6 @@
rounded
/>
</div>
<ComfyImage :src="result.url" class="task-output-image" />
</template>
<!-- TODO: handle more media types -->
<div v-else class="task-result-preview">
Expand All @@ -25,7 +29,8 @@
import { ResultItemImpl } from '@/stores/queueStore'
import ComfyImage from '@/components/common/ComfyImage.vue'
import Button from 'primevue/button'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useSettingStore } from '@/stores/settingStore'

const props = defineProps<{
result: ResultItemImpl
Expand All @@ -36,6 +41,10 @@ const emit = defineEmits<{
}>()

const resultContainer = ref<HTMLElement | null>(null)
const settingStore = useSettingStore()
const imageFit = computed<string>(() =>
settingStore.get('Comfy.Queue.ImageFit')
)

onMounted(() => {
if (props.result.mediaType === 'images') {
Expand All @@ -58,13 +67,6 @@ onMounted(() => {
align-items: center;
}

:deep(.task-output-image) {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}

.image-preview-mask {
position: absolute;
left: 50%;
Expand All @@ -75,6 +77,7 @@ onMounted(() => {
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1;
}

.result-container:hover .image-preview-mask {
Expand Down
21 changes: 18 additions & 3 deletions src/components/sidebar/tabs/queue/TaskItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
<div class="task-item-details">
<div class="tag-wrapper status-tag-group">
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
{{ node?.type }} (#{{ node?.id }})
<Button
class="task-node-link"
:label="`${node?.type} (#${node?.id})`"
link
size="small"
@click="app.goToNode(node?.id)"
/>
</Tag>
<Tag :severity="taskTagSeverity(task.displayStatus)">
<span v-html="taskStatusText(task.displayStatus)"></span>
Expand Down Expand Up @@ -59,6 +65,7 @@ import Tag from 'primevue/tag'
import ResultItem from './ResultItem.vue'
import { TaskItemDisplayStatus, type TaskItemImpl } from '@/stores/queueStore'
import { ComfyNode } from '@/types/comfyWorkflow'
import { app } from '@/scripts/app'

const props = defineProps<{
task: TaskItemImpl
Expand All @@ -77,13 +84,16 @@ const node: ComfyNode | null = flatOutputs.length
: null

const emit = defineEmits<{
(e: 'contextmenu', value: { task: TaskItemImpl; event: MouseEvent }): void
(
e: 'contextmenu',
value: { task: TaskItemImpl; event: MouseEvent; node?: ComfyNode }
): void
(e: 'preview', value: TaskItemImpl): void
(e: 'task-output-length-clicked', value: TaskItemImpl): void
}>()

const handleContextMenu = (e: MouseEvent) => {
emit('contextmenu', { task: props.task, event: e })
emit('contextmenu', { task: props.task, event: e, node })
}

const handlePreview = () => {
Expand Down Expand Up @@ -164,6 +174,11 @@ const formatTime = (time?: number) => {
justify-content: space-between;
align-items: center;
width: 100%;
z-index: 1;
}

.task-node-link {
padding: 2px;
}

/* In dark mode, transparent background color for tags is not ideal for tags that
Expand Down
5 changes: 4 additions & 1 deletion src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const messages = {
experimental: 'BETA',
deprecated: 'DEPR',
loadWorkflow: 'Load Workflow',
goToNode: 'Go to Node',
settings: 'Settings',
searchSettings: 'Search Settings',
searchNodes: 'Search Nodes',
Expand All @@ -45,7 +46,9 @@ const messages = {
},
queueTab: {
showFlatList: 'Show Flat List',
backToAllTasks: 'Back to All Tasks'
backToAllTasks: 'Back to All Tasks',
containImagePreview: 'Fill Image Preview',
coverImagePreview: 'Fit Image Preview'
}
}
},
Expand Down
Loading
Loading