From 0f6c9060bf5e186919a2078d456bdf11843f26b2 Mon Sep 17 00:00:00 2001
From: genki <123@1234.com>
Date: Sun, 11 Jan 2026 21:06:38 -0500
Subject: [PATCH] Pre UI Sweep
---
.idea/deviceManager.xml | 7 +-
.../ui/presentation/AppDrawerContent.kt | 114 ++--
.../ui/trainingprep/FacePickerDialog.kt | 491 ++++++++++--------
3 files changed, 326 insertions(+), 286 deletions(-)
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
index 91f9558..5df2b2d 100644
--- a/.idea/deviceManager.xml
+++ b/.idea/deviceManager.xml
@@ -5,9 +5,14 @@
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt
index 9cd6331..c012ac4 100644
--- a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt
+++ b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt
@@ -18,8 +18,7 @@ import androidx.compose.material.icons.filled.*
import com.placeholder.sherpai2.ui.navigation.AppRoutes
/**
- * Beautiful app drawer with sections, gradient header, and polish
- * UPDATED: Tour → Explore
+ * SLIMMED DOWN AppDrawer - 280dp width, inline logo, cleaner sections
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -28,12 +27,12 @@ fun AppDrawerContent(
onDestinationClicked: (String) -> Unit
) {
ModalDrawerSheet(
- modifier = Modifier.width(300.dp),
+ modifier = Modifier.width(280.dp), // SLIMMER (was 300dp)
drawerContainerColor = MaterialTheme.colorScheme.surface
) {
Column(modifier = Modifier.fillMaxSize()) {
- // ===== BEAUTIFUL GRADIENT HEADER =====
+ // ===== COMPACT HEADER - Icon + Text Inline =====
Box(
modifier = Modifier
.fillMaxWidth()
@@ -45,15 +44,16 @@ fun AppDrawerContent(
)
)
)
- .padding(24.dp)
+ .padding(20.dp) // Reduced padding
) {
- Column(
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
- // App icon/logo area
+ // App icon - smaller
Surface(
- modifier = Modifier.size(56.dp),
- shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.size(48.dp), // Smaller (was 56dp)
+ shape = RoundedCornerShape(14.dp),
color = MaterialTheme.colorScheme.primary,
shadowElevation = 4.dp
) {
@@ -61,44 +61,47 @@ fun AppDrawerContent(
Icon(
Icons.Default.Face,
contentDescription = null,
- modifier = Modifier.size(32.dp),
+ modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
- Text(
- "SherpAI",
- style = MaterialTheme.typography.headlineMedium,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface
- )
+ // Text next to icon
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ Text(
+ "SherpAI",
+ style = MaterialTheme.typography.titleLarge, // Smaller (was headlineMedium)
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
- Text(
- "Face Recognition System",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ Text(
+ "Face Recognition System",
+ style = MaterialTheme.typography.bodySmall, // Smaller
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
}
}
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(4.dp)) // Reduced spacing
// ===== NAVIGATION SECTIONS =====
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
- .padding(horizontal = 12.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ .padding(horizontal = 8.dp), // Reduced padding
+ verticalArrangement = Arrangement.spacedBy(2.dp) // Tighter spacing
) {
// Photos Section
DrawerSection(title = "Photos")
val photoItems = listOf(
- DrawerItem(AppRoutes.SEARCH, "Search", Icons.Default.Search, "Find photos by tag or person"),
- DrawerItem(AppRoutes.EXPLORE, "Explore", Icons.Default.Explore, "Browse smart albums")
+ DrawerItem(AppRoutes.SEARCH, "Search", Icons.Default.Search),
+ DrawerItem(AppRoutes.EXPLORE, "Explore", Icons.Default.Explore)
)
photoItems.forEach { item ->
@@ -109,15 +112,15 @@ fun AppDrawerContent(
)
}
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(4.dp))
// Face Recognition Section
DrawerSection(title = "Face Recognition")
val faceItems = listOf(
- DrawerItem(AppRoutes.INVENTORY, "People", Icons.Default.Face, "Existing models"),
- DrawerItem(AppRoutes.TRAIN, "Create Person", Icons.Default.ModelTraining, "New person model"),
- DrawerItem(AppRoutes.MODELS, "Models", Icons.Default.SmartToy, "AI model management")
+ DrawerItem(AppRoutes.INVENTORY, "People", Icons.Default.Face),
+ DrawerItem(AppRoutes.TRAIN, "Train New", Icons.Default.ModelTraining),
+ DrawerItem(AppRoutes.MODELS, "Models", Icons.Default.SmartToy)
)
faceItems.forEach { item ->
@@ -128,14 +131,14 @@ fun AppDrawerContent(
)
}
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(4.dp))
// Organization Section
DrawerSection(title = "Organization")
val orgItems = listOf(
- DrawerItem(AppRoutes.TAGS, "Tags", Icons.AutoMirrored.Filled.Label, "Manage Tags"),
- DrawerItem(AppRoutes.UTILITIES, "Util.", Icons.Default.NewReleases, "Manage Collection")
+ DrawerItem(AppRoutes.TAGS, "Tags", Icons.AutoMirrored.Filled.Label),
+ DrawerItem(AppRoutes.UTILITIES, "Utilities", Icons.Default.Build)
)
orgItems.forEach { item ->
@@ -150,7 +153,7 @@ fun AppDrawerContent(
// Settings at bottom
HorizontalDivider(
- modifier = Modifier.padding(vertical = 8.dp),
+ modifier = Modifier.padding(vertical = 6.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
@@ -158,35 +161,34 @@ fun AppDrawerContent(
item = DrawerItem(
AppRoutes.SETTINGS,
"Settings",
- Icons.Default.Settings,
- "App preferences"
+ Icons.Default.Settings
),
selected = AppRoutes.SETTINGS == currentRoute,
onClick = { onDestinationClicked(AppRoutes.SETTINGS) }
)
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(4.dp))
}
}
}
}
/**
- * Section header in drawer
+ * Section header - more compact
*/
@Composable
private fun DrawerSection(title: String) {
Text(
text = title,
- style = MaterialTheme.typography.labelMedium,
+ style = MaterialTheme.typography.labelSmall, // Smaller
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) // Reduced padding
)
}
/**
- * Individual navigation item with icon, label, and subtitle
+ * Navigation item - cleaner, no subtitle
*/
@Composable
private fun DrawerNavigationItem(
@@ -196,33 +198,24 @@ private fun DrawerNavigationItem(
) {
NavigationDrawerItem(
label = {
- Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
- Text(
- text = item.label,
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
- )
- item.subtitle?.let {
- Text(
- text = it,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
- )
- }
- }
+ Text(
+ text = item.label,
+ style = MaterialTheme.typography.bodyMedium, // Slightly smaller
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
+ )
},
icon = {
Icon(
item.icon,
contentDescription = item.label,
- modifier = Modifier.size(24.dp)
+ modifier = Modifier.size(22.dp) // Slightly smaller
)
},
selected = selected,
onClick = onClick,
modifier = Modifier
.padding(NavigationDrawerItemDefaults.ItemPadding)
- .clip(RoundedCornerShape(12.dp)),
+ .clip(RoundedCornerShape(10.dp)), // Slightly smaller radius
colors = NavigationDrawerItemDefaults.colors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.primary,
@@ -233,11 +226,10 @@ private fun DrawerNavigationItem(
}
/**
- * Data class for drawer items
+ * Simplified drawer item (no subtitle)
*/
private data class DrawerItem(
val route: String,
val label: String,
- val icon: androidx.compose.ui.graphics.vector.ImageVector,
- val subtitle: String? = null
+ val icon: androidx.compose.ui.graphics.vector.ImageVector
)
\ No newline at end of file
diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerDialog.kt b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerDialog.kt
index 125e171..db10908 100644
--- a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerDialog.kt
+++ b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerDialog.kt
@@ -5,65 +5,99 @@ import android.graphics.BitmapFactory
import android.graphics.Rect
import android.net.Uri
import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
+import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.face.FaceDetection
+import com.google.mlkit.vision.face.FaceDetectorOptions
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
+import java.io.InputStream
/**
* Dialog for selecting a face from multiple detected faces
+ *
+ * CRITICAL: Re-detects faces on full resolution bitmap to ensure accurate cropping.
+ * Face bounds from FaceDetectionHelper are from downsampled images and won't match
+ * the full resolution bitmap loaded here.
*/
@Composable
fun FacePickerDialog(
result: FaceDetectionHelper.FaceDetectionResult,
onDismiss: () -> Unit,
- onFaceSelected: (Int, Bitmap) -> Unit // faceIndex, croppedFaceBitmap
+ onFaceSelected: (Int, Bitmap) -> Unit
) {
val context = LocalContext.current
- var selectedFaceIndex by remember { mutableStateOf(null) }
+ var selectedFaceIndex by remember { mutableStateOf(0) }
var croppedFaces by remember { mutableStateOf>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
+ var errorMessage by remember { mutableStateOf(null) }
- // Load and crop all faces
+ // Load and crop all faces - RE-DETECT to get accurate bounds
LaunchedEffect(result) {
isLoading = true
- croppedFaces = withContext(Dispatchers.IO) {
- val bitmap = loadBitmapFromUri(context, result.uri)
- bitmap?.let { bmp ->
- result.faceBounds.map { bounds ->
- cropFaceFromBitmap(bmp, bounds)
+ errorMessage = null
+
+ try {
+ croppedFaces = withContext(Dispatchers.IO) {
+ // Load the FULL resolution bitmap (no downsampling)
+ val fullBitmap = loadFullResolutionBitmap(context, result.uri)
+
+ if (fullBitmap == null) {
+ errorMessage = "Failed to load image"
+ return@withContext emptyList()
}
- } ?: emptyList()
- }
- isLoading = false
- // Auto-select the first (largest) face
- if (croppedFaces.isNotEmpty()) {
- selectedFaceIndex = 0
+
+ // Re-detect faces on the full resolution bitmap to get accurate bounds
+ val accurateFaceBounds = detectFacesOnBitmap(fullBitmap)
+
+ if (accurateFaceBounds.isEmpty()) {
+ // Fallback: try to use the original bounds with scaling
+ val scaledBounds = result.faceBounds.map { originalBounds ->
+ cropFaceFromBitmap(fullBitmap, originalBounds)
+ }
+ fullBitmap.recycle()
+ return@withContext scaledBounds
+ }
+
+ // Crop faces using accurate bounds
+ val croppedList = accurateFaceBounds.map { bounds ->
+ cropFaceFromBitmap(fullBitmap, bounds)
+ }
+
+ // CRITICAL: Recycle AFTER all cropping is done
+ fullBitmap.recycle()
+
+ croppedList
+ }
+
+ if (croppedFaces.isEmpty() && errorMessage == null) {
+ errorMessage = "No faces found in full resolution image"
+ }
+
+ } catch (e: Exception) {
+ errorMessage = "Error processing faces: ${e.message}"
+ } finally {
+ isLoading = false
}
}
@@ -73,15 +107,18 @@ fun FacePickerDialog(
) {
Card(
modifier = Modifier
- .fillMaxWidth(0.95f)
- .fillMaxHeight(0.9f),
- shape = RoundedCornerShape(16.dp)
+ .fillMaxWidth(0.94f)
+ .wrapContentHeight(),
+ shape = RoundedCornerShape(24.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ )
) {
Column(
modifier = Modifier
- .fillMaxSize()
- .padding(20.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ .fillMaxWidth()
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
) {
// Header
Row(
@@ -92,7 +129,7 @@ fun FacePickerDialog(
Column {
Text(
text = "Pick a Face",
- style = MaterialTheme.typography.headlineSmall,
+ style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Text(
@@ -103,14 +140,19 @@ fun FacePickerDialog(
}
IconButton(onClick = onDismiss) {
- Icon(Icons.Default.Close, "Close")
+ Icon(
+ Icons.Default.Close,
+ contentDescription = "Close",
+ modifier = Modifier.size(24.dp)
+ )
}
}
// Instruction
Text(
text = "Tap a face below to select it for training:",
- style = MaterialTheme.typography.bodyMedium
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (isLoading) {
@@ -118,43 +160,75 @@ fun FacePickerDialog(
Box(
modifier = Modifier
.fillMaxWidth()
- .weight(1f),
+ .height(200.dp),
contentAlignment = Alignment.Center
) {
- CircularProgressIndicator()
- }
- } else {
- // Original image with face boxes overlay
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .weight(1f),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant
- )
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- FaceOverlayImage(
- imageUri = result.uri,
- faceBounds = result.faceBounds,
- selectedFaceIndex = selectedFaceIndex,
- onFaceClick = { index ->
- selectedFaceIndex = index
- }
+ CircularProgressIndicator()
+ Text(
+ "Processing faces...",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
+ } else if (errorMessage != null) {
+ // Error state
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Error,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.error
+ )
+ Text(
+ text = errorMessage ?: "Unknown error",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onErrorContainer
+ )
+ }
+ }
+ } else {
+ // Original image preview
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ AsyncImage(
+ model = result.uri,
+ contentDescription = "Original image",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Fit
+ )
+ }
- // Face previews grid
+ // Face previews section
Text(
text = "Preview (tap to select):",
- style = MaterialTheme.typography.bodyMedium,
+ style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
+ // Face preview cards
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
@@ -162,13 +236,41 @@ fun FacePickerDialog(
croppedFaces.forEachIndexed { index, faceBitmap ->
FacePreviewCard(
faceBitmap = faceBitmap,
- index = index,
+ index = index + 1,
isSelected = selectedFaceIndex == index,
onClick = { selectedFaceIndex = index },
modifier = Modifier.weight(1f)
)
}
}
+
+ // Helper text
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Info,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ "The selected face will be used for training",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
}
// Action buttons
@@ -178,142 +280,37 @@ fun FacePickerDialog(
) {
OutlinedButton(
onClick = onDismiss,
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
+ .height(52.dp),
+ shape = RoundedCornerShape(14.dp)
) {
- Text("Cancel")
+ Text("Cancel", style = MaterialTheme.typography.titleMedium)
}
Button(
onClick = {
- selectedFaceIndex?.let { index ->
- if (index < croppedFaces.size) {
- onFaceSelected(index, croppedFaces[index])
- }
+ if (selectedFaceIndex < croppedFaces.size) {
+ onFaceSelected(selectedFaceIndex, croppedFaces[selectedFaceIndex])
}
},
- modifier = Modifier.weight(1f),
- enabled = selectedFaceIndex != null && !isLoading
+ modifier = Modifier
+ .weight(1f)
+ .height(52.dp),
+ enabled = !isLoading && croppedFaces.isNotEmpty() && errorMessage == null,
+ shape = RoundedCornerShape(14.dp)
) {
- Icon(Icons.Default.CheckCircle, contentDescription = null)
- Spacer(modifier = Modifier.width(8.dp))
- Text("Select")
- }
- }
- }
- }
- }
-}
-
-/**
- * Image with interactive face boxes overlay
- */
-@Composable
-private fun FaceOverlayImage(
- imageUri: Uri,
- faceBounds: List,
- selectedFaceIndex: Int?,
- onFaceClick: (Int) -> Unit
-) {
- var imageSize by remember { mutableStateOf(Size.Zero) }
- var imageBounds by remember { mutableStateOf(Rect()) }
-
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- // Original image
- AsyncImage(
- model = imageUri,
- contentDescription = "Original image",
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp),
- contentScale = ContentScale.Fit,
- onSuccess = { state ->
- val drawable = state.result.drawable
- imageBounds = Rect(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
- }
- )
-
- // Face boxes overlay
- Canvas(
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp)
- ) {
- if (imageBounds.width() > 0 && imageBounds.height() > 0) {
- // Calculate scale to fit image in canvas
- val scaleX = size.width / imageBounds.width()
- val scaleY = size.height / imageBounds.height()
- val scale = minOf(scaleX, scaleY)
-
- // Calculate offset to center image
- val scaledWidth = imageBounds.width() * scale
- val scaledHeight = imageBounds.height() * scale
- val offsetX = (size.width - scaledWidth) / 2
- val offsetY = (size.height - scaledHeight) / 2
-
- faceBounds.forEachIndexed { index, bounds ->
- val isSelected = selectedFaceIndex == index
-
- // Scale and position the face box
- val left = bounds.left * scale + offsetX
- val top = bounds.top * scale + offsetY
- val width = bounds.width() * scale
- val height = bounds.height() * scale
-
- // Draw box
- drawRect(
- color = if (isSelected) Color(0xFF4CAF50) else Color(0xFF2196F3),
- topLeft = Offset(left, top),
- size = Size(width, height),
- style = Stroke(width = if (isSelected) 6f else 4f)
- )
-
- // Draw semi-transparent fill for selected
- if (isSelected) {
- drawRect(
- color = Color(0xFF4CAF50).copy(alpha = 0.2f),
- topLeft = Offset(left, top),
- size = Size(width, height)
+ Icon(
+ Icons.Default.CheckCircle,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("Use This Face", style = MaterialTheme.typography.titleMedium)
}
-
- // Draw face number label
- drawCircle(
- color = if (isSelected) Color(0xFF4CAF50) else Color(0xFF2196F3),
- radius = 20f * scale,
- center = Offset(left + 20f * scale, top + 20f * scale)
- )
}
}
}
-
- // Clickable areas for each face
- faceBounds.forEachIndexed { index, bounds ->
- if (imageBounds.width() > 0 && imageBounds.height() > 0) {
- val scaleX = imageSize.width / imageBounds.width()
- val scaleY = imageSize.height / imageBounds.height()
- val scale = minOf(scaleX, scaleY)
-
- val scaledWidth = imageBounds.width() * scale
- val scaledHeight = imageBounds.height() * scale
- val offsetX = (imageSize.width - scaledWidth) / 2
- val offsetY = (imageSize.height - scaledHeight) / 2
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .clickable { onFaceClick(index) }
- )
- }
- }
- }
-
- // Update image size
- BoxWithConstraints {
- LaunchedEffect(constraints) {
- imageSize = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat())
- }
}
}
@@ -330,69 +327,88 @@ private fun FacePreviewCard(
) {
Card(
modifier = modifier
- .aspectRatio(1f)
+ .aspectRatio(0.75f)
.clickable(onClick = onClick),
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
- MaterialTheme.colorScheme.surface
+ MaterialTheme.colorScheme.surfaceVariant
),
border = if (isSelected)
BorderStroke(3.dp, MaterialTheme.colorScheme.primary)
else
- BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
+ BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = if (isSelected) 8.dp else 2.dp
+ )
) {
- Box(
- modifier = Modifier.fillMaxSize()
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- androidx.compose.foundation.Image(
- bitmap = faceBitmap.asImageBitmap(),
- contentDescription = "Face ${index + 1}",
- modifier = Modifier.fillMaxSize(),
- contentScale = ContentScale.Crop
- )
+ // Face image
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ) {
+ Image(
+ bitmap = faceBitmap.asImageBitmap(),
+ contentDescription = "Face $index",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
- // Selected checkmark (only show when selected)
- if (isSelected) {
- Surface(
- modifier = Modifier
- .align(Alignment.Center),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
- ) {
- Icon(
- Icons.Default.CheckCircle,
- contentDescription = "Selected",
- modifier = Modifier
- .padding(12.dp)
- .size(32.dp),
- tint = MaterialTheme.colorScheme.onPrimary
- )
+ // Selected overlay with checkmark
+ if (isSelected) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ shadowElevation = 4.dp
+ ) {
+ Icon(
+ Icons.Default.CheckCircle,
+ contentDescription = "Selected",
+ modifier = Modifier
+ .padding(12.dp)
+ .size(40.dp),
+ tint = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ }
+ }
}
}
- // Face number badge (always in top-right, small)
+ // Face number label
Surface(
- modifier = Modifier
- .align(Alignment.TopEnd)
- .padding(4.dp),
- shape = CircleShape,
+ modifier = Modifier.fillMaxWidth(),
color = if (isSelected)
MaterialTheme.colorScheme.primary
else
- MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f),
- shadowElevation = 2.dp
+ MaterialTheme.colorScheme.surface,
+ shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)
) {
Text(
- text = "${index + 1}",
- modifier = Modifier.padding(6.dp),
- style = MaterialTheme.typography.labelSmall,
+ text = "Face $index",
+ modifier = Modifier.padding(vertical = 12.dp),
+ style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
color = if (isSelected)
MaterialTheme.colorScheme.onPrimary
else
- MaterialTheme.colorScheme.onSurfaceVariant
+ MaterialTheme.colorScheme.onSurface
)
}
}
@@ -400,14 +416,14 @@ private fun FacePreviewCard(
}
/**
- * Helper function to load bitmap from URI
+ * Load full resolution bitmap WITHOUT downsampling
*/
-private suspend fun loadBitmapFromUri(
+private suspend fun loadFullResolutionBitmap(
context: android.content.Context,
uri: Uri
): Bitmap? = withContext(Dispatchers.IO) {
try {
- val inputStream = context.contentResolver.openInputStream(uri)
+ val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
BitmapFactory.decodeStream(inputStream)?.also {
inputStream?.close()
}
@@ -417,7 +433,34 @@ private suspend fun loadBitmapFromUri(
}
/**
- * Helper function to crop face from bitmap
+ * Re-detect faces on full resolution bitmap to get accurate bounds
+ */
+private suspend fun detectFacesOnBitmap(bitmap: Bitmap): List = withContext(Dispatchers.Default) {
+ try {
+ val faceDetectorOptions = FaceDetectorOptions.Builder()
+ .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
+ .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
+ .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
+ .setMinFaceSize(0.15f)
+ .build()
+
+ val detector = FaceDetection.getClient(faceDetectorOptions)
+ val inputImage = InputImage.fromBitmap(bitmap, 0)
+
+ val faces = detector.process(inputImage).await()
+
+ // Sort by face size (largest first)
+ faces.sortedByDescending { face ->
+ face.boundingBox.width() * face.boundingBox.height()
+ }.map { it.boundingBox }
+
+ } catch (e: Exception) {
+ emptyList()
+ }
+}
+
+/**
+ * Helper function to crop face from bitmap with padding
*/
private fun cropFaceFromBitmap(bitmap: Bitmap, faceBounds: Rect): Bitmap {
// Add 20% padding around the face