Beta 1.0 Release #11
@@ -0,0 +1,72 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Übergibt die fertige PDF-Datei selbst (statt in JPEG-Seiten umgewandelt, s.
|
||||
* [PdfToImagesConverter]/[PdfImageSharer]) an den Android-"Senden an"-Dialog
|
||||
* (`ACTION_SEND`, `application/pdf`) - Änderungswunsch: Nutzer soll beim Antippen des
|
||||
* Teilen-Buttons wählen können, ob als JPG ODER als PDF geteilt wird.
|
||||
*
|
||||
* Bewusst getrennt von [PdfOpener] (öffnet dieselbe Datei stattdessen per `ACTION_VIEW` in
|
||||
* einem PDF-Reader) - unterschiedliche Nutzerabsicht (ansehen vs. weitergeben), auch wenn
|
||||
* beide dieselbe Datei über denselben FileProvider-Pfad freigeben.
|
||||
*/
|
||||
interface PdfSharer {
|
||||
fun share(pdfFile: File): PdfShareResult
|
||||
}
|
||||
|
||||
sealed interface PdfShareResult {
|
||||
data object Success : PdfShareResult
|
||||
|
||||
sealed interface Failure : PdfShareResult {
|
||||
data class UriResolutionFailed(val message: String) : Failure
|
||||
data object NoAppAvailable : Failure
|
||||
data class Unexpected(val message: String) : Failure
|
||||
}
|
||||
}
|
||||
|
||||
fun PdfShareResult.Failure.toUserMessage(): String = when (this) {
|
||||
is PdfShareResult.Failure.UriResolutionFailed ->
|
||||
"Die PDF-Datei konnte nicht zum Versenden freigegeben werden."
|
||||
PdfShareResult.Failure.NoAppAvailable ->
|
||||
"Auf diesem Gerät ist keine App zum Versenden von PDFs installiert."
|
||||
is PdfShareResult.Failure.Unexpected ->
|
||||
"Die PDF-Datei konnte nicht versendet werden."
|
||||
}
|
||||
|
||||
class AndroidPdfSharer(private val context: Context) : PdfSharer {
|
||||
|
||||
override fun share(pdfFile: File): PdfShareResult {
|
||||
val authority = "${context.packageName}.fileprovider"
|
||||
val uri = try {
|
||||
FileProvider.getUriForFile(context, authority, pdfFile)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return PdfShareResult.Failure.UriResolutionFailed(e.message ?: "Uri konnte nicht erzeugt werden.")
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/pdf"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooser = Intent.createChooser(intent, "PDF senden an")
|
||||
|
||||
return try {
|
||||
context.startActivity(chooser)
|
||||
PdfShareResult.Success
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Bewusst KEIN vorheriges intent.resolveActivity(...) - s. Begründung in
|
||||
// AndroidPdfOpener.open() (Package-Visibility-Einschränkungen ab Android 11).
|
||||
PdfShareResult.Failure.NoAppAvailable
|
||||
} catch (e: SecurityException) {
|
||||
PdfShareResult.Failure.Unexpected(e.message ?: "Zugriff auf die PDF-Datei wurde verweigert.")
|
||||
} catch (e: Exception) {
|
||||
PdfShareResult.Failure.Unexpected(e.message ?: "Unbekannter Fehler beim Versenden der PDF-Datei.")
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
-10
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -46,6 +47,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -66,6 +68,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -78,8 +81,10 @@ import de.medapp.wunddoku.domain.model.Photo
|
||||
import de.medapp.wunddoku.domain.model.Signature
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfImageSharer
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfOpener
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfSharer
|
||||
import de.medapp.wunddoku.pdf.PdfImageShareResult
|
||||
import de.medapp.wunddoku.pdf.PdfOpenResult
|
||||
import de.medapp.wunddoku.pdf.PdfShareResult
|
||||
import de.medapp.wunddoku.pdf.toUserMessage
|
||||
import de.medapp.wunddoku.ui.documentation.edit.signature.AddSignatureDialog
|
||||
|
||||
@@ -141,6 +146,7 @@ fun DocumentationEditScreen(
|
||||
val context = LocalContext.current
|
||||
val pdfOpener = remember(context) { AndroidPdfOpener(context) }
|
||||
val pdfImageSharer = remember(context) { AndroidPdfImageSharer(context) }
|
||||
val pdfSharer = remember(context) { AndroidPdfSharer(context) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
LaunchedEffect(uiState.pdfExportMessage) {
|
||||
val message = uiState.pdfExportMessage ?: return@LaunchedEffect
|
||||
@@ -163,7 +169,7 @@ fun DocumentationEditScreen(
|
||||
|
||||
// Änderungswunsch: erzeugte Bilder direkt über den Android-"Senden an"-Dialog verschicken
|
||||
// können. Sobald das ViewModel frische JPEG-Seiten bereitstellt (s.
|
||||
// DocumentationEditViewModel.exportAndShareAsImages), öffnet dieser Effekt den System-
|
||||
// DocumentationEditViewModel.shareAsImages), öffnet dieser Effekt den System-
|
||||
// Share-Dialog - Intent/Activity-Start ist bewusst UI-Aufgabe, nicht Sache des ViewModels.
|
||||
LaunchedEffect(uiState.imagesToShare) {
|
||||
val images = uiState.imagesToShare ?: return@LaunchedEffect
|
||||
@@ -173,10 +179,20 @@ fun DocumentationEditScreen(
|
||||
}
|
||||
viewModel.onImageShareHandled()
|
||||
}
|
||||
LaunchedEffect(uiState.imageShareMessage) {
|
||||
val message = uiState.imageShareMessage ?: return@LaunchedEffect
|
||||
// Analog zum Bilderversand oben, nur für die Format-Auswahl "PDF" (s.
|
||||
// DocumentationEditViewModel.shareAsPdf).
|
||||
LaunchedEffect(uiState.pdfToShare) {
|
||||
val pdfFile = uiState.pdfToShare ?: return@LaunchedEffect
|
||||
when (val shareResult = pdfSharer.share(pdfFile)) {
|
||||
PdfShareResult.Success -> Unit
|
||||
is PdfShareResult.Failure -> snackbarHostState.showSnackbar(shareResult.toUserMessage())
|
||||
}
|
||||
viewModel.onPdfShareHandled()
|
||||
}
|
||||
LaunchedEffect(uiState.shareMessage) {
|
||||
val message = uiState.shareMessage ?: return@LaunchedEffect
|
||||
snackbarHostState.showSnackbar(message)
|
||||
viewModel.dismissImageShareMessage()
|
||||
viewModel.dismissShareMessage()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -206,14 +222,14 @@ fun DocumentationEditScreen(
|
||||
Icon(Icons.Filled.PictureAsPdf, contentDescription = "PDF erzeugen")
|
||||
}
|
||||
}
|
||||
// Änderungswunsch: PDF zusätzlich als JPEG speichern/versenden. Exportiert
|
||||
// intern frisch als PDF, wandelt seitenweise in JPEG um und öffnet
|
||||
// anschließend automatisch den "Senden an"-Dialog (s. LaunchedEffect oben).
|
||||
if (uiState.imageShareInProgress) {
|
||||
// Änderungswunsch: PDF zusätzlich als JPEG speichern/versenden, wahlweise
|
||||
// auch als PDF selbst - Format wird per Dialog abgefragt (s. weiter unten),
|
||||
// BEVOR irgendetwas exportiert wird.
|
||||
if (uiState.shareInProgress) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
IconButton(onClick = viewModel::exportAndShareAsImages) {
|
||||
Icon(Icons.Filled.Share, contentDescription = "Als Bilder senden")
|
||||
IconButton(onClick = viewModel::requestShareFormatChoice) {
|
||||
Icon(Icons.Filled.Share, contentDescription = "Teilen")
|
||||
}
|
||||
}
|
||||
// "Fertig" beendet die Bearbeitung bereits jetzt (Inhalte sind ohnehin per
|
||||
@@ -346,6 +362,52 @@ fun DocumentationEditScreen(
|
||||
onDismiss = { viewedPhoto = null },
|
||||
)
|
||||
}
|
||||
|
||||
// Änderungswunsch: Format-Auswahl beim Antippen des Teilen-Buttons.
|
||||
if (uiState.showShareFormatDialog) {
|
||||
ShareFormatDialog(
|
||||
onDismiss = viewModel::dismissShareFormatDialog,
|
||||
onChooseImages = viewModel::shareAsImages,
|
||||
onChoosePdf = viewModel::shareAsPdf,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt beim Antippen des Teilen-Buttons ab, ob als JPG (Bilder, eine Datei pro Seite) oder als
|
||||
* PDF (die Datei selbst) geteilt werden soll (Änderungswunsch). Erst NACH dieser Auswahl wird
|
||||
* überhaupt exportiert (s. [DocumentationEditViewModel.shareAsImages]/[DocumentationEditViewModel.shareAsPdf]) -
|
||||
* kein unnötiger Export, falls der Nutzer den Dialog stattdessen abbricht.
|
||||
*/
|
||||
@Composable
|
||||
private fun ShareFormatDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onChooseImages: () -> Unit,
|
||||
onChoosePdf: () -> Unit,
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surface) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(20.dp)) {
|
||||
Text("Teilen als", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Als Bilder (JPG) oder als PDF teilen?",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
OutlinedButton(onClick = onChooseImages) { Text("JPG") }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(onClick = onChoosePdf) { Text("PDF") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+80
-15
@@ -65,12 +65,17 @@ data class DocumentationEditUiState(
|
||||
* referenziert werden kann, während ein neuer Export läuft.
|
||||
*/
|
||||
val lastExportedPdfFile: File? = null,
|
||||
/** true, während PDF-Export + JPEG-Konvertierung für den Bilderversand laufen (ein
|
||||
* gemeinsamer Ladezustand für beide Schritte, s. [DocumentationEditViewModel.exportAndShareAsImages]). */
|
||||
val imageShareInProgress: Boolean = false,
|
||||
/** Fehlermeldung, falls Export oder Konvertierung fehlgeschlagen sind - erfolgreich
|
||||
* erzeugte Bilder lösen dagegen KEINE Meldung aus, sondern direkt [imagesToShare]. */
|
||||
val imageShareMessage: String? = null,
|
||||
/** true, während PDF-Export + Konvertierung für den Versand laufen - gemeinsamer
|
||||
* Ladezustand für BEIDE Formate (JPG über [DocumentationEditViewModel.shareAsImages],
|
||||
* PDF über [DocumentationEditViewModel.shareAsPdf]), da immer zuerst frisch exportiert
|
||||
* wird. */
|
||||
val shareInProgress: Boolean = false,
|
||||
/** Fehlermeldung, falls Export oder Konvertierung fehlgeschlagen sind - ein erfolgreicher
|
||||
* Export löst dagegen KEINE Meldung aus, sondern direkt [imagesToShare] bzw. [pdfToShare]. */
|
||||
val shareMessage: String? = null,
|
||||
/** true, solange die Auswahl "Als JPG oder PDF teilen?" angezeigt wird (Änderungswunsch:
|
||||
* Format-Auswahl beim Antippen des Teilen-Buttons, bevor irgendetwas exportiert wird). */
|
||||
val showShareFormatDialog: Boolean = false,
|
||||
/**
|
||||
* Frisch erzeugte JPEG-Seiten, bereit zum Teilen (Änderungswunsch: "alle generierten Bilder
|
||||
* über 'Senden an' direkt verschicken können"). Wird vom Screen per LaunchedEffect
|
||||
@@ -79,6 +84,9 @@ data class DocumentationEditUiState(
|
||||
* Activity-Start) ist bewusst UI-Aufgabe, nicht Sache des ViewModels.
|
||||
*/
|
||||
val imagesToShare: List<File>? = null,
|
||||
/** Analog [imagesToShare], nur für die Variante "als PDF teilen" - Gegenstück
|
||||
* [DocumentationEditViewModel.onPdfShareHandled]. */
|
||||
val pdfToShare: File? = null,
|
||||
) {
|
||||
val blockCount: Int get() = documentation?.blocks?.size ?: 0
|
||||
|
||||
@@ -373,6 +381,31 @@ class DocumentationEditViewModel(
|
||||
_uiState.update { it.copy(pdfExportMessage = null) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet die Auswahl "Als JPG oder PDF teilen?" (Änderungswunsch: Format-Auswahl beim
|
||||
* Antippen des Teilen-Buttons, BEVOR irgendetwas exportiert wird - vorher wurde beim
|
||||
* Antippen sofort und ausschließlich als JPG geteilt).
|
||||
*/
|
||||
fun requestShareFormatChoice() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = true) }
|
||||
}
|
||||
|
||||
fun dismissShareFormatDialog() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = false) }
|
||||
}
|
||||
|
||||
/** Nutzer hat sich für "Als JPG teilen" entschieden. */
|
||||
fun shareAsImages() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = false) }
|
||||
exportAndShareAsImages()
|
||||
}
|
||||
|
||||
/** Nutzer hat sich für "Als PDF teilen" entschieden. */
|
||||
fun shareAsPdf() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = false) }
|
||||
exportAndSharePdf()
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert die Dokumentation frisch als PDF und wandelt das Ergebnis anschließend
|
||||
* seitenweise in JPEG-Bilder um (Änderungswunsch: "PDF zusätzlich als JPEG speichern/senden
|
||||
@@ -384,10 +417,10 @@ class DocumentationEditViewModel(
|
||||
* der Screen anhand von [DocumentationEditUiState.imagesToShare] - Intent/Activity-Start ist
|
||||
* UI-Aufgabe (s. [de.medapp.wunddoku.pdf.PdfImageSharer]).
|
||||
*/
|
||||
fun exportAndShareAsImages() {
|
||||
private fun exportAndShareAsImages() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(imageShareInProgress = true, imageShareMessage = null, imagesToShare = null)
|
||||
it.copy(shareInProgress = true, shareMessage = null, imagesToShare = null)
|
||||
}
|
||||
|
||||
val exportResult = repository.exportPdf(documentationId)
|
||||
@@ -396,8 +429,8 @@ class DocumentationEditViewModel(
|
||||
is PdfExportResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
imageShareInProgress = false,
|
||||
imageShareMessage = "PDF-Erzeugung fehlgeschlagen: ${exportResult.error.toUserMessage()}",
|
||||
shareInProgress = false,
|
||||
shareMessage = "PDF-Erzeugung fehlgeschlagen: ${exportResult.error.toUserMessage()}",
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
@@ -406,13 +439,40 @@ class DocumentationEditViewModel(
|
||||
|
||||
when (val imagesResult = repository.convertPdfToImages(documentationId, pdfFile)) {
|
||||
is PdfToImagesResult.Success -> _uiState.update {
|
||||
it.copy(imageShareInProgress = false, imagesToShare = imagesResult.imageFiles)
|
||||
it.copy(shareInProgress = false, imagesToShare = imagesResult.imageFiles)
|
||||
}
|
||||
|
||||
is PdfToImagesResult.Failure -> _uiState.update {
|
||||
it.copy(
|
||||
imageShareInProgress = false,
|
||||
imageShareMessage = "Bildexport fehlgeschlagen: ${imagesResult.toUserMessage()}",
|
||||
shareInProgress = false,
|
||||
shareMessage = "Bildexport fehlgeschlagen: ${imagesResult.toUserMessage()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert die Dokumentation frisch als PDF und stellt sie zum Teilen bereit
|
||||
* (Änderungswunsch: Format-Auswahl "PDF" statt "JPG") - ohne die JPEG-Konvertierung, die
|
||||
* geteilte Datei ist die PDF selbst. Das eigentliche Teilen übernimmt der Screen anhand von
|
||||
* [DocumentationEditUiState.pdfToShare] (s. [de.medapp.wunddoku.pdf.PdfSharer]).
|
||||
*/
|
||||
private fun exportAndSharePdf() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(shareInProgress = true, shareMessage = null, pdfToShare = null)
|
||||
}
|
||||
|
||||
when (val exportResult = repository.exportPdf(documentationId)) {
|
||||
is PdfExportResult.Success -> _uiState.update {
|
||||
it.copy(shareInProgress = false, pdfToShare = exportResult.file)
|
||||
}
|
||||
|
||||
is PdfExportResult.Failure -> _uiState.update {
|
||||
it.copy(
|
||||
shareInProgress = false,
|
||||
shareMessage = "PDF-Erzeugung fehlgeschlagen: ${exportResult.error.toUserMessage()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -426,8 +486,13 @@ class DocumentationEditViewModel(
|
||||
_uiState.update { it.copy(imagesToShare = null) }
|
||||
}
|
||||
|
||||
fun dismissImageShareMessage() {
|
||||
_uiState.update { it.copy(imageShareMessage = null) }
|
||||
/** Analog [onImageShareHandled], nur für [DocumentationEditUiState.pdfToShare]. */
|
||||
fun onPdfShareHandled() {
|
||||
_uiState.update { it.copy(pdfToShare = null) }
|
||||
}
|
||||
|
||||
fun dismissShareMessage() {
|
||||
_uiState.update { it.copy(shareMessage = null) }
|
||||
}
|
||||
|
||||
private fun PdfExportError.toUserMessage(): String = when (this) {
|
||||
|
||||
Reference in New Issue
Block a user