Multi-page step 2: FinalizeDocumentScreen
This commit is contained in:
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
// TODO Rename to LiveAnalysisState
|
||||
data class CameraScreenState(
|
||||
val detectionMessage: String? = null,
|
||||
val inferenceTime: Long = 0L,
|
||||
|
||||
@@ -22,7 +22,7 @@ import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||
import org.mydomain.myscan.view.CameraScreen
|
||||
import org.mydomain.myscan.view.PagePreviewScreen
|
||||
import org.mydomain.myscan.view.FinalizeDocumentScreen
|
||||
import org.opencv.android.OpenCVLoader
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -38,22 +38,24 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
||||
val cameraScreenState by viewModel.cameraScreenState.collectAsStateWithLifecycle()
|
||||
val pages by viewModel.pages.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
MyScanTheme {
|
||||
Scaffold { innerPadding ->
|
||||
Column (modifier = Modifier.padding(innerPadding)) {
|
||||
when (val screen = currentScreen) {
|
||||
when (currentScreen) {
|
||||
is Screen.Camera -> {
|
||||
CameraScreen(viewModel, cameraScreenState,
|
||||
onImageAnalyzed = { image -> viewModel.segment(image) } )
|
||||
onImageAnalyzed = { image -> viewModel.segment(image) },
|
||||
onFinalizePressed = { viewModel.navigateTo(Screen.FinalizeDocument) }
|
||||
)
|
||||
}
|
||||
is Screen.PagePreview -> {
|
||||
PagePreviewScreen (
|
||||
image = screen.image,
|
||||
isProcessing = screen.isProcessing,
|
||||
is Screen.FinalizeDocument -> {
|
||||
FinalizeDocumentScreen (
|
||||
viewModel,
|
||||
onBackPressed = { viewModel.navigateTo(Screen.Camera) },
|
||||
onSavePressed = createPdfAndSave(context),
|
||||
onSharePressed = createPdfAndShare(context),
|
||||
onSavePressed = savePdf(pages, context),
|
||||
// TODO "on share"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,8 +95,11 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createPdfAndSave(context: Context): (Bitmap) -> Unit = { bitmap ->
|
||||
val document = createPdfFromBitmaps(listOf(bitmap))
|
||||
private fun savePdf(
|
||||
pages: List<Bitmap>,
|
||||
context: Context
|
||||
): () -> Unit = {
|
||||
val document = createPdfFromBitmaps(pages)
|
||||
try {
|
||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -33,6 +35,10 @@ class MainViewModel(private val imageSegmentationService: ImageSegmentationServi
|
||||
private val _currentScreen = MutableStateFlow<Screen>(Screen.Camera)
|
||||
val currentScreen: StateFlow<Screen> = _currentScreen.asStateFlow()
|
||||
|
||||
// TODO store images on disk
|
||||
private val _pages = MutableStateFlow<List<Bitmap>>(listOf())
|
||||
val pages: StateFlow<List<Bitmap>> = _pages
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
imageSegmentationService.initialize()
|
||||
@@ -96,6 +102,8 @@ class MainViewModel(private val imageSegmentationService: ImageSegmentationServi
|
||||
}
|
||||
|
||||
fun addPage(bitmap: Bitmap) {
|
||||
// TODO
|
||||
_pages.update { list -> list.plus(bitmap) }
|
||||
}
|
||||
|
||||
fun pageCount(): Int = _pages.value.size
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package org.mydomain.myscan
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
sealed class Screen {
|
||||
object Camera : Screen()
|
||||
data class PagePreview(
|
||||
val image: Bitmap? = null,
|
||||
val isProcessing: Boolean = true
|
||||
) : Screen()
|
||||
object FinalizeDocument : Screen()
|
||||
}
|
||||
|
||||
@@ -19,14 +19,17 @@ import androidx.camera.core.resolutionselector.ResolutionSelector
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.Canvas
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -58,11 +61,14 @@ import org.mydomain.myscan.scaledTo
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
// TODO Split this big file
|
||||
|
||||
@Composable
|
||||
fun CameraScreen(
|
||||
viewModel: MainViewModel,
|
||||
uiState: CameraScreenState,
|
||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||
onFinalizePressed: () -> Unit
|
||||
) {
|
||||
// TODO Should we move those variables to ViewModel?
|
||||
// TODO pause the live analysis when displaying the PageValidationDialogs
|
||||
@@ -91,19 +97,8 @@ fun CameraScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
val width = LocalConfiguration.current.screenWidthDp
|
||||
val height = width / 3 * 4
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(width.dp)
|
||||
.height(height.dp)
|
||||
) {
|
||||
CameraPreview(
|
||||
onImageAnalyzed = onImageAnalyzed,
|
||||
captureController = captureController)
|
||||
AnalysisOverlay(uiState)
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CameraPreviewWithOverlay(onImageAnalyzed, captureController, uiState)
|
||||
MessageBox(uiState.inferenceTime)
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -121,10 +116,14 @@ fun CameraScreen(
|
||||
}
|
||||
}
|
||||
)},
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 96.dp),
|
||||
) {
|
||||
Text("Capture")
|
||||
}
|
||||
CameraScreenFooter(
|
||||
pageCount = viewModel.pageCount(),
|
||||
onFinalizePressed = onFinalizePressed,
|
||||
modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
if (showPageDialog.value) {
|
||||
@@ -145,6 +144,27 @@ fun CameraScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraPreviewWithOverlay(
|
||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||
captureController: CameraCaptureController,
|
||||
uiState: CameraScreenState
|
||||
) {
|
||||
val width = LocalConfiguration.current.screenWidthDp
|
||||
val height = width / 3 * 4
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(width.dp)
|
||||
.height(height.dp)
|
||||
) {
|
||||
CameraPreview(
|
||||
onImageAnalyzed = onImageAnalyzed,
|
||||
captureController = captureController
|
||||
)
|
||||
AnalysisOverlay(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CameraPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -299,3 +319,36 @@ fun MessageBox(inferenceTime: Long) {
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CameraScreenFooter(
|
||||
pageCount: Int,
|
||||
onFinalizePressed: () -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Surface (
|
||||
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
tonalElevation = 4.dp,
|
||||
modifier = modifier.fillMaxWidth().height(56.dp)
|
||||
) {
|
||||
Row (
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Pages : $pageCount",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Button (
|
||||
onClick = onFinalizePressed,
|
||||
enabled = pageCount > 0
|
||||
) {
|
||||
Text("Finish")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.mydomain.myscan.view
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.mydomain.myscan.MainViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FinalizeDocumentScreen(
|
||||
viewModel: MainViewModel = viewModel(),
|
||||
onBackPressed: () -> Unit,
|
||||
onSavePressed: () -> Unit
|
||||
) {
|
||||
val pages: List<Bitmap> by viewModel.pages.collectAsStateWithLifecycle()
|
||||
Scaffold (
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Finalize document") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackPressed) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Button(onClick = onSavePressed) {
|
||||
Text(text = "Save PDF")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn (
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.background(Color(0xFFF0F0F0)),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(pages) { bitmap ->
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = CardDefaults.cardElevation(4.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentDescription = "Page",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(bitmap.width.toFloat() / bitmap.height)
|
||||
.background(Color.White)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package org.mydomain.myscan.view
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun PagePreviewScreen(
|
||||
image: Bitmap?,
|
||||
isProcessing: Boolean,
|
||||
onBackPressed: () -> Unit,
|
||||
onSavePressed: (Bitmap) -> Unit,
|
||||
onSharePressed: (Bitmap) -> Unit,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
when {
|
||||
isProcessing -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
image != null -> {
|
||||
Image(
|
||||
bitmap = image.asImageBitmap(),
|
||||
contentDescription = "Document preview",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Button (onClick = { onSavePressed(image) }) {
|
||||
Text("Save PDF")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button (onClick = { onSharePressed(image) }) {
|
||||
Text("Share PDF")
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Text(
|
||||
text = "No image is available.",
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton (
|
||||
onClick = onBackPressed,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user