code review and fix

home, add profile, camera capture
This commit is contained in:
SaiD 2025-12-20 13:37:33 +05:30
parent d5902cba08
commit a6ea1d5ce0
16 changed files with 359 additions and 571 deletions

View File

@ -43,7 +43,6 @@ class MainActivity : ComponentActivity() {
val settings by appDataUseCases.getSettings().collectAsState(initial = null) val settings by appDataUseCases.getSettings().collectAsState(initial = null)
val context = LocalContext.current val context = LocalContext.current
// Update locale and provide it through CompositionLocalProvider
val localizedContext = settings?.let { val localizedContext = settings?.let {
LocaleHelper.applyLocale(context, it.language) LocaleHelper.applyLocale(context, it.language)
} ?: context } ?: context
@ -65,13 +64,7 @@ class MainActivity : ComponentActivity() {
) )
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) { Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) {
val startDestination = viewModel.startDestination.value val startDestination = viewModel.startDestination.value
// Ensure startDestination is not null before rendering NavGraph NavGraph(startDestination = startDestination)
if (startDestination != null) {
NavGraph(startDestination = startDestination)
} else {
// Optional: Show a loading indicator if startDestination is null
// for an extended period, though splash screen should handle it.
}
} }
} }
} }

View File

@ -247,9 +247,17 @@ class CSVDataSource(
calvingNumber = row[INDEX_CALVING].toIntOrNull() ?: 0, calvingNumber = row[INDEX_CALVING].toIntOrNull() ?: 0,
reproductiveStatus = row[INDEX_REPRO], reproductiveStatus = row[INDEX_REPRO],
description = row[INDEX_DESC], description = row[INDEX_DESC],
images = row[INDEX_IMAGES].split(";").filter { it.isNotBlank() }, images = row[INDEX_IMAGES].split(';').asSequence().filter { it.isNotBlank() }
.map { pair ->
val (k, v) = pair.split('=', limit = 2)
k to v
}.toMap(),
video = row[INDEX_VIDEO], video = row[INDEX_VIDEO],
segmentedImages = row.getOrNull(INDEX_SEGMENTED_IMAGES)?.split(";")?.filter { it.isNotBlank() } ?: emptyList() segmentedImages = row.getOrNull(INDEX_SEGMENTED_IMAGES)?.split(';')?.asSequence()?.filter { it.isNotBlank() }
?.map { pair ->
val (k, v) = pair.split('=', limit = 2)
k to v
}?.toMap() ?: emptyMap()
) )
} }
@ -313,9 +321,9 @@ class CSVDataSource(
row[INDEX_CALVING] = d.calvingNumber.toString() row[INDEX_CALVING] = d.calvingNumber.toString()
row[INDEX_REPRO] = d.reproductiveStatus row[INDEX_REPRO] = d.reproductiveStatus
row[INDEX_DESC] = d.description row[INDEX_DESC] = d.description
row[INDEX_IMAGES] = d.images.joinToString(";") row[INDEX_IMAGES] = d.images.entries.joinToString(";") { (k, v) -> "$k=$v" }
row[INDEX_VIDEO] = d.video row[INDEX_VIDEO] = d.video
row[INDEX_SEGMENTED_IMAGES] = d.segmentedImages.joinToString(";") row[INDEX_SEGMENTED_IMAGES] = d.segmentedImages.entries.joinToString(";") { (k, v) -> "$k=$v" }
return row return row
} }

View File

@ -16,13 +16,5 @@ class AnimalDetailsRepositoryImpl(
dataSource.setAnimalDetails(animalDetails) dataSource.setAnimalDetails(animalDetails)
} }
override suspend fun deleteAnimalDetails(id: String) { override suspend fun deleteAnimalDetails(id: String) { }
// Currently only full profile deletion is exposed by DataSource as per request
// but we can call that if needed, or just leave it as no-op if details deletion is specific
// Assuming for now it deletes the profile or we might need to add specific delete to DataSource
// But the prompt said "DataSource should have get, set and delete... get and delete based on a string which will be id"
// And "deleteAnimalProfile - takes an Id deletes that animals complete profile"
// So strictly for details, maybe we don't have a specific delete or we just don't impl it yet.
// However, to satisfy the interface:
}
} }

View File

