PDF generation: change to a BottomSheet, button to "open" the saved PDF
This commit is contained in:
@@ -14,9 +14,11 @@
|
||||
*/
|
||||
package org.mydomain.myscan
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
@@ -28,6 +30,7 @@ import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -40,6 +43,8 @@ import org.mydomain.myscan.view.DocumentScreen
|
||||
import org.opencv.android.OpenCVLoader
|
||||
import java.io.File
|
||||
|
||||
private const val PDF_MIME_TYPE = "application/pdf"
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -71,9 +76,10 @@ class MainActivity : ComponentActivity() {
|
||||
startGeneration = viewModel::startPdfGeneration,
|
||||
cancelGeneration = viewModel::cancelPdfGeneration,
|
||||
setFilename = viewModel::setFilename,
|
||||
generatedPdfFlow = viewModel.generatedPdf,
|
||||
uiStateFlow = viewModel.pdfUiState,
|
||||
sharePdf = { sharePdf(viewModel.getFinalPdf()) },
|
||||
savePdf = { savePdf(viewModel.getFinalPdf()) },
|
||||
savePdf = { savePdf(viewModel.getFinalPdf(), viewModel) },
|
||||
openPdf = { openPdf(viewModel.pdfUiState.value.savedFileUri) }
|
||||
),
|
||||
onStartNew = {
|
||||
viewModel.startNewDocument()
|
||||
@@ -93,7 +99,7 @@ class MainActivity : ComponentActivity() {
|
||||
val authority = "${applicationContext.packageName}.fileprovider"
|
||||
val fileUri = FileProvider.getUriForFile(this, authority, file)
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/pdf"
|
||||
type = PDF_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
@@ -107,7 +113,7 @@ class MainActivity : ComponentActivity() {
|
||||
startActivity(chooser)
|
||||
}
|
||||
|
||||
private fun savePdf(generatedPdf: GeneratedPdf?) {
|
||||
private fun savePdf(generatedPdf: GeneratedPdf?, viewModel: MainViewModel) {
|
||||
if (generatedPdf == null)
|
||||
return
|
||||
val appScope = CoroutineScope(Dispatchers.IO)
|
||||
@@ -123,17 +129,13 @@ class MainActivity : ComponentActivity() {
|
||||
val targetFile = File(downloadsDir, generatedFile.name)
|
||||
// TODO Handle case where the target file already exists (choose a unique name)
|
||||
generatedFile.copyTo(targetFile)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// TODO Display a link to the file
|
||||
Toast.makeText(context, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
viewModel.markFileSaved(targetFile.toUri())
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
MediaScannerConnection.scanFile(
|
||||
context,
|
||||
arrayOf(targetFile.absolutePath),
|
||||
arrayOf("application/pdf")
|
||||
arrayOf(PDF_MIME_TYPE)
|
||||
) { _, _ -> continuation.resume(Unit) {} }
|
||||
}
|
||||
} 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() {
|
||||
com.tom_roush.pdfbox.android.PDFBoxResourceLoader.init(applicationContext)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
@@ -32,8 +33,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mydomain.myscan.ui.PdfGenerationUiState
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -208,7 +211,9 @@ class MainViewModel(
|
||||
}
|
||||
|
||||
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 desiredFilename: String = ""
|
||||
@@ -218,29 +223,56 @@ class MainViewModel(
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
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() {
|
||||
generationJob?.cancel()
|
||||
_generatedPdf.value = null
|
||||
_pdfUiState.value = PdfGenerationUiState()
|
||||
}
|
||||
|
||||
fun getFinalPdf(): GeneratedPdf? {
|
||||
val temp = _generatedPdf.value ?: return null
|
||||
val tempFile = temp.uri.toFile()
|
||||
val tempPdf = _pdfUiState.value.generatedPdf ?: return null
|
||||
val tempFile = tempPdf.uri.toFile()
|
||||
val newFile = File(tempFile.parentFile, desiredFilename)
|
||||
if (tempFile.absolutePath != newFile.absolutePath) {
|
||||
if (newFile.exists()) newFile.delete()
|
||||
val success = tempFile.renameTo(newFile)
|
||||
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,
|
||||
)
|
||||
|
||||
// TODO Move somewhere else: ViewModel should not depend on that
|
||||
data class PdfGenerationActions(
|
||||
val startGeneration: () -> Unit,
|
||||
val cancelGeneration: () -> 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 savePdf: () -> Unit
|
||||
val savePdf: () -> Unit,
|
||||
val openPdf: () -> Unit,
|
||||
)
|
||||
|
||||
26
app/src/main/java/org/mydomain/myscan/ui/UiState.kt
Normal file
26
app/src/main/java/org/mydomain/myscan/ui/UiState.kt
Normal 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
|
||||
)
|
||||
@@ -67,6 +67,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import net.engawapg.lib.zoomable.rememberZoomState
|
||||
import net.engawapg.lib.zoomable.zoomable
|
||||
import org.mydomain.myscan.PdfGenerationActions
|
||||
import org.mydomain.myscan.ui.PdfGenerationUiState
|
||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -292,8 +293,8 @@ fun DocumentScreenPreview() {
|
||||
toCameraScreen = {},
|
||||
pdfActions = PdfGenerationActions(
|
||||
{}, {}, {},
|
||||
MutableStateFlow(null),
|
||||
{}, {}),
|
||||
MutableStateFlow(PdfGenerationUiState()),
|
||||
{}, {}, {}),
|
||||
onStartNew = {},
|
||||
onDeleteImage = { _ -> {} }
|
||||
)
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,7 @@
|
||||
<cache-path
|
||||
name="pdfs"
|
||||
path="pdfs/" />
|
||||
<external-path
|
||||
name="external_files"
|
||||
path="." />
|
||||
</paths>
|
||||
|
||||
Reference in New Issue
Block a user