Added onClick from Albumviewscreen.kt
This commit is contained in:
@@ -85,4 +85,7 @@ dependencies {
|
|||||||
|
|
||||||
// Gson for storing FloatArrays in Room
|
// Gson for storing FloatArrays in Room
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
|
||||||
|
// Zoomable
|
||||||
|
implementation(libs.zoomable)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.placeholder.sherpai2.ui.album
|
package com.placeholder.sherpai2.ui.album
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.grid.*
|
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
|
import com.placeholder.sherpai2.ui.search.components.ImageGridItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AlbumViewScreen - Beautiful album detail view
|
* AlbumViewScreen - UPDATED with clickable images
|
||||||
*
|
*
|
||||||
* Features:
|
* Changes:
|
||||||
* - Album stats
|
* - PhotoCard now clickable
|
||||||
* - Search within album
|
* - Passes onImageClick to ImageGridItem
|
||||||
* - Date filtering
|
* - Entire card surface clickable as backup
|
||||||
* - Simple/Verbose toggle
|
|
||||||
* - Clean person display
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -279,6 +278,9 @@ private fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, labe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PhotoCard - UPDATED: Now fully clickable
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun PhotoCard(
|
private fun PhotoCard(
|
||||||
photo: AlbumPhoto,
|
photo: AlbumPhoto,
|
||||||
@@ -286,15 +288,19 @@ private fun PhotoCard(
|
|||||||
onImageClick: (String) -> Unit
|
onImageClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onImageClick(photo.image.imageUri) }, // ✅ ENTIRE CARD CLICKABLE
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
// Image (also clickable via ImageGridItem)
|
||||||
ImageGridItem(
|
ImageGridItem(
|
||||||
image = photo.image,
|
image = photo.image,
|
||||||
onClick = { onImageClick(photo.image.imageUri) }
|
onClick = { onImageClick(photo.image.imageUri) } // ✅ IMAGE CLICKABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Person tags
|
||||||
if (photo.persons.isNotEmpty()) {
|
if (photo.persons.isNotEmpty()) {
|
||||||
when (displayMode) {
|
when (displayMode) {
|
||||||
DisplayMode.SIMPLE -> {
|
DisplayMode.SIMPLE -> {
|
||||||
|
|||||||
@@ -1,86 +1,322 @@
|
|||||||
package com.placeholder.sherpai2.ui.imagedetail
|
package com.placeholder.sherpai2.ui.imagedetail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.placeholder.sherpai2.data.local.entity.TagEntity
|
||||||
import com.placeholder.sherpai2.ui.imagedetail.viewmodel.ImageDetailViewModel
|
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:
|
* Features:
|
||||||
* - Add tags
|
* - Full-screen zoomable image
|
||||||
* - Remove tags
|
* - Previous/Next navigation buttons
|
||||||
* - Validate write propagation
|
* - Image counter (3/45)
|
||||||
|
* - Tags button (toggle show/hide)
|
||||||
|
* - Shows all tags on photo
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageDetailScreen(
|
fun ImageDetailScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageUri: String,
|
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) {
|
LaunchedEffect(imageUri) {
|
||||||
viewModel.loadImage(imageUri)
|
viewModel.loadImage(imageUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tags by viewModel.tags.collectAsStateWithLifecycle()
|
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(
|
Scaffold(
|
||||||
modifier = modifier
|
topBar = {
|
||||||
.fillMaxSize()
|
TopAppBar(
|
||||||
.padding(12.dp)
|
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(
|
// Previous button
|
||||||
model = imageUri,
|
if (navController != null && allImageUris.isNotEmpty()) {
|
||||||
contentDescription = null,
|
IconButton(
|
||||||
modifier = Modifier
|
onClick = {
|
||||||
.fillMaxWidth()
|
if (canGoPrevious) {
|
||||||
.aspectRatio(1f)
|
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))
|
// Next button
|
||||||
|
IconButton(
|
||||||
OutlinedTextField(
|
onClick = {
|
||||||
value = newTag,
|
if (canGoNext) {
|
||||||
onValueChange = { newTag = it },
|
val nextUri = allImageUris[currentIndex + 1]
|
||||||
label = { Text("Add tag") },
|
val encoded = URLEncoder.encode(nextUri, "UTF-8")
|
||||||
modifier = Modifier.fillMaxWidth()
|
navController.navigate("image_detail/$encoded") {
|
||||||
)
|
popUpTo("image_detail/${URLEncoder.encode(imageUri, "UTF-8")}") {
|
||||||
|
inclusive = true
|
||||||
Button(
|
}
|
||||||
onClick = {
|
}
|
||||||
viewModel.addTag(newTag)
|
}
|
||||||
newTag = ""
|
},
|
||||||
},
|
enabled = canGoNext
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
) {
|
||||||
) {
|
Icon(Icons.Default.KeyboardArrowRight, "Next")
|
||||||
Text("Add Tag")
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
) { paddingValues ->
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Column(
|
||||||
|
modifier = modifier
|
||||||
tags.forEach { tag ->
|
.fillMaxSize()
|
||||||
Row(
|
.padding(paddingValues)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
// Zoomable image
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.background(Color.Black)
|
||||||
) {
|
) {
|
||||||
Text(tag.value)
|
val zoomState = rememberZoomState()
|
||||||
TextButton(onClick = { viewModel.removeTag(tag) }) {
|
|
||||||
Text("Remove")
|
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.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -14,6 +15,7 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.placeholder.sherpai2.ui.devscreens.DummyScreen
|
import com.placeholder.sherpai2.ui.devscreens.DummyScreen
|
||||||
import com.placeholder.sherpai2.ui.album.AlbumViewScreen
|
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.explore.ExploreScreen
|
||||||
import com.placeholder.sherpai2.ui.imagedetail.ImageDetailScreen
|
import com.placeholder.sherpai2.ui.imagedetail.ImageDetailScreen
|
||||||
import com.placeholder.sherpai2.ui.modelinventory.PersonInventoryScreen
|
import com.placeholder.sherpai2.ui.modelinventory.PersonInventoryScreen
|
||||||
@@ -30,20 +32,12 @@ import java.net.URLDecoder
|
|||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppNavHost - Main navigation graph
|
* AppNavHost - UPDATED with image list navigation
|
||||||
* UPDATED: Added Explore and Tags screens
|
|
||||||
*
|
*
|
||||||
* Complete flow:
|
* Changes:
|
||||||
* - Photo browsing (Search, Explore, Detail)
|
* - Search/Album screens pass full image list to detail screen
|
||||||
* - Face recognition (Inventory, Train)
|
* - Detail screen can navigate prev/next
|
||||||
* - Organization (Tags, Upload)
|
* - Image URIs stored in SavedStateHandle for navigation
|
||||||
* - Settings
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - URL encoding for safe navigation
|
|
||||||
* - Proper back stack management
|
|
||||||
* - State preservation
|
|
||||||
* - Beautiful placeholders
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
@@ -61,19 +55,27 @@ fun AppNavHost(
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SEARCH SCREEN
|
* SEARCH SCREEN - UPDATED: Stores image list for navigation
|
||||||
* Main photo browser with face tag search
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.SEARCH) {
|
composable(AppRoutes.SEARCH) {
|
||||||
val searchViewModel: SearchViewModel = hiltViewModel()
|
val searchViewModel: SearchViewModel = hiltViewModel()
|
||||||
|
val images by searchViewModel
|
||||||
|
.searchImages()
|
||||||
|
.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
SearchScreen(
|
SearchScreen(
|
||||||
searchViewModel = searchViewModel,
|
searchViewModel = searchViewModel,
|
||||||
onImageClick = { imageUri ->
|
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")
|
val encodedUri = URLEncoder.encode(imageUri, "UTF-8")
|
||||||
navController.navigate("${AppRoutes.IMAGE_DETAIL}/$encodedUri")
|
navController.navigate("${AppRoutes.IMAGE_DETAIL}/$encodedUri")
|
||||||
},
|
},
|
||||||
onAlbumClick = { tagValue ->
|
onAlbumClick = { tagValue ->
|
||||||
// Navigate to tag-based album
|
|
||||||
navController.navigate("album/tag/$tagValue")
|
navController.navigate("album/tag/$tagValue")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -81,7 +83,6 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* EXPLORE SCREEN
|
* EXPLORE SCREEN
|
||||||
* Browse smart albums (auto-generated from tags)
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.EXPLORE) {
|
composable(AppRoutes.EXPLORE) {
|
||||||
ExploreScreen(
|
ExploreScreen(
|
||||||
@@ -92,8 +93,7 @@ fun AppNavHost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IMAGE DETAIL SCREEN
|
* IMAGE DETAIL SCREEN - UPDATED: Receives image list for navigation
|
||||||
* Single photo view with metadata
|
|
||||||
*/
|
*/
|
||||||
composable(
|
composable(
|
||||||
route = "${AppRoutes.IMAGE_DETAIL}/{imageUri}",
|
route = "${AppRoutes.IMAGE_DETAIL}/{imageUri}",
|
||||||
@@ -107,15 +107,22 @@ fun AppNavHost(
|
|||||||
?.let { URLDecoder.decode(it, "UTF-8") }
|
?.let { URLDecoder.decode(it, "UTF-8") }
|
||||||
?: error("imageUri missing from navigation")
|
?: error("imageUri missing from navigation")
|
||||||
|
|
||||||
|
// Get image list from previous screen
|
||||||
|
val allImageUris = navController.previousBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.get<List<String>>("all_image_uris")
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
ImageDetailScreen(
|
ImageDetailScreen(
|
||||||
imageUri = imageUri,
|
imageUri = imageUri,
|
||||||
onBack = { navController.popBackStack() }
|
onBack = { navController.popBackStack() },
|
||||||
|
navController = navController,
|
||||||
|
allImageUris = allImageUris
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ALBUM VIEW SCREEN
|
* ALBUM VIEW SCREEN - UPDATED: Stores image list for navigation
|
||||||
* View photos in a specific album (tag, person, or time-based)
|
|
||||||
*/
|
*/
|
||||||
composable(
|
composable(
|
||||||
route = "album/{albumType}/{albumId}",
|
route = "album/{albumType}/{albumId}",
|
||||||
@@ -128,11 +135,27 @@ fun AppNavHost(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
val albumViewModel: AlbumViewModel = hiltViewModel()
|
||||||
|
val uiState by albumViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
AlbumViewScreen(
|
AlbumViewScreen(
|
||||||
onBack = {
|
onBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
onImageClick = { imageUri ->
|
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")
|
val encodedUri = URLEncoder.encode(imageUri, "UTF-8")
|
||||||
navController.navigate("${AppRoutes.IMAGE_DETAIL}/$encodedUri")
|
navController.navigate("${AppRoutes.IMAGE_DETAIL}/$encodedUri")
|
||||||
}
|
}
|
||||||
@@ -145,19 +168,10 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PERSON INVENTORY SCREEN
|
* 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) {
|
composable(AppRoutes.INVENTORY) {
|
||||||
PersonInventoryScreen(
|
PersonInventoryScreen(
|
||||||
onViewPersonPhotos = { personId ->
|
onViewPersonPhotos = { personId ->
|
||||||
// Navigate back to search
|
|
||||||
// TODO: In future, add person filter to search screen
|
|
||||||
navController.navigate(AppRoutes.SEARCH)
|
navController.navigate(AppRoutes.SEARCH)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -165,22 +179,13 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TRAINING FLOW
|
* 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 ->
|
composable(AppRoutes.TRAIN) { entry ->
|
||||||
val trainViewModel: TrainViewModel = hiltViewModel()
|
val trainViewModel: TrainViewModel = hiltViewModel()
|
||||||
val uiState by trainViewModel.uiState.collectAsState()
|
val uiState by trainViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// Get images selected from ImageSelector
|
|
||||||
val selectedUris = entry.savedStateHandle.get<List<Uri>>("selected_image_uris")
|
val selectedUris = entry.savedStateHandle.get<List<Uri>>("selected_image_uris")
|
||||||
|
|
||||||
// Start scanning when new images are selected
|
|
||||||
LaunchedEffect(selectedUris) {
|
LaunchedEffect(selectedUris) {
|
||||||
if (selectedUris != null && uiState is ScanningState.Idle) {
|
if (selectedUris != null && uiState is ScanningState.Idle) {
|
||||||
trainViewModel.scanAndTagFaces(selectedUris)
|
trainViewModel.scanAndTagFaces(selectedUris)
|
||||||
@@ -190,7 +195,6 @@ fun AppNavHost(
|
|||||||
|
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
is ScanningState.Idle -> {
|
is ScanningState.Idle -> {
|
||||||
// Show start screen with "Select Images" button
|
|
||||||
TrainingScreen(
|
TrainingScreen(
|
||||||
onSelectImages = {
|
onSelectImages = {
|
||||||
navController.navigate(AppRoutes.IMAGE_SELECTOR)
|
navController.navigate(AppRoutes.IMAGE_SELECTOR)
|
||||||
@@ -198,11 +202,9 @@ fun AppNavHost(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Show validation results and training UI
|
|
||||||
ScanResultsScreen(
|
ScanResultsScreen(
|
||||||
state = uiState,
|
state = uiState,
|
||||||
onFinish = {
|
onFinish = {
|
||||||
// After training, go to inventory to see new person
|
|
||||||
navController.navigate(AppRoutes.INVENTORY) {
|
navController.navigate(AppRoutes.INVENTORY) {
|
||||||
popUpTo(AppRoutes.TRAIN) { inclusive = true }
|
popUpTo(AppRoutes.TRAIN) { inclusive = true }
|
||||||
}
|
}
|
||||||
@@ -214,12 +216,10 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* IMAGE SELECTOR SCREEN
|
* IMAGE SELECTOR SCREEN
|
||||||
* Pick images for training (internal screen)
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.IMAGE_SELECTOR) {
|
composable(AppRoutes.IMAGE_SELECTOR) {
|
||||||
ImageSelectorScreen(
|
ImageSelectorScreen(
|
||||||
onImagesSelected = { uris ->
|
onImagesSelected = { uris ->
|
||||||
// Pass selected URIs back to Train screen
|
|
||||||
navController.previousBackStackEntry
|
navController.previousBackStackEntry
|
||||||
?.savedStateHandle
|
?.savedStateHandle
|
||||||
?.set("selected_image_uris", uris)
|
?.set("selected_image_uris", uris)
|
||||||
@@ -230,7 +230,6 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* MODELS SCREEN
|
* MODELS SCREEN
|
||||||
* AI model management (placeholder)
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.MODELS) {
|
composable(AppRoutes.MODELS) {
|
||||||
DummyScreen(
|
DummyScreen(
|
||||||
@@ -245,7 +244,6 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TAGS SCREEN
|
* TAGS SCREEN
|
||||||
* Manage photo tags with auto-tagging features
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.TAGS) {
|
composable(AppRoutes.TAGS) {
|
||||||
TagManagementScreen()
|
TagManagementScreen()
|
||||||
@@ -253,7 +251,6 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* UTILITIES SCREEN
|
* UTILITIES SCREEN
|
||||||
* Photo collection management tools
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.UTILITIES) {
|
composable(AppRoutes.UTILITIES) {
|
||||||
PhotoUtilitiesScreen()
|
PhotoUtilitiesScreen()
|
||||||
@@ -265,7 +262,6 @@ fun AppNavHost(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SETTINGS SCREEN
|
* SETTINGS SCREEN
|
||||||
* App preferences (placeholder)
|
|
||||||
*/
|
*/
|
||||||
composable(AppRoutes.SETTINGS) {
|
composable(AppRoutes.SETTINGS) {
|
||||||
DummyScreen(
|
DummyScreen(
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ tensorflow-lite = "2.14.0"
|
|||||||
tensorflow-lite-support = "0.4.4"
|
tensorflow-lite-support = "0.4.4"
|
||||||
gson = "2.10.1"
|
gson = "2.10.1"
|
||||||
|
|
||||||
|
#Album/Image View Tools
|
||||||
|
zoomable = "1.6.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
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" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user