mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 09:57:33 +00:00
Compare commits
2 Commits
main
...
feat/queue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fba5f8914 | ||
|
|
72124a9fb0 |
@@ -4,7 +4,7 @@
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div ref="queueAreaRef" class="mx-1 flex flex-col items-end gap-1">
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke px-2 shadow-interface"
|
||||
>
|
||||
@@ -40,12 +40,16 @@
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
</div>
|
||||
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
||||
<QueueProgressOverlay
|
||||
v-model:expanded="isQueueOverlayExpanded"
|
||||
:external-hovered="isQueueAreaHovered"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useMouse } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -68,6 +72,20 @@ const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
const { t } = useI18n()
|
||||
const isQueueOverlayExpanded = ref(false)
|
||||
|
||||
// Track hover over the rectangular bounding box of the queue area
|
||||
// Using mouse position + element bounds instead of mouseenter/mouseleave
|
||||
// because QueueProgressOverlay has pointer-events-none on its wrapper
|
||||
const queueAreaRef = ref<HTMLElement | null>(null)
|
||||
const { x: mouseX, y: mouseY } = useMouse()
|
||||
const { left, top, right, bottom } = useElementBounding(queueAreaRef)
|
||||
const isQueueAreaHovered = computed(
|
||||
() =>
|
||||
mouseX.value >= left.value &&
|
||||
mouseX.value <= right.value &&
|
||||
mouseY.value >= top.value &&
|
||||
mouseY.value <= bottom.value
|
||||
)
|
||||
const queueStore = useQueueStore()
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
v-tooltip.top="cancelJobTooltip"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
||||
@click="$emit('interruptAll')"
|
||||
>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div
|
||||
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
|
||||
:class="containerClass"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
@mouseenter="isHoveredInternal = true"
|
||||
@mouseleave="isHoveredInternal = false"
|
||||
>
|
||||
<!-- Expanded state -->
|
||||
<QueueOverlayExpanded
|
||||
@@ -87,6 +87,8 @@ type OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'
|
||||
|
||||
const props = defineProps<{
|
||||
expanded?: boolean
|
||||
/** External hover state from parent container */
|
||||
externalHovered?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -109,7 +111,10 @@ const {
|
||||
totalProgressStyle,
|
||||
currentNodeProgressStyle
|
||||
} = useQueueProgress()
|
||||
const isHovered = ref(false)
|
||||
const isHoveredInternal = ref(false)
|
||||
const isHovered = computed(
|
||||
() => isHoveredInternal.value || props.externalHovered
|
||||
)
|
||||
const internalExpanded = ref(false)
|
||||
const isExpanded = computed({
|
||||
get: () =>
|
||||
|
||||
@@ -82,7 +82,16 @@
|
||||
:src="iconImageUrl"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
<i v-else :class="[iconClass, 'size-4']" />
|
||||
<i
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
iconClass,
|
||||
'size-4',
|
||||
props.state === 'pending' && 'animate-spin'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,6 +102,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
TODO: Refactor action buttons to use a declarative config system.
|
||||
|
||||
Instead of hardcoding button visibility logic in the template, define an array of
|
||||
action button configs with properties like:
|
||||
- icon, label, action, tooltip
|
||||
- visibleStates: JobState[] (which job states show this button)
|
||||
- alwaysVisible: boolean (show without hover)
|
||||
- destructive: boolean (use destructive styling)
|
||||
|
||||
Then render buttons in two groups:
|
||||
1. Always-visible buttons (outside Transition)
|
||||
2. Hover-only buttons (inside Transition)
|
||||
|
||||
This would eliminate the current duplication where the cancel button exists
|
||||
both outside (for running) and inside (for pending) the Transition.
|
||||
-->
|
||||
<div class="relative z-[1] flex items-center gap-2 text-text-secondary">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
@@ -113,18 +139,22 @@
|
||||
v-tooltip.top="deleteTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
|
||||
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
|
||||
:aria-label="t('g.delete')"
|
||||
@click.stop="emit('delete')"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="props.state !== 'completed' && computedShowClear"
|
||||
v-else-if="
|
||||
props.state !== 'completed' &&
|
||||
props.state !== 'running' &&
|
||||
computedShowClear
|
||||
"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
|
||||
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="emit('cancel')"
|
||||
>
|
||||
@@ -143,17 +173,33 @@
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
class="size-6 transform gap-1 rounded bg-modal-card-button-surface text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
:aria-label="t('g.more')"
|
||||
@click.stop="emit('menu', $event)"
|
||||
>
|
||||
<i class="icon-[lucide--more-horizontal] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div v-else key="secondary" class="pr-2">
|
||||
<div
|
||||
v-else-if="props.state !== 'running'"
|
||||
key="secondary"
|
||||
class="pr-2"
|
||||
>
|
||||
<slot name="secondary">{{ props.rightText }}</slot>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- Running job cancel button - always visible -->
|
||||
<IconButton
|
||||
v-if="props.state === 'running' && computedShowClear"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="emit('cancel')"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,6 +216,7 @@ import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
||||
@@ -24,7 +24,7 @@ type JobDisplay = {
|
||||
export const iconForJobState = (state: JobState): string => {
|
||||
switch (state) {
|
||||
case 'pending':
|
||||
return 'icon-[lucide--clock]'
|
||||
return 'icon-[lucide--loader-circle]'
|
||||
case 'initialization':
|
||||
return 'icon-[lucide--server-crash]'
|
||||
case 'running':
|
||||
|
||||
Reference in New Issue
Block a user