Kundenwuensche
Nur angehakte Checkboxen im PDF Fotos in Originalgröße per Antippen Freitext zuerst in jedem Block Akkordeon-Umbau (angefangen)
This commit is contained in:
+127
-46
@@ -7,6 +7,7 @@ import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.filled.Add
|
||||
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.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
@@ -95,6 +99,13 @@ fun DocumentationEditScreen(
|
||||
) {
|
||||
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
|
||||
// Bereich verlässt (App gewechselt, Bildschirm gesperrt, ...). KEINE Garantie bei hartem
|
||||
// Prozessabbruch, aber sinnvolle zusätzliche Absicherung für den häufigen Fall.
|
||||
@@ -153,8 +164,8 @@ fun DocumentationEditScreen(
|
||||
if (uiState.blockCount > 0) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.documentation_edit_position,
|
||||
uiState.currentBlockIndex + 1,
|
||||
R.string.documentation_edit_progress,
|
||||
uiState.editedBlockCount,
|
||||
uiState.blockCount,
|
||||
)
|
||||
)
|
||||
@@ -193,23 +204,20 @@ fun DocumentationEditScreen(
|
||||
|
||||
else -> {
|
||||
val documentation = uiState.documentation
|
||||
val currentBlock = documentation?.blocks?.getOrNull(uiState.currentBlockIndex)
|
||||
if (documentation == null || currentBlock == null) {
|
||||
if (documentation == null || documentation.blocks.isEmpty()) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp)) {
|
||||
Text("Diese Dokumentation enthält keine Blöcke.")
|
||||
}
|
||||
} else {
|
||||
DocumentationEditContent(
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
block = currentBlock,
|
||||
blocks = documentation.blocks,
|
||||
expandedBlockId = uiState.expandedBlockId,
|
||||
onToggleBlockExpanded = viewModel::toggleBlockExpanded,
|
||||
photos = documentation.photos,
|
||||
signatures = documentation.signatures,
|
||||
onCheckedChanged = viewModel::onCheckedChanged,
|
||||
onFreeTextChanged = viewModel::onFreeTextChanged,
|
||||
canGoBack = uiState.currentBlockIndex > 0,
|
||||
canGoNext = uiState.currentBlockIndex < uiState.blockCount - 1,
|
||||
onPrevious = viewModel::onPreviousBlock,
|
||||
onNext = viewModel::onNextBlock,
|
||||
photoImportInProgress = uiState.photoImportInProgress,
|
||||
photoImportError = uiState.photoImportError,
|
||||
onDismissPhotoImportError = viewModel::dismissPhotoImportError,
|
||||
@@ -220,6 +228,7 @@ fun DocumentationEditScreen(
|
||||
},
|
||||
onDeletePhotoRequested = viewModel::requestDeletePhoto,
|
||||
loadThumbnail = viewModel::loadPhotoThumbnail,
|
||||
onPhotoClick = { viewedPhoto = it },
|
||||
onAddSignatureClick = viewModel::requestAddSignature,
|
||||
onDeleteSignatureRequested = viewModel::requestDeleteSignature,
|
||||
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,
|
||||
* Freitext-Option, Vor-/Zurück-Navigation) gefolgt vom dokumentationsweiten Fotobereich. Als
|
||||
* Gesamter scrollbarer Inhalt der Dokumentationsbearbeitung: Akkordeon aller Blöcke
|
||||
* (Ä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
|
||||
* sind/werden.
|
||||
*/
|
||||
@Composable
|
||||
private fun DocumentationEditContent(
|
||||
modifier: Modifier = Modifier,
|
||||
block: DocumentationBlock,
|
||||
blocks: List<DocumentationBlock>,
|
||||
expandedBlockId: String?,
|
||||
onToggleBlockExpanded: (blockStateId: String) -> Unit,
|
||||
photos: List<Photo>,
|
||||
signatures: List<Signature>,
|
||||
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
|
||||
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
|
||||
canGoBack: Boolean,
|
||||
canGoNext: Boolean,
|
||||
onPrevious: () -> Unit,
|
||||
onNext: () -> Unit,
|
||||
photoImportInProgress: Boolean,
|
||||
photoImportError: String?,
|
||||
onDismissPhotoImportError: () -> Unit,
|
||||
onAddPhotosClick: () -> Unit,
|
||||
onDeletePhotoRequested: (photoId: String) -> Unit,
|
||||
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||
onPhotoClick: (Photo) -> Unit,
|
||||
onAddSignatureClick: () -> Unit,
|
||||
onDeleteSignatureRequested: (signatureId: String) -> Unit,
|
||||
loadSignatureBitmap: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||
@@ -330,34 +351,14 @@ private fun DocumentationEditContent(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item(key = "block_title") {
|
||||
Text(block.title, style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
|
||||
items(block.items, key = { it.checkItemStateId }) { item ->
|
||||
if (item.isFreeText) {
|
||||
FreeTextCheckItemRow(
|
||||
item = item,
|
||||
onCheckedChanged = onCheckedChanged,
|
||||
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))
|
||||
}
|
||||
}
|
||||
items(blocks, key = { it.blockStateId }) { block ->
|
||||
BlockAccordionItem(
|
||||
block = block,
|
||||
expanded = block.blockStateId == expandedBlockId,
|
||||
onHeaderClick = { onToggleBlockExpanded(block.blockStateId) },
|
||||
onCheckedChanged = onCheckedChanged,
|
||||
onFreeTextChanged = onFreeTextChanged,
|
||||
)
|
||||
}
|
||||
|
||||
// MEDIA-003/004: dokumentationsweiter Signaturbereich, analog zum Fotobereich direkt
|
||||
@@ -451,7 +452,12 @@ private fun DocumentationEditContent(
|
||||
}
|
||||
} else {
|
||||
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>,
|
||||
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||
onDeleteRequested: (photoId: String) -> Unit,
|
||||
onPhotoClick: (Photo) -> Unit,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
photos.forEach { photo ->
|
||||
@@ -471,6 +478,7 @@ private fun PhotoRow(
|
||||
photo = photo,
|
||||
loadThumbnail = loadThumbnail,
|
||||
onDeleteRequested = { onDeleteRequested(photo.id) },
|
||||
onClick = { onPhotoClick(photo) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -483,12 +491,17 @@ private fun PhotoRow(
|
||||
* (siehe Kommentar an [DocumentationEditViewModel.loadPhotoThumbnail]). Verlässt die Kachel die
|
||||
* Komposition (z. B. weil die Dokumentation viele Fotos hat und diese Zeile aktuell nicht
|
||||
* 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
|
||||
private fun PhotoThumbnail(
|
||||
photo: Photo,
|
||||
loadThumbnail: suspend (documentationId: String, fileName: String) -> Bitmap?,
|
||||
onDeleteRequested: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
var bitmap by remember(photo.id) { mutableStateOf<Bitmap?>(null) }
|
||||
var loadFailed by remember(photo.id) { mutableStateOf(false) }
|
||||
@@ -503,7 +516,8 @@ private fun PhotoThumbnail(
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.clickable(enabled = bitmap != null, onClick = onClick),
|
||||
) {
|
||||
val currentBitmap = bitmap
|
||||
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
|
||||
private fun CheckItemRow(
|
||||
item: CheckItem,
|
||||
|
||||
+26
-11
@@ -28,7 +28,11 @@ import java.io.File
|
||||
|
||||
data class DocumentationEditUiState(
|
||||
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 error: String? = null,
|
||||
/** 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 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) }
|
||||
}
|
||||
|
||||
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 ->
|
||||
val lastIndex = state.blockCount - 1
|
||||
if (lastIndex < 0) return@update state
|
||||
state.copy(currentBlockIndex = (state.currentBlockIndex + 1).coerceAtMost(lastIndex))
|
||||
}
|
||||
}
|
||||
|
||||
fun onPreviousBlock() {
|
||||
_uiState.update { state ->
|
||||
state.copy(currentBlockIndex = (state.currentBlockIndex - 1).coerceAtLeast(0))
|
||||
val newExpandedId = if (state.expandedBlockId == blockStateId) null else blockStateId
|
||||
state.copy(expandedBlockId = newExpandedId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +376,10 @@ class DocumentationEditViewModel(
|
||||
suspend fun loadPhotoThumbnail(documentationId: String, fileName: String) =
|
||||
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
|
||||
* [loadPhotoThumbnail], siehe dortigen Kommentar zur bewussten Nicht-Haltung im UiState. */
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user