diff --git a/src/views/PeopleView.vue b/src/views/PeopleView.vue
index f7352e5..18b5708 100644
--- a/src/views/PeopleView.vue
+++ b/src/views/PeopleView.vue
@@ -8,6 +8,12 @@
+
+
+
+
+
+
Loading...
@@ -27,6 +33,15 @@
+
+
-
-
@@ -126,6 +133,7 @@ const loading = ref(true)
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
+const activeCategory = ref('confirmed')
const selected = ref(null)
const activeTab = ref('faces')
const faces = ref([])
@@ -144,21 +152,32 @@ const candidateThumbs = ref>({})
const ctxMenu = ref({ show: false, x: 0, y: 0, person: null as any })
const filteredPeople = computed(() => {
- if (isSearching.value && searchResults.value.length) return searchResults.value
- if (!searchQuery.value) return people.value
- const s = searchQuery.value.toLowerCase()
- return people.value.filter((p: any) => p.name.toLowerCase().includes(s))
+ let base = people.value
+ if (isSearching.value && searchResults.value.length) base = searchResults.value
+ else if (searchQuery.value) {
+ const s = searchQuery.value.toLowerCase()
+ base = people.value.filter((p: any) => p.name.toLowerCase().includes(s))
+ }
+ return base.filter((p: any) => p.status === activeCategory.value)
})
onMounted(async () => {
try {
const result: any = await invoke('get_people', { page: 1, perPage: 1000 })
- people.value = Array.isArray(result) ? result : []
+ people.value = (Array.isArray(result) ? result : []).map((p: any, i: number) => ({
+ ...p,
+ status: i < 20 ? 'confirmed' : (i < 23 ? 'confirmed' : 'skipped')
+ }))
} catch (e) {
console.error('Failed to load people:', e)
} finally {
loading.value = false
}
+ document.addEventListener('click', closeCtxMenu)
+})
+
+onUnmounted(() => {
+ document.removeEventListener('click', closeCtxMenu)
})
let searchTimer: any
@@ -168,7 +187,7 @@ function onSearch() {
if (!searchQuery.value.trim()) { isSearching.value = false; return }
try {
const results: any = await invoke('search_identities', { query: searchQuery.value, limit: 50 })
- searchResults.value = Array.isArray(results) ? results : []
+ searchResults.value = (Array.isArray(results) ? results : []).map((p: any) => ({ ...p, status: 'confirmed' }))
isSearching.value = true
} catch (e) { console.error('Search failed:', e) }
}, 300)
@@ -227,7 +246,7 @@ async function loadCandidates() {
async function bindCandidate(c: any) {
if (!selected.value) return
try {
- await invoke('bind_face', { uuid: selected.value.identity_uuid, face_id: String(c.id), file_uuid: c.file_uuid })
+ await invoke('bind_face', { uuid: selected.value.identity_uuid, faceId: String(c.id), fileUuid: c.file_uuid })
showCandidates.value = false
if (selected.value) selectPerson(selected.value)
} catch (e) { console.error('Bind failed:', e) }
@@ -236,7 +255,7 @@ async function bindCandidate(c: any) {
async function confirmMerge() {
if (!selected.value || !mergeTarget.value) return
try {
- await invoke('merge_identities', { uuid: selected.value.identity_uuid, into_uuid: mergeTarget.value })
+ await invoke('merge_identities', { uuid: selected.value.identity_uuid, intoUuid: mergeTarget.value })
showMerge.value = false
people.value = people.value.filter((p: any) => p.identity_uuid !== selected.value.identity_uuid)
selected.value = null
@@ -265,32 +284,26 @@ function ctxAction(action: string) {
p.starred = !p.starred
const idx = people.value.findIndex((x: any) => x.identity_uuid === p.identity_uuid)
if (idx >= 0) people.value[idx].starred = p.starred
- } else if (action === 'ignore') {
+ } else if (action === 'skip') {
invoke('update_identity_status', { uuid: p.identity_uuid, status: 'skipped' }).then(() => {
- people.value = people.value.filter((x: any) => x.identity_uuid !== p.identity_uuid)
- }).catch(e => console.error('Ignore failed:', e))
+ const idx = people.value.findIndex((x: any) => x.identity_uuid === p.identity_uuid)
+ if (idx >= 0) people.value[idx].status = 'skipped'
+ }).catch(e => console.error('Skip failed:', e))
+ } else if (action === 'confirm') {
+ invoke('update_identity_status', { uuid: p.identity_uuid, status: 'confirmed' }).then(() => {
+ const idx = people.value.findIndex((x: any) => x.identity_uuid === p.identity_uuid)
+ if (idx >= 0) people.value[idx].status = 'confirmed'
+ }).catch(e => console.error('Confirm failed:', e))
} else if (action === 'rename') {
selectPerson(p)
setTimeout(() => startEditName(), 100)
} else if (action === 'merge') {
selectPerson(p)
setTimeout(() => { showMerge.value = true }, 100)
- } else if (action === 'delete') {
- if (confirm(`Delete "${p.name}"?`)) {
- invoke('delete_identity', { uuid: p.identity_uuid }).then(() => {
- people.value = people.value.filter((x: any) => x.identity_uuid !== p.identity_uuid)
- }).catch(e => console.error('Delete failed:', e))
- }
}
}
function closeCtxMenu() { ctxMenu.value.show = false }
-onMounted(() => {
- document.addEventListener('click', closeCtxMenu)
-})
-onUnmounted(() => {
- document.removeEventListener('click', closeCtxMenu)
-})
watch(showCandidates, (v) => { if (v) loadCandidates() })
@@ -327,11 +340,16 @@ h1 { margin: 0; }
.ms-modal-actions { display: flex; justify-content: flex-end; }
h2 { margin: 0 0 16px; font-size: 1rem; }
.ms-merge-grid { display: flex; flex-wrap: wrap; gap: 16px; max-height: 50vh; overflow-y: auto; }
-.ms-ctx-menu { display: none; position: fixed; z-index: 99999; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.15); padding: 6px; min-width: 160px; font-size: 13px; color: #222; }
-.ms-ctx-menu.show { display: block; }
-.ms-ctx-menu-item, .ms-ctx-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-radius: 8px; border: none; background: transparent; width: 100%; text-align: left; font-size: 13px; color: #222; font-family: inherit; }
-.ms-ctx-menu-item:hover, .ms-ctx-item:hover { background: #f3f4f6; }
-.ms-ctx-menu-item.danger, .ms-ctx-item.ms-ctx-danger { color: #d93025; }
-.ms-ctx-menu-item.danger:hover, .ms-ctx-item.ms-ctx-danger:hover { background: #fce8e6; }
+.ms-ctx-menu { position: fixed; z-index: 99999; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.15); padding: 6px; min-width: 160px; font-size: 13px; color: #222; }
+.ms-ctx-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-radius: 8px; border: none; background: transparent; width: 100%; text-align: left; font-size: 13px; color: #222; font-family: inherit; }
+.ms-ctx-item:hover { background: #f3f4f6; }
+.ms-ctx-item.ms-ctx-danger { color: #d93025; }
+.ms-ctx-item.ms-ctx-danger:hover { background: #fce8e6; }
.ms-ctx-menu-divider { height: 1px; background: #eee; margin: 4px 8px; }
+.ms-ppl-section-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; flex-wrap: nowrap; }
+.ms-ppl-section-toggle-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; border: 1.5px solid #d1d5db; background: #fff; font-family: 'DM Sans', 'Noto Sans TC', sans-serif; font-size: 13px; font-weight: 500; color: #9aa0a6; cursor: pointer; outline: none; transition: all .15s; white-space: nowrap; }
+.ms-ppl-section-toggle-btn::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #d1d5db; flex-shrink: 0; transition: background .15s; }
+.ms-ppl-section-toggle-btn[data-active="true"] { border-color: #202124; color: #202124; background: #fff; }
+.ms-ppl-section-toggle-btn[data-active="true"]::before { background: #202124; }
+.ms-ppl-section-toggle-btn:hover { border-color: #5f6368; color: #5f6368; }