PDF generation: change to a BottomSheet, button to "open" the saved PDF

This commit is contained in:
Pierre-Yves Nicolas
2025-07-05 18:30:18 +02:00
parent 7b2e60ee14
commit 97b4d333d4
7 changed files with 322 additions and 190 deletions

View File

@@ -14,9 +14,11 @@
*/ */
package org.mydomain.myscan package org.mydomain.myscan
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
@@ -28,6 +30,7 @@ import androidx.activity.viewModels
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -40,6 +43,8 @@ import org.mydomain.myscan.view.DocumentScreen
import org.opencv.android.OpenCVLoader import org.opencv.android.OpenCVLoader
import java.io.File import java.io.File
private const val PDF_MIME_TYPE = "application/pdf"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -71,9 +76,10 @@ class MainActivity : ComponentActivity() {
startGeneration = viewModel::startPdfGeneration, startGeneration = viewModel::startPdfGeneration,
cancelGeneration = viewModel::cancelPdfGeneration, cancelGeneration = viewModel::cancelPdfGeneration,
setFilename = viewModel::setFilename, setFilename = viewModel::setFilename,
generatedPdfFlow = viewModel.generatedPdf, uiStateFlow = viewModel.pdfUiState,
sharePdf = { sharePdf(viewModel.getFinalPdf()) }, sharePdf = { sharePdf(viewModel.getFinalPdf()) },
savePdf = { savePdf(viewModel.getFinalPdf()) }, savePdf = { savePdf(viewModel.getFinalPdf(), viewModel) },
openPdf = { openPdf(viewModel.pdfUiState.value.savedFileUri) }
), ),
onStartNew = { onStartNew = {
viewModel.startNewDocument() viewModel.startNewDocument()
@@ -93,7 +99,7 @@ class MainActivity : ComponentActivity() {
val authority = "${applicationContext.packageName}.fileprovider" val authority = "${applicationContext.packageName}.fileprovider"
val fileUri = FileProvider.getUriForFile(this, authority, file) val fileUri = FileProvider.getUriForFile(this, authority, file)
val shareIntent = Intent(Intent.ACTION_SEND).apply { val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf" type = PDF_MIME_TYPE
putExtra(Intent.EXTRA_STREAM, fileUri) putExtra(Intent.EXTRA_STREAM, fileUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
@@ -107,7 +113,7 @@ class MainActivity : ComponentActivity() {
startActivity(chooser) startActivity(chooser)
} }
private fun savePdf(generatedPdf: GeneratedPdf?) { private fun savePdf(generatedPdf: GeneratedPdf?, viewModel: MainViewModel) {
if (generatedPdf == null) if (generatedPdf == null)
return return
val appScope = CoroutineScope(Dispatchers.IO) val appScope = CoroutineScope(Dispatchers.IO)
@@ -123,17 +129,13 @@ class MainActivity : ComponentActivity() {
val targetFile = File(downloadsDir, generatedFile.name) val targetFile = File(downloadsDir, generatedFile.name)
// TODO Handle case where the target file already exists (choose a unique name) // TODO Handle case where the target file already exists (choose a unique name)
generatedFile.copyTo(targetFile) generatedFile.copyTo(targetFile)
viewModel.markFileSaved(targetFile.toUri())
withContext(Dispatchers.Main) {
// TODO Display a link to the file
Toast.makeText(context, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
}
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile( MediaScannerConnection.scanFile(
context, context,
arrayOf(targetFile.absolutePath), arrayOf(targetFile.absolutePath),
arrayOf("application/pdf") arrayOf(PDF_MIME_TYPE)
) { _, _ -> continuation.resume(Unit) {} } ) { _, _ -> continuation.resume(Unit) {} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -145,6 +147,24 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun openPdf(fileUri: Uri?) {
if (fileUri == null) return
val uri = FileProvider.getUriForFile(
this,
"${applicationContext.packageName}.fileprovider",
fileUri.toFile()
)
val openIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, PDF_MIME_TYPE)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
try {
startActivity(Intent.createChooser(openIntent, "Open PDF"))
} catch (_: ActivityNotFoundException) {
Toast.makeText(this, "No app found to open PDF", Toast.LENGTH_SHORT).show()
}
}
private fun initLibraries() { private fun initLibraries() {
com.tom_roush.pdfbox.android.PDFBoxResourceLoader.init(applicationContext) com.tom_roush.pdfbox.android.PDFBoxResourceLoader.init(applicationContext)

View File

@@ -18,6 +18,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -32,8 +33,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.mydomain.myscan.ui.PdfGenerationUiState
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -208,7 +211,9 @@ class MainViewModel(
} }
private val _generatedPdf = MutableStateFlow<GeneratedPdf?>(null) private val _generatedPdf = MutableStateFlow<GeneratedPdf?>(null)
val generatedPdf: StateFlow<GeneratedPdf?> = _generatedPdf
private val _pdfUiState = MutableStateFlow(PdfGenerationUiState())
val pdfUiState: StateFlow<PdfGenerationUiState> = _pdfUiState.asStateFlow()
private var generationJob: Job? = null private var generationJob: Job? = null
private var desiredFilename: String = "" private var desiredFilename: String = ""
@@ -218,29 +223,56 @@ class MainViewModel(
} }
fun startPdfGeneration() { fun startPdfGeneration() {
if (_generatedPdf.value != null) return val currentState = _pdfUiState.value
if (currentState.isGenerating || currentState.generatedPdf != null) return
_pdfUiState.update { it.copy(isGenerating = true, errorMessage = null) }
generationJob = viewModelScope.launch { generationJob = viewModelScope.launch {
try {
val result = generatePdf() val result = generatePdf()
_generatedPdf.value = result _pdfUiState.update {
it.copy(
isGenerating = false,
generatedPdf = result
)
}
} catch (e: Exception) {
Log.e("MyScan", "PDF generation failed", e)
_pdfUiState.update {
it.copy(
isGenerating = false,
errorMessage = "PDF generation failed"
)
}
}
} }
} }
fun cancelPdfGeneration() { fun cancelPdfGeneration() {
generationJob?.cancel() generationJob?.cancel()
_generatedPdf.value = null _pdfUiState.value = PdfGenerationUiState()
} }
fun getFinalPdf(): GeneratedPdf? { fun getFinalPdf(): GeneratedPdf? {
val temp = _generatedPdf.value ?: return null val tempPdf = _pdfUiState.value.generatedPdf ?: return null
val tempFile = temp.uri.toFile() val tempFile = tempPdf.uri.toFile()
val newFile = File(tempFile.parentFile, desiredFilename) val newFile = File(tempFile.parentFile, desiredFilename)
if (tempFile.absolutePath != newFile.absolutePath) { if (tempFile.absolutePath != newFile.absolutePath) {
if (newFile.exists()) newFile.delete() if (newFile.exists()) newFile.delete()
val success = tempFile.renameTo(newFile) val success = tempFile.renameTo(newFile)
if (!success) return null if (!success) return null
_generatedPdf.value = GeneratedPdf(uri = newFile.toUri(), temp.sizeInBytes, temp.pageCount) _pdfUiState.update {
it.copy(generatedPdf = GeneratedPdf(
uri = newFile.toUri(), tempPdf.sizeInBytes, tempPdf.pageCount)
)
} }
return _generatedPdf.value }
return _pdfUiState.value.generatedPdf
}
fun markFileSaved(uri: Uri) {
_pdfUiState.update { it.copy(savedFileUri = uri) }
} }
} }
@@ -250,11 +282,13 @@ data class GeneratedPdf(
val pageCount: Int, val pageCount: Int,
) )
// TODO Move somewhere else: ViewModel should not depend on that
data class PdfGenerationActions( data class PdfGenerationActions(
val startGeneration: () -> Unit, val startGeneration: () -> Unit,
val cancelGeneration: () -> Unit, val cancelGeneration: () -> Unit,
val setFilename: (String) -> Unit, val setFilename: (String) -> Unit,
val generatedPdfFlow: StateFlow<GeneratedPdf?>, val uiStateFlow: StateFlow<PdfGenerationUiState>,// TODO is it ok to have that here?
val sharePdf: () -> Unit, val sharePdf: () -> Unit,
val savePdf: () -> Unit val savePdf: () -> Unit,
val openPdf: () -> Unit,
) )

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan.ui
import android.net.Uri
import org.mydomain.myscan.GeneratedPdf
data class PdfGenerationUiState(
val isGenerating: Boolean = false,
val generatedPdf: GeneratedPdf? = null,
val desiredFilename: String = "",
val savedFileUri: Uri? = null,
val errorMessage: String? = null
)

View File

@@ -67,6 +67,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
import org.mydomain.myscan.PdfGenerationActions import org.mydomain.myscan.PdfGenerationActions
import org.mydomain.myscan.ui.PdfGenerationUiState
import org.mydomain.myscan.ui.theme.MyScanTheme import org.mydomain.myscan.ui.theme.MyScanTheme
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -292,8 +293,8 @@ fun DocumentScreenPreview() {
toCameraScreen = {}, toCameraScreen = {},
pdfActions = PdfGenerationActions( pdfActions = PdfGenerationActions(
{}, {}, {}, {}, {}, {},
MutableStateFlow(null), MutableStateFlow(PdfGenerationUiState()),
{}, {}), {}, {}, {}),
onStartNew = {}, onStartNew = {},
onDeleteImage = { _ -> {} } onDeleteImage = { _ -> {} }
) )

View File

@@ -0,0 +1,215 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan.view
import android.content.Context
import android.text.format.Formatter
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import org.mydomain.myscan.GeneratedPdf
import org.mydomain.myscan.PdfGenerationActions
import org.mydomain.myscan.ui.PdfGenerationUiState
import org.mydomain.myscan.ui.theme.MyScanTheme
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PdfGenerationDialogWrapper(
onDismiss: () -> Unit,
pdfActions: PdfGenerationActions,
modifier: Modifier = Modifier,
) {
var filename by remember { mutableStateOf(defaultFilename()) }
val uiState by pdfActions.uiStateFlow.collectAsState()
LaunchedEffect(Unit) {
pdfActions.setFilename(filename)
pdfActions.startGeneration()
}
ModalBottomSheet(
onDismissRequest = onDismiss,
modifier = modifier
) {
PdfGenerationBottomSheet(
filename = filename,
onFilenameChange = {
filename = it
pdfActions.setFilename(it)
},
uiState = uiState,
onDismiss = {
pdfActions.cancelGeneration()
onDismiss()
},
onShare = { pdfActions.sharePdf() },
onSave = { pdfActions.savePdf() },
onOpen = { pdfActions.openPdf() },
)
}
}
// TODO Handle error in PDF generation
@Composable
fun PdfGenerationBottomSheet(
filename: String,
onFilenameChange: (String) -> Unit,
uiState: PdfGenerationUiState,
onDismiss: () -> Unit,
onShare: () -> Unit,
onSave: () -> Unit,
onOpen: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text("Generate PDF", style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = filename,
onValueChange = onFilenameChange,
label = { Text("Filename") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
val pdf = uiState.generatedPdf
if (uiState.isGenerating) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
} else if (pdf != null) {
val context = LocalContext.current
Text("${pdf.pageCount} pages · ${formatFileSize(pdf.sizeInBytes, context)}")
}
Spacer(Modifier.height(24.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = onDismiss,
modifier = Modifier.weight(1f)
) { Text("Close") }
OutlinedButton(
onClick = onShare,
enabled = pdf != null,
modifier = Modifier.weight(1f)
) { Text("Share") }
OutlinedButton(
onClick = onSave,
enabled = pdf != null,
modifier = Modifier.weight(1f)
) { Text("Save") }
}
if (uiState.savedFileUri != null) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "PDF saved to Downloads",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = onOpen) {
Icon(Icons.AutoMirrored.Filled.OpenInNew, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Open")
}
}
}
}
fun defaultFilename(): String {
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
return "scan_$timestamp.pdf"
}
fun formatFileSize(sizeInBytes: Long?, context: Context): String {
return if (sizeInBytes == null) "Unknown size"
else Formatter.formatShortFileSize(context, sizeInBytes)
}
@Preview(showBackground = true)
@Composable
fun PreviewPdfGenerationDialogDuringGeneration() {
MyScanTheme {
PdfGenerationBottomSheet(
filename = "scan_20250702_174042.pdf",
uiState = PdfGenerationUiState(isGenerating = true),
onFilenameChange = {},
onDismiss = {},
onShare = {},
onSave = {},
onOpen = {},
)
}
}
@Preview(showBackground = true)
@Composable
fun PreviewPdfGenerationDialogAfterGeneration() {
MyScanTheme {
PdfGenerationBottomSheet(
filename = "scan_20250702_174042.pdf",
uiState = PdfGenerationUiState(
isGenerating = false,
generatedPdf = GeneratedPdf("file://fake.pdf".toUri(), 42897L, 3)
),
onFilenameChange = {},
onDismiss = {},
onShare = {},
onSave = {},
onOpen = {},
)
}
}

View File

@@ -1,167 +0,0 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan.view
import android.content.Context
import android.text.format.Formatter
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import org.mydomain.myscan.GeneratedPdf
import org.mydomain.myscan.PdfGenerationActions
import org.mydomain.myscan.ui.theme.MyScanTheme
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable
fun PdfGenerationDialogWrapper(
onDismiss: () -> Unit,
pdfActions: PdfGenerationActions,
) {
var filename by remember { mutableStateOf(defaultFilename()) }
val generatedPdf by pdfActions.generatedPdfFlow.collectAsState()
LaunchedEffect(Unit) {
pdfActions.setFilename(filename)
pdfActions.startGeneration()
}
PdfGenerationDialog(
filename = filename,
onFilenameChange = {
filename = it
pdfActions.setFilename(it)
},
pdf = generatedPdf,
onDismiss = {
pdfActions.cancelGeneration()
onDismiss()
},
onShare = { pdfActions.sharePdf() },
onSave = { pdfActions.savePdf() },
)
}
// TODO Handle error in PDF generation
@Composable
fun PdfGenerationDialog(
filename: String,
onFilenameChange: (String) -> Unit,
pdf: GeneratedPdf?,
onDismiss: () -> Unit,
onShare: () -> Unit,
onSave: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Generate PDF") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = filename,
onValueChange = onFilenameChange,
label = { Text("Filename") },
modifier = Modifier.fillMaxWidth()
)
if (pdf == null) {
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
Spacer(Modifier.width(8.dp))
Text("Generating PDF…")
}
} else {
val context = LocalContext.current
Text("${pdf.pageCount} pages ${formatFileSize(pdf.sizeInBytes, context)}")
}
}
},
confirmButton = {
if (pdf != null) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = onShare) { Text("Share") }
TextButton(onClick = onSave) { Text("Save") }
}
}
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Close") }
}
)
}
fun defaultFilename(): String {
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
return "scan_$timestamp.pdf"
}
fun formatFileSize(sizeInBytes: Long?, context: Context): String {
return if (sizeInBytes == null) "Unknown size"
else Formatter.formatShortFileSize(context, sizeInBytes)
}
@Preview
@Composable
fun PreviewPdfGenerationDialogDuringGeneration() {
MyScanTheme {
PdfGenerationDialog(
filename = "scan_20250702_174042.pdf",
pdf = null,
onFilenameChange = {},
onDismiss = {},
onShare = {},
onSave = {},
)
}
}
@Preview
@Composable
fun PreviewPdfGenerationDialogAfterGeneration() {
MyScanTheme {
PdfGenerationDialog(
filename = "scan_20250702_174042.pdf",
pdf = GeneratedPdf("file://fake.pdf".toUri(), 42897L, 3),
onFilenameChange = {},
onDismiss = {},
onShare = {},
onSave = {},
)
}
}

View File

@@ -3,4 +3,7 @@
<cache-path <cache-path
name="pdfs" name="pdfs"
path="pdfs/" /> path="pdfs/" />
<external-path
name="external_files"
path="." />
</paths> </paths>