Merge pull request 'Kundenwuensche' (#6) from dev-klock into dev
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -292,6 +292,19 @@ class DocumentationRepository(
|
|||||||
suspend fun loadPhotoThumbnail(documentationId: String, fileName: String): Bitmap? =
|
suspend fun loadPhotoThumbnail(documentationId: String, fileName: String): Bitmap? =
|
||||||
photoStorage.decodeThumbnail(documentationId, fileName, ImageDownsampler.THUMBNAIL_MAX_DIMENSION_PX)
|
photoStorage.decodeThumbnail(documentationId, fileName, ImageDownsampler.THUMBNAIL_MAX_DIMENSION_PX)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt ein Foto in Originalgröße für die Vollbildansicht beim Antippen einer Foto-Kachel
|
||||||
|
* (Änderungswunsch). Verwendet bewusst [de.medapp.wunddoku.media.PhotoStorage.decodeFitted]
|
||||||
|
* statt eines unskalierten Rohzugriffs: Fotos werden bereits beim Import auf eine sinnvolle
|
||||||
|
* Maximalauflösung begrenzt (s. PhotoStorage-Importlogik), ein zusätzliches Herunterrechnen
|
||||||
|
* auf höchstens die tatsächliche Displaygröße ([maxWidthPx]/[maxHeightPx], vom Aufrufer aus
|
||||||
|
* der Bildschirmauflösung ermittelt) vermeidet unnötig hohen Speicherbedarf, ohne das Bild
|
||||||
|
* sichtbar zu verkleinern - der Nutzer sieht es exakt so groß wie der Bildschirm es ohnehin
|
||||||
|
* darstellen könnte.
|
||||||
|
*/
|
||||||
|
suspend fun loadPhotoFullSize(documentationId: String, fileName: String, maxWidthPx: Int, maxHeightPx: Int): Bitmap? =
|
||||||
|
photoStorage.decodeFitted(documentationId, fileName, maxWidthPx, maxHeightPx)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt eine Signatur für die Vorschau in der Dokumentationsbearbeitung. Kein separates
|
* Lädt eine Signatur für die Vorschau in der Dokumentationsbearbeitung. Kein separates
|
||||||
* Thumbnail-Downsampling nötig - das logische Signaturformat (1000x400px) ist bereits klein
|
* Thumbnail-Downsampling nötig - das logische Signaturformat (1000x400px) ist bereits klein
|
||||||
@@ -364,8 +377,15 @@ class DocumentationRepository(
|
|||||||
/**
|
/**
|
||||||
* Legt für jeden Katalog-Block einen BlockState an, sowie je ein CheckItemState pro
|
* Legt für jeden Katalog-Block einen BlockState an, sowie je ein CheckItemState pro
|
||||||
* Katalog-Checkbox PLUS eine zusätzliche, automatisch angehängte Freitext-Option
|
* Katalog-Checkbox PLUS eine zusätzliche, automatisch angehängte Freitext-Option
|
||||||
* (isFreeText = true, letzte Position im Block) - siehe DocumentationCatalog.FREE_TEXT_LABEL
|
* (isFreeText = true) - siehe DocumentationCatalog.FREE_TEXT_LABEL und
|
||||||
* und BlockDefinition.freeTextDefinitionId.
|
* BlockDefinition.freeTextDefinitionId.
|
||||||
|
*
|
||||||
|
* Änderungswunsch: Die Freitext-Option erscheint als ERSTES Element im Block (itemOrder = 0,
|
||||||
|
* niedriger als jede Katalog-Checkbox, deren `order` bei 1 beginnt) - Vorbereitung dafür,
|
||||||
|
* dass echte Multiple-Choice-Varianten später als weitere Optionen NACH dem Freitext folgen
|
||||||
|
* können. Betrifft ausschließlich NEU erstellte Dokumentationen: bereits bestehende
|
||||||
|
* BlockState-/CheckItemState-Zeilen behalten ihre historisch gespeicherte itemOrder
|
||||||
|
* unverändert (Snapshot-Prinzip, s. Klassendokumentation zu titleSnapshot/labelSnapshot).
|
||||||
*/
|
*/
|
||||||
private suspend fun insertBlocksFromCatalog(documentationId: String, now: Long) {
|
private suspend fun insertBlocksFromCatalog(documentationId: String, now: Long) {
|
||||||
for (blockDef in DocumentationCatalog.blocks) {
|
for (blockDef in DocumentationCatalog.blocks) {
|
||||||
@@ -400,7 +420,7 @@ class DocumentationRepository(
|
|||||||
blockStateId = blockStateId,
|
blockStateId = blockStateId,
|
||||||
checkItemDefinitionId = blockDef.freeTextDefinitionId,
|
checkItemDefinitionId = blockDef.freeTextDefinitionId,
|
||||||
labelSnapshot = DocumentationCatalog.FREE_TEXT_LABEL,
|
labelSnapshot = DocumentationCatalog.FREE_TEXT_LABEL,
|
||||||
itemOrder = blockDef.items.size + 1,
|
itemOrder = 0,
|
||||||
checked = false,
|
checked = false,
|
||||||
isFreeText = true,
|
isFreeText = true,
|
||||||
freeTextValue = "",
|
freeTextValue = "",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package de.medapp.wunddoku.pdf
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.pdf.PdfDocument
|
import android.graphics.pdf.PdfDocument
|
||||||
@@ -84,10 +83,9 @@ class AndroidPdfDocumentExporter(
|
|||||||
private const val SIGNATURE_SPACING_AFTER_BLOCK = 16f
|
private const val SIGNATURE_SPACING_AFTER_BLOCK = 16f
|
||||||
private const val SIGNATURE_SECTION_TITLE_SPACING_AFTER = 8f
|
private const val SIGNATURE_SECTION_TITLE_SPACING_AFTER = 8f
|
||||||
|
|
||||||
// Vektor-Checkbox: Kantenlänge des gezeichneten Kästchens und Abstand zum Label-Text.
|
// Änderungswunsch: kein Checkbox-Symbol mehr im PDF - nur noch eine schlichte Einrückung
|
||||||
private const val CHECKBOX_SIZE = 10f
|
// der (bereits ausschließlich angehakten) Optionen unter dem Blocktitel.
|
||||||
private const val CHECKBOX_GAP = 6f
|
private const val ITEM_INDENT = 10f
|
||||||
private const val CHECKBOX_INDENT = CHECKBOX_SIZE + CHECKBOX_GAP
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zielauflösung für den Fotoanhang, bewusst als Kompromiss zwischen Bildqualität und
|
* Zielauflösung für den Fotoanhang, bewusst als Kompromiss zwischen Bildqualität und
|
||||||
@@ -244,21 +242,6 @@ class AndroidPdfDocumentExporter(
|
|||||||
color = Color.DKGRAY
|
color = Color.DKGRAY
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reine Vektor-Checkbox-Paints - keine Textschrift/Glyphen-Abhängigkeit.
|
|
||||||
val checkboxOutline = Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
style = Paint.Style.STROKE
|
|
||||||
strokeWidth = 1.2f
|
|
||||||
color = Color.BLACK
|
|
||||||
}
|
|
||||||
val checkboxMark = Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
style = Paint.Style.STROKE
|
|
||||||
strokeWidth = 1.5f
|
|
||||||
strokeCap = Paint.Cap.ROUND
|
|
||||||
color = Color.BLACK
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Für das Zeichnen der Fotoanhang-Bitmaps (PDF-004). `isFilterBitmap` sorgt für
|
/** Für das Zeichnen der Fotoanhang-Bitmaps (PDF-004). `isFilterBitmap` sorgt für
|
||||||
* weiche statt treppenartige Kanten beim Canvas-seitigen Skalieren. */
|
* weiche statt treppenartige Kanten beim Canvas-seitigen Skalieren. */
|
||||||
val photo = Paint().apply {
|
val photo = Paint().apply {
|
||||||
@@ -294,24 +277,23 @@ class AndroidPdfDocumentExporter(
|
|||||||
cursor.drawDivider(spacingBefore = 4f, spacingAfter = 12f)
|
cursor.drawDivider(spacingBefore = 4f, spacingAfter = 12f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Änderungswunsch: KEIN Checkbox-Symbol mehr im PDF. Da [PdfExportBlock.items] bereits nur
|
||||||
|
* angehakte Optionen enthält (s. Mapper), wäre ein "immer angekreuztes" Kästchen ohnehin
|
||||||
|
* redundant - stattdessen ein einfacher, leicht eingerückter Label-Text je Option. */
|
||||||
private fun drawItem(cursor: PdfPageCursor, item: PdfExportItem, paints: ExportPaints) {
|
private fun drawItem(cursor: PdfPageCursor, item: PdfExportItem, paints: ExportPaints) {
|
||||||
// Die Freitext-Option bekommt einen abschließenden Doppelpunkt, normale Optionen nicht.
|
// Die Freitext-Option bekommt einen abschließenden Doppelpunkt, normale Optionen nicht.
|
||||||
val labelText = if (item.isFreeText) "${item.label}:" else item.label
|
val labelText = if (item.isFreeText) "${item.label}:" else item.label
|
||||||
val checked = item.checked
|
|
||||||
|
|
||||||
cursor.drawText(
|
cursor.drawText(
|
||||||
text = labelText,
|
text = labelText,
|
||||||
paint = paints.item,
|
paint = paints.item,
|
||||||
indent = CHECKBOX_INDENT,
|
indent = ITEM_INDENT,
|
||||||
spacingAfter = 3f,
|
spacingAfter = 3f,
|
||||||
leadingMark = { canvas, left, top, bottom ->
|
|
||||||
drawCheckboxGlyph(canvas, left, top, bottom, checked, paints)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Freitext nur zeichnen, wenn die Option angehakt UND tatsächlich Text vorhanden ist -
|
// Freitext nur zeichnen, wenn tatsächlich Text vorhanden ist - ein angehakter, aber
|
||||||
// ein angehakter, aber leerer Freitext erzeugt keinen leeren Bereich im PDF.
|
// leerer Freitext erzeugt keinen leeren Bereich im PDF.
|
||||||
if (item.isFreeText && checked && item.freeTextValue.isNotBlank()) {
|
if (item.isFreeText && item.freeTextValue.isNotBlank()) {
|
||||||
cursor.drawText(
|
cursor.drawText(
|
||||||
text = item.freeTextValue,
|
text = item.freeTextValue,
|
||||||
paint = paints.freeText,
|
paint = paints.freeText,
|
||||||
@@ -321,33 +303,6 @@ class AndroidPdfDocumentExporter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Zeichnet ein Checkbox-Symbol rein per Canvas-Vektorprimitiven: ein Quadrat-Umriss, bei
|
|
||||||
* `checked = true` zusätzlich ein Kreuz darin. [top]/[bottom] sind die Geräte-Y-Koordinaten
|
|
||||||
* der zugehörigen ersten Textzeile - das Kästchen wird vertikal darauf zentriert.
|
|
||||||
*/
|
|
||||||
private fun drawCheckboxGlyph(
|
|
||||||
canvas: Canvas,
|
|
||||||
left: Float,
|
|
||||||
top: Float,
|
|
||||||
bottom: Float,
|
|
||||||
checked: Boolean,
|
|
||||||
paints: ExportPaints,
|
|
||||||
) {
|
|
||||||
val centerY = (top + bottom) / 2f
|
|
||||||
val boxTop = centerY - CHECKBOX_SIZE / 2f
|
|
||||||
val boxBottom = centerY + CHECKBOX_SIZE / 2f
|
|
||||||
val boxRight = left + CHECKBOX_SIZE
|
|
||||||
|
|
||||||
canvas.drawRect(left, boxTop, boxRight, boxBottom, paints.checkboxOutline)
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
val inset = CHECKBOX_SIZE * 0.2f
|
|
||||||
canvas.drawLine(left + inset, boxTop + inset, boxRight - inset, boxBottom - inset, paints.checkboxMark)
|
|
||||||
canvas.drawLine(boxRight - inset, boxTop + inset, left + inset, boxBottom - inset, paints.checkboxMark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeichnet den kompletten Signaturbereich (PDF-003): eine einmalige Überschrift
|
* Zeichnet den kompletten Signaturbereich (PDF-003): eine einmalige Überschrift
|
||||||
* "Unterschriften", gefolgt von einem atomaren Block je Signatur (bereits vom Mapper stabil
|
* "Unterschriften", gefolgt von einem atomaren Block je Signatur (bereits vom Mapper stabil
|
||||||
|
|||||||
@@ -28,12 +28,16 @@ object DocumentationToPdfExportModelMapper {
|
|||||||
.map { block ->
|
.map { block ->
|
||||||
PdfExportBlock(
|
PdfExportBlock(
|
||||||
title = block.title,
|
title = block.title,
|
||||||
|
// Änderungswunsch: nur angehakte Optionen erscheinen im PDF - nicht
|
||||||
|
// angehakte Checkboxen werden komplett weggelassen (nicht nur unausgefüllt
|
||||||
|
// dargestellt). Ein leeres Ergebnis ist hier ausgeschlossen: BlockEditedPolicy
|
||||||
|
// (Filter oben) lässt nur Blöcke mit mindestens einer angehakten Option durch.
|
||||||
items = block.items
|
items = block.items
|
||||||
|
.filter { it.checked }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
.map { item ->
|
.map { item ->
|
||||||
PdfExportItem(
|
PdfExportItem(
|
||||||
label = item.label,
|
label = item.label,
|
||||||
checked = item.checked,
|
|
||||||
isFreeText = item.isFreeText,
|
isFreeText = item.isFreeText,
|
||||||
freeTextValue = item.freeTextValue,
|
freeTextValue = item.freeTextValue,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,14 +28,15 @@ data class PdfExportModel(
|
|||||||
|
|
||||||
data class PdfExportBlock(
|
data class PdfExportBlock(
|
||||||
val title: String,
|
val title: String,
|
||||||
/** Bereits sortiert nach itemOrder. Enthält ALLE Optionen des Blocks (angehakt und nicht
|
/** Bereits gefiltert (NUR angehakte Optionen, s. Mapper - Änderungswunsch) und sortiert
|
||||||
* angehakt) - siehe Architekturentscheidung zur Checkbox-Darstellung. */
|
* (itemOrder). Nicht angehakte Optionen erscheinen im PDF überhaupt nicht mehr. */
|
||||||
val items: List<PdfExportItem>,
|
val items: List<PdfExportItem>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Kein `checked`-Feld mehr: Da [PdfExportBlock.items] bereits ausschließlich angehakte
|
||||||
|
* Optionen enthält, wäre es immer `true` und damit bedeutungslos - s. Mapper-Filter. */
|
||||||
data class PdfExportItem(
|
data class PdfExportItem(
|
||||||
val label: String,
|
val label: String,
|
||||||
val checked: Boolean,
|
|
||||||
val isFreeText: Boolean,
|
val isFreeText: Boolean,
|
||||||
val freeTextValue: String,
|
val freeTextValue: String,
|
||||||
)
|
)
|
||||||
|
|||||||
+124
-43
@@ -7,6 +7,7 @@ import androidx.activity.result.PickVisualMediaRequest
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -24,7 +25,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.BrokenImage
|
import androidx.compose.material.icons.filled.BrokenImage
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.PictureAsPdf
|
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -95,6 +99,13 @@ fun DocumentationEditScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
// Vollbildansicht eines Fotos beim Antippen einer Kachel (Änderungswunsch) - rein lokaler
|
||||||
|
// UI-Zustand, bewusst nicht im ViewModel gehalten (analog zu den transient geladenen
|
||||||
|
// Thumbnail-/Signatur-Bitmaps, s. Kommentare an PhotoThumbnail/loadPhotoThumbnail): welches
|
||||||
|
// Foto gerade großformatig angezeigt wird, ist reine Präsentationslogik ohne fachliche
|
||||||
|
// Bedeutung, die einen Prozesstod überdauern müsste.
|
||||||
|
var viewedPhoto by remember { mutableStateOf<Photo?>(null) }
|
||||||
|
|
||||||
// Best-effort-Flush ausstehender Freitext-Änderungen, wenn der Screen den sichtbaren
|
// Best-effort-Flush ausstehender Freitext-Änderungen, wenn der Screen den sichtbaren
|
||||||
// Bereich verlässt (App gewechselt, Bildschirm gesperrt, ...). KEINE Garantie bei hartem
|
// Bereich verlässt (App gewechselt, Bildschirm gesperrt, ...). KEINE Garantie bei hartem
|
||||||
// Prozessabbruch, aber sinnvolle zusätzliche Absicherung für den häufigen Fall.
|
// Prozessabbruch, aber sinnvolle zusätzliche Absicherung für den häufigen Fall.
|
||||||
@@ -153,8 +164,8 @@ fun DocumentationEditScreen(
|
|||||||
if (uiState.blockCount > 0) {
|
if (uiState.blockCount > 0) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.documentation_edit_position,
|
R.string.documentation_edit_progress,
|
||||||
uiState.currentBlockIndex + 1,
|
uiState.editedBlockCount,
|
||||||
uiState.blockCount,
|
uiState.blockCount,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -193,23 +204,20 @@ fun DocumentationEditScreen(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val documentation = uiState.documentation
|
val documentation = uiState.documentation
|
||||||
val currentBlock = documentation?.blocks?.getOrNull(uiState.currentBlockIndex)
|
if (documentation == null || documentation.blocks.isEmpty()) {
|
||||||
if (documentation == null || currentBlock == null) {
|
|
||||||
Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp)) {
|
Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp)) {
|
||||||
Text("Diese Dokumentation enthält keine Blöcke.")
|
Text("Diese Dokumentation enthält keine Blöcke.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DocumentationEditContent(
|
DocumentationEditContent(
|
||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
block = currentBlock,
|
blocks = documentation.blocks,
|
||||||
|
expandedBlockId = uiState.expandedBlockId,
|
||||||
|
onToggleBlockExpanded = viewModel::toggleBlockExpanded,
|
||||||
photos = documentation.photos,
|
photos = documentation.photos,
|
||||||
signatures = documentation.signatures,
|
signatures = documentation.signatures,
|
||||||
onCheckedChanged = viewModel::onCheckedChanged,
|
onCheckedChanged = viewModel::onCheckedChanged,
|
||||||
onFreeTextChanged = viewModel::onFreeTextChanged,
|
onFreeTextChanged = viewModel::onFreeTextChanged,
|
||||||
canGoBack = uiState.currentBlockIndex > 0,
|
|
||||||
canGoNext = uiState.currentBlockIndex < uiState.blockCount - 1,
|
|
||||||
onPrevious = viewModel::onPreviousBlock,
|
|
||||||
onNext = viewModel::onNextBlock,
|
|
||||||
photoImportInProgress = uiState.photoImportInProgress,
|
photoImportInProgress = uiState.photoImportInProgress,
|
||||||
photoImportError = uiState.photoImportError,
|
photoImportError = uiState.photoImportError,
|
||||||
onDismissPhotoImportError = viewModel::dismissPhotoImportError,
|
onDismissPhotoImportError = viewModel::dismissPhotoImportError,
|
||||||
@@ -220,6 +228,7 @@ fun DocumentationEditScreen(
|
|||||||
},
|
},
|
||||||
onDeletePhotoRequested = viewModel::requestDeletePhoto,
|
onDeletePhotoRequested = viewModel::requestDeletePhoto,
|
||||||
loadThumbnail = viewModel::loadPhotoThumbnail,
|
loadThumbnail = viewModel::loadPhotoThumbnail,
|
||||||
|
onPhotoClick = { viewedPhoto = it },
|
||||||
onAddSignatureClick = viewModel::requestAddSignature,
|
onAddSignatureClick = viewModel::requestAddSignature,
|
||||||
onDeleteSignatureRequested = viewModel::requestDeleteSignature,
|
onDeleteSignatureRequested = viewModel::requestDeleteSignature,
|
||||||
loadSignatureBitmap = viewModel::loadSignatureBitmap,
|
loadSignatureBitmap = viewModel::loadSignatureBitmap,
|
||||||
@@ -295,32 +304,44 @@ fun DocumentationEditScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Änderungswunsch: Foto in Originalgröße anzeigen.
|
||||||
|
val photoToView = viewedPhoto
|
||||||
|
if (photoToView != null) {
|
||||||
|
PhotoViewerDialog(
|
||||||
|
photo = photoToView,
|
||||||
|
loadFullSize = viewModel::loadPhotoFullSize,
|
||||||
|
onDismiss = { viewedPhoto = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gesamter scrollbarer Inhalt der Dokumentationsbearbeitung: aktueller Block (Titel, Optionen,
|
* Gesamter scrollbarer Inhalt der Dokumentationsbearbeitung: Akkordeon aller Blöcke
|
||||||
* Freitext-Option, Vor-/Zurück-Navigation) gefolgt vom dokumentationsweiten Fotobereich. Als
|
* (Änderungswunsch) - zunächst stehen nur die Überschriften untereinander, Antippen einer
|
||||||
|
* Überschrift klappt genau diesen Block auf und schließt dabei automatisch einen zuvor
|
||||||
|
* aufgeklappten (s. [DocumentationEditViewModel.toggleBlockExpanded] - IMMER höchstens ein
|
||||||
|
* Block gleichzeitig offen) - gefolgt vom dokumentationsweiten Signatur- und Fotobereich. Als
|
||||||
* einzelne [LazyColumn] umgesetzt, damit Foto-Zeilen erst decodiert werden, wenn sie sichtbar
|
* einzelne [LazyColumn] umgesetzt, damit Foto-Zeilen erst decodiert werden, wenn sie sichtbar
|
||||||
* sind/werden.
|
* sind/werden.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun DocumentationEditContent(
|
private fun DocumentationEditContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
block: DocumentationBlock,
|
blocks: List<DocumentationBlock>,
|
||||||
|
expandedBlockId: String?,
|
||||||
|
onToggleBlockExpanded: (blockStateId: String) -> Unit,
|
||||||
photos: List<Photo>,
|
photos: List<Photo>,
|
||||||
signatures: List<Signature>,
|
signatures: List<Signature>,
|
||||||
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
|
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
|
||||||
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
|
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
|
||||||
canGoBack: Boolean,
|
|
||||||
canGoNext: Boolean,
|
|
||||||
onPrevious: () -> Unit,
|
|
||||||
onNext: () -> Unit,
|
|
||||||
photoImportInProgress: Boolean,
|
photoImportInProgress: Boolean,
|
||||||
photoImportError: String?,
|
photoImportError: String?,
|
||||||
onDismissPhotoImportError: () -> Unit,
|
onDismissPhotoImportError: () -> Unit,
|
||||||
onAddPhotosClick: () -> Unit,
|
onAddPhotosClick: () -> Unit,
|
||||||
onDeletePhotoRequested: (photoId: String) -> Unit,
|
onDeletePhotoRequested: (photoId: String) -> Unit,
|
||||||
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||||
|
onPhotoClick: (Photo) -> Unit,
|
||||||
onAddSignatureClick: () -> Unit,
|
onAddSignatureClick: () -> Unit,
|
||||||
onDeleteSignatureRequested: (signatureId: String) -> Unit,
|
onDeleteSignatureRequested: (signatureId: String) -> Unit,
|
||||||
loadSignatureBitmap: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
loadSignatureBitmap: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||||
@@ -330,34 +351,14 @@ private fun DocumentationEditContent(
|
|||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
item(key = "block_title") {
|
items(blocks, key = { it.blockStateId }) { block ->
|
||||||
Text(block.title, style = MaterialTheme.typography.headlineSmall)
|
BlockAccordionItem(
|
||||||
}
|
block = block,
|
||||||
|
expanded = block.blockStateId == expandedBlockId,
|
||||||
items(block.items, key = { it.checkItemStateId }) { item ->
|
onHeaderClick = { onToggleBlockExpanded(block.blockStateId) },
|
||||||
if (item.isFreeText) {
|
|
||||||
FreeTextCheckItemRow(
|
|
||||||
item = item,
|
|
||||||
onCheckedChanged = onCheckedChanged,
|
onCheckedChanged = onCheckedChanged,
|
||||||
onFreeTextChanged = onFreeTextChanged,
|
onFreeTextChanged = onFreeTextChanged,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
CheckItemRow(item = item, onCheckedChanged = onCheckedChanged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item(key = "block_navigation") {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
OutlinedButton(onClick = onPrevious, enabled = canGoBack) {
|
|
||||||
Text(stringResource(R.string.documentation_edit_previous))
|
|
||||||
}
|
|
||||||
Button(onClick = onNext, enabled = canGoNext) {
|
|
||||||
Text(stringResource(R.string.documentation_edit_next))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MEDIA-003/004: dokumentationsweiter Signaturbereich, analog zum Fotobereich direkt
|
// MEDIA-003/004: dokumentationsweiter Signaturbereich, analog zum Fotobereich direkt
|
||||||
@@ -451,7 +452,12 @@ private fun DocumentationEditContent(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items(photos.chunked(PHOTO_GRID_COLUMNS), key = { row -> row.joinToString(separator = "_") { it.id } }) { row ->
|
items(photos.chunked(PHOTO_GRID_COLUMNS), key = { row -> row.joinToString(separator = "_") { it.id } }) { row ->
|
||||||
PhotoRow(photos = row, loadThumbnail = loadThumbnail, onDeleteRequested = onDeletePhotoRequested)
|
PhotoRow(
|
||||||
|
photos = row,
|
||||||
|
loadThumbnail = loadThumbnail,
|
||||||
|
onDeleteRequested = onDeletePhotoRequested,
|
||||||
|
onPhotoClick = onPhotoClick,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,6 +470,7 @@ private fun PhotoRow(
|
|||||||
photos: List<Photo>,
|
photos: List<Photo>,
|
||||||
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||||
onDeleteRequested: (photoId: String) -> Unit,
|
onDeleteRequested: (photoId: String) -> Unit,
|
||||||
|
onPhotoClick: (Photo) -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
photos.forEach { photo ->
|
photos.forEach { photo ->
|
||||||
@@ -471,6 +478,7 @@ private fun PhotoRow(
|
|||||||
photo = photo,
|
photo = photo,
|
||||||
loadThumbnail = loadThumbnail,
|
loadThumbnail = loadThumbnail,
|
||||||
onDeleteRequested = { onDeleteRequested(photo.id) },
|
onDeleteRequested = { onDeleteRequested(photo.id) },
|
||||||
|
onClick = { onPhotoClick(photo) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,12 +491,17 @@ private fun PhotoRow(
|
|||||||
* (siehe Kommentar an [DocumentationEditViewModel.loadPhotoThumbnail]). Verlässt die Kachel die
|
* (siehe Kommentar an [DocumentationEditViewModel.loadPhotoThumbnail]). Verlässt die Kachel die
|
||||||
* Komposition (z. B. weil die Dokumentation viele Fotos hat und diese Zeile aktuell nicht
|
* Komposition (z. B. weil die Dokumentation viele Fotos hat und diese Zeile aktuell nicht
|
||||||
* sichtbar ist), wird der Zustand automatisch freigegeben.
|
* sichtbar ist), wird der Zustand automatisch freigegeben.
|
||||||
|
*
|
||||||
|
* Ein Antippen der Kachel (außerhalb des Löschen-Buttons) öffnet die Vollbildansicht
|
||||||
|
* (Änderungswunsch) - [onClick] statt eines eigenen Ladevorgangs hier, da die Kachel selbst nur
|
||||||
|
* das kleine, bereits geladene Thumbnail kennt.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun PhotoThumbnail(
|
private fun PhotoThumbnail(
|
||||||
photo: Photo,
|
photo: Photo,
|
||||||
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||||
onDeleteRequested: () -> Unit,
|
onDeleteRequested: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var bitmap by remember(photo.id) { mutableStateOf<Bitmap?>(null) }
|
var bitmap by remember(photo.id) { mutableStateOf<Bitmap?>(null) }
|
||||||
var loadFailed by remember(photo.id) { mutableStateOf(false) }
|
var loadFailed by remember(photo.id) { mutableStateOf(false) }
|
||||||
@@ -503,7 +516,8 @@ private fun PhotoThumbnail(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(96.dp)
|
.size(96.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
.clickable(enabled = bitmap != null, onClick = onClick),
|
||||||
) {
|
) {
|
||||||
val currentBitmap = bitmap
|
val currentBitmap = bitmap
|
||||||
when {
|
when {
|
||||||
@@ -611,6 +625,73 @@ private fun SignatureRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einzelner Block im Akkordeon (Änderungswunsch): eine antippbare Kopfzeile mit Titel,
|
||||||
|
* einem kleinen "bearbeitet"-Indikator (grüner Haken, s. [DocumentationBlock.isEdited]) und
|
||||||
|
* einem Pfeil-Icon, das den Auf-/Zugeklappt-Zustand anzeigt. Der eigentliche Inhalt (Checkboxen
|
||||||
|
* inkl. Freitext-Option) wird ausschließlich gerendert, wenn [expanded] `true` ist - das
|
||||||
|
* entspricht fachlich einem Dropdown/Expansion-Panel, technisch einfach bedingtes Komponieren
|
||||||
|
* ohne eigene Lazy-Liste (ein Block hat nur wenige Optionen).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun BlockAccordionItem(
|
||||||
|
block: DocumentationBlock,
|
||||||
|
expanded: Boolean,
|
||||||
|
onHeaderClick: () -> Unit,
|
||||||
|
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
|
||||||
|
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onHeaderClick)
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (block.isEdited) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.CheckCircle,
|
||||||
|
contentDescription = stringResource(R.string.documentation_edit_block_edited),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
block.title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 4.dp, bottom = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
for (item in block.items) {
|
||||||
|
if (item.isFreeText) {
|
||||||
|
FreeTextCheckItemRow(
|
||||||
|
item = item,
|
||||||
|
onCheckedChanged = onCheckedChanged,
|
||||||
|
onFreeTextChanged = onFreeTextChanged,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CheckItemRow(item = item, onCheckedChanged = onCheckedChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CheckItemRow(
|
private fun CheckItemRow(
|
||||||
item: CheckItem,
|
item: CheckItem,
|
||||||
|
|||||||
+26
-11
@@ -28,7 +28,11 @@ import java.io.File
|
|||||||
|
|
||||||
data class DocumentationEditUiState(
|
data class DocumentationEditUiState(
|
||||||
val documentation: Documentation? = null,
|
val documentation: Documentation? = null,
|
||||||
val currentBlockIndex: Int = 0,
|
/** ID des BlockState, der gerade im Akkordeon aufgeklappt ist - `null`, wenn keiner
|
||||||
|
* aufgeklappt ist (Änderungswunsch: zunächst nur alle Überschriften untereinander, Antippen
|
||||||
|
* einer Überschrift klappt sie auf). Es ist IMMER höchstens ein Block gleichzeitig
|
||||||
|
* aufgeklappt, s. [DocumentationEditViewModel.toggleBlockExpanded]. */
|
||||||
|
val expandedBlockId: String? = null,
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
/** true, während ausgewählte Fotos importiert werden (z. B. für einen Ladeindikator). */
|
/** true, während ausgewählte Fotos importiert werden (z. B. für einen Ladeindikator). */
|
||||||
@@ -61,6 +65,13 @@ data class DocumentationEditUiState(
|
|||||||
val lastExportedPdfFile: File? = null,
|
val lastExportedPdfFile: File? = null,
|
||||||
) {
|
) {
|
||||||
val blockCount: Int get() = documentation?.blocks?.size ?: 0
|
val blockCount: Int get() = documentation?.blocks?.size ?: 0
|
||||||
|
|
||||||
|
/** Anzahl bereits bearbeiteter Blöcke (s. [de.medapp.wunddoku.domain.usecase.BlockEditedPolicy])
|
||||||
|
* - ersetzt die frühere "X / Y"-Positionsanzeige, die sich auf den einzelnen, gerade
|
||||||
|
* angezeigten Block bezog; im Akkordeon-Layout sind ALLE Blöcke gleichzeitig sichtbar
|
||||||
|
* (nur der Inhalt ist ein-/ausklappbar), daher ist ein Fortschritt sinnvoller als eine
|
||||||
|
* Position. */
|
||||||
|
val editedBlockCount: Int get() = documentation?.blocks?.count { it.isEdited } ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,17 +174,17 @@ class DocumentationEditViewModel(
|
|||||||
viewModelScope.launch { repository.setChecked(checkItemStateId, checked) }
|
viewModelScope.launch { repository.setChecked(checkItemStateId, checked) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNextBlock() {
|
/**
|
||||||
|
* Klappt den Block mit [blockStateId] auf bzw. zu (Änderungswunsch: Akkordeon statt
|
||||||
|
* Vor-/Zurück-Navigation). Antippen eines bereits aufgeklappten Blocks klappt ihn zu;
|
||||||
|
* Antippen eines anderen Blocks klappt diesen auf und ersetzt dabei automatisch einen zuvor
|
||||||
|
* aufgeklappten Block - es ist also IMMER höchstens ein Block gleichzeitig aufgeklappt,
|
||||||
|
* ganz ohne zusätzliche Schließen-Logik für den vorherigen.
|
||||||
|
*/
|
||||||
|
fun toggleBlockExpanded(blockStateId: String) {
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
val lastIndex = state.blockCount - 1
|
val newExpandedId = if (state.expandedBlockId == blockStateId) null else blockStateId
|
||||||
if (lastIndex < 0) return@update state
|
state.copy(expandedBlockId = newExpandedId)
|
||||||
state.copy(currentBlockIndex = (state.currentBlockIndex + 1).coerceAtMost(lastIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPreviousBlock() {
|
|
||||||
_uiState.update { state ->
|
|
||||||
state.copy(currentBlockIndex = (state.currentBlockIndex - 1).coerceAtLeast(0))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +376,10 @@ class DocumentationEditViewModel(
|
|||||||
suspend fun loadPhotoThumbnail(documentationId: String, fileName: String) =
|
suspend fun loadPhotoThumbnail(documentationId: String, fileName: String) =
|
||||||
repository.loadPhotoThumbnail(documentationId, fileName)
|
repository.loadPhotoThumbnail(documentationId, fileName)
|
||||||
|
|
||||||
|
/** Für die Vollbildansicht beim Antippen einer Foto-Kachel (Änderungswunsch). */
|
||||||
|
suspend fun loadPhotoFullSize(documentationId: String, fileName: String, maxWidthPx: Int, maxHeightPx: Int) =
|
||||||
|
repository.loadPhotoFullSize(documentationId, fileName, maxWidthPx, maxHeightPx)
|
||||||
|
|
||||||
/** Lädt eine Signatur für die Vorschau in der Dokumentationsbearbeitung - analog
|
/** Lädt eine Signatur für die Vorschau in der Dokumentationsbearbeitung - analog
|
||||||
* [loadPhotoThumbnail], siehe dortigen Kommentar zur bewussten Nicht-Haltung im UiState. */
|
* [loadPhotoThumbnail], siehe dortigen Kommentar zur bewussten Nicht-Haltung im UiState. */
|
||||||
suspend fun loadSignatureBitmap(documentationId: String, fileName: String) =
|
suspend fun loadSignatureBitmap(documentationId: String, fileName: String) =
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package de.medapp.wunddoku.ui.documentation.edit
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BrokenImage
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import de.medapp.wunddoku.R
|
||||||
|
import de.medapp.wunddoku.domain.model.Photo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vollbild-Ansicht eines einzelnen Fotos in Originalgröße (Änderungswunsch: "Fotos in der App
|
||||||
|
* durch Antippen in Originalgröße angezeigt werden"). Lädt das Foto über [loadFullSize] separat
|
||||||
|
* von der kleinen Grid-Vorschau ([DocumentationEditViewModel.loadPhotoThumbnail]) - dieselbe
|
||||||
|
* Trennung wie bei den Signatur-Vorschauen: die kleine Kachel und die Vollbildansicht sind
|
||||||
|
* unabhängige Ladevorgänge mit unterschiedlicher Zielauflösung.
|
||||||
|
*
|
||||||
|
* "Originalgröße" bedeutet hier: so groß wie der Bildschirm es zulässt, ohne unnötig über die
|
||||||
|
* tatsächliche Displayauflösung hinaus zu decodieren (s. Kommentar an
|
||||||
|
* [de.medapp.wunddoku.data.repository.DocumentationRepository.loadPhotoFullSize]) - eine echte
|
||||||
|
* 1:1-Pixel-Darstellung würde bei modernen Fotoauflösungen ohnehin über die Bildschirmgröße
|
||||||
|
* hinausgehen und wäre nur per Zoom/Scroll sichtbar, was hier nicht gefordert ist.
|
||||||
|
*
|
||||||
|
* Volle Bildschirmbreite/-höhe via [DialogProperties.usePlatformDefaultWidth] = false, da der
|
||||||
|
* Standard-Dialog-Rahmen von Compose sonst deutlich schmaler als der Bildschirm wäre. Schließen
|
||||||
|
* über den Schließen-Button ODER Antippen des dunklen Hintergrunds.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PhotoViewerDialog(
|
||||||
|
photo: Photo,
|
||||||
|
loadFullSize: suspend (documentationId: String, fileName: String, maxWidthPx: Int, maxHeightPx: Int) -> Bitmap?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
var bitmap by remember(photo.id) { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var loadFailed by remember(photo.id) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
LaunchedEffect(photo.id) {
|
||||||
|
val maxWidthPx = with(density) { configuration.screenWidthDp.dp.roundToPx() }
|
||||||
|
val maxHeightPx = with(density) { configuration.screenHeightDp.dp.roundToPx() }
|
||||||
|
val result = runCatching {
|
||||||
|
loadFullSize(photo.documentationId, photo.fileName, maxWidthPx, maxHeightPx)
|
||||||
|
}.getOrNull()
|
||||||
|
bitmap = result
|
||||||
|
loadFailed = result == null
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
.clickable(onClick = onDismiss),
|
||||||
|
) {
|
||||||
|
val currentBitmap = bitmap
|
||||||
|
when {
|
||||||
|
currentBitmap != null -> Image(
|
||||||
|
bitmap = currentBitmap.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
|
||||||
|
loadFailed -> Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.BrokenImage,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.align(Alignment.Center).size(48.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.documentation_edit_photo_load_failed),
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter).padding(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> CircularProgressIndicator(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
color = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.action_cancel), tint = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,14 +32,14 @@
|
|||||||
<string name="patient_form_phone3">Telefonnummer 3 (optional)</string>
|
<string name="patient_form_phone3">Telefonnummer 3 (optional)</string>
|
||||||
<string name="patient_form_save">Speichern</string>
|
<string name="patient_form_save">Speichern</string>
|
||||||
|
|
||||||
<string name="documentation_edit_position">%1$d / %2$d</string>
|
<string name="documentation_edit_progress">%1$d / %2$d bearbeitet</string>
|
||||||
<string name="documentation_edit_previous">Zurück</string>
|
<string name="documentation_edit_block_edited">Block bearbeitet</string>
|
||||||
<string name="documentation_edit_next">Weiter</string>
|
|
||||||
<string name="documentation_edit_free_text_label">Freitext</string>
|
<string name="documentation_edit_free_text_label">Freitext</string>
|
||||||
|
|
||||||
<string name="documentation_edit_photos_title">Fotos</string>
|
<string name="documentation_edit_photos_title">Fotos</string>
|
||||||
<string name="documentation_edit_add_photos">Hinzufügen</string>
|
<string name="documentation_edit_add_photos">Hinzufügen</string>
|
||||||
<string name="documentation_edit_no_photos">Noch keine Fotos vorhanden.</string>
|
<string name="documentation_edit_no_photos">Noch keine Fotos vorhanden.</string>
|
||||||
|
<string name="documentation_edit_photo_load_failed">Foto konnte nicht geladen werden.</string>
|
||||||
<string name="documentation_edit_delete_photo_confirm_title">Foto löschen?</string>
|
<string name="documentation_edit_delete_photo_confirm_title">Foto löschen?</string>
|
||||||
<string name="documentation_edit_delete_photo_confirm_text">Dieses Foto wird endgültig aus der Dokumentation entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
<string name="documentation_edit_delete_photo_confirm_text">Dieses Foto wird endgültig aus der Dokumentation entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user