Allow export to JPEG (#68)

* SettingScreen: export format

* Allow export to JPEG

* Adjust messages in UI to work with PDF and JPEG

* Message for file size should depend on number of files

* Fix call to MediaScanner to avoid crash when scanning multiple files

* Fix strange handling of Open button
This commit is contained in:
pynicolas
2025-11-30 16:55:36 +01:00
committed by GitHub
parent 7fbda5339a
commit 4453eb1be0
21 changed files with 508 additions and 299 deletions

View File

@@ -23,7 +23,7 @@ import androidx.lifecycle.viewmodel.CreationExtras
import org.fairscan.app.data.FileLogger import org.fairscan.app.data.FileLogger
import org.fairscan.app.data.ImageRepository import org.fairscan.app.data.ImageRepository
import org.fairscan.app.data.LogRepository import org.fairscan.app.data.LogRepository
import org.fairscan.app.data.PdfFileManager import org.fairscan.app.data.FileManager
import org.fairscan.app.data.recentDocumentsDataStore import org.fairscan.app.data.recentDocumentsDataStore
import org.fairscan.app.domain.ImageSegmentationService import org.fairscan.app.domain.ImageSegmentationService
import org.fairscan.app.platform.AndroidPdfWriter import org.fairscan.app.platform.AndroidPdfWriter
@@ -51,8 +51,9 @@ class AppContainer(context: Context) {
private val density = context.resources.displayMetrics.density private val density = context.resources.displayMetrics.density
private val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt() private val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt()
val imageRepository = ImageRepository(context.filesDir, OpenCvTransformations(), thumbnailSizePx) val imageRepository = ImageRepository(context.filesDir, OpenCvTransformations(), thumbnailSizePx)
val pdfFileManager = PdfFileManager( val preparationDir = File(context.cacheDir, "pdfs")
File(context.cacheDir, "pdfs"), val fileManager = FileManager(
preparationDir,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
AndroidPdfWriter() AndroidPdfWriter()
) )

View File

@@ -45,12 +45,10 @@ import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat.checkSelfPermission import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.fairscan.app.data.GeneratedPdf
import org.fairscan.app.ui.Navigation import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.Screen import org.fairscan.app.ui.Screen
import org.fairscan.app.ui.components.rememberCameraPermissionState import org.fairscan.app.ui.components.rememberCameraPermissionState
@@ -63,18 +61,18 @@ import org.fairscan.app.ui.screens.camera.CameraEvent
import org.fairscan.app.ui.screens.camera.CameraScreen import org.fairscan.app.ui.screens.camera.CameraScreen
import org.fairscan.app.ui.screens.camera.CameraViewModel import org.fairscan.app.ui.screens.camera.CameraViewModel
import org.fairscan.app.ui.screens.export.ExportEvent import org.fairscan.app.ui.screens.export.ExportEvent
import org.fairscan.app.ui.screens.export.ExportResult
import org.fairscan.app.ui.screens.export.ExportScreenWrapper import org.fairscan.app.ui.screens.export.ExportScreenWrapper
import org.fairscan.app.ui.screens.export.ExportViewModel import org.fairscan.app.ui.screens.export.ExportViewModel
import org.fairscan.app.ui.screens.export.PdfGenerationActions import org.fairscan.app.ui.screens.export.ExportActions
import org.fairscan.app.ui.screens.home.HomeScreen import org.fairscan.app.ui.screens.home.HomeScreen
import org.fairscan.app.ui.screens.home.HomeViewModel import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.screens.settings.ExportFormat
import org.fairscan.app.ui.screens.settings.SettingsScreen import org.fairscan.app.ui.screens.settings.SettingsScreen
import org.fairscan.app.ui.screens.settings.SettingsViewModel import org.fairscan.app.ui.screens.settings.SettingsViewModel
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
import org.opencv.android.OpenCVLoader import org.opencv.android.OpenCVLoader
private const val PDF_MIME_TYPE = "application/pdf"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -89,7 +87,7 @@ class MainActivity : ComponentActivity() {
val settingsViewModel: SettingsViewModel val settingsViewModel: SettingsViewModel
by viewModels { appContainer.settingsViewModelFactory } by viewModels { appContainer.settingsViewModelFactory }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
exportViewModel.cleanUpOldPdfs(1000 * 3600) exportViewModel.cleanUpOldPreparedFiles(1000 * 3600)
} }
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@@ -97,6 +95,7 @@ class MainActivity : ComponentActivity() {
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle() val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle() val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
val document by viewModel.documentUiModel.collectAsStateWithLifecycle() val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
val exportUiState by exportViewModel.uiState.collectAsStateWithLifecycle()
val cameraPermission = rememberCameraPermissionState() val cameraPermission = rememberCameraPermissionState()
CollectCameraEvents(cameraViewModel, viewModel) CollectCameraEvents(cameraViewModel, viewModel)
CollectExportEvents(context, exportViewModel) CollectExportEvents(context, exportViewModel)
@@ -113,7 +112,7 @@ class MainActivity : ComponentActivity() {
navigation = navigation, navigation = navigation,
onClearScan = { viewModel.startNewDocument() }, onClearScan = { viewModel.startNewDocument() },
recentDocuments = recentDocs, recentDocuments = recentDocs,
onOpenPdf = { fileUri -> openPdf(fileUri) } onOpenPdf = { fileUri -> openUri(fileUri, ExportFormat.PDF.mimeType) }
) )
} }
is Screen.Main.Camera -> { is Screen.Main.Camera -> {
@@ -140,13 +139,13 @@ class MainActivity : ComponentActivity() {
is Screen.Main.Export -> { is Screen.Main.Export -> {
ExportScreenWrapper( ExportScreenWrapper(
navigation = navigation, navigation = navigation,
pdfActions = PdfGenerationActions( uiState = exportUiState,
startGeneration = exportViewModel::startPdfGeneration, pdfActions = ExportActions(
initializeExportScreen = exportViewModel::initializeExportScreen,
setFilename = exportViewModel::setFilename, setFilename = exportViewModel::setFilename,
uiStateFlow = exportViewModel.pdfUiState, share = { share(exportViewModel.applyRenaming(), exportViewModel) },
sharePdf = { sharePdf(exportViewModel.getFinalPdf(), exportViewModel) }, save = { exportViewModel.onSaveClicked() },
savePdf = { exportViewModel.onSavePdfClicked() }, open = { item -> openUri(item.uri, item.format.mimeType) }
openPdf = { openPdf(exportViewModel.pdfUiState.value.savedFileUri) }
), ),
onCloseScan = { onCloseScan = {
viewModel.startNewDocument() viewModel.startNewDocument()
@@ -188,7 +187,8 @@ class MainActivity : ComponentActivity() {
settingsUiState, settingsUiState,
onChooseDirectoryClick = { launcher.launch(null) }, onChooseDirectoryClick = { launcher.launch(null) },
onResetExportDirClick = { settingsViewModel.setExportDirUri(null) }, onResetExportDirClick = { settingsViewModel.setExportDirUri(null) },
onBack = nav.back onExportFormatChanged = { format -> settingsViewModel.setExportFormat(format) },
onBack = nav.back,
) )
} }
@@ -222,7 +222,7 @@ class MainActivity : ComponentActivity() {
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted -> ) { isGranted ->
if (isGranted) { if (isGranted) {
exportViewModel.onSavePdfClicked() exportViewModel.onSaveClicked()
} else { } else {
val message = getString(R.string.storage_permission_denied) val message = getString(R.string.storage_permission_denied)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
@@ -231,9 +231,9 @@ class MainActivity : ComponentActivity() {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
exportViewModel.events.collect { event -> exportViewModel.events.collect { event ->
when (event) { when (event) {
ExportEvent.RequestSavePdf -> { ExportEvent.RequestSave -> {
checkPermissionThen(storagePermissionLauncher) { checkPermissionThen(storagePermissionLauncher) {
exportViewModel.onRequestPdfSave(context) exportViewModel.onRequestSave(context)
} }
} }
@@ -260,25 +260,36 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun sharePdf(generatedPdf: GeneratedPdf?, viewModel: ExportViewModel) { private fun share(result: ExportResult?, viewModel: ExportViewModel) {
if (generatedPdf == null) if (result == null || result.files.isEmpty()) return
return
viewModel.setPdfAsShared() viewModel.setAsShared()
val file = generatedPdf.file
val authority = "${applicationContext.packageName}.fileprovider" val authority = "${applicationContext.packageName}.fileprovider"
val fileUri = FileProvider.getUriForFile(this, authority, file) val uris = result.files.map { file ->
val shareIntent = Intent(Intent.ACTION_SEND).apply { FileProvider.getUriForFile(this, authority, file)
type = PDF_MIME_TYPE }
putExtra(Intent.EXTRA_STREAM, fileUri) val intent = Intent().apply {
action = if (uris.size == 1) Intent.ACTION_SEND else Intent.ACTION_SEND_MULTIPLE
type = result.format.mimeType
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (uris.size == 1) {
putExtra(Intent.EXTRA_STREAM, uris[0])
} else {
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
}
}
val chooser = Intent.createChooser(intent, getString(R.string.share_document))
val resolveInfos = packageManager.queryIntentActivities(chooser, PackageManager.MATCH_DEFAULT_ONLY)
for (info in resolveInfos) {
val pkg = info.activityInfo.packageName
for (uri in uris) {
grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
} }
val chooser = Intent.createChooser(shareIntent, getString(R.string.share_pdf))
val resInfoList = packageManager.queryIntentActivities(chooser, PackageManager.MATCH_DEFAULT_ONLY)
for (resInfo in resInfoList) {
val packageName = resInfo.activityInfo.packageName
grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(chooser) startActivity(chooser)
} }
@@ -295,7 +306,7 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun openPdf(fileUri: Uri?) { private fun openUri(fileUri: Uri?, mimeType: String) {
if (fileUri == null) return if (fileUri == null) return
val uriToOpen: Uri = val uriToOpen: Uri =
if (fileUri.scheme == ContentResolver.SCHEME_CONTENT) { if (fileUri.scheme == ContentResolver.SCHEME_CONTENT) {
@@ -305,13 +316,13 @@ class MainActivity : ComponentActivity() {
FileProvider.getUriForFile(this, authority, fileUri.toFile()) FileProvider.getUriForFile(this, authority, fileUri.toFile())
} }
val openIntent = Intent(Intent.ACTION_VIEW).apply { val openIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uriToOpen, PDF_MIME_TYPE) setDataAndType(uriToOpen, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
try { try {
startActivity(Intent.createChooser(openIntent, getString(R.string.open_pdf))) startActivity(Intent.createChooser(openIntent, getString(R.string.open_file)))
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
Toast.makeText(this, getString(R.string.error_no_pdf_app), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.error_no_app), Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -28,13 +28,13 @@ fun interface PdfWriter {
fun writePdfFromJpegs(jpegs: Sequence<ByteArray>, outputStream: OutputStream): Int fun writePdfFromJpegs(jpegs: Sequence<ByteArray>, outputStream: OutputStream): Int
} }
class PdfFileManager( class FileManager(
private val pdfDir: File, private val pdfDir: File,
private val externalDir: File, private val externalDir: File,
private val pdfWriter: PdfWriter private val pdfWriter: PdfWriter
) { ) {
companion object { companion object {
fun addExtensionIfMissing(fileName: String): String { fun addPdfExtensionIfMissing(fileName: String): String {
return if (fileName.lowercase().endsWith(".pdf")) return if (fileName.lowercase().endsWith(".pdf"))
fileName fileName
else else

View File

@@ -111,11 +111,12 @@ class ImageRepository(
} }
fun getContent(id: String): ByteArray? { fun getContent(id: String): ByteArray? {
return getFileFor(id)?.readBytes()
}
fun getFileFor(id: String): File? {
val file = File(scanDir, id) val file = File(scanDir, id)
if (file.exists()) { return if (file.exists()) file else null
return file.readBytes()
}
return null
} }
fun getThumbnail(id: String): ByteArray? { fun getThumbnail(id: String): ByteArray? {

View File

@@ -32,7 +32,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.RotateLeft import androidx.compose.material.icons.automirrored.filled.RotateLeft
import androidx.compose.material.icons.automirrored.filled.RotateRight import androidx.compose.material.icons.automirrored.filled.RotateRight
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.PictureAsPdf import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -55,8 +55,8 @@ import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import net.engawapg.lib.zoomable.ZoomState import net.engawapg.lib.zoomable.ZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
import org.fairscan.app.ui.Navigation
import org.fairscan.app.R import org.fairscan.app.R
import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.components.CommonPageListState import org.fairscan.app.ui.components.CommonPageListState
import org.fairscan.app.ui.components.ConfirmationDialog import org.fairscan.app.ui.components.ConfirmationDialog
import org.fairscan.app.ui.components.MainActionButton import org.fairscan.app.ui.components.MainActionButton
@@ -225,8 +225,8 @@ private fun BottomBar(
) { ) {
MainActionButton( MainActionButton(
onClick = navigation.toExportScreen, onClick = navigation.toExportScreen,
icon = Icons.Default.PictureAsPdf, icon = Icons.Default.Description,
text = stringResource(R.string.export_pdf), text = stringResource(R.string.export),
) )
} }
} }

View File

@@ -48,7 +48,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -61,13 +60,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import org.fairscan.app.R import org.fairscan.app.R
import org.fairscan.app.data.GeneratedPdf
import org.fairscan.app.ui.Navigation import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.components.AppOverflowMenu import org.fairscan.app.ui.components.AppOverflowMenu
import org.fairscan.app.ui.components.BackButton import org.fairscan.app.ui.components.BackButton
@@ -76,6 +75,8 @@ import org.fairscan.app.ui.components.NewDocumentDialog
import org.fairscan.app.ui.components.isLandscape import org.fairscan.app.ui.components.isLandscape
import org.fairscan.app.ui.components.pageCountText import org.fairscan.app.ui.components.pageCountText
import org.fairscan.app.ui.dummyNavigation import org.fairscan.app.ui.dummyNavigation
import org.fairscan.app.ui.screens.settings.ExportFormat
import org.fairscan.app.ui.screens.settings.ExportFormat.PDF
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -85,17 +86,17 @@ import java.util.Locale
@Composable @Composable
fun ExportScreenWrapper( fun ExportScreenWrapper(
navigation: Navigation, navigation: Navigation,
pdfActions: PdfGenerationActions, uiState: ExportUiState,
pdfActions: ExportActions,
onCloseScan: () -> Unit, onCloseScan: () -> Unit,
) { ) {
BackHandler { navigation.back() } BackHandler { navigation.back() }
val showConfirmationDialog = rememberSaveable { mutableStateOf(false) } val showConfirmationDialog = rememberSaveable { mutableStateOf(false) }
val filename = remember { mutableStateOf(defaultFilename()) } val filename = remember { mutableStateOf(defaultFilename()) }
val uiState by pdfActions.uiStateFlow.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
pdfActions.setFilename(filename.value) pdfActions.setFilename(filename.value)
pdfActions.startGeneration() pdfActions.initializeExportScreen()
} }
val onFilenameChange = { newName:String -> val onFilenameChange = { newName:String ->
@@ -116,15 +117,15 @@ fun ExportScreenWrapper(
navigation = navigation, navigation = navigation,
onShare = { onShare = {
ensureCorrectFileName() ensureCorrectFileName()
pdfActions.sharePdf() pdfActions.share()
}, },
onSave = { onSave = {
ensureCorrectFileName() ensureCorrectFileName()
pdfActions.savePdf() pdfActions.save()
}, },
onOpen = { pdfActions.openPdf() }, onOpen = pdfActions.open,
onCloseScan = { onCloseScan = {
if (uiState.hasSavedOrSharedPdf) if (uiState.hasSavedOrShared)
onCloseScan() onCloseScan()
else else
showConfirmationDialog.value = true showConfirmationDialog.value = true
@@ -141,17 +142,17 @@ fun ExportScreenWrapper(
fun ExportScreen( fun ExportScreen(
filename: MutableState<String>, filename: MutableState<String>,
onFilenameChange: (String) -> Unit, onFilenameChange: (String) -> Unit,
uiState: PdfGenerationUiState, uiState: ExportUiState,
navigation: Navigation, navigation: Navigation,
onShare: () -> Unit, onShare: () -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
onOpen: () -> Unit, onOpen: (SavedItem) -> Unit,
onCloseScan: () -> Unit, onCloseScan: () -> Unit,
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.export_pdf)) }, title = { Text(stringResource(R.string.export_as, uiState.format)) },
navigationIcon = { BackButton(navigation.back) }, navigationIcon = { BackButton(navigation.back) },
actions = { actions = {
AppOverflowMenu(navigation) AppOverflowMenu(navigation)
@@ -195,12 +196,12 @@ fun ExportScreen(
private fun TextFieldAndPdfInfos( private fun TextFieldAndPdfInfos(
filename: MutableState<String>, filename: MutableState<String>,
onFilenameChange: (String) -> Unit, onFilenameChange: (String) -> Unit,
uiState: PdfGenerationUiState, uiState: ExportUiState,
onOpen: () -> Unit, onOpen: (SavedItem) -> Unit,
) { ) {
FilenameTextField(filename, onFilenameChange) FilenameTextField(filename, onFilenameChange)
val pdf = uiState.generatedPdf val result = uiState.result
// PDF infos // PDF infos
Column( Column(
@@ -208,20 +209,22 @@ private fun TextFieldAndPdfInfos(
) { ) {
if (uiState.isGenerating) { if (uiState.isGenerating) {
Text(stringResource(R.string.creating_pdf), fontStyle = FontStyle.Italic) Text(stringResource(R.string.creating_export), fontStyle = FontStyle.Italic)
} else if (pdf != null) { } else if (result != null) {
val context = LocalContext.current val context = LocalContext.current
val formattedFileSize = formatFileSize(pdf.sizeInBytes, context) val formattedFileSize = formatFileSize(result.sizeInBytes, context)
Text(text = pageCountText(pdf.pageCount)) Text(text = pageCountText(result.pageCount))
val sizeMessageKey =
if (result.files.size == 1) R.string.file_size else R.string.file_size_total
Text( Text(
text = stringResource(R.string.file_size, formattedFileSize), text = stringResource(sizeMessageKey, formattedFileSize),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
) )
} }
} }
if (uiState.savedFileUri != null) { if (uiState.savedBundle != null) {
SavedPdfBar(uiState, onOpen) SaveInfoBar(uiState.savedBundle, onOpen)
} }
if (uiState.errorMessage != null) { if (uiState.errorMessage != null) {
ErrorBar(uiState.errorMessage) ErrorBar(uiState.errorMessage)
@@ -257,7 +260,7 @@ private fun FilenameTextField(
@Composable @Composable
private fun MainActions( private fun MainActions(
uiState: PdfGenerationUiState, uiState: ExportUiState,
onShare: () -> Unit, onShare: () -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
onCloseScan: () -> Unit, onCloseScan: () -> Unit,
@@ -271,16 +274,16 @@ private fun MainActions(
) { ) {
ExportButton( ExportButton(
onClick = onShare, onClick = onShare,
enabled = uiState.generatedPdf != null, enabled = uiState.result != null,
isPrimary = !uiState.hasSavedOrSharedPdf, isPrimary = !uiState.hasSavedOrShared,
icon = Icons.Default.Share, icon = Icons.Default.Share,
text = stringResource(R.string.share), text = stringResource(R.string.share),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
ExportButton( ExportButton(
onClick = onSave, onClick = onSave,
enabled = uiState.generatedPdf != null, enabled = uiState.result != null,
isPrimary = !uiState.hasSavedOrSharedPdf, isPrimary = !uiState.hasSavedOrShared,
icon = Icons.Default.Download, icon = Icons.Default.Download,
text = stringResource(R.string.save), text = stringResource(R.string.save),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@@ -291,7 +294,7 @@ private fun MainActions(
text = stringResource(R.string.end_scan), text = stringResource(R.string.end_scan),
onClick = onCloseScan, onClick = onCloseScan,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
isPrimary = uiState.hasSavedOrSharedPdf, isPrimary = uiState.hasSavedOrShared,
) )
} }
} }
@@ -335,8 +338,8 @@ fun ExportButton(
} }
@Composable @Composable
private fun SavedPdfBar(uiState: PdfGenerationUiState, onOpen: () -> Unit) { private fun SaveInfoBar(savedBundle: SavedBundle, onOpen: (SavedItem) -> Unit) {
val dirName = uiState.exportDirName?:stringResource(R.string.download_dirname) val dirName = savedBundle.exportDirName?:stringResource(R.string.download_dirname)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Absolute.SpaceBetween, horizontalArrangement = Arrangement.Absolute.SpaceBetween,
@@ -345,17 +348,26 @@ private fun SavedPdfBar(uiState: PdfGenerationUiState, onOpen: () -> Unit) {
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
.padding(vertical = 8.dp, horizontal = 16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
) { ) {
val items = savedBundle.items
val nbFiles = items.size
val firstFileName = items[0].fileName
Text( Text(
text = stringResource(R.string.pdf_saved_to, dirName), text = LocalResources.current.getQuantityString(
R.plurals.files_saved_to,
nbFiles,
nbFiles, firstFileName, dirName
),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
Spacer(Modifier.width(8.dp)) if (nbFiles == 1) {
MainActionButton( Spacer(Modifier.width(8.dp))
onClick = onOpen, MainActionButton(
text = stringResource(R.string.open), onClick = { onOpen(items[0]) },
icon = Icons.AutoMirrored.Filled.OpenInNew, text = stringResource(R.string.open),
) icon = Icons.AutoMirrored.Filled.OpenInNew,
)
}
} }
} }
@@ -386,7 +398,7 @@ fun formatFileSize(sizeInBytes: Long?, context: Context): String {
@Composable @Composable
fun PreviewExportScreenDuringGeneration() { fun PreviewExportScreenDuringGeneration() {
ExportPreviewToCustomize( ExportPreviewToCustomize(
uiState = PdfGenerationUiState(isGenerating = true) uiState = ExportUiState(isGenerating = true)
) )
} }
@@ -395,8 +407,8 @@ fun PreviewExportScreenDuringGeneration() {
fun PreviewExportScreenAfterGeneration() { fun PreviewExportScreenAfterGeneration() {
val file = File("fake.pdf") val file = File("fake.pdf")
ExportPreviewToCustomize( ExportPreviewToCustomize(
uiState = PdfGenerationUiState( uiState = ExportUiState(
generatedPdf = GeneratedPdf(file, 442897L, 3), result = ExportResult.Pdf(file, 442897L, 3),
), ),
) )
} }
@@ -406,9 +418,11 @@ fun PreviewExportScreenAfterGeneration() {
fun PreviewExportScreenAfterSave() { fun PreviewExportScreenAfterSave() {
val file = File("fake.pdf") val file = File("fake.pdf")
ExportPreviewToCustomize( ExportPreviewToCustomize(
uiState = PdfGenerationUiState( uiState = ExportUiState(
generatedPdf = GeneratedPdf(file, 442897L, 3), result = ExportResult.Pdf(file, 442897L, 3),
savedFileUri = file.toUri(), savedBundle = SavedBundle(
listOf(SavedItem(file.toUri(), defaultFilename() + ".pdf", PDF))
),
), ),
) )
} }
@@ -417,7 +431,7 @@ fun PreviewExportScreenAfterSave() {
@Composable @Composable
fun ExportScreenPreviewWithError() { fun ExportScreenPreviewWithError() {
ExportPreviewToCustomize( ExportPreviewToCustomize(
PdfGenerationUiState(errorMessage = "PDF generation failed") ExportUiState(errorMessage = "PDF generation failed")
) )
} }
@@ -426,16 +440,17 @@ fun ExportScreenPreviewWithError() {
fun PreviewExportScreenAfterSaveHorizontal() { fun PreviewExportScreenAfterSaveHorizontal() {
val file = File("fake.pdf") val file = File("fake.pdf")
ExportPreviewToCustomize( ExportPreviewToCustomize(
uiState = PdfGenerationUiState( uiState = ExportUiState(
generatedPdf = GeneratedPdf(file, 442897L, 3), result = ExportResult.Pdf(file, 442897L, 3),
savedFileUri = file.toUri(), savedBundle = SavedBundle(
exportDirName = "MyVeryVeryLongDirectoryName" listOf(SavedItem(file.toUri(), "my_file.pdf", PDF)),
exportDirName="MyVeryVeryLongDirectoryName"),
), ),
) )
} }
@Composable @Composable
fun ExportPreviewToCustomize(uiState: PdfGenerationUiState) { fun ExportPreviewToCustomize(uiState: ExportUiState) {
FairScanTheme { FairScanTheme {
ExportScreen( ExportScreen(
filename = remember { mutableStateOf("Scan 2025-07-02 17.40.42") }, filename = remember { mutableStateOf("Scan 2025-07-02 17.40.42") },

View File

@@ -15,16 +15,28 @@
package org.fairscan.app.ui.screens.export package org.fairscan.app.ui.screens.export
import android.net.Uri import android.net.Uri
import org.fairscan.app.data.GeneratedPdf import org.fairscan.app.ui.screens.settings.ExportFormat
data class PdfGenerationUiState( data class ExportUiState(
val format: ExportFormat = ExportFormat.PDF,
val isGenerating: Boolean = false, val isGenerating: Boolean = false,
val generatedPdf: GeneratedPdf? = null, val result: ExportResult? = null,
val desiredFilename: String = "", val desiredFilename: String = "",
val savedFileUri: Uri? = null, val savedBundle: SavedBundle? = null,
val exportDirName: String? = null, val hasShared: Boolean = false,
val hasSharedPdf: Boolean = false,
val errorMessage: String? = null, val errorMessage: String? = null,
) { ) {
val hasSavedOrSharedPdf get() = savedFileUri != null || hasSharedPdf val hasSavedOrShared get() = savedBundle != null || hasShared
} }
data class SavedItem(
val uri: Uri,
val fileName: String,
val format: ExportFormat,
)
data class SavedBundle(
val items: List<SavedItem>,
val exportDir: Uri? = null,
val exportDirName: String? = null,
)

View File

@@ -22,7 +22,6 @@ import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,26 +31,25 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.fairscan.app.AppContainer import org.fairscan.app.AppContainer
import org.fairscan.app.RecentDocument import org.fairscan.app.RecentDocument
import org.fairscan.app.data.GeneratedPdf import org.fairscan.app.data.FileManager
import org.fairscan.app.data.PdfFileManager import org.fairscan.app.ui.screens.settings.ExportFormat
import org.fairscan.app.ui.screens.home.HomeViewModel
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import kotlin.coroutines.resume
private const val PDF_MIME_TYPE = "application/pdf" import kotlin.coroutines.suspendCoroutine
sealed interface ExportEvent { sealed interface ExportEvent {
data object RequestSavePdf : ExportEvent data object RequestSave : ExportEvent
data object SaveError : ExportEvent data object SaveError : ExportEvent
} }
class ExportViewModel(container: AppContainer): ViewModel() { class ExportViewModel(container: AppContainer): ViewModel() {
private val pdfFileManager = container.pdfFileManager private val preparationDir = container.preparationDir
private val fileManager = container.fileManager
private val imageRepository = container.imageRepository private val imageRepository = container.imageRepository
private val settingsRepository = container.settingsRepository private val settingsRepository = container.settingsRepository
private val recentDocumentsDataStore = container.recentDocumentsDataStore private val recentDocumentsDataStore = container.recentDocumentsDataStore
@@ -60,134 +58,166 @@ class ExportViewModel(container: AppContainer): ViewModel() {
private val _events = MutableSharedFlow<ExportEvent>() private val _events = MutableSharedFlow<ExportEvent>()
val events = _events.asSharedFlow() val events = _events.asSharedFlow()
private suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) { private suspend fun generatePdf(): ExportResult.Pdf = withContext(Dispatchers.IO) {
val imageIds = imageRepository.imageIds() val imageIds = imageRepository.imageIds()
val jpegs = imageIds.asSequence() val jpegs = imageIds.asSequence()
.map { id -> imageRepository.getContent(id) } .mapNotNull { id -> imageRepository.getContent(id) }
.filterNotNull() val pdf = fileManager.generatePdf(jpegs)
return@withContext pdfFileManager.generatePdf(jpegs) return@withContext ExportResult.Pdf(pdf.file, pdf.sizeInBytes, pdf.pageCount)
} }
private val _pdfUiState = MutableStateFlow(PdfGenerationUiState()) private val _uiState = MutableStateFlow(ExportUiState())
val pdfUiState: StateFlow<PdfGenerationUiState> = _pdfUiState.asStateFlow() val uiState: StateFlow<ExportUiState> = _uiState.asStateFlow()
private var generationJob: Job? = null private var preparationJob: Job? = null
private var desiredFilename: String = "" private var desiredFilename: String = ""
private var exportFormat = ExportFormat.PDF
fun setFilename(name: String) { fun setFilename(name: String) {
desiredFilename = name desiredFilename = name
} }
fun startPdfGeneration() { fun initializeExportScreen() {
cancelPdfGeneration() cancelPreparation()
generationJob = viewModelScope.launch {
preparationJob = viewModelScope.launch {
exportFormat = settingsRepository.exportFormat.first()
_uiState.update { it.copy(format = exportFormat) }
try { try {
val result = generatePdf() val result = if (exportFormat == ExportFormat.JPEG) {
_pdfUiState.update { val jpegFiles = imageRepository.imageIds()
it.copy( .mapNotNull { id -> imageRepository.getFileFor(id) }
isGenerating = false, .map { f -> f.copyTo(File(preparationDir, f.name), overwrite = true) }
generatedPdf = result val sizeInBytes = jpegFiles.sumOf { it.length() }
) ExportResult.Jpeg(jpegFiles, sizeInBytes)
} else {
generatePdf()
}
_uiState.update {
it.copy(isGenerating = false, result = result)
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.e("FairScan", "PDF generation failed", e) val message = "Failed to prepare $exportFormat export"
_pdfUiState.update { logger.e("FairScan", message, e)
_uiState.update {
it.copy( it.copy(
isGenerating = false, isGenerating = false,
errorMessage = "PDF generation failed" errorMessage = message
) )
} }
} }
} }
} }
fun cancelPdfGeneration() { fun cancelPreparation() {
generationJob?.cancel() preparationJob?.cancel()
_pdfUiState.value = PdfGenerationUiState() _uiState.value = ExportUiState()
} }
fun setPdfAsShared() { fun setAsShared() {
_pdfUiState.update { it.copy(hasSharedPdf = true) } _uiState.update { it.copy(hasShared = true) }
} }
fun getFinalPdf(): GeneratedPdf? { fun applyRenaming(): ExportResult? {
val tempPdf = _pdfUiState.value.generatedPdf ?: return null val result = _uiState.value.result ?: return null
val tempFile = tempPdf.file when (result) {
val fileName = PdfFileManager.addExtensionIfMissing(desiredFilename) is ExportResult.Pdf -> {
val newFile = File(tempFile.parentFile, fileName) val fileName = FileManager.addPdfExtensionIfMissing(desiredFilename)
if (tempFile.absolutePath != newFile.absolutePath) { val newFile = File(result.file.parentFile, fileName)
if (newFile.exists()) newFile.delete() val tempFile = result.file
val success = tempFile.renameTo(newFile) if (tempFile.absolutePath != newFile.absolutePath) {
if (!success) return null if (newFile.exists()) newFile.delete()
_pdfUiState.update { val success = tempFile.renameTo(newFile)
it.copy(generatedPdf = GeneratedPdf( if (!success) return null
newFile, tempPdf.sizeInBytes, tempPdf.pageCount) _uiState.update {
) it.copy(result = ExportResult.Pdf(
newFile, result.sizeInBytes, result.pageCount)
)
}
}
}
is ExportResult.Jpeg -> {
val base = desiredFilename.removeSuffix(".jpg")
val renamedFiles = result.files.mapIndexed { index, file ->
val newFile = File(file.parentFile, "${base}_${index + 1}.jpg")
if (newFile.exists()) newFile.delete()
file.renameTo(newFile)
newFile
}
val updated = result.copy(jpegFiles = renamedFiles)
_uiState.update { it.copy(result = updated) }
} }
} }
return _pdfUiState.value.generatedPdf return _uiState.value.result
} }
fun onSavePdfClicked() { fun onSaveClicked() {
viewModelScope.launch { viewModelScope.launch {
_events.emit(ExportEvent.RequestSavePdf) _events.emit(ExportEvent.RequestSave)
} }
} }
fun onRequestPdfSave(context: Context) { fun onRequestSave(context: Context) {
viewModelScope.launch { viewModelScope.launch {
performPdfSave(context) try {
save(context)
} catch (e: Exception) {
logger.e("FairScan", "Failed to save PDF", e)
_events.emit(ExportEvent.SaveError)
}
} }
} }
private suspend fun performPdfSave(context: Context) { private suspend fun save(context:Context) {
try { val result = applyRenaming() ?: return
val pdf = getFinalPdf() ?: return val exportDir = settingsRepository.exportDirUri.first()?.toUri()
val savedItems = mutableListOf<SavedItem>()
val filesForMediaScan = mutableListOf<File>()
val exportDir = settingsRepository.exportDirUri.first() for (file in result.files) {
var fileInDownloads: File? = null val saved = if (exportDir == null) {
val out = fileManager.copyToExternalDir(file)
var savedName: String filesForMediaScan.add(out)
val savedUri: Uri SavedItem(out.toUri(), out.name, exportFormat)
if (exportDir == null) {
fileInDownloads = pdfFileManager.copyToExternalDir(pdf.file)
savedUri = fileInDownloads.toUri()
savedName = fileInDownloads.name
} else { } else {
val saved = copyViaSaf(context, pdf.file, exportDir.toUri()) val safFile = copyViaSaf(context, file, exportDir, exportFormat)
savedUri = saved.uri SavedItem(safFile.uri, safFile.name ?: file.name, exportFormat)
savedName = saved.name?:pdf.file.name
} }
savedItems += saved
_pdfUiState.update {
it.copy(
savedFileUri = savedUri,
exportDirName = resolveExportDirName(context, exportDir?.toUri()))
}
fileInDownloads?.let { mediaScan(context, it) }
addRecentDocument(savedUri, savedName, pdf.pageCount)
} catch (e: Exception) {
logger.e("FairScan", "Failed to save PDF", e)
_events.emit(ExportEvent.SaveError)
} }
val exportDirName = resolveExportDirName(context, exportDir)
val bundle = SavedBundle(savedItems, exportDir, exportDirName)
_uiState.update { it.copy(savedBundle = bundle) }
if (exportFormat == ExportFormat.PDF) {
savedItems.forEach { item ->
addRecentDocument(item.uri, item.fileName, result.pageCount)
}
}
filesForMediaScan.forEach { f -> mediaScan(context, f, exportFormat.mimeType) }
} }
@OptIn(ExperimentalCoroutinesApi::class) private suspend fun mediaScan(
private suspend fun mediaScan(context: Context, file: File) = context: Context,
suspendCancellableCoroutine { continuation -> file: File,
MediaScannerConnection.scanFile( mimeType: String
context, ): Uri? = suspendCoroutine { cont ->
arrayOf(file.absolutePath), MediaScannerConnection.scanFile(
arrayOf(PDF_MIME_TYPE) context,
) { _, _ -> continuation.resume(Unit) {} } arrayOf(file.absolutePath),
arrayOf(mimeType)
) { _, uri ->
cont.resume(uri)
} }
}
private fun copyViaSaf( private fun copyViaSaf(
context: Context, context: Context,
source: File, source: File,
exportDirUri: Uri, exportDirUri: Uri,
exportFormat: ExportFormat,
): DocumentFile { ): DocumentFile {
val resolver = context.contentResolver val resolver = context.contentResolver
@@ -195,7 +225,7 @@ class ExportViewModel(container: AppContainer): ViewModel() {
?: throw IllegalStateException("Invalid SAF directory") ?: throw IllegalStateException("Invalid SAF directory")
// Name collisions are handled automatically by SAF provider // Name collisions are handled automatically by SAF provider
val target = tree.createFile(PDF_MIME_TYPE, source.name) val target = tree.createFile(exportFormat.mimeType, source.name)
?: throw IllegalStateException("Unable to create SAF file") ?: throw IllegalStateException("Unable to create SAF file")
resolver.openOutputStream(target.uri)?.use { output -> resolver.openOutputStream(target.uri)?.use { output ->
@@ -207,8 +237,8 @@ class ExportViewModel(container: AppContainer): ViewModel() {
return target return target
} }
fun cleanUpOldPdfs(thresholdInMillis: Int) { fun cleanUpOldPreparedFiles(thresholdInMillis: Int) {
pdfFileManager.cleanUpOldFiles(thresholdInMillis) fileManager.cleanUpOldFiles(thresholdInMillis)
} }
private fun resolveExportDirName(context: Context, exportDirUri: Uri?): String? { private fun resolveExportDirName(context: Context, exportDirUri: Uri?): String? {
@@ -241,11 +271,35 @@ class ExportViewModel(container: AppContainer): ViewModel() {
} }
} }
data class PdfGenerationActions( sealed class ExportResult {
val startGeneration: () -> Unit, abstract val files: List<File>
abstract val sizeInBytes: Long
abstract val pageCount: Int
abstract val format: ExportFormat
data class Pdf(
val file: File,
override val sizeInBytes: Long,
override val pageCount: Int,
) : ExportResult() {
override val files get() = listOf(file)
override val format: ExportFormat = ExportFormat.PDF
}
data class Jpeg(
val jpegFiles: List<File>,
override val sizeInBytes: Long,
) : ExportResult() {
override val files get() = jpegFiles
override val pageCount get() = jpegFiles.size
override val format: ExportFormat = ExportFormat.JPEG
}
}
data class ExportActions(
val initializeExportScreen: () -> Unit,
val setFilename: (String) -> Unit, val setFilename: (String) -> Unit,
val uiStateFlow: StateFlow<PdfGenerationUiState>,// TODO is it ok to have that here? val share: () -> Unit,
val sharePdf: () -> Unit, val save: () -> Unit,
val savePdf: () -> Unit, val open: (SavedItem) -> Unit,
val openPdf: () -> Unit,
) )

View File

@@ -26,12 +26,22 @@ private val Context.dataStore by preferencesDataStore(name = "fairscan_settings"
class SettingsRepository(private val context: Context) { class SettingsRepository(private val context: Context) {
private val EXPORT_DIR_URI = stringPreferencesKey("export_dir_uri") private val EXPORT_DIR_URI = stringPreferencesKey("export_dir_uri")
private val EXPORT_FORMAT = stringPreferencesKey("export_format")
val exportDirUri: Flow<String?> = val exportDirUri: Flow<String?> =
context.dataStore.data.map { prefs -> context.dataStore.data.map { prefs ->
prefs[EXPORT_DIR_URI] prefs[EXPORT_DIR_URI]
} }
val exportFormat: Flow<ExportFormat> =
context.dataStore.data.map { prefs ->
when (prefs[EXPORT_FORMAT]) {
"JPEG" -> ExportFormat.JPEG
"PDF", null -> ExportFormat.PDF
else -> ExportFormat.PDF
}
}
suspend fun setExportDirUri(uri: String?) { suspend fun setExportDirUri(uri: String?) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
if (uri == null) { if (uri == null) {
@@ -41,4 +51,15 @@ class SettingsRepository(private val context: Context) {
} }
} }
} }
suspend fun setExportFormat(format: ExportFormat) {
context.dataStore.edit { prefs ->
prefs[EXPORT_FORMAT] = format.name
}
}
}
enum class ExportFormat(val mimeType: String) {
PDF("application/pdf"),
JPEG("image/jpeg"),
} }

View File

@@ -27,6 +27,8 @@ 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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Folder
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -34,6 +36,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -56,6 +59,7 @@ fun SettingsScreen(
uiState: SettingsUiState, uiState: SettingsUiState,
onChooseDirectoryClick: () -> Unit, onChooseDirectoryClick: () -> Unit,
onResetExportDirClick: () -> Unit, onResetExportDirClick: () -> Unit,
onExportFormatChanged: (ExportFormat) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
BackHandler { onBack() } BackHandler { onBack() }
@@ -67,10 +71,13 @@ fun SettingsScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
SettingsContent(uiState, onChooseDirectoryClick, onResetExportDirClick, modifier = Modifier.padding(paddingValues)) SettingsContent(
uiState,
onChooseDirectoryClick,
onResetExportDirClick,
onExportFormatChanged,
modifier = Modifier.padding(paddingValues))
} }
} }
@Composable @Composable
@@ -78,6 +85,7 @@ private fun SettingsContent(
uiState: SettingsUiState, uiState: SettingsUiState,
onChooseDirectoryClick: () -> Unit, onChooseDirectoryClick: () -> Unit,
onResetExportDirClick: () -> Unit, onResetExportDirClick: () -> Unit,
onExportFormatChanged: (ExportFormat) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -89,6 +97,7 @@ private fun SettingsContent(
modifier modifier
.fillMaxSize() .fillMaxSize()
.padding(20.dp) .padding(20.dp)
.verticalScroll(rememberScrollState())
) { ) {
DirectorySettingItem( DirectorySettingItem(
label = stringResource(R.string.export_directory), label = stringResource(R.string.export_directory),
@@ -106,6 +115,26 @@ private fun SettingsContent(
Text(stringResource(R.string.reset_to_default)) Text(stringResource(R.string.reset_to_default))
} }
} }
Spacer(Modifier.height(32.dp))
Text("Export format", style = MaterialTheme.typography.titleLarge)
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = uiState.exportFormat == ExportFormat.PDF,
onClick = { onExportFormatChanged(ExportFormat.PDF) },
)
Text("PDF")
}
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = uiState.exportFormat == ExportFormat.JPEG,
onClick = { onExportFormatChanged(ExportFormat.JPEG) },
)
Text("JPEG")
}
} }
} }
@@ -173,6 +202,12 @@ fun SettingsScreenPreviewWithDir() {
@Composable @Composable
fun SettingsScreenPreview(uiState: SettingsUiState) { fun SettingsScreenPreview(uiState: SettingsUiState) {
FairScanTheme { FairScanTheme {
SettingsScreen(uiState, onChooseDirectoryClick = {}, onResetExportDirClick = {}, onBack= {}) SettingsScreen(
uiState,
onChooseDirectoryClick = {},
onResetExportDirClick = {},
onExportFormatChanged = {},
onBack = {}
)
} }
} }

View File

@@ -21,25 +21,37 @@ import kotlinx.coroutines.launch
import org.fairscan.app.AppContainer import org.fairscan.app.AppContainer
data class SettingsUiState( data class SettingsUiState(
val exportDirUri: String? = null val exportDirUri: String? = null,
val exportFormat: ExportFormat = ExportFormat.PDF,
) )
class SettingsViewModel(container: AppContainer) : ViewModel() { class SettingsViewModel(container: AppContainer) : ViewModel() {
private val repo = container.settingsRepository private val repo = container.settingsRepository
val uiState: StateFlow<SettingsUiState> = val uiState = combine(
repo.exportDirUri repo.exportDirUri,
.map { uri -> SettingsUiState(exportDirUri = uri) } repo.exportFormat,
.stateIn( ) { dir, format ->
viewModelScope, SettingsUiState(
SharingStarted.WhileSubscribed(5000), exportDirUri = dir,
SettingsUiState() exportFormat = format,
) )
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
SettingsUiState()
)
fun setExportDirUri(uri: String?) { fun setExportDirUri(uri: String?) {
viewModelScope.launch { viewModelScope.launch {
repo.setExportDirUri(uri) repo.setExportDirUri(uri)
} }
} }
fun setExportFormat(format: ExportFormat) {
viewModelScope.launch {
repo.setExportFormat(format)
}
}
} }

View File

@@ -11,7 +11,7 @@
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>
<string name="copied_logs">Protokoly zkopírovány do schránky</string> <string name="copied_logs">Protokoly zkopírovány do schránky</string>
<string name="copy_logs">Kopírovat protokoly</string> <string name="copy_logs">Kopírovat protokoly</string>
<string name="creating_pdf">Vytváření PDF</string> <string name="creating_export">Příprava exportu</string>
<string name="delete_page">Smazat stránku</string> <string name="delete_page">Smazat stránku</string>
<string name="delete_page_warning">Chcete smazat tuto stránku?</string> <string name="delete_page_warning">Chcete smazat tuto stránku?</string>
<string name="developer">Vývojář</string> <string name="developer">Vývojář</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">stažených</string> <string name="download_dirname">stažených</string>
<string name="end_scan">Ukončit skenování</string> <string name="end_scan">Ukončit skenování</string>
<string name="error">Chyba: %1$s</string> <string name="error">Chyba: %1$s</string>
<string name="error_no_app">Nebyla nalezena žádná aplikace pro otevření tohoto souboru</string>
<string name="error_no_document">Nebyl rozpoznán žádná dokument</string> <string name="error_no_document">Nebyl rozpoznán žádná dokument</string>
<string name="error_no_pdf_app">Nebyla nazelena žádná aplikace pro otevření PDF</string> <string name="error_save">Soubor se nepodařilo uložit</string>
<string name="error_save">Chyba při ukládání PDF</string> <string name="export">Exportovat</string>
<string name="export_as">Exportovat jako %1$s</string>
<string name="export_directory">Složka pro export</string> <string name="export_directory">Složka pro export</string>
<string name="export_pdf">Exportovat PDF</string>
<string name="file_size">Velikost souboru: %1$s</string> <string name="file_size">Velikost souboru: %1$s</string>
<string name="file_size_total">Celková velikost: %1$s</string>
<string name="filename">Název souboru</string> <string name="filename">Název souboru</string>
<string name="grant_permission">Povolit přístup</string> <string name="grant_permission">Povolit přístup</string>
<string name="last_saved_pdf_files">Poslední PDF uložené v tomto zařízení:</string> <string name="last_saved_pdf_files">Poslední PDF uložené v tomto zařízení:</string>
@@ -36,8 +38,7 @@
<string name="menu">Menu</string> <string name="menu">Menu</string>
<string name="new_document_warning">Toto skenování bude ztraceno. Chcete pokračovat?</string> <string name="new_document_warning">Toto skenování bude ztraceno. Chcete pokračovat?</string>
<string name="open">Otevřít</string> <string name="open">Otevřít</string>
<string name="open_pdf">Otevřít PDF</string> <string name="open_file">Otevřít soubor</string>
<string name="pdf_saved_to">PDF bylo uloženo do %1s</string>
<string name="reset_to_default">Obnovit výchozí</string> <string name="reset_to_default">Obnovit výchozí</string>
<string name="resume">Obnovit</string> <string name="resume">Obnovit</string>
<string name="save">Uložit</string> <string name="save">Uložit</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Probíhá skenování</string> <string name="scan_in_progress">Probíhá skenování</string>
<string name="settings">Nastavení</string> <string name="settings">Nastavení</string>
<string name="share">Sdílet</string> <string name="share">Sdílet</string>
<string name="share_pdf">Sdílet PDF</string> <string name="share_document">Sdílet dokument</string>
<string name="storage_permission_denied">Nelze uložit PDF: přístup zakázán</string> <string name="storage_permission_denied">Nelze uložit soubor: oprávnění bylo odmítnuto</string>
<string name="turn_off_torch">Vypnout svítilnu</string> <string name="turn_off_torch">Vypnout svítilnu</string>
<string name="turn_on_torch">Zapnout svítilnu</string> <string name="turn_on_torch">Zapnout svítilnu</string>
<string name="unknown_size">Neznámá velikost</string> <string name="unknown_size">Neznámá velikost</string>
@@ -54,6 +55,12 @@
<string name="view_full_list">Zobrazit úplný seznam</string> <string name="view_full_list">Zobrazit úplný seznam</string>
<string name="view_the_full_license">Zobrazit úplnou licenci</string> <string name="view_the_full_license">Zobrazit úplnou licenci</string>
<string name="yes">Ano</string> <string name="yes">Ano</string>
<plurals name="files_saved_to">
<item quantity="one">%2$s uložen do %3$s</item>
<item quantity="few">%1$d soubory uloženy do %3$s</item>
<item quantity="many">%1$d souborů uloženo do %3$s</item>
<item quantity="other">%1$d souborů uloženo do %3$s</item>
</plurals>
<plurals name="page_count"> <plurals name="page_count">
<item quantity="one">%d stránka</item> <!-- 1 --> <item quantity="one">%d stránka</item> <!-- 1 -->
<item quantity="few">%d stránky</item> <!-- 24 --> <item quantity="few">%d stránky</item> <!-- 24 -->

View File

@@ -11,7 +11,7 @@
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>
<string name="copied_logs">Logs in die Zwischenablage kopiert</string> <string name="copied_logs">Logs in die Zwischenablage kopiert</string>
<string name="copy_logs">Logs kopieren</string> <string name="copy_logs">Logs kopieren</string>
<string name="creating_pdf">PDF wird erstellt…</string> <string name="creating_export">Export wird vorbereitet…</string>
<string name="delete_page">Seite löschen</string> <string name="delete_page">Seite löschen</string>
<string name="delete_page_warning">Möchten Sie diese Seite löschen?</string> <string name="delete_page_warning">Möchten Sie diese Seite löschen?</string>
<string name="developer">Entwickler</string> <string name="developer">Entwickler</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">Downloads</string> <string name="download_dirname">Downloads</string>
<string name="end_scan">Scan beenden</string> <string name="end_scan">Scan beenden</string>
<string name="error">Fehler: %1$s</string> <string name="error">Fehler: %1$s</string>
<string name="error_no_app">Keine App zum Öffnen dieser Datei gefunden</string>
<string name="error_no_document">Kein Dokument erkannt</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">Datei konnte nicht gespeichert werden</string>
<string name="error_save">PDF konnte nicht gespeichert werden</string> <string name="export">Exportieren</string>
<string name="export_as">Als %1$s exportieren</string>
<string name="export_directory">Exportordner</string> <string name="export_directory">Exportordner</string>
<string name="export_pdf">PDF exportieren</string>
<string name="file_size">Dateigröße: %1$s</string> <string name="file_size">Dateigröße: %1$s</string>
<string name="file_size_total">Gesamtgröße: %1$s</string>
<string name="filename">Dateiname</string> <string name="filename">Dateiname</string>
<string name="grant_permission">Berechtigung erteilen</string> <string name="grant_permission">Berechtigung erteilen</string>
<string name="last_saved_pdf_files">Zuletzt auf diesem Gerät gespeicherte PDFs:</string> <string name="last_saved_pdf_files">Zuletzt auf diesem Gerät gespeicherte PDFs:</string>
@@ -36,8 +38,7 @@
<string name="menu">Menü</string> <string name="menu">Menü</string>
<string name="new_document_warning">Das aktuelle Dokument geht verloren. 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">Öffnen</string>
<string name="open_pdf">PDF öffnen</string> <string name="open_file">Datei öffnen</string>
<string name="pdf_saved_to">PDF gespeichert in %1s</string>
<string name="reset_to_default">Auf Standard zurücksetzen</string> <string name="reset_to_default">Auf Standard zurücksetzen</string>
<string name="resume">Fortsetzen</string> <string name="resume">Fortsetzen</string>
<string name="save">Speichern</string> <string name="save">Speichern</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Scan läuft</string> <string name="scan_in_progress">Scan läuft</string>
<string name="settings">Einstellungen</string> <string name="settings">Einstellungen</string>
<string name="share">Teilen</string> <string name="share">Teilen</string>
<string name="share_pdf">PDF teilen</string> <string name="share_document">Dokument teilen</string>
<string name="storage_permission_denied">PDF-Datei kann nicht gespeichert werden: Berechtigung verweigert</string> <string name="storage_permission_denied">Datei kann nicht gespeichert werden: Berechtigung verweigert</string>
<string name="turn_off_torch">Taschenlampe ausschalten</string> <string name="turn_off_torch">Taschenlampe ausschalten</string>
<string name="turn_on_torch">Taschenlampe einschalten</string> <string name="turn_on_torch">Taschenlampe einschalten</string>
<string name="unknown_size">Unbekannte Größe</string> <string name="unknown_size">Unbekannte Größe</string>
@@ -54,6 +55,10 @@
<string name="view_full_list">Vollständige Liste anzeigen</string> <string name="view_full_list">Vollständige Liste anzeigen</string>
<string name="view_the_full_license">Vollständige Lizenz anzeigen</string> <string name="view_the_full_license">Vollständige Lizenz anzeigen</string>
<string name="yes">Ja</string> <string name="yes">Ja</string>
<plurals name="files_saved_to">
<item quantity="one">%2$s gespeichert in %3$s</item>
<item quantity="other">%1$d Dateien gespeichert in %3$s</item>
</plurals>
<plurals name="page_count"> <plurals name="page_count">
<item quantity="one">%d Seite</item> <item quantity="one">%d Seite</item>
<item quantity="other">%d Seiten</item> <item quantity="other">%d Seiten</item>

View File

@@ -11,7 +11,7 @@
<string name="contact">Contacto</string> <string name="contact">Contacto</string>
<string name="copied_logs">Registros copiados al portapapeles</string> <string name="copied_logs">Registros copiados al portapapeles</string>
<string name="copy_logs">Copiar registros</string> <string name="copy_logs">Copiar registros</string>
<string name="creating_pdf">Creando PDF</string> <string name="creating_export">Preparando la exportación</string>
<string name="delete_page">Eliminar página</string> <string name="delete_page">Eliminar página</string>
<string name="delete_page_warning">¿Quieres eliminar esta página?</string> <string name="delete_page_warning">¿Quieres eliminar esta página?</string>
<string name="developer">Desarrollador</string> <string name="developer">Desarrollador</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">Descargas</string> <string name="download_dirname">Descargas</string>
<string name="end_scan">Finalizar escaneo</string> <string name="end_scan">Finalizar escaneo</string>
<string name="error">Error: %1$s</string> <string name="error">Error: %1$s</string>
<string name="error_no_app">No se encontró ninguna aplicación para abrir este archivo</string>
<string name="error_no_document">No se detectó ningún documento</string> <string name="error_no_document">No se detectó ningún documento</string>
<string name="error_no_pdf_app">No se encontró ninguna aplicación para abrir PDF</string> <string name="error_save">No se pudo guardar el archivo</string>
<string name="error_save">Error al guardar el PDF</string> <string name="export">Exportar</string>
<string name="export_as">Exportar como %1$s</string>
<string name="export_directory">Carpeta de exportación</string> <string name="export_directory">Carpeta de exportación</string>
<string name="export_pdf">Exportar PDF</string>
<string name="file_size">Tamaño del archivo: %1$s</string> <string name="file_size">Tamaño del archivo: %1$s</string>
<string name="file_size_total">Tamaño total: %1$s</string>
<string name="filename">Nombre del archivo</string> <string name="filename">Nombre del archivo</string>
<string name="grant_permission">Conceder permiso</string> <string name="grant_permission">Conceder permiso</string>
<string name="last_saved_pdf_files">PDF recientes guardados en este dispositivo:</string> <string name="last_saved_pdf_files">PDF recientes guardados en este dispositivo:</string>
@@ -36,8 +38,7 @@
<string name="menu">Menú</string> <string name="menu">Menú</string>
<string name="new_document_warning">El escaneo actual se perderá. ¿Deseas continuar?</string> <string name="new_document_warning">El escaneo actual se perderá. ¿Deseas continuar?</string>
<string name="open">Abrir</string> <string name="open">Abrir</string>
<string name="open_pdf">Abrir PDF</string> <string name="open_file">Abrir archivo</string>
<string name="pdf_saved_to">PDF guardado en %1s</string>
<string name="reset_to_default">Restablecer valores predeterminados</string> <string name="reset_to_default">Restablecer valores predeterminados</string>
<string name="resume">Reanudar</string> <string name="resume">Reanudar</string>
<string name="save">Guardar</string> <string name="save">Guardar</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Escaneo en curso</string> <string name="scan_in_progress">Escaneo en curso</string>
<string name="settings">Ajustes</string> <string name="settings">Ajustes</string>
<string name="share">Compartir</string> <string name="share">Compartir</string>
<string name="share_pdf">Compartir PDF</string> <string name="share_document">Compartir documento</string>
<string name="storage_permission_denied">No se puede guardar el archivo PDF: permiso denegado</string> <string name="storage_permission_denied">No se puede guardar el archivo: permiso denegado</string>
<string name="turn_off_torch">Apagar linterna</string> <string name="turn_off_torch">Apagar linterna</string>
<string name="turn_on_torch">Encender linterna</string> <string name="turn_on_torch">Encender linterna</string>
<string name="unknown_size">Tamaño desconocido</string> <string name="unknown_size">Tamaño desconocido</string>
@@ -54,6 +55,10 @@
<string name="view_full_list">Ver lista completa</string> <string name="view_full_list">Ver lista completa</string>
<string name="view_the_full_license">Ver la licencia completa</string> <string name="view_the_full_license">Ver la licencia completa</string>
<string name="yes"></string> <string name="yes"></string>
<plurals name="files_saved_to" tools:ignore="MissingQuantity">
<item quantity="one">%2$s guardado en %3$s</item>
<item quantity="other">%1$d archivos guardados en %3$s</item>
</plurals>
<plurals name="page_count" tools:ignore="MissingQuantity"> <plurals name="page_count" tools:ignore="MissingQuantity">
<item quantity="one">%d página</item> <item quantity="one">%d página</item>
<item quantity="other">%d páginas</item> <item quantity="other">%d páginas</item>

View File

@@ -11,7 +11,7 @@
<string name="contact">Contact</string> <string name="contact">Contact</string>
<string name="copied_logs">Logs copiés dans le presse-papiers</string> <string name="copied_logs">Logs copiés dans le presse-papiers</string>
<string name="copy_logs">Copier les logs</string> <string name="copy_logs">Copier les logs</string>
<string name="creating_pdf">Création du PDF</string> <string name="creating_export">Pparation de lexport</string>
<string name="delete_page">Supprimer la page</string> <string name="delete_page">Supprimer la page</string>
<string name="delete_page_warning">Voulez-vous supprimer cette page ?</string> <string name="delete_page_warning">Voulez-vous supprimer cette page ?</string>
<string name="developer">Développeur</string> <string name="developer">Développeur</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">Téléchargements</string> <string name="download_dirname">Téléchargements</string>
<string name="end_scan">Terminer le scan</string> <string name="end_scan">Terminer le scan</string>
<string name="error">Erreur : %1$s</string> <string name="error">Erreur : %1$s</string>
<string name="error_no_app">Aucune application trouvée pour ouvrir ce fichier</string>
<string name="error_no_document">Aucun document détecté</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 fichier</string>
<string name="error_save">Échec de l\'enregistrement du PDF</string> <string name="export">Exporter</string>
<string name="export_as">Exporter en %1$s</string>
<string name="export_directory">Dossier dexport</string> <string name="export_directory">Dossier dexport</string>
<string name="export_pdf">Exporter en PDF</string>
<string name="file_size">Taille du fichier : %1$s</string> <string name="file_size">Taille du fichier : %1$s</string>
<string name="file_size_total">Taille totale : %1$s</string>
<string name="filename">Nom de fichier</string> <string name="filename">Nom de fichier</string>
<string name="grant_permission">Autoriser</string> <string name="grant_permission">Autoriser</string>
<string name="last_saved_pdf_files">Derniers PDF enregistrés sur lappareil :</string> <string name="last_saved_pdf_files">Derniers PDF enregistrés sur lappareil :</string>
@@ -36,8 +38,7 @@
<string name="menu">Menu</string> <string name="menu">Menu</string>
<string name="new_document_warning">Le scan en cours sera perdu. 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">Ouvrir</string>
<string name="open_pdf">Ouvrir le PDF</string> <string name="open_file">Ouvrir le fichier</string>
<string name="pdf_saved_to">PDF enregistré dans %1s</string>
<string name="reset_to_default">Réinitialiser par défaut</string> <string name="reset_to_default">Réinitialiser par défaut</string>
<string name="resume">Reprendre</string> <string name="resume">Reprendre</string>
<string name="save">Enregistrer</string> <string name="save">Enregistrer</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Scan en cours</string> <string name="scan_in_progress">Scan en cours</string>
<string name="settings">Paramètres</string> <string name="settings">Paramètres</string>
<string name="share">Partager</string> <string name="share">Partager</string>
<string name="share_pdf">Partager le PDF</string> <string name="share_document">Partager le document</string>
<string name="storage_permission_denied">Impossible denregistrer le fichier PDF : permission refusée</string> <string name="storage_permission_denied">Impossible denregistrer le fichier : permission refusée</string>
<string name="turn_off_torch">Éteindre la torche</string> <string name="turn_off_torch">Éteindre la torche</string>
<string name="turn_on_torch">Allumer la torche</string> <string name="turn_on_torch">Allumer la torche</string>
<string name="unknown_size">Taille inconnue</string> <string name="unknown_size">Taille inconnue</string>
@@ -54,6 +55,10 @@
<string name="view_full_list">Voir la liste complète</string> <string name="view_full_list">Voir la liste complète</string>
<string name="view_the_full_license">Voir la licence complète</string> <string name="view_the_full_license">Voir la licence complète</string>
<string name="yes">Oui</string> <string name="yes">Oui</string>
<plurals name="files_saved_to" tools:ignore="MissingQuantity">
<item quantity="one">%2$s enregistré dans %3$s</item>
<item quantity="other">%1$d fichiers enregistrés dans %3$s</item>
</plurals>
<plurals name="page_count" tools:ignore="MissingQuantity"> <plurals name="page_count" tools:ignore="MissingQuantity">
<item quantity="one">%d page</item> <item quantity="one">%d page</item>
<item quantity="other">%d pages</item> <item quantity="other">%d pages</item>

View File

@@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="about">Informazioni</string> <string name="about">Informazioni</string>
<string name="add_page">Aggiungi pagina</string> <string name="add_page">Aggiungi pagina</string>
<string name="app_tagline">Un\'app semplice e rispettosa per scansionare i tuoi documenti.</string> <string name="app_tagline">Un\'app semplice e rispettosa per scansionare i tuoi documenti.</string>
@@ -11,7 +11,7 @@
<string name="contact">Contatti</string> <string name="contact">Contatti</string>
<string name="copied_logs">Log copiati negli appunti</string> <string name="copied_logs">Log copiati negli appunti</string>
<string name="copy_logs">Copia log</string> <string name="copy_logs">Copia log</string>
<string name="creating_pdf">Creazione PDF</string> <string name="creating_export">Preparazione dellesportazione</string>
<string name="delete_page">Elimina pagina</string> <string name="delete_page">Elimina pagina</string>
<string name="delete_page_warning">Vuoi eliminare questa pagina?</string> <string name="delete_page_warning">Vuoi eliminare questa pagina?</string>
<string name="developer">Sviluppatore</string> <string name="developer">Sviluppatore</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">Download</string> <string name="download_dirname">Download</string>
<string name="end_scan">Termina scansione</string> <string name="end_scan">Termina scansione</string>
<string name="error">Errore: %1$s</string> <string name="error">Errore: %1$s</string>
<string name="error_no_app">Nessuna app trovata per aprire questo file</string>
<string name="error_no_document">Nessun documento rilevato</string> <string name="error_no_document">Nessun documento rilevato</string>
<string name="error_no_pdf_app">Nessuna app trovata per aprire PDF</string> <string name="error_save">Impossibile salvare il file</string>
<string name="error_save">Salvataggio PDF fallito</string> <string name="export">Esporta</string>
<string name="export_as">Esporta come %1$s</string>
<string name="export_directory">Cartella di esportazione</string> <string name="export_directory">Cartella di esportazione</string>
<string name="export_pdf">Esporta PDF</string> <string name="file_size">Dimensione del file: %1$s</string>
<string name="file_size">Dimensione file: %1$s</string> <string name="file_size_total">Dimensione totale: %1$s</string>
<string name="filename">Nome file</string> <string name="filename">Nome file</string>
<string name="grant_permission">Concendi autorizzazione</string> <string name="grant_permission">Concendi autorizzazione</string>
<string name="last_saved_pdf_files">PDF recenti salvati su questo dispositivo:</string> <string name="last_saved_pdf_files">PDF recenti salvati su questo dispositivo:</string>
@@ -36,8 +38,7 @@
<string name="menu">Menu</string> <string name="menu">Menu</string>
<string name="new_document_warning">La scansiona attuale verrà persa. Vuoi continuare?</string> <string name="new_document_warning">La scansiona attuale verrà persa. Vuoi continuare?</string>
<string name="open">Apri</string> <string name="open">Apri</string>
<string name="open_pdf">Apri PDF</string> <string name="open_file">Apri file</string>
<string name="pdf_saved_to">PDF salvato in %1s</string>
<string name="reset_to_default">Ripristina impostazioni predefinite</string> <string name="reset_to_default">Ripristina impostazioni predefinite</string>
<string name="resume">Riprendi</string> <string name="resume">Riprendi</string>
<string name="save">Salva</string> <string name="save">Salva</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Scansione in corso</string> <string name="scan_in_progress">Scansione in corso</string>
<string name="settings">Impostazioni</string> <string name="settings">Impostazioni</string>
<string name="share">Condividi</string> <string name="share">Condividi</string>
<string name="share_pdf">Condividi PDF</string> <string name="share_document">Condividi documento</string>
<string name="storage_permission_denied">Impossibile salvare il file PDF: autorizzazione negata</string> <string name="storage_permission_denied">Impossibile salvare il file: permesso negato</string>
<string name="turn_off_torch">Spegni la torcia</string> <string name="turn_off_torch">Spegni la torcia</string>
<string name="turn_on_torch">Accendi la torcia</string> <string name="turn_on_torch">Accendi la torcia</string>
<string name="unknown_size">Dimensione sconosciuta</string> <string name="unknown_size">Dimensione sconosciuta</string>
@@ -54,7 +55,11 @@
<string name="view_full_list">Vedi l\'elenco completo</string> <string name="view_full_list">Vedi l\'elenco completo</string>
<string name="view_the_full_license">Vedi la licenza completa</string> <string name="view_the_full_license">Vedi la licenza completa</string>
<string name="yes"></string> <string name="yes"></string>
<plurals name="page_count"> <plurals name="files_saved_to" tools:ignore="MissingQuantity">
<item quantity="one">%2$s salvato in %3$s</item>
<item quantity="other">%1$d file salvati in %3$s</item>
</plurals>
<plurals name="page_count" tools:ignore="MissingQuantity">
<item quantity="one">%d pagina</item> <item quantity="one">%d pagina</item>
<item quantity="other">%d pagine</item> <item quantity="other">%d pagine</item>
</plurals> </plurals>

View File

@@ -11,7 +11,7 @@
<string name="contact">Contato</string> <string name="contact">Contato</string>
<string name="copied_logs">Registros copiados para a área de transferência</string> <string name="copied_logs">Registros copiados para a área de transferência</string>
<string name="copy_logs">Copiar registros</string> <string name="copy_logs">Copiar registros</string>
<string name="creating_pdf">Criando PDF</string> <string name="creating_export">Preparando exportação</string>
<string name="delete_page">Excluir página</string> <string name="delete_page">Excluir página</string>
<string name="delete_page_warning">Deseja excluir esta página?</string> <string name="delete_page_warning">Deseja excluir esta página?</string>
<string name="developer">Desenvolvedor</string> <string name="developer">Desenvolvedor</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">Downloads</string> <string name="download_dirname">Downloads</string>
<string name="end_scan">Finalizar digitalização</string> <string name="end_scan">Finalizar digitalização</string>
<string name="error">Erro: %1$s</string> <string name="error">Erro: %1$s</string>
<string name="error_no_app">Nenhum app encontrado para abrir este arquivo</string>
<string name="error_no_document">Nenhum documento detectado</string> <string name="error_no_document">Nenhum documento detectado</string>
<string name="error_no_pdf_app">Nenhum aplicativo encontrado para abrir PDF</string> <string name="error_save">Falha ao salvar o arquivo</string>
<string name="error_save">Falha ao salvar PDF</string> <string name="export">Exportar</string>
<string name="export_as">Exportar como %1$s</string>
<string name="export_directory">Diretório de exportação</string> <string name="export_directory">Diretório de exportação</string>
<string name="export_pdf">Exportar PDF</string>
<string name="file_size">Tamanho do arquivo: %1$s</string> <string name="file_size">Tamanho do arquivo: %1$s</string>
<string name="file_size_total">Tamanho total: %1$s</string>
<string name="filename">Nome do arquivo</string> <string name="filename">Nome do arquivo</string>
<string name="grant_permission">Conceder permissão</string> <string name="grant_permission">Conceder permissão</string>
<string name="last_saved_pdf_files">PDFs recentes salvos neste dispositivo:</string> <string name="last_saved_pdf_files">PDFs recentes salvos neste dispositivo:</string>
@@ -36,8 +38,7 @@
<string name="menu">Menu</string> <string name="menu">Menu</string>
<string name="new_document_warning">A digitalização atual será perdida. Deseja continuar?</string> <string name="new_document_warning">A digitalização atual será perdida. Deseja continuar?</string>
<string name="open">Abrir</string> <string name="open">Abrir</string>
<string name="open_pdf">Abrir PDF</string> <string name="open_file">Abrir arquivo</string>
<string name="pdf_saved_to">PDF salvo em %1s</string>
<string name="reset_to_default">Restaurar padrão</string> <string name="reset_to_default">Restaurar padrão</string>
<string name="resume">Retomar</string> <string name="resume">Retomar</string>
<string name="save">Salvar</string> <string name="save">Salvar</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Digitalização em andamento</string> <string name="scan_in_progress">Digitalização em andamento</string>
<string name="settings">Configurações</string> <string name="settings">Configurações</string>
<string name="share">Compartilhar</string> <string name="share">Compartilhar</string>
<string name="share_pdf">Compartilhar PDF</string> <string name="share_document">Compartilhar documento</string>
<string name="storage_permission_denied">Não foi possível salvar o arquivo PDF: permissão negada</string> <string name="storage_permission_denied">Não foi possível salvar o arquivo: permissão negada</string>
<string name="turn_off_torch">Desligar lanterna</string> <string name="turn_off_torch">Desligar lanterna</string>
<string name="turn_on_torch">Ligar lanterna</string> <string name="turn_on_torch">Ligar lanterna</string>
<string name="unknown_size">Tamanho desconhecido</string> <string name="unknown_size">Tamanho desconhecido</string>
@@ -54,6 +55,10 @@
<string name="view_full_list">Ver lista completa</string> <string name="view_full_list">Ver lista completa</string>
<string name="view_the_full_license">Ver licença completa</string> <string name="view_the_full_license">Ver licença completa</string>
<string name="yes">Sim</string> <string name="yes">Sim</string>
<plurals name="files_saved_to" tools:ignore="MissingQuantity">
<item quantity="one">%2$s salvo em %3$s</item>
<item quantity="other">%1$d arquivos salvos em %3$s</item>
</plurals>
<plurals name="page_count" tools:ignore="MissingQuantity"> <plurals name="page_count" tools:ignore="MissingQuantity">
<item quantity="one">%d página</item> <item quantity="one">%d página</item>
<item quantity="other">%d páginas</item> <item quantity="other">%d páginas</item>

View File

@@ -11,7 +11,7 @@
<string name="contact">Контакты</string> <string name="contact">Контакты</string>
<string name="copied_logs">Журналы скопированы в буфер обмена</string> <string name="copied_logs">Журналы скопированы в буфер обмена</string>
<string name="copy_logs">Копировать журналы</string> <string name="copy_logs">Копировать журналы</string>
<string name="creating_pdf">Создание PDF</string> <string name="creating_export">Подготовка экспорта</string>
<string name="delete_page">Удалить страницу</string> <string name="delete_page">Удалить страницу</string>
<string name="delete_page_warning">Вы желаете удалить эту страницу?</string> <string name="delete_page_warning">Вы желаете удалить эту страницу?</string>
<string name="developer">Разработчик</string> <string name="developer">Разработчик</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">Download</string> <string name="download_dirname">Download</string>
<string name="end_scan">Закончить</string> <string name="end_scan">Закончить</string>
<string name="error">Ошибка: %1$s</string> <string name="error">Ошибка: %1$s</string>
<string name="error_no_app">Не найдено приложение для открытия этого файла</string>
<string name="error_no_document">Документ не обнаружен</string> <string name="error_no_document">Документ не обнаружен</string>
<string name="error_no_pdf_app">Приложения для работы с PDF не обнаружено</string> <string name="error_save">Не удалось сохранить файл</string>
<string name="error_save">Сбой при сохранении PDF</string> <string name="export">Экспорт</string>
<string name="export_as">Экспортировать как %1$s</string>
<string name="export_directory">Папка экспорта</string> <string name="export_directory">Папка экспорта</string>
<string name="export_pdf">Экспорт PDF</string>
<string name="file_size">Размер файла: %1$s</string> <string name="file_size">Размер файла: %1$s</string>
<string name="file_size_total">Общий размер: %1$s</string>
<string name="filename">Имя файла</string> <string name="filename">Имя файла</string>
<string name="grant_permission">Предоставить разрешение</string> <string name="grant_permission">Предоставить разрешение</string>
<string name="last_saved_pdf_files">Последние PDF, сохранённые на этом устройстве:</string> <string name="last_saved_pdf_files">Последние PDF, сохранённые на этом устройстве:</string>
@@ -36,8 +38,7 @@
<string name="menu">Меню</string> <string name="menu">Меню</string>
<string name="new_document_warning">Результаты текущего сканирования будут потеряны. Желаете продолжить?</string> <string name="new_document_warning">Результаты текущего сканирования будут потеряны. Желаете продолжить?</string>
<string name="open">Открыть</string> <string name="open">Открыть</string>
<string name="open_pdf">Открыть PDF</string> <string name="open_file">Открыть файл</string>
<string name="pdf_saved_to">PDF сохранен в %1s</string>
<string name="reset_to_default">Сбросить по умолчанию</string> <string name="reset_to_default">Сбросить по умолчанию</string>
<string name="resume">Продолжить</string> <string name="resume">Продолжить</string>
<string name="save">Сохранить</string> <string name="save">Сохранить</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">Сканирование выполняется</string> <string name="scan_in_progress">Сканирование выполняется</string>
<string name="settings">Настройки</string> <string name="settings">Настройки</string>
<string name="share">Поделиться</string> <string name="share">Поделиться</string>
<string name="share_pdf">Поделиться PDF</string> <string name="share_document">Поделиться документом</string>
<string name="storage_permission_denied">Не удается сохранить файл PDF: в разрешении отказано</string> <string name="storage_permission_denied">Невозможно сохранить файл: доступ запрещён</string>
<string name="turn_off_torch">Выключить фонарик</string> <string name="turn_off_torch">Выключить фонарик</string>
<string name="turn_on_torch">Включить фонарик</string> <string name="turn_on_torch">Включить фонарик</string>
<string name="unknown_size">Неизвестный размер</string> <string name="unknown_size">Неизвестный размер</string>
@@ -54,6 +55,12 @@
<string name="view_full_list">Просмотреть полный список</string> <string name="view_full_list">Просмотреть полный список</string>
<string name="view_the_full_license">Просмотреть полную лицензию</string> <string name="view_the_full_license">Просмотреть полную лицензию</string>
<string name="yes">Да</string> <string name="yes">Да</string>
<plurals name="files_saved_to">
<item quantity="one">%2$s сохранён в %3$s</item>
<item quantity="few">%1$d файла сохранены в %3$s</item>
<item quantity="many">%1$d файлов сохранено в %3$s</item>
<item quantity="other">%1$d файла сохранено в %3$s</item>
</plurals>
<plurals name="page_count"> <plurals name="page_count">
<item quantity="one">%d страница</item> <item quantity="one">%d страница</item>
<item quantity="few">%d страницы</item> <item quantity="few">%d страницы</item>

View File

@@ -11,7 +11,7 @@
<string name="contact">联系人</string> <string name="contact">联系人</string>
<string name="copied_logs">日志已复制到剪贴板</string> <string name="copied_logs">日志已复制到剪贴板</string>
<string name="copy_logs">复制日志</string> <string name="copy_logs">复制日志</string>
<string name="creating_pdf">正在创建 PDF</string> <string name="creating_export">正在准备导出</string>
<string name="delete_page">删除页面</string> <string name="delete_page">删除页面</string>
<string name="delete_page_warning">是否要删除此页面?</string> <string name="delete_page_warning">是否要删除此页面?</string>
<string name="developer">开发者</string> <string name="developer">开发者</string>
@@ -19,12 +19,14 @@
<string name="download_dirname">下载</string> <string name="download_dirname">下载</string>
<string name="end_scan">结束扫描</string> <string name="end_scan">结束扫描</string>
<string name="error">错误: %1$s</string> <string name="error">错误: %1$s</string>
<string name="error_no_app">未找到可打开此文件的应用</string>
<string name="error_no_document">未检测到任何文档</string> <string name="error_no_document">未检测到任何文档</string>
<string name="error_no_pdf_app">未找到可打开PDF的应用</string> <string name="error_save">无法保存文件</string>
<string name="error_save">保存PDF失败</string> <string name="export">导出</string>
<string name="export_as">导出为 %1$s</string>
<string name="export_directory">导出目录</string> <string name="export_directory">导出目录</string>
<string name="export_pdf">导出PDF</string> <string name="file_size">文件大小:%1$s</string>
<string name="file_size">文件大小: %1$s</string> <string name="file_size_total">总大小:%1$s</string>
<string name="filename">文件名字</string> <string name="filename">文件名字</string>
<string name="grant_permission">授予权限</string> <string name="grant_permission">授予权限</string>
<string name="last_saved_pdf_files">最近保存在此设备上的 PDF</string> <string name="last_saved_pdf_files">最近保存在此设备上的 PDF</string>
@@ -36,8 +38,7 @@
<string name="menu">菜单</string> <string name="menu">菜单</string>
<string name="new_document_warning">当前扫描将丢失。是否继续?</string> <string name="new_document_warning">当前扫描将丢失。是否继续?</string>
<string name="open">打开</string> <string name="open">打开</string>
<string name="open_pdf">打开 PDF</string> <string name="open_file">打开文件</string>
<string name="pdf_saved_to">PDF 已保存到 %1$s</string>
<string name="reset_to_default">恢复默认设置</string> <string name="reset_to_default">恢复默认设置</string>
<string name="resume">恢复</string> <string name="resume">恢复</string>
<string name="save">保存</string> <string name="save">保存</string>
@@ -45,8 +46,8 @@
<string name="scan_in_progress">正在进行扫描</string> <string name="scan_in_progress">正在进行扫描</string>
<string name="settings">设置</string> <string name="settings">设置</string>
<string name="share">共享</string> <string name="share">共享</string>
<string name="share_pdf">共享 PDF</string> <string name="share_document">分享文档</string>
<string name="storage_permission_denied">无法保存PDF文件:权限被拒绝</string> <string name="storage_permission_denied">无法保存文件:权限被拒绝</string>
<string name="turn_off_torch">关闭手电筒</string> <string name="turn_off_torch">关闭手电筒</string>
<string name="turn_on_torch">打开手电筒</string> <string name="turn_on_torch">打开手电筒</string>
<string name="unknown_size">未知大小</string> <string name="unknown_size">未知大小</string>
@@ -54,8 +55,10 @@
<string name="view_full_list">查看完整列表</string> <string name="view_full_list">查看完整列表</string>
<string name="view_the_full_license">查看完整许可证</string> <string name="view_the_full_license">查看完整许可证</string>
<string name="yes"></string> <string name="yes"></string>
<plurals name="files_saved_to">
<item quantity="other">%1$d 个文件已保存到 %3$s</item>
</plurals>
<plurals name="page_count"> <plurals name="page_count">
<item quantity="one">%d 页</item>
<item quantity="other">%d 页</item> <item quantity="other">%d 页</item>
</plurals> </plurals>
</resources> </resources>

View File

@@ -12,7 +12,7 @@
<string name="contact">Contact</string> <string name="contact">Contact</string>
<string name="copied_logs">Logs copied to clipboard</string> <string name="copied_logs">Logs copied to clipboard</string>
<string name="copy_logs">Copy logs</string> <string name="copy_logs">Copy logs</string>
<string name="creating_pdf">Creating PDF</string> <string name="creating_export">Preparing export</string>
<string name="delete_page">Delete page</string> <string name="delete_page">Delete page</string>
<string name="delete_page_warning">Do you want to delete this page?</string> <string name="delete_page_warning">Do you want to delete this page?</string>
<string name="developer">Developer</string> <string name="developer">Developer</string>
@@ -20,12 +20,14 @@
<string name="download_dirname">Downloads</string> <string name="download_dirname">Downloads</string>
<string name="end_scan">End scan</string> <string name="end_scan">End scan</string>
<string name="error">Error: %1$s</string> <string name="error">Error: %1$s</string>
<string name="error_no_app">No app found to open this file</string>
<string name="error_no_document">No document detected</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 file</string>
<string name="error_save">Failed to save PDF</string> <string name="export">Export</string>
<string name="export_as">Export as %1$s</string>
<string name="export_directory">Export directory</string> <string name="export_directory">Export directory</string>
<string name="export_pdf">Export PDF</string>
<string name="file_size">File size: %1$s</string> <string name="file_size">File size: %1$s</string>
<string name="file_size_total">Total size: %1$s</string>
<string name="filename">Filename</string> <string name="filename">Filename</string>
<string name="grant_permission">Grant permission</string> <string name="grant_permission">Grant permission</string>
<string name="last_saved_pdf_files">Recent PDFs saved on this device:</string> <string name="last_saved_pdf_files">Recent PDFs saved on this device:</string>
@@ -37,8 +39,7 @@
<string name="menu">Menu</string> <string name="menu">Menu</string>
<string name="new_document_warning">The current scan will be lost. 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">Open</string>
<string name="open_pdf">Open PDF</string> <string name="open_file">Open file</string>
<string name="pdf_saved_to">PDF saved in %1s</string>
<string name="reset_to_default">Reset to default</string> <string name="reset_to_default">Reset to default</string>
<string name="resume">Resume</string> <string name="resume">Resume</string>
<string name="save">Save</string> <string name="save">Save</string>
@@ -46,8 +47,8 @@
<string name="scan_in_progress">Scan in progress</string> <string name="scan_in_progress">Scan in progress</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="share_pdf">Share PDF</string> <string name="share_document">Share document</string>
<string name="storage_permission_denied">Cannot save PDF file: permission was denied</string> <string name="storage_permission_denied">Cannot save file: permission was denied</string>
<string name="turn_off_torch">Turn off torch</string> <string name="turn_off_torch">Turn off torch</string>
<string name="turn_on_torch">Turn on torch</string> <string name="turn_on_torch">Turn on torch</string>
<string name="unknown_size">Unknown size</string> <string name="unknown_size">Unknown size</string>
@@ -55,6 +56,10 @@
<string name="view_full_list">View full list</string> <string name="view_full_list">View full list</string>
<string name="view_the_full_license">View the full license</string> <string name="view_the_full_license">View the full license</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>
<plurals name="files_saved_to">
<item quantity="one">%2$s saved to %3$s</item>
<item quantity="other">%1$d files saved to %3$s</item>
</plurals>
<plurals name="page_count"> <plurals name="page_count">
<item quantity="one">%d page</item> <item quantity="one">%d page</item>
<item quantity="other">%d pages</item> <item quantity="other">%d pages</item>

View File

@@ -20,7 +20,7 @@ import java.io.File
import java.io.OutputStream import java.io.OutputStream
import kotlin.io.path.createTempDirectory import kotlin.io.path.createTempDirectory
class PdfFileManagerTest { class FileManagerTest {
val pdfDir: File = createTempDirectory().toFile() val pdfDir: File = createTempDirectory().toFile()
val externalDir: File = createTempDirectory().toFile() val externalDir: File = createTempDirectory().toFile()
@@ -33,7 +33,7 @@ class PdfFileManagerTest {
val f = File(externalDir, "f.pdf") val f = File(externalDir, "f.pdf")
assertThat(f).doesNotExist() assertThat(f).doesNotExist()
val manager = PdfFileManager(pdfDir, externalDir, dummyPdfWriter) val manager = FileManager(pdfDir, externalDir, dummyPdfWriter)
assertThat(manager.copyToExternalDir(original)) assertThat(manager.copyToExternalDir(original))
.isEqualTo(f) .isEqualTo(f)
.hasContent("original content") .hasContent("original content")
@@ -48,7 +48,7 @@ class PdfFileManagerTest {
@Test @Test
fun cleanUpOldFiles() { fun cleanUpOldFiles() {
val subDir = File(pdfDir,"subDir") val subDir = File(pdfDir,"subDir")
val manager = PdfFileManager(subDir, externalDir, dummyPdfWriter) val manager = FileManager(subDir, externalDir, dummyPdfWriter)
manager.cleanUpOldFiles(10) manager.cleanUpOldFiles(10)
assertThat(subDir).doesNotExist() assertThat(subDir).doesNotExist()
@@ -76,7 +76,7 @@ class PdfFileManagerTest {
return list.size return list.size
} }
} }
val manager = PdfFileManager(pdfDir, externalDir, fakePdfWriter) val manager = FileManager(pdfDir, externalDir, fakePdfWriter)
val jpegs = listOf(byteArrayOf(0x01, 0x02), byteArrayOf(0x11)).asSequence() val jpegs = listOf(byteArrayOf(0x01, 0x02), byteArrayOf(0x11)).asSequence()
val pdf = manager.generatePdf(jpegs) val pdf = manager.generatePdf(jpegs)
assertThat(pdf.pageCount).isEqualTo(2) assertThat(pdf.pageCount).isEqualTo(2)
@@ -87,9 +87,9 @@ class PdfFileManagerTest {
@Test @Test
fun addExtensionIfMissing() { fun addExtensionIfMissing() {
assertThat(PdfFileManager.addExtensionIfMissing("f1.pdf")).isEqualTo("f1.pdf") assertThat(FileManager.addPdfExtensionIfMissing("f1.pdf")).isEqualTo("f1.pdf")
assertThat(PdfFileManager.addExtensionIfMissing("f2.PDF")).isEqualTo("f2.PDF") assertThat(FileManager.addPdfExtensionIfMissing("f2.PDF")).isEqualTo("f2.PDF")
assertThat(PdfFileManager.addExtensionIfMissing("f3")).isEqualTo("f3.pdf") assertThat(FileManager.addPdfExtensionIfMissing("f3")).isEqualTo("f3.pdf")
assertThat(PdfFileManager.addExtensionIfMissing("f4.txt")).isEqualTo("f4.txt.pdf") assertThat(FileManager.addPdfExtensionIfMissing("f4.txt")).isEqualTo("f4.txt.pdf")
} }
} }