New dialog for PDF generation
This commit is contained in:
@@ -14,9 +14,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.mydomain.myscan
|
package org.mydomain.myscan
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
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
|
||||||
@@ -25,18 +25,15 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||||
import org.mydomain.myscan.view.CameraScreen
|
import org.mydomain.myscan.view.CameraScreen
|
||||||
import org.mydomain.myscan.view.DocumentScreen
|
import org.mydomain.myscan.view.DocumentScreen
|
||||||
import org.opencv.android.OpenCVLoader
|
import org.opencv.android.OpenCVLoader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@@ -49,7 +46,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
||||||
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
||||||
val pageIds by viewModel.pageIds.collectAsStateWithLifecycle()
|
val pageIds by viewModel.pageIds.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
MyScanTheme {
|
MyScanTheme {
|
||||||
when (val screen = currentScreen) {
|
when (val screen = currentScreen) {
|
||||||
is Screen.Camera -> {
|
is Screen.Camera -> {
|
||||||
@@ -66,8 +62,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
initialPage = screen.initialPage,
|
initialPage = screen.initialPage,
|
||||||
imageLoader = { id -> viewModel.getBitmap(id) },
|
imageLoader = { id -> viewModel.getBitmap(id) },
|
||||||
toCameraScreen = { viewModel.navigateTo(Screen.Camera) },
|
toCameraScreen = { viewModel.navigateTo(Screen.Camera) },
|
||||||
onSavePressed = savePdf(viewModel, context),
|
// TODO Save and share files with the filename chosen by the user
|
||||||
onSharePressed = sharePdf(viewModel, context),
|
pdfActions = PdfGenerationActions(
|
||||||
|
generatePdf = viewModel::generatePdf,
|
||||||
|
onShare = { uri -> sharePdf(uri) },
|
||||||
|
onSave = { uri -> savePdf(uri) },
|
||||||
|
onOpen = { uri -> savePdf(uri) /* TODO Open */}
|
||||||
|
),
|
||||||
onStartNew = {
|
onStartNew = {
|
||||||
viewModel.startNewDocument()
|
viewModel.startNewDocument()
|
||||||
viewModel.navigateTo(Screen.Camera) },
|
viewModel.navigateTo(Screen.Camera) },
|
||||||
@@ -79,57 +80,32 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sharePdf(
|
private fun sharePdf(fileUri: Uri) {
|
||||||
viewModel: MainViewModel,
|
val fileUri = FileProvider.getUriForFile(
|
||||||
context: Context
|
this,
|
||||||
): () -> Unit = {
|
"${applicationContext.packageName}.fileprovider",
|
||||||
val outputDir = File(cacheDir, "pdfs").apply { mkdirs() }
|
fileUri.toFile()
|
||||||
val outputFile = File(outputDir, "scan_${System.currentTimeMillis()}.pdf")
|
)
|
||||||
var success = true
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
try {
|
type = "application/pdf"
|
||||||
val fileOutputStream = FileOutputStream(outputFile)
|
putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||||
viewModel.createPdf(fileOutputStream)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
} catch (_: IOException) {
|
|
||||||
Toast.makeText(context, "Failed to share PDF", Toast.LENGTH_SHORT).show()
|
|
||||||
success = false
|
|
||||||
}
|
|
||||||
if (success) {
|
|
||||||
val uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${applicationContext.packageName}.fileprovider",
|
|
||||||
outputFile
|
|
||||||
)
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "application/pdf"
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
startActivity(Intent.createChooser(shareIntent, "Share PDF"))
|
|
||||||
}
|
}
|
||||||
|
startActivity(Intent.createChooser(shareIntent, "Share PDF"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun savePdf(
|
private fun savePdf(fileUri: Uri) {
|
||||||
viewModel: MainViewModel,
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
context: Context
|
if (!downloadsDir.exists()) {
|
||||||
): () -> Unit = {
|
downloadsDir.mkdirs()
|
||||||
try {
|
|
||||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
|
||||||
val file = File(downloadsDir, "scan_${System.currentTimeMillis()}.pdf")
|
|
||||||
val outputStream = FileOutputStream(file)
|
|
||||||
viewModel.createPdf(outputStream)
|
|
||||||
outputStream.flush()
|
|
||||||
outputStream.close()
|
|
||||||
|
|
||||||
MediaScannerConnection.scanFile(
|
|
||||||
context, arrayOf(file.absolutePath), arrayOf("application/pdf"), null
|
|
||||||
)
|
|
||||||
|
|
||||||
Toast.makeText(context, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MyScan", "Failed to save PDF", e)
|
|
||||||
Toast.makeText(context, "Failed to save PDF", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
val generatedFile = fileUri.toFile()
|
||||||
|
val targetFile = File(downloadsDir, generatedFile.name)
|
||||||
|
generatedFile.copyTo(targetFile)
|
||||||
|
MediaScannerConnection.scanFile(
|
||||||
|
this, arrayOf(targetFile.absolutePath), arrayOf("application/pdf"), null
|
||||||
|
)
|
||||||
|
Toast.makeText(this, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initLibraries() {
|
private fun initLibraries() {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ package org.mydomain.myscan
|
|||||||
import android.content.Context
|
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 androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -31,18 +33,25 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val imageSegmentationService: ImageSegmentationService,
|
private val imageSegmentationService: ImageSegmentationService,
|
||||||
private val imageRepository: ImageRepository,
|
private val imageRepository: ImageRepository,
|
||||||
|
private val pdfDir: File,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||||
return MainViewModel(ImageSegmentationService(context), ImageRepository(context.filesDir)) as T
|
return MainViewModel(
|
||||||
|
ImageSegmentationService(context),
|
||||||
|
ImageRepository(context.filesDir),
|
||||||
|
File(context.cacheDir, "pdfs"),
|
||||||
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,4 +200,26 @@ class MainViewModel(
|
|||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
writePdfFromJpegs(jpegs, outputStream)
|
writePdfFromJpegs(jpegs, outputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) {
|
||||||
|
val pageCount = imageRepository.imageIds().size
|
||||||
|
val file = File(pdfDir,"${System.currentTimeMillis()}.pdf")
|
||||||
|
createPdf(FileOutputStream(file))
|
||||||
|
val sizeBytes = file.length()
|
||||||
|
val uri = file.toUri()
|
||||||
|
return@withContext GeneratedPdf(uri, sizeBytes, pageCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GeneratedPdf(
|
||||||
|
val uri: Uri,
|
||||||
|
val sizeInBytes: Long,
|
||||||
|
val pageCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PdfGenerationActions(
|
||||||
|
val generatePdf: suspend () -> GeneratedPdf?,
|
||||||
|
val onShare: (Uri) -> Unit,
|
||||||
|
val onSave: (Uri) -> Unit,
|
||||||
|
val onOpen: (Uri) -> Unit
|
||||||
|
)
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.BottomAppBar
|
import androidx.compose.material3.BottomAppBar
|
||||||
@@ -66,6 +65,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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.ui.theme.MyScanTheme
|
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -75,13 +75,13 @@ fun DocumentScreen(
|
|||||||
initialPage: Int,
|
initialPage: Int,
|
||||||
imageLoader: (String) -> Bitmap?,
|
imageLoader: (String) -> Bitmap?,
|
||||||
toCameraScreen: () -> Unit,
|
toCameraScreen: () -> Unit,
|
||||||
onSavePressed: () -> Unit,
|
pdfActions: PdfGenerationActions,
|
||||||
onSharePressed: () -> Unit,
|
|
||||||
onStartNew: () -> Unit,
|
onStartNew: () -> Unit,
|
||||||
onDeleteImage: (String) -> Unit,
|
onDeleteImage: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
// TODO Check how often images are loaded
|
// TODO Check how often images are loaded
|
||||||
var showDialog = rememberSaveable { mutableStateOf(false) }
|
val showNewDocDialog = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val showPdfDialog = rememberSaveable { mutableStateOf(false) }
|
||||||
val currentPageIndex = rememberSaveable { mutableIntStateOf(initialPage) }
|
val currentPageIndex = rememberSaveable { mutableIntStateOf(initialPage) }
|
||||||
if (currentPageIndex.intValue >= pageIds.size) {
|
if (currentPageIndex.intValue >= pageIds.size) {
|
||||||
currentPageIndex.intValue = pageIds.size - 1
|
currentPageIndex.intValue = pageIds.size - 1
|
||||||
@@ -111,23 +111,17 @@ fun DocumentScreen(
|
|||||||
BottomAppBar(
|
BottomAppBar(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
actions = {
|
actions = {
|
||||||
Button(onClick = onSharePressed) {
|
Button(onClick = { showPdfDialog.value = true }) {
|
||||||
Icon(Icons.Default.Share, contentDescription = "Share")
|
Icon(Icons.Default.PictureAsPdf, contentDescription = "Generate PDF")
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Share")
|
Text("Generate PDF")
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
|
||||||
Button(onClick = onSavePressed) {
|
|
||||||
Icon(Icons.Default.Download, contentDescription = "Save")
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text("Save")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
MyIconButton(
|
MyIconButton(
|
||||||
icon = Icons.Default.RestartAlt,
|
icon = Icons.Default.RestartAlt,
|
||||||
contentDescription = "Restart",
|
contentDescription = "Restart",
|
||||||
onClick = { showDialog.value = true },
|
onClick = { showNewDocDialog.value = true },
|
||||||
modifier = Modifier.padding(vertical = 8.dp)
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -136,8 +130,14 @@ fun DocumentScreen(
|
|||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, padding)
|
DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, padding)
|
||||||
if (showDialog.value) {
|
if (showNewDocDialog.value) {
|
||||||
NewDocumentDialog(onConfirm = onStartNew, showDialog)
|
NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog)
|
||||||
|
}
|
||||||
|
if (showPdfDialog.value) {
|
||||||
|
PdfGenerationDialogWrapper(
|
||||||
|
onDismiss = { showPdfDialog.value = false },
|
||||||
|
pdfActions = pdfActions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,8 +289,7 @@ fun DocumentScreenPreview() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
toCameraScreen = {},
|
toCameraScreen = {},
|
||||||
onSavePressed = {},
|
pdfActions = PdfGenerationActions({ null }, {}, {}, {}),
|
||||||
onSharePressed = {},
|
|
||||||
onStartNew = {},
|
onStartNew = {},
|
||||||
onDeleteImage = { _ -> {} }
|
onDeleteImage = { _ -> {} }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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 kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
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()) }
|
||||||
|
var isGenerating by remember { mutableStateOf(true) }
|
||||||
|
var pdf by remember { mutableStateOf<GeneratedPdf?>(null) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var job by remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
job = coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
val result = pdfActions.generatePdf()
|
||||||
|
pdf = result
|
||||||
|
isGenerating = false
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
// Cancelled
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Error
|
||||||
|
isGenerating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfGenerationDialog(
|
||||||
|
filename = filename,
|
||||||
|
onFilenameChange = { filename = it },
|
||||||
|
isGenerating = isGenerating,
|
||||||
|
pdf = pdf,
|
||||||
|
onDismiss = {
|
||||||
|
job?.cancel()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
onShare = { pdf?.uri?.let(pdfActions.onShare) },
|
||||||
|
onSave = { pdf?.uri?.let(pdfActions.onSave) },
|
||||||
|
onOpen = { pdf?.uri?.let(pdfActions.onOpen) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PdfGenerationDialog(
|
||||||
|
filename: String,
|
||||||
|
onFilenameChange: (String) -> Unit,
|
||||||
|
isGenerating: Boolean,
|
||||||
|
pdf: GeneratedPdf?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onShare: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onOpen: () -> 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 (isGenerating) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Generating PDF…")
|
||||||
|
}
|
||||||
|
} else if (pdf != null) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Text("${pdf.pageCount} pages – ${formatFileSize(pdf.sizeInBytes, context)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO 4 buttons (counting dismissButton): that's too many
|
||||||
|
confirmButton = {
|
||||||
|
if (!isGenerating && pdf != null) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
TextButton(onClick = onShare) { Text("Share") }
|
||||||
|
TextButton(onClick = onSave) { Text("Save") }
|
||||||
|
TextButton(onClick = onOpen) { Text("Open") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
isGenerating = true,
|
||||||
|
pdf = null,
|
||||||
|
onFilenameChange = {},
|
||||||
|
onDismiss = {},
|
||||||
|
onShare = {},
|
||||||
|
onSave = {},
|
||||||
|
onOpen = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewPdfGenerationDialogAfterGeneration() {
|
||||||
|
MyScanTheme {
|
||||||
|
PdfGenerationDialog(
|
||||||
|
filename = "scan_20250702_174042.pdf",
|
||||||
|
isGenerating = false,
|
||||||
|
pdf = GeneratedPdf("file://fake.pdf".toUri(), 42897L, 3),
|
||||||
|
onFilenameChange = {},
|
||||||
|
onDismiss = {},
|
||||||
|
onShare = {},
|
||||||
|
onSave = {},
|
||||||
|
onOpen = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user