commit bc7e437b1abc0f3e4fb6be197e0721a68b3df2a6
parent debed0496a59a9ba77c89b1d795ae8d27a870449
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Tue, 29 Jul 2025 16:35:43 +0200
Initial map setup
Diffstat:
5 files changed, 174 insertions(+), 28 deletions(-)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
@@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
+ alias(libs.plugins.kotlin.serialization)
}
android {
@@ -70,6 +71,12 @@ dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
implementation(libs.hilt.navigation.compose)
+ implementation(libs.maplibre.composeMaterial3)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.content.negotiation)
+ implementation(libs.ktor.serialization.kotlinx.json)
+ implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.client.android)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
+ <uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".NordicCalendarApp"
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt
@@ -1,5 +1,7 @@
package space.midnightthoughts.nordiccalendar.screens
+import android.location.Geocoder
+import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -21,8 +23,12 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+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
@@ -34,6 +40,14 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
+import dev.sargunv.maplibrecompose.compose.MaplibreMap
+import dev.sargunv.maplibrecompose.compose.layer.SymbolLayer
+import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
+import dev.sargunv.maplibrecompose.core.BaseStyle
+import dev.sargunv.maplibrecompose.core.source.GeoJsonData
+import io.github.dellisd.spatialk.geojson.Feature
+import io.github.dellisd.spatialk.geojson.Point
+import io.github.dellisd.spatialk.geojson.Position
import sh.calvin.autolinktext.rememberAutoLinkText
import space.midnightthoughts.nordiccalendar.R
import space.midnightthoughts.nordiccalendar.components.AppScaffold
@@ -43,6 +57,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
+import java.util.Locale
@Composable
fun EventDetailsView(
@@ -78,6 +93,54 @@ fun EventDetailsView(
val scrollState = rememberScrollState()
+ val locationText = event.value?.location?.trim()
+ var geocodeResult by remember(locationText) {
+ mutableStateOf<Pair<Boolean, android.location.Address?>>(
+ false to null
+ )
+ }
+ val context = LocalContext.current
+ val geocoderAvailable = remember { Geocoder.isPresent() }
+ Log.d("EventDetailsView", "Geocoder available: $geocoderAvailable")
+ LaunchedEffect(locationText, geocoderAvailable) {
+ if (!locationText.isNullOrEmpty() && geocoderAvailable) {
+ try {
+ val geocoder = Geocoder(context, Locale.getDefault())
+ if (android.os.Build.VERSION.SDK_INT >= 33) {
+ geocoder.getFromLocationName(
+ locationText,
+ 1,
+ object : Geocoder.GeocodeListener {
+ override fun onGeocode(addresses: List<android.location.Address>) {
+ geocodeResult = if (addresses.isNotEmpty()) {
+ true to addresses[0]
+ } else {
+ false to null
+ }
+ }
+
+ override fun onError(errorMessage: String?) {
+ geocodeResult = false to null
+ }
+ }
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ val results = geocoder.getFromLocationName(locationText, 1)
+ geocodeResult = if (!results.isNullOrEmpty()) {
+ true to results[0]
+ } else {
+ false to null
+ }
+ }
+ } catch (_: Exception) {
+ geocodeResult = false to null
+ }
+ } else {
+ geocodeResult = false to null
+ }
+ }
+
AppScaffold(
title = event.value?.title
?: stringResource(
@@ -145,11 +208,7 @@ fun EventDetailsView(
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(48.dp))
- Row(
- modifier = Modifier.fillMaxSize(),
- verticalAlignment = Alignment.Top,
- horizontalArrangement = Arrangement.Start
- ) {
+ Row {
if (event.value?.calendar?.color != null) {
// Round color dot
Box(
@@ -209,27 +268,66 @@ fun EventDetailsView(
color = MaterialTheme.colorScheme.outlineVariant
)
Spacer(modifier = Modifier.height(16.dp))
- // TODO: map if location is a valid address
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.Top,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- text = stringResource(R.string.location),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold
- )
- Spacer(modifier = Modifier.width(24.dp))
- Text(
- text = if (event.value?.location?.trim().isNullOrEmpty()) {
- stringResource(R.string.no_location_available)
- } else {
- event.value?.location ?: ""
- },
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurface,
- )
+ if (!locationText.isNullOrEmpty()) {
+ if (geocodeResult.first && geocodeResult.second != null) {
+ val address = geocodeResult.second!!
+ val position = Position(
+ address.longitude,
+ address.latitude
+ )
+ Column {
+ Text(
+ text = stringResource(R.string.location),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ LocationMap(coordinate = position)
+ }
+ } else {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.location),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.width(24.dp))
+ Text(
+ text = AnnotatedString.rememberAutoLinkText(
+ if (event.value?.location?.trim().isNullOrEmpty()) {
+ stringResource(R.string.no_location_available)
+ } else {
+ event.value?.location ?: ""
+ }
+ ),
+
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ }
+ } else {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.location),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.width(24.dp))
+ Text(
+ text = stringResource(R.string.no_location_available),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
}
// TODO: Invitees and attendees and alerts
@@ -278,4 +376,35 @@ fun EventDetailsView(
}
}
}
-}
-\ No newline at end of file
+}
+
+@Composable
+fun LocationMap(coordinate: Position) {
+ MaplibreMap(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty")
+ ) {
+ MapContent(coordinate = coordinate)
+ }
+}
+
+@Composable
+fun MapContent(
+ coordinate: Position,
+) {
+ val marker = rememberGeoJsonSource(
+ GeoJsonData.Features(
+ Feature(Point(coordinates = coordinate))
+ )
+ )
+
+ SymbolLayer(
+ id = "marker",
+ source = marker,
+ //iconImage = "marker_icon", // Ensure you have a marker icon in your resources
+ //iconSize = 1.0f,
+ )
+
+}
diff --git a/build.gradle.kts b/build.gradle.kts
@@ -5,4 +5,5 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.kotlin.serialization) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
@@ -15,6 +15,8 @@ autolinktext = "2.0.2"
hilt = "2.57"
ksp = "2.2.0-2.0.2"
hiltNavigationCompose = "1.2.0"
+maplibreCompose = "0.10.4"
+ktorVersion = "3.2.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -38,6 +40,12 @@ autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version.ref = "
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
+maplibre-composeMaterial3 = { module = "dev.sargunv.maplibre-compose:maplibre-compose-material3", version.ref = "maplibreCompose" }
+ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktorVersion" }
+ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktorVersion" }
+ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktorVersion" }
+ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktorVersion" }
+ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktorVersion" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -45,3 +53,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }