feat: add clickable file name bar with full-file playback
- Add card-file-info bar at top of search result cards - Click file name to play full file (startFrame: 0, endFrame: 0) - Show video/document icon based on file extension - Display file UUID badge - Remove duplicate card-file section from bottom of card
This commit is contained in:
@@ -119,7 +119,14 @@
|
|||||||
|
|
||||||
<div v-if="msg.results.length" class="card-grid">
|
<div v-if="msg.results.length" class="card-grid">
|
||||||
<div v-for="(r, j) in msg.results" :key="j" class="result-card" :class="{ 'ocr-grouped': r.ocr_frames && r.ocr_frames.length > 1 }">
|
<div v-for="(r, j) in msg.results" :key="j" class="result-card" :class="{ 'ocr-grouped': r.ocr_frames && r.ocr_frames.length > 1 }">
|
||||||
<div class="card-thumb" @click="playVideo(r)">
|
<!-- File info bar at top-left -->
|
||||||
|
<div class="card-file-info" @click.stop="playFullFile(r)">
|
||||||
|
<svg v-if="isVideoFile(r)" class="card-file-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||||
|
<svg v-else class="card-file-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
<span class="card-file-name">{{ r.file_name || 'Unknown' }}</span>
|
||||||
|
<span class="card-file-uuid-badge">{{ r.file_uuid }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-thumb" @click="playVideo(r)">
|
||||||
<img v-if="r.thumbnail_url" :src="r.thumbnail_url" alt="" loading="lazy">
|
<img v-if="r.thumbnail_url" :src="r.thumbnail_url" alt="" loading="lazy">
|
||||||
<img v-else-if="thumbs[r.file_uuid + ':' + r.start_frame]" :src="thumbs[r.file_uuid + ':' + r.start_frame]" alt="" loading="lazy">
|
<img v-else-if="thumbs[r.file_uuid + ':' + r.start_frame]" :src="thumbs[r.file_uuid + ':' + r.start_frame]" alt="" loading="lazy">
|
||||||
<div v-else class="thumb-placeholder" v-observe="() => loadThumb(r.file_uuid, r.start_frame || 0)">
|
<div v-else class="thumb-placeholder" v-observe="() => loadThumb(r.file_uuid, r.start_frame || 0)">
|
||||||
@@ -149,8 +156,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="r._isPerson" class="card-text card-person-stats">{{ r.text_content }}</div>
|
<div v-if="r._isPerson" class="card-text card-person-stats">{{ r.text_content }}</div>
|
||||||
<div v-if="r.text_content && !r._isPerson" class="card-text" v-html="highlightMatch(r.text_content.slice(0, 120), r.search_query)"></div>
|
<div v-if="r.text_content && !r._isPerson" class="card-text" v-html="highlightMatch(r.text_content.slice(0, 120), r.search_query)"></div>
|
||||||
<div class="card-file" v-if="!r._isPerson">{{ r.file_name || '' }}<br><span class="card-file-uuid">{{ r.file_uuid }}</span></div>
|
|
||||||
<div class="card-file card-person-file" v-if="r._isPerson">{{ r.display_file_name }}<br><span class="card-file-uuid">{{ r.file_uuid }}</span></div>
|
|
||||||
<div class="card-meta">
|
<div class="card-meta">
|
||||||
<span v-if="!r.ocr_frames && r.start_frame" class="card-frame">F{{ r.start_frame }}–{{ r.end_frame }}</span>
|
<span v-if="!r.ocr_frames && r.start_frame" class="card-frame">F{{ r.start_frame }}–{{ r.end_frame }}</span>
|
||||||
<span v-if="r.start_time != null" class="card-time">({{ formatTime(r.start_time) }}–{{ formatTime(r.end_time) }})</span>
|
<span v-if="r.start_time != null" class="card-time">({{ formatTime(r.start_time) }}–{{ formatTime(r.end_time) }})</span>
|
||||||
@@ -1517,6 +1522,22 @@ function playVideoAtFrame(r: any, frame: number) {
|
|||||||
playing.value = true
|
playing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playFullFile(r: any) {
|
||||||
|
currentVideo.value = {
|
||||||
|
fileUuid: r.file_uuid,
|
||||||
|
startFrame: 0,
|
||||||
|
endFrame: 0, // 0 = play to end
|
||||||
|
title: r.file_name || 'Full Video'
|
||||||
|
}
|
||||||
|
playing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVideoFile(r: any): boolean {
|
||||||
|
if (!r.file_name) return false
|
||||||
|
const ext = r.file_name.split('.').pop()?.toLowerCase()
|
||||||
|
return ['mp4', 'mov', 'mkv', 'avi', 'webm'].includes(ext || '')
|
||||||
|
}
|
||||||
|
|
||||||
function isVariantFrame(r: any, frame: number): boolean {
|
function isVariantFrame(r: any, frame: number): boolean {
|
||||||
return r.variants?.some((v: any) => v.frames.includes(frame))
|
return r.variants?.some((v: any) => v.frames.includes(frame))
|
||||||
}
|
}
|
||||||
@@ -2068,6 +2089,26 @@ function openSource(src: SourceInfo) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.card-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
.card-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.card-file-info {
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
padding: 5px 8px; background: var(--card-background);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
cursor: pointer; transition: background .15s;
|
||||||
|
}
|
||||||
|
.card-file-info:hover {
|
||||||
|
background: rgba(var(--primary-color-rgb), 0.1);
|
||||||
|
}
|
||||||
|
.card-file-icon {
|
||||||
|
flex-shrink: 0; color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.card-file-name {
|
||||||
|
font-size: 11px; font-weight: 600; color: var(--text-primary);
|
||||||
|
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-file-uuid-badge {
|
||||||
|
font-size: 8px; color: var(--muted-text); font-family: monospace;
|
||||||
|
max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
.ocr-group-badge {
|
.ocr-group-badge {
|
||||||
position: absolute; top: 6px; right: 6px;
|
position: absolute; top: 6px; right: 6px;
|
||||||
background: rgba(var(--primary-color-rgb), 0.85); color: #fff;
|
background: rgba(var(--primary-color-rgb), 0.85); color: #fff;
|
||||||
|
|||||||
Reference in New Issue
Block a user