status hinzugefügt

regelbasiertes styling
architektur überarbeitet
This commit is contained in:
2026-02-06 15:34:39 +01:00
parent 8eeeb2ce99
commit 7781551e02
13 changed files with 960 additions and 301 deletions

View File

@@ -59,6 +59,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.compose.runtime)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -3,54 +3,26 @@ package com.example.snapandsolve
import DamageFilterDialog import DamageFilterDialog
import DamageListDialog import DamageListDialog
import MapViewModel import MapViewModel
import android.Manifest
import android.R.attr.enabled
import android.app.Application import android.app.Application
import android.graphics.BitmapFactory
import androidx.activity.compose.rememberLauncherForActivityResult
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.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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.AddLocation
import androidx.compose.material.icons.filled.AddLocationAlt
import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.filled.FormatListNumbered import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import applyDamageFilter import applyDamageFilter
import com.arcgismaps.data.ArcGISFeature
// Hier holen wir die ArcGIS Klassen
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.toolkit.geoviewcompose.MapView
// Hier deine eigenen Klassen (Pfade prüfen!)
import com.example.snapandsolve.camera.AlbumViewModel import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.camera.AlbumViewState
import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.* import com.example.snapandsolve.ui.theme.*
import com.example.snapandsolve.ui.theme.composable.SideSlider
import com.example.snapandsolve.ui.theme.composable.SliderMenuItem
import com.example.snapandsolve.view.ReportDialog
import getActiveFilters import getActiveFilters
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -60,17 +32,14 @@ import kotlinx.coroutines.launch
fun MainScreen(modifier: Modifier = Modifier, application: Application) { fun MainScreen(modifier: Modifier = Modifier, application: Application) {
var showReport by rememberSaveable { mutableStateOf(false) } var showReport by rememberSaveable { mutableStateOf(false) }
var sliderOpen by rememberSaveable { mutableStateOf(false) } var sliderOpen by rememberSaveable { mutableStateOf(false) }
val mapViewModel = remember { MapViewModel(application) } val mapViewModel = remember { MapViewModel(application) }
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) } val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
// test
fun openReport() { fun openReport() {
showReport = true showReport = true
sliderOpen = false sliderOpen = false
} }
// test
fun closeReport() { fun closeReport() {
showReport = false showReport = false
} }
@@ -160,7 +129,7 @@ fun ContentScreen(
// Report Overlay // Report Overlay
if (showReport) { if (showReport) {
ReportOverlay( ReportDialog(
onCancel = onDismissReport, onCancel = onDismissReport,
onClose = onDismissReport, onClose = onDismissReport,
viewModel = albumViewModel, viewModel = albumViewModel,
@@ -236,219 +205,7 @@ fun AppTopBar(
) )
} }
@Composable /*
fun ReportOverlay(
onCancel: () -> Unit,
onClose: () -> Unit,
viewModel: AlbumViewModel,
mapViewModel: MapViewModel
) {
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
val currentContext = LocalContext.current
val hasPoint = mapViewModel.reportDraft.point != null
var dropdownExpanded by remember { mutableStateOf(false) }
LaunchedEffect(viewState.selectedPictures) {
mapViewModel.updateReportDraft { copy(photos = viewState.selectedPictures) }
}
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(20)
) { urls ->
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
}
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
) { isImageSaved ->
if (isImageSaved) viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
else viewModel.onReceive(Intent.OnImageSavingCanceled)
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { permissionGranted ->
if (permissionGranted) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
else viewModel.onReceive(Intent.OnPermissionDenied)
}
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)
}
LaunchedEffect(viewState.tempFileUrl) {
viewState.tempFileUrl?.let { cameraLauncher.launch(it) }
}
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 }
) {
MapViewModel.DAMAGE_TYPES.forEach { typ -> // <-- Nutzt zentrale Liste
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")
}
}
}
}
@Composable
fun FeatureInfoOverlay(
feature: ArcGISFeature,
onClose: () -> Unit
) {
val typ = feature.attributes["Typ"].toString()
val beschreibung = feature.attributes["Beschreibung"].toString()
val id = feature.attributes["OBJECTID"].toString()
var image by remember { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(feature) {
image = loadFirstAttachmentBitmap(feature)
}
OverlayShell(
title = "Meldung $id",
footer = {
OutlinedButton(
onClick = {
onClose()
}
) { Text("Schließen", color = Color.Black) }
}
) {
image?.let {
Image(
bitmap = it,
contentDescription = "Feature Bild",
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentScale = ContentScale.Crop
)
}
Text("Typ: $typ", color = Color.Black)
Text("Beschreibung:", color = Color.Black)
Text(beschreibung, color = Color.Black)
}
}
suspend fun loadFirstAttachmentBitmap( suspend fun loadFirstAttachmentBitmap(
feature: ArcGISFeature feature: ArcGISFeature
): ImageBitmap? { ): ImageBitmap? {
@@ -467,4 +224,5 @@ suspend fun loadFirstAttachmentBitmap(
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size) val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
return bitmap.asImageBitmap() return bitmap.asImageBitmap()
} }
*/

View File

@@ -28,10 +28,13 @@ import com.arcgismaps.mapping.view.LocationDisplay
import com.arcgismaps.mapping.view.ScreenCoordinate import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.example.snapandsolve.view.createTypStatusRenderer
import com.example.snapandsolve.viewmodel.findNearbyDamageOfSameType
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class MapViewModel(application: Application) : AndroidViewModel(application) { class MapViewModel(application: Application) : AndroidViewModel(application) {
companion object { companion object {
@@ -47,6 +50,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply { val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0) initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
} }
var duplicateDamages by mutableStateOf<List<DamageWithDistance>>(emptyList())
private set
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT) var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
var reopenReport by mutableStateOf(false) var reopenReport by mutableStateOf(false)
private set private set
@@ -74,7 +79,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
init { init {
tempOverlay.graphics.add(pointGraphic) tempOverlay.graphics.add(pointGraphic)
viewModelScope.launch { viewModelScope.launch {
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/arcgis/rest/services/si_StrassenSchaeden/FeatureServer/0") serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/251120_StrassenSchaeden/FeatureServer/0")
serviceFeatureTable.load().onSuccess { serviceFeatureTable.load().onSuccess {
val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" } val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" }
val attributeDomain = typeDamageField?.domain as? CodedValueDomain val attributeDomain = typeDamageField?.domain as? CodedValueDomain
@@ -94,6 +99,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}") println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}")
} }
featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable) featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
featureLayer.renderer = createTypStatusRenderer()
map.operationalLayers.add(featureLayer) map.operationalLayers.add(featureLayer)
// ===== DEBUG: Felder nach dem Hinzufügen zur Map ===== // ===== DEBUG: Felder nach dem Hinzufügen zur Map =====
@@ -359,6 +365,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
geometry = draft.point geometry = draft.point
attributes["Beschreibung"] = draft.beschreibung attributes["Beschreibung"] = draft.beschreibung
attributes["Typ"] = draft.typ attributes["Typ"] = draft.typ
attributes["status"] = draft.status
} }
// 2) Erst addFeature + applyEdits => Feature existiert am Server (ObjectID) // 2) Erst addFeature + applyEdits => Feature existiert am Server (ObjectID)
@@ -405,6 +412,17 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
selectedFeature = null selectedFeature = null
featureLayer.clearSelection() featureLayer.clearSelection()
} }
suspend fun isDuplicateNearby(radiusMeters: Double): Boolean {
val p = reportDraft.point ?: return false
val t = reportDraft.typ
duplicateDamages = findNearbyDamageOfSameType(serviceFeatureTable, p, t, radiusMeters)
return duplicateDamages.isNotEmpty()
}
fun clearDuplicateDamages() {
duplicateDamages = emptyList()
}
} }
enum class FeatureOperationType(val operationName: String, val instruction: String) { enum class FeatureOperationType(val operationName: String, val instruction: String) {
@@ -419,7 +437,8 @@ data class ReportDraft(
val beschreibung: String = "", val beschreibung: String = "",
val typ: String = "Schadenstyp wählen...", val typ: String = "Schadenstyp wählen...",
val photos: List<ImageBitmap> = emptyList(), val photos: List<ImageBitmap> = emptyList(),
val point: Point? = null val point: Point? = null,
val status: String = "neu"
) { ) {
val isValid: Boolean val isValid: Boolean
get() = get() =

View File

@@ -17,6 +17,49 @@ import kotlinx.coroutines.withContext
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
/**
* Checkbox-Item für einen Filter
*/
@Composable
fun FilterCheckboxItem(
label: String,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = isChecked,
onCheckedChange = onCheckedChange
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
Text(
text = when (label) {
"Straße" -> "🛣️"
"Gehweg" -> "🚶"
"Fahrradweg" -> "🚴"
"Beleuchtung" -> "💡"
"Sonstiges" -> "📍"
else -> ""
},
style = MaterialTheme.typography.headlineSmall
)
}
}
/** /**
* Dialog für Schaden-Filter * Dialog für Schaden-Filter
* Ermöglicht das Filtern von Features nach Typ UND Datum (unabhängig voneinander) * Ermöglicht das Filtern von Features nach Typ UND Datum (unabhängig voneinander)
@@ -268,48 +311,6 @@ fun DamageFilterDialog(
} }
} }
/**
* Checkbox-Item für einen Filter
*/
@Composable
fun FilterCheckboxItem(
label: String,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = isChecked,
onCheckedChange = onCheckedChange
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
Text(
text = when (label) {
"Straße" -> "🛣️"
"Gehweg" -> "🚶"
"Fahrradweg" -> "🚴"
"Beleuchtung" -> "💡"
"Sonstiges" -> "📍"
else -> ""
},
style = MaterialTheme.typography.headlineSmall
)
}
}
/** /**
* Extension-Funktion für MapViewModel * Extension-Funktion für MapViewModel
* Wendet Filter auf den FeatureLayer an (Typ + Datum - UNABHÄNGIG) * Wendet Filter auf den FeatureLayer an (Typ + Datum - UNABHÄNGIG)

View File

@@ -1,4 +1,3 @@
import MapViewModel
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.widget.Toast import android.widget.Toast
@@ -11,8 +10,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.MyLocation import androidx.compose.material.icons.filled.MyLocation
import androidx.compose.material.icons.filled.TrendingUp import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -20,7 +17,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@@ -44,7 +40,7 @@ import kotlin.math.sqrt
*/ */
data class DamageWithDistance( data class DamageWithDistance(
val feature: ArcGISFeature, val feature: ArcGISFeature,
val distanceInMeters: Double, val distanceInMeters: Double?,
val typ: String, val typ: String,
val beschreibung: String, val beschreibung: String,
val objectId: Long, val objectId: Long,
@@ -639,9 +635,9 @@ suspend fun MapViewModel.loadDamagesNearby(
} }
} }
fun formatDistance(meters: Double): String { fun formatDistance(meters: Double?): String {
return if (meters < 1000) { return if (meters == null || meters < 1000) {
"${meters.roundToInt()} m" "${meters?.roundToInt()} m"
} else { } else {
"${"%.1f".format(meters / 1000)} km" "${"%.1f".format(meters / 1000)} km"
} }

View File

@@ -0,0 +1,339 @@
package com.example.snapandsolve.ui.theme.composable
import DamageWithDistance
import androidx.compose.foundation.BorderStroke
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
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import formatDistance
import getEmojiForType
@Composable
fun DialogContainer(
title: String,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(20.dp),
maxWidthFraction: Float = 0.9f,
maxHeightFraction: Float = 0.85f,
footer: (@Composable () -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = modifier
.fillMaxWidth(maxWidthFraction)
.fillMaxHeight(maxHeightFraction),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
// ---------- HEADER ----------
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Schließen"
)
}
}
Divider(modifier = Modifier.padding(vertical = 16.dp))
// ---------- CONTENT (scrollable) ----------
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
content()
}
// ---------- FOOTER ----------
if (footer != null) {
Divider(modifier = Modifier.padding(vertical = 16.dp))
footer()
}
}
}
}
}
@Composable
fun EqualWidthButtonRow(
modifier: Modifier = Modifier,
title: String? = null,
spacing: Dp = 12.dp,
content: @Composable RowScope.() -> Unit
) {
Column(modifier = modifier.fillMaxWidth()) {
if (title != null) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 2.dp)
)
}
Row(
modifier = modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}
enum class AppButtonStyle { Filled, Outlined }
data class AppButtonColors(
val container: Color? = null,
val content: Color? = null,
val border: Color? = null
)
@Composable
fun AppButton(
text: () -> String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
style: AppButtonStyle = AppButtonStyle.Filled,
icon: ImageVector? = null,
contentDescription: String? = null,
colors: AppButtonColors? = null
) {
val scheme = MaterialTheme.colorScheme
// 👇 Explizite, gute Defaults
val defaultContainer = scheme.primary // klassisches Blau
val defaultContent = scheme.onPrimary // weiß
val disabledContainer = scheme.surfaceVariant
val disabledContent = scheme.onSurfaceVariant
val content: @Composable RowScope.() -> Unit = {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(2.dp))
}
Text(
text = text(),
maxLines = Int.MAX_VALUE
)
}
when (style) {
AppButtonStyle.Filled -> Button(
onClick = onClick,
enabled = enabled,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = colors?.container ?: defaultContainer,
contentColor = colors?.content ?: defaultContent,
disabledContainerColor = disabledContainer,
disabledContentColor = disabledContent
),
content = content
)
AppButtonStyle.Outlined -> OutlinedButton(
onClick = onClick,
enabled = enabled,
modifier = modifier,
colors = ButtonDefaults.outlinedButtonColors(
contentColor = colors?.content ?: scheme.primary,
disabledContentColor = disabledContent
),
border = BorderStroke(
1.dp,
colors?.border ?: scheme.primary
),
content = content
)
}
}
@Composable
fun DamageListItem(
damage: DamageWithDistance,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.height(100.dp)
.clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
// Links: Foto
Box(
modifier = Modifier
.width(100.dp)
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
) {
if (damage.photo != null) {
Image(
bitmap = damage.photo,
contentDescription = "Schadensfoto",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Text("📷", style = MaterialTheme.typography.headlineLarge)
}
}
}
// Rechts: Infos
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(getEmojiForType(damage.typ), style = MaterialTheme.typography.titleLarge)
Text(
text = damage.typ,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
val shortDesc =
damage.beschreibung.take(40) + if (damage.beschreibung.length > 40) "..." else ""
Text(
text = shortDesc,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Text("📍", style = MaterialTheme.typography.labelSmall)
Text(
text = formatDistance(damage.distanceInMeters),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Text("👥", style = MaterialTheme.typography.labelSmall)
Text(
text = "${damage.rating}",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
color = if (damage.rating > 0)
MaterialTheme.colorScheme.tertiary
else
MaterialTheme.colorScheme.outline
)
}
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
package com.example.snapandsolve.ui.theme.composable
import androidx.compose.ui.graphics.Color
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@Composable
fun LegendItem(
label: String,
modifier: Modifier = Modifier,
marker: @Composable () -> Unit
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(20.dp),
contentAlignment = Alignment.Center
) {
marker()
}
Spacer(Modifier.width(8.dp))
Text(label)
}
}
@Composable
fun LegendMarkerCircle(color: Color) {
Box(
modifier = Modifier
.size(12.dp)
.background(color, CircleShape)
)
}
@Composable
fun LegendMarkerIcon(
@DrawableRes iconRes: Int,
tint: Color? = null
) {
Icon(
painter = painterResource(iconRes),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = tint ?: Color.Black
)
}

View File

@@ -1,4 +1,4 @@
package com.example.snapandsolve.ui.theme package com.example.snapandsolve.ui.theme.composable
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@@ -15,6 +15,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.example.snapandsolve.ui.theme.WidgetColor
@Composable @Composable

View File

@@ -0,0 +1,60 @@
package com.example.snapandsolve.view
import DamageWithDistance
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.snapandsolve.ui.theme.composable.AppButton
import com.example.snapandsolve.ui.theme.composable.AppButtonStyle
import com.example.snapandsolve.ui.theme.composable.DamageListItem
import com.example.snapandsolve.ui.theme.composable.DialogContainer
import com.example.snapandsolve.ui.theme.composable.EqualWidthButtonRow
@Composable
fun CloseDamageDialog(
hits: List<DamageWithDistance>,
onDismiss: () -> Unit,
onProceedAnyway: () -> Unit,
) {
DialogContainer(
title = "Ähnliche Schäden in der Nähe",
onDismiss = onDismiss,
footer = {
EqualWidthButtonRow {
AppButton(
text = { "Abbrechen" },
style = AppButtonStyle.Outlined,
onClick = onDismiss,
modifier = Modifier.weight(1f)
)
AppButton(
text = { "Trotzdem hinzufügen" },
style = AppButtonStyle.Filled,
onClick = onProceedAnyway,
modifier = Modifier.weight(1f)
)
}
}
) {
Text(
text = "Es wurden ${hits.size} ähnliche Meldung(en) in deiner Nähe.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(12.dp))
// Liste
hits.forEach { hit ->
DamageListItem(
damage = hit,
onClick = {}
)
Spacer(Modifier.height(10.dp))
}
}
}

View File

@@ -0,0 +1,253 @@
package com.example.snapandsolve.view
import MapViewModel
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.camera.AlbumViewState
import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.composable.AppButton
import com.example.snapandsolve.ui.theme.composable.AppButtonStyle
import com.example.snapandsolve.ui.theme.composable.DialogContainer
import com.example.snapandsolve.ui.theme.composable.EqualWidthButtonRow
import kotlinx.coroutines.launch
@Composable
fun ReportDialog(
onCancel: () -> Unit,
onClose: () -> Unit,
viewModel: AlbumViewModel,
mapViewModel: MapViewModel
) {
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
val currentContext = LocalContext.current
val hasPoint = mapViewModel.reportDraft.point != null
var dropdownExpanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var showDuplicateDialog by remember { mutableStateOf(false) }
var checking by remember { mutableStateOf(false) }
LaunchedEffect(viewState.selectedPictures) {
mapViewModel.updateReportDraft { copy(photos = viewState.selectedPictures) }
}
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(20)
) { urls ->
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
}
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
) { isImageSaved ->
if (isImageSaved) viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
else viewModel.onReceive(Intent.OnImageSavingCanceled)
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { permissionGranted ->
if (permissionGranted) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
else viewModel.onReceive(Intent.OnPermissionDenied)
}
fun startCamera() {
val hasPermission =
currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
if (hasPermission) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
else permissionLauncher.launch(Manifest.permission.CAMERA)
}
LaunchedEffect(viewState.tempFileUrl) {
viewState.tempFileUrl?.let { cameraLauncher.launch(it) }
}
DialogContainer(
title = "Neue Meldung",
onDismiss = onCancel,
maxWidthFraction = 0.98f,
maxHeightFraction = 0.95f,
contentPadding = PaddingValues(16.dp),
footer = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
AppButton(
text = { "Hinzufügen" },
style = AppButtonStyle.Outlined,
onClick = {
scope.launch {
checking = true
showDuplicateDialog = mapViewModel.isDuplicateNearby(20.0)
checking = false
if (!showDuplicateDialog) {
mapViewModel.submitDraftToLayer()
viewModel.clearSelection()
onCancel()
}
}
},
enabled = mapViewModel.reportDraft.isValid,
modifier = Modifier.fillMaxWidth(0.7f)
)
}
}
) {
// ---------- CONTENT ----------
Text(
text = "Schadensbeschreibung:",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 2.dp)
)
// Typ Dropdown
Box {
OutlinedButton(
onClick = { dropdownExpanded = true },
modifier = Modifier.fillMaxWidth()
) {
Text("Typ: ${mapViewModel.reportDraft.typ}")
}
DropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
) {
MapViewModel.DAMAGE_TYPES.forEach { typ ->
DropdownMenuItem(
text = { Text(typ) },
onClick = {
mapViewModel.updateReportDraft { copy(typ = typ) }
dropdownExpanded = false
}
)
}
}
}
Spacer(Modifier.height(12.dp))
// Kamera Buttons
EqualWidthButtonRow(title = "Foto aufnehmen...") {
AppButton(
text = { "Foto aufnehmen" },
style = AppButtonStyle.Filled,
modifier = Modifier.weight(1f),
onClick = { startCamera() }
)
AppButton(
text = { "Aus Galerie" },
style = AppButtonStyle.Filled,
modifier = Modifier.weight(1f),
onClick = {
pickImageFromAlbumLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
)
}
Spacer(Modifier.height(12.dp))
// 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
)
)
Spacer(Modifier.height(12.dp))
// Bilder Grid
if (viewState.selectedPictures.isNotEmpty()) {
Text("Ausgewählte Bilder (${viewState.selectedPictures.size})")
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
)
}
}
}
Spacer(Modifier.height(12.dp))
// Position Buttons
EqualWidthButtonRow(title = "Position...") {
AppButton(
text = { if (hasPoint) "neu aus Standort" else "aus Standort" },
style = AppButtonStyle.Filled,
modifier = Modifier.weight(1f),
onClick = { mapViewModel.pickCurrentLocation() }
)
AppButton(
text = { if (hasPoint) "manuell neu setzen" else "manuell setzen" },
style = AppButtonStyle.Filled,
modifier = Modifier.weight(1f),
onClick = {
mapViewModel.startPickReportLocation()
onClose()
}
)
}
}
if (showDuplicateDialog) {
CloseDamageDialog(
hits = mapViewModel.duplicateDamages,
onDismiss = {
showDuplicateDialog = false
mapViewModel.clearDuplicateDamages()
},
onProceedAnyway = {
mapViewModel.submitDraftToLayer()
viewModel.clearSelection()
showDuplicateDialog = false
mapViewModel.clearDuplicateDamages()
}
)
}
}

