From c9b2b262a82e409883d7d67022dd5752dc9d78b3 Mon Sep 17 00:00:00 2001 From: fr2651 Date: Sun, 18 Jan 2026 18:08:42 +0100 Subject: [PATCH] OverlayShell in Widget ausgelagert, zur wiederverwendbarkeit --- .../com/example/snapandsolve/MainScreen.kt | 345 +++++++----------- .../example/snapandsolve/ui/theme/Widget.kt | 96 +++++ 2 files changed, 220 insertions(+), 221 deletions(-) create mode 100644 app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt diff --git a/app/src/main/java/com/example/snapandsolve/MainScreen.kt b/app/src/main/java/com/example/snapandsolve/MainScreen.kt index 1ffaa96..b052dd7 100644 --- a/app/src/main/java/com/example/snapandsolve/MainScreen.kt +++ b/app/src/main/java/com/example/snapandsolve/MainScreen.kt @@ -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,244 +192,156 @@ 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 - - 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) - ) { - Text("Schadensbeschreibung:", color = Color.Black) - - // Typ-Auswahl (Dropdown) - Box { - OutlinedButton( - onClick = { dropdownExpanded = true }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Typ: ${mapViewModel.reportDraft.typ}") - } - DropdownMenu( - expanded = dropdownExpanded, - onDismissRequest = { dropdownExpanded = false } - ) { - listOf( - "Straße", - "Gehweg", - "Fahrradweg", - "Beleuchtung", - "Sonstiges" - ).forEach { typ -> - DropdownMenuItem( - text = { - Text(typ) - }, - onClick = { - mapViewModel.updateReportDraft { - copy(typ = typ) - } - dropdownExpanded = false - } - ) - } - } - } - - // Kamera-Buttons - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { startCamera() }, - modifier = Modifier.weight(1f) - ) { - Text(text = "Foto aufnehmen") - } - Button( - onClick = { - pickImageFromAlbumLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - }, - modifier = Modifier.weight(1f) - ) { - Text(text = "Aus Galerie") - } - } - - // Textfeld für Beschreibung - TextField( - value = mapViewModel.reportDraft.beschreibung, - onValueChange = { - mapViewModel.updateReportDraft { - copy(beschreibung = it) - } - }, - modifier = Modifier - .fillMaxWidth() - .height(220.dp), - placeholder = { Text("Beschreibung eingeben...") }, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.White, - unfocusedContainerColor = Color.White - ) - ) - - // Bilder-Grid - if (viewState.selectedPictures.isNotEmpty()) { - Text( - text = "Ausgewählte Bilder (${viewState.selectedPictures.size})", - color = Color.Black - ) - LazyVerticalGrid( - columns = GridCells.Fixed(3), - userScrollEnabled = false, - modifier = Modifier - .fillMaxWidth() - .heightIn(0.dp, 200.dp) - ) { - itemsIndexed(viewState.selectedPictures) { index, picture -> - Image( - modifier = Modifier.padding(4.dp), - bitmap = picture, - contentDescription = "Bild ${index + 1}", - contentScale = ContentScale.Crop - ) - } - } - } - - 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.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") - } - } + OverlayShell( + title = "Neue Meldung", + footer = { + OutlinedButton( + onClick = { + mapViewModel.resetDraft() + viewModel.clearSelection() + onCancel() } + ) { Text("Abbrechen", color = Color.Black) } + + Button( + onClick = { + mapViewModel.submitDraftToLayer() + viewModel.clearSelection() + onCancel() + }, + enabled = mapViewModel.reportDraft.isValid + ) { Text("Hinzufügen") } + } + ) { + Text("Schadensbeschreibung:", color = Color.Black) + + // Typ Dropdown + Box { + OutlinedButton( + onClick = { dropdownExpanded = true }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Typ: ${mapViewModel.reportDraft.typ}") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + listOf("Straße", "Gehweg", "Fahrradweg", "Beleuchtung", "Sonstiges").forEach { typ -> + DropdownMenuItem( + text = { Text(typ) }, + onClick = { + mapViewModel.updateReportDraft { copy(typ = typ) } + dropdownExpanded = false + } + ) + } + } + } + + // Kamera Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button(onClick = { startCamera() }, modifier = Modifier.weight(1f)) { + Text("Foto aufnehmen") + } + Button( + onClick = { + pickImageFromAlbumLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + }, + modifier = Modifier.weight(1f) + ) { + Text("Aus Galerie") + } + } + + // Beschreibung + TextField( + value = mapViewModel.reportDraft.beschreibung, + onValueChange = { text -> mapViewModel.updateReportDraft { copy(beschreibung = text) } }, + modifier = Modifier.fillMaxWidth().height(220.dp), + placeholder = { Text("Beschreibung eingeben...") }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White + ) + ) + + // Bilder Grid (body ist scrollbar, grid selbst nicht) + if (viewState.selectedPictures.isNotEmpty()) { + Text( + text = "Ausgewählte Bilder (${viewState.selectedPictures.size})", + color = Color.Black + ) + LazyVerticalGrid( + columns = GridCells.Fixed(3), + userScrollEnabled = false, + modifier = Modifier.fillMaxWidth().heightIn(0.dp, 200.dp) + ) { + itemsIndexed(viewState.selectedPictures) { index, picture -> + Image( + modifier = Modifier.padding(4.dp), + bitmap = picture, + contentDescription = "Bild ${index + 1}", + contentScale = ContentScale.Crop + ) + } + } + } + + // 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.startPickReportLocation() + onClose() + } + ) { + Text(if (hasPoint) "Position neu setzen" else "Position manuell setzen") } } } diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt new file mode 100644 index 0000000..53c2e3c --- /dev/null +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt @@ -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 + ) + } + } + } + } +} +