From 76a94172594840f4d5c736bb110980833f20fed5 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:33:58 +0200 Subject: [PATCH] Move Save & Share to a new Export screen --- .../java/org/fairscan/app/MainActivity.kt | 22 +- .../java/org/fairscan/app/MainViewModel.kt | 11 +- .../main/java/org/fairscan/app/Navigation.kt | 3 + .../main/java/org/fairscan/app/ui/UiState.kt | 7 +- .../java/org/fairscan/app/view/Dialogs.kt | 56 ++++ .../org/fairscan/app/view/DocumentScreen.kt | 72 +---- ...nerationBottomSheet.kt => ExportScreen.kt} | 252 +++++++++--------- .../org/fairscan/app/view/PreviewUtils.kt | 2 +- app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values-fr/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- .../java/org/fairscan/app/NavigationTest.kt | 4 + 12 files changed, 226 insertions(+), 218 deletions(-) create mode 100644 app/src/main/java/org/fairscan/app/view/Dialogs.kt rename app/src/main/java/org/fairscan/app/view/{PdfGenerationBottomSheet.kt => ExportScreen.kt} (61%) diff --git a/app/src/main/java/org/fairscan/app/MainActivity.kt b/app/src/main/java/org/fairscan/app/MainActivity.kt index 7171353..751a4f1 100644 --- a/app/src/main/java/org/fairscan/app/MainActivity.kt +++ b/app/src/main/java/org/fairscan/app/MainActivity.kt @@ -41,6 +41,7 @@ import org.fairscan.app.ui.theme.MyScanTheme import org.fairscan.app.view.AboutScreen import org.fairscan.app.view.CameraScreen import org.fairscan.app.view.DocumentScreen +import org.fairscan.app.view.ExportScreenWrapper import org.fairscan.app.view.HomeScreen import org.fairscan.app.view.LibrariesScreen import org.opencv.android.OpenCVLoader @@ -67,6 +68,7 @@ class MainActivity : ComponentActivity() { toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) }, toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) }, toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) }, + toExportScreen = { viewModel.navigateTo(Screen.Main.Export) }, toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) }, toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) }, back = { viewModel.navigateBack() } @@ -97,21 +99,26 @@ class MainActivity : ComponentActivity() { DocumentScreen ( document = document, initialPage = screen.initialPage, + navigation = navigation, + onDeleteImage = { id -> viewModel.deletePage(id) }, + onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) } + ) + } + is Screen.Main.Export -> { + ExportScreenWrapper( navigation = navigation, pdfActions = PdfGenerationActions( startGeneration = viewModel::startPdfGeneration, - cancelGeneration = viewModel::cancelPdfGeneration, setFilename = viewModel::setFilename, uiStateFlow = viewModel.pdfUiState, - sharePdf = { sharePdf(viewModel.getFinalPdf()) }, + sharePdf = { sharePdf(viewModel.getFinalPdf(), viewModel) }, savePdf = { savePdf(viewModel.getFinalPdf(), viewModel) }, openPdf = { openPdf(viewModel.pdfUiState.value.savedFileUri) } ), - onStartNew = { + onCloseScan = { viewModel.startNewDocument() - viewModel.navigateTo(Screen.Main.Home) }, - onDeleteImage = { id -> viewModel.deletePage(id) }, - onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) } + viewModel.navigateTo(Screen.Main.Home) + }, ) } is Screen.Overlay.About -> { @@ -125,9 +132,10 @@ class MainActivity : ComponentActivity() { } } - private fun sharePdf(generatedPdf: GeneratedPdf?) { + private fun sharePdf(generatedPdf: GeneratedPdf?, viewModel: MainViewModel) { if (generatedPdf == null) return + viewModel.setPdfAsShared() val file = generatedPdf.file val authority = "${applicationContext.packageName}.fileprovider" val fileUri = FileProvider.getUriForFile(this, authority, file) diff --git a/app/src/main/java/org/fairscan/app/MainViewModel.kt b/app/src/main/java/org/fairscan/app/MainViewModel.kt index 9085352..eda0363 100644 --- a/app/src/main/java/org/fairscan/app/MainViewModel.kt +++ b/app/src/main/java/org/fairscan/app/MainViewModel.kt @@ -268,11 +268,7 @@ class MainViewModel( } fun startPdfGeneration() { - val currentState = _pdfUiState.value - if (currentState.isGenerating || currentState.generatedPdf != null) return - - _pdfUiState.update { it.copy(isGenerating = true, errorMessage = null) } - + cancelPdfGeneration() generationJob = viewModelScope.launch { try { val result = generatePdf() @@ -299,6 +295,10 @@ class MainViewModel( _pdfUiState.value = PdfGenerationUiState() } + fun setPdfAsShared() { + _pdfUiState.update { it.copy(hasSharedPdf = true) } + } + fun getFinalPdf(): GeneratedPdf? { val tempPdf = _pdfUiState.value.generatedPdf ?: return null val tempFile = tempPdf.file @@ -374,7 +374,6 @@ data class GeneratedPdf( // TODO Move somewhere else: ViewModel should not depend on that data class PdfGenerationActions( val startGeneration: () -> Unit, - val cancelGeneration: () -> Unit, val setFilename: (String) -> Unit, val uiStateFlow: StateFlow,// TODO is it ok to have that here? val sharePdf: () -> Unit, diff --git a/app/src/main/java/org/fairscan/app/Navigation.kt b/app/src/main/java/org/fairscan/app/Navigation.kt index 7ba5b13..844ebc8 100644 --- a/app/src/main/java/org/fairscan/app/Navigation.kt +++ b/app/src/main/java/org/fairscan/app/Navigation.kt @@ -19,6 +19,7 @@ sealed class Screen { object Home : Main() object Camera : Main() data class Document(val initialPage: Int = 0) : Main() + object Export : Main() } sealed class Overlay : Screen() { object About : Overlay() @@ -30,6 +31,7 @@ data class Navigation( val toHomeScreen: () -> Unit, val toCameraScreen: () -> Unit, val toDocumentScreen: () -> Unit, + val toExportScreen: () -> Unit, val toAboutScreen: () -> Unit, val toLibrariesScreen: () -> Unit, val back: () -> Unit, @@ -57,6 +59,7 @@ data class NavigationState private constructor(val stack: List) { is Screen.Main.Home -> this // Back handled by system is Screen.Main.Camera -> copy(stack = listOf(Screen.Main.Home)) is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera)) + is Screen.Main.Export -> copy(stack = listOf(Screen.Main.Document())) is Screen.Overlay -> copy(stack = stack.dropLast(1)) } } diff --git a/app/src/main/java/org/fairscan/app/ui/UiState.kt b/app/src/main/java/org/fairscan/app/ui/UiState.kt index 520832a..978b88e 100644 --- a/app/src/main/java/org/fairscan/app/ui/UiState.kt +++ b/app/src/main/java/org/fairscan/app/ui/UiState.kt @@ -24,8 +24,11 @@ data class PdfGenerationUiState( val desiredFilename: String = "", val savedFileUri: Uri? = null, val saveDirectoryName: String? = null, - val errorMessage: String? = null -) + val hasSharedPdf: Boolean = false, + val errorMessage: String? = null, +) { + val hasSavedOrSharedPdf get() = savedFileUri != null || hasSharedPdf +} data class RecentDocumentUiState( val file: File, diff --git a/app/src/main/java/org/fairscan/app/view/Dialogs.kt b/app/src/main/java/org/fairscan/app/view/Dialogs.kt new file mode 100644 index 0000000..058dac4 --- /dev/null +++ b/app/src/main/java/org/fairscan/app/view/Dialogs.kt @@ -0,0 +1,56 @@ +/* + * 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.fairscan.app.view + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import org.fairscan.app.R + +@Composable +fun NewDocumentDialog(onConfirm: () -> Unit, showDialog: MutableState, title: String) { + ConfirmationDialog(title, stringResource(R.string.new_document_warning), showDialog, onConfirm) +} + +@Composable +fun ConfirmationDialog( + title: String, + message: String, + showDialog: MutableState, + onConfirm: () -> Unit, +) { + AlertDialog( + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + TextButton(onClick = { + showDialog.value = false + onConfirm() + }) { + Text(stringResource(R.string.yes), fontWeight = FontWeight.Bold) + } + }, + dismissButton = { + TextButton(onClick = { showDialog.value = false }) { + Text(stringResource(R.string.cancel), fontWeight = FontWeight.Bold) + } + }, + onDismissRequest = { showDialog.value = false }, + ) +} diff --git a/app/src/main/java/org/fairscan/app/view/DocumentScreen.kt b/app/src/main/java/org/fairscan/app/view/DocumentScreen.kt index d7a50e5..8209338 100644 --- a/app/src/main/java/org/fairscan/app/view/DocumentScreen.kt +++ b/app/src/main/java/org/fairscan/app/view/DocumentScreen.kt @@ -32,18 +32,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.RotateLeft import androidx.compose.material.icons.automirrored.filled.RotateRight import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.PictureAsPdf import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableIntState -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -53,16 +49,12 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview 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.fairscan.app.Navigation -import org.fairscan.app.PdfGenerationActions import org.fairscan.app.R -import org.fairscan.app.ui.PdfGenerationUiState import org.fairscan.app.ui.theme.MyScanTheme @OptIn(ExperimentalMaterial3Api::class) @@ -71,15 +63,11 @@ fun DocumentScreen( document: DocumentUiModel, initialPage: Int, navigation: Navigation, - pdfActions: PdfGenerationActions, - onStartNew: () -> Unit, onDeleteImage: (String) -> Unit, onRotateImage: (String, Boolean) -> Unit, ) { // TODO Check how often images are loaded - val showNewDocDialog = rememberSaveable { mutableStateOf(false) } val showDeletePageDialog = rememberSaveable { mutableStateOf(false) } - val showPdfDialog = rememberSaveable { mutableStateOf(false) } val currentPageIndex = rememberSaveable { mutableIntStateOf(initialPage) } if (currentPageIndex.intValue >= document.pageCount()) { currentPageIndex.intValue = document.pageCount() - 1 @@ -105,7 +93,7 @@ fun DocumentScreen( ), onBack = navigation.back, bottomBar = { - BottomBar(showPdfDialog, showNewDocDialog) + BottomBar(navigation) }, pageListButton = { SecondaryActionButton( @@ -121,9 +109,6 @@ fun DocumentScreen( { showDeletePageDialog.value = true }, onRotateImage, modifier) - if (showNewDocDialog.value) { - NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog, stringResource(R.string.close_document)) - } if (showDeletePageDialog.value) { ConfirmationDialog( title = stringResource(R.string.delete_page), @@ -131,12 +116,6 @@ fun DocumentScreen( showDialog = showDeletePageDialog ) { onDeleteImage(document.pageId(currentPageIndex.intValue)) } } - if (showPdfDialog.value) { - PdfGenerationBottomSheetWrapper( - onDismiss = { showPdfDialog.value = false }, - pdfActions = pdfActions, - ) - } } } @@ -226,8 +205,7 @@ fun RotationButtons( @Composable private fun BottomBar( - showPdfDialog: MutableState, - showNewDocDialog: MutableState, + navigation: Navigation, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -235,52 +213,13 @@ private fun BottomBar( horizontalArrangement = Arrangement.End ) { MainActionButton( - onClick = { showPdfDialog.value = true }, + onClick = navigation.toExportScreen, icon = Icons.Default.PictureAsPdf, text = stringResource(R.string.export_pdf), ) - Spacer(modifier = Modifier.width(8.dp)) - SecondaryActionButton( - icon = Icons.Default.Close, - contentDescription = stringResource(R.string.close_document), - onClick = { showNewDocDialog.value = true }, - modifier = Modifier.padding(vertical = 8.dp) - ) } } -@Composable -fun NewDocumentDialog(onConfirm: () -> Unit, showDialog: MutableState, title: String) { - ConfirmationDialog(title, stringResource(R.string.new_document_warning), showDialog, onConfirm) -} - -@Composable -private fun ConfirmationDialog( - title: String, - message: String, - showDialog: MutableState, - onConfirm: () -> Unit, -) { - AlertDialog( - title = { Text(title) }, - text = { Text(message) }, - confirmButton = { - TextButton(onClick = { - showDialog.value = false - onConfirm() - }) { - Text(stringResource(R.string.yes), fontWeight = FontWeight.Bold) - } - }, - dismissButton = { - TextButton(onClick = { showDialog.value = false }) { - Text(stringResource(R.string.cancel), fontWeight = FontWeight.Bold) - } - }, - onDismissRequest = { showDialog.value = false }, - ) -} - @Composable @Preview fun DocumentScreenPreview() { @@ -291,11 +230,6 @@ fun DocumentScreenPreview() { LocalContext.current), initialPage = 1, navigation = dummyNavigation(), - pdfActions = PdfGenerationActions( - {}, {}, {}, - MutableStateFlow(PdfGenerationUiState()), - {}, {}, {}), - onStartNew = {}, onDeleteImage = { _ -> }, onRotateImage = { _,_ -> }, ) diff --git a/app/src/main/java/org/fairscan/app/view/PdfGenerationBottomSheet.kt b/app/src/main/java/org/fairscan/app/view/ExportScreen.kt similarity index 61% rename from app/src/main/java/org/fairscan/app/view/PdfGenerationBottomSheet.kt rename to app/src/main/java/org/fairscan/app/view/ExportScreen.kt index 32b263c..28f51d9 100644 --- a/app/src/main/java/org/fairscan/app/view/PdfGenerationBottomSheet.kt +++ b/app/src/main/java/org/fairscan/app/view/ExportScreen.kt @@ -16,33 +16,31 @@ package org.fairscan.app.view import android.content.Context import android.text.format.Formatter +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background 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.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.PictureAsPdf import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -50,16 +48,19 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import org.fairscan.app.GeneratedPdf +import org.fairscan.app.Navigation import org.fairscan.app.PdfGenerationActions import org.fairscan.app.R import org.fairscan.app.ui.PdfGenerationUiState @@ -69,13 +70,15 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun PdfGenerationBottomSheetWrapper( - onDismiss: () -> Unit, +fun ExportScreenWrapper( + navigation: Navigation, pdfActions: PdfGenerationActions, - modifier: Modifier = Modifier, + onCloseScan: () -> Unit, ) { + BackHandler { navigation.back() } + + val showConfirmationDialog = rememberSaveable { mutableStateOf(false) } val filename = remember { mutableStateOf(defaultFilename()) } val uiState by pdfActions.uiStateFlow.collectAsState() LaunchedEffect(Unit) { @@ -83,11 +86,6 @@ fun PdfGenerationBottomSheetWrapper( pdfActions.startGeneration() } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - LaunchedEffect(Unit) { - sheetState.expand() - } - val onFilenameChange = { newName:String -> filename.value = newName pdfActions.setFilename(newName) @@ -99,110 +97,128 @@ fun PdfGenerationBottomSheetWrapper( } } - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - modifier = modifier.navigationBarsPadding() - ) { - PdfGenerationBottomSheet( - filename = filename, - onFilenameChange = onFilenameChange, - uiState = uiState, - onDismiss = { - pdfActions.cancelGeneration() - onDismiss() - }, - onShare = { - ensureCorrectFileName() - pdfActions.sharePdf() - }, - onSave = { - ensureCorrectFileName() - pdfActions.savePdf() - }, - onOpen = { pdfActions.openPdf() }, - ) + ExportScreen( + filename = filename, + onFilenameChange = onFilenameChange, + uiState = uiState, + navigation = navigation, + onShare = { + ensureCorrectFileName() + pdfActions.sharePdf() + }, + onSave = { + ensureCorrectFileName() + pdfActions.savePdf() + }, + onOpen = { pdfActions.openPdf() }, + onCloseScan = { + if (uiState.hasSavedOrSharedPdf) + onCloseScan() + else + showConfirmationDialog.value = true + }, + ) + + if (showConfirmationDialog.value) { + NewDocumentDialog(onCloseScan, showConfirmationDialog, stringResource(R.string.end_scan)) } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun PdfGenerationBottomSheet( +fun ExportScreen( filename: MutableState, onFilenameChange: (String) -> Unit, uiState: PdfGenerationUiState, - onDismiss: () -> Unit, + navigation: Navigation, onShare: () -> Unit, onSave: () -> Unit, onOpen: () -> Unit, + onCloseScan: () -> Unit, ) { - Column() { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.export_pdf)) }, + navigationIcon = { BackButton(navigation.back) }, + actions = { + AboutScreenNavButton(onClick = navigation.toAboutScreen) + } + ) + } + ) { innerPadding -> Column( modifier = Modifier - .fillMaxWidth() - .padding(top = 0.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + .padding(innerPadding) + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Row (verticalAlignment = Alignment.CenterVertically) { - Row { - Icon( - Icons.Default.PictureAsPdf, contentDescription = "PDF", - modifier = Modifier - .size(34.dp) - .padding(end = 8.dp) - ) - Text( - stringResource(R.string.export_pdf), - style = MaterialTheme.typography.headlineSmall - ) - } - CloseButton(onDismiss) - } - - Spacer(Modifier.height(16.dp)) - val focusRequester = remember { FocusRequester() } OutlinedTextField( value = filename.value, onValueChange = onFilenameChange, label = { Text(stringResource(R.string.filename)) }, singleLine = true, - modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), trailingIcon = { if (filename.value.isNotEmpty()) { IconButton(onClick = { filename.value = "" focusRequester.requestFocus() }) { - Icon(Icons.Default.Clear, contentDescription = "Effacer") + Icon(Icons.Default.Clear, stringResource(R.string.clear_text)) } } }, ) - Spacer(Modifier.height(8.dp)) - val pdf = uiState.generatedPdf - if (uiState.isGenerating) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } else if (pdf != null) { - val context = LocalContext.current - val formattedFileSize = formatFileSize(pdf.sizeInBytes, context) - Text( - text = "${pageCountText(pdf.pageCount)} · $formattedFileSize", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) + + // PDF infos + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + + if (uiState.isGenerating) { + Text(stringResource(R.string.creating_pdf), fontStyle = FontStyle.Italic) + } else if (pdf != null) { + val context = LocalContext.current + val formattedFileSize = formatFileSize(pdf.sizeInBytes, context) + Text(text = pageCountText(pdf.pageCount)) + Text( + text = stringResource(R.string.file_size, formattedFileSize), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } } - Spacer(Modifier.height(24.dp)) + if (uiState.saveDirectoryName != null) { + SavePdfBar(onOpen, uiState.saveDirectoryName) + } + if (uiState.errorMessage != null) { + ErrorBar(uiState.errorMessage) + } - MainActions(pdf, onShare, onSave) - } + Spacer(Modifier.weight(1f)) // push buttons down - if (uiState.saveDirectoryName != null) { - SavePdfBar(onOpen, uiState.saveDirectoryName) - } - if (uiState.errorMessage != null) { - ErrorBar(uiState.errorMessage) + // Export actions + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + MainActions(pdf, onShare, onSave) + + OutlinedButton( + onClick = onCloseScan, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Default.Done, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.end_scan)) + } + } } } } @@ -271,21 +287,6 @@ private fun ErrorBar(errorMessage: String) { ) } -@Composable -private fun CloseButton(onDismiss: () -> Unit) { - Box(Modifier.fillMaxWidth()) { - IconButton( - onClick = onDismiss, - modifier = Modifier.align(Alignment.TopEnd) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.close) - ) - } - } -} - fun defaultFilename(): String { val timestamp = SimpleDateFormat("yyyy-MM-dd HH.mm.ss", Locale.getDefault()).format(Date()) return "Scan $timestamp" @@ -296,57 +297,48 @@ fun formatFileSize(sizeInBytes: Long?, context: Context): String { else Formatter.formatShortFileSize(context, sizeInBytes) } -@Preview(showBackground = true) +@Preview @Composable -fun PreviewPdfGenerationDialogDuringGeneration() { - PreviewToCustomize( +fun PreviewExportScreenDuringGeneration() { + ExportPreviewToCustomize( uiState = PdfGenerationUiState(isGenerating = true) ) } -@Preview(showBackground = true) +@Preview @Composable -fun PreviewPdfGenerationDialogAfterGeneration() { - PreviewToCustomize( - uiState = PdfGenerationUiState( - generatedPdf = GeneratedPdf(File("fake.pdf"), 442897L, 1) - ) - ) -} - -@Preview(showBackground = true) -@Composable -fun PreviewPdfGenerationDialogAfterSave() { +fun PreviewExportScreenAfterSave() { val file = File("fake.pdf") - PreviewToCustomize( + ExportPreviewToCustomize( uiState = PdfGenerationUiState( generatedPdf = GeneratedPdf(file, 442897L, 3), - savedFileUri = file.toUri() - ) + savedFileUri = file.toUri(), + saveDirectoryName = "Downloads", + ), ) } -@Preview(showBackground = true) +@Preview @Composable -fun PreviewPdfGenerationDialogWithError() { - PreviewToCustomize( - uiState = PdfGenerationUiState( - errorMessage = "PDF generation failed" - ) +fun ExportScreenPreviewWithError() { + ExportPreviewToCustomize( + PdfGenerationUiState(errorMessage = "PDF generation failed") ) } @Composable -fun PreviewToCustomize(uiState: PdfGenerationUiState) { +fun ExportPreviewToCustomize(uiState: PdfGenerationUiState) { MyScanTheme { - PdfGenerationBottomSheet( - filename = remember { mutableStateOf("Scan 2025-07-02 17.40.42.pdf") }, + ExportScreen( + filename = remember { mutableStateOf("Scan 2025-07-02 17.40.42") }, + onFilenameChange = {_->}, + navigation = dummyNavigation(), uiState = uiState, - onFilenameChange = {}, - onDismiss = {}, onShare = {}, onSave = {}, onOpen = {}, + onCloseScan = {}, ) } } + diff --git a/app/src/main/java/org/fairscan/app/view/PreviewUtils.kt b/app/src/main/java/org/fairscan/app/view/PreviewUtils.kt index 6ccfa1b..5eddd80 100644 --- a/app/src/main/java/org/fairscan/app/view/PreviewUtils.kt +++ b/app/src/main/java/org/fairscan/app/view/PreviewUtils.kt @@ -19,7 +19,7 @@ import android.graphics.BitmapFactory import org.fairscan.app.Navigation fun dummyNavigation(): Navigation { - return Navigation({}, {}, {}, {}, {}, {}) + return Navigation({}, {}, {}, {}, {}, {}, {}) } fun fakeDocument(pageIds: List, context: Context): DocumentUiModel { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8d42425..801906e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,16 +10,19 @@ Text löschen Schließen Dokument schließen + PDF wird erstellt… Aktuelles Dokument Seite löschen Möchten Sie diese Seite löschen? Dokument + Scan beenden Fehler: %1$s Kein Dokument erkannt Keine App zum Öffnen von PDF gefunden PDF konnte nicht gespeichert werden PDF exportieren Dateiname + Dateigröße: %1$s Berechtigung erteilen Zuletzt gespeicherte Dokumente Bibliotheken @@ -28,7 +31,7 @@ Lizenz Diese Anwendung ist unter der GNU General Public License v3.0 lizenziert. Neues Dokument - Das aktuelle Dokument geht verloren, wenn Sie es nicht gespeichert haben. Möchten Sie fortfahren? + Das aktuelle Dokument geht verloren. Möchten Sie fortfahren? Öffnen PDF öffnen PDF gespeichert unter %1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0634bd9..48c7b69 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,16 +9,19 @@ Effacer le text Fermer Fermer le document + Création du PDF… Document en cours Supprimer la page Voulez-vous supprimer cette page ? Document + Terminer le scan Erreur : %1$s Aucun document détecté Aucune application trouvée pour ouvrir un PDF Échec de l\'enregistrement du PDF Exporter en PDF Nom de fichier + Taille du fichier : %1$s Autoriser Derniers documents enregistrés Bibliothèques @@ -27,7 +30,7 @@ Licence Cette application est distribuée sous licence GNU General Public License v3.0. Nouveau document - Le document en cours sera perdu si vous ne l\'avez pas enregistré. Voulez-vous continuer ? + Le scan en cours sera perdu. Voulez-vous continuer ? Ouvrir Ouvrir le PDF PDF enregistré dans %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be7ed56..b92427a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,16 +10,19 @@ Clear text Close Close document + Creating PDF… Current document Delete page Do you want to delete this page? Document + End scan Error: %1$s No document detected No app found to open PDF Failed to save PDF Export PDF Filename + File size: %1$s Grant permission Last saved documents Libraries @@ -28,7 +31,7 @@ License This application is licensed under the GNU General Public License v3.0. New document - The current document will be lost if you haven\'t saved it. Do you want to continue? + The current scan will be lost. Do you want to continue? Open Open PDF PDF saved to %1$s diff --git a/app/src/test/java/org/fairscan/app/NavigationTest.kt b/app/src/test/java/org/fairscan/app/NavigationTest.kt index 8c546fc..f306145 100644 --- a/app/src/test/java/org/fairscan/app/NavigationTest.kt +++ b/app/src/test/java/org/fairscan/app/NavigationTest.kt @@ -17,6 +17,7 @@ package org.fairscan.app import org.assertj.core.api.Assertions.assertThat import org.fairscan.app.Screen.Main.Camera import org.fairscan.app.Screen.Main.Document +import org.fairscan.app.Screen.Main.Export import org.fairscan.app.Screen.Main.Home import org.fairscan.app.Screen.Overlay.About import org.fairscan.app.Screen.Overlay.Libraries @@ -36,10 +37,12 @@ class NavigationTest { val atHome = NavigationState.initial() val atCamera = atHome.navigateTo(Camera) val atDocument = atHome.navigateTo(Document()) + val atExport = atHome.navigateTo(Export) assertThat(atHome.current).isEqualTo(Home) assertThat(atCamera.current).isEqualTo(Camera) assertThat(atDocument.current).isEqualTo(Document()) + assertThat(atExport.current).isEqualTo(Export) assertThat(atCamera.navigateTo(Document())).isEqualTo(atDocument) assertThat(atDocument.navigateTo(Home)).isEqualTo(atHome) @@ -48,6 +51,7 @@ class NavigationTest { assertThat(atHome.navigateBack()).isEqualTo(atHome) assertThat(atCamera.navigateBack()).isEqualTo(atHome) assertThat(atDocument.navigateBack()).isEqualTo(atCamera) + assertThat(atExport.navigateBack()).isEqualTo(atDocument) } @Test