Adjust Android document actions
This commit is contained in:
@@ -18,8 +18,8 @@ android {
|
||||
applicationId "net.linkedcorp.linkedsign"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 10102
|
||||
versionName "1.1.2"
|
||||
versionCode 10103
|
||||
versionName "1.1.3"
|
||||
|
||||
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.linkedcorp.linkedsign.ui
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
@@ -8,6 +9,7 @@ import android.hardware.biometrics.BiometricPrompt
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
@@ -15,7 +17,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@@ -48,6 +49,7 @@ import androidx.compose.material.icons.outlined.Badge
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Dashboard
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Draw
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Fingerprint
|
||||
@@ -116,6 +118,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -890,6 +893,7 @@ private fun DocumentsScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
var deleteTarget by remember { mutableStateOf<UnifiedDocumentItem?>(null) }
|
||||
var showSignedDocuments by rememberSaveable { mutableStateOf(true) }
|
||||
var documentPreview by remember { mutableStateOf<PdfPreviewState?>(null) }
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
uri ?: return@rememberLauncherForActivityResult
|
||||
try {
|
||||
@@ -963,26 +967,56 @@ private fun DocumentsScreen(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
onSelect = if (item.isSigned) null else ({ onSelectDocumentToSign(item.documentId) }),
|
||||
onView = {
|
||||
onPreview = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
if (item.isSigned && item.signedSignatureId != null) {
|
||||
openSecurePdf(
|
||||
val token = accessToken()
|
||||
val file = if (item.isSigned && item.signedSignatureId != null) {
|
||||
downloadSecuredFileToCache(
|
||||
appContext,
|
||||
"${BuildConfig.API_BASE_URL}/signatures/${item.signedSignatureId}/download",
|
||||
accessToken(),
|
||||
token,
|
||||
item.signedFilename ?: "firmado.pdf"
|
||||
)
|
||||
} else {
|
||||
openSecurePdf(
|
||||
downloadSecuredFileToCache(
|
||||
appContext,
|
||||
"${BuildConfig.API_BASE_URL}/documents/${item.documentId}/download",
|
||||
accessToken(),
|
||||
token,
|
||||
item.originalFilename
|
||||
)
|
||||
}
|
||||
renderPdfPreview(file, 0)
|
||||
}.onSuccess {
|
||||
documentPreview = it
|
||||
}.onFailure {
|
||||
Toast.makeText(appContext, it.message ?: "No se pudo abrir el PDF.", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(appContext, it.message ?: "No se pudo abrir el PDF dentro de la app.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDownload = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
val token = accessToken()
|
||||
if (item.isSigned && item.signedSignatureId != null) {
|
||||
downloadFile(
|
||||
appContext,
|
||||
"${BuildConfig.API_BASE_URL}/signatures/${item.signedSignatureId}/download",
|
||||
token,
|
||||
item.signedFilename ?: "firmado.pdf"
|
||||
)
|
||||
} else {
|
||||
downloadFile(
|
||||
appContext,
|
||||
"${BuildConfig.API_BASE_URL}/documents/${item.documentId}/download",
|
||||
token,
|
||||
item.originalFilename
|
||||
)
|
||||
}
|
||||
}.onSuccess {
|
||||
Toast.makeText(appContext, "Descarga iniciada.", Toast.LENGTH_SHORT).show()
|
||||
}.onFailure {
|
||||
Toast.makeText(appContext, it.message ?: "No se pudo descargar el PDF.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1022,6 +1056,22 @@ private fun DocumentsScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
documentPreview?.let { preview ->
|
||||
PdfPreviewDialog(
|
||||
preview = preview,
|
||||
onDismiss = { documentPreview = null },
|
||||
onPageChange = { page ->
|
||||
scope.launch {
|
||||
runCatching { renderPdfPreview(preview.file, page) }
|
||||
.onSuccess { documentPreview = it }
|
||||
.onFailure {
|
||||
Toast.makeText(appContext, it.message ?: "No se pudo cargar la página.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1465,7 +1515,14 @@ private fun SectionCard(title: String, content: @Composable ColumnScope.() -> Un
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DocumentRow(title: String, subtitle: String, onSelect: (() -> Unit)? = null, onView: () -> Unit, onDelete: () -> Unit) {
|
||||
private fun DocumentRow(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onSelect: (() -> Unit)? = null,
|
||||
onPreview: () -> Unit,
|
||||
onDownload: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val isSigned = subtitle.contains("firmad", ignoreCase = true) || subtitle.contains("signed", ignoreCase = true)
|
||||
val compactSubtitle = subtitle
|
||||
.replace("signed", "", ignoreCase = true)
|
||||
@@ -1525,14 +1582,16 @@ private fun DocumentRow(title: String, subtitle: String, onSelect: (() -> Unit)?
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (isSigned) Icons.Outlined.Verified else Icons.Outlined.PictureAsPdf,
|
||||
contentDescription = null,
|
||||
tint = if (isSigned) Success else Warning,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
IconButton(onClick = onView, modifier = Modifier.size(28.dp)) {
|
||||
Icon(Icons.Outlined.Visibility, contentDescription = "Ver documento", tint = Brand, modifier = Modifier.size(17.dp))
|
||||
IconButton(onClick = onPreview, modifier = Modifier.size(28.dp)) {
|
||||
Icon(
|
||||
if (isSigned) Icons.Outlined.Verified else Icons.Outlined.PictureAsPdf,
|
||||
contentDescription = "Ver PDF dentro de la app",
|
||||
tint = if (isSigned) Success else Warning,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDownload, modifier = Modifier.size(28.dp)) {
|
||||
Icon(Icons.Outlined.Download, contentDescription = "Descargar documento", tint = Brand, modifier = Modifier.size(17.dp))
|
||||
}
|
||||
IconButton(onClick = onDelete, modifier = Modifier.size(28.dp)) {
|
||||
Icon(Icons.Outlined.Delete, contentDescription = "Eliminar documento", tint = Danger, modifier = Modifier.size(17.dp))
|
||||
@@ -1544,6 +1603,64 @@ private fun DocumentRow(title: String, subtitle: String, onSelect: (() -> Unit)?
|
||||
Spacer(Modifier.height(2.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfPreviewDialog(
|
||||
preview: PdfPreviewState,
|
||||
onDismiss: () -> Unit,
|
||||
onPageChange: (Int) -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color.White,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Vista local del PDF", color = BrandDark, fontSize = AppFontSizes.lg, fontWeight = FontWeight.SemiBold)
|
||||
TextButton(onClick = onDismiss) { Text("Cerrar") }
|
||||
}
|
||||
Text(
|
||||
"Página ${preview.pageIndex + 1} / ${preview.pageCount}",
|
||||
color = TextMuted,
|
||||
fontSize = AppFontSizes.sm
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFFF3F6FC), RoundedCornerShape(16.dp))
|
||||
.padding(10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
bitmap = preview.bitmap,
|
||||
contentDescription = "Vista local PDF",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
OutlinedActionButton("‹", Icons.Outlined.Description, modifier = Modifier.weight(1f)) {
|
||||
if (preview.pageIndex > 0) onPageChange(preview.pageIndex - 1)
|
||||
}
|
||||
OutlinedActionButton("›", Icons.Outlined.Description, modifier = Modifier.weight(1f)) {
|
||||
if (preview.pageIndex < preview.pageCount - 1) onPageChange(preview.pageIndex + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CertificateRow(certificate: CertificateItem) {
|
||||
val isActive = (certificate.isActive ?: 0) == 1
|
||||
@@ -1824,16 +1941,19 @@ private fun queryFileName(context: Context, uri: Uri): String? {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openSecurePdf(context: Context, url: String, accessToken: String?, fileName: String) {
|
||||
val targetFile = downloadSecuredFileToCache(context, url, accessToken, fileName)
|
||||
private fun downloadFile(context: Context, url: String, accessToken: String?, fileName: String) {
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
.setTitle(fileName)
|
||||
.setDescription("Descargando archivo de LinkedSign")
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
||||
|
||||
val uri = FileProvider.getUriForFile(context, "net.linkedcorp.linkedsign.fileprovider", targetFile)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/pdf")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (!accessToken.isNullOrBlank()) {
|
||||
request.addRequestHeader("Authorization", "Bearer $accessToken")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
||||
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
manager.enqueue(request)
|
||||
}
|
||||
|
||||
private suspend fun downloadSecuredFileToCache(
|
||||
|
||||
Reference in New Issue
Block a user