@ -181,7 +181,7 @@ val appModule = module {
viewModel { HomeViewModel(get()) } viewModel { HomeViewModel(get()) }
viewModel { OnBoardingViewModel(get()) } viewModel { OnBoardingViewModel(get()) }
viewModel { (savedStateHandle: androidx.lifecycle.SavedStateHandle?) -> viewModel { (savedStateHandle: androidx.lifecycle.SavedStateHandle?) ->
AddProfileViewModel(get(), get(), get(), androidContext(), savedStateHandle) AddProfileViewModel(get(), get(), androidContext(), savedStateHandle)
} }
viewModel { ListingsViewModel(get()) } viewModel { ListingsViewModel(get()) }
viewModel { SettingsViewModel(get()) } viewModel { SettingsViewModel(get()) }

View File

@ -8,7 +8,6 @@ import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns
import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
@ -19,254 +18,119 @@ import kotlin.coroutines.resumeWithException
class SubjectSegmenterHelper(private val context: Context) { class SubjectSegmenterHelper(private val context: Context) {
suspend fun segmentToBitmap(inputBitmap: Bitmap): Bitmap? { private suspend fun segmentInternal(image: InputImage): Bitmap? =
return suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
try {
val image = InputImage.fromBitmap(inputBitmap, 0)
val options = SubjectSegmenterOptions.Builder()
.enableMultipleSubjects(
SubjectSegmenterOptions.SubjectResultOptions.Builder()
.enableSubjectBitmap()
.build()
)
.build()
val segmenter = SubjectSegmentation.getClient(options)
segmenter.process(image) val options = SubjectSegmenterOptions.Builder()
.addOnSuccessListener { result -> .enableMultipleSubjects(
val subjects = result.subjects SubjectSegmenterOptions.SubjectResultOptions.Builder()
if (subjects.isNotEmpty()) { .enableSubjectBitmap()
// Find the largest subject .build()
val mainSubject = subjects.maxByOrNull { it.width * it.height } )
.build()
if (mainSubject != null && mainSubject.bitmap != null) {
try { val segmenter = SubjectSegmentation.getClient(options)
val resultBitmap = Bitmap.createBitmap(
image.width, segmenter.process(image)
image.height, .addOnSuccessListener { result ->
Bitmap.Config.ARGB_8888 val subject = result.subjects
) .maxByOrNull { it.width * it.height }
val canvas = Canvas(resultBitmap)
canvas.drawColor(Color.BLACK) if (subject?.bitmap == null) {
continuation.resume(null)
val subjectBitmap = mainSubject.bitmap!! return@addOnSuccessListener
canvas.drawBitmap(
subjectBitmap,
mainSubject.startX.toFloat(),
mainSubject.startY.toFloat(),
null
)
continuation.resume(resultBitmap)
} catch (e: Exception) {
continuation.resumeWithException(e)
}
} else {
continuation.resume(null)
}
} else {
continuation.resume(null)
}
} }
.addOnFailureListener { e ->
try {
val output = Bitmap.createBitmap(
image.width,
image.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(output)
canvas.drawColor(Color.BLACK)
canvas.drawBitmap(
subject.bitmap!!,
subject.startX.toFloat(),
subject.startY.toFloat(),
null
)
continuation.resume(output)
} catch (e: Exception) {
continuation.resumeWithException(e) continuation.resumeWithException(e)
} }
.addOnCompleteListener {
segmenter.close()
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
suspend fun segmentAndSave(inputBitmap: Bitmap, animalId: String, orientation: String, subFolder: String? = null): Uri? {
return suspendCancellableCoroutine { continuation ->
try {
val image = InputImage.fromBitmap(inputBitmap, 0)
val options = SubjectSegmenterOptions.Builder()
.enableMultipleSubjects(
SubjectSegmenterOptions.SubjectResultOptions.Builder()
.enableSubjectBitmap()
.build()
)
.build()
val segmenter = SubjectSegmentation.getClient(options)
segmenter.process(image)
.addOnSuccessListener { result ->
val subjects = result.subjects
if (subjects.isNotEmpty()) {
// Find the largest subject
val mainSubject = subjects.maxByOrNull { it.width * it.height }
if (mainSubject != null && mainSubject.bitmap != null) {
try {
val resultBitmap = Bitmap.createBitmap(
image.width,
image.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(resultBitmap)
canvas.drawColor(Color.BLACK)
val subjectBitmap = mainSubject.bitmap!!
canvas.drawBitmap(
subjectBitmap,
mainSubject.startX.toFloat(),
mainSubject.startY.toFloat(),
null
)
val filename = "${animalId}_${orientation}_segmented.jpg"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val path = if (subFolder != null) "Pictures/LivingAI/$animalId/$subFolder" else "Pictures/LivingAI/$animalId"
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
}
}
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
val outputStream: OutputStream? = context.contentResolver.openOutputStream(uri)
outputStream?.use { out ->
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
}
continuation.resume(uri)
} else {
continuation.resume(null)
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
} else {
continuation.resume(null)
}
} else {
continuation.resume(null)
}
}
.addOnFailureListener { e ->
continuation.resumeWithException(e)
}
.addOnCompleteListener {
segmenter.close()
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
suspend fun segmentAndSave(inputUri: Uri, animalId: String, orientation: String, subFolder: String? = null): Uri? {
return suspendCancellableCoroutine { continuation ->
try {
val image = InputImage.fromFilePath(context, inputUri)
val options = SubjectSegmenterOptions.Builder()
.enableMultipleSubjects(
SubjectSegmenterOptions.SubjectResultOptions.Builder()
.enableSubjectBitmap()
.build()
)
.build()
val segmenter = SubjectSegmentation.getClient(options)
segmenter.process(image)
.addOnSuccessListener { result ->
val subjects = result.subjects
if (subjects.isNotEmpty()) {
// Find the largest subject (assuming it's the one in front/main subject)
val mainSubject = subjects.maxByOrNull { it.width * it.height }
if (mainSubject != null && mainSubject.bitmap != null) {
try {
val resultBitmap = Bitmap.createBitmap(
image.width,
image.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(resultBitmap)
canvas.drawColor(Color.BLACK)
val subjectBitmap = mainSubject.bitmap!!
canvas.drawBitmap(
subjectBitmap,
mainSubject.startX.toFloat(),
mainSubject.startY.toFloat(),
null
)
val filename = "${animalId}_${orientation}_segmented.jpg"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val path = if (subFolder != null) "Pictures/LivingAI/$animalId/$subFolder" else "Pictures/LivingAI/$animalId"
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
}
}
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
val outputStream: OutputStream? = context.contentResolver.openOutputStream(uri)
outputStream?.use { out ->
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
}
continuation.resume(uri)
} else {
continuation.resume(null)
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
} else {
continuation.resume(null)
}
} else {
continuation.resume(null)
}
}
.addOnFailureListener { e ->
continuation.resumeWithException(e)
}
.addOnCompleteListener {
segmenter.close()
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
private fun getFileName(uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (index >= 0) {
result = cursor.getString(index)
}
} }
} catch (e: Exception) { .addOnFailureListener { e ->
// ignore continuation.resumeWithException(e)
} finally { }
cursor?.close() .addOnCompleteListener {
segmenter.close()
}
}
suspend fun segmentToBitmap(inputBitmap: Bitmap): Bitmap? {
val image = InputImage.fromBitmap(inputBitmap, 0)
return segmentInternal(image)
}
suspend fun segmentAndSave(
inputBitmap: Bitmap,
animalId: String,
orientation: String,
subFolder: String? = null
): Uri? {
val image = InputImage.fromBitmap(inputBitmap, 0)
val bitmap = segmentInternal(image) ?: return null
return saveBitmap(bitmap, animalId, orientation, subFolder)
}
suspend fun segmentAndSave(
inputUri: Uri,
animalId: String,
orientation: String,
subFolder: String? = null
): Uri? {
val image = InputImage.fromFilePath(context, inputUri)
val bitmap = segmentInternal(image) ?: return null
return saveBitmap(bitmap, animalId, orientation, subFolder)
}
private fun saveBitmap(
bitmap: Bitmap,
animalId: String,
orientation: String,
subFolder: String?
): Uri? {
val filename = "${animalId}_${orientation}_segmented.jpg"
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val path =
if (subFolder != null)
"Pictures/LivingAI/$animalId/$subFolder"
else
"Pictures/LivingAI/$animalId"
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
} }
} }
if (result == null) {
result = uri.path val uri = context.contentResolver.insert(
val cut = result?.lastIndexOf('/') MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
if (cut != null && cut != -1) { values
result = result?.substring(cut + 1) ) ?: return null
}
val outputStream: OutputStream? =
context.contentResolver.openOutputStream(uri)
outputStream?.use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
} }
return result
return uri
} }
} }

View File

@ -12,7 +12,7 @@ data class AnimalDetails(
val calvingNumber: Int, val calvingNumber: Int,
val reproductiveStatus: String, val reproductiveStatus: String,
val description: String, val description: String,
val images: List<String>, val images: Map<String, String>,
val video: String, val video: String,
val segmentedImages: List<String> = emptyList() val segmentedImages: Map<String, String>
) )

View File

@ -47,6 +47,8 @@ import com.example.livingai.utils.Constants
fun AddProfileScreen( fun AddProfileScreen(
navController: NavController, navController: NavController,
viewModel: AddProfileViewModel, viewModel: AddProfileViewModel,
animalId: String?,
loadEntry: Boolean,
onSave: () -> Unit, onSave: () -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
onTakePhoto: (String) -> Unit, onTakePhoto: (String) -> Unit,
@ -54,11 +56,12 @@ fun AddProfileScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
// If opened for edit, attempt to load existing animal details LaunchedEffect(animalId, loadEntry) {
LaunchedEffect(Unit) { if (loadEntry && animalId != null) {
val existing = viewModel.savedStateHandle?.get<String>("animalId") viewModel.loadAnimal(animalId)
val loadEntry = viewModel.savedStateHandle?.get<Boolean>("loadEntry") } else {
if (existing != null && loadEntry == true) viewModel.loadAnimal(existing) viewModel.initializeNewProfile()
}
} }
val speciesList = stringArrayResource(id = R.array.species_list).toList() val speciesList = stringArrayResource(id = R.array.species_list).toList()
@ -137,7 +140,7 @@ fun AddProfileScreen(
labelRes = R.string.label_species, labelRes = R.string.label_species,
options = speciesList, options = speciesList,
selected = species, selected = species,
onSelected = { species = it }, onSelected = viewModel::validateSpeciesInputs,
modifier = Modifier.focusRequester(speciesFocus), modifier = Modifier.focusRequester(speciesFocus),
isError = speciesError != null, isError = speciesError != null,
supportingText = speciesError supportingText = speciesError
@ -147,7 +150,7 @@ fun AddProfileScreen(
labelRes = R.string.label_breed, labelRes = R.string.label_breed,
options = breedList, options = breedList,
selected = breed, selected = breed,
onSelected = { breed = it }, onSelected = viewModel::validateBreedInputs,
modifier = Modifier.focusRequester(breedFocus), modifier = Modifier.focusRequester(breedFocus),
isError = breedError != null, isError = breedError != null,
supportingText = breedError supportingText = breedError
@ -163,7 +166,7 @@ fun AddProfileScreen(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.focusRequester(ageFocus), .focusRequester(ageFocus),
onValueChange = { age = it }, onValueChange = viewModel::validateAgeInputs,
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
isError = ageError != null, isError = ageError != null,
supportingText = ageError supportingText = ageError
@ -175,7 +178,7 @@ fun AddProfileScreen(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.focusRequester(milkYieldFocus), .focusRequester(milkYieldFocus),
onValueChange = { milkYield = it }, onValueChange = viewModel::validateMilkYieldInputs,
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
isError = milkYieldError != null, isError = milkYieldError != null,
supportingText = milkYieldError supportingText = milkYieldError
@ -188,7 +191,7 @@ fun AddProfileScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(calvingNumberFocus), .focusRequester(calvingNumberFocus),
onValueChange = { calvingNumber = it }, onValueChange = viewModel::validateCalvingInputs,
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
isError = calvingNumberError != null, isError = calvingNumberError != null,
supportingText = calvingNumberError supportingText = calvingNumberError
@ -198,7 +201,7 @@ fun AddProfileScreen(
titleRes = R.string.label_reproductive_status, titleRes = R.string.label_reproductive_status,
options = reproList, options = reproList,
selected = reproductiveStatus, selected = reproductiveStatus,
onSelected = { reproductiveStatus = it }, onSelected = viewModel::validateReproductiveStatusInputs,
isError = reproductiveStatusError != null isError = reproductiveStatusError != null
) )
if (reproductiveStatusError != null) { if (reproductiveStatusError != null) {

View File

@ -22,7 +22,6 @@ import kotlinx.coroutines.withContext
class AddProfileViewModel( class AddProfileViewModel(
private val profileEntryUseCase: ProfileEntryUseCase, private val profileEntryUseCase: ProfileEntryUseCase,
private val getAnimalDetails: GetAnimalDetails,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val context: Context, private val context: Context,
val savedStateHandle: SavedStateHandle? = null val savedStateHandle: SavedStateHandle? = null
@ -57,99 +56,43 @@ class AddProfileViewModel(
// State for photos and video // State for photos and video
val photos = mutableStateMapOf<String, String>() val photos = mutableStateMapOf<String, String>()
val segmentedImages = mutableStateMapOf<String, String>()
private val _videoUri = mutableStateOf<String?>(null) private val _videoUri = mutableStateOf<String?>(null)
val videoUri: State<String?> = _videoUri val videoUri: State<String?> = _videoUri
// State for segmented images
val segmentedImages = mutableListOf<String>()
fun loadAnimal(animalId: String?) { fun loadAnimal(animalId: String) {
if (animalId == null) { if (animalId == _currentAnimalId.value) return
val newId = IdGenerator.generateAnimalId()
_currentAnimalId.value = newId
_animalDetails.value = null
// Reset UI State
species.value = null
breed.value = null
age.value = ""
milkYield.value = ""
calvingNumber.value = ""
reproductiveStatus.value = null
description.value = ""
clearErrors()
photos.clear()
segmentedImages.clear()
_videoUri.value = null
} else {
_currentAnimalId.value = animalId
profileEntryUseCase.getAnimalDetails(animalId).onEach { details ->
if (details != null) {
_animalDetails.value = details
// Populate UI State
species.value = details.species.ifBlank { null }
breed.value = details.breed.ifBlank { null }
age.value = if (details.age == 0) "" else details.age.toString()
milkYield.value = if (details.milkYield == 0) "" else details.milkYield.toString()
calvingNumber.value = if (details.calvingNumber == 0) "" else details.calvingNumber.toString()
reproductiveStatus.value = details.reproductiveStatus.ifBlank { null }
description.value = details.description
clearErrors()
// Populate photos
photos.clear()
segmentedImages.clear()
segmentedImages.addAll(details.segmentedImages)
// Process images on IO thread as it may involve DB queries
withContext(dispatchers.io) {
val photoMap = mutableMapOf<String, String>()
details.images.forEach { path ->
val uri = Uri.parse(path)
val filename = getFileName(uri) ?: path.substringAfterLast('/')
val nameWithoutExt = filename.substringBeforeLast('.')
// Skip segmented images for the main thumbnails
if (nameWithoutExt.contains("segmented", ignoreCase = true)) {
return@forEach
}
// Find orientation in filename _currentAnimalId.value = animalId
var foundOrientation: String? = null
for (o in Constants.silhouetteList) { profileEntryUseCase.getAnimalDetails(animalId).onEach { details ->
if (nameWithoutExt.contains(o, ignoreCase = true)) { if (details != null) {
foundOrientation = o _animalDetails.value = details
}
} // Populate UI State
species.value = details.species.ifBlank { null }
val parts = nameWithoutExt.split('_') breed.value = details.breed.ifBlank { null }
val matchingPart = parts.find { part -> age.value = if (details.age == 0) "" else details.age.toString()
Constants.silhouetteList.any { it.equals(part, ignoreCase = true) } milkYield.value = if (details.milkYield == 0) "" else details.milkYield.toString()
} calvingNumber.value = if (details.calvingNumber == 0) "" else details.calvingNumber.toString()
reproductiveStatus.value = details.reproductiveStatus.ifBlank { null }
if (matchingPart != null) { description.value = details.description
val key = Constants.silhouetteList.find { it.equals(matchingPart, ignoreCase = true) } clearErrors()
if (key != null) {
photoMap[key] = path photos.clear()
} segmentedImages.clear()
} else {
val sortedOrientations = Constants.silhouetteList.sortedByDescending { it.length } withContext(dispatchers.main) {
val match = sortedOrientations.find { nameWithoutExt.contains(it, ignoreCase = true) } details.images.entries.forEach { (orientation, path) ->
if (match != null) { photos[orientation] = path
photoMap[match] = path }
} details.segmentedImages.entries.forEach { (orientation, path) ->
} segmentedImages[orientation] = path
}
withContext(dispatchers.main) {
photoMap.forEach { (k, v) -> photos[k] = v }
}
} }
_videoUri.value = details.video.ifBlank { null }
} }
}.launchIn(viewModelScope) _videoUri.value = details.video.ifBlank { null }
} }
}.launchIn(viewModelScope)
} }
private fun clearErrors() { private fun clearErrors() {
@ -160,57 +103,39 @@ class AddProfileViewModel(
breedError.value = null breedError.value = null
reproductiveStatusError.value = null reproductiveStatusError.value = null
} }
private fun getFileName(uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (index >= 0) {
result = cursor.getString(index)
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/')
if (cut != null && cut != -1) {
result = result?.substring(cut + 1)
}
}
return result
}
fun addPhoto(orientation: String, uri: String) { fun addPhoto(orientation: String, uri: String) {
photos[orientation] = uri photos[orientation] = uri
} }
fun addSegmentedImage(uri: String) { fun addSegmentedImage(orientation: String, uri: String) {
if (!segmentedImages.contains(uri)) { segmentedImages[orientation] = uri
segmentedImages.add(uri)
}
} }
fun setVideo(uri: String) { fun setVideo(uri: String) {
_videoUri.value = uri _videoUri.value = uri
} }
fun validateInputs(): Boolean { fun validateSpeciesInputs(s: String? = null): Boolean {
if (s != null)
species.value = s
var isValid = true var isValid = true
if (species.value.isNullOrBlank()) { if (species.value.isNullOrBlank()) {
speciesError.value = "Species is required" speciesError.value = "Species is required"
isValid = false isValid = false
} else { } else {
speciesError.value = null speciesError.value = null
} }
return isValid
}
fun validateBreedInputs(b: String? = null): Boolean {
if (b != null)
breed.value = b
var isValid = true
if (breed.value.isNullOrBlank()) { if (breed.value.isNullOrBlank()) {
breedError.value = "Breed is required" breedError.value = "Breed is required"
@ -219,21 +144,47 @@ class AddProfileViewModel(
breedError.value = null breedError.value = null
} }
return isValid
}
fun validateReproductiveStatusInputs(r: String? = null): Boolean {
if (r != null)
reproductiveStatus.value = r
var isValid = true
if (reproductiveStatus.value.isNullOrBlank()) { if (reproductiveStatus.value.isNullOrBlank()) {
reproductiveStatusError.value = "Status is required" reproductiveStatusError.value = "Status is required"
isValid = false isValid = false
} else { } else {
reproductiveStatusError.value = null reproductiveStatusError.value = null
} }
return isValid
}
fun validateAgeInputs(a: String? = null): Boolean {
if (a != null)
age.value = a
var isValid = true
val ageInt = age.value.toIntOrNull() val ageInt = age.value.toIntOrNull()
if (ageInt == null || ageInt <= 0 || ageInt > 20) { if (ageInt == null || ageInt <= 0 || ageInt > 20) {
ageError.value = "Invalid age" ageError.value = "Invalid age"
isValid = false isValid = false
} else { } else {
ageError.value = null ageError.value = null
} }
return isValid
}
fun validateMilkYieldInputs(m: String? = null): Boolean {
if (m != null)
milkYield.value = m
var isValid = true
val milkInt = milkYield.value.toIntOrNull() val milkInt = milkYield.value.toIntOrNull()
if (milkInt == null || milkInt <= 0 || milkInt > 75) { if (milkInt == null || milkInt <= 0 || milkInt > 75) {
milkYieldError.value = "Invalid milk yield" milkYieldError.value = "Invalid milk yield"
@ -241,6 +192,15 @@ class AddProfileViewModel(
} else { } else {
milkYieldError.value = null milkYieldError.value = null
} }
return isValid
}
fun validateCalvingInputs(c: String? = null): Boolean {
if (c != null)
calvingNumber.value = c
var isValid = true
val calvingInt = calvingNumber.value.toIntOrNull() val calvingInt = calvingNumber.value.toIntOrNull()
if (calvingInt == null || calvingInt < 0 || calvingInt > 12) { if (calvingInt == null || calvingInt < 0 || calvingInt > 12) {
@ -253,6 +213,11 @@ class AddProfileViewModel(
return isValid return isValid
} }
private fun validateInputs(): Boolean {
return validateSpeciesInputs() && validateBreedInputs() && validateAgeInputs()
&& validateMilkYieldInputs() && validateCalvingInputs() && validateReproductiveStatusInputs()
}
fun saveAnimalDetails(): Boolean { fun saveAnimalDetails(): Boolean {
if (!validateInputs()) return false if (!validateInputs()) return false
@ -267,9 +232,9 @@ class AddProfileViewModel(
calvingNumber = calvingNumber.value.toIntOrNull() ?: 0, calvingNumber = calvingNumber.value.toIntOrNull() ?: 0,
reproductiveStatus = reproductiveStatus.value ?: "", reproductiveStatus = reproductiveStatus.value ?: "",
description = description.value, description = description.value,
images = photos.values.toList(), images = photos,
video = _videoUri.value ?: "", video = _videoUri.value ?: "",
segmentedImages = segmentedImages.toList(), segmentedImages = segmentedImages,
name = "", sex = "", weight = 0 name = "", sex = "", weight = 0
) )
@ -285,8 +250,23 @@ class AddProfileViewModel(
_saveSuccess.value = false _saveSuccess.value = false
} }
init { fun initializeNewProfile() {
val animalId: String? = savedStateHandle?.get<String>("animalId") val newId = IdGenerator.generateAnimalId()
loadAnimal(animalId) _currentAnimalId.value = newId
_animalDetails.value = null
// Reset UI State
species.value = null
breed.value = null
age.value = ""
milkYield.value = ""
calvingNumber.value = ""
reproductiveStatus.value = null
description.value = ""
clearErrors()
photos.clear()
segmentedImages.clear()
_videoUri.value = null
} }
} }

View File

@ -134,9 +134,6 @@ fun CameraCaptureScreen(
} }
if (uri != null) { if (uri != null) {
// This screen can be called from AddProfileScreen or ViewImageScreen (for retakes).
// In both cases, we pop the back stack and set the result on the previous entry.
// The NavGraph logic for each screen handles the received URI accordingly.
navController.previousBackStackEntry?.savedStateHandle?.set("newImageUri", uri.toString()) navController.previousBackStackEntry?.savedStateHandle?.set("newImageUri", uri.toString())
navController.previousBackStackEntry?.savedStateHandle?.set("newImageOrientation", orientation) navController.previousBackStackEntry?.savedStateHandle?.set("newImageOrientation", orientation)
navController.popBackStack() navController.popBackStack()
@ -326,33 +323,13 @@ fun SegmentationOverlay(
val offsetX = (canvasWidth - imageWidth * scale) / 2f val offsetX = (canvasWidth - imageWidth * scale) / 2f
val offsetY = (canvasHeight - imageHeight * scale) / 2f val offsetY = (canvasHeight - imageHeight * scale) / 2f
// The mask corresponds to the cropped and resized area of the silhouette,
// but here we are receiving the raw mask from MockPoseAnalyzer which seems to match the resized bitmap
// used for comparison (silhouette.croppedBitmap size).
// However, MockPoseAnalyzer.segment returns a mask of size `bitmap.width * bitmap.height`
// where `bitmap` is the resized crop.
// Wait, looking at MockPoseAnalyzer.analyze:
// 1. crops image to animalBounds
// 2. resizes crop to silhouette.croppedBitmap dimensions
// 3. segments resized crop -> mask
// So the mask is small (e.g. 100x100). We need to draw it scaled up to the animalBounds on screen.
val boxLeft = animalBounds.left * scale + offsetX val boxLeft = animalBounds.left * scale + offsetX
val boxTop = animalBounds.top * scale + offsetY val boxTop = animalBounds.top * scale + offsetY
val boxWidth = animalBounds.width() * scale val boxWidth = animalBounds.width() * scale
val boxHeight = animalBounds.height() * scale val boxHeight = animalBounds.height() * scale
// We need to know the dimensions of the mask grid to draw it properly.
// Since we don't pass dimensions, we can infer if it's square or pass it.
// Assuming square for simplicity as per SilhouetteManager usually?
// Actually, we can just draw points.
val maskSize = kotlin.math.sqrt(mask.size.toDouble()).toInt() val maskSize = kotlin.math.sqrt(mask.size.toDouble()).toInt()
// Ideally we should pass width/height of the mask.
// For now let's assume the mask matches the aspect ratio of the box or is just a grid.
if (maskSize > 0) { if (maskSize > 0) {
val pixelW = boxWidth / maskSize val pixelW = boxWidth / maskSize
val pixelH = boxHeight / maskSize val pixelH = boxHeight / maskSize
@ -395,7 +372,6 @@ fun InstructionOverlay(
color = Color.White, color = Color.White,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
// Visual GIF logic would go here using instruction.animationResId
} }
} }
} }
@ -413,8 +389,6 @@ fun DetectionOverlay(
val canvasWidth = size.width val canvasWidth = size.width
val canvasHeight = size.height val canvasHeight = size.height
// This calculation assumes the camera preview's scale type is `FILL_CENTER`.
// It maintains the aspect ratio of the image and centers it.
val widthRatio = canvasWidth / imageWidth val widthRatio = canvasWidth / imageWidth
val heightRatio = canvasHeight / imageHeight val heightRatio = canvasHeight / imageHeight
val scale = max(widthRatio, heightRatio) val scale = max(widthRatio, heightRatio)
@ -422,7 +396,6 @@ fun DetectionOverlay(
val offsetX = (canvasWidth - imageWidth * scale) / 2f val offsetX = (canvasWidth - imageWidth * scale) / 2f
val offsetY = (canvasHeight - imageHeight * scale) / 2f val offsetY = (canvasHeight - imageHeight * scale) / 2f
// Helper to transform coordinates
val transform: (RectF) -> RectF = { box -> val transform: (RectF) -> RectF = { box ->
RectF( RectF(
box.left * scale + offsetX, box.left * scale + offsetX,
@ -432,7 +405,6 @@ fun DetectionOverlay(
) )
} }
// Draw animal box (Yellow)
detection.animalBounds?.let { detection.animalBounds?.let {
val transformedBox = transform(it) val transformedBox = transform(it)
drawRect( drawRect(
@ -443,7 +415,6 @@ fun DetectionOverlay(
) )
} }
// Draw reference object boxes (Cyan)
detection.referenceObjects.forEach { refObject -> detection.referenceObjects.forEach { refObject ->
val transformedBox = transform(refObject.bounds) val transformedBox = transform(refObject.bounds)
drawRect( drawRect(

View File

@ -1,19 +0,0 @@
// Obsolete file, replaced by CameraCaptureScreen.kt
// This file is kept to avoid breaking changes if referenced elsewhere, but the content is commented out to resolve errors.
// TODO: Migrate completely to CameraCaptureScreen or new CameraViewModel structure.
package com.example.livingai.pages.camera
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import org.koin.androidx.compose.koinViewModel
@Composable
fun CameraScreen(
// viewModel: CameraViewModel = koinViewModel(), // Commented out to fix build errors
navController: NavController,
orientation: String? = null,
animalId: String
) {
// Placeholder content
}

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -48,7 +49,6 @@ fun ViewImageScreen(
showAccept: Boolean, showAccept: Boolean,
showBack: Boolean, showBack: Boolean,
showSegment: Boolean = false, showSegment: Boolean = false,
isSegmented: Boolean = false,
animalId: String, animalId: String,
orientation: String? = null, orientation: String? = null,
onRetake: () -> Unit, onRetake: () -> Unit,
@ -61,10 +61,6 @@ fun ViewImageScreen(
var boundingBox by remember { mutableStateOf<String?>(null) } var boundingBox by remember { mutableStateOf<String?>(null) }
var imageWidth by remember { mutableStateOf(0f) } var imageWidth by remember { mutableStateOf(0f) }
var imageHeight by remember { mutableStateOf(0f) } var imageHeight by remember { mutableStateOf(0f) }
// Check if this image is likely a segmented result based on the filename or uri content if available.
// However, we now have an explicit isSegmented flag which is more reliable for navigation flow
val isSegmentedResult = isSegmented || imageUri.contains("segmented")
val displayedUri = Uri.parse(imageUri) val displayedUri = Uri.parse(imageUri)
var isSegmenting by remember { mutableStateOf(false) } var isSegmenting by remember { mutableStateOf(false) }
@ -77,8 +73,7 @@ fun ViewImageScreen(
context.contentResolver.openInputStream(uri)?.use { inputStream -> context.contentResolver.openInputStream(uri)?.use { inputStream ->
val exif = ExifInterface(inputStream) val exif = ExifInterface(inputStream)
boundingBox = exif.getAttribute(ExifInterface.TAG_USER_COMMENT) boundingBox = exif.getAttribute(ExifInterface.TAG_USER_COMMENT)
// Get image dimensions from Exif if possible, or we will rely on loading
val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
@ -115,22 +110,19 @@ fun ViewImageScreen(
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
alignment = Alignment.Center alignment = Alignment.Center
) )
// Draw Bounding Box if available AND NOT segmented if (boundingBox != null && imageWidth > 0 && imageHeight > 0) {
if (!isSegmentedResult && boundingBox != null && imageWidth > 0 && imageHeight > 0) {
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width val canvasWidth = size.width
val canvasHeight = size.height val canvasHeight = size.height
// Parse bounding box string "left,top,right,bottom"
val parts = boundingBox!!.split(",") val parts = boundingBox!!.split(",")
if (parts.size == 4) { if (parts.size == 4) {
val left = parts[0].toFloatOrNull() ?: 0f val left = parts[0].toFloatOrNull() ?: 0f
val top = parts[1].toFloatOrNull() ?: 0f val top = parts[1].toFloatOrNull() ?: 0f
val right = parts[2].toFloatOrNull() ?: 0f val right = parts[2].toFloatOrNull() ?: 0f
val bottom = parts[3].toFloatOrNull() ?: 0f val bottom = parts[3].toFloatOrNull() ?: 0f
// Calculate scale and offset (CenterInside/Fit logic)
val widthRatio = canvasWidth / imageWidth val widthRatio = canvasWidth / imageWidth
val heightRatio = canvasHeight / imageHeight val heightRatio = canvasHeight / imageHeight
val scale = min(widthRatio, heightRatio) val scale = min(widthRatio, heightRatio)
@ -140,8 +132,7 @@ fun ViewImageScreen(
val offsetX = (canvasWidth - displayedWidth) / 2 val offsetX = (canvasWidth - displayedWidth) / 2
val offsetY = (canvasHeight - displayedHeight) / 2 val offsetY = (canvasHeight - displayedHeight) / 2
// Transform coordinates
val rectLeft = left * scale + offsetX val rectLeft = left * scale + offsetX
val rectTop = top * scale + offsetY val rectTop = top * scale + offsetY
val rectRight = right * scale + offsetX val rectRight = right * scale + offsetX
@ -176,52 +167,84 @@ fun ViewImageScreen(
Button(onClick = { Button(onClick = {
scope.launch { scope.launch {
isSegmenting = true isSegmenting = true
// Parse bounding box to crop try {
var cropBitmap: Bitmap? = null val originalBitmap = withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(displayedUri)?.use {
if (boundingBox != null) { BitmapFactory.decodeStream(it)
try { }
withContext(Dispatchers.IO) { }
context.contentResolver.openInputStream(displayedUri)?.use { stream ->
val original = BitmapFactory.decodeStream(stream) if (originalBitmap == null) {
val parts = boundingBox!!.split(",") isSegmenting = false
if (parts.size == 4 && original != null) { return@launch
val left = parts[0].toFloatOrNull()?.toInt() ?: 0 }
val top = parts[1].toFloatOrNull()?.toInt() ?: 0
val right = parts[2].toFloatOrNull()?.toInt() ?: 0 val finalBitmap = if (boundingBox != null) {
val bottom = parts[3].toFloatOrNull()?.toInt() ?: 0
val parts = boundingBox!!.split(",")
val w = right - left if (parts.size == 4) {
val h = bottom - top val left = parts[0].toFloatOrNull()?.toInt() ?: 0
val top = parts[1].toFloatOrNull()?.toInt() ?: 0
if (w > 0 && h > 0 && left >= 0 && top >= 0 && val right = parts[2].toFloatOrNull()?.toInt() ?: 0
left + w <= original.width && top + h <= original.height) { val bottom = parts[3].toFloatOrNull()?.toInt() ?: 0
cropBitmap = Bitmap.createBitmap(original, left, top, w, h)
} else { val w = right - left
cropBitmap = original val h = bottom - top
if (
w > 0 && h > 0 &&
left >= 0 && top >= 0 &&
left + w <= originalBitmap.width &&
top + h <= originalBitmap.height
) {
val cropped = Bitmap.createBitmap(originalBitmap, left, top, w, h)
val segmentedCrop =
segmenterHelper.segmentToBitmap(cropped)
if (segmentedCrop != null) {
Bitmap.createBitmap(
originalBitmap.width,
originalBitmap.height,
Bitmap.Config.ARGB_8888
).apply {
android.graphics.Canvas(this).apply {
drawColor(Color.Black.toArgb())
drawBitmap(segmentedCrop, left.toFloat(), top.toFloat(), null)
}
} }
} else { } else {
cropBitmap = original null
} }
} else {
segmenterHelper.segmentToBitmap(originalBitmap)
} }
} else {
segmenterHelper.segmentToBitmap(originalBitmap)
} }
} catch (e: Exception) { } else {
e.printStackTrace() segmenterHelper.segmentToBitmap(originalBitmap)
} }
val resultUri = finalBitmap?.let {
segmenterHelper.segmentAndSave(
it,
animalId,
orientation ?: "unknown",
"Segmented images"
)
}
if (resultUri != null) {
onSegmented(resultUri.toString())
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
isSegmenting = false
} }
val bitmapToSegment = cropBitmap
val resultUri = if (bitmapToSegment != null) {
segmenterHelper.segmentAndSave(bitmapToSegment, animalId, orientation ?: "unknown", "Segmented images")
} else {
segmenterHelper.segmentAndSave(displayedUri, animalId, orientation ?: "unknown", "Segmented images")
}
if (resultUri != null) {
onSegmented(resultUri.toString())
}
isSegmenting = false
} }
}) { }) {
Text("Segment") Text("Segment")

View File

@ -35,20 +35,6 @@ import com.example.livingai.utils.Constants
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen(navController: NavController) { fun HomeScreen(navController: NavController) {
val context = LocalContext.current
val silhouetteMap = remember {
Constants.silhouetteList.associateWith { item ->
val resId = context.resources.getIdentifier("label_${item}", "string", context.packageName)
if (resId != 0) context.getString(resId) else item
}
}
// Reverse map for lookup (Display Name -> ID)
val displayToIdMap = remember { silhouetteMap.entries.associate { (k, v) -> v to k } }
val orientationOptions = remember { silhouetteMap.values.toList() }
var selectedOrientationDisplay by remember { mutableStateOf(orientationOptions.firstOrNull() ?: "") }
CommonScaffold( CommonScaffold(
navController = navController, navController = navController,
title = stringResource(id = R.string.app_name), title = stringResource(id = R.string.app_name),
@ -84,26 +70,6 @@ fun HomeScreen(navController: NavController) {
text = stringResource(id = R.string.top_bar_add_profile), text = stringResource(id = R.string.top_bar_add_profile),
onClick = { navController.navigate(Route.AddProfileScreen()) } onClick = { navController.navigate(Route.AddProfileScreen()) }
) )
// Spacer(modifier = Modifier.height(Dimentions.SMALL_PADDING))
//
// // Dropdown for selecting orientation
// LabeledDropdown(
// labelRes = R.string.default_orientation_label, // Or create a generic "Orientation" label
// options = orientationOptions,
// selected = selectedOrientationDisplay,
// onSelected = { selectedOrientationDisplay = it },
// modifier = Modifier.fillMaxWidth()
// )
//
// HomeButton(
// text = "Camera Capture",
// onClick = {
// val orientationId = displayToIdMap[selectedOrientationDisplay] ?: "side"
// navController.navigate(Route.CameraScreen(orientation = orientationId, animalId = "home_test"))
// }
// )
} }
} }
} }

View File

@ -21,11 +21,7 @@ class HomeViewModel(
init { init {
appDataUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> appDataUseCases.readAppEntry().onEach { shouldStartFromHomeScreen ->
// if(shouldStartFromHomeScreen){ _startDestination.value = Route.HomeNavigation
_startDestination.value = Route.HomeNavigation
// }else{
// _startDestination.value = Route.AppStartNavigation
// }
delay(350) //Without this delay, the onBoarding screen will show for a momentum. delay(350) //Without this delay, the onBoarding screen will show for a momentum.
_splashCondition.value = false _splashCondition.value = false
}.launchIn(viewModelScope) }.launchIn(viewModelScope)

View File

@ -3,6 +3,9 @@ package com.example.livingai.pages.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
@ -62,25 +65,23 @@ fun NavGraph(
val newImageUri = backStackEntry.savedStateHandle.get<String>("newImageUri") val newImageUri = backStackEntry.savedStateHandle.get<String>("newImageUri")
val newImageOrientation = backStackEntry.savedStateHandle.get<String>("newImageOrientation") val newImageOrientation = backStackEntry.savedStateHandle.get<String>("newImageOrientation")
val newVideoUri = backStackEntry.savedStateHandle.get<String>("newVideoUri") val newVideoUri = backStackEntry.savedStateHandle.get<String>("newVideoUri")
// We listen for segmented image here too
val newSegmentedUri = backStackEntry.savedStateHandle.get<String>("newSegmentedUri") val newSegmentedUri = backStackEntry.savedStateHandle.get<String>("newSegmentedUri")
LaunchedEffect(newImageUri, newImageOrientation) { LaunchedEffect(newImageUri, newImageOrientation, newSegmentedUri) {
if (newImageUri != null && newImageOrientation != null) { if (newImageOrientation != null) {
viewModel.addPhoto(newImageOrientation, newImageUri) if (newSegmentedUri != null) {
backStackEntry.savedStateHandle.remove<String>("newImageUri") viewModel.addSegmentedImage(newImageOrientation, newSegmentedUri)
backStackEntry.savedStateHandle.remove<String>("newSegmentedUri")
}
if (newImageUri != null) {
viewModel.addPhoto(newImageOrientation, newImageUri)
backStackEntry.savedStateHandle.remove<String>("newImageUri")
}
backStackEntry.savedStateHandle.remove<String>("newImageOrientation") backStackEntry.savedStateHandle.remove<String>("newImageOrientation")
} }
} }
LaunchedEffect(newSegmentedUri) {
if (newSegmentedUri != null) {
viewModel.addSegmentedImage(newSegmentedUri)
backStackEntry.savedStateHandle.remove<String>("newSegmentedUri")
}
}
LaunchedEffect(newVideoUri) { LaunchedEffect(newVideoUri) {
if (newVideoUri != null) { if (newVideoUri != null) {
viewModel.setVideo(newVideoUri) viewModel.setVideo(newVideoUri)
@ -93,6 +94,8 @@ fun NavGraph(
AddProfileScreen( AddProfileScreen(
navController = navController, navController = navController,
viewModel = viewModel, viewModel = viewModel,
animalId = route.animalId,
loadEntry = route.loadEntry,
onSave = { onSave = {
val isSaved = viewModel.saveAnimalDetails() val isSaved = viewModel.saveAnimalDetails()
if (isSaved) if (isSaved)
@ -151,8 +154,17 @@ fun NavGraph(
composable<Route.ViewImageScreen> { backStackEntry -> composable<Route.ViewImageScreen> { backStackEntry ->
val args: Route.ViewImageScreen = backStackEntry.toRoute() val args: Route.ViewImageScreen = backStackEntry.toRoute()
var imageUri by remember { mutableStateOf(args.imageUri) }
val newImageUri = backStackEntry.savedStateHandle.get<String>("newImageUri")
LaunchedEffect(newImageUri) {
if (newImageUri != null) {
imageUri = newImageUri
backStackEntry.savedStateHandle.remove<String>("newImageUri")
}
}
ViewImageScreen( ViewImageScreen(
imageUri = args.imageUri, imageUri = imageUri,
shouldAllowRetake = args.shouldAllowRetake, shouldAllowRetake = args.shouldAllowRetake,
showAccept = args.showAccept, showAccept = args.showAccept,
showBack = args.showBack, showBack = args.showBack,
@ -160,16 +172,14 @@ fun NavGraph(
animalId = args.animalId, animalId = args.animalId,
orientation = args.orientation, orientation = args.orientation,
onRetake = { onRetake = {
navController.popBackStack()
args.orientation?.let { navController.navigate(Route.CameraScreen(orientation = it, animalId = args.animalId)) } args.orientation?.let { navController.navigate(Route.CameraScreen(orientation = it, animalId = args.animalId)) }
}, },
onAccept = { uri -> onAccept = { uri ->
// If it's a segmented result, add to segmented list if (imageUri.contains("segmented")) {
if (args.imageUri.contains("segmented")) {
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newSegmentedUri"] = uri navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newSegmentedUri"] = uri
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageOrientation"] = args.orientation
navController.popBackStack<Route.AddProfileScreen>(inclusive = false) navController.popBackStack<Route.AddProfileScreen>(inclusive = false)
} else { } else {
// Normal image
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageUri"] = uri navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageUri"] = uri
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageOrientation"] = args.orientation navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageOrientation"] = args.orientation
navController.popBackStack<Route.AddProfileScreen>(inclusive = false) navController.popBackStack<Route.AddProfileScreen>(inclusive = false)
@ -183,7 +193,8 @@ fun NavGraph(
showAccept = true, showAccept = true,
showBack = true, showBack = true,
showSegment = false, showSegment = false,
animalId = args.animalId animalId = args.animalId,
isSegmented = true
)) ))
}, },
onBack = { navController.popBackStack() } onBack = { navController.popBackStack() }

View File

@ -24,7 +24,7 @@ class RatingViewModel(
private val _ratingState = MutableStateFlow<AnimalRating?>(null) private val _ratingState = MutableStateFlow<AnimalRating?>(null)
val ratingState = _ratingState.asStateFlow() val ratingState = _ratingState.asStateFlow()
private val _animalImages = MutableStateFlow<List<String>>(emptyList()) private val _animalImages = MutableStateFlow<Map<String, String>>(emptyMap())
val animalImages = _animalImages.asStateFlow() val animalImages = _animalImages.asStateFlow()
private val animalId: String = savedStateHandle.get<String>("animalId")!! private val animalId: String = savedStateHandle.get<String>("animalId")!!
@ -36,7 +36,7 @@ class RatingViewModel(
private fun loadAnimalDetails() { private fun loadAnimalDetails() {
getAnimalDetails(animalId).onEach { getAnimalDetails(animalId).onEach {
_animalImages.value = it?.images ?: emptyList() _animalImages.value = it?.images ?: emptyMap()
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
} }