Skip to content

Commit 6c36aaa

Browse files
viva-jinyiclaude
authored andcommitted
feat: Improve MediaAssetCard video controls and add gallery view (#6065)
## Summary - Enhanced video control visibility logic for better UX - Added fullscreen gallery view with zoom-in button - Fixed hover interaction issues with overlays ## Changes ### Video Controls - **Before**: Controls hidden when not hovering - **After**: Controls always visible when not playing, hover-based during playback ### Overlay Behavior - **Before**: All overlays hidden during video playback - **After**: All overlays (actions, tags, layers) show on hover even during playback ### Gallery View - Added zoom-in button to top-right corner (all media types except 3D) - Integrated with existing ResultGallery component - Gallery closes when clicking dimmed background area ### Bug Fixes - Fixed hover flicker issue by proper event handling on overlay elements ## Test Plan - [x] Test video controls visibility (paused vs playing) - [x] Test overlay hover behavior during video playback - [x] Test zoom-in button opens gallery view - [x] Test gallery closes on background click - [x] Test 3D assets don't show zoom button - [x] Test in Storybook with various media types 🤖 Generated with [Claude Code](https://claude.ai/code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6065-feat-Improve-MediaAssetCard-video-controls-and-add-gallery-view-28d6d73d3650818c90cfc5d0d00e4826) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <[email protected]>
1 parent 6944ef0 commit 6c36aaa

File tree

7 files changed

+348
-49
lines changed

7 files changed

+348
-49
lines changed

src/platform/assets/components/MediaAssetCard.stories.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
import type { Meta, StoryObj } from '@storybook/vue3-vite'
22

3+
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
4+
5+
import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
36
import type { AssetMeta } from '../schemas/mediaAssetSchema'
47
import MediaAssetCard from './MediaAssetCard.vue'
58

69
const meta: Meta<typeof MediaAssetCard> = {
7-
title: 'AssetLibrary/MediaAssetCard',
10+
title: 'Platform/Assets/MediaAssetCard',
811
component: MediaAssetCard,
12+
decorators: [
13+
() => ({
14+
components: { ResultGallery },
15+
setup() {
16+
const galleryStore = useMediaAssetGalleryStore()
17+
return { galleryStore }
18+
},
19+
template: `
20+
<div>
21+
<story />
22+
<ResultGallery
23+
v-model:active-index="galleryStore.activeIndex"
24+
:all-gallery-items="galleryStore.items"
25+
/>
26+
</div>
27+
`
28+
})
29+
],
930
argTypes: {
1031
context: {
1132
control: 'select',

src/platform/assets/components/MediaAssetCard.vue

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -33,39 +33,56 @@
3333
:is="getTopComponent(asset.kind)"
3434
:asset="asset"
3535
:context="context"
36-
@view="actions.viewAsset(asset!.id)"
36+
@view="handleZoomClick"
3737
@download="actions.downloadAsset(asset!.id)"
3838
@play="actions.playAsset(asset!.id)"
3939
@video-playing-state-changed="isVideoPlaying = $event"
4040
@video-controls-changed="showVideoControls = $event"
4141
/>
4242
</template>
4343

44-
<!-- Actions overlay (top-left) - show on hover or when menu is open, but not when video is playing -->
44+
<!-- Actions overlay (top-left) - show on hover or when menu is open -->
4545
<template v-if="showActionsOverlay" #top-left>
46-
<MediaAssetActions @menu-state-changed="isMenuOpen = $event" />
46+
<MediaAssetActions
47+
@menu-state-changed="isMenuOpen = $event"
48+
@mouseenter="handleOverlayMouseEnter"
49+
@mouseleave="handleOverlayMouseLeave"
50+
/>
4751
</template>
4852

49-
<!-- Zoom button (top-right) - show on hover, but not when video is playing -->
53+
<!-- Zoom button (top-right) - show on hover for all media types -->
5054
<template v-if="showZoomOverlay" #top-right>
51-
<IconButton size="sm" @click="actions.viewAsset(asset!.id)">
55+
<IconButton
56+
size="sm"
57+
@click.stop="handleZoomClick"
58+
@mouseenter="handleOverlayMouseEnter"
59+
@mouseleave="handleOverlayMouseLeave"
60+
>
5261
<i class="icon-[lucide--zoom-in] size-4" />
5362
</IconButton>
5463
</template>
5564

56-
<!-- Duration/Format chips (bottom-left) - hide when video is playing -->
65+
<!-- Duration/Format chips (bottom-left) - show on hover even when playing -->
5766
<template v-if="showDurationChips" #bottom-left>
58-
<SquareChip variant="light" :label="formattedDuration" />
59-
<SquareChip v-if="fileFormat" variant="light" :label="fileFormat" />
67+
<div
68+
class="flex flex-wrap items-center gap-1"
69+
@mouseenter="handleOverlayMouseEnter"
70+
@mouseleave="handleOverlayMouseLeave"
71+
>
72+
<SquareChip variant="light" :label="formattedDuration" />
73+
<SquareChip v-if="fileFormat" variant="light" :label="fileFormat" />
74+
</div>
6075
</template>
6176

62-
<!-- Output count (bottom-right) - hide when video is playing -->
77+
<!-- Output count (bottom-right) - show on hover even when playing -->
6378
<template v-if="showOutputCount" #bottom-right>
6479
<IconTextButton
6580
type="secondary"
6681
size="sm"
6782
:label="context?.outputCount?.toString() ?? '0'"
68-
@click="actions.openMoreOutputs(asset?.id || '')"
83+
@click.stop="actions.openMoreOutputs(asset?.id || '')"
84+
@mouseenter="handleOverlayMouseEnter"
85+
@mouseleave="handleOverlayMouseLeave"
6986
>
7087
<template #icon>
7188
<i class="icon-[lucide--layers] size-4" />
@@ -116,6 +133,7 @@ import { formatDuration } from '@/utils/formatUtil'
116133
import { cn } from '@/utils/tailwindUtil'
117134
118135
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
136+
import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
119137
import type {
120138
AssetContext,
121139
AssetMeta,
@@ -159,10 +177,12 @@ const cardContainerRef = ref<HTMLElement>()
159177
const isVideoPlaying = ref(false)
160178
const isMenuOpen = ref(false)
161179
const showVideoControls = ref(false)
180+
const isOverlayHovered = ref(false)
162181
163182
const isHovered = useElementHover(cardContainerRef)
164183
165184
const actions = useMediaAssetActions()
185+
const galleryStore = useMediaAssetGalleryStore()
166186
167187
provide(MediaAssetKey, {
168188
asset: toRef(() => asset),
@@ -171,14 +191,14 @@ provide(MediaAssetKey, {
171191
showVideoControls
172192
})
173193
174-
const containerClasses = computed(() => {
175-
return cn(
194+
const containerClasses = computed(() =>
195+
cn(
176196
'gap-1',
177197
selected
178198
? 'border-3 border-zinc-900 dark-theme:border-white bg-zinc-200 dark-theme:bg-zinc-700'
179199
: 'hover:bg-zinc-100 dark-theme:hover:bg-zinc-800'
180200
)
181-
})
201+
)
182202
183203
const formattedDuration = computed(() => {
184204
if (!asset?.duration) return ''
@@ -201,33 +221,58 @@ const durationChipClasses = computed(() => {
201221
return ''
202222
})
203223
204-
const showHoverActions = computed(() => {
205-
return !loading && !!asset && (isHovered.value || isMenuOpen.value)
206-
})
224+
const isCardOrOverlayHovered = computed(
225+
() => isHovered.value || isOverlayHovered.value || isMenuOpen.value
226+
)
207227
208-
const showZoomButton = computed(() => {
209-
return asset?.kind === 'image' || asset?.kind === '3D'
210-
})
228+
const showHoverActions = computed(
229+
() => !loading && !!asset && isCardOrOverlayHovered.value
230+
)
211231
212-
const showActionsOverlay = computed(() => {
213-
return showHoverActions.value && !isVideoPlaying.value
214-
})
232+
const showActionsOverlay = computed(
233+
() =>
234+
showHoverActions.value &&
235+
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
236+
)
215237
216-
const showZoomOverlay = computed(() => {
217-
return showHoverActions.value && showZoomButton.value && !isVideoPlaying.value
218-
})
238+
const showZoomOverlay = computed(
239+
() =>
240+
showHoverActions.value &&
241+
asset?.kind !== '3D' &&
242+
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
243+
)
219244
220-
const showDurationChips = computed(() => {
221-
return !loading && asset?.duration && !isVideoPlaying.value
222-
})
245+
const showDurationChips = computed(
246+
() =>
247+
!loading &&
248+
asset?.duration &&
249+
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
250+
)
223251
224-
const showOutputCount = computed(() => {
225-
return !loading && context?.outputCount && !isVideoPlaying.value
226-
})
252+
const showOutputCount = computed(
253+
() =>
254+
!loading &&
255+
context?.outputCount &&
256+
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
257+
)
227258
228259
const handleCardClick = () => {
229260
if (asset) {
230261
actions.selectAsset(asset)
231262
}
232263
}
264+
265+
const handleOverlayMouseEnter = () => {
266+
isOverlayHovered.value = true
267+
}
268+
269+
const handleOverlayMouseLeave = () => {
270+
isOverlayHovered.value = false
271+
}
272+
273+
const handleZoomClick = () => {
274+
if (asset) {
275+
galleryStore.openSingle(asset)
276+
}
277+
}
233278
</script>

src/platform/assets/components/MediaAssetMoreMenu.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<div class="flex flex-col">
33
<IconTextButton
4+
v-if="asset?.kind !== '3D'"
45
type="transparent"
56
class="dark-theme:text-white"
67
label="Inspect asset"
@@ -93,6 +94,7 @@ import { computed, inject } from 'vue'
9394
import IconTextButton from '@/components/button/IconTextButton.vue'
9495
9596
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
97+
import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
9698
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
9799
import MediaAssetButtonDivider from './MediaAssetButtonDivider.vue'
98100
@@ -102,14 +104,13 @@ const { close } = defineProps<{
102104
103105
const { asset, context } = inject(MediaAssetKey)!
104106
const actions = useMediaAssetActions()
107+
const galleryStore = useMediaAssetGalleryStore()
105108
106-
const showWorkflowOptions = computed(() => {
107-
return context.value.type
108-
})
109+
const showWorkflowOptions = computed(() => context.value.type)
109110
110111
const handleInspect = () => {
111112
if (asset.value) {
112-
actions.viewAsset(asset.value.id)
113+
galleryStore.openSingle(asset.value)
113114
}
114115
close()
115116
}
Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
<template>
22
<div
33
class="relative h-full w-full overflow-hidden rounded bg-black"
4-
@mouseenter="showControls = true"
5-
@mouseleave="showControls = false"
4+
@mouseenter="isHovered = true"
5+
@mouseleave="isHovered = false"
66
>
77
<video
88
ref="videoRef"
9-
:controls="showControls"
9+
:controls="shouldShowControls"
1010
preload="none"
1111
:poster="asset.preview_url"
1212
class="relative h-full w-full object-contain"
1313
@click.stop
1414
@play="onVideoPlay"
1515
@pause="onVideoPause"
16+
@ended="onVideoEnded"
1617
>
1718
<source :src="asset.src || ''" />
1819
</video>
1920
</div>
2021
</template>
2122

2223
<script setup lang="ts">
23-
import { onMounted, ref, watch } from 'vue'
24+
import { computed, onMounted, ref, watch } from 'vue'
2425
2526
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
2627
@@ -36,22 +37,32 @@ const emit = defineEmits<{
3637
}>()
3738
3839
const videoRef = ref<HTMLVideoElement>()
39-
const showControls = ref(true)
40+
const isHovered = ref(false)
41+
const isPlaying = ref(false)
4042
41-
watch(showControls, (controlsVisible) => {
43+
// Always show controls when not playing, hide/show based on hover when playing
44+
const shouldShowControls = computed(() => !isPlaying.value || isHovered.value)
45+
46+
watch(shouldShowControls, (controlsVisible) => {
4247
emit('videoControlsChanged', controlsVisible)
4348
})
4449
4550
onMounted(() => {
46-
emit('videoControlsChanged', showControls.value)
51+
emit('videoControlsChanged', shouldShowControls.value)
4752
})
4853
4954
const onVideoPlay = () => {
50-
showControls.value = true
55+
isPlaying.value = true
5156
emit('videoPlayingStateChanged', true)
5257
}
5358
5459
const onVideoPause = () => {
60+
isPlaying.value = false
61+
emit('videoPlayingStateChanged', false)
62+
}
63+
64+
const onVideoEnded = () => {
65+
isPlaying.value = false
5566
emit('videoPlayingStateChanged', false)
5667
}
5768
</script>

src/platform/assets/composables/useMediaAssetActions.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ export function useMediaAssetActions() {
66
console.log('Asset selected:', asset)
77
}
88

9-
const viewAsset = (assetId: string) => {
10-
console.log('Viewing asset:', assetId)
11-
}
12-
139
const downloadAsset = (assetId: string) => {
1410
console.log('Downloading asset:', assetId)
1511
}
@@ -48,7 +44,6 @@ export function useMediaAssetActions() {
4844

4945
return {
5046
selectAsset,
51-
viewAsset,
5247
downloadAsset,
5348
deleteAsset,
5449
playAsset,

0 commit comments

Comments
 (0)