feat: bind/unbind by id, video streaming proxy, unmute default, library refresh, hamburger click-outside
- Core API: bind/unbind accept id (integer PK) when face_id is null - Tauri proxy: stream video responses instead of buffering for seek bar - VideoPlayer: remove muted attribute, unmute by default - LibraryView: call invalidateFiles() before ensureFiles() on register/unregister - PeopleView: close sort panel on click-outside, not just hamburger toggle - bind_face: prefer face_id, fallback to id when face_id is null
This commit is contained in:
221
src/store.ts
Normal file
221
src/store.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { ref } from 'vue'
|
||||
import { apiCall } from '@/api'
|
||||
import { isTauri } from './api/config'
|
||||
|
||||
export const filesCache = ref<any[]>([])
|
||||
export const filesLoaded = ref(false)
|
||||
|
||||
export const peopleCache = ref<any[]>([])
|
||||
export const peopleLoaded = ref(false)
|
||||
|
||||
export const faceCandidatesCache = ref<any[]>([])
|
||||
export const faceCandidatesLoaded = ref(false)
|
||||
|
||||
export const thumbnailsCache = ref<Record<string, string>>({})
|
||||
export const profilesCache = ref<Record<string, string>>({})
|
||||
export const faceThumbsCache = ref<Record<string, string>>({})
|
||||
|
||||
const thumbQueue: (() => Promise<void>)[] = []
|
||||
let activeThumbLoads = 0
|
||||
const MAX_CONCURRENT = 16
|
||||
|
||||
const profileQueue: (() => Promise<void>)[] = []
|
||||
let activeProfileLoads = 0
|
||||
const MAX_PROFILE_CONCURRENT = 4
|
||||
|
||||
const loadingThumbs = new Set<string>()
|
||||
const loadingProfiles = new Set<string>()
|
||||
const loadingFaceThumbs = new Set<string>()
|
||||
|
||||
function drainThumbQueue() {
|
||||
if (activeThumbLoads >= MAX_CONCURRENT || thumbQueue.length === 0) return
|
||||
activeThumbLoads++
|
||||
const task = thumbQueue.shift()!
|
||||
task().then(() => {
|
||||
activeThumbLoads--
|
||||
drainThumbQueue()
|
||||
}).catch(() => {
|
||||
activeThumbLoads--
|
||||
drainThumbQueue()
|
||||
})
|
||||
}
|
||||
|
||||
function drainProfileQueue() {
|
||||
if (activeProfileLoads >= MAX_PROFILE_CONCURRENT || profileQueue.length === 0) return
|
||||
activeProfileLoads++
|
||||
const task = profileQueue.shift()!
|
||||
task().then(() => {
|
||||
activeProfileLoads--
|
||||
drainProfileQueue()
|
||||
}).catch(() => {
|
||||
activeProfileLoads--
|
||||
drainProfileQueue()
|
||||
})
|
||||
}
|
||||
|
||||
function queueThumb(fn: () => Promise<void>) {
|
||||
thumbQueue.push(fn)
|
||||
drainThumbQueue()
|
||||
}
|
||||
|
||||
function queueProfile(fn: () => Promise<void>) {
|
||||
profileQueue.push(fn)
|
||||
drainProfileQueue()
|
||||
}
|
||||
|
||||
let _peopleLoading = false
|
||||
let _filesLoading = false
|
||||
let _faceCandidatesLoading = false
|
||||
|
||||
export async function ensureFiles() {
|
||||
if (filesLoaded.value) return
|
||||
if (_filesLoading) {
|
||||
await new Promise<void>(r => { const check = setInterval(() => { if (filesLoaded.value || !_filesLoading) { clearInterval(check); r() } }, 100) })
|
||||
return
|
||||
}
|
||||
_filesLoading = true
|
||||
try {
|
||||
const result = await apiCall('get_files', { args: { pageSize: 500 } })
|
||||
filesCache.value = Array.isArray(result) ? result : []
|
||||
} catch (e) {
|
||||
console.error('Failed to load files:', e)
|
||||
} finally {
|
||||
_filesLoading = false
|
||||
filesLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensurePeople() {
|
||||
if (peopleLoaded.value) return
|
||||
if (_peopleLoading) {
|
||||
await new Promise<void>(r => { const check = setInterval(() => { if (peopleLoaded.value || !_peopleLoading) { clearInterval(check); r() } }, 100) })
|
||||
return
|
||||
}
|
||||
_peopleLoading = true
|
||||
try {
|
||||
if (isTauri) {
|
||||
const result: any = await apiCall('get_people', { page: 1, perPage: 100 })
|
||||
peopleCache.value = Array.isArray(result) ? result : []
|
||||
} else {
|
||||
const allPeople: any[] = []
|
||||
for (let page = 1; page <= 5; page++) {
|
||||
const batch: any = await apiCall('get_people', { page, perPage: 20 })
|
||||
const arr = Array.isArray(batch) ? batch : []
|
||||
if (!arr.length) break
|
||||
allPeople.push(...arr)
|
||||
if (arr.length < 20) break
|
||||
}
|
||||
peopleCache.value = allPeople
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load people:', e)
|
||||
} finally {
|
||||
_peopleLoading = false
|
||||
peopleLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureFaceCandidates() {
|
||||
if (faceCandidatesLoaded.value) return
|
||||
if (_faceCandidatesLoading) {
|
||||
await new Promise<void>(r => { const check = setInterval(() => { if (faceCandidatesLoaded.value || !_faceCandidatesLoading) { clearInterval(check); r() } }, 100) })
|
||||
return
|
||||
}
|
||||
_faceCandidatesLoading = true
|
||||
try {
|
||||
if (isTauri) {
|
||||
const fc: any = await apiCall('get_face_candidates', { page: 1, perPage: 100 })
|
||||
faceCandidatesCache.value = Array.isArray(fc) ? fc : []
|
||||
} else {
|
||||
const all: any[] = []
|
||||
for (let page = 1; page <= 5; page++) {
|
||||
const batch: any = await apiCall('get_face_candidates', { page, perPage: 20 })
|
||||
const arr = Array.isArray(batch) ? batch : []
|
||||
if (!arr.length) break
|
||||
all.push(...arr)
|
||||
if (arr.length < 20) break
|
||||
}
|
||||
faceCandidatesCache.value = all
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load face candidates:', e)
|
||||
} finally {
|
||||
_faceCandidatesLoading = false
|
||||
faceCandidatesLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
export function loadThumbnail(uuid: string, frame = 30) {
|
||||
const key = `${uuid}:${frame}`
|
||||
if (!uuid || thumbnailsCache.value[key] || loadingThumbs.has(key)) return
|
||||
loadingThumbs.add(key)
|
||||
queueThumb(async () => {
|
||||
try {
|
||||
const result = await apiCall('get_thumbnail', { uuid, frame })
|
||||
if (result) thumbnailsCache.value[key] = result
|
||||
} catch (e) {
|
||||
console.error('loadThumbnail failed:', uuid, frame, e)
|
||||
} finally {
|
||||
loadingThumbs.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function loadProfile(uuid: string) {
|
||||
if (!uuid) { console.error('[loadProfile] called with empty uuid'); return }
|
||||
if (profilesCache.value[uuid]) return
|
||||
if (loadingProfiles.has(uuid)) return
|
||||
loadingProfiles.add(uuid)
|
||||
console.error('[loadProfile] requesting', uuid)
|
||||
queueProfile(async () => {
|
||||
try {
|
||||
const result = await apiCall('get_identity_profile', { uuid })
|
||||
console.error('[loadProfile] got result for', uuid, typeof result, result ? (result as string).substring(0, 30) : 'null')
|
||||
if (result) profilesCache.value[uuid] = result
|
||||
else console.error('[loadProfile] empty result for', uuid)
|
||||
} catch (e) {
|
||||
console.error('loadProfile failed:', uuid, e)
|
||||
} finally {
|
||||
loadingProfiles.delete(uuid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function loadFaceThumb(key: string, uuid: string, frame: number, bbox?: any) {
|
||||
if (!uuid || faceThumbsCache.value[key] || loadingFaceThumbs.has(key)) return
|
||||
loadingFaceThumbs.add(key)
|
||||
queueThumb(async () => {
|
||||
try {
|
||||
const args: any = { uuid, frame }
|
||||
if (bbox) {
|
||||
args.bboxX = Math.round(bbox.x)
|
||||
args.bboxY = Math.round(bbox.y)
|
||||
args.bboxW = Math.round(bbox.width)
|
||||
args.bboxH = Math.round(bbox.height)
|
||||
}
|
||||
const result = await apiCall('get_face_thumbnail', args)
|
||||
if (result) faceThumbsCache.value[key] = result
|
||||
} catch (e) {
|
||||
console.error('loadFaceThumb failed:', key, uuid, e)
|
||||
} finally {
|
||||
loadingFaceThumbs.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function invalidateFiles() {
|
||||
filesLoaded.value = false
|
||||
filesCache.value = []
|
||||
}
|
||||
|
||||
export function invalidatePeople() {
|
||||
peopleLoaded.value = false
|
||||
peopleCache.value = []
|
||||
faceCandidatesLoaded.value = false
|
||||
faceCandidatesCache.value = []
|
||||
}
|
||||
|
||||
export function invalidateProfile(uuid: string) {
|
||||
delete profilesCache.value[uuid]
|
||||
loadingProfiles.delete(uuid)
|
||||
}
|
||||
Reference in New Issue
Block a user