Adjust Android document actions
This commit is contained in:
+6
-1
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
## Versión actual
|
## Versión actual
|
||||||
|
|
||||||
- Versión base: `1.1.2`
|
- Versión base: `1.1.3`
|
||||||
|
|
||||||
## Historial
|
## Historial
|
||||||
|
|
||||||
|
### 1.1.3 - 2026-04-23
|
||||||
|
|
||||||
|
- Se cambia en Android el botón de ojo por descarga real del PDF.
|
||||||
|
- El botón con ícono PDF ahora abre el documento localmente dentro de la app.
|
||||||
|
|
||||||
### 1.1.2 - 2026-04-23
|
### 1.1.2 - 2026-04-23
|
||||||
|
|
||||||
- Se separa en Android la vista de documentos firmados y originales.
|
- Se separa en Android la vista de documentos firmados y originales.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId "net.linkedcorp.linkedsign"
|
applicationId "net.linkedcorp.linkedsign"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
versionCode 10102
|
versionCode 10103
|
||||||
versionName "1.1.2"
|
versionName "1.1.3"
|
||||||
|
|
||||||
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.linkedcorp.linkedsign.ui
|
package net.linkedcorp.linkedsign.ui
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -8,6 +9,7 @@ import android.hardware.biometrics.BiometricPrompt
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
|
import android.os.Environment
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@@ -15,7 +17,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
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.CheckCircle
|
||||||
import androidx.compose.material.icons.outlined.Dashboard
|
import androidx.compose.material.icons.outlined.Dashboard
|
||||||
import androidx.compose.material.icons.outlined.Description
|
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.Draw
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Fingerprint
|
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.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -890,6 +893,7 @@ private fun DocumentsScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var deleteTarget by remember { mutableStateOf<UnifiedDocumentItem?>(null) }
|
var deleteTarget by remember { mutableStateOf<UnifiedDocumentItem?>(null) }
|
||||||
var showSignedDocuments by rememberSaveable { mutableStateOf(true) }
|
var showSignedDocuments by rememberSaveable { mutableStateOf(true) }
|
||||||
|
var documentPreview by remember { mutableStateOf<PdfPreviewState?>(null) }
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||||
uri ?: return@rememberLauncherForActivityResult
|
uri ?: return@rememberLauncherForActivityResult
|
||||||
try {
|
try {
|
||||||
@@ -963,26 +967,56 @@ private fun DocumentsScreen(
|
|||||||
title = item.title,
|
title = item.title,
|
||||||
subtitle = item.subtitle,
|
subtitle = item.subtitle,
|
||||||
onSelect = if (item.isSigned) null else ({ onSelectDocumentToSign(item.documentId) }),
|
onSelect = if (item.isSigned) null else ({ onSelectDocumentToSign(item.documentId) }),
|
||||||
onView = {
|
onPreview = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (item.isSigned && item.signedSignatureId != null) {
|
val token = accessToken()
|
||||||
openSecurePdf(
|
val file = if (item.isSigned && item.signedSignatureId != null) {
|
||||||
|
downloadSecuredFileToCache(
|
||||||
appContext,
|
appContext,
|
||||||
"${BuildConfig.API_BASE_URL}/signatures/${item.signedSignatureId}/download",
|
"${BuildConfig.API_BASE_URL}/signatures/${item.signedSignatureId}/download",
|
||||||
accessToken(),
|
token,
|
||||||
item.signedFilename ?: "firmado.pdf"
|
item.signedFilename ?: "firmado.pdf"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
openSecurePdf(
|
downloadSecuredFileToCache(
|
||||||
appContext,
|
appContext,
|
||||||
"${BuildConfig.API_BASE_URL}/documents/${item.documentId}/download",
|
"${BuildConfig.API_BASE_URL}/documents/${item.documentId}/download",
|
||||||
accessToken(),
|
token,
|
||||||
item.originalFilename
|
item.originalFilename
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
renderPdfPreview(file, 0)
|
||||||
|
}.onSuccess {
|
||||||
|
documentPreview = it
|
||||||
}.onFailure {
|
}.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
|
@Composable
|
||||||
@@ -1465,7 +1515,14 @@ private fun SectionCard(title: String, content: @Composable ColumnScope.() -> Un
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 isSigned = subtitle.contains("firmad", ignoreCase = true) || subtitle.contains("signed", ignoreCase = true)
|
||||||
val compactSubtitle = subtitle
|
val compactSubtitle = subtitle
|
||||||
.replace("signed", "", ignoreCase = true)
|
.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) {
|
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onPreview, modifier = Modifier.size(28.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
if (isSigned) Icons.Outlined.Verified else Icons.Outlined.PictureAsPdf,
|
if (isSigned) Icons.Outlined.Verified else Icons.Outlined.PictureAsPdf,
|
||||||
contentDescription = null,
|
contentDescription = "Ver PDF dentro de la app",
|
||||||
tint = if (isSigned) Success else Warning,
|
tint = if (isSigned) Success else Warning,
|
||||||
modifier = Modifier.size(18.dp)
|
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 = 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)) {
|
IconButton(onClick = onDelete, modifier = Modifier.size(28.dp)) {
|
||||||
Icon(Icons.Outlined.Delete, contentDescription = "Eliminar documento", tint = Danger, modifier = Modifier.size(17.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))
|
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
|
@Composable
|
||||||
private fun CertificateRow(certificate: CertificateItem) {
|
private fun CertificateRow(certificate: CertificateItem) {
|
||||||
val isActive = (certificate.isActive ?: 0) == 1
|
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) {
|
private fun downloadFile(context: Context, url: String, accessToken: String?, fileName: String) {
|
||||||
val targetFile = downloadSecuredFileToCache(context, url, accessToken, fileName)
|
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)
|
if (!accessToken.isNullOrBlank()) {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
request.addRequestHeader("Authorization", "Bearer $accessToken")
|
||||||
setDataAndType(uri, "application/pdf")
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
|
||||||
|
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
manager.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadSecuredFileToCache(
|
private suspend fun downloadSecuredFileToCache(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkedsign-backend",
|
"name": "linkedsign-backend",
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Initial backend for LinkedSign",
|
"description": "Initial backend for LinkedSign",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user