From ec35bea989a75c060744fd00dfdcb94efb5efed1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Sun, 6 Jul 2025 17:57:32 +0200 Subject: [PATCH] AboutScreen: basic version Update navigation to work with AboutScreen --- .../java/org/mydomain/myscan/MainActivity.kt | 16 ++- .../java/org/mydomain/myscan/MainViewModel.kt | 13 +- .../java/org/mydomain/myscan/Navigation.kt | 10 +- .../org/mydomain/myscan/view/AboutScreen.kt | 112 ++++++++++++++++++ .../org/mydomain/myscan/view/CameraScreen.kt | 21 +++- .../mydomain/myscan/view/DocumentScreen.kt | 23 +++- 6 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/mydomain/myscan/view/AboutScreen.kt diff --git a/app/src/main/java/org/mydomain/myscan/MainActivity.kt b/app/src/main/java/org/mydomain/myscan/MainActivity.kt index 9a8d468..61bae12 100644 --- a/app/src/main/java/org/mydomain/myscan/MainActivity.kt +++ b/app/src/main/java/org/mydomain/myscan/MainActivity.kt @@ -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) + } } } } diff --git a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt index ceed9b4..a9c076d 100644 --- a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt +++ b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt @@ -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.asStateFlow() - private val _currentScreen = MutableStateFlow(Screen.Camera) - val currentScreen: StateFlow = _currentScreen.asStateFlow() + private val _screenStack = MutableStateFlow>(listOf(Screen.Camera)) + val currentScreen: StateFlow = _screenStack.map { it.last() } + .stateIn(viewModelScope, SharingStarted.Eagerly, Screen.Camera) private val _pageIds = MutableStateFlow>(imageRepository.imageIds()) val pageIds: StateFlow> = _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?) { diff --git a/app/src/main/java/org/mydomain/myscan/Navigation.kt b/app/src/main/java/org/mydomain/myscan/Navigation.kt index 1558024..e7a76e4 100644 --- a/app/src/main/java/org/mydomain/myscan/Navigation.kt +++ b/app/src/main/java/org/mydomain/myscan/Navigation.kt @@ -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, +) diff --git a/app/src/main/java/org/mydomain/myscan/view/AboutScreen.kt b/app/src/main/java/org/mydomain/myscan/view/AboutScreen.kt new file mode 100644 index 0000000..8ebbed2 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/AboutScreen.kt @@ -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 . + */ +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 = {}) +} \ No newline at end of file diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt index 1551fbb..911e99f 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt @@ -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, + 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 = {} ) } } diff --git a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt index 83a0f61..78c30ac 100644 --- a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt @@ -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, 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()),