From 0b76c3fc1e6cd39e0ffb28bd85dd85a35b4902a8 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:16:05 +0200 Subject: [PATCH] Multi-page step 1: PageValidationDialog --- .../java/org/mydomain/myscan/MainViewModel.kt | 14 ++-- .../java/org/mydomain/myscan/view/Camera.kt | 30 ++++++- .../mydomain/myscan/view/PageValidation.kt | 84 +++++++++++++++++++ 3 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/mydomain/myscan/view/PageValidation.kt diff --git a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt index 7e9d4f2..58efa1a 100644 --- a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt +++ b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt @@ -3,7 +3,6 @@ package org.mydomain.myscan import android.content.Context import android.graphics.Bitmap import android.graphics.Matrix -import android.util.Log import androidx.camera.core.ImageProxy import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -68,13 +67,10 @@ class MainViewModel(private val imageSegmentationService: ImageSegmentationServi _currentScreen.value = screen } - fun processCapturedImageAndNavigate(imageProxy: ImageProxy) { + fun processCapturedImageThen(imageProxy: ImageProxy, onResult: (Bitmap?) -> Unit) { viewModelScope.launch { - Log.d("MyScan", "Navigating to spinner") - navigateTo(Screen.PagePreview(image = null, isProcessing = true)) val processedImage = processCapturedImage(imageProxy) - Log.d("MyScan", "Navigating to result image") - navigateTo(Screen.PagePreview(image = processedImage, isProcessing = false)) + onResult(processedImage) } } @@ -98,4 +94,8 @@ class MainViewModel(private val imageSegmentationService: ImageSegmentationServi val matrix = Matrix().apply { postRotate(degrees.toFloat()) } return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) } -} \ No newline at end of file + + fun addPage(bitmap: Bitmap) { + // TODO + } +} diff --git a/app/src/main/java/org/mydomain/myscan/view/Camera.kt b/app/src/main/java/org/mydomain/myscan/view/Camera.kt index 779d8d5..dc839ad 100644 --- a/app/src/main/java/org/mydomain/myscan/view/Camera.kt +++ b/app/src/main/java/org/mydomain/myscan/view/Camera.kt @@ -64,6 +64,12 @@ fun CameraScreen( uiState: CameraScreenState, onImageAnalyzed: (ImageProxy) -> Unit, ) { + // TODO Should we move those variables to ViewModel? + // TODO pause the live analysis when displaying the PageValidationDialogs + val showPageDialog = remember { mutableStateOf(false) } + val isProcessing = remember { mutableStateOf(false) } + val processedPage = remember { mutableStateOf(null) } + val context = LocalContext.current val requestPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() @@ -101,10 +107,15 @@ fun CameraScreen( MessageBox(uiState.inferenceTime) Button( onClick = { + showPageDialog.value = true + isProcessing.value = true captureController.takePicture( onImageCaptured = { imageProxy -> if (imageProxy != null) { - viewModel.processCapturedImageAndNavigate(imageProxy) + viewModel.processCapturedImageThen(imageProxy) { result -> + processedPage.value = result + isProcessing.value = false + } } else { Log.e("MyScan", "Error during image capture") } @@ -115,6 +126,23 @@ fun CameraScreen( Text("Capture") } } + + if (showPageDialog.value) { + PageValidationDialog( + isProcessing = isProcessing.value, + pageBitmap = processedPage.value, + onConfirm = { + viewModel.addPage(processedPage.value!!) + showPageDialog.value = false + }, + onReject = { + showPageDialog.value = false + }, + onDismiss = { + showPageDialog.value = false + } + ) + } } @Composable diff --git a/app/src/main/java/org/mydomain/myscan/view/PageValidation.kt b/app/src/main/java/org/mydomain/myscan/view/PageValidation.kt new file mode 100644 index 0000000..cc53810 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/PageValidation.kt @@ -0,0 +1,84 @@ +package org.mydomain.myscan.view + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +@Composable +fun PageValidationDialog( + isProcessing: Boolean, + pageBitmap: Bitmap?, + onConfirm: () -> Unit, + onReject: () -> Unit, + onDismiss: () -> Unit +) { + Dialog (onDismissRequest = onDismiss) { + Surface ( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp, + modifier = Modifier + .fillMaxSize() + .aspectRatio(3f / 4f) + ) { + if (isProcessing) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else { + Column ( + Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + if (pageBitmap == null) { + Text("Failed to process image") + } else { + Image( + bitmap = pageBitmap.asImageBitmap(), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentScale = ContentScale.Fit + ) + } + + Row ( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedButton (onClick = onReject) { + Text("Reject") + } + Button (onClick = onConfirm) { + Text("OK") + } + } + } + } + } + } +}