Beta 1.0 Release #11
@@ -29,10 +29,13 @@ import de.medapp.wunddoku.pdf.DocumentationPdfExporter
|
|||||||
import de.medapp.wunddoku.pdf.DocumentationToPdfExportModelMapper
|
import de.medapp.wunddoku.pdf.DocumentationToPdfExportModelMapper
|
||||||
import de.medapp.wunddoku.pdf.PdfExportError
|
import de.medapp.wunddoku.pdf.PdfExportError
|
||||||
import de.medapp.wunddoku.pdf.PdfExportResult
|
import de.medapp.wunddoku.pdf.PdfExportResult
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesConverter
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesResult
|
||||||
import de.medapp.wunddoku.signature.SignatureBitmapValidation
|
import de.medapp.wunddoku.signature.SignatureBitmapValidation
|
||||||
import de.medapp.wunddoku.signature.SignatureStorage
|
import de.medapp.wunddoku.signature.SignatureStorage
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.io.File
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -60,6 +63,7 @@ class DocumentationRepository(
|
|||||||
private val signatureStorage: SignatureStorage,
|
private val signatureStorage: SignatureStorage,
|
||||||
private val woundCarerRepository: WoundCarerRepository,
|
private val woundCarerRepository: WoundCarerRepository,
|
||||||
private val pdfExporter: DocumentationPdfExporter,
|
private val pdfExporter: DocumentationPdfExporter,
|
||||||
|
private val pdfToImagesConverter: PdfToImagesConverter,
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -284,6 +288,16 @@ class DocumentationRepository(
|
|||||||
return pdfExporter.export(model)
|
return pdfExporter.export(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert eine bereits erzeugte PDF-Datei (s. [exportPdf]) seitenweise in JPEG-Dateien
|
||||||
|
* (Änderungswunsch: "PDF zusätzlich als JPEG speichern/senden können"). Nimmt bewusst die
|
||||||
|
* fertige [pdfFile] entgegen, statt die Dokumentation erneut zu laden/zu mappen - der
|
||||||
|
* Aufrufer hat sie direkt aus einem vorangegangenen [exportPdf]-Aufruf zur Verfügung, ein
|
||||||
|
* erneutes Laden wäre unnötige Doppelarbeit.
|
||||||
|
*/
|
||||||
|
suspend fun convertPdfToImages(documentationId: String, pdfFile: File): PdfToImagesResult =
|
||||||
|
pdfToImagesConverter.convert(documentationId, pdfFile)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liefert eine speicherschonende Vorschauversion eines Fotos für die Anzeige im Fotogitter
|
* Liefert eine speicherschonende Vorschauversion eines Fotos für die Anzeige im Fotogitter
|
||||||
* der Dokumentationsbearbeitung (kein Laden der vollen gespeicherten Auflösung). Wird von
|
* der Dokumentationsbearbeitung (kein Laden der vollen gespeicherten Auflösung). Wird von
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import de.medapp.wunddoku.data.repository.WoundCarerRepository
|
|||||||
import de.medapp.wunddoku.media.InternalPhotoStorage
|
import de.medapp.wunddoku.media.InternalPhotoStorage
|
||||||
import de.medapp.wunddoku.media.PhotoStorage
|
import de.medapp.wunddoku.media.PhotoStorage
|
||||||
import de.medapp.wunddoku.pdf.AndroidPdfDocumentExporter
|
import de.medapp.wunddoku.pdf.AndroidPdfDocumentExporter
|
||||||
|
import de.medapp.wunddoku.pdf.AndroidPdfToImagesConverter
|
||||||
import de.medapp.wunddoku.pdf.DocumentationPdfExporter
|
import de.medapp.wunddoku.pdf.DocumentationPdfExporter
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesConverter
|
||||||
import de.medapp.wunddoku.signature.InternalSignatureStorage
|
import de.medapp.wunddoku.signature.InternalSignatureStorage
|
||||||
import de.medapp.wunddoku.signature.SignatureStorage
|
import de.medapp.wunddoku.signature.SignatureStorage
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ class AppContainer(context: Context) {
|
|||||||
private val pdfExporter: DocumentationPdfExporter by lazy {
|
private val pdfExporter: DocumentationPdfExporter by lazy {
|
||||||
AndroidPdfDocumentExporter(context, photoStorage, signatureStorage)
|
AndroidPdfDocumentExporter(context, photoStorage, signatureStorage)
|
||||||
}
|
}
|
||||||
|
private val pdfToImagesConverter: PdfToImagesConverter by lazy { AndroidPdfToImagesConverter(context) }
|
||||||
|
|
||||||
val patientRepository: PatientRepository by lazy {
|
val patientRepository: PatientRepository by lazy {
|
||||||
PatientRepository(patientDao = database.patientDao())
|
PatientRepository(patientDao = database.patientDao())
|
||||||
@@ -62,6 +65,7 @@ class AppContainer(context: Context) {
|
|||||||
signatureStorage = signatureStorage,
|
signatureStorage = signatureStorage,
|
||||||
woundCarerRepository = woundCarerRepository,
|
woundCarerRepository = woundCarerRepository,
|
||||||
pdfExporter = pdfExporter,
|
pdfExporter = pdfExporter,
|
||||||
|
pdfToImagesConverter = pdfToImagesConverter,
|
||||||
db = database,
|
db = database,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package de.medapp.wunddoku.pdf
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Übergibt alle als JPEG gerenderten PDF-Seiten (s. [PdfToImagesConverter]) GEMEINSAM an den
|
||||||
|
* Android-"Senden an"-Dialog (`ACTION_SEND_MULTIPLE`) - Änderungswunsch: "alle generierten
|
||||||
|
* Bilder über 'Senden an' direkt verschicken können", nicht Seite für Seite einzeln.
|
||||||
|
*
|
||||||
|
* Bewusst getrennt von [PdfToImagesConverter] (reine Datei-Logik): das Starten einer Activity
|
||||||
|
* ist eine UI-/Plattform-Aktion, keine Repository-Aufgabe - exakt dieselbe Trennung wie bei
|
||||||
|
* [DocumentationPdfExporter] (Erzeugung) vs. [PdfOpener] (Anzeige).
|
||||||
|
*
|
||||||
|
* VulneraDoc implementiert ausdrücklich KEINEN eigenen Versand-Mechanismus; welche konkrete App
|
||||||
|
* (Mail, Messenger, Cloud-Speicher, ...) die Bilder entgegennimmt, entscheidet ausschließlich
|
||||||
|
* Android/der Nutzer über den System-Dialog.
|
||||||
|
*/
|
||||||
|
interface PdfImageSharer {
|
||||||
|
fun share(imageFiles: List<File>): PdfImageShareResult
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface PdfImageShareResult {
|
||||||
|
data object Success : PdfImageShareResult
|
||||||
|
|
||||||
|
sealed interface Failure : PdfImageShareResult {
|
||||||
|
/** Es wurden keine Bilddateien übergeben - nichts zum Teilen vorhanden. */
|
||||||
|
data object NoImages : Failure
|
||||||
|
|
||||||
|
/** FileProvider konnte für mindestens eine Datei keine content://-Uri erzeugen. */
|
||||||
|
data class UriResolutionFailed(val message: String) : Failure
|
||||||
|
|
||||||
|
/** Keine installierte App kann Bilder (image/jpeg) entgegennehmen. */
|
||||||
|
data object NoAppAvailable : Failure
|
||||||
|
|
||||||
|
data class Unexpected(val message: String) : Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PdfImageShareResult.Failure.toUserMessage(): String = when (this) {
|
||||||
|
PdfImageShareResult.Failure.NoImages ->
|
||||||
|
"Es sind keine Bilder zum Versenden vorhanden."
|
||||||
|
is PdfImageShareResult.Failure.UriResolutionFailed ->
|
||||||
|
"Die Bilder konnten nicht zum Versenden freigegeben werden."
|
||||||
|
PdfImageShareResult.Failure.NoAppAvailable ->
|
||||||
|
"Auf diesem Gerät ist keine App zum Versenden von Bildern installiert."
|
||||||
|
is PdfImageShareResult.Failure.Unexpected ->
|
||||||
|
"Die Bilder konnten nicht versendet werden."
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidPdfImageSharer(private val context: Context) : PdfImageSharer {
|
||||||
|
|
||||||
|
override fun share(imageFiles: List<File>): PdfImageShareResult {
|
||||||
|
if (imageFiles.isEmpty()) return PdfImageShareResult.Failure.NoImages
|
||||||
|
|
||||||
|
val authority = "${context.packageName}.fileprovider"
|
||||||
|
val uris = try {
|
||||||
|
imageFiles.map { file -> FileProvider.getUriForFile(context, authority, file) }
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// Wird von FileProvider geworfen, wenn eine Datei außerhalb der in file_paths.xml
|
||||||
|
// freigegebenen Verzeichnisse liegt oder die Authority nicht passt.
|
||||||
|
return PdfImageShareResult.Failure.UriResolutionFailed(e.message ?: "Uri konnte nicht erzeugt werden.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||||
|
type = "image/jpeg"
|
||||||
|
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
// Zusätzlich zum Flag oben wird die Leseberechtigung explizit über ClipData an JEDE
|
||||||
|
// einzelne Uri gehängt - bei ACTION_SEND_MULTIPLE mit mehreren content://-Uris ist
|
||||||
|
// das der zuverlässige Weg, damit wirklich ALLE empfangenden Apps Zugriff auf ALLE
|
||||||
|
// Bilder bekommen (nicht nur auf die im Intent-Datenfeld primär referenzierte).
|
||||||
|
clipData = ClipData.newUri(context.contentResolver, "Wunddokumentation", uris.first()).apply {
|
||||||
|
for (uri in uris.drop(1)) {
|
||||||
|
addItem(ClipData.Item(uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val chooser = Intent.createChooser(intent, "Bilder senden an")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
context.startActivity(chooser)
|
||||||
|
PdfImageShareResult.Success
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
// Bewusst KEIN vorheriges intent.resolveActivity(...) - s. Begründung in
|
||||||
|
// AndroidPdfOpener.open() (Package-Visibility-Einschränkungen ab Android 11).
|
||||||
|
PdfImageShareResult.Failure.NoAppAvailable
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
PdfImageShareResult.Failure.Unexpected(e.message ?: "Zugriff auf die Bilddateien wurde verweigert.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
PdfImageShareResult.Failure.Unexpected(e.message ?: "Unbekannter Fehler beim Versenden der Bilder.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package de.medapp.wunddoku.pdf
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.pdf.PdfRenderer
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert eine bereits fertig erzeugte PDF-Datei (s. [DocumentationPdfExporter]) seitenweise in
|
||||||
|
* einzelne JPEG-Dateien - Änderungswunsch: "PDF zusätzlich als JPEG speichern/senden können".
|
||||||
|
*
|
||||||
|
* Bewusst als eigener Schritt NACH dem PDF-Export statt als Alternative dazu: Es gibt weiterhin
|
||||||
|
* genau eine Quelle der Wahrheit für das Seitenlayout (der bestehende [PdfPageCursor]-basierte
|
||||||
|
* Export) - dieser Konverter rastert lediglich das bereits fertige Ergebnis, statt eine zweite,
|
||||||
|
* parallele Layout-Logik zu pflegen.
|
||||||
|
*
|
||||||
|
* Verwendet [android.graphics.pdf.PdfRenderer] (Android-Bordmittel seit API 21, unproblematisch
|
||||||
|
* bei minSdk 26) - keine zusätzliche Bibliothek nötig. Eine mehrseitige Wunddokumentation (Kopf/
|
||||||
|
* Blöcke, Signaturen, ggf. 30+ Fotoseiten) ergibt dabei bewusst EIN JPEG PRO SEITE, kein
|
||||||
|
* einzelnes Sammelbild - beim späteren Teilen (s. [PdfImageSharer]) werden alle Seiten
|
||||||
|
* gemeinsam als Bilder-Mehrfachauswahl übergeben.
|
||||||
|
*/
|
||||||
|
interface PdfToImagesConverter {
|
||||||
|
suspend fun convert(documentationId: String, pdfFile: File): PdfToImagesResult
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface PdfToImagesResult {
|
||||||
|
data class Success(val imageFiles: List<File>) : PdfToImagesResult
|
||||||
|
|
||||||
|
sealed interface Failure : PdfToImagesResult {
|
||||||
|
/** Die übergebene PDF-Datei existiert nicht (mehr). */
|
||||||
|
data object FileNotFound : Failure
|
||||||
|
|
||||||
|
/** PdfRenderer meldet 0 Seiten - sollte bei einem selbst erzeugten PDF nie vorkommen. */
|
||||||
|
data object EmptyDocument : Failure
|
||||||
|
|
||||||
|
data class Unexpected(val message: String) : Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PdfToImagesResult.Failure.toUserMessage(): String = when (this) {
|
||||||
|
PdfToImagesResult.Failure.FileNotFound ->
|
||||||
|
"Die PDF-Datei wurde nicht gefunden. Bitte zuerst die Dokumentation erneut exportieren."
|
||||||
|
PdfToImagesResult.Failure.EmptyDocument ->
|
||||||
|
"Die PDF-Datei enthält keine Seiten."
|
||||||
|
is PdfToImagesResult.Failure.Unexpected ->
|
||||||
|
"Die PDF-Datei konnte nicht in Bilder umgewandelt werden."
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidPdfToImagesConverter(private val context: Context) : PdfToImagesConverter {
|
||||||
|
|
||||||
|
override suspend fun convert(documentationId: String, pdfFile: File): PdfToImagesResult =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (!pdfFile.exists()) return@withContext PdfToImagesResult.Failure.FileNotFound
|
||||||
|
|
||||||
|
val outputDir = File(File(context.filesDir, "pdf_images"), documentationId)
|
||||||
|
// Bilder eines vorherigen Exports zuerst entfernen, damit bei einer inzwischen
|
||||||
|
// kürzeren PDF (z. B. Fotos gelöscht) keine veralteten, zu vielen Seiten-Dateien
|
||||||
|
// liegen bleiben.
|
||||||
|
outputDir.deleteRecursively()
|
||||||
|
outputDir.mkdirs()
|
||||||
|
|
||||||
|
try {
|
||||||
|
ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
|
||||||
|
PdfRenderer(fd).use { renderer ->
|
||||||
|
if (renderer.pageCount == 0) {
|
||||||
|
return@withContext PdfToImagesResult.Failure.EmptyDocument
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageFiles = mutableListOf<File>()
|
||||||
|
for (pageIndex in 0 until renderer.pageCount) {
|
||||||
|
renderer.openPage(pageIndex).use { page ->
|
||||||
|
val scale = RENDER_DPI / POINTS_PER_INCH
|
||||||
|
val width = (page.width * scale).roundToInt().coerceAtLeast(1)
|
||||||
|
val height = (page.height * scale).roundToInt().coerceAtLeast(1)
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
// WICHTIG: PdfRenderer zeichnet nur die tatsächlich im PDF
|
||||||
|
// vorhandenen Inhalte - unbedeckte Flächen bleiben transparent
|
||||||
|
// (Alpha 0). JPEG kennt keine Transparenz; ohne dieses
|
||||||
|
// Vorab-Weißfüllen würde jede unbedeckte Fläche beim
|
||||||
|
// Komprimieren als SCHWARZ statt als weißer Seitenhintergrund
|
||||||
|
// enden (bekannte PdfRenderer-Falle, s. offizielle Android-
|
||||||
|
// Beispiele).
|
||||||
|
bitmap.eraseColor(Color.WHITE)
|
||||||
|
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||||
|
|
||||||
|
val imageFile = File(outputDir, "seite_%02d.jpg".format(pageIndex + 1))
|
||||||
|
FileOutputStream(imageFile).use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out)
|
||||||
|
}
|
||||||
|
bitmap.recycle()
|
||||||
|
imageFiles.add(imageFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PdfToImagesResult.Success(imageFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
PdfToImagesResult.Failure.Unexpected(e.message ?: "Unbekannter Fehler bei der Bildkonvertierung.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** PDF-Koordinaten sind in Punkten (1/72 Zoll) angegeben - diese Ziel-Auflösung ergibt
|
||||||
|
* ein für Ausdruck/Weitergabe ausreichend scharfes, aber nicht unnötig großes Bild. */
|
||||||
|
private const val RENDER_DPI = 200f
|
||||||
|
private const val POINTS_PER_INCH = 72f
|
||||||
|
private const val JPEG_QUALITY = 90
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-3
@@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.Close
|
|||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.PictureAsPdf
|
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
@@ -75,7 +76,9 @@ import de.medapp.wunddoku.domain.model.CheckItem
|
|||||||
import de.medapp.wunddoku.domain.model.DocumentationBlock
|
import de.medapp.wunddoku.domain.model.DocumentationBlock
|
||||||
import de.medapp.wunddoku.domain.model.Photo
|
import de.medapp.wunddoku.domain.model.Photo
|
||||||
import de.medapp.wunddoku.domain.model.Signature
|
import de.medapp.wunddoku.domain.model.Signature
|
||||||
|
import de.medapp.wunddoku.pdf.AndroidPdfImageSharer
|
||||||
import de.medapp.wunddoku.pdf.AndroidPdfOpener
|
import de.medapp.wunddoku.pdf.AndroidPdfOpener
|
||||||
|
import de.medapp.wunddoku.pdf.PdfImageShareResult
|
||||||
import de.medapp.wunddoku.pdf.PdfOpenResult
|
import de.medapp.wunddoku.pdf.PdfOpenResult
|
||||||
import de.medapp.wunddoku.pdf.toUserMessage
|
import de.medapp.wunddoku.pdf.toUserMessage
|
||||||
import de.medapp.wunddoku.ui.documentation.edit.signature.AddSignatureDialog
|
import de.medapp.wunddoku.ui.documentation.edit.signature.AddSignatureDialog
|
||||||
@@ -137,6 +140,7 @@ fun DocumentationEditScreen(
|
|||||||
// Starten einer externen Activity ist eine UI-/Plattform-Aktion, keine Repository-Aufgabe.
|
// Starten einer externen Activity ist eine UI-/Plattform-Aktion, keine Repository-Aufgabe.
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val pdfOpener = remember(context) { AndroidPdfOpener(context) }
|
val pdfOpener = remember(context) { AndroidPdfOpener(context) }
|
||||||
|
val pdfImageSharer = remember(context) { AndroidPdfImageSharer(context) }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
LaunchedEffect(uiState.pdfExportMessage) {
|
LaunchedEffect(uiState.pdfExportMessage) {
|
||||||
val message = uiState.pdfExportMessage ?: return@LaunchedEffect
|
val message = uiState.pdfExportMessage ?: return@LaunchedEffect
|
||||||
@@ -157,6 +161,24 @@ fun DocumentationEditScreen(
|
|||||||
viewModel.dismissPdfExportMessage()
|
viewModel.dismissPdfExportMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ä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-
|
||||||
|
// Share-Dialog - Intent/Activity-Start ist bewusst UI-Aufgabe, nicht Sache des ViewModels.
|
||||||
|
LaunchedEffect(uiState.imagesToShare) {
|
||||||
|
val images = uiState.imagesToShare ?: return@LaunchedEffect
|
||||||
|
when (val shareResult = pdfImageSharer.share(images)) {
|
||||||
|
PdfImageShareResult.Success -> Unit
|
||||||
|
is PdfImageShareResult.Failure -> snackbarHostState.showSnackbar(shareResult.toUserMessage())
|
||||||
|
}
|
||||||
|
viewModel.onImageShareHandled()
|
||||||
|
}
|
||||||
|
LaunchedEffect(uiState.imageShareMessage) {
|
||||||
|
val message = uiState.imageShareMessage ?: return@LaunchedEffect
|
||||||
|
snackbarHostState.showSnackbar(message)
|
||||||
|
viewModel.dismissImageShareMessage()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -175,9 +197,8 @@ fun DocumentationEditScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
// PDF-005 (Öffnen)/PDF-006 (Teilen) folgen später - dieser Button erzeugt
|
// PDF-005 (Öffnen) ist umgesetzt (s. LaunchedEffect oben); dieser Button
|
||||||
// ausschließlich die Datei im internen Speicher (PDF-001/002), als
|
// erzeugt die Datei im internen Speicher (PDF-001/002).
|
||||||
// minimaler Trigger zum manuellen Testen der Export-Funktionalität.
|
|
||||||
if (uiState.pdfExportInProgress) {
|
if (uiState.pdfExportInProgress) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||||
} else {
|
} else {
|
||||||
@@ -185,6 +206,16 @@ fun DocumentationEditScreen(
|
|||||||
Icon(Icons.Filled.PictureAsPdf, contentDescription = "PDF erzeugen")
|
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) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = viewModel::exportAndShareAsImages) {
|
||||||
|
Icon(Icons.Filled.Share, contentDescription = "Als Bilder senden")
|
||||||
|
}
|
||||||
|
}
|
||||||
// "Fertig" beendet die Bearbeitung bereits jetzt (Inhalte sind ohnehin per
|
// "Fertig" beendet die Bearbeitung bereits jetzt (Inhalte sind ohnehin per
|
||||||
// Autosave gesichert, kein expliziter Speichern-Button nötig).
|
// Autosave gesichert, kein expliziter Speichern-Button nötig).
|
||||||
Button(onClick = onFinished) { Text("Fertig") }
|
Button(onClick = onFinished) { Text("Fertig") }
|
||||||
|
|||||||
+73
@@ -15,6 +15,8 @@ import de.medapp.wunddoku.domain.model.Documentation
|
|||||||
import de.medapp.wunddoku.domain.model.WoundCarer
|
import de.medapp.wunddoku.domain.model.WoundCarer
|
||||||
import de.medapp.wunddoku.pdf.PdfExportError
|
import de.medapp.wunddoku.pdf.PdfExportError
|
||||||
import de.medapp.wunddoku.pdf.PdfExportResult
|
import de.medapp.wunddoku.pdf.PdfExportResult
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesResult
|
||||||
|
import de.medapp.wunddoku.pdf.toUserMessage
|
||||||
import de.medapp.wunddoku.ui.navigation.Screen
|
import de.medapp.wunddoku.ui.navigation.Screen
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -63,6 +65,20 @@ data class DocumentationEditUiState(
|
|||||||
* referenziert werden kann, während ein neuer Export läuft.
|
* referenziert werden kann, während ein neuer Export läuft.
|
||||||
*/
|
*/
|
||||||
val lastExportedPdfFile: File? = null,
|
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,
|
||||||
|
/**
|
||||||
|
* Frisch erzeugte JPEG-Seiten, bereit zum Teilen (Änderungswunsch: "alle generierten Bilder
|
||||||
|
* über 'Senden an' direkt verschicken können"). Wird vom Screen per LaunchedEffect
|
||||||
|
* beobachtet, der daraufhin den System-"Senden an"-Dialog öffnet und anschließend
|
||||||
|
* [DocumentationEditViewModel.onImageShareHandled] aufruft - das Teilen selbst (Intent/
|
||||||
|
* Activity-Start) ist bewusst UI-Aufgabe, nicht Sache des ViewModels.
|
||||||
|
*/
|
||||||
|
val imagesToShare: List<File>? = null,
|
||||||
) {
|
) {
|
||||||
val blockCount: Int get() = documentation?.blocks?.size ?: 0
|
val blockCount: Int get() = documentation?.blocks?.size ?: 0
|
||||||
|
|
||||||
@@ -357,6 +373,63 @@ class DocumentationEditViewModel(
|
|||||||
_uiState.update { it.copy(pdfExportMessage = null) }
|
_uiState.update { it.copy(pdfExportMessage = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* können"). Bewusst ein eigener, vollständiger Export-Lauf statt Wiederverwendung von
|
||||||
|
* [uiState.lastExportedPdfFile]: so spiegeln die Bilder IMMER den aktuellen Stand wider,
|
||||||
|
* unabhängig davon, ob/wann zuvor schon einmal "PDF erzeugen" angetippt wurde.
|
||||||
|
*
|
||||||
|
* Das eigentliche Teilen (Android-"Senden an"-Dialog) übernimmt NICHT diese Methode, sondern
|
||||||
|
* der Screen anhand von [DocumentationEditUiState.imagesToShare] - Intent/Activity-Start ist
|
||||||
|
* UI-Aufgabe (s. [de.medapp.wunddoku.pdf.PdfImageSharer]).
|
||||||
|
*/
|
||||||
|
fun exportAndShareAsImages() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(imageShareInProgress = true, imageShareMessage = null, imagesToShare = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val exportResult = repository.exportPdf(documentationId)
|
||||||
|
val pdfFile = when (exportResult) {
|
||||||
|
is PdfExportResult.Success -> exportResult.file
|
||||||
|
is PdfExportResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
imageShareInProgress = false,
|
||||||
|
imageShareMessage = "PDF-Erzeugung fehlgeschlagen: ${exportResult.error.toUserMessage()}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val imagesResult = repository.convertPdfToImages(documentationId, pdfFile)) {
|
||||||
|
is PdfToImagesResult.Success -> _uiState.update {
|
||||||
|
it.copy(imageShareInProgress = false, imagesToShare = imagesResult.imageFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PdfToImagesResult.Failure -> _uiState.update {
|
||||||
|
it.copy(
|
||||||
|
imageShareInProgress = false,
|
||||||
|
imageShareMessage = "Bildexport fehlgeschlagen: ${imagesResult.toUserMessage()}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wird vom Screen aufgerufen, nachdem der "Senden an"-Dialog für [DocumentationEditUiState.imagesToShare]
|
||||||
|
* geöffnet wurde (erfolgreich oder nicht) - verhindert ein erneutes Öffnen bei der nächsten
|
||||||
|
* Recomposition, solange keine neuen Bilder erzeugt wurden. */
|
||||||
|
fun onImageShareHandled() {
|
||||||
|
_uiState.update { it.copy(imagesToShare = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissImageShareMessage() {
|
||||||
|
_uiState.update { it.copy(imageShareMessage = null) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun PdfExportError.toUserMessage(): String = when (this) {
|
private fun PdfExportError.toUserMessage(): String = when (this) {
|
||||||
PdfExportError.DocumentationNotFound -> "Die Dokumentation wurde nicht gefunden."
|
PdfExportError.DocumentationNotFound -> "Die Dokumentation wurde nicht gefunden."
|
||||||
is PdfExportError.DirectoryCreationFailed -> "Zielverzeichnis konnte nicht angelegt werden."
|
is PdfExportError.DirectoryCreationFailed -> "Zielverzeichnis konnte nicht angelegt werden."
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<files-path name="pdf_exports" path="pdf/" />
|
<files-path name="pdf_exports" path="pdf/" />
|
||||||
|
<files-path name="pdf_images" path="pdf_images/" />
|
||||||
<files-path name="photos" path="photos/" />
|
<files-path name="photos" path="photos/" />
|
||||||
<files-path name="signatures" path="signatures/" />
|
<files-path name="signatures" path="signatures/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
Reference in New Issue
Block a user