app integration
This commit is contained in:
parent
3bbf128d9c
commit
bcb4ee1a39
Binary file not shown.
|
|
@ -22,7 +22,7 @@ class MidasDepthEstimator(private val context: Context) {
|
||||||
private var interpreter: Interpreter? = null
|
private var interpreter: Interpreter? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MODEL_NAME = "midas_v2_1_small.tflite"
|
private const val MODEL_NAME = ""
|
||||||
private const val INPUT_SIZE = 256
|
private const val INPUT_SIZE = 256
|
||||||
|
|
||||||
private val NORM_MEAN = floatArrayOf(123.675f, 116.28f, 103.53f)
|
private val NORM_MEAN = floatArrayOf(123.675f, 116.28f, 103.53f)
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ val appModule = module {
|
||||||
viewModel { ListingsViewModel(get()) }
|
viewModel { ListingsViewModel(get()) }
|
||||||
viewModel { SettingsViewModel(get()) }
|
viewModel { SettingsViewModel(get()) }
|
||||||
viewModel { RatingViewModel(get(), get(), get(), get()) }
|
viewModel { RatingViewModel(get(), get(), get(), get()) }
|
||||||
viewModel { CameraViewModel(get(), get(), get(), get(), get(), get(), get()) }
|
viewModel { CameraViewModel(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { VideoViewModel(get(), get(), get()) }
|
viewModel { VideoViewModel(get(), get(), get()) }
|
||||||
viewModel { ImagePreviewViewModel() }
|
viewModel { ImagePreviewViewModel() }
|
||||||
viewModel { VideoPreviewViewModel() }
|
viewModel { VideoPreviewViewModel() }
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,11 @@ import androidx.compose.material3.Text
|
||||||
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.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -80,9 +83,40 @@ fun AddProfileScreen(
|
||||||
var reproductiveStatus by viewModel.reproductiveStatus
|
var reproductiveStatus by viewModel.reproductiveStatus
|
||||||
var description by viewModel.description
|
var description by viewModel.description
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
val speciesError by viewModel.speciesError
|
||||||
|
val breedError by viewModel.breedError
|
||||||
|
val ageError by viewModel.ageError
|
||||||
|
val milkYieldError by viewModel.milkYieldError
|
||||||
|
val calvingNumberError by viewModel.calvingNumberError
|
||||||
|
val reproductiveStatusError by viewModel.reproductiveStatusError
|
||||||
|
|
||||||
val photos = viewModel.photos
|
val photos = viewModel.photos
|
||||||
val videoUri by viewModel.videoUri
|
val videoUri by viewModel.videoUri
|
||||||
|
|
||||||
|
// Focus Requesters
|
||||||
|
val speciesFocus = remember { FocusRequester() }
|
||||||
|
val breedFocus = remember { FocusRequester() }
|
||||||
|
val ageFocus = remember { FocusRequester() }
|
||||||
|
val milkYieldFocus = remember { FocusRequester() }
|
||||||
|
val calvingNumberFocus = remember { FocusRequester() }
|
||||||
|
val reproductiveStatusFocus = remember { FocusRequester() } // Probably not useful for RadioGroup but good for consistency
|
||||||
|
|
||||||
|
// Auto-focus logic on error
|
||||||
|
LaunchedEffect(speciesError, breedError, ageError, milkYieldError, calvingNumberError, reproductiveStatusError) {
|
||||||
|
if (speciesError != null) {
|
||||||
|
speciesFocus.requestFocus()
|
||||||
|
} else if (breedError != null) {
|
||||||
|
breedFocus.requestFocus()
|
||||||
|
} else if (ageError != null) {
|
||||||
|
ageFocus.requestFocus()
|
||||||
|
} else if (milkYieldError != null) {
|
||||||
|
milkYieldFocus.requestFocus()
|
||||||
|
} else if (calvingNumberError != null) {
|
||||||
|
calvingNumberFocus.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CommonScaffold(
|
CommonScaffold(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
title = stringResource(id = R.string.top_bar_add_profile)
|
title = stringResource(id = R.string.top_bar_add_profile)
|
||||||
|
|
@ -103,14 +137,20 @@ fun AddProfileScreen(
|
||||||
labelRes = R.string.label_species,
|
labelRes = R.string.label_species,
|
||||||
options = speciesList,
|
options = speciesList,
|
||||||
selected = species,
|
selected = species,
|
||||||
onSelected = { species = it }
|
onSelected = { species = it },
|
||||||
|
modifier = Modifier.focusRequester(speciesFocus),
|
||||||
|
isError = speciesError != null,
|
||||||
|
supportingText = speciesError
|
||||||
)
|
)
|
||||||
|
|
||||||
LabeledDropdown(
|
LabeledDropdown(
|
||||||
labelRes = R.string.label_breed,
|
labelRes = R.string.label_breed,
|
||||||
options = breedList,
|
options = breedList,
|
||||||
selected = breed,
|
selected = breed,
|
||||||
onSelected = { breed = it }
|
onSelected = { breed = it },
|
||||||
|
modifier = Modifier.focusRequester(breedFocus),
|
||||||
|
isError = breedError != null,
|
||||||
|
supportingText = breedError
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -120,34 +160,55 @@ fun AddProfileScreen(
|
||||||
LabeledTextField(
|
LabeledTextField(
|
||||||
labelRes = R.string.label_age,
|
labelRes = R.string.label_age,
|
||||||
value = age,
|
value = age,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.focusRequester(ageFocus),
|
||||||
onValueChange = { age = it },
|
onValueChange = { age = it },
|
||||||
keyboardType = KeyboardType.Number
|
keyboardType = KeyboardType.Number,
|
||||||
|
isError = ageError != null,
|
||||||
|
supportingText = ageError
|
||||||
)
|
)
|
||||||
|
|
||||||
LabeledTextField(
|
LabeledTextField(
|
||||||
labelRes = R.string.label_milk_yield,
|
labelRes = R.string.label_milk_yield,
|
||||||
value = milkYield,
|
value = milkYield,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.focusRequester(milkYieldFocus),
|
||||||
onValueChange = { milkYield = it },
|
onValueChange = { milkYield = it },
|
||||||
keyboardType = KeyboardType.Number
|
keyboardType = KeyboardType.Number,
|
||||||
|
isError = milkYieldError != null,
|
||||||
|
supportingText = milkYieldError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LabeledTextField(
|
LabeledTextField(
|
||||||
labelRes = R.string.label_calving_number,
|
labelRes = R.string.label_calving_number,
|
||||||
value = calvingNumber,
|
value = calvingNumber,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(calvingNumberFocus),
|
||||||
onValueChange = { calvingNumber = it },
|
onValueChange = { calvingNumber = it },
|
||||||
keyboardType = KeyboardType.Number
|
keyboardType = KeyboardType.Number,
|
||||||
|
isError = calvingNumberError != null,
|
||||||
|
supportingText = calvingNumberError
|
||||||
)
|
)
|
||||||
|
|
||||||
RadioGroup(
|
RadioGroup(
|
||||||
titleRes = R.string.label_reproductive_status,
|
titleRes = R.string.label_reproductive_status,
|
||||||
options = reproList,
|
options = reproList,
|
||||||
selected = reproductiveStatus,
|
selected = reproductiveStatus,
|
||||||
onSelected = { reproductiveStatus = it }
|
onSelected = { reproductiveStatus = it },
|
||||||
|
isError = reproductiveStatusError != null
|
||||||
)
|
)
|
||||||
|
if (reproductiveStatusError != null) {
|
||||||
|
Text(
|
||||||
|
text = reproductiveStatusError ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(start = Dimentions.SMALL_PADDING_TEXT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LabeledTextField(
|
LabeledTextField(
|
||||||
labelRes = R.string.label_description,
|
labelRes = R.string.label_description,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,18 @@ class AddProfileViewModel(
|
||||||
var reproductiveStatus = mutableStateOf<String?>(null)
|
var reproductiveStatus = mutableStateOf<String?>(null)
|
||||||
var description = mutableStateOf("")
|
var description = mutableStateOf("")
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
var ageError = mutableStateOf<String?>(null)
|
||||||
|
var milkYieldError = mutableStateOf<String?>(null)
|
||||||
|
var calvingNumberError = mutableStateOf<String?>(null)
|
||||||
|
var speciesError = mutableStateOf<String?>(null)
|
||||||
|
var breedError = mutableStateOf<String?>(null)
|
||||||
|
var reproductiveStatusError = mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
// Save state
|
||||||
|
private val _saveSuccess = mutableStateOf(false)
|
||||||
|
val saveSuccess: State<Boolean> = _saveSuccess
|
||||||
|
|
||||||
// State for photos and video
|
// State for photos and video
|
||||||
val photos = mutableStateMapOf<String, String>()
|
val photos = mutableStateMapOf<String, String>()
|
||||||
private val _videoUri = mutableStateOf<String?>(null)
|
private val _videoUri = mutableStateOf<String?>(null)
|
||||||
|
|
@ -61,6 +73,7 @@ class AddProfileViewModel(
|
||||||
calvingNumber.value = ""
|
calvingNumber.value = ""
|
||||||
reproductiveStatus.value = null
|
reproductiveStatus.value = null
|
||||||
description.value = ""
|
description.value = ""
|
||||||
|
clearErrors()
|
||||||
|
|
||||||
photos.clear()
|
photos.clear()
|
||||||
_videoUri.value = null
|
_videoUri.value = null
|
||||||
|
|
@ -78,6 +91,7 @@ class AddProfileViewModel(
|
||||||
calvingNumber.value = if (details.calvingNumber == 0) "" else details.calvingNumber.toString()
|
calvingNumber.value = if (details.calvingNumber == 0) "" else details.calvingNumber.toString()
|
||||||
reproductiveStatus.value = details.reproductiveStatus.ifBlank { null }
|
reproductiveStatus.value = details.reproductiveStatus.ifBlank { null }
|
||||||
description.value = details.description
|
description.value = details.description
|
||||||
|
clearErrors()
|
||||||
|
|
||||||
// Populate photos
|
// Populate photos
|
||||||
photos.clear()
|
photos.clear()
|
||||||
|
|
@ -104,6 +118,15 @@ class AddProfileViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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? {
|
private fun getFileName(uri: Uri): String? {
|
||||||
var result: String? = null
|
var result: String? = null
|
||||||
if (uri.scheme == "content") {
|
if (uri.scheme == "content") {
|
||||||
|
|
@ -139,7 +162,60 @@ class AddProfileViewModel(
|
||||||
_videoUri.value = uri
|
_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) {
|
||||||
|
ageError.value = "Invalid age"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
ageError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val milkInt = milkYield.value.toIntOrNull()
|
||||||
|
if (milkInt == null || milkInt < 0) {
|
||||||
|
milkYieldError.value = "Invalid milk yield"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
milkYieldError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val calvingInt = calvingNumber.value.toIntOrNull()
|
||||||
|
if (calvingInt == null || calvingInt < 0) {
|
||||||
|
calvingNumberError.value = "Invalid calving number"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
calvingNumberError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
fun saveAnimalDetails() {
|
fun saveAnimalDetails() {
|
||||||
|
if (!validateInputs()) return
|
||||||
|
|
||||||
val id = _currentAnimalId.value ?: IdGenerator.generateAnimalId().also { _currentAnimalId.value = it }
|
val id = _currentAnimalId.value ?: IdGenerator.generateAnimalId().also { _currentAnimalId.value = it }
|
||||||
|
|
||||||
val details = AnimalDetails(
|
val details = AnimalDetails(
|
||||||
|
|
@ -158,9 +234,14 @@ class AddProfileViewModel(
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
profileEntryUseCase.setAnimalDetails(details)
|
profileEntryUseCase.setAnimalDetails(details)
|
||||||
|
_saveSuccess.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onSaveComplete() {
|
||||||
|
_saveSuccess.value = false
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Try to auto-load when editing via saved state handle
|
// Try to auto-load when editing via saved state handle
|
||||||
val animalId: String? = savedStateHandle?.get<String>("animalId")
|
val animalId: String? = savedStateHandle?.get<String>("animalId")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package com.example.livingai.pages.camera
|
package com.example.livingai.pages.camera
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import androidx.camera.view.LifecycleCameraController
|
import androidx.camera.view.LifecycleCameraController
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
|
@ -12,6 +16,7 @@ import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Camera
|
import androidx.compose.material.icons.filled.Camera
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
|
@ -30,22 +35,30 @@ import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.example.livingai.domain.camera.RealWorldMetrics
|
import com.example.livingai.domain.camera.RealWorldMetrics
|
||||||
import com.example.livingai.domain.model.camera.*
|
import com.example.livingai.domain.model.camera.*
|
||||||
import com.example.livingai.pages.components.CameraPreview
|
import com.example.livingai.pages.components.CameraPreview
|
||||||
import com.example.livingai.pages.components.PermissionWrapper
|
import com.example.livingai.pages.components.PermissionWrapper
|
||||||
import com.example.livingai.utils.SilhouetteManager
|
import com.example.livingai.utils.SilhouetteManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import java.io.OutputStream
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CameraCaptureScreen(
|
fun CameraCaptureScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
orientation: String?,
|
orientation: String?,
|
||||||
|
animalId: String,
|
||||||
viewModel: CameraViewModel = koinViewModel()
|
viewModel: CameraViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(orientation) {
|
LaunchedEffect(orientation) {
|
||||||
if (orientation != null) {
|
if (orientation != null) {
|
||||||
|
|
@ -61,6 +74,71 @@ fun CameraCaptureScreen(
|
||||||
onRetake = { viewModel.resetCamera() },
|
onRetake = { viewModel.resetCamera() },
|
||||||
onSelectReference = { ref, height ->
|
onSelectReference = { ref, height ->
|
||||||
viewModel.onReferenceObjectSelected(ref, height)
|
viewModel.onReferenceObjectSelected(ref, height)
|
||||||
|
},
|
||||||
|
onAccept = {
|
||||||
|
scope.launch {
|
||||||
|
val bitmap = uiState.captureData!!.image
|
||||||
|
val filename = "${animalId}_${orientation ?: "unknown"}.jpg"
|
||||||
|
|
||||||
|
var uri: Uri? = null
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
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) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/LivingAI/$animalId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageUri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
|
||||||
|
if (imageUri != null) {
|
||||||
|
val outputStream: OutputStream? = context.contentResolver.openOutputStream(imageUri)
|
||||||
|
outputStream?.use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Exif metadata for bounding box
|
||||||
|
val detection = uiState.lastDetectionResult
|
||||||
|
val analysisSize = uiState.analysisSize
|
||||||
|
|
||||||
|
var bounds = detection?.animalBounds
|
||||||
|
|
||||||
|
if (bounds != null && analysisSize != null) {
|
||||||
|
// Scale bounds to captured image size
|
||||||
|
val scaleX = bitmap.width.toFloat() / analysisSize.width
|
||||||
|
val scaleY = bitmap.height.toFloat() / analysisSize.height
|
||||||
|
bounds = RectF(
|
||||||
|
bounds.left * scaleX,
|
||||||
|
bounds.top * scaleY,
|
||||||
|
bounds.right * scaleX,
|
||||||
|
bounds.bottom * scaleY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds != null) {
|
||||||
|
try {
|
||||||
|
context.contentResolver.openFileDescriptor(imageUri, "rw")?.use { fd ->
|
||||||
|
val exif = ExifInterface(fd.fileDescriptor)
|
||||||
|
exif.setAttribute(ExifInterface.TAG_USER_COMMENT, "${bounds.left},${bounds.top},${bounds.right},${bounds.bottom}")
|
||||||
|
exif.saveAttributes()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = imageUri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
navController.previousBackStackEntry?.savedStateHandle?.set("newImageUri", uri.toString())
|
||||||
|
navController.previousBackStackEntry?.savedStateHandle?.set("newImageOrientation", orientation)
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -136,9 +214,16 @@ fun ActiveCameraScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.shouldCapture) {
|
||||||
|
if (uiState.shouldCapture) {
|
||||||
|
captureImage()
|
||||||
|
viewModel.onCaptureTriggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (uiState.isReadyToCapture) {
|
if (uiState.isReadyToCapture && !uiState.isAutoCaptureOn) {
|
||||||
FloatingActionButton(onClick = { captureImage() }) {
|
FloatingActionButton(onClick = { captureImage() }) {
|
||||||
Icon(Icons.Default.Camera, contentDescription = "Capture")
|
Icon(Icons.Default.Camera, contentDescription = "Capture")
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +383,8 @@ fun CapturePreviewScreen(
|
||||||
captureData: CaptureData,
|
captureData: CaptureData,
|
||||||
realWorldMetrics: RealWorldMetrics?,
|
realWorldMetrics: RealWorldMetrics?,
|
||||||
onRetake: () -> Unit,
|
onRetake: () -> Unit,
|
||||||
onSelectReference: (ReferenceObject, Float) -> Unit
|
onSelectReference: (ReferenceObject, Float) -> Unit,
|
||||||
|
onAccept: () -> Unit
|
||||||
) {
|
) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
var selectedRefObject by remember { mutableStateOf<ReferenceObject?>(null) }
|
var selectedRefObject by remember { mutableStateOf<ReferenceObject?>(null) }
|
||||||
|
|
@ -364,13 +450,10 @@ fun CapturePreviewScreen(
|
||||||
Text("Retake")
|
Text("Retake")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (captureData.referenceObjects.isNotEmpty()) {
|
Button(onClick = onAccept) {
|
||||||
Button(onClick = {
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
selectedRefObject = captureData.referenceObjects.first()
|
Spacer(Modifier.width(8.dp))
|
||||||
showDialog = true
|
Text("Accept")
|
||||||
}) {
|
|
||||||
Text("Select Ref Object")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
package com.example.livingai.pages.camera
|
package com.example.livingai.pages.camera
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.Size
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.livingai.domain.camera.*
|
import com.example.livingai.domain.camera.*
|
||||||
import com.example.livingai.domain.model.camera.*
|
import com.example.livingai.domain.model.camera.*
|
||||||
|
import com.example.livingai.domain.usecases.AppDataUseCases
|
||||||
import com.example.livingai.utils.TiltSensorManager
|
import com.example.livingai.utils.TiltSensorManager
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CameraViewModel(
|
class CameraViewModel(
|
||||||
|
|
@ -18,7 +24,8 @@ class CameraViewModel(
|
||||||
private val poseAnalyzer: PoseAnalyzer,
|
private val poseAnalyzer: PoseAnalyzer,
|
||||||
private val captureHandler: CaptureHandler,
|
private val captureHandler: CaptureHandler,
|
||||||
private val measurementCalculator: MeasurementCalculator,
|
private val measurementCalculator: MeasurementCalculator,
|
||||||
private val tiltSensorManager: TiltSensorManager
|
private val tiltSensorManager: TiltSensorManager,
|
||||||
|
private val appDataUseCases: AppDataUseCases
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(CameraUiState())
|
private val _uiState = MutableStateFlow(CameraUiState())
|
||||||
|
|
@ -27,9 +34,18 @@ class CameraViewModel(
|
||||||
|
|
||||||
private var frameCounter = 0
|
private var frameCounter = 0
|
||||||
private val frameSkipInterval = 5
|
private val frameSkipInterval = 5
|
||||||
|
private var isAutoCaptureOn = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
tiltSensorManager.start()
|
tiltSensorManager.start()
|
||||||
|
observeSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeSettings() {
|
||||||
|
appDataUseCases.getSettings().onEach {
|
||||||
|
isAutoCaptureOn = it.isAutoCaptureOn
|
||||||
|
_uiState.value = _uiState.value.copy(isAutoCaptureOn = it.isAutoCaptureOn)
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
@ -59,6 +75,8 @@ class CameraViewModel(
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val currentTilt = tilt.value
|
val currentTilt = tilt.value
|
||||||
|
val currentAnalysisSize = Size(image.width, image.height)
|
||||||
|
|
||||||
val input = PipelineInput(
|
val input = PipelineInput(
|
||||||
image = image,
|
image = image,
|
||||||
deviceOrientation = deviceOrientation,
|
deviceOrientation = deviceOrientation,
|
||||||
|
|
@ -76,49 +94,58 @@ class CameraViewModel(
|
||||||
// Step 1: Check Orientation
|
// Step 1: Check Orientation
|
||||||
val orientationInstruction = orientationChecker.analyze(input)
|
val orientationInstruction = orientationChecker.analyze(input)
|
||||||
if (!orientationInstruction.isValid) {
|
if (!orientationInstruction.isValid) {
|
||||||
updateState(orientationInstruction)
|
updateState(orientationInstruction, analysisSize = currentAnalysisSize)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Check Tilt
|
// Step 2: Check Tilt
|
||||||
val tiltInstruction = tiltChecker.analyze(input)
|
val tiltInstruction = tiltChecker.analyze(input)
|
||||||
if (!tiltInstruction.isValid) {
|
if (!tiltInstruction.isValid) {
|
||||||
updateState(tiltInstruction)
|
updateState(tiltInstruction, analysisSize = currentAnalysisSize)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Detect Objects
|
// Step 3: Detect Objects
|
||||||
val detectionInstruction = objectDetector.analyze(input)
|
val detectionInstruction = objectDetector.analyze(input)
|
||||||
if (!detectionInstruction.isValid) {
|
if (!detectionInstruction.isValid) {
|
||||||
updateState(detectionInstruction)
|
updateState(detectionInstruction, analysisSize = currentAnalysisSize)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Check Pose (Silhouette matching)
|
// Step 4: Check Pose (Silhouette matching)
|
||||||
val poseInstruction = poseAnalyzer.analyze(input.copy(previousDetectionResult = detectionInstruction.result as? DetectionResult))
|
val poseInstruction = poseAnalyzer.analyze(input.copy(previousDetectionResult = detectionInstruction.result as? DetectionResult))
|
||||||
if (!poseInstruction.isValid) {
|
if (!poseInstruction.isValid) {
|
||||||
updateState(poseInstruction, detectionInstruction.result as? DetectionResult)
|
updateState(poseInstruction, detectionInstruction.result as? DetectionResult, analysisSize = currentAnalysisSize)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// All checks passed
|
// All checks passed
|
||||||
_uiState.value = _uiState.value.copy(
|
val currentState = _uiState.value
|
||||||
|
_uiState.value = currentState.copy(
|
||||||
currentInstruction = Instruction("Ready to capture", isValid = true),
|
currentInstruction = Instruction("Ready to capture", isValid = true),
|
||||||
isReadyToCapture = true,
|
isReadyToCapture = true,
|
||||||
lastDetectionResult = detectionInstruction.result as? DetectionResult
|
lastDetectionResult = detectionInstruction.result as? DetectionResult,
|
||||||
|
analysisSize = currentAnalysisSize,
|
||||||
|
shouldCapture = isAutoCaptureOn && !currentState.isPreviewMode && !currentState.shouldCapture
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateState(instruction: Instruction, latestDetectionResult: DetectionResult? = null) {
|
private fun updateState(instruction: Instruction, latestDetectionResult: DetectionResult? = null, analysisSize: Size? = null) {
|
||||||
val detectionResult = (instruction.result as? DetectionResult) ?: latestDetectionResult
|
val detectionResult = (instruction.result as? DetectionResult) ?: latestDetectionResult
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
currentInstruction = instruction,
|
currentInstruction = instruction,
|
||||||
isReadyToCapture = false,
|
isReadyToCapture = false,
|
||||||
lastDetectionResult = detectionResult ?: _uiState.value.lastDetectionResult
|
lastDetectionResult = detectionResult ?: _uiState.value.lastDetectionResult,
|
||||||
|
analysisSize = analysisSize ?: _uiState.value.analysisSize,
|
||||||
|
shouldCapture = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onCaptureTriggered() {
|
||||||
|
_uiState.value = _uiState.value.copy(shouldCapture = false)
|
||||||
|
}
|
||||||
|
|
||||||
fun onCaptureClicked(
|
fun onCaptureClicked(
|
||||||
image: Bitmap,
|
image: Bitmap,
|
||||||
deviceOrientation: Int,
|
deviceOrientation: Int,
|
||||||
|
|
@ -127,8 +154,36 @@ class CameraViewModel(
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val detectionResult = _uiState.value.lastDetectionResult ?: return@launch
|
val detectionResult = _uiState.value.lastDetectionResult ?: return@launch
|
||||||
|
val analysisSize = _uiState.value.analysisSize
|
||||||
val currentTilt = tilt.value
|
val currentTilt = tilt.value
|
||||||
|
|
||||||
|
// Scale detection result to match capture image resolution
|
||||||
|
val scaledDetectionResult = if (analysisSize != null) {
|
||||||
|
val scaleX = image.width.toFloat() / analysisSize.width
|
||||||
|
val scaleY = image.height.toFloat() / analysisSize.height
|
||||||
|
val matrix = Matrix()
|
||||||
|
matrix.postScale(scaleX, scaleY)
|
||||||
|
|
||||||
|
val scaledBounds = detectionResult.animalBounds?.let {
|
||||||
|
val r = RectF(it)
|
||||||
|
matrix.mapRect(r)
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
val scaledRefs = detectionResult.referenceObjects.map { ref ->
|
||||||
|
val r = RectF(ref.bounds)
|
||||||
|
matrix.mapRect(r)
|
||||||
|
ref.copy(bounds = r)
|
||||||
|
}
|
||||||
|
|
||||||
|
detectionResult.copy(
|
||||||
|
animalBounds = scaledBounds,
|
||||||
|
referenceObjects = scaledRefs
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
detectionResult
|
||||||
|
}
|
||||||
|
|
||||||
val input = PipelineInput(
|
val input = PipelineInput(
|
||||||
image = image,
|
image = image,
|
||||||
deviceOrientation = deviceOrientation,
|
deviceOrientation = deviceOrientation,
|
||||||
|
|
@ -142,7 +197,7 @@ class CameraViewModel(
|
||||||
orientation = _uiState.value.targetOrientation
|
orientation = _uiState.value.targetOrientation
|
||||||
)
|
)
|
||||||
|
|
||||||
val captureData = captureHandler.capture(input, detectionResult)
|
val captureData = captureHandler.capture(input, scaledDetectionResult)
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
captureData = captureData,
|
captureData = captureData,
|
||||||
|
|
@ -171,7 +226,8 @@ class CameraViewModel(
|
||||||
captureData = null,
|
captureData = null,
|
||||||
realWorldMetrics = null,
|
realWorldMetrics = null,
|
||||||
isReadyToCapture = false,
|
isReadyToCapture = false,
|
||||||
currentInstruction = null
|
currentInstruction = null,
|
||||||
|
shouldCapture = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +239,10 @@ data class CameraUiState(
|
||||||
val currentInstruction: Instruction? = null,
|
val currentInstruction: Instruction? = null,
|
||||||
val isReadyToCapture: Boolean = false,
|
val isReadyToCapture: Boolean = false,
|
||||||
val lastDetectionResult: DetectionResult? = null,
|
val lastDetectionResult: DetectionResult? = null,
|
||||||
|
val analysisSize: Size? = null,
|
||||||
val isPreviewMode: Boolean = false,
|
val isPreviewMode: Boolean = false,
|
||||||
val captureData: CaptureData? = null,
|
val captureData: CaptureData? = null,
|
||||||
val realWorldMetrics: RealWorldMetrics? = null
|
val realWorldMetrics: RealWorldMetrics? = null,
|
||||||
|
val shouldCapture: Boolean = false,
|
||||||
|
val isAutoCaptureOn: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.example.livingai.pages.camera
|
package com.example.livingai.pages.camera
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -17,11 +18,26 @@ import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import com.example.livingai.ui.theme.LivingAITheme
|
import com.example.livingai.ui.theme.LivingAITheme
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ViewImageScreen(
|
fun ViewImageScreen(
|
||||||
|
|
@ -33,16 +49,94 @@ fun ViewImageScreen(
|
||||||
onAccept: () -> Unit,
|
onAccept: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var boundingBox by remember { mutableStateOf<String?>(null) }
|
||||||
|
var imageWidth by remember { mutableStateOf(0f) }
|
||||||
|
var imageHeight by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
LaunchedEffect(imageUri) {
|
||||||
|
val uri = Uri.parse(imageUri)
|
||||||
|
try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
val exif = ExifInterface(inputStream)
|
||||||
|
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 height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
imageWidth = width.toFloat()
|
||||||
|
imageHeight = height.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LivingAITheme {
|
LivingAITheme {
|
||||||
Scaffold {
|
Scaffold {
|
||||||
Box(modifier = Modifier
|
Box(modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(it)) {
|
.padding(it)) {
|
||||||
Image(
|
Image(
|
||||||
painter = rememberAsyncImagePainter(model = Uri.parse(imageUri)),
|
painter = rememberAsyncImagePainter(
|
||||||
|
model = Uri.parse(imageUri),
|
||||||
|
onSuccess = { state ->
|
||||||
|
if (imageWidth == 0f || imageHeight == 0f) {
|
||||||
|
imageWidth = state.result.drawable.intrinsicWidth.toFloat()
|
||||||
|
imageHeight = state.result.drawable.intrinsicHeight.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
contentDescription = "Captured Image",
|
contentDescription = "Captured Image",
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
alignment = Alignment.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Draw Bounding Box if available
|
||||||
|
if (boundingBox != null && imageWidth > 0 && imageHeight > 0) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val canvasWidth = size.width
|
||||||
|
val canvasHeight = size.height
|
||||||
|
|
||||||
|
// Parse bounding box string "left,top,right,bottom"
|
||||||
|
val parts = boundingBox!!.split(",")
|
||||||
|
if (parts.size == 4) {
|
||||||
|
val left = parts[0].toFloatOrNull() ?: 0f
|
||||||
|
val top = parts[1].toFloatOrNull() ?: 0f
|
||||||
|
val right = parts[2].toFloatOrNull() ?: 0f
|
||||||
|
val bottom = parts[3].toFloatOrNull() ?: 0f
|
||||||
|
|
||||||
|
// Calculate scale and offset (CenterInside/Fit logic)
|
||||||
|
// Image composable uses 'Fit' by default which scales to fit within bounds while maintaining aspect ratio
|
||||||
|
val widthRatio = canvasWidth / imageWidth
|
||||||
|
val heightRatio = canvasHeight / imageHeight
|
||||||
|
val scale = min(widthRatio, heightRatio)
|
||||||
|
|
||||||
|
val displayedWidth = imageWidth * scale
|
||||||
|
val displayedHeight = imageHeight * scale
|
||||||
|
|
||||||
|
val offsetX = (canvasWidth - displayedWidth) / 2
|
||||||
|
val offsetY = (canvasHeight - displayedHeight) / 2
|
||||||
|
|
||||||
|
// Transform coordinates
|
||||||
|
val rectLeft = left * scale + offsetX
|
||||||
|
val rectTop = top * scale + offsetY
|
||||||
|
val rectRight = right * scale + offsetX
|
||||||
|
val rectBottom = bottom * scale + offsetY
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
color = Color.Yellow,
|
||||||
|
topLeft = Offset(rectLeft, rectTop),
|
||||||
|
size = Size(rectRight - rectLeft, rectBottom - rectTop),
|
||||||
|
style = Stroke(width = 3.dp.toPx())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ fun LabeledDropdown(
|
||||||
options: List<String>,
|
options: List<String>,
|
||||||
selected: String?,
|
selected: String?,
|
||||||
onSelected: (String) -> Unit,
|
onSelected: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
isError: Boolean = false,
|
||||||
|
supportingText: String? = null
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -46,7 +48,11 @@ fun LabeledDropdown(
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.menuAnchor(MenuAnchorType.PrimaryEditable, true)
|
.menuAnchor(MenuAnchorType.PrimaryEditable, true)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
|
isError = isError,
|
||||||
|
supportingText = if (supportingText != null) {
|
||||||
|
{ Text(supportingText) }
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ fun LabeledTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
keyboardType: KeyboardType = KeyboardType.Text
|
keyboardType: KeyboardType = KeyboardType.Text,
|
||||||
|
isError: Boolean = false,
|
||||||
|
supportingText: String? = null
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
|
@ -27,7 +29,11 @@ fun LabeledTextField(
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
label = { Text(stringResource(id = labelRes)) },
|
label = { Text(stringResource(id = labelRes)) },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = keyboardType)
|
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
|
||||||
|
isError = isError,
|
||||||
|
supportingText = if (supportingText != null) {
|
||||||
|
{ Text(supportingText) }
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,13 +20,14 @@ fun RadioGroup(
|
||||||
@StringRes titleRes: Int,
|
@StringRes titleRes: Int,
|
||||||
options: List<String>,
|
options: List<String>,
|
||||||
selected: String?,
|
selected: String?,
|
||||||
onSelected: (String) -> Unit
|
onSelected: (String) -> Unit,
|
||||||
|
isError: Boolean = false
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = titleRes),
|
text = stringResource(id = titleRes),
|
||||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -85,24 +85,24 @@ fun HomeScreen(navController: NavController) {
|
||||||
onClick = { navController.navigate(Route.AddProfileScreen()) }
|
onClick = { navController.navigate(Route.AddProfileScreen()) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Dimentions.SMALL_PADDING))
|
// Spacer(modifier = Modifier.height(Dimentions.SMALL_PADDING))
|
||||||
|
//
|
||||||
// Dropdown for selecting orientation
|
// // Dropdown for selecting orientation
|
||||||
LabeledDropdown(
|
// LabeledDropdown(
|
||||||
labelRes = R.string.default_orientation_label, // Or create a generic "Orientation" label
|
// labelRes = R.string.default_orientation_label, // Or create a generic "Orientation" label
|
||||||
options = orientationOptions,
|
// options = orientationOptions,
|
||||||
selected = selectedOrientationDisplay,
|
// selected = selectedOrientationDisplay,
|
||||||
onSelected = { selectedOrientationDisplay = it },
|
// onSelected = { selectedOrientationDisplay = it },
|
||||||
modifier = Modifier.fillMaxWidth()
|
// modifier = Modifier.fillMaxWidth()
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
HomeButton(
|
// HomeButton(
|
||||||
text = "Camera Capture",
|
// text = "Camera Capture",
|
||||||
onClick = {
|
// onClick = {
|
||||||
val orientationId = displayToIdMap[selectedOrientationDisplay] ?: "side"
|
// val orientationId = displayToIdMap[selectedOrientationDisplay] ?: "side"
|
||||||
navController.navigate(Route.CameraScreen(orientation = orientationId))
|
// navController.navigate(Route.CameraScreen(orientation = orientationId, animalId = "home_test"))
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ class HomeViewModel(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
appDataUseCases.readAppEntry().onEach { shouldStartFromHomeScreen ->
|
appDataUseCases.readAppEntry().onEach { shouldStartFromHomeScreen ->
|
||||||
if(shouldStartFromHomeScreen){
|
// if(shouldStartFromHomeScreen){
|
||||||
_startDestination.value = Route.HomeNavigation
|
_startDestination.value = Route.HomeNavigation
|
||||||
}else{
|
// }else{
|
||||||
_startDestination.value = Route.AppStartNavigation
|
// _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)
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ fun NavGraph(
|
||||||
animalId = currentId ?: "unknown"
|
animalId = currentId ?: "unknown"
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
// navController.navigate(Route.CameraScreen(orientation = orientation, animalId = currentId ?: "unknown")) // Commented until existing camera flow is restored or migrated
|
navController.navigate(Route.CameraScreen(orientation = orientation, animalId = currentId ?: "unknown"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTakeVideo = {
|
onTakeVideo = {
|
||||||
|
|
@ -126,7 +126,7 @@ fun NavGraph(
|
||||||
|
|
||||||
composable<Route.CameraScreen> { backStackEntry ->
|
composable<Route.CameraScreen> { backStackEntry ->
|
||||||
val route: Route.CameraScreen = backStackEntry.toRoute()
|
val route: Route.CameraScreen = backStackEntry.toRoute()
|
||||||
CameraCaptureScreen(navController = navController, orientation = route.orientation)
|
CameraCaptureScreen(navController = navController, orientation = route.orientation, animalId = route.animalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Route.VideoRecordScreen> { backStackEntry ->
|
composable<Route.VideoRecordScreen> { backStackEntry ->
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ sealed class Route {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RatingScreen(val animalId: String) : Route()
|
data class RatingScreen(val animalId: String) : Route()
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CameraScreen(val orientation: String) : Route()
|
data class CameraScreen(val orientation: String, val animalId: String) : Route()
|
||||||
@Serializable
|
@Serializable
|
||||||
data class OldCameraScreen(val orientation: String? = null, val animalId: String) : Route()
|
data class OldCameraScreen(val orientation: String? = null, val animalId: String) : Route()
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue