Multi-page step 2: FinalizeDocumentScreen
This commit is contained in:
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
// TODO Rename to LiveAnalysisState
|
||||||
data class CameraScreenState(
|
data class CameraScreenState(
|
||||||
val detectionMessage: String? = null,
|
val detectionMessage: String? = null,
|
||||||
val inferenceTime: Long = 0L,
|
val inferenceTime: Long = 0L,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import androidx.core.content.FileProvider
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||||
import org.mydomain.myscan.view.CameraScreen
|
import org.mydomain.myscan.view.CameraScreen
|
||||||
import org.mydomain.myscan.view.PagePreviewScreen
|
import org.mydomain.myscan.view.FinalizeDocumentScreen
|
||||||
import org.opencv.android.OpenCVLoader
|
import org.opencv.android.OpenCVLoader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -38,22 +38,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
||||||
val cameraScreenState by viewModel.cameraScreenState.collectAsStateWithLifecycle()
|
val cameraScreenState by viewModel.cameraScreenState.collectAsStateWithLifecycle()
|
||||||
|
val pages by viewModel.pages.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
MyScanTheme {
|
MyScanTheme {
|
||||||
Scaffold { innerPadding ->
|
Scaffold { innerPadding ->
|
||||||
Column (modifier = Modifier.padding(innerPadding)) {
|
Column (modifier = Modifier.padding(innerPadding)) {
|
||||||
when (val screen = currentScreen) {
|
when (currentScreen) {
|
||||||
is Screen.Camera -> {
|
is Screen.Camera -> {
|
||||||
CameraScreen(viewModel, cameraScreenState,
|
CameraScreen(viewModel, cameraScreenState,
|
||||||
onImageAnalyzed = { image -> viewModel.segment(image) } )
|
onImageAnalyzed = { image -> viewModel.segment(image) },
|
||||||
|
onFinalizePressed = { viewModel.navigateTo(Screen.FinalizeDocument) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is Screen.PagePreview -> {
|
is Screen.FinalizeDocument -> {
|
||||||
PagePreviewScreen (
|
FinalizeDocumentScreen (
|
||||||
image = screen.image,
|
viewModel,
|
||||||
isProcessing = screen.isProcessing,
|
|
||||||
onBackPressed = { viewModel.navigateTo(Screen.Camera) },
|
onBackPressed = { viewModel.navigateTo(Screen.Camera) },
|
||||||
onSavePressed = createPdfAndSave(context),
|
onSavePressed = savePdf(pages, context),
|
||||||
onSharePressed = createPdfAndShare(context),
|
// TODO "on share"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,8 +95,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPdfAndSave(context: Context): (Bitmap) -> Unit = { bitmap ->
|
private fun savePdf(
|
||||||
val document = createPdfFromBitmaps(listOf(bitmap))
|
pages: List<Bitmap>,
|
||||||
|
context: Context
|
||||||
|
): () -> Unit = {
|
||||||
|
val document = createPdfFromBitmaps(pages)
|
||||||
try {
|
try {
|
||||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -33,6 +35,10 @@ class MainViewModel(private val imageSegmentationService: ImageSegmentationServi
|
|||||||
private val _currentScreen = MutableStateFlow<Screen>(Screen.Camera)
|
private val _currentScreen = MutableStateFlow<Screen>(Screen.Camera)
|
||||||
val currentScreen: StateFlow<Screen> = _currentScreen.asStateFlow()
|
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 {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageSegmentationService.initialize()
|
imageSegmentationService.initialize()
|
||||||
@@ -96,6 +102,8 @@ class MainViewModel(private val imageSegmentationService: ImageSegmentationServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addPage(bitmap: Bitmap) {
|
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
|
package org.mydomain.myscan
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
|
|
||||||
sealed class Screen {
|
sealed class Screen {
|
||||||
object Camera : Screen()
|
object Camera : Screen()
|
||||||
data class PagePreview(
|
object FinalizeDocument : Screen()
|
||||||
val image: Bitmap? = null,
|
|
||||||
val isProcessing: Boolean = true
|
|
||||||
) : Screen()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,17 @@ import androidx.camera.core.resolutionselector.ResolutionSelector
|
|||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -58,11 +61,14 @@ import org.mydomain.myscan.scaledTo
|
|||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
// TODO Split this big file
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CameraScreen(
|
fun CameraScreen(
|
||||||
viewModel: MainViewModel,
|
viewModel: MainViewModel,
|
||||||
uiState: CameraScreenState,
|
uiState: CameraScreenState,
|
||||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||||
|
onFinalizePressed: () -> Unit
|
||||||
) {
|
) {
|
||||||
// TODO Should we move those variables to ViewModel?
|
// TODO Should we move those variables to ViewModel?
|
||||||
// TODO pause the live analysis when displaying the PageValidationDialogs
|
// TODO pause the live analysis when displaying the PageValidationDialogs
|
||||||
@@ -91,19 +97,8 @@ fun CameraScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
val width = LocalConfiguration.current.screenWidthDp
|
CameraPreviewWithOverlay(onImageAnalyzed, captureController, uiState)
|
||||||
val height = width / 3 * 4
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(width.dp)
|
|
||||||
.height(height.dp)
|
|
||||||
) {
|
|
||||||
CameraPreview(
|
|
||||||
onImageAnalyzed = onImageAnalyzed,
|
|
||||||
captureController = captureController)
|
|
||||||
AnalysisOverlay(uiState)
|
|
||||||
}
|
|
||||||
MessageBox(uiState.inferenceTime)
|
MessageBox(uiState.inferenceTime)
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -121,10 +116,14 @@ fun CameraScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)},
|
)},
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 96.dp),
|
||||||
) {
|
) {
|
||||||
Text("Capture")
|
Text("Capture")
|
||||||
}
|
}
|
||||||
|
CameraScreenFooter(
|
||||||
|
pageCount = viewModel.pageCount(),
|
||||||
|
onFinalizePressed = onFinalizePressed,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPageDialog.value) {
|
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
|
@Composable
|
||||||
fun CameraPreview(
|
fun CameraPreview(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -299,3 +319,36 @@ fun MessageBox(inferenceTime: Long) {
|
|||||||
color = Color.Gray,
|
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