Skip to content

Commit f2ae87e

Browse files
committed
feat: Add multi-select functionality and improve folder view for media assets
- Add bulk download/delete for multiple selected assets - Hide delete buttons in folder view - Activate keyboard shortcuts only when sidebar is open - Add activate/deactivate lifecycle methods to useAssetSelection
1 parent 7699225 commit f2ae87e

File tree

7 files changed

+196
-47
lines changed

7 files changed

+196
-47
lines changed

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
:selected="isSelected(item.id)"
5959
:show-output-count="shouldShowOutputCount(item)"
6060
:output-count="getOutputCount(item)"
61+
:show-delete-button="!isInFolderView"
6162
@click="handleAssetSelect(item)"
6263
@zoom="handleZoomClick(item)"
6364
@output-count-click="enterFolderView(item)"
@@ -115,6 +116,7 @@
115116
</div>
116117
<div class="flex gap-2">
117118
<IconTextButton
119+
v-if="!isInFolderView"
118120
:label="$t('mediaAsset.selection.deleteSelected')"
119121
type="secondary"
120122
icon-position="right"
@@ -147,7 +149,7 @@
147149
<script setup lang="ts">
148150
import ProgressSpinner from 'primevue/progressspinner'
149151
import { useToast } from 'primevue/usetoast'
150-
import { computed, onUnmounted, ref, watch } from 'vue'
152+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
151153
152154
import IconTextButton from '@/components/button/IconTextButton.vue'
153155
import TextButton from '@/components/button/TextButton.vue'
@@ -160,6 +162,7 @@ import { t } from '@/i18n'
160162
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
161163
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
162164
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
165+
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
163166
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
164167
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
165168
import { ResultItemImpl } from '@/stores/queueStore'
@@ -202,11 +205,11 @@ const {
202205
selectedCount,
203206
clearSelection,
204207
getSelectedAssets,
205-
reset: resetSelection
208+
activate: activateSelection,
209+
deactivate: deactivateSelection
206210
} = useAssetSelection()
207211
208-
// Asset actions - will be used for individual assets later
209-
// const { downloadAsset, deleteAsset } = useMediaAssetActions()
212+
const { downloadMultipleAssets, deleteMultipleAssets } = useMediaAssetActions()
210213
211214
// Hover state for selection count
212215
const isHoveringSelectionCount = ref(false)
@@ -347,9 +350,12 @@ const exitFolderView = () => {
347350
clearSelection()
348351
}
349352
350-
// Clean up selection when component unmounts
353+
onMounted(() => {
354+
activateSelection()
355+
})
356+
351357
onUnmounted(() => {
352-
resetSelection()
358+
deactivateSelection()
353359
})
354360
355361
const handleDeselectAll = () => {
@@ -380,15 +386,13 @@ const copyJobId = async () => {
380386
381387
const handleDownloadSelected = () => {
382388
const selectedAssets = getSelectedAssets(displayAssets.value)
383-
// TODO: Implement actual download logic
384-
// eslint-disable-next-line no-console
385-
console.log('Download selected assets:', selectedAssets)
389+
downloadMultipleAssets(selectedAssets)
390+
clearSelection()
386391
}
387392
388393
const handleDeleteSelected = async () => {
389394
const selectedAssets = getSelectedAssets(displayAssets.value)
390-
// TODO: Implement actual delete logic
391-
// eslint-disable-next-line no-console
392-
console.log('Delete selected assets:', selectedAssets)
395+
await deleteMultipleAssets(selectedAssets)
396+
clearSelection()
393397
}
394398
</script>

src/locales/en/main.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1779,6 +1779,8 @@
17791779
"mediaAsset": {
17801780
"deleteAssetTitle": "Delete this asset?",
17811781
"deleteAssetDescription": "This asset will be permanently removed.",
1782+
"deleteSelectedTitle": "Delete selected assets?",
1783+
"deleteSelectedDescription": "{count} asset(s) will be permanently removed.",
17821784
"assetDeletedSuccessfully": "Asset deleted successfully",
17831785
"deletingImportedFilesCloudOnly": "Deleting imported files is only supported in cloud version",
17841786
"failedToDeleteAsset": "Failed to delete asset",
@@ -1793,7 +1795,10 @@
17931795
"deselectAll": "Deselect all",
17941796
"downloadSelected": "Download",
17951797
"deleteSelected": "Delete",
1796-
"downloadStarted": "Downloading {count} files..."
1798+
"downloadStarted": "Downloading {count} files...",
1799+
"downloadsStarted": "Started downloading {count} file(s)",
1800+
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
1801+
"failedToDeleteAssets": "Failed to delete selected assets"
17971802
}
17981803
},
17991804
"actionbar": {

src/platform/assets/components/MediaAssetActions.vue

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<IconGroup>
3-
<IconButton v-if="showDeleteButton" size="sm" @click="handleDelete">
3+
<IconButton v-if="shouldShowDeleteButton" size="sm" @click="handleDelete">
44
<i class="icon-[lucide--trash-2] size-4" />
55
</IconButton>
66
<IconButton size="sm" @click="handleDownload">
@@ -14,6 +14,7 @@
1414
<template #default="{ close }">
1515
<MediaAssetMoreMenu
1616
:close="close"
17+
:show-delete-button="showDeleteButton"
1718
@inspect="emit('inspect')"
1819
@asset-deleted="emit('asset-deleted')"
1920
/>
@@ -34,6 +35,10 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
3435
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
3536
import MediaAssetMoreMenu from './MediaAssetMoreMenu.vue'
3637
38+
const { showDeleteButton } = defineProps<{
39+
showDeleteButton?: boolean
40+
}>()
41+
3742
const emit = defineEmits<{
3843
menuStateChanged: [isOpen: boolean]
3944
inspect: []
@@ -47,10 +52,12 @@ const assetType = computed(() => {
4752
return context?.value?.type || asset.value?.tags?.[0] || 'output'
4853
})
4954
50-
const showDeleteButton = computed(() => {
51-
return (
55+
const shouldShowDeleteButton = computed(() => {
56+
const propAllows = showDeleteButton ?? true
57+
const typeAllows =
5258
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
53-
)
59+
60+
return propAllows && typeAllows
5461
})
5562
5663
const handleDelete = async () => {

src/platform/assets/components/MediaAssetCard.vue

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
variant="ghost"
1616
rounded="lg"
1717
:class="containerClasses"
18-
@click="handleCardClick"
19-
@keydown.enter="handleCardClick"
20-
@keydown.space.prevent="handleCardClick"
2118
>
2219
<template #top>
2320
<CardTop
@@ -50,6 +47,7 @@
5047
<!-- Actions overlay (top-left) - show on hover or when menu is open -->
5148
<template v-if="showActionsOverlay" #top-left>
5249
<MediaAssetActions
50+
:show-delete-button="showDeleteButton ?? true"
5351
@menu-state-changed="isMenuOpen = $event"
5452
@inspect="handleZoomClick"
5553
@asset-deleted="handleAssetDelete"
@@ -174,12 +172,20 @@ function getBottomComponent(kind: MediaKind) {
174172
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
175173
}
176174
177-
const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
175+
const {
176+
asset,
177+
loading,
178+
selected,
179+
showOutputCount,
180+
outputCount,
181+
showDeleteButton
182+
} = defineProps<{
178183
asset?: AssetItem
179184
loading?: boolean
180185
selected?: boolean
181186
showOutputCount?: boolean
182187
outputCount?: number
188+
showDeleteButton?: boolean
183189
}>()
184190
185191
const emit = defineEmits<{
@@ -312,12 +318,6 @@ const showFileFormatChip = computed(
312318
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
313319
)
314320
315-
const handleCardClick = () => {
316-
if (adaptedAsset.value) {
317-
actions.selectAsset(adaptedAsset.value)
318-
}
319-
}
320-
321321
const handleOverlayMouseEnter = () => {
322322
isOverlayHovered.value = true
323323
}

src/platform/assets/components/MediaAssetMoreMenu.vue

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@
7575
</template>
7676
</IconTextButton>
7777

78-
<MediaAssetButtonDivider v-if="showCopyJobId && showDeleteButton" />
78+
<MediaAssetButtonDivider v-if="showCopyJobId && shouldShowDeleteButton" />
7979

8080
<IconTextButton
81-
v-if="showDeleteButton"
81+
v-if="shouldShowDeleteButton"
8282
type="transparent"
8383
class="dark-theme:text-white"
8484
label="Delete"
@@ -101,8 +101,9 @@ import { useMediaAssetActions } from '../composables/useMediaAssetActions'
101101
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
102102
import MediaAssetButtonDivider from './MediaAssetButtonDivider.vue'
103103
104-
const { close } = defineProps<{
104+
const { close, showDeleteButton } = defineProps<{
105105
close: () => void
106+
showDeleteButton?: boolean
106107
}>()
107108
108109
const emit = defineEmits<{
@@ -124,13 +125,12 @@ const showCopyJobId = computed(() => {
124125
return assetType.value !== 'input'
125126
})
126127
127-
// Delete button should be shown for:
128-
// - All output files (can be deleted via history)
129-
// - Input files only in cloud environment
130-
const showDeleteButton = computed(() => {
131-
return (
128+
const shouldShowDeleteButton = computed(() => {
129+
const propAllows = showDeleteButton ?? true
130+
const typeAllows =
132131
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
133-
)
132+
133+
return propAllows && typeAllows
134134
})
135135
136136
const handleInspect = () => {

src/platform/assets/composables/useAssetSelection.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { useKeyModifier } from '@vueuse/core'
2-
import { computed } from 'vue'
2+
import { computed, ref } from 'vue'
33

44
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
55
import { useAssetSelectionStore } from '@/platform/assets/composables/useAssetSelectionStore'
66

77
export function useAssetSelection() {
88
const selectionStore = useAssetSelectionStore()
99

10-
// Key modifiers
11-
const shiftKey = useKeyModifier('Shift')
12-
const ctrlKey = useKeyModifier('Control')
13-
const metaKey = useKeyModifier('Meta')
10+
// Track whether the asset selection is active (e.g., when sidebar is open)
11+
const isActive = ref<boolean>(true)
12+
13+
// Key modifiers - raw values
14+
const shiftKeyRaw = useKeyModifier('Shift')
15+
const ctrlKeyRaw = useKeyModifier('Control')
16+
const metaKeyRaw = useKeyModifier('Meta')
17+
18+
// Only respond to key modifiers when active
19+
const shiftKey = computed(() => isActive.value && shiftKeyRaw.value)
20+
const ctrlKey = computed(() => isActive.value && ctrlKeyRaw.value)
21+
const metaKey = computed(() => isActive.value && metaKeyRaw.value)
1422
const cmdOrCtrlKey = computed(() => ctrlKey.value || metaKey.value)
1523

1624
/**
@@ -80,6 +88,22 @@ export function useAssetSelection() {
8088
return allAssets.filter((asset) => selectionStore.isSelected(asset.id))
8189
}
8290

91+
/**
92+
* Activate key event listeners (when sidebar opens)
93+
*/
94+
function activate() {
95+
isActive.value = true
96+
}
97+
98+
/**
99+
* Deactivate key event listeners (when sidebar closes)
100+
*/
101+
function deactivate() {
102+
isActive.value = false
103+
// Reset selection state to ensure clean state when deactivated
104+
selectionStore.reset()
105+
}
106+
83107
return {
84108
// Selection state
85109
selectedIds: computed(() => selectionStore.selectedAssetIds),
@@ -94,6 +118,10 @@ export function useAssetSelection() {
94118
getSelectedAssets,
95119
reset: () => selectionStore.reset(),
96120

121+
// Lifecycle management
122+
activate,
123+
deactivate,
124+
97125
// Key states (for UI feedback)
98126
shiftKey,
99127
cmdOrCtrlKey

0 commit comments

Comments
 (0)