AboutScreen: basic version

Update navigation to work with AboutScreen
This commit is contained in:
Pierre-Yves Nicolas
2025-07-06 17:57:32 +02:00
parent 7a0309a052
commit ec35bea989
6 changed files with 181 additions and 14 deletions

View File

@@ -38,6 +38,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.mydomain.myscan.ui.theme.MyScanTheme
import org.mydomain.myscan.view.AboutScreen
import org.mydomain.myscan.view.CameraScreen
import org.mydomain.myscan.view.DocumentScreen
import org.opencv.android.OpenCVLoader
@@ -57,21 +58,27 @@ class MainActivity : ComponentActivity() {
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
val pageIds by viewModel.pageIds.collectAsStateWithLifecycle()
MyScanTheme {
val navigation = Navigation(
toCameraScreen = { viewModel.navigateTo(Screen.Camera) },
toDocumentScreen = { viewModel.navigateTo(Screen.Document()) },
toAboutScreen = { viewModel.navigateTo(Screen.About) },
back = { viewModel.navigateBack() }
)
when (val screen = currentScreen) {
is Screen.Camera -> {
CameraScreen(
viewModel,
liveAnalysisState,
onImageAnalyzed = { image -> viewModel.liveAnalysis(image) },
onFinalizePressed = { viewModel.navigateTo(Screen.FinalizeDocument()) },
onFinalizePressed = { viewModel.navigateTo(Screen.Document()) },
)
}
is Screen.FinalizeDocument -> {
is Screen.Document -> {
DocumentScreen (
pageIds,
initialPage = screen.initialPage,
imageLoader = { id -> viewModel.getBitmap(id) },
toCameraScreen = { viewModel.navigateTo(Screen.Camera) },
navigation = navigation,
pdfActions = PdfGenerationActions(
startGeneration = viewModel::startPdfGeneration,
cancelGeneration = viewModel::cancelPdfGeneration,
@@ -87,6 +94,9 @@ class MainActivity : ComponentActivity() {
onDeleteImage = { id -> viewModel.deletePage(id) }
)
}
is Screen.About -> {
AboutScreen(onBack = navigation.back)
}
}
}
}

View File

@@ -29,10 +29,12 @@ import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -63,8 +65,9 @@ class MainViewModel(
private var _liveAnalysisState = MutableStateFlow(LiveAnalysisState())
val liveAnalysisState: StateFlow<LiveAnalysisState> = _liveAnalysisState.asStateFlow()
private val _currentScreen = MutableStateFlow<Screen>(Screen.Camera)
val currentScreen: StateFlow<Screen> = _currentScreen.asStateFlow()
private val _screenStack = MutableStateFlow<List<Screen>>(listOf(Screen.Camera))
val currentScreen: StateFlow<Screen> = _screenStack.map { it.last() }
.stateIn(viewModelScope, SharingStarted.Eagerly, Screen.Camera)
private val _pageIds = MutableStateFlow<List<String>>(imageRepository.imageIds())
val pageIds: StateFlow<List<String>> = _pageIds
@@ -135,7 +138,11 @@ class MainViewModel(
}
fun navigateTo(screen: Screen) {
_currentScreen.value = screen
_screenStack.update { it + screen }
}
fun navigateBack() {
_screenStack.update { stack -> if (stack.size > 1) stack.dropLast(1) else stack }
}
fun onImageCaptured(imageProxy: ImageProxy?) {

View File

@@ -16,5 +16,13 @@ package org.mydomain.myscan
sealed class Screen {
object Camera : Screen()
data class FinalizeDocument(val initialPage: Int = 0) : Screen()
data class Document(val initialPage: Int = 0) : Screen()
object About : Screen()
}
data class Navigation(
val toCameraScreen: () -> Unit,
val toDocumentScreen: () -> Unit,
val toAboutScreen: () -> Unit,
val back: () -> Unit,
)

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(onBack: () -> Unit) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("About") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
)
}
) { paddingValues ->
AboutContent(Modifier.padding(paddingValues))
}
}
@Composable
fun AboutContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
"MyScan",
style = MaterialTheme.typography.headlineSmall
)
Spacer(Modifier.height(8.dp))
Text(
"A simple and respectful application to scan your documents.",
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(16.dp))
HorizontalDivider()
Spacer(Modifier.height(16.dp))
Text(
"Version",
style = MaterialTheme.typography.titleSmall
)
Text("1.0.0")
Spacer(Modifier.height(16.dp))
Text(
"License",
style = MaterialTheme.typography.titleSmall
)
Text("This application is published under the GPLv3 license.")
Spacer(Modifier.height(16.dp))
Text(
"This application is based on the following open-source libraries",
style = MaterialTheme.typography.titleSmall
)
Text("• CameraX\n• Jetpack Compose\n• LiteRT\n• OpenCV\n• PDFBox")
Spacer(Modifier.height(32.dp))
}
}
@Preview
@Composable
fun AboutScreenPreview() {
AboutScreen(onBack = {})
}

