parent
d5902cba08
commit
a6ea1d5ce0
|
|
@ -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
|
|
||||||
if (startDestination != null) {
|
|
||||||
NavGraph(startDestination = startDestination)
|
NavGraph(startDestination = startDestination)
|
||||||
} else {
|
|
||||||
// Optional: Show a loading indicator if startDestination is null
|
|
||||||
// for an extended period, though splash screen should handle it.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) }
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
private suspend fun segmentInternal(image: InputImage): Bitmap? =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
|
||||||
|
val options = SubjectSegmenterOptions.Builder()
|
||||||
|
.enableMultipleSubjects(
|
||||||
|
SubjectSegmenterOptions.SubjectResultOptions.Builder()
|
||||||
|
.enableSubjectBitmap()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val segmenter = SubjectSegmentation.getClient(options)
|
||||||
|
|
||||||
|
segmenter.process(image)
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
val subject = result.subjects
|
||||||
|
.maxByOrNull { it.width * it.height }
|
||||||
|
|
||||||
|
if (subject?.bitmap == null) {
|
||||||
|
continuation.resume(null)
|
||||||
|
return@addOnSuccessListener
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
.addOnCompleteListener {
|
||||||
|
segmenter.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun segmentToBitmap(inputBitmap: Bitmap): Bitmap? {
|
suspend fun segmentToBitmap(inputBitmap: Bitmap): Bitmap? {
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
try {
|
|
||||||
val image = InputImage.fromBitmap(inputBitmap, 0)
|
val image = InputImage.fromBitmap(inputBitmap, 0)
|
||||||
val options = SubjectSegmenterOptions.Builder()
|
return segmentInternal(image)
|
||||||
.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
|
|
||||||
)
|
|
||||||
continuation.resume(resultBitmap)
|
|
||||||
} 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(inputBitmap: Bitmap, animalId: String, orientation: String, subFolder: String? = null): Uri? {
|
suspend fun segmentAndSave(
|
||||||
return suspendCancellableCoroutine { continuation ->
|
inputBitmap: Bitmap,
|
||||||
try {
|
animalId: String,
|
||||||
|
orientation: String,
|
||||||
|
subFolder: String? = null
|
||||||
|
): Uri? {
|
||||||
val image = InputImage.fromBitmap(inputBitmap, 0)
|
val image = InputImage.fromBitmap(inputBitmap, 0)
|
||||||
val options = SubjectSegmenterOptions.Builder()
|
val bitmap = segmentInternal(image) ?: return null
|
||||||
.enableMultipleSubjects(
|
return saveBitmap(bitmap, animalId, orientation, subFolder)
|
||||||
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)
|
suspend fun segmentAndSave(
|
||||||
|
inputUri: Uri,
|
||||||
if (uri != null) {
|
animalId: String,
|
||||||
val outputStream: OutputStream? = context.contentResolver.openOutputStream(uri)
|
orientation: String,
|
||||||
outputStream?.use { out ->
|
subFolder: String? = null
|
||||||
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
): Uri? {
|
||||||
}
|
|
||||||
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 image = InputImage.fromFilePath(context, inputUri)
|
||||||
val options = SubjectSegmenterOptions.Builder()
|
val bitmap = segmentInternal(image) ?: return null
|
||||||
.enableMultipleSubjects(
|
return saveBitmap(bitmap, animalId, orientation, subFolder)
|
||||||
SubjectSegmenterOptions.SubjectResultOptions.Builder()
|
}
|
||||||
.enableSubjectBitmap()
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
val segmenter = SubjectSegmentation.getClient(options)
|
|
||||||
|
|
||||||
segmenter.process(image)
|
private fun saveBitmap(
|
||||||
.addOnSuccessListener { result ->
|
bitmap: Bitmap,
|
||||||
val subjects = result.subjects
|
animalId: String,
|
||||||
if (subjects.isNotEmpty()) {
|
orientation: String,
|
||||||
// Find the largest subject (assuming it's the one in front/main subject)
|
subFolder: String?
|
||||||
val mainSubject = subjects.maxByOrNull { it.width * it.height }
|
): Uri? {
|
||||||
|
|
||||||
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 filename = "${animalId}_${orientation}_segmented.jpg"
|
||||||
|
|
||||||
val contentValues = ContentValues().apply {
|
val values = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val path = if (subFolder != null) "Pictures/LivingAI/$animalId/$subFolder" else "Pictures/LivingAI/$animalId"
|
val path =
|
||||||
|
if (subFolder != null)
|
||||||
|
"Pictures/LivingAI/$animalId/$subFolder"
|
||||||
|
else
|
||||||
|
"Pictures/LivingAI/$animalId"
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
|
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
val uri = context.contentResolver.insert(
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
values
|
||||||
|
) ?: return null
|
||||||
|
|
||||||
|
val outputStream: OutputStream? =
|
||||||
|
context.contentResolver.openOutputStream(uri)
|
||||||
|
|
||||||
if (uri != null) {
|
|
||||||
val outputStream: OutputStream? = context.contentResolver.openOutputStream(uri)
|
|
||||||
outputStream?.use { out ->
|
outputStream?.use { out ->
|
||||||
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
bitmap.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? {
|
return uri
|
||||||
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) {
|
|
||||||
// ignore
|
|
||||||
} 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,14 +56,201 @@ 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
|
fun loadAnimal(animalId: String) {
|
||||||
val segmentedImages = mutableListOf<String>()
|
if (animalId == _currentAnimalId.value) return
|
||||||
|
|
||||||
fun loadAnimal(animalId: String?) {
|
_currentAnimalId.value = animalId
|
||||||
if (animalId == null) {
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
photos.clear()
|
||||||
|
segmentedImages.clear()
|
||||||
|
|
||||||
|
withContext(dispatchers.main) {
|
||||||
|
details.images.entries.forEach { (orientation, path) ->
|
||||||
|
photos[orientation] = path
|
||||||
|
}
|
||||||
|
details.segmentedImages.entries.forEach { (orientation, path) ->
|
||||||
|
segmentedImages[orientation] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_videoUri.value = details.video.ifBlank { null }
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearErrors() {
|
||||||
|
ageError.value = null
|
||||||
|
milkYieldError.value = null
|
||||||
|
calvingNumberError.value = null
|
||||||
|
speciesError.value = null
|
||||||
|
breedError.value = null
|
||||||
|
reproductiveStatusError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addPhoto(orientation: String, uri: String) {
|
||||||
|
photos[orientation] = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSegmentedImage(orientation: String, uri: String) {
|
||||||
|
segmentedImages[orientation] = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVideo(uri: String) {
|
||||||
|
_videoUri.value = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateSpeciesInputs(s: String? = null): Boolean {
|
||||||
|
if (s != null)
|
||||||
|
species.value = s
|
||||||
|
|
||||||
|
var isValid = true
|
||||||
|
|
||||||
|
if (species.value.isNullOrBlank()) {
|
||||||
|
speciesError.value = "Species is required"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
speciesError.value = null
|
||||||
|
}
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateBreedInputs(b: String? = null): Boolean {
|
||||||
|
if (b != null)
|
||||||
|
breed.value = b
|
||||||
|
|
||||||
|
var isValid = true
|
||||||
|
|
||||||
|
if (breed.value.isNullOrBlank()) {
|
||||||
|
breedError.value = "Breed is required"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
breedError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateReproductiveStatusInputs(r: String? = null): Boolean {
|
||||||
|
if (r != null)
|
||||||
|
reproductiveStatus.value = r
|
||||||
|
|
||||||
|
var isValid = true
|
||||||
|
|
||||||
|
if (reproductiveStatus.value.isNullOrBlank()) {
|
||||||
|
reproductiveStatusError.value = "Status is required"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
if (ageInt == null || ageInt <= 0 || ageInt > 20) {
|
||||||
|
ageError.value = "Invalid age"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
if (milkInt == null || milkInt <= 0 || milkInt > 75) {
|
||||||
|
milkYieldError.value = "Invalid milk yield"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
if (calvingInt == null || calvingInt < 0 || calvingInt > 12) {
|
||||||
|
calvingNumberError.value = "Invalid calving number"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
calvingNumberError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateInputs(): Boolean {
|
||||||
|
return validateSpeciesInputs() && validateBreedInputs() && validateAgeInputs()
|
||||||
|
&& validateMilkYieldInputs() && validateCalvingInputs() && validateReproductiveStatusInputs()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveAnimalDetails(): Boolean {
|
||||||
|
if (!validateInputs()) return false
|
||||||
|
|
||||||
|
val id = _currentAnimalId.value ?: IdGenerator.generateAnimalId().also { _currentAnimalId.value = it }
|
||||||
|
|
||||||
|
val details = AnimalDetails(
|
||||||
|
animalId = id,
|
||||||
|
species = species.value ?: "",
|
||||||
|
breed = breed.value ?: "",
|
||||||
|
age = age.value.toIntOrNull() ?: 0,
|
||||||
|
milkYield = milkYield.value.toIntOrNull() ?: 0,
|
||||||
|
calvingNumber = calvingNumber.value.toIntOrNull() ?: 0,
|
||||||
|
reproductiveStatus = reproductiveStatus.value ?: "",
|
||||||
|
description = description.value,
|
||||||
|
images = photos,
|
||||||
|
video = _videoUri.value ?: "",
|
||||||
|
segmentedImages = segmentedImages,
|
||||||
|
name = "", sex = "", weight = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
profileEntryUseCase.setAnimalDetails(details)
|
||||||
|
_saveSuccess.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveComplete() {
|
||||||
|
_saveSuccess.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeNewProfile() {
|
||||||
val newId = IdGenerator.generateAnimalId()
|
val newId = IdGenerator.generateAnimalId()
|
||||||
_currentAnimalId.value = newId
|
_currentAnimalId.value = newId
|
||||||
_animalDetails.value = null
|
_animalDetails.value = null
|
||||||
|
|
@ -82,211 +268,5 @@ class AddProfileViewModel(
|
||||||
photos.clear()
|
photos.clear()
|
||||||
segmentedImages.clear()
|
segmentedImages.clear()
|
||||||
_videoUri.value = null
|
_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
|
|
||||||
var foundOrientation: String? = null
|
|
||||||
for (o in Constants.silhouetteList) {
|
|
||||||
if (nameWithoutExt.contains(o, ignoreCase = true)) {
|
|
||||||
foundOrientation = o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val parts = nameWithoutExt.split('_')
|
|
||||||
val matchingPart = parts.find { part ->
|
|
||||||
Constants.silhouetteList.any { it.equals(part, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingPart != null) {
|
|
||||||
val key = Constants.silhouetteList.find { it.equals(matchingPart, ignoreCase = true) }
|
|
||||||
if (key != null) {
|
|
||||||
photoMap[key] = path
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val sortedOrientations = Constants.silhouetteList.sortedByDescending { it.length }
|
|
||||||
val match = sortedOrientations.find { nameWithoutExt.contains(it, ignoreCase = true) }
|
|
||||||
if (match != null) {
|
|
||||||
photoMap[match] = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(dispatchers.main) {
|
|
||||||
photoMap.forEach { (k, v) -> photos[k] = v }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_videoUri.value = details.video.ifBlank { null }
|
|
||||||
}
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearErrors() {
|
|
||||||
ageError.value = null
|
|
||||||
milkYieldError.value = null
|
|
||||||
calvingNumberError.value = null
|
|
||||||
speciesError.value = null
|
|
||||||
breedError.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) {
|
|
||||||
photos[orientation] = uri
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addSegmentedImage(uri: String) {
|
|
||||||
if (!segmentedImages.contains(uri)) {
|
|
||||||
segmentedImages.add(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setVideo(uri: String) {
|
|
||||||
_videoUri.value = uri
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validateInputs(): Boolean {
|
|
||||||
var isValid = true
|
|
||||||
|
|
||||||
if (species.value.isNullOrBlank()) {
|
|
||||||
speciesError.value = "Species is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
speciesError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (breed.value.isNullOrBlank()) {
|
|
||||||
breedError.value = "Breed is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
breedError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reproductiveStatus.value.isNullOrBlank()) {
|
|
||||||
reproductiveStatusError.value = "Status is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
reproductiveStatusError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
val ageInt = age.value.toIntOrNull()
|
|
||||||
if (ageInt == null || ageInt <= 0 || ageInt > 20) {
|
|
||||||
ageError.value = "Invalid age"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
ageError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
val milkInt = milkYield.value.toIntOrNull()
|
|
||||||
if (milkInt == null || milkInt <= 0 || milkInt > 75) {
|
|
||||||
milkYieldError.value = "Invalid milk yield"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
milkYieldError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
val calvingInt = calvingNumber.value.toIntOrNull()
|
|
||||||
if (calvingInt == null || calvingInt < 0 || calvingInt > 12) {
|
|
||||||
calvingNumberError.value = "Invalid calving number"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
calvingNumberError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveAnimalDetails(): Boolean {
|
|
||||||
if (!validateInputs()) return false
|
|
||||||
|
|
||||||
val id = _currentAnimalId.value ?: IdGenerator.generateAnimalId().also { _currentAnimalId.value = it }
|
|
||||||
|
|
||||||
val details = AnimalDetails(
|
|
||||||
animalId = id,
|
|
||||||
species = species.value ?: "",
|
|
||||||
breed = breed.value ?: "",
|
|
||||||
age = age.value.toIntOrNull() ?: 0,
|
|
||||||
milkYield = milkYield.value.toIntOrNull() ?: 0,
|
|
||||||
calvingNumber = calvingNumber.value.toIntOrNull() ?: 0,
|
|
||||||
reproductiveStatus = reproductiveStatus.value ?: "",
|
|
||||||
description = description.value,
|
|
||||||
images = photos.values.toList(),
|
|
||||||
video = _videoUri.value ?: "",
|
|
||||||
segmentedImages = segmentedImages.toList(),
|
|
||||||
name = "", sex = "", weight = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
profileEntryUseCase.setAnimalDetails(details)
|
|
||||||
_saveSuccess.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSaveComplete() {
|
|
||||||
_saveSuccess.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val animalId: String? = savedStateHandle?.get<String>("animalId")
|
|
||||||
loadAnimal(animalId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,32 +323,12 @@ 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
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -62,10 +62,6 @@ fun ViewImageScreen(
|
||||||
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) }
|
||||||
|
|
||||||
|
|
@ -78,7 +74,6 @@ fun ViewImageScreen(
|
||||||
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) {
|
||||||
|
|
@ -116,13 +111,11 @@ fun ViewImageScreen(
|
||||||
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
|
||||||
|
|
@ -130,7 +123,6 @@ fun ViewImageScreen(
|
||||||
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)
|
||||||
|
|
@ -141,7 +133,6 @@ 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
|
||||||
|
|
@ -177,16 +168,22 @@ fun ViewImageScreen(
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isSegmenting = true
|
isSegmenting = true
|
||||||
|
|
||||||
// Parse bounding box to crop
|
|
||||||
var cropBitmap: Bitmap? = null
|
|
||||||
|
|
||||||
if (boundingBox != null) {
|
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
val originalBitmap = withContext(Dispatchers.IO) {
|
||||||
context.contentResolver.openInputStream(displayedUri)?.use { stream ->
|
context.contentResolver.openInputStream(displayedUri)?.use {
|
||||||
val original = BitmapFactory.decodeStream(stream)
|
BitmapFactory.decodeStream(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalBitmap == null) {
|
||||||
|
isSegmenting = false
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalBitmap = if (boundingBox != null) {
|
||||||
|
|
||||||
val parts = boundingBox!!.split(",")
|
val parts = boundingBox!!.split(",")
|
||||||
if (parts.size == 4 && original != null) {
|
if (parts.size == 4) {
|
||||||
val left = parts[0].toFloatOrNull()?.toInt() ?: 0
|
val left = parts[0].toFloatOrNull()?.toInt() ?: 0
|
||||||
val top = parts[1].toFloatOrNull()?.toInt() ?: 0
|
val top = parts[1].toFloatOrNull()?.toInt() ?: 0
|
||||||
val right = parts[2].toFloatOrNull()?.toInt() ?: 0
|
val right = parts[2].toFloatOrNull()?.toInt() ?: 0
|
||||||
|
|
@ -195,34 +192,60 @@ fun ViewImageScreen(
|
||||||
val w = right - left
|
val w = right - left
|
||||||
val h = bottom - top
|
val h = bottom - top
|
||||||
|
|
||||||
if (w > 0 && h > 0 && left >= 0 && top >= 0 &&
|
if (
|
||||||
left + w <= original.width && top + h <= original.height) {
|
w > 0 && h > 0 &&
|
||||||
cropBitmap = Bitmap.createBitmap(original, left, top, w, h)
|
left >= 0 && top >= 0 &&
|
||||||
} else {
|
left + w <= originalBitmap.width &&
|
||||||
cropBitmap = original
|
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 bitmapToSegment = cropBitmap
|
val resultUri = finalBitmap?.let {
|
||||||
val resultUri = if (bitmapToSegment != null) {
|
segmenterHelper.segmentAndSave(
|
||||||
segmenterHelper.segmentAndSave(bitmapToSegment, animalId, orientation ?: "unknown", "Segmented images")
|
it,
|
||||||
} else {
|
animalId,
|
||||||
segmenterHelper.segmentAndSave(displayedUri, animalId, orientation ?: "unknown", "Segmented images")
|
orientation ?: "unknown",
|
||||||
|
"Segmented images"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultUri != null) {
|
if (resultUri != null) {
|
||||||
onSegmented(resultUri.toString())
|
onSegmented(resultUri.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
isSegmenting = false
|
isSegmenting = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("Segment")
|
Text("Segment")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -63,24 +66,22 @@ fun NavGraph(
|
||||||
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) {
|
||||||
|
if (newSegmentedUri != null) {
|
||||||
|
viewModel.addSegmentedImage(newImageOrientation, newSegmentedUri)
|
||||||
|
backStackEntry.savedStateHandle.remove<String>("newSegmentedUri")
|
||||||
|
}
|
||||||
|
if (newImageUri != null) {
|
||||||
viewModel.addPhoto(newImageOrientation, newImageUri)
|
viewModel.addPhoto(newImageOrientation, newImageUri)
|
||||||
backStackEntry.savedStateHandle.remove<String>("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() }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue