Location: add icons for available payment methods (#1408)

* add backend for PaymentMethod.kt

* change type of acceptedPaymentMethods to Map<PaymentMethod, Boolean>

* add icons to UI and reduce PaymentMethod.kt to Cash and Card

* make OSM parsing for acceptedPaymentMethods correct (in an opinionated way)

* use Toll and TollOff icons

* fix seperator on empty payment methods

* fix sharedElement and animateEnterExit labels that are not part of the sharedElement

* (feat) shape schemes

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
shtrophic
2025-06-01 17:27:18 +02:00
committed by GitHub
co-authored by MM20
parent 1efffcf586
commit 78fa1d71dc
50 changed files with 2313 additions and 965 deletions
@@ -1,7 +1,6 @@
package de.mm20.launcher2.locations
import android.content.Context
import android.util.Log
import de.mm20.launcher2.locations.providers.PluginLocation
import de.mm20.launcher2.locations.providers.PluginLocationProvider
import de.mm20.launcher2.locations.providers.openstreetmaps.OsmLocation
@@ -18,10 +17,10 @@ import de.mm20.launcher2.search.location.Attribution
import de.mm20.launcher2.search.location.Departure
import de.mm20.launcher2.search.location.LocationIcon
import de.mm20.launcher2.search.location.OpeningSchedule
import de.mm20.launcher2.search.location.PaymentMethod
import de.mm20.launcher2.serialization.Json
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@Serializable
internal data class SerializedLocation(
@@ -42,6 +41,7 @@ internal data class SerializedLocation(
val departures: List<Departure>? = null,
val fixMeUrl: String? = null,
val attribution: Attribution? = null,
val acceptedPaymentMethods: Map<PaymentMethod, Boolean>? = null,
val authority: String? = null,
val storageStrategy: StorageStrategy? = null,
)
@@ -67,6 +67,7 @@ internal class OsmLocationSerializer : SearchableSerializer {
timestamp = searchable.timestamp,
departures = searchable.departures,
fixMeUrl = searchable.fixMeUrl,
acceptedPaymentMethods = searchable.acceptedPaymentMethods
)
)
}
@@ -96,6 +97,7 @@ internal class OsmLocationDeserializer(
userRating = json.userRating,
openingSchedule = json.openingSchedule,
timestamp = json.timestamp ?: return null,
acceptedPaymentMethods = json.acceptedPaymentMethods,
updatedSelf = {
osmProvider.update(id)
}
@@ -134,6 +136,7 @@ internal class PluginLocationSerializer : SearchableSerializer {
openingSchedule = searchable.openingSchedule,
timestamp = searchable.timestamp,
departures = searchable.departures,
acceptedPaymentMethods = searchable.acceptedPaymentMethods,
fixMeUrl = searchable.fixMeUrl,
authority = searchable.authority,
storageStrategy = searchable.storageStrategy,
@@ -185,6 +188,7 @@ internal class PluginLocationDeserializer(
departures = json.departures,
fixMeUrl = json.fixMeUrl,
attribution = json.attribution,
acceptedPaymentMethods = json.acceptedPaymentMethods,
authority = authority,
storageStrategy = strategy,
updatedSelf = {
@@ -14,6 +14,7 @@ import de.mm20.launcher2.search.location.Attribution
import de.mm20.launcher2.search.location.Departure
import de.mm20.launcher2.search.location.LocationIcon
import de.mm20.launcher2.search.location.OpeningSchedule
import de.mm20.launcher2.search.location.PaymentMethod
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -34,6 +35,7 @@ data class PluginLocation(
override val label: String,
override val timestamp: Long,
override val attribution: Attribution?,
override val acceptedPaymentMethods: Map<PaymentMethod, Boolean>?,
override val updatedSelf: (suspend (SavableSearchable) -> UpdateResult<Location>)?,
override val labelOverride: String? = null,
val authority: String,
@@ -84,6 +84,7 @@ internal class PluginLocationProvider(
userRatingCount = cursor[LocationColumns.UserRatingCount],
departures = cursor[LocationColumns.Departures],
attribution = cursor[LocationColumns.Attribution],
acceptedPaymentMethods = cursor[LocationColumns.AcceptedPaymentMethods],
authority = pluginAuthority,
updatedSelf = {
if (it !is PluginLocation) UpdateResult.TemporarilyUnavailable()
@@ -117,6 +118,7 @@ internal class PluginLocationProvider(
set(LocationColumns.UserRatingCount, userRatingCount)
set(LocationColumns.Departures, departures)
set(LocationColumns.Attribution, attribution)
set(LocationColumns.AcceptedPaymentMethods, acceptedPaymentMethods)
}
}
}
@@ -4,6 +4,7 @@ import android.content.Context
import de.mm20.launcher2.ktx.ifNullOrEmpty
import de.mm20.launcher2.ktx.into
import de.mm20.launcher2.ktx.map
import de.mm20.launcher2.ktx.stripStartOrNull
import de.mm20.launcher2.locations.OsmLocationSerializer
import de.mm20.launcher2.openstreetmaps.R
import de.mm20.launcher2.search.Location
@@ -16,6 +17,7 @@ import de.mm20.launcher2.search.location.Departure
import de.mm20.launcher2.search.location.LocationIcon
import de.mm20.launcher2.search.location.OpeningHours
import de.mm20.launcher2.search.location.OpeningSchedule
import de.mm20.launcher2.search.location.PaymentMethod
import de.westnordost.osm_opening_hours.model.ClockTime
import de.westnordost.osm_opening_hours.model.ExtendedClockTime
import de.westnordost.osm_opening_hours.model.LastNth
@@ -62,7 +64,8 @@ internal data class OsmLocation(
override val labelOverride: String? = null,
override val timestamp: Long,
override var updatedSelf: (suspend (SavableSearchable) -> UpdateResult<Location>)? = null,
override val userRating: Float?
override val userRating: Float?,
override val acceptedPaymentMethods: Map<PaymentMethod, Boolean>?
) : Location, UpdatableSearchable<Location> {
override val domain: String
@@ -115,7 +118,25 @@ internal data class OsmLocation(
emailAddress = it.tags["email"] ?: it.tags["contact:email"],
timestamp = System.currentTimeMillis(),
userRating = it.tags["stars"]?.runCatching { this.toInt() }?.getOrNull()
?.let { min(it, 5) / 5.0f }
?.let { min(it, 5) / 5.0f },
acceptedPaymentMethods = with(
it.tags.mapNotNull { (key, value) ->
(key.stripStartOrNull("payment:") ?: return@mapNotNull null) to value
}.toMap()
) {
// best-effort way to take any method payment as it being available,
// otherwise as being unavailable, or undefined
mapOf(
PaymentMethod.Card to listOf("credit_cards", "debit_cards", "cards"),
PaymentMethod.Cash to listOf("cash")
).mapNotNull { (method, values) ->
when {
values.any { this[it] in listOf("yes", "only") } -> method to true
values.any { this[it] == "no" } -> method to false
else -> null
}
}.toMap().takeUnless { it.isEmpty() }
}
)
}
}