SmoothTraining-FaceScanning
Adding visual clarity for duplicates detected
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
package com.placeholder.sherpai2.ui.modelinventory
|
package com.placeholder.sherpai2.ui.modelinventory
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -14,17 +13,19 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PersonInventoryScreen - SUPERCHARGED UI with warnings and improved buttons
|
* PersonInventoryScreen - Simplified to match corrected ViewModel
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Background scanning with navigation warnings
|
* - List of all persons with face models
|
||||||
* - Real-time performance stats (images/sec)
|
* - Scan button to find person in library
|
||||||
* - Fixed button layout (no text wrapping)
|
* - Real-time scanning progress
|
||||||
* - Progress tracking
|
* - Delete person functionality
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -32,53 +33,8 @@ fun PersonInventoryScreen(
|
|||||||
viewModel: PersonInventoryViewModel = hiltViewModel(),
|
viewModel: PersonInventoryViewModel = hiltViewModel(),
|
||||||
onNavigateToPersonDetail: (String) -> Unit
|
onNavigateToPersonDetail: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val personsWithModels by viewModel.personsWithModels.collectAsStateWithLifecycle()
|
||||||
val scanningState by viewModel.scanningState.collectAsState()
|
val scanningState by viewModel.scanningState.collectAsStateWithLifecycle()
|
||||||
val isScanningInBackground by viewModel.isScanningInBackground.collectAsState()
|
|
||||||
|
|
||||||
// Navigation warning dialog
|
|
||||||
var showNavigationWarning by remember { mutableStateOf(false) }
|
|
||||||
var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) }
|
|
||||||
|
|
||||||
// Intercept back button when scanning
|
|
||||||
BackHandler(enabled = isScanningInBackground) {
|
|
||||||
showNavigationWarning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation warning dialog
|
|
||||||
if (showNavigationWarning) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showNavigationWarning = false },
|
|
||||||
title = { Text("Scan in Progress") },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text("A face scanning operation is running in the background.")
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
"Leaving now will cancel the scan and you'll need to restart it.",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.cancelScan()
|
|
||||||
showNavigationWarning = false
|
|
||||||
pendingNavigation?.invoke()
|
|
||||||
pendingNavigation = null
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Cancel Scan & Leave", color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showNavigationWarning = false }) {
|
|
||||||
Text("Continue Scanning")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -86,9 +42,9 @@ fun PersonInventoryScreen(
|
|||||||
title = {
|
title = {
|
||||||
Column {
|
Column {
|
||||||
Text("People")
|
Text("People")
|
||||||
if (isScanningInBackground) {
|
if (scanningState is ScanningState.Scanning) {
|
||||||
Text(
|
Text(
|
||||||
"⚡ Scanning in background",
|
"⚡ Scanning...",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
@@ -103,69 +59,51 @@ fun PersonInventoryScreen(
|
|||||||
) { padding ->
|
) { padding ->
|
||||||
Column(Modifier.padding(padding)) {
|
Column(Modifier.padding(padding)) {
|
||||||
// Stats card
|
// Stats card
|
||||||
when (uiState) {
|
if (personsWithModels.isNotEmpty()) {
|
||||||
is PersonInventoryViewModel.InventoryUiState.Success -> {
|
StatsCard(personsWithModels)
|
||||||
StatsCard((uiState as PersonInventoryViewModel.InventoryUiState.Success).persons)
|
}
|
||||||
|
|
||||||
|
// Scanning progress (if active)
|
||||||
|
when (val state = scanningState) {
|
||||||
|
is ScanningState.Scanning -> {
|
||||||
|
ScanningProgressCard(state)
|
||||||
|
}
|
||||||
|
is ScanningState.Complete -> {
|
||||||
|
CompletionCard(state) {
|
||||||
|
viewModel.resetScanningState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ScanningState.Error -> {
|
||||||
|
ErrorCard(state) {
|
||||||
|
viewModel.resetScanningState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanning progress (if active)
|
|
||||||
if (scanningState is PersonInventoryViewModel.ScanningState.Scanning) {
|
|
||||||
ScanningProgressCard(scanningState as PersonInventoryViewModel.ScanningState.Scanning)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Completion message
|
|
||||||
if (scanningState is PersonInventoryViewModel.ScanningState.Complete) {
|
|
||||||
CompletionCard(scanningState as PersonInventoryViewModel.ScanningState.Complete)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Person list
|
// Person list
|
||||||
when (val state = uiState) {
|
if (personsWithModels.isEmpty()) {
|
||||||
is PersonInventoryViewModel.InventoryUiState.Loading -> {
|
EmptyState()
|
||||||
Box(
|
} else {
|
||||||
modifier = Modifier.fillMaxSize(),
|
PersonList(
|
||||||
contentAlignment = Alignment.Center
|
persons = personsWithModels,
|
||||||
) {
|
onScan = { personId ->
|
||||||
CircularProgressIndicator()
|
viewModel.scanForPerson(personId)
|
||||||
|
},
|
||||||
|
onView = { personId ->
|
||||||
|
onNavigateToPersonDetail(personId)
|
||||||
|
},
|
||||||
|
onDelete = { personId ->
|
||||||
|
viewModel.deletePerson(personId)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
is PersonInventoryViewModel.InventoryUiState.Success -> {
|
|
||||||
if (state.persons.isEmpty()) {
|
|
||||||
EmptyState()
|
|
||||||
} else {
|
|
||||||
PersonList(
|
|
||||||
persons = state.persons,
|
|
||||||
onScan = { personId, faceModelId ->
|
|
||||||
viewModel.scanLibraryForPerson(personId, faceModelId)
|
|
||||||
},
|
|
||||||
onImprove = { personId, faceModelId ->
|
|
||||||
viewModel.startModelImprovement(personId, faceModelId)
|
|
||||||
},
|
|
||||||
onView = { personId ->
|
|
||||||
if (isScanningInBackground) {
|
|
||||||
showNavigationWarning = true
|
|
||||||
pendingNavigation = { onNavigateToPersonDetail(personId) }
|
|
||||||
} else {
|
|
||||||
onNavigateToPersonDetail(personId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDelete = { personId, faceModelId ->
|
|
||||||
viewModel.deletePerson(personId, faceModelId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is PersonInventoryViewModel.InventoryUiState.Error -> {
|
|
||||||
ErrorState(state.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
|
private fun StatsCard(persons: List<PersonWithModelInfo>) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -187,7 +125,7 @@ fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
|
|||||||
)
|
)
|
||||||
StatItem(
|
StatItem(
|
||||||
icon = Icons.Default.Collections,
|
icon = Icons.Default.Collections,
|
||||||
value = persons.sumOf { it.stats.taggedPhotoCount }.toString(),
|
value = persons.sumOf { it.taggedPhotoCount }.toString(),
|
||||||
label = "Tagged"
|
label = "Tagged"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,7 +133,11 @@ fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: String, label: String) {
|
private fun StatItem(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
value: String,
|
||||||
|
label: String
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.padding(8.dp)
|
modifier = Modifier.padding(8.dp)
|
||||||
@@ -210,6 +152,7 @@ fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: Strin
|
|||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -221,16 +164,21 @@ fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: Strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning) {
|
private fun ScanningProgressCard(state: ScanningState.Scanning) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(16.dp)) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -238,45 +186,34 @@ fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning)
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Scanning for ${state.personName}",
|
"Scanning for ${state.personName}",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"${state.progress}/${state.total}",
|
"${state.completed} / ${state.total}",
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { state.progress.toFloat() / state.total.toFloat() },
|
progress = { if (state.total > 0) state.completed.toFloat() / state.total.toFloat() else 0f },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Performance stats
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"⚡ ${String.format("%.1f", state.imagesPerSecond)} images/sec",
|
"✓ ${state.facesFound} matches found",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Found: ${state.facesFound}",
|
"%.1f img/sec".format(state.speed),
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.imagesSkipped > 0) {
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
"Skipped: ${state.imagesSkipped} (no faces)",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.outline
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,73 +221,127 @@ fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CompletionCard(state: PersonInventoryViewModel.ScanningState.Complete) {
|
private fun CompletionCard(state: ScanningState.Complete, onDismiss: () -> Unit) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Row(
|
||||||
Icons.Default.CheckCircle,
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
contentDescription = null,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
modifier = Modifier.size(32.dp),
|
) {
|
||||||
tint = MaterialTheme.colorScheme.tertiary
|
Icon(
|
||||||
)
|
Icons.Default.CheckCircle,
|
||||||
Spacer(Modifier.width(12.dp))
|
contentDescription = null,
|
||||||
Column {
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
Text(
|
modifier = Modifier.size(32.dp)
|
||||||
"Scan Complete!",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"Found ${state.facesFound} photos of ${state.personName} in ${String.format("%.1f", state.durationSeconds)}s",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
|
||||||
)
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Scan Complete!",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Found ${state.personName} in ${state.facesFound} photos",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, "Dismiss")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PersonList(
|
private fun ErrorCard(state: ScanningState.Error, onDismiss: () -> Unit) {
|
||||||
persons: List<PersonInventoryViewModel.PersonWithStats>,
|
Card(
|
||||||
onScan: (String, String) -> Unit,
|
modifier = Modifier
|
||||||
onImprove: (String, String) -> Unit,
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Scan Failed",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
state.message,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, "Dismiss")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PersonList(
|
||||||
|
persons: List<PersonWithModelInfo>,
|
||||||
|
onScan: (String) -> Unit,
|
||||||
onView: (String) -> Unit,
|
onView: (String) -> Unit,
|
||||||
onDelete: (String, String) -> Unit
|
onDelete: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
contentPadding = PaddingValues(vertical = 8.dp)
|
||||||
contentPadding = PaddingValues(bottom = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
items(persons, key = { it.person.id }) { personWithStats ->
|
items(
|
||||||
|
items = persons,
|
||||||
|
key = { it.person.id }
|
||||||
|
) { person ->
|
||||||
PersonCard(
|
PersonCard(
|
||||||
person = personWithStats,
|
person = person,
|
||||||
onScan = { onScan(personWithStats.person.id, personWithStats.stats.faceModelId) },
|
onScan = { onScan(person.person.id) },
|
||||||
onImprove = { onImprove(personWithStats.person.id, personWithStats.stats.faceModelId) },
|
onView = { onView(person.person.id) },
|
||||||
onView = { onView(personWithStats.person.id) },
|
onDelete = { onDelete(person.person.id) }
|
||||||
onDelete = { onDelete(personWithStats.person.id, personWithStats.stats.faceModelId) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PersonCard(
|
private fun PersonCard(
|
||||||
person: PersonInventoryViewModel.PersonWithStats,
|
person: PersonWithModelInfo,
|
||||||
onScan: () -> Unit,
|
onScan: () -> Unit,
|
||||||
onImprove: () -> Unit,
|
|
||||||
onView: () -> Unit,
|
onView: () -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -410,10 +401,13 @@ fun PersonCard(
|
|||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
person.person.name,
|
person.person.name,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val trainingCount = person.faceModel?.trainingImageCount ?: 0
|
||||||
Text(
|
Text(
|
||||||
"${person.stats.taggedPhotoCount} photos • ${person.stats.trainingImageCount} trained",
|
"${person.taggedPhotoCount} photos • $trainingCount trained",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.outline
|
color = MaterialTheme.colorScheme.outline
|
||||||
)
|
)
|
||||||
@@ -431,12 +425,12 @@ fun PersonCard(
|
|||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
// IMPROVED BUTTON LAYOUT - No text wrapping!
|
// Action buttons
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Primary action - Scan
|
// Scan button
|
||||||
Button(
|
Button(
|
||||||
onClick = onScan,
|
onClick = onScan,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
@@ -447,10 +441,10 @@ fun PersonCard(
|
|||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Scan", maxLines = 1)
|
Text("Scan Library", maxLines = 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secondary action - View
|
// View button
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onView,
|
onClick = onView,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
@@ -461,51 +455,7 @@ fun PersonCard(
|
|||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("View", maxLines = 1)
|
Text("View Photos", maxLines = 1)
|
||||||
}
|
|
||||||
|
|
||||||
// Tertiary - More menu
|
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Box {
|
|
||||||
IconButton(
|
|
||||||
onClick = { showMenu = true },
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.border(
|
|
||||||
1.dp,
|
|
||||||
MaterialTheme.colorScheme.outline,
|
|
||||||
MaterialTheme.shapes.small
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.MoreVert, "More")
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showMenu,
|
|
||||||
onDismissRequest = { showMenu = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("Improve Model") },
|
|
||||||
onClick = {
|
|
||||||
showMenu = false
|
|
||||||
onImprove()
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.TrendingUp, null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("Export Data") },
|
|
||||||
onClick = {
|
|
||||||
showMenu = false
|
|
||||||
// TODO: Handle export
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.Share, null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,7 +463,7 @@ fun PersonCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyState() {
|
private fun EmptyState() {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -522,21 +472,19 @@ fun EmptyState() {
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.PersonAdd,
|
Icons.Default.PersonAdd,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(72.dp),
|
||||||
tint = MaterialTheme.colorScheme.outline
|
tint = MaterialTheme.colorScheme.outline
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
Text(
|
Text(
|
||||||
"No People Yet",
|
"No People Yet",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
"Train your first face model to get started",
|
"Train your first face model to get started",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
@@ -545,37 +493,3 @@ fun EmptyState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ErrorState(message: String) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(32.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Error,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(64.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
"Error Loading People",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
message,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,8 @@ import com.placeholder.sherpai2.ml.FaceNetModel
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -186,26 +188,27 @@ class PersonInventoryViewModel @Inject constructor(
|
|||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
// Batch collection for DB writes (mutex-protected)
|
// Batch collection for DB writes (mutex-protected)
|
||||||
val batchMatches = mutableListOf<Triple<String, String, Float>>() // (personId, imageId, confidence)
|
val batchMatches = mutableListOf<Triple<String, String, Float>>()
|
||||||
|
|
||||||
// ✅ MASSIVE PARALLELIZATION: Process all images concurrently
|
// ✅ MASSIVE PARALLELIZATION: Process all images concurrently
|
||||||
// Semaphore(50) limits to 50 simultaneous operations
|
// Semaphore(50) limits to 50 simultaneous operations
|
||||||
untaggedImages.map { image ->
|
val deferredResults = untaggedImages.map { image ->
|
||||||
kotlinx.coroutines.async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
try {
|
try {
|
||||||
// Load and detect faces
|
// Load and detect faces
|
||||||
val uri = Uri.parse(image.imageUri)
|
val uri = Uri.parse(image.imageUri)
|
||||||
val inputStream = context.contentResolver.openInputStream(uri) ?: return@withPermit
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
if (inputStream == null) return@withPermit
|
||||||
|
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
|
|
||||||
if (bitmap == null) return@withPermit
|
if (bitmap == null) return@withPermit
|
||||||
|
|
||||||
val mlImage = InputImage.fromBitmap(bitmap, 0)
|
val mlImage = InputImage.fromBitmap(bitmap, 0)
|
||||||
val faces = com.google.android.gms.tasks.Tasks.await(
|
val facesTask = detector.process(mlImage)
|
||||||
detector.process(mlImage)
|
val faces = com.google.android.gms.tasks.Tasks.await(facesTask)
|
||||||
)
|
|
||||||
|
|
||||||
// Check each detected face
|
// Check each detected face
|
||||||
for (face in faces) {
|
for (face in faces) {
|
||||||
@@ -271,7 +274,10 @@ class PersonInventoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.forEach { it.await() } // Wait for all to complete
|
}
|
||||||
|
|
||||||
|
// Wait for all to complete
|
||||||
|
deferredResults.awaitAll()
|
||||||
|
|
||||||
// Flush remaining batch
|
// Flush remaining batch
|
||||||
batchUpdateMutex.withLock {
|
batchUpdateMutex.withLock {
|
||||||
|
|||||||
Reference in New Issue
Block a user