mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Add ... context menu to list view
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
:preview-alt="job.title"
|
||||
:icon-name="job.iconName"
|
||||
:icon-class="getJobIconClass(job)"
|
||||
:icon-wrapper-class="getJobIconWrapperClass(job)"
|
||||
:primary-text="job.title"
|
||||
:secondary-text="job.meta"
|
||||
:progress-total-percent="job.progressTotalPercent"
|
||||
@@ -69,8 +68,22 @@
|
||||
:icon-name="getAssetIconName(item.asset)"
|
||||
:primary-text="getAssetPrimaryText(item.asset)"
|
||||
:secondary-text="getAssetSecondaryText(item.asset)"
|
||||
@mouseenter="onAssetEnter(item.asset.id)"
|
||||
@mouseleave="onAssetLeave(item.asset.id)"
|
||||
@contextmenu.prevent="handleAssetMenuClick($event, item.asset)"
|
||||
@click.stop="emit('select-asset', item.asset)"
|
||||
/>
|
||||
>
|
||||
<template v-if="hoveredAssetId === item.asset.id" #actions>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('g.moreOptions')"
|
||||
@click.stop="handleAssetMenuClick($event, item.asset)"
|
||||
>
|
||||
<i class="icon-[lucide--ellipsis] size-4" />
|
||||
</Button>
|
||||
</template>
|
||||
</AssetsListCard>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
</div>
|
||||
@@ -106,6 +119,7 @@ const { assets, isSelected } = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-asset', asset: AssetItem): void
|
||||
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
|
||||
(e: 'approach-end'): void
|
||||
}>()
|
||||
|
||||
@@ -113,6 +127,7 @@ const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
const { getJobActions, runJobAction } = useJobActions()
|
||||
const hoveredJobId = ref<string | null>(null)
|
||||
const hoveredAssetId = ref<string | null>(null)
|
||||
|
||||
type AssetListItem = { key: string; asset: AssetItem }
|
||||
|
||||
@@ -196,11 +211,14 @@ function onJobLeave(jobId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getJobIconWrapperClass(job: JobListItem): string | undefined {
|
||||
if (job.state === 'failed') {
|
||||
return 'bg-modal-card-placeholder-background'
|
||||
function onAssetEnter(assetId: string) {
|
||||
hoveredAssetId.value = assetId
|
||||
}
|
||||
|
||||
function onAssetLeave(assetId: string) {
|
||||
if (hoveredAssetId.value === assetId) {
|
||||
hoveredAssetId.value = null
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getJobIconClass(job: JobListItem): string | undefined {
|
||||
@@ -209,13 +227,15 @@ function getJobIconClass(job: JobListItem): string | undefined {
|
||||
if (!job.iconImageUrl && iconName === iconForJobState('pending')) {
|
||||
classes.push('animate-spin')
|
||||
}
|
||||
if (job.state === 'failed') {
|
||||
classes.push('text-destructive-background')
|
||||
}
|
||||
return classes.length ? classes.join(' ') : undefined
|
||||
}
|
||||
|
||||
function handleJobAction(action: JobAction, job: JobListItem) {
|
||||
void runJobAction(action, job)
|
||||
}
|
||||
|
||||
function handleAssetMenuClick(event: MouseEvent, asset: AssetItem) {
|
||||
event.stopPropagation()
|
||||
emit('context-menu', event, asset)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
:assets="displayAssets"
|
||||
:is-selected="isSelected"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
/>
|
||||
<VirtualGrid
|
||||
@@ -120,13 +121,10 @@
|
||||
:selected="isSelected(item.id)"
|
||||
:show-output-count="shouldShowOutputCount(item)"
|
||||
:output-count="getOutputCount(item)"
|
||||
:show-delete-button="shouldShowDeleteButton"
|
||||
:open-context-menu-id="openContextMenuId"
|
||||
@click="handleAssetSelect(item)"
|
||||
@zoom="handleZoomClick(item)"
|
||||
@output-count-click="enterFolderView(item)"
|
||||
@asset-deleted="refreshAssets"
|
||||
@context-menu-opened="openContextMenuId = item.id"
|
||||
@context-menu="handleAssetContextMenu($event, item)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
@@ -193,13 +191,23 @@
|
||||
v-model:active-index="galleryActiveIndex"
|
||||
:all-gallery-items="galleryItems"
|
||||
/>
|
||||
<MediaAssetContextMenu
|
||||
v-if="contextMenuAsset"
|
||||
ref="contextMenuRef"
|
||||
:asset="contextMenuAsset"
|
||||
:asset-type="contextMenuAssetType"
|
||||
:file-kind="contextMenuFileKind"
|
||||
:show-delete-button="shouldShowDeleteButton"
|
||||
@zoom="handleContextMenuZoom"
|
||||
@asset-deleted="refreshAssets"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
@@ -212,13 +220,16 @@ import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import MediaAssetContextMenu from '@/platform/assets/components/MediaAssetContextMenu.vue'
|
||||
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
|
||||
import { getAssetType } from '@/platform/assets/composables/media/assetMappers'
|
||||
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
||||
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
|
||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||
import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAssetFiltering'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -243,9 +254,8 @@ const isQueuePanelV2Enabled = computed(() =>
|
||||
const isListView = computed(
|
||||
() => isQueuePanelV2Enabled.value && viewMode.value === 'list'
|
||||
)
|
||||
|
||||
// Track which asset's context menu is open (for single-instance context menu management)
|
||||
const openContextMenuId = ref<string | null>(null)
|
||||
const contextMenuRef = ref<InstanceType<typeof MediaAssetContextMenu>>()
|
||||
const contextMenuAsset = ref<AssetItem | null>(null)
|
||||
|
||||
// Determine if delete button should be shown
|
||||
// Hide delete button when in input tab and not in cloud (OSS mode - files are from local folders)
|
||||
@@ -254,6 +264,14 @@ const shouldShowDeleteButton = computed(() => {
|
||||
return true
|
||||
})
|
||||
|
||||
const contextMenuAssetType = computed(() =>
|
||||
contextMenuAsset.value ? getAssetType(contextMenuAsset.value.tags) : 'input'
|
||||
)
|
||||
|
||||
const contextMenuFileKind = computed<MediaKind>(() =>
|
||||
getMediaTypeFromFilename(contextMenuAsset.value?.name ?? '')
|
||||
)
|
||||
|
||||
const getOutputCount = (item: AssetItem): number => {
|
||||
const count = item.user_metadata?.outputCount
|
||||
return typeof count === 'number' && count > 0 ? count : 1
|
||||
@@ -439,6 +457,18 @@ const handleAssetSelect = (asset: AssetItem) => {
|
||||
handleAssetClick(asset, index, displayAssets.value)
|
||||
}
|
||||
|
||||
function handleAssetContextMenu(event: MouseEvent, asset: AssetItem) {
|
||||
contextMenuAsset.value = asset
|
||||
void nextTick(() => {
|
||||
contextMenuRef.value?.show(event)
|
||||
})
|
||||
}
|
||||
|
||||
function handleContextMenuZoom() {
|
||||
if (!contextMenuAsset.value) return
|
||||
handleZoomClick(contextMenuAsset.value)
|
||||
}
|
||||
|
||||
const handleZoomClick = (asset: AssetItem) => {
|
||||
const mediaType = getMediaTypeFromFilename(asset.name)
|
||||
|
||||
|
||||
@@ -110,21 +110,10 @@
|
||||
</CardBottom>
|
||||
</template>
|
||||
</CardContainer>
|
||||
|
||||
<MediaAssetContextMenu
|
||||
v-if="asset"
|
||||
ref="contextMenu"
|
||||
:asset="asset"
|
||||
:asset-type="assetType"
|
||||
:file-kind="fileKind"
|
||||
:show-delete-button="showDeleteButton"
|
||||
@zoom="handleZoomClick"
|
||||
@asset-deleted="emit('asset-deleted')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementHover, whenever } from '@vueuse/core'
|
||||
import { useElementHover } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
|
||||
|
||||
import IconGroup from '@/components/button/IconGroup.vue'
|
||||
@@ -141,7 +130,6 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import type { AssetItem } from '../schemas/assetSchema'
|
||||
import type { MediaKind } from '../schemas/mediaAssetSchema'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetContextMenu from './MediaAssetContextMenu.vue'
|
||||
|
||||
const mediaComponents = {
|
||||
top: {
|
||||
@@ -166,34 +154,22 @@ function getBottomComponent(kind: MediaKind) {
|
||||
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
|
||||
}
|
||||
|
||||
const {
|
||||
asset,
|
||||
loading,
|
||||
selected,
|
||||
showOutputCount,
|
||||
outputCount,
|
||||
showDeleteButton,
|
||||
openContextMenuId
|
||||
} = defineProps<{
|
||||
const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
|
||||
asset?: AssetItem
|
||||
loading?: boolean
|
||||
selected?: boolean
|
||||
showOutputCount?: boolean
|
||||
outputCount?: number
|
||||
showDeleteButton?: boolean
|
||||
openContextMenuId?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
zoom: [asset: AssetItem]
|
||||
'output-count-click': []
|
||||
'asset-deleted': []
|
||||
'context-menu-opened': []
|
||||
'context-menu': [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const cardContainerRef = ref<HTMLElement>()
|
||||
const contextMenu = ref<InstanceType<typeof MediaAssetContextMenu>>()
|
||||
|
||||
const isVideoPlaying = ref(false)
|
||||
const showVideoControls = ref(false)
|
||||
@@ -302,15 +278,6 @@ const handleOutputCountClick = () => {
|
||||
}
|
||||
|
||||
const handleContextMenu = (event: MouseEvent) => {
|
||||
emit('context-menu-opened')
|
||||
contextMenu.value?.show(event)
|
||||
emit('context-menu', event)
|
||||
}
|
||||
|
||||
// Close this context menu when another opens
|
||||
whenever(
|
||||
() => openContextMenuId && openContextMenuId !== asset?.id,
|
||||
() => {
|
||||
contextMenu.value?.hide()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user