View File

@@ -0,0 +1,86 @@
package com.example.snapandsolve.view
import com.arcgismaps.Color
import com.arcgismaps.mapping.symbology.*
import kotlin.collections.iterator
fun createTypStatusRenderer(): UniqueValueRenderer {
// Status -> Hintergrundfarbe
val statusColor = mapOf(
"neu" to Color.fromRgba(220, 50, 50, 255),
"in Bearbeitung" to Color.fromRgba(255, 180, 0, 255),
"Schaden behoben" to Color.fromRgba(60, 180, 75, 255)
)
// Typ -> Icon-Style (aus Standardbibliothek)
val typeIcon = mapOf(
"Straße" to SimpleMarkerSymbolStyle.Square,
"Gehweg" to SimpleMarkerSymbolStyle.Triangle,
"Fahrradweg" to SimpleMarkerSymbolStyle.Diamond,
"Beleuchtung" to SimpleMarkerSymbolStyle.X,
"Sonstiges" to SimpleMarkerSymbolStyle.Cross
)
val renderer = UniqueValueRenderer().apply {
fieldNames.addAll(listOf("typ", "status"))
defaultLabel = "Sonstige"
defaultSymbol = makeStatusTypeSymbol(
iconStyle = SimpleMarkerSymbolStyle.Circle,
backgroundFill = Color.fromRgba(180, 180, 180, 255)
)
}
for ((typ, iconStyle) in typeIcon) {
for ((status, bgColor) in statusColor) {
val label = "$typ$status"
renderer.uniqueValues.add(
UniqueValue(
label = label,
description = label,
symbol = makeStatusTypeSymbol(iconStyle, bgColor),
values = listOf(typ, status)
)
)
}
}
return renderer
}
fun makeStatusTypeSymbol(
iconStyle: SimpleMarkerSymbolStyle,
backgroundFill: Color
): Symbol {
val white = Color.fromRgba(255, 255, 255, 255)
// Hintergrund: Kreis in Statusfarbe
val background = SimpleMarkerSymbol(
style = SimpleMarkerSymbolStyle.Circle,
color = backgroundFill,
size = 18f
).apply {
outline = SimpleLineSymbol(
style = SimpleLineSymbolStyle.Solid,
color = white,
width = 1.5f
)
}
// Vordergrund: "Icon" als Outline (weiß), ohne Füllung
val foreground = SimpleMarkerSymbol(
style = iconStyle,
color = Color.fromRgba(0, 0, 0, 0), // transparent => nur Outline sichtbar
size = 10f
).apply {
outline = SimpleLineSymbol(
style = SimpleLineSymbolStyle.Solid,
color = white,
width = 2.0f
)
}
return CompositeSymbol(listOf(background, foreground))
}

