Skip to content

Commit e3f19ab

Browse files
authored
feat: Add media type filtering to Media Asset Panel (#6701)
1 parent a9f4162 commit e3f19ab

File tree

7 files changed

+190
-9
lines changed

7 files changed

+190
-9
lines changed

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
<MediaAssetFilterBar
4444
v-model:search-query="searchQuery"
4545
v-model:sort-by="sortBy"
46+
v-model:media-type-filters="mediaTypeFilters"
4647
:show-generation-time-sort="activeTab === 'output'"
4748
/>
4849
</template>
@@ -255,7 +256,7 @@ const baseAssets = computed(() => {
255256
})
256257
257258
// Use media asset filtering composable
258-
const { searchQuery, sortBy, filteredAssets } =
259+
const { searchQuery, sortBy, mediaTypeFilters, filteredAssets } =
259260
useMediaAssetFiltering(baseAssets)
260261
261262
const displayAssets = computed(() => {

src/locales/en/main.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,11 @@
623623
"sortNewestFirst": "Newest first",
624624
"sortOldestFirst": "Oldest first",
625625
"sortLongestFirst": "Generation time (longest first)",
626-
"sortFastestFirst": "Generation time (fastest first)"
626+
"sortFastestFirst": "Generation time (fastest first)",
627+
"filterImage": "Image",
628+
"filterVideo": "Video",
629+
"filterAudio": "Audio",
630+
"filter3D": "3D"
627631
},
628632
"backToAssets": "Back to all assets",
629633
"searchAssets": "Search assets...",
@@ -2009,6 +2013,7 @@
20092013
"unknown": "Unknown",
20102014
"fileFormats": "File formats",
20112015
"baseModels": "Base models",
2016+
"filterBy": "Filter by",
20122017
"sortBy": "Sort by",
20132018
"sortAZ": "A-Z",
20142019
"sortZA": "Z-A",

src/platform/assets/components/MediaAssetFilterBar.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
size="lg"
77
@update:model-value="handleSearchChange"
88
/>
9+
<MediaAssetFilterButton
10+
v-if="isCloud"
11+
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
12+
size="md"
13+
>
14+
<template #default="{ close }">
15+
<MediaAssetFilterMenu
16+
:media-type-filters="mediaTypeFilters"
17+
:close="close"
18+
@update:media-type-filters="handleMediaTypeFiltersChange"
19+
/>
20+
</template>
21+
</MediaAssetFilterButton>
922
<AssetSortButton
1023
v-if="isCloud"
1124
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
@@ -27,18 +40,22 @@
2740
import SearchBox from '@/components/input/SearchBox.vue'
2841
import { isCloud } from '@/platform/distribution/types'
2942
43+
import MediaAssetFilterButton from './MediaAssetFilterButton.vue'
44+
import MediaAssetFilterMenu from './MediaAssetFilterMenu.vue'
3045
import AssetSortButton from './MediaAssetSortButton.vue'
3146
import MediaAssetSortMenu from './MediaAssetSortMenu.vue'
3247
3348
const { showGenerationTimeSort = false } = defineProps<{
3449
searchQuery: string
3550
sortBy: 'newest' | 'oldest' | 'longest' | 'fastest'
3651
showGenerationTimeSort?: boolean
52+
mediaTypeFilters: string[]
3753
}>()
3854
3955
const emit = defineEmits<{
4056
'update:searchQuery': [value: string]
4157
'update:sortBy': [value: 'newest' | 'oldest' | 'longest' | 'fastest']
58+
'update:mediaTypeFilters': [value: string[]]
4259
}>()
4360
4461
const handleSearchChange = (value: string | undefined) => {
@@ -50,4 +67,8 @@ const handleSortChange = (
5067
) => {
5168
emit('update:sortBy', value)
5269
}
70+
71+
const handleMediaTypeFiltersChange = (value: string[]) => {
72+
emit('update:mediaTypeFilters', value)
73+
}
5374
</script>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<template>
2+
<div class="relative inline-flex items-center">
3+
<IconButton :size :type @click="toggle">
4+
<i class="icon-[lucide--list-filter] text-sm" />
5+
</IconButton>
6+
7+
<Popover
8+
ref="popover"
9+
append-to="#vue-app"
10+
auto-z-index
11+
:base-z-index="1000"
12+
dismissable
13+
close-on-escape
14+
unstyled
15+
:pt="pt"
16+
@show="$emit('menuOpened')"
17+
@hide="$emit('menuClosed')"
18+
>
19+
<div class="flex min-w-40 flex-col gap-2 p-2">
20+
<slot :close="hide" />
21+
</div>
22+
</Popover>
23+
</div>
24+
</template>
25+
26+
<script setup lang="ts">
27+
import Popover from 'primevue/popover'
28+
import { computed, ref } from 'vue'
29+
30+
import IconButton from '@/components/button/IconButton.vue'
31+
import type { BaseButtonProps } from '@/types/buttonTypes'
32+
import { cn } from '@/utils/tailwindUtil'
33+
34+
interface AssetFilterButtonProps extends BaseButtonProps {}
35+
36+
const popover = ref<InstanceType<typeof Popover>>()
37+
38+
const { size = 'md', type = 'secondary' } =
39+
defineProps<AssetFilterButtonProps>()
40+
41+
defineEmits<{
42+
menuOpened: []
43+
menuClosed: []
44+
}>()
45+
46+
const toggle = (event: Event) => {
47+
popover.value?.toggle(event)
48+
}
49+
50+
const hide = () => {
51+
popover.value?.hide()
52+
}
53+
54+
const pt = computed(() => ({
55+
root: {
56+
class: cn('absolute z-50')
57+
},
58+
content: {
59+
class: cn(
60+
'mt-1 rounded-lg',
61+
'bg-base-background text-base-foreground border border-border-default',
62+
'shadow-lg'
63+
)
64+
}
65+
}))
66+
</script>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!--
2+
TODO: Extract checkbox pattern into reusable Checkbox component
3+
- Create src/components/input/Checkbox.vue with:
4+
- Hidden native <input type="checkbox"> for accessibility
5+
- Custom visual styling matching this implementation
6+
- Semantic tokens (--primary-background, --input-surface, etc.)
7+
- Use this Checkbox component in:
8+
- MediaAssetFilterMenu.vue (this file)
9+
- MultiSelect.vue option template
10+
- SingleSelect.vue if needed
11+
- Benefits: Consistent checkbox UI, better maintainability, reusable design system component
12+
-->
13+
<template>
14+
<div class="flex flex-col gap-0 p-0 m-0">
15+
<div
16+
v-for="filter in filters"
17+
:key="filter.type"
18+
class="flex h-10 cursor-pointer items-center gap-2 rounded-lg px-2 hover:bg-secondary-background-hover"
19+
tabindex="0"
20+
role="checkbox"
21+
:aria-checked="mediaTypeFilters.includes(filter.type)"
22+
@click="toggleMediaType(filter.type)"
23+
@keydown.enter.prevent="toggleMediaType(filter.type)"
24+
@keydown.space.prevent="toggleMediaType(filter.type)"
25+
>
26+
<div
27+
class="flex h-4 w-4 shrink-0 items-center justify-center rounded p-0.5 transition-all duration-200"
28+
:class="
29+
mediaTypeFilters.includes(filter.type)
30+
? 'bg-primary-background border-primary-background'
31+
: 'bg-secondary-background'
32+
"
33+
>
34+
<i
35+
v-if="mediaTypeFilters.includes(filter.type)"
36+
class="icon-[lucide--check] text-xs font-bold text-white"
37+
/>
38+
</div>
39+
<span class="text-sm">{{ $t(filter.label) }}</span>
40+
</div>
41+
</div>
42+
</template>
43+
44+
<script setup lang="ts">
45+
const { mediaTypeFilters } = defineProps<{
46+
mediaTypeFilters: string[]
47+
close: () => void
48+
}>()
49+
50+
const emit = defineEmits<{
51+
'update:mediaTypeFilters': [value: string[]]
52+
}>()
53+
54+
const filters = [
55+
{ type: 'image', label: 'sideToolbar.mediaAssets.filterImage' },
56+
{ type: 'video', label: 'sideToolbar.mediaAssets.filterVideo' },
57+
{ type: 'audio', label: 'sideToolbar.mediaAssets.filterAudio' },
58+
{ type: '3d', label: 'sideToolbar.mediaAssets.filter3D' }
59+
]
60+
61+
const toggleMediaType = (type: string) => {
62+
const isCurrentlySelected = mediaTypeFilters.includes(type)
63+
if (isCurrentlySelected) {
64+
emit(
65+
'update:mediaTypeFilters',
66+
mediaTypeFilters.filter((t) => t !== type)
67+
)
68+
} else {
69+
emit('update:mediaTypeFilters', [...mediaTypeFilters, type])
70+
}
71+
}
72+
</script>

src/platform/assets/composables/useMediaAssetFiltering.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { computed, ref } from 'vue'
55
import type { Ref } from 'vue'
66

77
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
8+
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
89

910
type SortOption = 'newest' | 'oldest' | 'longest' | 'fastest'
1011

@@ -33,6 +34,7 @@ export function useMediaAssetFiltering(assets: Ref<AssetItem[]>) {
3334
const searchQuery = ref('')
3435
const debouncedSearchQuery = refDebounced(searchQuery, 50)
3536
const sortBy = ref<SortOption>('newest')
37+
const mediaTypeFilters = ref<string[]>([])
3638

3739
const fuseOptions = {
3840
keys: ['name'],
@@ -51,32 +53,45 @@ export function useMediaAssetFiltering(assets: Ref<AssetItem[]>) {
5153
return results.map((result) => result.item)
5254
})
5355

56+
const typeFiltered = computed(() => {
57+
// Apply media type filter
58+
if (mediaTypeFilters.value.length === 0) {
59+
return searchFiltered.value
60+
}
61+
62+
return searchFiltered.value.filter((asset) => {
63+
const mediaType = getMediaTypeFromFilename(asset.name)
64+
// Convert '3D' to '3d' for comparison
65+
const normalizedType = mediaType.toLowerCase()
66+
return mediaTypeFilters.value.includes(normalizedType)
67+
})
68+
})
69+
5470
const filteredAssets = computed(() => {
5571
// Sort by create_time (output assets) or created_at (input assets)
5672
switch (sortBy.value) {
5773
case 'oldest':
5874
// Ascending order (oldest first)
59-
return sortByUtil(searchFiltered.value, [getAssetTime])
75+
return sortByUtil(typeFiltered.value, [getAssetTime])
6076
case 'longest':
6177
// Descending order (longest execution time first)
62-
return sortByUtil(searchFiltered.value, [
78+
return sortByUtil(typeFiltered.value, [
6379
(asset) => -getAssetExecutionTime(asset)
6480
])
6581
case 'fastest':
6682
// Ascending order (fastest execution time first)
67-
return sortByUtil(searchFiltered.value, [getAssetExecutionTime])
83+
return sortByUtil(typeFiltered.value, [getAssetExecutionTime])
6884
case 'newest':
6985
default:
7086
// Descending order (newest first) - negate for descending
71-
return sortByUtil(searchFiltered.value, [
72-
(asset) => -getAssetTime(asset)
73-
])
87+
return sortByUtil(typeFiltered.value, [(asset) => -getAssetTime(asset)])
7488
}
7589
})
7690

7791
return {
7892
searchQuery,
7993
sortBy,
94+
mediaTypeFilters,
8095
filteredAssets
8196
}
8297
}

src/platform/assets/schemas/assetMetadataSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
12
import type { ResultItemImpl } from '@/stores/queueStore'
23

34
/**
@@ -10,7 +11,7 @@ export interface OutputAssetMetadata extends Record<string, unknown> {
1011
subfolder: string
1112
executionTimeInSeconds?: number
1213
format?: string
13-
workflow?: unknown
14+
workflow?: ComfyWorkflowJSON
1415
outputCount?: number
1516
allOutputs?: ResultItemImpl[]
1617
}

0 commit comments

Comments
 (0)