Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
94884d7
feat: add queue view toggle stub
benceruleanlu Dec 20, 2025
5ddea4e
Extract to component
benceruleanlu Dec 20, 2025
a0dad31
Add feature flag
benceruleanlu Dec 22, 2025
3d0c0d1
Move feature flag to setting
benceruleanlu Dec 31, 2025
dc53cbe
Add N active jobs and clear queue button
benceruleanlu Dec 22, 2025
2fd9a73
Readd divider as v-else
benceruleanlu Dec 23, 2025
0c6ea56
fix: read QPOV2 setting in assets sidebar
benceruleanlu Jan 1, 2026
5b14568
Add AssetsListCard base template
benceruleanlu Dec 22, 2025
da48899
Add AssetsListCard stories
benceruleanlu Dec 22, 2025
f614914
Remove view action from AssetsListCard story
benceruleanlu Dec 22, 2025
85c6825
Add list view
benceruleanlu Dec 23, 2025
627db67
knip
benceruleanlu Dec 23, 2025
afa4664
[automated] Apply ESLint and Prettier fixes
actions-user Dec 23, 2025
f128c61
Remove special failed job styling
benceruleanlu Dec 23, 2025
db3edd5
Add list view
benceruleanlu Dec 23, 2025
d8e57c6
Add stories for list view and general job card
benceruleanlu Dec 23, 2025
f78b6ee
Add and use knipIgnoreUnusedButUsedByStorybook
benceruleanlu Dec 23, 2025
79af715
fix: avoid unused export in job actions
benceruleanlu Jan 1, 2026
5561efc
Add ... context menu to list view
benceruleanlu Dec 23, 2025
55db4fe
Merge remote-tracking branch 'origin/main' into bl-precious-earthworm
benceruleanlu Jan 6, 2026
5ed4e5f
Rename card to item
benceruleanlu Jan 7, 2026
9e477d4
Add select-none to list item
benceruleanlu Jan 7, 2026
e9d675e
refactor: share progress bar background helpers
benceruleanlu Jan 7, 2026
ede934a
Inline job card class
benceruleanlu Jan 7, 2026
114ece5
Inline list card base class
benceruleanlu Jan 7, 2026
f9eb1c4
Extract media icon mapping
benceruleanlu Jan 7, 2026
7c82218
refactor: simplify job cancel action
benceruleanlu Jan 7, 2026
9c4f808
knip
benceruleanlu Jan 7, 2026
72a9571
refactor: simplify job action sets
benceruleanlu Jan 7, 2026
ab09e7a
refactor: make job menu actions explicit
benceruleanlu Jan 7, 2026
a8fa3e3
feat: add icon slot to assets list item
benceruleanlu Jan 7, 2026
434ee52
fix: prevent active job items from shrinking
benceruleanlu Jan 7, 2026
578b8fb
Refactor job actions to single cancel and formalUtil extraction
benceruleanlu Jan 9, 2026
0f02b9f
refactor: make job cancel action reactive
benceruleanlu Jan 9, 2026
c63352c
Merge remote-tracking branch 'origin/bl-precious-earthworm' into bl-s…
benceruleanlu Jan 9, 2026
97d20a5
Merge remote-tracking branch 'origin/bl-spontaneous-guppy' into bl-ya…
benceruleanlu Jan 10, 2026
92b1123
Merge remote-tracking branch 'origin/main' into bl-yappiest-fish
benceruleanlu Jan 11, 2026
faaf031
Remove unneeded knip flags
benceruleanlu Jan 11, 2026
461864a
Inline asset context menu handler
benceruleanlu Jan 12, 2026
d706ce9
Unify menu
benceruleanlu Jan 13, 2026
9d86d28
Inline zoom handle
benceruleanlu Jan 13, 2026
b0afc7f
nit
benceruleanlu Jan 13, 2026
cfca61d
Remove redundant guard
benceruleanlu Jan 13, 2026
3a94cd8
Clear asset on context menu hide
benceruleanlu Jan 13, 2026
a18b2c0
Use asset-specific more options label
benceruleanlu Jan 13, 2026
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
28 changes: 27 additions & 1 deletion src/components/sidebar/tabs/AssetsSidebarListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,22 @@
"
:primary-text="getAssetPrimaryText(item.asset)"
:secondary-text="getAssetSecondaryText(item.asset)"
@mouseenter="onAssetEnter(item.asset.id)"
@mouseleave="onAssetLeave(item.asset.id)"
@contextmenu.prevent.stop="emit('context-menu', $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('mediaAsset.actions.moreOptions')"
@click.stop="emit('context-menu', $event, item.asset)"
>
<i class="icon-[lucide--ellipsis] size-4" />
</Button>
</template>
</AssetsListItem>
</template>
</VirtualGrid>
</div>
Expand Down Expand Up @@ -111,12 +125,14 @@ 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
}>()

