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:
138
src/composables/useSearchHistory.ts
Normal file
138
src/composables/useSearchHistory.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ref } from 'vue'
|
||||
import { apiCall } from '@/api'
|
||||
|
||||
export interface HistoryItem {
|
||||
id: string
|
||||
query: string
|
||||
title: string
|
||||
chat_state: string | null
|
||||
mode: string | null
|
||||
pinned: boolean
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export function useSearchHistory() {
|
||||
const history = ref<HistoryItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadHistory() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await apiCall('get_search_history', { limit: 30 })
|
||||
history.value = Array.isArray(result) ? result : []
|
||||
} catch (e) {
|
||||
console.error('Failed to load history:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToHistory(query: string, chatState: string, mode: string) {
|
||||
const id = `h_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
try {
|
||||
await apiCall('save_search_history', {
|
||||
id,
|
||||
query,
|
||||
title: query,
|
||||
chatState,
|
||||
mode,
|
||||
})
|
||||
await loadHistory()
|
||||
} catch (e) {
|
||||
console.error('Failed to save history:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function renameHistory(id: string, title: string) {
|
||||
try {
|
||||
await apiCall('rename_search_history', { id, title })
|
||||
await loadHistory()
|
||||
} catch (e) {
|
||||
console.error('Failed to rename history:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function pinHistory(id: string, pinned: boolean) {
|
||||
try {
|
||||
await apiCall('pin_search_history', { id, pinned })
|
||||
await loadHistory()
|
||||
} catch (e) {
|
||||
console.error('Failed to pin history:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHistory(id: string) {
|
||||
try {
|
||||
await apiCall('delete_search_history', { id })
|
||||
await loadHistory()
|
||||
} catch (e) {
|
||||
console.error('Failed to delete history:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function restoreFromHistory(item: HistoryItem): any[] | null {
|
||||
if (!item.chat_state) return null
|
||||
try {
|
||||
return JSON.parse(item.chat_state)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
history,
|
||||
loading,
|
||||
loadHistory,
|
||||
saveToHistory,
|
||||
renameHistory,
|
||||
pinHistory,
|
||||
deleteHistory,
|
||||
restoreFromHistory,
|
||||
}
|
||||
}
|
||||
|
||||
export interface BookmarkItem {
|
||||
id: number
|
||||
label: string
|
||||
history_id: string | null
|
||||
created_at: string | null
|
||||
}
|
||||
|
||||
export function useBookmarks() {
|
||||
const bookmarks = ref<BookmarkItem[]>([])
|
||||
|
||||
async function loadBookmarks() {
|
||||
try {
|
||||
const result = await apiCall('get_bookmarks', {})
|
||||
bookmarks.value = Array.isArray(result) ? result : []
|
||||
} catch (e) {
|
||||
console.error('Failed to load bookmarks:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBookmark(label: string, historyId?: string) {
|
||||
try {
|
||||
await apiCall('save_bookmark', { label, historyId: historyId || null })
|
||||
await loadBookmarks()
|
||||
} catch (e) {
|
||||
console.error('Failed to save bookmark:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBookmark(id: number) {
|
||||
try {
|
||||
await apiCall('delete_bookmark', { id })
|
||||
await loadBookmarks()
|
||||
} catch (e) {
|
||||
console.error('Failed to delete bookmark:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bookmarks,
|
||||
loadBookmarks,
|
||||
saveBookmark,
|
||||
deleteBookmark,
|
||||
}
|
||||
}
|
||||
253
src/composables/useUndoRedo.ts
Normal file
253
src/composables/useUndoRedo.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { reactive } from 'vue'
|
||||
import { apiCall } from '@/api'
|
||||
import { invalidatePeople, ensurePeople } from '@/store'
|
||||
|
||||
interface HistoryCounts {
|
||||
patchUndo: number
|
||||
patchRedo: number
|
||||
bindUndo: number
|
||||
bindRedo: number
|
||||
}
|
||||
|
||||
interface MergeHistoryItem {
|
||||
mergeId: string
|
||||
sourceUuid: string
|
||||
targetUuid: string
|
||||
sourceName: string
|
||||
targetName: string
|
||||
undone: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface ActionHistoryItem {
|
||||
type: 'patch' | 'bind' | 'merge' | 'delete'
|
||||
label: string
|
||||
uuid: string
|
||||
timestamp: string
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
}
|
||||
|
||||
export function useUndoRedo() {
|
||||
const counts = reactive<Record<string, HistoryCounts>>({})
|
||||
const mergeHistory = reactive<MergeHistoryItem[]>([])
|
||||
const recentActions = reactive<ActionHistoryItem[]>([])
|
||||
|
||||
async function refreshCounts(uuid: string) {
|
||||
try {
|
||||
const [patchHist, bindHist] = await Promise.all([
|
||||
apiCall('identity_history', { uuid, page: 1, pageSize: 1 }),
|
||||
apiCall('identity_bind_history', { uuid, page: 1, pageSize: 1 }),
|
||||
])
|
||||
if (!counts[uuid]) counts[uuid] = { patchUndo: 0, patchRedo: 0, bindUndo: 0, bindRedo: 0 }
|
||||
counts[uuid].patchUndo = patchHist?.undo_stack_count ?? patchHist?.undoStackCount ?? 0
|
||||
counts[uuid].patchRedo = patchHist?.redo_stack_count ?? patchHist?.redoStackCount ?? 0
|
||||
counts[uuid].bindUndo = bindHist?.undo_stack_count ?? bindHist?.undoStackCount ?? 0
|
||||
counts[uuid].bindRedo = bindHist?.redo_stack_count ?? bindHist?.redoStackCount ?? 0
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh undo counts:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMergeHistory(uuid?: string) {
|
||||
try {
|
||||
const params: Record<string, any> = { pageSize: 10 }
|
||||
if (uuid) params.sourceUuid = uuid
|
||||
const data = await apiCall('merge_history', params)
|
||||
const items = data?.history || data?.merges || data || []
|
||||
mergeHistory.splice(0, mergeHistory.length)
|
||||
for (const m of Array.isArray(items) ? items : []) {
|
||||
mergeHistory.push({
|
||||
mergeId: m.merge_id ?? m.mergeId ?? '',
|
||||
sourceUuid: m.source_uuid ?? m.sourceUuid ?? '',
|
||||
targetUuid: m.target_uuid ?? m.targetUuid ?? '',
|
||||
sourceName: m.source_name ?? m.sourceName ?? '',
|
||||
targetName: m.target_name ?? m.targetName ?? '',
|
||||
undone: m.is_undone ?? m.isUndone ?? false,
|
||||
createdAt: m.created_at ?? m.createdAt ?? '',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh merge history:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshActionHistory(uuid: string) {
|
||||
try {
|
||||
const [patchHist, bindHist] = await Promise.all([
|
||||
apiCall('identity_history', { uuid, page: 1, pageSize: 10 }),
|
||||
apiCall('identity_bind_history', { uuid, page: 1, pageSize: 10 }),
|
||||
])
|
||||
const actions: ActionHistoryItem[] = []
|
||||
const patchItems = patchHist?.history || patchHist?.records || []
|
||||
for (const r of Array.isArray(patchItems) ? patchItems : []) {
|
||||
const op = r.operation ?? r.op ?? ''
|
||||
const label = op === 'delete' ? '刪除人物' : op === 'update' ? '更新人物' : op
|
||||
actions.push({
|
||||
type: op === 'delete' ? 'delete' : 'patch',
|
||||
label,
|
||||
uuid,
|
||||
timestamp: r.created_at ?? r.createdAt ?? r.timestamp ?? '',
|
||||
canUndo: !r.is_undone && !r.isUndone,
|
||||
canRedo: !!(r.is_undone || r.isUndone),
|
||||
})
|
||||
}
|
||||
const bindItems = bindHist?.history || bindHist?.records || []
|
||||
for (const r of Array.isArray(bindItems) ? bindItems : []) {
|
||||
const op = r.operation ?? r.op ?? ''
|
||||
const label = op === 'bind' ? '綁定面部' : op === 'unbind' ? '解綁面部' : op === 'bind_trace' ? '綁定軌跡' : op
|
||||
actions.push({
|
||||
type: 'bind',
|
||||
label,
|
||||
uuid,
|
||||
timestamp: r.created_at ?? r.createdAt ?? r.timestamp ?? '',
|
||||
canUndo: !(r.is_undone ?? r.isUndone),
|
||||
canRedo: !!(r.is_undone ?? r.isUndone),
|
||||
})
|
||||
}
|
||||
actions.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
||||
recentActions.splice(0, recentActions.length, ...actions.slice(0, 20))
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh action history:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function undo(uuid: string, steps = 1) {
|
||||
try {
|
||||
await apiCall('identity_undo', { uuid, steps })
|
||||
await refreshCounts(uuid)
|
||||
await refreshActionHistory(uuid)
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Undo failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function redo(uuid: string, steps = 1) {
|
||||
try {
|
||||
await apiCall('identity_redo', { uuid, steps })
|
||||
await refreshCounts(uuid)
|
||||
await refreshActionHistory(uuid)
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Redo failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function bindUndo(uuid: string, steps = 1) {
|
||||
try {
|
||||
await apiCall('identity_bind_undo', { uuid, steps })
|
||||
await refreshCounts(uuid)
|
||||
await refreshActionHistory(uuid)
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Bind undo failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function bindRedo(uuid: string, steps = 1) {
|
||||
try {
|
||||
await apiCall('identity_bind_redo', { uuid, steps })
|
||||
await refreshCounts(uuid)
|
||||
await refreshActionHistory(uuid)
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Bind redo failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeUndo(mergeId: string) {
|
||||
try {
|
||||
await apiCall('merge_undo', { mergeId })
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Merge undo failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeRedo(mergeId: string) {
|
||||
try {
|
||||
await apiCall('merge_redo', { mergeId })
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Merge redo failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function canUndo(uuid: string): boolean {
|
||||
const c = counts[uuid]
|
||||
if (!c) return false
|
||||
return c.patchUndo > 0 || c.bindUndo > 0
|
||||
}
|
||||
|
||||
function canRedo(uuid: string): boolean {
|
||||
const c = counts[uuid]
|
||||
if (!c) return false
|
||||
return c.patchRedo > 0 || c.bindRedo > 0
|
||||
}
|
||||
|
||||
async function undoAction(uuid: string, type: 'patch' | 'bind') {
|
||||
try {
|
||||
if (type === 'bind') {
|
||||
await apiCall('identity_bind_undo', { uuid, steps: 1 })
|
||||
} else {
|
||||
await apiCall('identity_undo', { uuid, steps: 1 })
|
||||
}
|
||||
await refreshCounts(uuid)
|
||||
await refreshActionHistory(uuid)
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Undo action failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function redoAction(uuid: string, type: 'patch' | 'bind') {
|
||||
try {
|
||||
if (type === 'bind') {
|
||||
await apiCall('identity_bind_redo', { uuid, steps: 1 })
|
||||
} else {
|
||||
await apiCall('identity_redo', { uuid, steps: 1 })
|
||||
}
|
||||
await refreshCounts(uuid)
|
||||
await refreshActionHistory(uuid)
|
||||
invalidatePeople()
|
||||
await ensurePeople()
|
||||
} catch (e) {
|
||||
console.error('Redo action failed:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
counts,
|
||||
mergeHistory,
|
||||
recentActions,
|
||||
refreshCounts,
|
||||
refreshMergeHistory,
|
||||
refreshActionHistory,
|
||||
undo,
|
||||
redo,
|
||||
bindUndo,
|
||||
bindRedo,
|
||||
mergeUndo,
|
||||
mergeRedo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
undoAction,
|
||||
redoAction,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user