Skip to content

Commit 5a0121f

Browse files
viva-jinyiclaude
andcommitted
feat: Add media type filtering to Media Asset Panel
미디어 애셋 패널에 미디어 타입 필터링 기능을 추가했습니다. - Image, Video, Audio, 3D 타입별로 멀티 선택 필터링 가능 - MediaAssetFilterButton 및 MediaAssetFilterMenu 컴포넌트 추가 - useMediaAssetFiltering composable에 타입 필터링 로직 구현 - 필터는 검색 및 정렬과 함께 동작 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a9f4162 commit 5a0121f

File tree

7 files changed

+174
-3
lines changed

7 files changed

+174
-3
lines changed

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
v-model:search-query="searchQuery"
4545
v-model:sort-by="sortBy"
4646
:show-generation-time-sort="activeTab === 'output'"
47+
v-model:media-type-filters="mediaTypeFilters"
4748
/>
4849
</template>
4950
<template #body>
@@ -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="size" :type="type" @click="toggle">
4+
<i class="icon-[lucide--list-filter] text-sm" />
5+
</IconButton>
6+
7+
<Popover
8+
ref="popover"
9+
:append-to="'body'"
10+
:auto-z-index="true"
11+
:base-z-index="1000"
12+
:dismissable="true"
13+
:close-on-escape="true"
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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<template>
2+
<div class="flex flex-col gap-0 p-0 m-0">
3+
<div
4+
v-for="filter in filters"
5+
:key="filter.type"
6+
class="flex h-10 cursor-pointer items-center gap-2 rounded-lg px-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50"
7+
tabindex="0"
8+
role="checkbox"
9+
:aria-checked="mediaTypeFilters.includes(filter.type)"
10+
@click="toggleMediaType(filter.type)"
11+
@keydown.enter.prevent="toggleMediaType(filter.type)"
12+
@keydown.space.prevent="toggleMediaType(filter.type)"
13+
>
14+
<div
15+
class="flex h-4 w-4 shrink-0 items-center justify-center rounded p-0.5 transition-all duration-200"
16+
:class="
17+
mediaTypeFilters.includes(filter.type)
18+
? 'bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
19+
: 'bg-neutral-100 dark-theme:bg-zinc-700'
20+
"
21+
>
22+
<i
23+
v-if="mediaTypeFilters.includes(filter.type)"
24+
class="icon-[lucide--check] text-xs font-bold text-white"
25+
/>
26+
</div>
27+
<span class="text-sm">{{ $t(filter.label) }}</span>
28+
</div>
29+
</div>
30+
</template>
31+
32+
<script setup lang="ts">
33+
const { mediaTypeFilters } = defineProps<{
34+
mediaTypeFilters: string[]
35+
close: () => void
36+
}>()
37+
38+
const emit = defineEmits<{
39+
'update:mediaTypeFilters': [value: string[]]
40+
}>()
41+
42+
const filters = [
43+
{ type: 'image', label: 'sideToolbar.mediaAssets.filterImage' },
44+
{ type: 'video', label: 'sideToolbar.mediaAssets.filterVideo' },
45+
{ type: 'audio', label: 'sideToolbar.mediaAssets.filterAudio' },
46+
{ type: '3d', label: 'sideToolbar.mediaAssets.filter3D' }
47+
]
48+
49+
const toggleMediaType = (type: string) => {
50+
const isCurrentlySelected = mediaTypeFilters.includes(type)
51+
if (isCurrentlySelected) {
52+
emit(
53+
'update:mediaTypeFilters',
54+
mediaTypeFilters.filter((t) => t !== type)
55+
)
56+
} else {
57+
emit('update:mediaTypeFilters', [...mediaTypeFilters, type])
58+
}
59+
}
60+
</script>

src/platform/assets/composables/useMediaAssetFiltering.ts

Lines changed: 17 additions & 0 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,6 +53,20 @@ 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) {
@@ -77,6 +93,7 @@ export function useMediaAssetFiltering(assets: Ref<AssetItem[]>) {
7793
return {
7894
searchQuery,
7995
sortBy,
96+
mediaTypeFilters,
8097
filteredAssets
8198
}
8299
}

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)