View File

@@ -45,7 +45,10 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
@@ -146,7 +149,7 @@ fun CameraScreen(
CommonPageList(
pageIds = pageIds,
imageLoader = { id -> viewModel.getBitmap(id) },
onPageClick = { index -> viewModel.navigateTo(Screen.FinalizeDocument(index)) },
onPageClick = { index -> viewModel.navigateTo(Screen.Document(index)) },
listState = listState,
onLastItemPosition =
{ offset -> thumbnailCoords.value = offset }
@@ -170,6 +173,7 @@ fun CameraScreen(
onFinalizePressed = onFinalizePressed,
onDebugModeSwitched = { isDebugMode = !isDebugMode },
thumbnailCoords = thumbnailCoords,
toAboutScreen = { viewModel.navigateTo(Screen.About) }
)
}
@@ -182,6 +186,7 @@ private fun CameraScreenScaffold(
onFinalizePressed: () -> Unit,
onDebugModeSwitched: () -> Unit,
thumbnailCoords: MutableState<Offset>,
toAboutScreen: () -> Unit,
) {
Box {
Scaffold(
@@ -199,6 +204,19 @@ private fun CameraScreenScaffold(
.padding(bottom = innerPadding.calculateBottomPadding())
.fillMaxSize()
) {
Box(
modifier = Modifier.fillMaxSize().padding(innerPadding)
) {
IconButton(
onClick = toAboutScreen,
modifier = Modifier.align(Alignment.TopEnd)
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = "About",
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f))
}
}
CameraPreviewWithOverlay(cameraPreview, cameraUiState, Modifier.align(Alignment.BottomCenter))
if (cameraUiState.isDebugMode) {
MessageBox(cameraUiState.liveAnalysisState.inferenceTime)
@@ -469,6 +487,7 @@ private fun ScreenPreview(captureState: CaptureState) {
onFinalizePressed = {},
onDebugModeSwitched = {},
thumbnailCoords = thumbnailCoords,
toAboutScreen = {}
)
}
}

View File

@@ -19,6 +19,7 @@ import android.graphics.BitmapFactory
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -36,6 +37,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.PictureAsPdf
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -65,6 +67,7 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.MutableStateFlow
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
import org.mydomain.myscan.Navigation
import org.mydomain.myscan.PdfGenerationActions
import org.mydomain.myscan.ui.PdfGenerationUiState
import org.mydomain.myscan.ui.theme.MyScanTheme
@@ -75,7 +78,7 @@ fun DocumentScreen(
pageIds: List<String>,
initialPage: Int,
imageLoader: (String) -> Bitmap?,
toCameraScreen: () -> Unit,
navigation: Navigation,
pdfActions: PdfGenerationActions,
onStartNew: () -> Unit,
onDeleteImage: (String) -> Unit,
@@ -88,11 +91,11 @@ fun DocumentScreen(
currentPageIndex.intValue = pageIds.size - 1
}
if (currentPageIndex.intValue < 0) {
toCameraScreen()
navigation.toCameraScreen()
return
}
BackHandler {
toCameraScreen()
navigation.back()
}
Scaffold (
topBar = {
@@ -103,15 +106,23 @@ fun DocumentScreen(
),
title = { Text("Document") },
navigationIcon = {
IconButton(onClick = toCameraScreen) {
IconButton(onClick = navigation.toCameraScreen) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = navigation.toAboutScreen) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = "About",
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f))
}
}
)
},
bottomBar = {
Column {
PageList(pageIds, imageLoader, currentPageIndex, toCameraScreen)
PageList(pageIds, imageLoader, currentPageIndex, navigation.toCameraScreen)
BottomBar(showPdfDialog, showNewDocDialog)
}
}
@@ -280,7 +291,7 @@ fun DocumentScreenPreview() {
BitmapFactory.decodeStream(input)
}
},
toCameraScreen = {},
navigation = Navigation({}, {}, {}, {}),
pdfActions = PdfGenerationActions(
{}, {}, {},
MutableStateFlow(PdfGenerationUiState()),