Initial commit: Momentry Studio v0.1.0
This commit is contained in:
142
src/views/PeopleView.vue
Normal file
142
src/views/PeopleView.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="people-view">
|
||||
<h1>People</h1>
|
||||
<input v-model="search" class="search-input" placeholder="Search people..." />
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<div v-else class="grid">
|
||||
<div v-for="p in filteredPeople" :key="p.identity_uuid" class="person-card" @click="selectPerson(p)">
|
||||
<div class="avatar">{{ p.name?.[0]?.toUpperCase() || '?' }}</div>
|
||||
<p class="name">{{ p.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selected" class="detail-modal" @click.self="selected = null">
|
||||
<div class="detail-box">
|
||||
<button class="close-btn" @click="selected = null">×</button>
|
||||
<div class="detail-header">
|
||||
<div class="detail-avatar">{{ selected.name?.[0]?.toUpperCase() || '?' }}</div>
|
||||
<div><h2>{{ selected.name }}</h2><p class="uuid">{{ selected.identity_uuid }}</p></div>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="section"><h3>Faces ({{ faces.length }})</h3>
|
||||
<div class="face-strip">
|
||||
<div v-for="f in faces.slice(0, 20)" :key="f.id" class="face-thumb">
|
||||
<div class="face-placeholder">{{ Math.round(f.confidence * 100) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section"><h3>Traces ({{ traces.length }})</h3>
|
||||
<div class="trace-list">
|
||||
<div v-for="t in traces.slice(0, 20)" :key="t.trace_id" class="trace-item" @click="playTrace(t)">
|
||||
<div class="trace-thumb">▶</div>
|
||||
<div class="trace-info">
|
||||
<span class="trace-time">{{ formatTime(t.first_sec) }} - {{ formatTime(t.last_sec) }}</span>
|
||||
<span class="trace-conf">{{ (t.avg_confidence * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VideoPlayer
|
||||
v-if="playing"
|
||||
:file-uuid="currentVideo.fileUuid"
|
||||
:start-time="currentVideo.startTime"
|
||||
:end-time="currentVideo.endTime"
|
||||
:title="currentVideo.title"
|
||||
@close="playing = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import VideoPlayer from '../components/VideoPlayer.vue'
|
||||
|
||||
const people = ref<any[]>([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const selected = ref<any>(null)
|
||||
const faces = ref<any[]>([])
|
||||
const traces = ref<any[]>([])
|
||||
const playing = ref(false)
|
||||
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, title: '' })
|
||||
|
||||
const filteredPeople = computed(() => {
|
||||
if (!search.value) return people.value
|
||||
const s = search.value.toLowerCase()
|
||||
return people.value.filter((p: any) => p.name.toLowerCase().includes(s))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result: any = await invoke('get_people', { page: 1, perPage: 1000 })
|
||||
people.value = Array.isArray(result) ? result : []
|
||||
} catch (e) {
|
||||
console.error('Failed to load people:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function selectPerson(p: any) {
|
||||
selected.value = p
|
||||
try {
|
||||
const fResult: any = await invoke('get_faces', { uuid: p.identity_uuid, perPage: 100 })
|
||||
const tResult: any = await invoke('get_traces', { uuid: p.identity_uuid, perPage: 100 })
|
||||
faces.value = Array.isArray(fResult) ? fResult : []
|
||||
traces.value = Array.isArray(tResult) ? tResult : []
|
||||
} catch (e) {
|
||||
console.error('Failed to load details:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(sec: number): string {
|
||||
const m = Math.floor(sec / 60)
|
||||
const s = Math.floor(sec % 60)
|
||||
return `${m}:${s.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function playTrace(t: any) {
|
||||
currentVideo.value = {
|
||||
fileUuid: t.file_uuid,
|
||||
startTime: t.first_sec,
|
||||
endTime: t.last_sec,
|
||||
title: `${selected.value?.name} - ${formatTime(t.first_sec)}-${formatTime(t.last_sec)}`
|
||||
}
|
||||
playing.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.people-view { max-width: 1200px; }
|
||||
h1 { margin-bottom: 20px; }
|
||||
.search-input { padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 8px; width: 300px; margin-bottom: 20px; }
|
||||
.loading { text-align: center; padding: 40px; color: #6b7280; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 16px; }
|
||||
.person-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; text-align: center; cursor: pointer; }
|
||||
.person-card:hover { border-color: #4f46e5; }
|
||||
.avatar { width: 56px; height: 56px; border-radius: 50%; background: #eef2ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 600; margin: 0 auto 8px; }
|
||||
.name { font-size: 0.75rem; color: #374151; }
|
||||
.detail-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.detail-box { background: #fff; border-radius: 20px; padding: 32px; width: min(700px, 90vw); max-height: 80vh; overflow-y: auto; position: relative; }
|
||||
.close-btn { position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%; background: #f3f4f6; border: none; font-size: 1.2rem; cursor: pointer; }
|
||||
.detail-header { display: flex; align-items: center; gap: 20px; margin-bottom: 24px; }
|
||||
.detail-avatar { width: 80px; height: 80px; border-radius: 20px; background: #eef2ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: 700; }
|
||||
.detail-header h2 { margin-bottom: 4px; }
|
||||
.uuid { color: #9ca3af; font-size: 0.8rem; font-family: monospace; }
|
||||
.section { margin-bottom: 24px; }
|
||||
.section h3 { margin-bottom: 12px; font-size: 1rem; }
|
||||
.face-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; }
|
||||
.face-thumb { width: 52px; height: 52px; border-radius: 8px; background: #e5e7eb; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.face-placeholder { font-size: 0.6rem; color: #6b7280; }
|
||||
.trace-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.trace-item { display: flex; align-items: center; gap: 12px; padding: 8px 12px; background: #f9fafb; border-radius: 8px; cursor: pointer; }
|
||||
.trace-item:hover { background: #eef2ff; }
|
||||
.trace-thumb { color: #4f46e5; }
|
||||
.trace-info { display: flex; gap: 12px; flex: 1; }
|
||||
.trace-time { color: #374151; font-size: 0.85rem; }
|
||||
.trace-conf { color: #4f46e5; font-weight: 500; font-size: 0.85rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user