const { t } = useI18n()
const { jobItems } = useJobList()
const hoveredJobId = ref<string | null>(null)
const hoveredAssetId = ref<string | null>(null)

type AssetListItem = { key: string; asset: AssetItem }

Expand Down Expand Up @@ -192,6 +208,16 @@ function onJobLeave(jobId: string) {
}
}

function onAssetEnter(assetId: string) {
hoveredAssetId.value = assetId
}

function onAssetLeave(assetId: string) {
if (hoveredAssetId.value === assetId) {
hoveredAssetId.value = null
}
}

function getJobIconClass(job: JobListItem): string | undefined {
const classes = []
const iconName = job.iconName ?? iconForJobState(job.state)
Expand Down
68 changes: 51 additions & 17 deletions src/components/sidebar/tabs/AssetsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
:assets="displayAssets"
:is-selected="isSelected"
@select-asset="handleAssetSelect"
@context-menu="handleAssetContextMenu"
@approach-end="handleApproachEnd"
/>
<VirtualGrid
Expand All @@ -120,17 +121,10 @@
:selected="isSelected(item.id)"
:show-output-count="shouldShowOutputCount(item)"
:output-count="getOutputCount(item)"
:show-delete-button="shouldShowDeleteButton"
:open-context-menu-id="openContextMenuId"
:selected-assets="getSelectedAssets(displayAssets)"
:has-selection="hasSelection"
@click="handleAssetSelect(item)"
@context-menu="handleAssetContextMenu"
@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 Down Expand Up @@ -196,14 +190,29 @@
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"
:selected-assets="selectedAssets"
:is-bulk-mode="isBulkMode"
@zoom="handleZoomClick(contextMenuAsset)"
@hide="handleContextMenuHide"
@asset-deleted="refreshAssets"
@bulk-download="handleBulkDownload"
@bulk-delete="handleBulkDelete"
/>
</template>

<script setup lang="ts">
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
import Divider from 'primevue/divider'
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'
Expand All @@ -216,13 +225,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'
Expand All @@ -248,8 +260,8 @@ 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)
Expand All @@ -258,6 +270,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 shouldShowOutputCount = (item: AssetItem): boolean => {
if (activeTab.value !== 'output' || isInFolderView.value) {
return false
Expand Down Expand Up @@ -327,8 +347,7 @@ const isHoveringSelectionCount = useElementHover(selectionCountButtonRef)

// Total output count for all selected assets
const totalOutputCount = computed(() => {
const selectedAssets = getSelectedAssets(displayAssets.value)
return getTotalOutputCount(selectedAssets)
return getTotalOutputCount(selectedAssets.value)
})

const currentAssets = computed(() =>
Expand Down Expand Up @@ -359,6 +378,12 @@ const displayAssets = computed(() => {
return filteredAssets.value
})

const selectedAssets = computed(() => getSelectedAssets(displayAssets.value))

const isBulkMode = computed(
() => hasSelection.value && selectedAssets.value.length > 1
)

const showLoadingState = computed(
() =>
loading.value &&
Expand Down Expand Up @@ -444,6 +469,17 @@ 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 handleContextMenuHide() {
contextMenuAsset.value = null
}

const handleZoomClick = (asset: AssetItem) => {
const mediaType = getMediaTypeFromFilename(asset.name)

Expand Down Expand Up @@ -552,14 +588,12 @@ const copyJobId = async () => {
}

const handleDownloadSelected = () => {
const selectedAssets = getSelectedAssets(displayAssets.value)
downloadMultipleAssets(selectedAssets)
downloadMultipleAssets(selectedAssets.value)
clearSelection()
}

const handleDeleteSelected = async () => {
const selectedAssets = getSelectedAssets(displayAssets.value)
await deleteMultipleAssets(selectedAssets)
await deleteMultipleAssets(selectedAssets.value)
clearSelection()
}

Expand Down
61 changes: 9 additions & 52 deletions src/platform/assets/components/MediaAssetCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"
:data-selected="selected"
@click.stop="$emit('click')"
@contextmenu.prevent="handleContextMenu"
@contextmenu.prevent.stop="
asset ? emit('context-menu', $event, asset) : undefined
"
>
<!-- Top Area: Media Preview -->
<div class="relative aspect-square overflow-hidden p-0">
Expand Down Expand Up @@ -64,7 +66,9 @@
variant="overlay-white"
size="icon"
:aria-label="$t('mediaAsset.actions.moreOptions')"
@click.stop="handleContextMenu"
@click.stop="
asset ? emit('context-menu', $event, asset) : undefined
"
>
<i class="icon-[lucide--ellipsis] size-4" />
</Button>
Expand Down Expand Up @@ -119,25 +123,10 @@
</div>
</div>
</div>

<MediaAssetContextMenu
v-if="asset"
ref="contextMenu"
:asset="asset"
: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>

<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'
Expand All @@ -155,7 +144,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'
import MediaTitle from './MediaTitle.vue'

const mediaComponents = {
Expand All @@ -171,40 +159,22 @@ function getTopComponent(kind: MediaKind) {
return mediaComponents.top[kind] || mediaComponents.top.image
}

const {
asset,
loading,
selected,
showOutputCount,
outputCount,
showDeleteButton,
openContextMenuId,
selectedAssets,
hasSelection
} = defineProps<{
const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
asset?: AssetItem
loading?: boolean
selected?: boolean
showOutputCount?: boolean
outputCount?: number
showDeleteButton?: boolean
openContextMenuId?: string | null
selectedAssets?: AssetItem[]
hasSelection?: boolean
}>()

const emit = defineEmits<{
click: []
zoom: [asset: AssetItem]
'output-count-click': []
'asset-deleted': []
'context-menu-opened': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
'context-menu': [event: MouseEvent, asset: AssetItem]
}>()

const cardContainerRef = ref<HTMLElement>()
const contextMenu = ref<InstanceType<typeof MediaAssetContextMenu>>()

const isVideoPlaying = ref(false)
const showVideoControls = ref(false)
Expand Down Expand Up @@ -299,17 +269,4 @@ const handleImageLoaded = (width: number, height: number) => {
const handleOutputCountClick = () => {
emit('output-count-click')
}

const handleContextMenu = (event: MouseEvent) => {
emit('context-menu-opened')
contextMenu.value?.show(event)
}

// Close this context menu when another opens
whenever(
() => openContextMenuId && openContextMenuId !== asset?.id,
() => {
contextMenu.value?.hide()
}
)
</script>
2 changes: 2 additions & 0 deletions src/platform/assets/components/MediaAssetContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
}
}"
@hide="emit('hide')"
>
<template #item="{ item, props }">
<Button
Expand Down Expand Up @@ -65,6 +66,7 @@ const emit = defineEmits<{
'asset-deleted': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
hide: []
}>()

const contextMenu = ref<InstanceType<typeof ContextMenu>>()
Expand Down