Move Save & Share to a new Export screen
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<PdfGenerationUiState>,// TODO is it ok to have that here?
|
||||
val sharePdf: () -> Unit,
|
||||
|
||||
@@ -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<Screen>) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
56
app/src/main/java/org/fairscan/app/view/Dialogs.kt
Normal file
56
app/src/main/java/org/fairscan/app/view/Dialogs.kt
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Boolean>, title: String) {
|
||||
ConfirmationDialog(title, stringResource(R.string.new_document_warning), showDialog, onConfirm)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConfirmationDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
showDialog: MutableState<Boolean>,
|
||||
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 },
|
||||
)
|
||||
}
|
||||
@@ -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<Boolean>,
|
||||
showNewDocDialog: MutableState<Boolean>,
|
||||
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<Boolean>, title: String) {
|
||||
ConfirmationDialog(title, stringResource(R.string.new_document_warning), showDialog, onConfirm)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmationDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
showDialog: MutableState<Boolean>,
|
||||
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 = { _,_ -> },
|
||||
)
|
||||
|
||||
@@ -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<String>,
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import android.graphics.BitmapFactory
|
||||
import org.fairscan.app.Navigation
|
||||
|
||||
fun dummyNavigation(): Navigation {
|
||||
return Navigation({}, {}, {}, {}, {}, {})
|
||||
return Navigation({}, {}, {}, {}, {}, {}, {})
|
||||
}
|
||||
|
||||
fun fakeDocument(pageIds: List<String>, context: Context): DocumentUiModel {
|
||||
|
||||
@@ -10,16 +10,19 @@
|
||||
<string name="clear_text">Text löschen</string>
|
||||
<string name="close">Schließen</string>
|
||||
<string name="close_document">Dokument schließen</string>
|
||||
<string name="creating_pdf">PDF wird erstellt…</string>
|
||||
<string name="current_document">Aktuelles Dokument</string>
|
||||
<string name="delete_page">Seite löschen</string>
|
||||
<string name="delete_page_warning">Möchten Sie diese Seite löschen?</string>
|
||||
<string name="document">Dokument</string>
|
||||
<string name="end_scan">Scan beenden</string>
|
||||
<string name="error">Fehler: %1$s</string>
|
||||
<string name="error_no_document">Kein Dokument erkannt</string>
|
||||
<string name="error_no_pdf_app">Keine App zum Öffnen von PDF gefunden</string>
|
||||
<string name="error_save">PDF konnte nicht gespeichert werden</string>
|
||||
<string name="export_pdf">PDF exportieren</string>
|
||||
<string name="filename">Dateiname</string>
|
||||
<string name="file_size">Dateigröße: %1$s</string>
|
||||
<string name="grant_permission">Berechtigung erteilen</string>
|
||||
<string name="last_saved_documents">Zuletzt gespeicherte Dokumente</string>
|
||||
<string name="libraries">Bibliotheken</string>
|
||||
@@ -28,7 +31,7 @@
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="licensed_under">Diese Anwendung ist unter der GNU General Public License v3.0 lizenziert.</string>
|
||||
<string name="new_document">Neues Dokument</string>
|
||||
<string name="new_document_warning">Das aktuelle Dokument geht verloren, wenn Sie es nicht gespeichert haben. Möchten Sie fortfahren?</string>
|
||||
<string name="new_document_warning">Das aktuelle Dokument geht verloren. Möchten Sie fortfahren?</string>
|
||||
<string name="open">Öffnen</string>
|
||||
<string name="open_pdf">PDF öffnen</string>
|
||||
<string name="pdf_saved_to">PDF gespeichert unter %1$s</string>
|
||||
|
||||
@@ -9,16 +9,19 @@
|
||||
<string name="clear_text">Effacer le text</string>
|
||||
<string name="close">Fermer</string>
|
||||
<string name="close_document">Fermer le document</string>
|
||||
<string name="creating_pdf">Création du PDF…</string>
|
||||
<string name="current_document">Document en cours</string>
|
||||
<string name="delete_page">Supprimer la page</string>
|
||||
<string name="delete_page_warning">Voulez-vous supprimer cette page ?</string>
|
||||
<string name="document">Document</string>
|
||||
<string name="end_scan">Terminer le scan</string>
|
||||
<string name="error">Erreur : %1$s</string>
|
||||
<string name="error_no_document">Aucun document détecté</string>
|
||||
<string name="error_no_pdf_app">Aucune application trouvée pour ouvrir un PDF</string>
|
||||
<string name="error_save">Échec de l\'enregistrement du PDF</string>
|
||||
<string name="export_pdf">Exporter en PDF</string>
|
||||
<string name="filename">Nom de fichier</string>
|
||||
<string name="file_size">Taille du fichier : %1$s</string>
|
||||
<string name="grant_permission">Autoriser</string>
|
||||
<string name="last_saved_documents">Derniers documents enregistrés</string>
|
||||
<string name="libraries">Bibliothèques</string>
|
||||
@@ -27,7 +30,7 @@
|
||||
<string name="license">Licence</string>
|
||||
<string name="licensed_under">Cette application est distribuée sous licence GNU General Public License v3.0.</string>
|
||||
<string name="new_document">Nouveau document</string>
|
||||
<string name="new_document_warning">Le document en cours sera perdu si vous ne l\'avez pas enregistré. Voulez-vous continuer ?</string>
|
||||
<string name="new_document_warning">Le scan en cours sera perdu. Voulez-vous continuer ?</string>
|
||||
<string name="open">Ouvrir</string>
|
||||
<string name="open_pdf">Ouvrir le PDF</string>
|
||||
<string name="pdf_saved_to">PDF enregistré dans %1$s</string>
|
||||
|
||||
@@ -10,16 +10,19 @@
|
||||
<string name="clear_text">Clear text</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="close_document">Close document</string>
|
||||
<string name="creating_pdf">Creating PDF…</string>
|
||||
<string name="current_document">Current document</string>
|
||||
<string name="delete_page">Delete page</string>
|
||||
<string name="delete_page_warning">Do you want to delete this page?</string>
|
||||
<string name="document">Document</string>
|
||||
<string name="end_scan">End scan</string>
|
||||
<string name="error">Error: %1$s</string>
|
||||
<string name="error_no_document">No document detected</string>
|
||||
<string name="error_no_pdf_app">No app found to open PDF</string>
|
||||
<string name="error_save">Failed to save PDF</string>
|
||||
<string name="export_pdf">Export PDF</string>
|
||||
<string name="filename">Filename</string>
|
||||
<string name="file_size">File size: %1$s</string>
|
||||
<string name="grant_permission">Grant permission</string>
|
||||
<string name="last_saved_documents">Last saved documents</string>
|
||||
<string name="libraries">Libraries</string>
|
||||
@@ -28,7 +31,7 @@
|
||||
<string name="license">License</string>
|
||||
<string name="licensed_under">This application is licensed under the GNU General Public License v3.0.</string>
|
||||
<string name="new_document">New document</string>
|
||||
<string name="new_document_warning">The current document will be lost if you haven\'t saved it. Do you want to continue?</string>
|
||||
<string name="new_document_warning">The current scan will be lost. Do you want to continue?</string>
|
||||
<string name="open">Open</string>
|
||||
<string name="open_pdf">Open PDF</string>
|
||||
<string name="pdf_saved_to">PDF saved to %1$s</string>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user