Added onClick from Albumviewscreen.kt
This commit is contained in:
@@ -85,4 +85,7 @@ dependencies {
|
||||
|
||||
// Gson for storing FloatArrays in Room
|
||||
implementation(libs.gson)
|
||||
|
||||
// Zoomable
|
||||
implementation(libs.zoomable)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.placeholder.sherpai2.ui.album
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
@@ -23,14 +24,12 @@ import com.placeholder.sherpai2.ui.search.DisplayMode
|
||||
import com.placeholder.sherpai2.ui.search.components.ImageGridItem
|
||||
|
||||
/**
|
||||
* AlbumViewScreen - Beautiful album detail view
|
||||
* AlbumViewScreen - UPDATED with clickable images
|
||||
*
|
||||
* Features:
|
||||
* - Album stats
|
||||
* - Search within album
|
||||
* - Date filtering
|
||||
* - Simple/Verbose toggle
|
||||
* - Clean person display
|
||||
* Changes:
|
||||
* - PhotoCard now clickable
|
||||
* - Passes onImageClick to ImageGridItem
|
||||
* - Entire card surface clickable as backup
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -279,6 +278,9 @@ private fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, labe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PhotoCard - UPDATED: Now fully clickable
|
||||
*/
|
||||
@Composable
|
||||
private fun PhotoCard(
|
||||
photo: AlbumPhoto,
|
||||
@@ -286,15 +288,19 @@ private fun PhotoCard(
|
||||
onImageClick: (String) -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onImageClick(photo.image.imageUri) }, // ✅ ENTIRE CARD CLICKABLE
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column {
|
||||
// Image (also clickable via ImageGridItem)
|
||||
ImageGridItem(
|
||||
image = photo.image,
|
||||
onClick = { onImageClick(photo.image.imageUri) }
|
||||
onClick = { onImageClick(photo.image.imageUri) } // ✅ IMAGE CLICKABLE
|
||||
)
|
||||
|
||||
// Person tags
|
||||
if (photo.persons.isNotEmpty()) {
|
||||
when (displayMode) {
|
||||
DisplayMode.SIMPLE -> {
|
||||
|
||||
@@ -1,86 +1,322 @@
|
||||
package com.placeholder.sherpai2.ui.imagedetail
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import com.placeholder.sherpai2.data.local.entity.TagEntity
|
||||
import com.placeholder.sherpai2.ui.imagedetail.viewmodel.ImageDetailViewModel
|
||||
|
||||
|
||||
|
||||
import net.engawapg.lib.zoomable.rememberZoomState
|
||||
import net.engawapg.lib.zoomable.zoomable
|
||||
import java.net.URLEncoder
|
||||
|
||||
/**
|
||||
* ImageDetailScreen
|
||||
* ImageDetailScreen - COMPLETE with navigation and tags
|
||||
*
|
||||
* Purpose:
|
||||
* - Add tags
|
||||
* - Remove tags
|
||||
* - Validate write propagation
|
||||
* Features:
|
||||
* - Full-screen zoomable image
|
||||
* - Previous/Next navigation buttons
|
||||
* - Image counter (3/45)
|
||||
* - Tags button (toggle show/hide)
|
||||
* - Shows all tags on photo
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImageDetailScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
imageUri: String,
|
||||
onBack: () -> Unit
|
||||
onBack: () -> Unit,
|
||||
navController: NavController? = null,
|
||||
allImageUris: List<String> = emptyList(), // Pass from caller
|
||||
viewModel: ImageDetailViewModel = hiltViewModel() // ✅ FIXED: Use hiltViewModel
|
||||
) {
|
||||
val viewModel: ImageDetailViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
|
||||
LaunchedEffect(imageUri) {
|
||||
viewModel.loadImage(imageUri)
|
||||
}
|
||||
|
||||
val tags by viewModel.tags.collectAsStateWithLifecycle()
|
||||
var showTags by remember { mutableStateOf(false) }
|
||||
|
||||
var newTag by remember { mutableStateOf("") }
|
||||
// Navigation state
|
||||
val currentIndex = if (allImageUris.isNotEmpty()) allImageUris.indexOf(imageUri) else -1
|
||||
val canGoPrevious = currentIndex > 0
|
||||
val canGoNext = currentIndex in 0 until allImageUris.size - 1
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (currentIndex >= 0) {
|
||||
Text(
|
||||
"${currentIndex + 1} / ${allImageUris.size}",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
} else {
|
||||
Text("Photo")
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// Tags toggle button
|
||||
IconButton(onClick = { showTags = !showTags }) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (tags.isNotEmpty()) {
|
||||
Badge(
|
||||
containerColor = if (showTags)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Text(
|
||||
tags.size.toString(),
|
||||
color = if (showTags)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
if (showTags) Icons.Default.Label else Icons.Default.LocalOffer,
|
||||
"Show Tags",
|
||||
tint = if (showTags)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = imageUri,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
// Previous button
|
||||
if (navController != null && allImageUris.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (canGoPrevious) {
|
||||
val prevUri = allImageUris[currentIndex - 1]
|
||||
val encoded = URLEncoder.encode(prevUri, "UTF-8")
|
||||
navController.navigate("image_detail/$encoded") {
|
||||
popUpTo("image_detail/${URLEncoder.encode(imageUri, "UTF-8")}") {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = canGoPrevious
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowLeft, "Previous")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = newTag,
|
||||
onValueChange = { newTag = it },
|
||||
label = { Text("Add tag") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.addTag(newTag)
|
||||
newTag = ""
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
Text("Add Tag")
|
||||
// Next button
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (canGoNext) {
|
||||
val nextUri = allImageUris[currentIndex + 1]
|
||||
val encoded = URLEncoder.encode(nextUri, "UTF-8")
|
||||
navController.navigate("image_detail/$encoded") {
|
||||
popUpTo("image_detail/${URLEncoder.encode(imageUri, "UTF-8")}") {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = canGoNext
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowRight, "Next")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
tags.forEach { tag ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
// Zoomable image
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.background(Color.Black)
|
||||
) {
|
||||
Text(tag.value)
|
||||
TextButton(onClick = { viewModel.removeTag(tag) }) {
|
||||
Text("Remove")
|
||||
val zoomState = rememberZoomState()
|
||||
|
||||
AsyncImage(
|
||||
model = imageUri,
|
||||
contentDescription = "Photo",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zoomable(zoomState),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
|
||||
// Tags panel (slides up when enabled)
|
||||
if (showTags) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
tonalElevation = 3.dp
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
"Tags (${tags.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
if (tags.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
"No tags yet",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(tags, key = { it.tagId }) { tag ->
|
||||
TagCard(
|
||||
tag = tag,
|
||||
onRemove = { viewModel.removeTag(tag) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TagCard(
|
||||
tag: TagEntity,
|
||||
onRemove: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = when (tag.type) {
|
||||
"PERSON" -> MaterialTheme.colorScheme.primaryContainer
|
||||
"SYSTEM" -> MaterialTheme.colorScheme.secondaryContainer
|
||||
else -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
}
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (tag.type) {
|
||||
"PERSON" -> Icons.Default.Face
|
||||
"SYSTEM" -> Icons.Default.AutoAwesome
|
||||
else -> Icons.Default.Label
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = when (tag.type) {
|
||||
"PERSON" -> MaterialTheme.colorScheme.primary
|
||||
"SYSTEM" -> MaterialTheme.colorScheme.secondary
|
||||
else -> MaterialTheme.colorScheme.tertiary
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = tag.getDisplayValue(), // Uses TagEntity's built-in method
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = tag.type.lowercase().replaceFirstChar { it.uppercase() },
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "•",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = formatTimestamp(tag.createdAt),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove button (only for user-created tags)
|
||||
if (tag.isUserTag()) {
|
||||
IconButton(
|
||||
onClick = onRemove,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Delete, "Remove tag")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to relative time
|
||||
*/
|
||||
private fun formatTimestamp(timestamp: Long): String {
|
||||
val now = System.currentTimeMillis()
|
||||
val diff = now - timestamp
|
||||
|
||||
return when {
|
||||
diff < 60_000 -> "Just now"
|
||||
diff < 3600_000 -> "${diff / 60_000}m ago"
|
||||
diff < 86400_000 -> "${diff / 3600_000}h ago"
|
||||
diff < 604800_000 -> "${diff / 86400_000}d ago"
|
||||
else -> "${diff / 604800_000}w ago"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -14,6 +15,7 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.placeholder.sherpai2.ui.devscreens.DummyScreen
|
||||
import com.placeholder.sherpai2.ui.album.AlbumViewScreen
|
||||
import com.placeholder.sherpai2.ui.album.AlbumViewModel
|
||||
import com.placeholder.sherpai2.ui.explore.ExploreScreen
|
||||
import com.placeholder.sherpai2.ui.imagedetail.ImageDetailScreen
|
||||
import com.placeholder.sherpai2.ui.modelinventory.PersonInventoryScreen
|
||||
@@ -30,20 +32,12 @@ import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
/**
|
||||
* AppNavHost - Main navigation graph
|
||||
* UPDATED: Added Explore and Tags screens
|
||||
* AppNavHost - UPDATED with image list navigation
|
||||
*
|
||||
* Complete flow:
|
||||
* - Photo browsing (Search, Explore, Detail)
|
||||
* - Face recognition (Inventory, Train)
|
||||
* - Organization (Tags, Upload)
|
||||
* - Settings
|
||||
*
|
||||
* Features:
|
||||
* - URL encoding for safe navigation
|
||||
* - Proper back stack management
|
||||
* - State preservation
|
||||
* - Beautiful placeholders
|
||||
* Changes:
|
||||
* - Search/Album screens pass full image list to detail screen
|
||||
* - Detail screen can navigate prev/next
|
||||
* - Image URIs stored in SavedStateHandle for navigation
|
||||
*/
|
||||
@Composable
|
||||
fun AppNavHost(
|
||||
@@ -61,19 +55,27 @@ fun AppNavHost(
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* SEARCH SCREEN
|
||||
* Main photo browser with face tag search
|
||||
* SEARCH SCREEN - UPDATED: Stores image list for navigation
|
||||
*/
|
||||
composable(AppRoutes.SEARCH) {
|
||||
val searchViewModel: SearchViewModel = hiltViewModel()
|
||||
val images by searchViewModel
|
||||
.searchImages()
|
||||
.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
SearchScreen(
|
||||
searchViewModel = searchViewModel,
|
||||
onImageClick = { imageUri ->
|
||||
// Store full image list for prev/next navigation
|
||||
val allImageUris = images.map { it.image.imageUri }
|
||||
navController.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("all_image_uris", allImageUris)
|
||||
|
||||
val encodedUri = URLEncoder.encode(imageUri, "UTF-8")
|
||||
navController.navigate("${AppRoutes.IMAGE_DETAIL}/$encodedUri")
|
||||
},
|
||||
onAlbumClick = { tagValue ->
|
||||
// Navigate to tag-based album
|
||||
navController.navigate("album/tag/$tagValue")
|
||||
}
|
||||
)
|
||||
@@ -81,7 +83,6 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* EXPLORE SCREEN
|
||||
* Browse smart albums (auto-generated from tags)
|
||||
*/
|
||||
composable(AppRoutes.EXPLORE) {
|
||||
ExploreScreen(
|
||||
@@ -92,8 +93,7 @@ fun AppNavHost(
|
||||
}
|
||||
|
||||
/**
|
||||
* IMAGE DETAIL SCREEN
|
||||
* Single photo view with metadata
|
||||
* IMAGE DETAIL SCREEN - UPDATED: Receives image list for navigation
|
||||
*/
|
||||
composable(
|
||||
route = "${AppRoutes.IMAGE_DETAIL}/{imageUri}",
|
||||
@@ -107,15 +107,22 @@ fun AppNavHost(
|
||||
?.let { URLDecoder.decode(it, "UTF-8") }
|
||||
?: error("imageUri missing from navigation")
|
||||
|
||||
// Get image list from previous screen
|
||||
val allImageUris = navController.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.get<List<String>>("all_image_uris")
|
||||
?: emptyList()
|
||||
|
||||
ImageDetailScreen(
|
||||
imageUri = imageUri,
|
||||
onBack = { navController.popBackStack() }
|
||||
onBack = { navController.popBackStack() },
|
||||
navController = navController,
|
||||
allImageUris = allImageUris
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ALBUM VIEW SCREEN
|
||||
* View photos in a specific album (tag, person, or time-based)
|
||||
* ALBUM VIEW SCREEN - UPDATED: Stores image list for navigation
|
||||
*/
|
||||
composable(
|
||||
route = "album/{albumType}/{albumId}",
|
||||
@@ -128,11 +135,27 @@ fun AppNavHost(
|
||||
}
|
||||
)
|
||||
) {
|
||||
val albumViewModel: AlbumViewModel = hiltViewModel()
|
||||
val uiState by albumViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
AlbumViewScreen(
|
||||
onBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onImageClick = { imageUri ->
|
||||
// Store full album image list
|
||||
val allImageUris = if (uiState is com.placeholder.sherpai2.ui.album.AlbumUiState.Success) {
|
||||
(uiState as com.placeholder.sherpai2.ui.album.AlbumUiState.Success)
|
||||
.photos
|
||||
.map { it.image.imageUri }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
navController.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("all_image_uris", allImageUris)
|
||||
|
||||
val encodedUri = URLEncoder.encode(imageUri, "UTF-8")
|
||||
navController.navigate("${AppRoutes.IMAGE_DETAIL}/$encodedUri")
|
||||
}
|
||||
@@ -145,19 +168,10 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* PERSON INVENTORY SCREEN
|
||||
* View all trained face models
|
||||
*
|
||||
* Features:
|
||||
* - List all trained people
|
||||
* - Show stats (training count, tagged photos, confidence)
|
||||
* - Delete models
|
||||
* - View photos containing each person
|
||||
*/
|
||||
composable(AppRoutes.INVENTORY) {
|
||||
PersonInventoryScreen(
|
||||
onViewPersonPhotos = { personId ->
|
||||
// Navigate back to search
|
||||
// TODO: In future, add person filter to search screen
|
||||
navController.navigate(AppRoutes.SEARCH)
|
||||
}
|
||||
)
|
||||
@@ -165,22 +179,13 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* TRAINING FLOW
|
||||
* Train new face recognition model
|
||||
*
|
||||
* Flow:
|
||||
* 1. TrainingScreen (select images button)
|
||||
* 2. ImageSelectorScreen (pick 15-50 photos)
|
||||
* 3. ScanResultsScreen (validation + name input)
|
||||
* 4. Training completes → navigate to Inventory
|
||||
*/
|
||||
composable(AppRoutes.TRAIN) { entry ->
|
||||
val trainViewModel: TrainViewModel = hiltViewModel()
|
||||
val uiState by trainViewModel.uiState.collectAsState()
|
||||
|
||||
// Get images selected from ImageSelector
|
||||
val selectedUris = entry.savedStateHandle.get<List<Uri>>("selected_image_uris")
|
||||
|
||||
// Start scanning when new images are selected
|
||||
LaunchedEffect(selectedUris) {
|
||||
if (selectedUris != null && uiState is ScanningState.Idle) {
|
||||
trainViewModel.scanAndTagFaces(selectedUris)
|
||||
@@ -190,7 +195,6 @@ fun AppNavHost(
|
||||
|
||||
when (uiState) {
|
||||
is ScanningState.Idle -> {
|
||||
// Show start screen with "Select Images" button
|
||||
TrainingScreen(
|
||||
onSelectImages = {
|
||||
navController.navigate(AppRoutes.IMAGE_SELECTOR)
|
||||
@@ -198,11 +202,9 @@ fun AppNavHost(
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Show validation results and training UI
|
||||
ScanResultsScreen(
|
||||
state = uiState,
|
||||
onFinish = {
|
||||
// After training, go to inventory to see new person
|
||||
navController.navigate(AppRoutes.INVENTORY) {
|
||||
popUpTo(AppRoutes.TRAIN) { inclusive = true }
|
||||
}
|
||||
@@ -214,12 +216,10 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* IMAGE SELECTOR SCREEN
|
||||
* Pick images for training (internal screen)
|
||||
*/
|
||||
composable(AppRoutes.IMAGE_SELECTOR) {
|
||||
ImageSelectorScreen(
|
||||
onImagesSelected = { uris ->
|
||||
// Pass selected URIs back to Train screen
|
||||
navController.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("selected_image_uris", uris)
|
||||
@@ -230,7 +230,6 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* MODELS SCREEN
|
||||
* AI model management (placeholder)
|
||||
*/
|
||||
composable(AppRoutes.MODELS) {
|
||||
DummyScreen(
|
||||
@@ -245,7 +244,6 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* TAGS SCREEN
|
||||
* Manage photo tags with auto-tagging features
|
||||
*/
|
||||
composable(AppRoutes.TAGS) {
|
||||
TagManagementScreen()
|
||||
@@ -253,7 +251,6 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* UTILITIES SCREEN
|
||||
* Photo collection management tools
|
||||
*/
|
||||
composable(AppRoutes.UTILITIES) {
|
||||
PhotoUtilitiesScreen()
|
||||
@@ -265,7 +262,6 @@ fun AppNavHost(
|
||||
|
||||
/**
|
||||
* SETTINGS SCREEN
|
||||
* App preferences (placeholder)
|
||||
*/
|
||||
composable(AppRoutes.SETTINGS) {
|
||||
DummyScreen(
|
||||
|
||||
@@ -28,6 +28,9 @@ tensorflow-lite = "2.14.0"
|
||||
tensorflow-lite-support = "0.4.4"
|
||||
gson = "2.10.1"
|
||||
|
||||
#Album/Image View Tools
|
||||
zoomable = "1.6.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
@@ -68,6 +71,10 @@ tensorflow-lite-gpu = { group = "org.tensorflow", name = "tensorflow-lite-gpu",
|
||||
|
||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||
|
||||
|
||||
#Album/Image View Tools
|
||||
zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user