View File

@@ -0,0 +1,79 @@
package com.example.snapandsolve.viewmodel
import DamageWithDistance
import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.QueryParameters
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
suspend fun findNearbyDamageOfSameType(
table: ServiceFeatureTable,
draftPoint: Point,
draftTyp: String,
radiusMeters: Double
): List<DamageWithDistance> {
val layerSR: SpatialReference? = table.spatialReference
val pointInLayerSR = if (layerSR != null && draftPoint.spatialReference != layerSR) {
(GeometryEngine.projectOrNull(draftPoint, layerSR) as? Point) ?: draftPoint
} else {
draftPoint
}
val buffer = GeometryEngine.bufferOrNull(pointInLayerSR, radiusMeters) ?: return emptyList()
val safeTyp = draftTyp.replace("'", "''")
val qp = QueryParameters().apply {
whereClause = "typ = '$safeTyp'"
geometry = buffer
}
val result = table.queryFeatures(qp).getOrThrow()
val hits = mutableListOf<DamageWithDistance>()
for (feature in result) {
val p = feature.geometry as? Point ?: continue
// ✅ Distanz korrekt
val dist = GeometryEngine.distanceOrNull(pointInLayerSR, p) ?: Double.POSITIVE_INFINITY
val typ = (feature.attributes["typ"] as? String).orEmpty()
val beschreibung = (feature.attributes["beschreibung"] as? String).orEmpty()
val rating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
val id = (feature.attributes["OBJECTID"] as? Number)?.toLong() ?: 0L
val photo: ImageBitmap? = try {
val arcFeature = feature as? ArcGISFeature
val attachments = arcFeature?.fetchAttachments()?.getOrNull()
val first = attachments?.firstOrNull()
// Je nach SDK: first.fetchData() oder first.data
val bytes: ByteArray? = first
?.fetchData()
?.getOrNull()
bytes?.let {
BitmapFactory.decodeByteArray(it, 0, it.size)?.asImageBitmap()
}
} catch (_: Exception) {
null
}
hits += DamageWithDistance(
feature = feature as ArcGISFeature,
typ = typ,
beschreibung = beschreibung,
photo = photo,
rating = rating,
distanceInMeters = dist,
objectId = id
)
}
return hits.sortedBy { it.distanceInMeters }
}

View File

@@ -10,6 +10,7 @@ activityCompose = "1.12.1"
composeBom = "2024.09.00" composeBom = "2024.09.00"
material3 = "1.4.0" material3 = "1.4.0"
arcgisMapsKotlin = "200.8.0" arcgisMapsKotlin = "200.8.0"
runtime = "1.10.2"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -31,6 +32,7 @@ arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.
arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" } arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" }
arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" } arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" }
arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" } arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }