feat: clickable file name in video player for full file playback

- File name in header is now clickable
- Click reloads video with full file range (startFrame=0, endFrame=999999)
- Added play icon and hover effect for visual feedback
This commit is contained in:
2026-10-21 01:32:23 +08:00
parent fc32fb0f86
commit 3b2c7c65ae

View File

@@ -3,7 +3,10 @@
<div class="ms-modal ms-modal-video" :class="{ 'ms-video-ui-idle': uiIdle }" @mouseenter="showUi" @mousemove="showUi" @mouseleave="hideUi"> <div class="ms-modal ms-modal-video" :class="{ 'ms-video-ui-idle': uiIdle }" @mouseenter="showUi" @mousemove="showUi" @mouseleave="hideUi">
<div class="ms-modal-video-header"> <div class="ms-modal-video-header">
<div class="ms-video-header-row"> <div class="ms-video-header-row">
<h3 v-if="actualFileName" class="ms-video-file-name">{{ actualFileName }}</h3> <h3 v-if="actualFileName" class="ms-video-file-name" @click="playFullFile" title="Click to play full file">
{{ actualFileName }}
<svg class="ms-video-fullfile-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</h3>
<button class="ms-modal-video-close" @click="close">&times;</button> <button class="ms-modal-video-close" @click="close">&times;</button>
</div> </div>
<div class="ms-video-header-row ms-video-title-row"> <div class="ms-video-header-row ms-video-title-row">
@@ -1370,6 +1373,35 @@ function formatTime(sec: number): string {
return `${m}:${s.toString().padStart(2, '0')}` return `${m}:${s.toString().padStart(2, '0')}`
} }
async function playFullFile() {
videoLoading.value = true
videoError.value = ''
marks.value = marks.value.filter(m => m.source !== 'face')
try {
const data = await apiCall('get_video_stream', {
uuid: props.fileUuid,
startTime: null,
endTime: null,
startFrame: 0,
endFrame: 999999,
original: props.useOriginal,
})
if (typeof data === 'string') {
videoSrc.value = data
} else {
const blob = new Blob([new Uint8Array(data)], { type: 'video/mp4' })
videoSrc.value = URL.createObjectURL(blob)
}
frameOffset.value = 0
await nextTick()
videoEl.value?.load()
} catch (e: any) {
videoError.value = e?.message || 'Failed to load full video'
} finally {
videoLoading.value = false
}
}
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
if (!visible.value) return if (!visible.value) return
switch (e.key) { switch (e.key) {
@@ -1538,7 +1570,10 @@ onUnmounted(() => {
} }
.ms-modal-video-header { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } .ms-modal-video-header { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.ms-video-header-row { display: flex; align-items: center; justify-content: space-between; } .ms-video-header-row { display: flex; align-items: center; justify-content: space-between; }
.ms-video-file-name { font-size: 12px; font-weight: 500; color: var(--text-secondary); margin: 0; max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ms-video-file-name { font-size: 12px; font-weight: 500; color: var(--text-secondary); margin: 0; max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; }
.ms-video-file-name:hover { color: var(--primary-color); }
.ms-video-fullfile-icon { opacity: 0.5; transition: opacity .15s; }
.ms-video-file-name:hover .ms-video-fullfile-icon { opacity: 1; }
.ms-video-frame-info { font-size: 12px; color: var(--text-secondary); white-space: nowrap; flex-shrink: 0; } .ms-video-frame-info { font-size: 12px; color: var(--text-secondary); white-space: nowrap; flex-shrink: 0; }
.ms-video-keyframe { color: #ff9800; font-weight: 600; cursor: pointer; } .ms-video-keyframe { color: #ff9800; font-weight: 600; cursor: pointer; }
.ms-video-keyframe:hover { text-decoration: underline; } .ms-video-keyframe:hover { text-decoration: underline; }