Skip to content
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
24 changes: 17 additions & 7 deletions src/components/sidebar/tabs/AssetsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,15 @@
:output-count="getOutputCount(item)"
:show-delete-button="shouldShowDeleteButton"
:open-context-menu-id="openContextMenuId"
:selected-assets="getSelectedAssets(displayAssets)"
:has-selection="hasSelection"
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
@context-menu-opened="openContextMenuId = item.id"
@bulk-download="handleBulkDownload"
@bulk-delete="handleBulkDelete"
/>
</template>
</VirtualGrid>
Expand All @@ -134,7 +138,6 @@
<div ref="selectionCountButtonRef" class="inline-flex w-48">
<Button
variant="secondary"
size="lg"
:class="cn(isCompact && 'text-left')"
@click="handleDeselectAll"
>
Expand Down Expand Up @@ -243,11 +246,6 @@ const shouldShowDeleteButton = computed(() => {
return true
})

const getOutputCount = (item: AssetItem): number => {
const count = item.user_metadata?.outputCount
return typeof count === 'number' && count > 0 ? count : 1
}

const shouldShowOutputCount = (item: AssetItem): boolean => {
if (activeTab.value !== 'output' || isInFolderView.value) {
return false
Expand Down Expand Up @@ -285,6 +283,8 @@ const {
hasSelection,
clearSelection,
getSelectedAssets,
getOutputCount,
getTotalOutputCount,
activate: activateSelection,
deactivate: deactivateSelection
} = useAssetSelection()
Expand Down Expand Up @@ -316,7 +316,7 @@ const isHoveringSelectionCount = useElementHover(selectionCountButtonRef)
// Total output count for all selected assets
const totalOutputCount = computed(() => {
const selectedAssets = getSelectedAssets(displayAssets.value)
return selectedAssets.reduce((sum, asset) => sum + getOutputCount(asset), 0)
return getTotalOutputCount(selectedAssets)
})

const currentAssets = computed(() =>
Expand Down Expand Up @@ -537,6 +537,16 @@ const handleDeleteSelected = async () => {
clearSelection()
}

const handleBulkDownload = (assets: AssetItem[]) => {
downloadMultipleAssets(assets)
clearSelection()
}

const handleBulkDelete = async (assets: AssetItem[]) => {
await deleteMultipleAssets(assets)
clearSelection()
}

const handleClearQueue = async () => {
await commandStore.execute('Comfy.ClearPendingTasks')
}
Expand Down
3 changes: 3 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2410,9 +2410,12 @@
},
"selection": {
"selectedCount": "Assets Selected: {count}",
"multipleSelectedAssets": "Multiple assets selected",
"deselectAll": "Deselect all",
"downloadSelected": "Download",
"downloadSelectedAll": "Download all",
"deleteSelected": "Delete",
"deleteSelectedAll": "Delete all",
"downloadStarted": "Downloading {count} files...",
"downloadsStarted": "Started downloading {count} file(s)",
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
Expand Down
12 changes: 11 additions & 1 deletion src/platform/assets/components/MediaAssetCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,12 @@
:asset-type="assetType"
:file-kind="fileKind"
:show-delete-button="showDeleteButton"
:selected-assets="selectedAssets"
:is-bulk-mode="hasSelection && (selectedAssets?.length ?? 0) > 1"
@zoom="handleZoomClick"
@asset-deleted="emit('asset-deleted')"
@bulk-download="emit('bulk-download', $event)"
@bulk-delete="emit('bulk-delete', $event)"
/>
</template>

Expand Down Expand Up @@ -174,7 +178,9 @@ const {
showOutputCount,
outputCount,
showDeleteButton,
openContextMenuId
openContextMenuId,
selectedAssets,
hasSelection
} = defineProps<{
asset?: AssetItem
loading?: boolean
Expand All @@ -183,6 +189,8 @@ const {
outputCount?: number
showDeleteButton?: boolean
openContextMenuId?: string | null
selectedAssets?: AssetItem[]
hasSelection?: boolean
}>()

const emit = defineEmits<{
Expand All @@ -191,6 +199,8 @@ const emit = defineEmits<{
'output-count-click': []
'asset-deleted': []
'context-menu-opened': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
}>()

const cardContainerRef = ref<HTMLElement>()
Expand Down
55 changes: 52 additions & 3 deletions src/platform/assets/components/MediaAssetContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
<template #item="{ item, props }">
<Button
variant="secondary"
size="sm"
class="w-full justify-start"
v-bind="props.action"
>
<i :class="item.icon" class="size-4" />
<i v-if="item.icon" :class="item.icon" class="size-4" />
<span>{{
typeof item.label === 'function' ? item.label() : (item.label ?? '')
}}</span>
Expand All @@ -45,16 +44,27 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import type { AssetItem } from '../schemas/assetSchema'
import type { AssetContext, MediaKind } from '../schemas/mediaAssetSchema'

const { asset, assetType, fileKind, showDeleteButton } = defineProps<{
const {
asset,
assetType,
fileKind,
showDeleteButton,
selectedAssets,
isBulkMode
} = defineProps<{
asset: AssetItem
assetType: AssetContext['type']
fileKind: MediaKind
showDeleteButton?: boolean
selectedAssets?: AssetItem[]
isBulkMode?: boolean
}>()

const emit = defineEmits<{
zoom: []
'asset-deleted': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
}>()

const contextMenu = ref<InstanceType<typeof ContextMenu>>()
Expand Down Expand Up @@ -112,6 +122,45 @@ const contextMenuItems = computed<MenuItem[]>(() => {

const items: MenuItem[] = []

// Check if current asset is part of the selection
const isCurrentAssetSelected = selectedAssets?.some(
(selectedAsset) => selectedAsset.id === asset.id
)

// Bulk mode: Show selected count and bulk actions only if current asset is selected
if (
isBulkMode &&
selectedAssets &&
selectedAssets.length > 0 &&
isCurrentAssetSelected
) {
// Header item showing selected count
items.push({
label: t('mediaAsset.selection.multipleSelectedAssets'),
disabled: true
})

// Bulk Download
items.push({
label: t('mediaAsset.selection.downloadSelectedAll'),
icon: 'icon-[lucide--download]',
command: () => emit('bulk-download', selectedAssets)
})

// Bulk Delete (if allowed)
if (shouldShowDeleteButton.value) {
items.push({
label: t('mediaAsset.selection.deleteSelectedAll'),
icon: 'icon-[lucide--trash-2]',
command: () => emit('bulk-delete', selectedAssets)
})
}

return items
}

// Individual mode: Show all menu options

// Inspect (if not 3D)
if (fileKind !== '3D') {
items.push({
Expand Down
18 changes: 18 additions & 0 deletions src/platform/assets/composables/useAssetSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,22 @@ export function useAssetSelection() {
return allAssets.filter((asset) => selectionStore.isSelected(asset.id))
}

/**
* Get the output count for a single asset
* Same logic as in AssetsSidebarTab.vue
*/
function getOutputCount(item: AssetItem): number {
const count = item.user_metadata?.outputCount
return typeof count === 'number' && count > 0 ? count : 1
}

/**
* Get the total output count for given assets
*/
function getTotalOutputCount(assets: AssetItem[]): number {
return assets.reduce((sum, asset) => sum + getOutputCount(asset), 0)
}

/**
* Activate key event listeners (when sidebar opens)
*/
Expand Down Expand Up @@ -116,6 +132,8 @@ export function useAssetSelection() {
selectAll,
clearSelection: () => selectionStore.clearSelection(),
getSelectedAssets,
getOutputCount,
getTotalOutputCount,
reset: () => selectionStore.reset(),

// Lifecycle management
Expand Down