OverlayShell in Widget ausgelagert, zur wiederverwendbarkeit

This commit is contained in:
2026-01-18 18:08:42 +01:00
parent 407316a4c5
commit c9b2b262a8
2 changed files with 220 additions and 221 deletions

View File

@@ -130,14 +130,6 @@ fun ContentScreen(
if (showReport) {
ReportOverlay(
onCancel = onDismissReport,
onAdd = { beschreibung, typ ->
mapViewModel.createFeatureAtCurrentLocation(
beschreibung = beschreibung,
typ = typ,
photos = albumViewModel.viewStateFlow.value.selectedPictures
)
onDismissReport()
},
onClose = onDismissReport,
viewModel = albumViewModel,
mapViewModel = mapViewModel
@@ -190,7 +182,6 @@ fun AppTopBar(
@Composable
fun ReportOverlay(
onCancel: () -> Unit,
onAdd: (beschreibung: String, typ: String) -> Unit,
onClose: () -> Unit,
viewModel: AlbumViewModel,
mapViewModel: MapViewModel
@@ -201,93 +192,64 @@ fun ReportOverlay(
var dropdownExpanded by remember { mutableStateOf(false) }
LaunchedEffect(viewState.selectedPictures) {
mapViewModel.updateReportDraft {
copy(photos = viewState.selectedPictures)
}
mapViewModel.updateReportDraft { copy(photos = viewState.selectedPictures) }
}
// Launcher für Bildauswahl aus Galerie
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(20)
) { urls ->
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
}
// Launcher für Kamera
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
) { isImageSaved ->
if (isImageSaved) {
viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
} else {
viewModel.onReceive(Intent.OnImageSavingCanceled)
}
if (isImageSaved) viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
else viewModel.onReceive(Intent.OnImageSavingCanceled)
}
// Launcher für Kamera-Berechtigung
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { permissionGranted ->
if (permissionGranted) {
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
} else {
viewModel.onReceive(Intent.OnPermissionDenied)
}
if (permissionGranted) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
else viewModel.onReceive(Intent.OnPermissionDenied)
}
// Funktion zum Starten der Kamera
fun startCamera() {
val hasPermission = currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
if (hasPermission) {
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
} else {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
if (hasPermission) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
else permissionLauncher.launch(Manifest.permission.CAMERA)
}
// Kamera starten, wenn tempFileUrl gesetzt ist
LaunchedEffect(key1 = viewState.tempFileUrl) {
viewState.tempFileUrl?.let {
cameraLauncher.launch(it)
}
LaunchedEffect(viewState.tempFileUrl) {
viewState.tempFileUrl?.let { cameraLauncher.launch(it) }
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.25f)),
contentAlignment = Alignment.Center
) {
BoxWithConstraints {
val verticalMargin = 50.dp // <-- dein gewünschter Mindestabstand oben + unten
val maxCardHeight = maxHeight - verticalMargin * 2
OverlayShell(
title = "Neue Meldung",
footer = {
OutlinedButton(
onClick = {
mapViewModel.resetDraft()
viewModel.clearSelection()
onCancel()
}
) { Text("Abbrechen", color = Color.Black) }
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.heightIn(
min = 400.dp,
max = maxCardHeight
),
shape = RoundedCornerShape(24.dp),
colors = CardColors(
containerColor = WidgetColor,
contentColor = ButtonColor,
disabledContainerColor = Color.White,
disabledContentColor = Color.White
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
Button(
onClick = {
mapViewModel.submitDraftToLayer()
viewModel.clearSelection()
onCancel()
},
enabled = mapViewModel.reportDraft.isValid
) { Text("Hinzufügen") }
}
) {
Text("Schadensbeschreibung:", color = Color.Black)
// Typ-Auswahl (Dropdown)
// Typ Dropdown
Box {
OutlinedButton(
onClick = { dropdownExpanded = true },
@@ -299,21 +261,11 @@ fun ReportOverlay(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
) {
listOf(
"Straße",
"Gehweg",
"Fahrradweg",
"Beleuchtung",
"Sonstiges"
).forEach { typ ->
listOf("Straße", "Gehweg", "Fahrradweg", "Beleuchtung", "Sonstiges").forEach { typ ->
DropdownMenuItem(
text = {
Text(typ)
},
text = { Text(typ) },
onClick = {
mapViewModel.updateReportDraft {
copy(typ = typ)
}
mapViewModel.updateReportDraft { copy(typ = typ) }
dropdownExpanded = false
}
)
@@ -321,16 +273,13 @@ fun ReportOverlay(
}
}
// Kamera-Buttons
// Kamera Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { startCamera() },
modifier = Modifier.weight(1f)
) {
Text(text = "Foto aufnehmen")
Button(onClick = { startCamera() }, modifier = Modifier.weight(1f)) {
Text("Foto aufnehmen")
}
Button(
onClick = {
@@ -340,21 +289,15 @@ fun ReportOverlay(
},
modifier = Modifier.weight(1f)
) {
Text(text = "Aus Galerie")
Text("Aus Galerie")
}
}
// Textfeld für Beschreibung
// Beschreibung
TextField(
value = mapViewModel.reportDraft.beschreibung,
onValueChange = {
mapViewModel.updateReportDraft {
copy(beschreibung = it)
}
},
modifier = Modifier
.fillMaxWidth()
.height(220.dp),
onValueChange = { text -> mapViewModel.updateReportDraft { copy(beschreibung = text) } },
modifier = Modifier.fillMaxWidth().height(220.dp),
placeholder = { Text("Beschreibung eingeben...") },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.White,
@@ -362,7 +305,7 @@ fun ReportOverlay(
)
)
// Bilder-Grid
// Bilder Grid (body ist scrollbar, grid selbst nicht)
if (viewState.selectedPictures.isNotEmpty()) {
Text(
text = "Ausgewählte Bilder (${viewState.selectedPictures.size})",
@@ -371,9 +314,7 @@ fun ReportOverlay(
LazyVerticalGrid(
columns = GridCells.Fixed(3),
userScrollEnabled = false,
modifier = Modifier
.fillMaxWidth()
.heightIn(0.dp, 200.dp)
modifier = Modifier.fillMaxWidth().heightIn(0.dp, 200.dp)
) {
itemsIndexed(viewState.selectedPictures) { index, picture ->
Image(
@@ -386,60 +327,22 @@ fun ReportOverlay(
}
}
// Position Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = {
mapViewModel.pickCurrentLocation()
},
) {
Text(if (hasPoint) "neue Position aus Standort" else "Position aus Standort")
Button(onClick = { mapViewModel.pickCurrentLocation() }) {
Text(if (hasPoint) "Neue Position aus Standort" else "Position aus Standort")
}
Button(
onClick = {
mapViewModel.startPickReportLocation()
onClose()
},
}
) {
Text(if (hasPoint) "Position neu setzen" else "Position manuell setzen")
}
}
// Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(
onClick = {
mapViewModel.resetDraft()
viewModel.clearSelection()
onCancel()
},
colors = ButtonColors(
containerColor = ButtonColor,
contentColor = Color.White,
disabledContainerColor = Color.White,
disabledContentColor = Color.White
)
) {
Text("Abbrechen", color = Color.Black)
}
Button(
onClick = {
mapViewModel.submitDraftToLayer()
viewModel.clearSelection()
onCancel()
},
enabled = mapViewModel.reportDraft.isValid
) {
Text("Hinzufügen")
}
}
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
package com.example.snapandsolve.ui.theme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun OverlayShell(
title: String,
footer: @Composable RowScope.() -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.25f)),
contentAlignment = Alignment.Center
) {
BoxWithConstraints {
val verticalMargin = 50.dp
val maxCardHeight = maxHeight - verticalMargin * 2
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.heightIn(min = 400.dp, max = maxCardHeight),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = WidgetColor)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = Color.Black
)
}
Spacer(Modifier.height(12.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f, fill = true)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
content = content
)
Spacer(Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
content = footer
)
}
}
}
}
}