app integration

This commit is contained in:
SaiD 2025-12-15 22:25:56 +05:30
parent 3bbf128d9c
commit bcb4ee1a39
16 changed files with 459 additions and 68 deletions

View File

@ -22,7 +22,7 @@ class MidasDepthEstimator(private val context: Context) {
private var interpreter: Interpreter? = null
companion object {
private const val MODEL_NAME = "midas_v2_1_small.tflite"
private const val MODEL_NAME = ""
private const val INPUT_SIZE = 256
private val NORM_MEAN = floatArrayOf(123.675f, 116.28f, 103.53f)

View File

@ -186,7 +186,7 @@ val appModule = module {
viewModel { ListingsViewModel(get()) }
viewModel { SettingsViewModel(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 { ImagePreviewViewModel() }
viewModel { VideoPreviewViewModel() }

View File

@ -20,8 +20,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.res.stringArrayResource
import androidx.compose.ui.res.stringResource
@ -80,9 +83,40 @@ fun AddProfileScreen(
var reproductiveStatus by viewModel.reproductiveStatus
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 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(
navController = navController,
title = stringResource(id = R.string.top_bar_add_profile)
@ -103,14 +137,20 @@ fun AddProfileScreen(
labelRes = R.string.label_species,
options = speciesList,
selected = species,
onSelected = { species = it }
onSelected = { species = it },
modifier = Modifier.focusRequester(speciesFocus),
isError = speciesError != null,
supportingText = speciesError
)
LabeledDropdown(
labelRes = R.string.label_breed,
options = breedList,
selected = breed,
onSelected = { breed = it }
onSelected = { breed = it },
modifier = Modifier.focusRequester(breedFocus),
isError = breedError != null,
supportingText = breedError
)
Row(
@ -120,34 +160,55 @@ fun AddProfileScreen(
LabeledTextField(
labelRes = R.string.label_age,
value = age,
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.focusRequester(ageFocus),
onValueChange = { age = it },
keyboardType = KeyboardType.Number
keyboardType = KeyboardType.Number,
isError = ageError != null,
supportingText = ageError
)
LabeledTextField(
labelRes = R.string.label_milk_yield,
value = milkYield,
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.focusRequester(milkYieldFocus),
onValueChange = { milkYield = it },
keyboardType = KeyboardType.Number
keyboardType = KeyboardType.Number,
isError = milkYieldError != null,
supportingText = milkYieldError
)
}
LabeledTextField(
labelRes = R.string.label_calving_number,
value = calvingNumber,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(calvingNumberFocus),
onValueChange = { calvingNumber = it },
keyboardType = KeyboardType.Number
keyboardType = KeyboardType.Number,
isError = calvingNumberError != null,
supportingText = calvingNumberError
)
RadioGroup(
titleRes = R.string.label_reproductive_status,
options = reproList,
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(
labelRes = R.string.label_description,

View File

@ -42,6 +42,18 @@ class AddProfileViewModel(
var reproductiveStatus = mutableStateOf<String?>(null)
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
val photos = mutableStateMapOf<String, String>()
private val _videoUri = mutableStateOf<String?>(null)
@ -61,6 +73,7 @@ class AddProfileViewModel(
calvingNumber.value = ""
reproductiveStatus.value = null
description.value = ""
clearErrors()
photos.clear()
_videoUri.value = null
@ -78,6 +91,7 @@ class AddProfileViewModel(
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()
@ -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? {
var result: String? = null
if (uri.scheme == "content") {
@ -139,7 +162,60 @@ class AddProfileViewModel(
_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() {
if (!validateInputs()) return
val id = _currentAnimalId.value ?: IdGenerator.generateAnimalId().also { _currentAnimalId.value = it }
val details = AnimalDetails(
@ -158,9 +234,14 @@ class AddProfileViewModel(
viewModelScope.launch {
profileEntryUseCase.setAnimalDetails(details)
_saveSuccess.value = true
}
}
fun onSaveComplete() {
_saveSuccess.value = false
}
init {
// Try to auto-load when editing via saved state handle
val animalId: String? = savedStateHandle?.get<String>("animalId")

View File

@ -1,8 +1,12 @@
package com.example.livingai.pages.camera
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.RectF
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.camera.core.ImageProxy
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.Canvas
@ -12,6 +16,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
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.unit.dp
import androidx.core.content.ContextCompat
import androidx.exifinterface.media.ExifInterface
import androidx.navigation.NavController
import com.example.livingai.domain.camera.RealWorldMetrics
import com.example.livingai.domain.model.camera.*
import com.example.livingai.pages.components.CameraPreview
import com.example.livingai.pages.components.PermissionWrapper
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 java.io.OutputStream
import kotlin.math.max
@Composable
fun CameraCaptureScreen(
navController: NavController,
orientation: String?,
animalId: String,
viewModel: CameraViewModel = koinViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val scope = rememberCoroutineScope()
val context = LocalContext.current
LaunchedEffect(orientation) {
if (orientation != null) {
@ -61,6 +74,71 @@ fun CameraCaptureScreen(
onRetake = { viewModel.resetCamera() },
onSelectReference = { 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 {
@ -136,9 +214,16 @@ fun ActiveCameraScreen(
)
}
LaunchedEffect(uiState.shouldCapture) {
if (uiState.shouldCapture) {
captureImage()
viewModel.onCaptureTriggered()
}
}
Scaffold(
floatingActionButton = {
if (uiState.isReadyToCapture) {
if (uiState.isReadyToCapture && !uiState.isAutoCaptureOn) {
FloatingActionButton(onClick = { captureImage() }) {
Icon(Icons.Default.Camera, contentDescription = "Capture")
}
@ -298,7 +383,8 @@ fun CapturePreviewScreen(
captureData: CaptureData,
realWorldMetrics: RealWorldMetrics?,
onRetake: () -> Unit,
onSelectReference: (ReferenceObject, Float) -> Unit
onSelectReference: (ReferenceObject, Float) -> Unit,
onAccept: () -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
var selectedRefObject by remember { mutableStateOf<ReferenceObject?>(null) }
@ -364,13 +450,10 @@ fun CapturePreviewScreen(
Text("Retake")
}
if (captureData.referenceObjects.isNotEmpty()) {
Button(onClick = {
selectedRefObject = captureData.referenceObjects.first()
showDialog = true
}) {
Text("Select Ref Object")
}
Button(onClick = onAccept) {
Icon(Icons.Default.Check, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Accept")
}
}
}

View File

@ -1,14 +1,20 @@
package com.example.livingai.pages.camera
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.RectF
import android.util.Size
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.livingai.domain.camera.*
import com.example.livingai.domain.model.camera.*
import com.example.livingai.domain.usecases.AppDataUseCases
import com.example.livingai.utils.TiltSensorManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class CameraViewModel(
@ -18,7 +24,8 @@ class CameraViewModel(
private val poseAnalyzer: PoseAnalyzer,
private val captureHandler: CaptureHandler,
private val measurementCalculator: MeasurementCalculator,
private val tiltSensorManager: TiltSensorManager
private val tiltSensorManager: TiltSensorManager,
private val appDataUseCases: AppDataUseCases
) : ViewModel() {
private val _uiState = MutableStateFlow(CameraUiState())
@ -27,9 +34,18 @@ class CameraViewModel(
private var frameCounter = 0
private val frameSkipInterval = 5
private var isAutoCaptureOn = false
init {
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() {
@ -59,6 +75,8 @@ class CameraViewModel(
viewModelScope.launch {
val currentTilt = tilt.value
val currentAnalysisSize = Size(image.width, image.height)
val input = PipelineInput(
image = image,
deviceOrientation = deviceOrientation,
@ -76,49 +94,58 @@ class CameraViewModel(
// Step 1: Check Orientation
val orientationInstruction = orientationChecker.analyze(input)
if (!orientationInstruction.isValid) {
updateState(orientationInstruction)
updateState(orientationInstruction, analysisSize = currentAnalysisSize)
return@launch
}
// Step 2: Check Tilt
val tiltInstruction = tiltChecker.analyze(input)
if (!tiltInstruction.isValid) {
updateState(tiltInstruction)
updateState(tiltInstruction, analysisSize = currentAnalysisSize)
return@launch
}
// Step 3: Detect Objects
val detectionInstruction = objectDetector.analyze(input)
if (!detectionInstruction.isValid) {
updateState(detectionInstruction)
updateState(detectionInstruction, analysisSize = currentAnalysisSize)
return@launch
}
// Step 4: Check Pose (Silhouette matching)
val poseInstruction = poseAnalyzer.analyze(input.copy(previousDetectionResult = detectionInstruction.result as? DetectionResult))
if (!poseInstruction.isValid) {
updateState(poseInstruction, detectionInstruction.result as? DetectionResult)
updateState(poseInstruction, detectionInstruction.result as? DetectionResult, analysisSize = currentAnalysisSize)
return@launch
}
// All checks passed
_uiState.value = _uiState.value.copy(
val currentState = _uiState.value
_uiState.value = currentState.copy(
currentInstruction = Instruction("Ready to capture", isValid = 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
_uiState.value = _uiState.value.copy(
currentInstruction = instruction,
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(
image: Bitmap,
deviceOrientation: Int,
@ -127,8 +154,36 @@ class CameraViewModel(
) {
viewModelScope.launch {
val detectionResult = _uiState.value.lastDetectionResult ?: return@launch
val analysisSize = _uiState.value.analysisSize
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(
image = image,
deviceOrientation = deviceOrientation,
@ -142,7 +197,7 @@ class CameraViewModel(
orientation = _uiState.value.targetOrientation
)
val captureData = captureHandler.capture(input, detectionResult)
val captureData = captureHandler.capture(input, scaledDetectionResult)
_uiState.value = _uiState.value.copy(
captureData = captureData,
@ -171,7 +226,8 @@ class CameraViewModel(
captureData = null,
realWorldMetrics = null,
isReadyToCapture = false,
currentInstruction = null
currentInstruction = null,
shouldCapture = false
)
}
}
@ -183,7 +239,10 @@ data class CameraUiState(
val currentInstruction: Instruction? = null,
val isReadyToCapture: Boolean = false,
val lastDetectionResult: DetectionResult? = null,
val analysisSize: Size? = null,
val isPreviewMode: Boolean = false,
val captureData: CaptureData? = null,
val realWorldMetrics: RealWorldMetrics? = null
val realWorldMetrics: RealWorldMetrics? = null,
val shouldCapture: Boolean = false,
val isAutoCaptureOn: Boolean = false
)

View File

@ -1,6 +1,7 @@
package com.example.livingai.pages.camera
import android.net.Uri
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -17,11 +18,26 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.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.exifinterface.media.ExifInterface
import coil.compose.rememberAsyncImagePainter
import com.example.livingai.ui.theme.LivingAITheme
import java.io.InputStream
import kotlin.math.max
import kotlin.math.min
@Composable
fun ViewImageScreen(
@ -33,16 +49,94 @@ fun ViewImageScreen(
onAccept: () -> 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 {
Scaffold {
Box(modifier = Modifier
.fillMaxSize()
.padding(it)) {
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",
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(
modifier = Modifier
.fillMaxWidth()

View File

@ -27,7 +27,9 @@ fun LabeledDropdown(
options: List<String>,
selected: String?,
onSelected: (String) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
isError: Boolean = false,
supportingText: String? = null
) {
var expanded by remember { mutableStateOf(false) }
@ -46,7 +48,11 @@ fun LabeledDropdown(
},
modifier = Modifier
.menuAnchor(MenuAnchorType.PrimaryEditable, true)
.fillMaxWidth()
.fillMaxWidth(),
isError = isError,
supportingText = if (supportingText != null) {
{ Text(supportingText) }
} else null
)
ExposedDropdownMenu(

View File

@ -19,7 +19,9 @@ fun LabeledTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
keyboardType: KeyboardType = KeyboardType.Text
keyboardType: KeyboardType = KeyboardType.Text,
isError: Boolean = false,
supportingText: String? = null
) {
Column(modifier = modifier) {
OutlinedTextField(
@ -27,7 +29,11 @@ fun LabeledTextField(
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(id = labelRes)) },
keyboardOptions = KeyboardOptions(keyboardType = keyboardType)
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
isError = isError,
supportingText = if (supportingText != null) {
{ Text(supportingText) }
} else null
)
}
}

View File

@ -20,13 +20,14 @@ fun RadioGroup(
@StringRes titleRes: Int,
options: List<String>,
selected: String?,
onSelected: (String) -> Unit
onSelected: (String) -> Unit,
isError: Boolean = false
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(id = titleRes),
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
)
Row(

View File

@ -85,24 +85,24 @@ fun HomeScreen(navController: NavController) {
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))
}
)
// Spacer(modifier = Modifier.height(Dimentions.SMALL_PADDING))
//
// // Dropdown for selecting orientation
// LabeledDropdown(
// labelRes = R.string.default_orientation_label, // Or create a generic "Orientation" label
// options = orientationOptions,
// selected = selectedOrientationDisplay,
// onSelected = { selectedOrientationDisplay = it },
// modifier = Modifier.fillMaxWidth()
// )
//
// HomeButton(
// text = "Camera Capture",
// onClick = {
// val orientationId = displayToIdMap[selectedOrientationDisplay] ?: "side"
// navController.navigate(Route.CameraScreen(orientation = orientationId, animalId = "home_test"))
// }
// )
}
}

View File

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

View File

@ -98,7 +98,7 @@ fun NavGraph(
animalId = currentId ?: "unknown"
))
} 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 = {
@ -126,7 +126,7 @@ fun NavGraph(
composable<Route.CameraScreen> { backStackEntry ->
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 ->

View File

@ -19,7 +19,7 @@ sealed class Route {
@Serializable
data class RatingScreen(val animalId: String) : Route()
@Serializable
data class CameraScreen(val orientation: String) : Route()
data class CameraScreen(val orientation: String, val animalId: String) : Route()
@Serializable
data class OldCameraScreen(val orientation: String? = null, val animalId: String) : Route()
@Serializable