commit e81bce5744ed9b3155e17a58a1d665015bc70b52
parent bc7e437b1abc0f3e4fb6be197e0721a68b3df2a6
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Tue, 29 Jul 2025 17:23:50 +0200
Improve the map and calendar naming
Diffstat:
6 files changed, 190 insertions(+), 74 deletions(-)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt
@@ -531,7 +531,8 @@ fun EventCardPreview() {
accountName = "Beispielkonto",
accountType = "com.example",
syncEvents = true,
- visible = true
+ visible = true,
+ displayName = "Beispielkalender",
)
)
EventCard(
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,8 +1,7 @@
package space.midnightthoughts.nordiccalendar.screens
-import android.location.Geocoder
-import android.util.Log
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -16,6 +15,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DividerDefaults
@@ -26,13 +26,13 @@ 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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
@@ -42,8 +42,10 @@ 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.rememberCameraState
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.BaseStyle
+import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.source.GeoJsonData
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Point
@@ -52,12 +54,12 @@ import sh.calvin.autolinktext.rememberAutoLinkText
import space.midnightthoughts.nordiccalendar.R
import space.midnightthoughts.nordiccalendar.components.AppScaffold
import space.midnightthoughts.nordiccalendar.getCurrentAppLocale
+import space.midnightthoughts.nordiccalendar.viewmodels.BoundingBox
import space.midnightthoughts.nordiccalendar.viewmodels.EventDetailsViewModel
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
-import java.util.Locale
@Composable
fun EventDetailsView(
@@ -93,51 +95,16 @@ 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
- }
- }
+ val boundingBox = remember(viewModel) {
+ viewModel.locationBoundingBox
+ }.collectAsState()
- 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
+ val locationText = event.value?.location?.trim()
+ val locationPosition by viewModel.locationPosition.collectAsState()
+ val appLocaleString = appLocale.language
+ LaunchedEffect(locationText, appLocaleString) {
+ if (!locationText.isNullOrEmpty()) {
+ viewModel.resolveLocation(locationText, appLocaleString)
}
}
@@ -208,12 +175,13 @@ fun EventDetailsView(
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(48.dp))
- Row {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
if (event.value?.calendar?.color != null) {
// Round color dot
Box(
modifier = Modifier
- .padding(top = 4.dp)
.size(16.dp)
.background(
color = if (event.value?.calendar?.color != null) {
@@ -227,7 +195,7 @@ fun EventDetailsView(
Spacer(modifier = Modifier.width(8.dp))
}
Text(
- text = event.value?.calendar?.name ?: "",
+ text = event.value?.calendar?.displayName ?: "",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -269,12 +237,7 @@ fun EventDetailsView(
)
Spacer(modifier = Modifier.height(16.dp))
if (!locationText.isNullOrEmpty()) {
- if (geocodeResult.first && geocodeResult.second != null) {
- val address = geocodeResult.second!!
- val position = Position(
- address.longitude,
- address.latitude
- )
+ if (locationPosition != null) {
Column {
Text(
text = stringResource(R.string.location),
@@ -282,7 +245,10 @@ fun EventDetailsView(
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
- LocationMap(coordinate = position)
+ LocationMap(
+ coordinate = locationPosition!!,
+ boundingBox = boundingBox.value
+ )
}
} else {
Row(
@@ -379,14 +345,57 @@ fun EventDetailsView(
}
@Composable
-fun LocationMap(coordinate: Position) {
- MaplibreMap(
+fun LocationMap(coordinate: Position, boundingBox: BoundingBox?) {
+ val shape = RoundedCornerShape(16.dp)
+ val uriHandler = LocalUriHandler.current
+ val camera = if (boundingBox != null) {
+ val centerLat = (boundingBox.north + boundingBox.south) / 2.0
+ val centerLon = (boundingBox.east + boundingBox.west) / 2.0
+ val center = Position(centerLon, centerLat)
+ val latDiff = boundingBox.north - boundingBox.south
+ val lonDiff = boundingBox.east - boundingBox.west
+ val zoom = when {
+ latDiff < 0.002 && lonDiff < 0.002 -> 17.0
+ latDiff < 0.01 && lonDiff < 0.01 -> 15.0
+ latDiff < 0.05 && lonDiff < 0.05 -> 14.0
+ else -> 13.0
+ }
+ rememberCameraState(
+ firstPosition = CameraPosition(
+ target = center,
+ zoom = zoom
+ )
+ )
+ } else {
+ rememberCameraState(
+ firstPosition = CameraPosition(
+ target = coordinate,
+ zoom = 13.0
+ )
+ )
+ }
+ Box(
modifier = Modifier
.fillMaxWidth()
- .height(200.dp),
- baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty")
+ .height(200.dp)
+ .clip(shape)
+ .border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape)
+ .background(MaterialTheme.colorScheme.surfaceVariant)
) {
- MapContent(coordinate = coordinate)
+ MaplibreMap(
+ modifier = Modifier.matchParentSize(),
+ baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"),
+ cameraState = camera,
+ onMapClick = { pos, _ ->
+ // Open preferred routing app
+ val uri =
+ "geo:${coordinate.latitude},${coordinate.longitude}?q=${coordinate.latitude},${coordinate.longitude}"
+ uriHandler.openUri(uri)
+ dev.sargunv.maplibrecompose.compose.ClickResult.Consume
+ },
+ ) {
+ MapContent(coordinate = coordinate)
+ }
}
}
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/util/CalendarData.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/util/CalendarData.kt
@@ -9,6 +9,7 @@ import android.util.Log
data class Calendar(
val id: Long,
val name: String,
+ val displayName: String,
val color: Long,
val accountName: String,
val accountType: String,
@@ -81,12 +82,13 @@ class CalendarData {
calendars.add(
Calendar(
id = calId,
- name = name ?: displayName,
+ name = displayName ?: name,
color = color,
accountName = accountName ?: "",
accountType = accountType ?: "",
syncEvents = syncEvents == 1,
- visible = visible == 1
+ visible = visible == 1,
+ displayName = displayName ?: "No Display Name",
)
)
}
@@ -183,7 +185,8 @@ class CalendarData {
accountName = "Unknown Account",
accountType = "Unknown Type",
syncEvents = false,
- visible = false
+ visible = false,
+ displayName = "Unknown Calendar" // Fallback if calendar not found
) // Fallback if calendar not found
)
)
@@ -297,7 +300,8 @@ class CalendarData {
accountName = "Unknown Account",
accountType = "Unknown Type",
syncEvents = false,
- visible = false
+ visible = false,
+ displayName = "Unknown Calendar" // Fallback if calendar not found
) // Fallback if calendar not found
)
} else null
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt
@@ -15,7 +15,7 @@ import javax.inject.Inject
@HiltViewModel
class CalendarViewModel @Inject constructor(
- @ApplicationContext private val context: Context,
+ @param:ApplicationContext private val context: Context,
private val repository: CalendarRepository
) : ViewModel() {
val events = repository.eventsFlow.stateIn(
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt
@@ -1,28 +1,85 @@
package space.midnightthoughts.nordiccalendar.viewmodels
import android.content.Context
+import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
+import io.github.dellisd.spatialk.geojson.Position
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.android.Android
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.request.get
+import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
import space.midnightthoughts.nordiccalendar.data.CalendarRepository
import space.midnightthoughts.nordiccalendar.util.Event
import javax.inject.Inject
+@Serializable
+data class NominatimResult(
+ val lat: String,
+ val lon: String,
+ @SerialName("display_name") val displayName: String? = null,
+ @SerialName("boundingbox") val boundingBox: List<String>? = null
+)
+
+data class BoundingBox(val south: Double, val north: Double, val west: Double, val east: Double)
+
@HiltViewModel
class EventDetailsViewModel @Inject constructor(
- @ApplicationContext private val context: Context,
+ @param:ApplicationContext private val context: Context,
private val repository: CalendarRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _event = MutableStateFlow<Event?>(null)
val event: StateFlow<Event?> = _event.asStateFlow()
+ private val _locationPosition = MutableStateFlow<Position?>(null)
+ val locationPosition: StateFlow<Position?> = _locationPosition.asStateFlow()
+
+ private val _locationBoundingBox = MutableStateFlow<BoundingBox?>(null)
+ val locationBoundingBox: StateFlow<BoundingBox?> = _locationBoundingBox.asStateFlow()
+
+ private val ktorClient = HttpClient(Android) {
+ install(ContentNegotiation) {
+ json(
+ Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ prettyPrint = true
+ explicitNulls = false
+ }
+ )
+ }
+ install(HttpTimeout) {
+ requestTimeoutMillis = 15000L
+ connectTimeoutMillis = 10000L
+ socketTimeoutMillis = 15000L
+ }
+ install(Logging) {
+ logger = object : Logger {
+ override fun log(message: String) {
+ android.util.Log.d("Ktor", message)
+ }
+ }
+ level = LogLevel.INFO
+ }
+ }
+
init {
val eventId = savedStateHandle.get<String>("eventId")?.toLongOrNull()
if (eventId != null) {
@@ -36,4 +93,46 @@ class EventDetailsViewModel @Inject constructor(
_event.value = event
}
}
+
+ fun resolveLocation(address: String, locale: String = "de") {
+ viewModelScope.launch {
+ try {
+ val url = "https://nominatim.openstreetmap.org/search"
+ val response: List<NominatimResult> = ktorClient.get(url) {
+ url {
+ parameters.append("q", address)
+ parameters.append("format", "json")
+ parameters.append("addressdetails", "1")
+ parameters.append("limit", "1")
+ parameters.append("accept-language", locale)
+ }
+ headers.append("User-Agent", "nordic-calendar-app")
+ }.body()
+ if (response.isNotEmpty()) {
+ val pos = Position(response[0].lon.toDouble(), response[0].lat.toDouble())
+ _locationPosition.value = pos
+ val bbox = response[0].boundingBox
+ if (bbox != null && bbox.size == 4) {
+ // boundingbox: [south, north, west, east] as strings
+ _locationBoundingBox.value = BoundingBox(
+ south = bbox[0].toDouble(),
+ north = bbox[1].toDouble(),
+ west = bbox[2].toDouble(),
+ east = bbox[3].toDouble()
+ )
+ } else {
+ _locationBoundingBox.value = null
+ }
+ } else {
+ _locationPosition.value = null
+ _locationBoundingBox.value = null
+ Log.w("EventDetailsViewModel", "No location found for address: $address")
+ }
+ } catch (e: Exception) {
+ _locationPosition.value = null
+ _locationBoundingBox.value = null
+ Log.e("EventDetailsViewModel", "Error resolving location: ${e.message}")
+ }
+ }
+ }
}
diff --git a/gradle.properties b/gradle.properties
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
@@ -20,4 +20,7 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
-\ No newline at end of file
+android.nonTransitiveRClass=true
+org.gradle.configuration-cache=true
+# Use this flag carefully, in case some of the plugins are not fully compatible.
+org.gradle.configuration-cache.problems=warn
+\ No newline at end of file