Compare commits

..

19 Commits

Author SHA1 Message Date
johan12345
31fcee97e1 increase version code 2022-09-14 08:37:34 +02:00
johan12345
de8fd364f4 Android Auto: show loading errors 2022-09-14 08:36:51 +02:00
johan12345
e3f271be5d increase version code 2022-09-12 23:06:40 +02:00
johan12345
99a2540398 MapScreen: move away from LiveData to avoid more refresh bugs 2022-09-12 23:06:08 +02:00
johan12345
85173b438b increase version code 2022-09-12 22:43:42 +02:00
johan12345
c288883572 Android Auto: fix switch between filter settings 2022-09-12 22:42:34 +02:00
johan12345
c8f949da01 increase version code 2022-09-12 08:20:37 +02:00
johan12345
fe33dca1bc Android Auto: fix loading data on first start 2022-09-12 08:19:00 +02:00
johan12345
4fb5090e9b Release 1.3.11 2022-09-11 20:19:21 +02:00
johan12345
d9b8bf382a ChargerDetailScreen: make images square 2022-09-11 19:41:27 +02:00
johan12345
d69456dfd0 fix more issues switching the data source, esp. on Android Auto 2022-09-11 19:14:12 +02:00
johan12345
4da2a273c7 MapScreen: avoid updating chargers twice, which can lead to crashes
due to template update limit
2022-09-11 18:49:28 +02:00
johan12345
8e622c881d Android Auto: limit maxRows in MapScreen to at most 25 2022-09-11 18:38:04 +02:00
johan12345
89b2175d89 fix crashes due to race conditions when changing data source 2022-09-11 18:07:08 +02:00
johan12345
3c30481821 add ChargeLocationRepository
encapsulates logic to load charging stations for future implementation of offline caching (#164, #97)
2022-09-10 19:46:14 +02:00
johan12345
385353689d French: ignore ImpliedQuantity 2022-09-04 15:46:26 +02:00
johan12345
7f9c838b9d remove ignore:MissingQuantity for French 2022-09-04 15:41:30 +02:00
Hosted Weblate
205814e6f6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.4% (223 of 261 strings)

Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2022-09-04 15:40:02 +02:00
Hosted Weblate
f30ae4a720 Translated using Weblate (French)
Currently translated at 100.0% (261 of 261 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2022-09-04 15:40:02 +02:00
29 changed files with 411 additions and 897 deletions

View File

@@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 106
versionName "1.3.10"
versionCode 116
versionName "1.3.11"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")

View File

@@ -3,6 +3,8 @@ package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.text.SpannableStringBuilder
@@ -32,17 +34,19 @@ import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.getReferenceData
import net.vonforst.evmap.viewmodel.awaitFinished
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.math.roundToInt
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
var charger: ChargeLocation? = null
var photo: Bitmap? = null
@@ -50,10 +54,8 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val imageSize = 128 // images should be 128dp according to docs
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
@@ -71,9 +73,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private var favoriteUpdateJob: Job? = null
init {
referenceData.observe(this) {
loadCharger()
}
loadCharger()
}
override fun onGetTemplate(): Template {
@@ -356,24 +356,21 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}
private fun loadCharger() {
val referenceData = referenceData.value ?: return
lifecycleScope.launch {
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
val response = repo.getChargepointDetail(chargerSparse.id).awaitFinished()
if (response.status == Status.SUCCESS) {
val charger = response.data!!
val photo = charger.photos?.firstOrNull()
photo?.let {
val density = carContext.resources.displayMetrics.density
val url = if (largeImageSupported) {
photo.getUrl(size = (imageSizeLarge * density).roundToInt())
} else {
photo.getUrl(size = (imageSize * density).roundToInt())
}
val size =
(density * if (largeImageSupported) imageSizeLarge else imageSize).roundToInt()
val url = photo.getUrl(size = size)
val request = ImageRequest.Builder(carContext).data(url).build()
var img =
val img =
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
// draw icon on top of image
@@ -383,19 +380,29 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
multi = charger.isMulti()
)
img = img.copy(Bitmap.Config.ARGB_8888, true)
val outImg = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val iconSmall = icon.scale(
(img.height * 0.4 / icon.height * icon.width).roundToInt(),
(img.height * 0.4).roundToInt()
(size * 0.4 / icon.height * icon.width).roundToInt(),
(size * 0.4).roundToInt()
)
val canvas = Canvas(outImg)
val m = Matrix()
m.setRectToRect(
RectF(0f, 0f, img.width.toFloat(), img.height.toFloat()),
RectF(0f, 0f, size.toFloat(), size.toFloat()),
Matrix.ScaleToFit.CENTER
)
canvas.drawBitmap(
img.copy(Bitmap.Config.ARGB_8888, false), m, null
)
val canvas = Canvas(img)
canvas.drawBitmap(
iconSmall,
0f,
(img.height - iconSmall.height * 1.1).toFloat(),
(size - iconSmall.height * 1.1).toFloat(),
null
)
this@ChargerDetailScreen.photo = img
this@ChargerDetailScreen.photo = outImg
}
this@ChargerDetailScreen.charger = charger

View File

@@ -13,7 +13,9 @@ import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.*
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
import net.vonforst.evmap.BuildConfig
@@ -24,14 +26,17 @@ import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterWithValue
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.awaitFinished
import net.vonforst.evmap.viewmodel.filtersWithValue
import net.vonforst.evmap.viewmodel.getFilterValues
import net.vonforst.evmap.viewmodel.getReferenceData
import java.io.IOException
import java.time.Duration
import java.time.Instant
@@ -62,26 +67,22 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var chargers: List<ChargeLocation>? = null
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val searchRadius = 5 // kilometers
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
HashMap()
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
min(
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
25
)
} else 6
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters =
Transformations.map(referenceData) { api.getFilters(it, carContext.stringProvider()) }
private val filtersWithValue = filtersWithValue(filters, filterValues)
private var filterStatus = prefs.filterStatus
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
private val hardwareMan: CarHardwareManager by lazy {
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
@@ -115,7 +116,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
prefs.placeSearchResultAndroidAutoName?.let {
carContext.getString(R.string.auto_chargers_near_location, it)
} ?: carContext.getString(
if (filterStatus.value == FILTERS_FAVORITES) {
if (filterStatus == FILTERS_FAVORITES) {
R.string.auto_favorites
} else {
R.string.auto_chargers_closeby
@@ -142,7 +143,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
builder.setNoItemsMessage(
carContext.getString(
if (filterStatus.value == FILTERS_FAVORITES) {
if (filterStatus == FILTERS_FAVORITES) {
R.string.auto_no_favorites_found
} else {
R.string.auto_no_chargers_found
@@ -154,18 +155,19 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
} ?: setLoading(true)
setCurrentLocationEnabled(true)
setHeaderAction(Action.APP_ICON)
val filtersCount = if (filterStatus.value == FILTERS_FAVORITES) 1 else {
filtersWithValue.value?.count {
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
filtersWithValue?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
}
setActionStrip(
ActionStrip.Builder()
.addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_settings
)
@@ -189,9 +191,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
.build()
)
.setOnClickListener {
screenManager.pushForResult(FilterScreen(carContext, session)) {
filterStatus.value = prefs.filterStatus
}
screenManager.push(FilterScreen(carContext, session))
session.mapScreen = null
}
.build())
@@ -289,10 +289,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
) {
return
}
val previousLocation = this.location
this.location = location
if (updateCoroutine != null) {
// don't update while still loading last update
return
if (previousLocation == null) {
loadChargers()
}
val now = Instant.now()
@@ -307,8 +307,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private fun loadChargers() {
val location = location ?: return
val referenceData = referenceData.value ?: return
val filters = filtersWithValue.value ?: return
val searchLocation =
prefs.placeSearchResultAndroidAuto ?: LatLng.fromLocation(location)
@@ -316,8 +314,14 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
updateCoroutine = lifecycleScope.launch {
try {
filterStatus = prefs.filterStatus
val filterValues =
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
val filters = repo.getFiltersAsync(carContext.stringProvider())
filtersWithValue = filtersWithValue(filters, filterValues)
// load chargers
if (filterStatus.value == FILTERS_FAVORITES) {
if (filterStatus == FILTERS_FAVORITES) {
chargers =
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
distanceBetween(
@@ -326,42 +330,51 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
}
} else {
val response = api.getChargepointsRadius(
referenceData,
val response = repo.getChargepointsRadius(
searchLocation,
searchRadius,
zoom = 16f,
filters
)
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR) {
withContext(Dispatchers.Main) { showLoadingError() }
return@launch
}
var chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
chargers?.let {
if (it.size < maxRows) {
// try again with larger radius
val response = api.getChargepointsRadius(
referenceData,
val response = repo.getChargepointsRadius(
searchLocation,
searchRadius * 10,
zoom = 16f,
filters
)
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR) {
withContext(Dispatchers.Main) { showLoadingError() }
return@launch
}
chargers =
response.data?.filterIsInstance(ChargeLocation::class.java)
}
}
this@MapScreen.chargers = chargers
}
updateCoroutine = null
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
withContext(Dispatchers.Main) { showLoadingError() }
}
}
}
private fun showLoadingError() {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
val isUpdate = this.energyLevel == null
this.energyLevel = energyLevel
@@ -373,10 +386,11 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
// Reloading chargers in onStart does not seem to count towards content limit.
// So let's do this so the user gets fresh chargers when re-entering the app.
invalidate()
filtersWithValue.observe(this@MapScreen) {
loadChargers()
if (prefs.dataSource != repo.api.value?.id) {
repo.api.value = createApi(prefs.dataSource, carContext)
}
invalidate()
loadChargers()
}
private fun setupListeners() {

View File

@@ -34,7 +34,8 @@ interface ChargepointApi<out T : ReferenceData> {
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
fun getName(): String
val name: String
val id: String
}
interface StringProvider {

View File

@@ -129,7 +129,8 @@ class GoingElectricApiWrapper(
private val clusterThreshold = 11f
val api = GoingElectricApi.create(apikey, baseurl, context)
override fun getName() = "GoingElectric.de"
override val name = "GoingElectric.de"
override val id = "going_electric"
override suspend fun getChargepoints(
referenceData: ReferenceData,

View File

@@ -108,7 +108,8 @@ class OpenChargeMapApiWrapper(
private val clusterThreshold = 11
val api = OpenChargeMapApi.create(apikey, baseurl, context)
override fun getName() = "OpenChargeMap.org"
override val name = "OpenChargeMap.org"
override val id = "open_charge_map"
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
if (value == null || value.all) null else value.values.joinToString(",")

View File

@@ -1,74 +0,0 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.FragmentChargepriceFeedbackBinding
import net.vonforst.evmap.viewmodel.ChargepriceFeedbackViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class ChargepriceFeedbackFragment : Fragment() {
private lateinit var binding: FragmentChargepriceFeedbackBinding
private val vm: ChargepriceFeedbackViewModel by viewModels(factoryProducer = {
viewModelFactory {
ChargepriceFeedbackViewModel(
requireActivity().application,
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val fragmentArgs: ChargepriceFeedbackFragmentArgs by navArgs()
vm.feedbackType.value = fragmentArgs.feedbackType
vm.charger.value = fragmentArgs.charger
vm.vehicle.value = fragmentArgs.vehicle
vm.chargePrices.value = fragmentArgs.chargePrices?.toList()
vm.batteryRange.value = fragmentArgs.batteryRange?.toList()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice_feedback, container, false
)
binding.lifecycleOwner = this
binding.vm = vm
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
binding.tariffSpinner.setAdapter(
ArrayAdapter<String>(
requireContext(),
R.layout.item_simple_multiline,
R.id.text,
mutableListOf()
)
)
}
}

View File

@@ -30,7 +30,6 @@ import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.ChargepriceFeedbackType
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
@@ -183,9 +182,6 @@ class ChargepriceFragment : Fragment() {
binding.btnSettings.setOnClickListener {
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
}
binding.btnFeedbackMissingPrice.setOnClickListener {
feedbackMissingPrice()
}
binding.batteryRange.setLabelFormatter { value: Float ->
val fmt = NumberFormat.getNumberInstance()
@@ -206,14 +202,6 @@ class ChargepriceFragment : Fragment() {
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
R.id.menu_feedback_missing_price -> {
feedbackMissingPrice()
true
}
R.id.menu_feedback_wrong_price -> {
feedbackWrongPrice()
true
}
else -> false
}
}
@@ -247,30 +235,4 @@ class ChargepriceFragment : Fragment() {
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}
private fun feedbackMissingPrice() {
findNavController().navigate(
R.id.action_chargeprice_to_chargepriceFeedbackFragment,
ChargepriceFeedbackFragmentArgs(
ChargepriceFeedbackType.MISSING_PRICE,
vm.charger.value,
vm.vehicle.value,
vm.chargePricesForChargepoint.value?.data?.toTypedArray(),
vm.batteryRange.value?.toFloatArray()
).toBundle()
)
}
private fun feedbackWrongPrice() {
findNavController().navigate(
R.id.action_chargeprice_to_chargepriceFeedbackFragment,
ChargepriceFeedbackFragmentArgs(
ChargepriceFeedbackType.WRONG_PRICE,
vm.charger.value,
vm.vehicle.value,
vm.chargePricesForChargepoint.value?.data?.toTypedArray(),
vm.batteryRange.value?.toFloatArray()
).toBundle()
)
}
}

View File

@@ -72,7 +72,6 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.bold
@@ -394,7 +393,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data
if (charger?.editUrl != null) {
(activity as? MapsActivity)?.openUrl(charger.editUrl)
if (vm.apiType == GoingElectricApiWrapper::class.java) {
if (vm.apiId == "going_electric") {
// instructions specific to GoingElectric
Toast.makeText(
requireContext(),

View File

@@ -1,34 +1,124 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import net.vonforst.evmap.model.ChargeLocation
import androidx.lifecycle.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.CoroutineScope
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.await
@Dao
interface ChargeLocationsDao {
abstract class ChargeLocationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg locations: ChargeLocation)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBlocking(vararg locations: ChargeLocation)
abstract suspend fun insert(vararg locations: ChargeLocation)
@Delete
suspend fun delete(vararg locations: ChargeLocation)
abstract suspend fun delete(vararg locations: ChargeLocation)
}
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocations(): LiveData<List<ChargeLocation>>
/**
* The ChargeLocationsRepository wraps the ChargepointApi and the DB to provide caching
* functionality.
*/
class ChargeLocationsRepository(
api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
private val db: AppDatabase, private val prefs: PreferenceDataSource
) {
val api = MutableLiveData<ChargepointApi<ReferenceData>>().apply { value = api }
@Query("SELECT * FROM chargelocation")
suspend fun getAllChargeLocationsAsync(): List<ChargeLocation>
val referenceData = this.api.switchMap { api ->
when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
private val chargeLocationsDao = db.chargeLocationsDao()
@Query("SELECT * FROM chargelocation WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
suspend fun getChargeLocationsInBoundsAsync(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double
): List<ChargeLocation>
fun getChargepoints(
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues?
): LiveData<Resource<List<ChargepointListItem>>> {
return liveData {
val refData = referenceData.await()
val result = api.value!!.getChargepoints(refData, bounds, zoom, filters)
emit(result)
}
}
fun getChargepointsRadius(
location: LatLng,
radius: Int,
zoom: Float,
filters: FilterValues?
): LiveData<Resource<List<ChargepointListItem>>> {
return liveData {
val refData = referenceData.await()
val result = api.value!!.getChargepointsRadius(refData, location, radius, zoom, filters)
emit(result)
}
}
fun getChargepointDetail(
id: Long
): LiveData<Resource<ChargeLocation>> {
return liveData {
val refData = referenceData.await()
val result = api.value!!.getChargepointDetail(refData, id)
emit(result)
}
}
fun getFilters(sp: StringProvider) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { refData: ReferenceData? ->
refData?.let { value = api.value!!.getFilters(refData, sp) }
}
}
suspend fun getFiltersAsync(sp: StringProvider): List<Filter<FilterValue>> {
val refData = referenceData.await()
return api.value!!.getFilters(refData, sp)
}
val chargeCardMap by lazy {
referenceData.map { refData: ReferenceData? ->
if (refData is GEReferenceData) {
refData.chargecards.associate {
it.id to it.convert()
}
} else {
null
}
}
}
}

View File

@@ -87,6 +87,7 @@ class GEReferenceDataRepository(
val networks = dao.getAllNetworks()
val chargeCards = dao.getAllChargeCards()
return MediatorLiveData<GEReferenceData>().apply {
value = null
listOf(chargeCards, networks, plugs).map { source ->
addSource(source) { _ ->
val p = plugs.value ?: return@addSource

View File

@@ -79,6 +79,7 @@ class OCMReferenceDataRepository(
val countries = dao.getAllCountries()
val operators = dao.getAllOperators()
return MediatorLiveData<OCMReferenceData>().apply {
value = null
listOf(countries, connectionTypes, operators).map { source ->
addSource(source) { _ ->
val ct = connectionTypes.value

View File

@@ -9,8 +9,6 @@ import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
@@ -130,18 +128,9 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
}
@BindingAdapter("data")
fun <T> setViewPager2Data(viewPager: ViewPager2, items: List<T>?) {
if (viewPager.adapter is ListAdapter<*, *>) {
(viewPager.adapter as ListAdapter<T, *>).submitList(items)
}
}
@BindingAdapter("data")
fun <T> setAutoCompleteTextViewData(atv: AutoCompleteTextView, items: List<T>?) {
if (atv.adapter is ArrayAdapter<*>) {
val arrayAdapter = atv.adapter as ArrayAdapter<T>
arrayAdapter.clear()
items?.let { arrayAdapter.addAll(it) }
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
}
}

View File

@@ -1,129 +0,0 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import java.io.IOException
enum class ChargepriceFeedbackType {
MISSING_PRICE, WRONG_PRICE, MISSING_VEHICLE
}
class ChargepriceFeedbackViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String
) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private var prefs = PreferenceDataSource(application)
// data supplied through fragment args
val feedbackType = MutableLiveData<ChargepriceFeedbackType>()
val charger = MutableLiveData<ChargeLocation>()
val chargepoint = MutableLiveData<Chargepoint>()
val vehicle = MutableLiveData<ChargepriceCar>()
val chargePrices = MutableLiveData<List<ChargePrice>>()
val batteryRange = MutableLiveData<List<Float>>()
// data input by user
val tariff = MutableLiveData<String>()
val price = MutableLiveData<String>()
val notes = MutableLiveData<String>()
val email = MutableLiveData<String>()
val loading = MutableLiveData<Boolean>().apply { value = false }
val chargePricesStrings = chargePrices.map {
it.map {
val name = if (!it.tariffName.lowercase().startsWith(it.provider.lowercase())) {
"${it.provider} ${it.tariffName}"
} else it.tariffName
val price = application.getString(
R.string.charge_price_format,
it.chargepointPrices[0].price,
currency(it.currency)
)
"$name: $price"
}.toList()
}
private val feedback = MediatorLiveData<ChargepriceUserFeedback>().apply {
listOf(
feedbackType,
charger,
chargepoint,
vehicle,
chargePrices,
tariff,
price,
notes,
email
).forEach {
addSource(it) {
try {
value = when (feedbackType.value!!) {
ChargepriceFeedbackType.MISSING_PRICE -> {
ChargepriceMissingPriceFeedback(
tariff.value ?: "",
charger.value?.network?.take(200) ?: "",
price.value ?: "",
charger.value?.let { ChargepriceApi.getPoiUrl(it) } ?: "",
notes.value ?: "",
email.value ?: "",
getChargepriceContext(),
ChargepriceApi.getChargepriceLanguage()
)
}
ChargepriceFeedbackType.WRONG_PRICE -> {
ChargepriceWrongPriceFeedback(
"", // TODO: dropdown value
charger.value?.network?.take(200) ?: "",
"", // TODO: dropdown value
price.value ?: "",
charger.value?.let { ChargepriceApi.getPoiUrl(it) } ?: "",
notes.value ?: "",
email.value ?: "",
getChargepriceContext(),
ChargepriceApi.getChargepriceLanguage()
)
}
ChargepriceFeedbackType.MISSING_VEHICLE -> {
TODO()
}
}
} catch (e: IllegalArgumentException) {
value = null
}
}
}
}
val formValid = feedback.map { it != null }
fun sendFeedback() {
val feedback = feedback.value ?: return
viewModelScope.launch {
loading.value = true
try {
api.userFeedback(feedback)
} catch (e: IOException) {
}
loading.value = false
}
}
private fun getChargepriceContext(): String {
val result = StringBuilder()
vehicle.value?.let { result.append("Vehicle: ${it.brand} ${it.name}\n") }
batteryRange.value?.let { result.append("Battery SOC: ${it[0]} to ${it[1]}\n") }
return result.toString()
}
}

View File

@@ -1,46 +1,15 @@
package net.vonforst.evmap.viewmodel
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.switchMap
import kotlinx.coroutines.CoroutineScope
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.*
import net.vonforst.evmap.model.Filter
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.FilterWithValue
import net.vonforst.evmap.storage.FilterValueDao
import kotlin.reflect.full.cast
fun ChargepointApi<ReferenceData>.getReferenceData(
scope: CoroutineScope,
ctx: Context
): LiveData<out ReferenceData> {
val db = AppDatabase.getInstance(ctx)
val prefs = PreferenceDataSource(ctx)
return when (this) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
this,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
this,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>
@@ -50,15 +19,20 @@ fun filtersWithValue(
addSource(it) {
val f = filters.value ?: return@addSource
val values = filterValues.value ?: return@addSource
value = f.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
value = filtersWithValue(f, values)
}
}
}
fun filtersWithValue(
filters: List<Filter<FilterValue>>,
values: List<FilterValue>
) = filters.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
fun FilterValueDao.getFilterValues(filterStatus: LiveData<Long>, dataSource: String) =
filterStatus.switchMap {
getFilterValues(it, dataSource)

View File

@@ -8,21 +8,17 @@ import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
class FilterViewModel(application: Application) : AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
private val referenceData = api.getReferenceData(viewModelScope, application)
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
value = api.getFilters(data, application.stringProvider())
}
}
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
private val repo = ChargeLocationsRepository(api, viewModelScope, db, prefs)
private val filters = repo.getFilters(application.stringProvider())
private val filterValues: LiveData<List<FilterValue>> by lazy {
db.filterValueDao().getFilterValues(FILTERS_CUSTOM, prefs.dataSource)

View File

@@ -7,29 +7,24 @@ import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OCMConnection
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween
import java.io.IOException
@Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
@@ -44,16 +39,19 @@ internal fun getClusterDistance(zoom: Float): Int? {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
val apiType: Class<ChargepointApi<ReferenceData>>
get() = api.value!!.javaClass
val apiId: String
get() = repo.api.value!!.id
val apiName: String
get() = api.value!!.getName()
get() = repo.api.value!!.name
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var api = MutableLiveData<ChargepointApi<ReferenceData>>().apply {
value = createApi(prefs.dataSource, application)
}
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val repo = ChargeLocationsRepository(
createApi(prefs.dataSource, application),
viewModelScope,
db,
prefs
)
val bottomSheetState: MutableLiveData<Int> by lazy {
state.getLiveData("bottomSheetState")
@@ -71,39 +69,20 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
}
private val filterValues: LiveData<List<FilterValue>> =
private val filterValues: LiveData<List<FilterValue>> = repo.api.switchMap {
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val referenceData =
Transformations.switchMap(api) { it.getReferenceData(viewModelScope, application) }
private val filters = Transformations.map(referenceData) {
api.value!!.getFilters(
it,
application.stringProvider()
)
}
private val filters = repo.getFilters(application.stringProvider())
private val filtersWithValue: LiveData<FilterValues> by lazy {
filtersWithValue(filters, filterValues)
}
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
val filterProfiles: LiveData<List<FilterProfile>> = repo.api.switchMap {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
MediatorLiveData<Map<Long, ChargeCard>>().apply {
value = null
addSource(referenceData) { data ->
value = if (data is GEReferenceData) {
data.chargecards.map {
it.id to it.convert()
}.toMap()
} else {
null
}
}
}
}
val chargeCardMap = repo.chargeCardMap
val filtersCount: LiveData<Int> by lazy {
MediatorLiveData<Int>().apply {
@@ -121,7 +100,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
value = Resource.loading(emptyList())
// this is not automatically updated with mapPosition, as we only want to update
// when map is idle.
listOf(filtersWithValue, referenceData).forEach {
listOf(filtersWithValue, repo.api).forEach {
addSource(it) {
reloadChargepoints()
}
@@ -141,28 +120,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
state.getLiveData("chargerSparse")
}
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
value = state["chargerDetails"]
listOf(chargerSparse, referenceData).forEach {
addSource(it) { _ ->
val charger = chargerSparse.value
val refData = referenceData.value
if (charger != null && refData != null) {
if (charger.id != value?.data?.id) {
loadChargerDetails(charger, refData)
}
} else {
value = null
}
}
}
observeForever {
// persist data in case fragment gets recreated
state["chargerDetails"] = it
}
val chargerDetails: LiveData<Resource<ChargeLocation>> = chargerSparse.switchMap { charger ->
charger?.id?.let {
repo.getChargepointDetail(it)
}
}.apply {
observeForever { chargerDetail ->
// persist data in case fragment gets recreated
state["chargerDetails"] = chargerDetail
}
}
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
addSource(chargerDetails) {
@@ -267,7 +235,9 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun reloadPrefs() {
filterStatus.value = prefs.filterStatus
api.value = createApi(prefs.dataSource, getApplication())
if (prefs.dataSource != apiId) {
repo.api.value = createApi(prefs.dataSource, getApplication())
}
}
fun toggleFilters() {
@@ -307,8 +277,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun reloadChargepoints() {
val pos = mapPosition.value ?: return
val filters = filtersWithValue.value ?: return
val referenceData = referenceData.value ?: return
chargepointLoader(Triple(pos, filters, referenceData))
chargepointLoader(pos to filters)
}
private val miniMarkerThreshold = 13f
@@ -335,17 +304,16 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}.distinctUntilChanged()
private var chargepointsInternal: LiveData<Resource<List<ChargepointListItem>>>? = null
private var chargepointLoader =
throttleLatest(
500L,
viewModelScope
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
) { data: Pair<MapPosition, FilterValues> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
val mapPosition = data.first
val filters = data.second
val api = api.value!!
val refData = data.third
if (filterStatus.value == FILTERS_FAVORITES) {
// load favorites from local DB
@@ -368,41 +336,40 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
return@throttleLatest
}
if (api is GoingElectricApiWrapper) {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val result = repo.getChargepoints(mapPosition.bounds, mapPosition.zoom, filters)
chargepointsInternal?.let { chargepoints.removeSource(it) }
chargepointsInternal = result
chargepoints.addSource(result) {
chargepoints.value = it
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else if (api is OpenChargeMapApiWrapper) {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
if (apiId == "going_electric") {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else if (apiId == "open_charge_map") {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
repo.referenceData.value!! as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
}
}
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
if (result.status == Status.ERROR && result.data == null) {
// keep old results if new data could not be loaded
result = Resource.error(result.message, chargepoints.value?.data)
}
chargepoints.value = result
}
private suspend fun loadAvailability(charger: ChargeLocation) {
@@ -417,47 +384,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
private var chargerLoadingTask: Job? = null
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
chargerDetails.value = Resource.loading(null)
chargerLoadingTask?.cancel()
chargerLoadingTask = viewModelScope.launch {
try {
val chargerDetail = api.value!!.getChargepointDetail(referenceData, charger.id)
chargerDetails.value = chargerDetail
if (favorites.value?.any { it.charger.id == chargerDetail.data?.id } == true) {
// update data of stored favorite
db.chargeLocationsDao().insert(charger)
}
} catch (e: IOException) {
chargerDetails.value = Resource.error(e.message, null)
e.printStackTrace()
fun loadChargerById(chargerId: Long) {
chargerSparse.value = null
repo.getChargepointDetail(chargerId).observeForever { response ->
if (response.status == Status.SUCCESS) {
chargerSparse.value = response.data
}
}
}
fun loadChargerById(chargerId: Long) {
chargerDetails.value = Resource.loading(null)
chargerSparse.value = null
referenceData.observeForever(object : Observer<ReferenceData> {
override fun onChanged(refData: ReferenceData) {
referenceData.removeObserver(this)
viewModelScope.launch {
val response = api.value!!.getChargepointDetail(refData, chargerId)
chargerDetails.value = response
if (response.status == Status.SUCCESS) {
chargerSparse.value = response.data
if (response.data != null && favorites.value?.any { it.charger.id == response.data.id } == true) {
// update data of stored favorite
db.chargeLocationsDao().insert(response.data)
}
} else {
chargerSparse.value = null
}
}
}
})
}
}

View File

@@ -4,10 +4,7 @@ import android.os.Parcelable
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.util.concurrent.atomic.AtomicBoolean
@@ -107,4 +104,41 @@ fun <T> throttleLatest(
waitingParam = param
}
}
}
public suspend fun <T> LiveData<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T?) {
if (value == null) return
removeObserver(this)
continuation.resume(value, null)
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<Resource<T>> {
override fun onChanged(value: Resource<T>) {
if (value.status != Status.LOADING) {
removeObserver(this)
continuation.resume(value, null)
}
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}

View File

@@ -168,8 +168,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/battery_range"
tools:itemCount="3"
tools:listitem="@layout/item_chargeprice"
tools:visibility="invisible" />
tools:listitem="@layout/item_chargeprice" />
<TextView
android:id="@+id/textView8"
@@ -180,11 +179,10 @@
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_tariffs_found"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toTopOf="@+id/btnFeedbackMissingPrice"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView9"
@@ -239,20 +237,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
<Button
android:id="@+id/btnFeedbackMissingPrice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/chargeprice_feedback_missing_price"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -1,209 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.ChargepriceFeedbackViewModel" />
<import type="net.vonforst.evmap.viewmodel.ChargepriceFeedbackType" />
<variable
name="vm"
type="ChargepriceFeedbackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:goneUnless="@{vm.loading}" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
app:goneUnless="@{!vm.loading}">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txtCPO"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@{@string/chargeprice_feedback_cpo(vm.charger.network)}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:goneUnless="@{vm.charger.network != null &amp;&amp; vm.feedbackType == ChargepriceFeedbackType.MISSING_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/chargeprice_feedback_cpo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_tariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:goneUnless="@{vm.feedbackType == ChargepriceFeedbackType.MISSING_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/txtCPO">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_tariff"
android:maxLength="100"
android:text="@={vm.tariff}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_tariff_spinner"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{vm.feedbackType == ChargepriceFeedbackType.WRONG_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tariff">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/tariff_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_tariff"
android:inputType="none"
app:data="@{vm.chargePricesStrings}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_price"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tariff_spinner">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_price"
android:maxLength="100"
android:text="@={vm.price}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="1000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_price">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="160dp"
android:gravity="top"
android:hint="@string/chargeprice_feedback_comment"
android:inputType="textMultiLine"
android:maxLength="1000"
android:text="@={vm.notes}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_comment">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_email"
android:inputType="textEmailAddress"
android:maxLength="100"
android:text="@={vm.email}" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:onClick="@{(view) -> vm.sendFeedback()}"
android:enabled="@{vm.formValid}"
android:text="@string/chargeprice_feedback_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_email" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="13dp"
android:paddingTop="13dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam" />
</FrameLayout>

View File

@@ -6,16 +6,6 @@
android:id="@+id/menu_help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_feedback_missing_price"
android:title="@string/chargeprice_feedback_missing_price"
app:showAsAction="never" />
<item
android:id="@+id/menu_feedback_wrong_price"
android:title="@string/chargeprice_feedback_wrong_price"
app:showAsAction="never" />
app:showAsAction="always" />
</menu>

View File

@@ -124,13 +124,6 @@
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_chargepriceFeedbackFragment"
app:destination="@id/chargeprice_feedback"
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_donateFragment"
app:destination="@id/donate" />
@@ -138,31 +131,6 @@
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
</fragment>
<fragment
android:id="@+id/chargeprice_feedback"
android:name="net.vonforst.evmap.fragment.ChargepriceFeedbackFragment"
android:label="@string/chargeprice_feedback"
tools:layout="@layout/fragment_chargeprice_feedback">
<argument
android:name="feedbackType"
app:argType="net.vonforst.evmap.viewmodel.ChargepriceFeedbackType" />
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation"
app:nullable="true" />
<argument
android:name="vehicle"
app:argType="net.vonforst.evmap.api.chargeprice.ChargepriceCar"
app:nullable="true" />
<argument
android:name="chargePrices"
app:argType="net.vonforst.evmap.api.chargeprice.ChargePrice[]"
app:nullable="true" />
<argument
android:name="batteryRange"
app:argType="float[]"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"

View File

@@ -270,13 +270,4 @@
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Mitwirkende</string>
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>
<string name="chargeprice_feedback">Feedback</string>
<string name="chargeprice_feedback_missing_price">Fehlenden Preis melden</string>
<string name="chargeprice_feedback_wrong_price">Falschen Preis melden</string>
<string name="chargeprice_feedback_email">E-Mail-Adresse für Rückfragen</string>
<string name="chargeprice_feedback_comment">Weitere Infos</string>
<string name="chargeprice_feedback_price">Preis (pro kWh, Minute, etc.)</string>
<string name="chargeprice_feedback_cpo">Betreiber der Station (CPO): %s</string>
<string name="chargeprice_feedback_tariff">Anbieter bzw. Tarif</string>
<string name="chargeprice_feedback_send">Absenden</string>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- tools:ignore="MissingQuantity" is temporary until Weblate 4.14 is released --><resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingQuantity">
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connecteurs</string>
<string name="no_maps_app_found">Aucune application de navigation trouvée</string>
<string name="no_browser_app_found">Aucun navigateur web trouvé</string>
<string name="no_maps_app_found">Installez d\'abord une application de navigation</string>
<string name="no_browser_app_found">Installez d\'abord un navigateur web</string>
<string name="address">Adresse</string>
<string name="operator">Opérateur</string>
<string name="network">Réseau</string>
@@ -22,19 +22,19 @@
<string name="menu_favs">Favoris</string>
<string name="menu_filter">Filtre</string>
<string name="not_implemented">pas encore mis en œuvre</string>
<string name="about">À propos d\'EVMap</string>
<string name="about">À propos</string>
<string name="github_link_title">Code source</string>
<string name="settings_ui">Interface utilisateur</string>
<string name="privacy">Politique de confidentialité</string>
<string name="fav_add">Ajouter aux favoris</string>
<string name="pref_navigate_use_maps">Démarrer la navigation immédiatement</string>
<string name="settings_ui">Interface</string>
<string name="privacy">Confidentialité</string>
<string name="fav_add">Enregistrer comme favori</string>
<string name="pref_navigate_use_maps">Naviguer maintenant</string>
<string name="coordinates">Coordonnées</string>
<string name="pref_navigate_use_maps_on">Le bouton de navigation lance immédiatement la navigation Google Maps</string>
<string name="pref_navigate_use_maps_on">Le bouton de navigation démarre le guidage d\'itinéraire avec Google Maps</string>
<string name="share">Partager</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_supercharger">Superchargeur Tesla</string>
<string name="show_less">moins…</string>
<string name="favorites_empty_state">Si vous ajoutez des chargeurs à vos favoris, ils apparaîtront ici.</string>
<string name="favorites_empty_state">Les chargeurs sauvegardés apparaissent ici</string>
<string name="donate">Faire un don</string>
<string name="map_type_satellite">Satellite</string>
<string name="map_type_terrain">Terrain</string>
@@ -70,10 +70,10 @@
<string name="category_zoo">Zoo</string>
<string name="menu_apply">Appliquer les filtres</string>
<string name="save_as_profile">Enregistrer en tant que profil</string>
<string name="welcome_1">Trouvez des chargeurs de véhicules électriques autour de vous.</string>
<string name="welcome_2">La couleur d\'un chargeur sur la carte vous indique sa puissance de charge maximale.</string>
<string name="welcome_2_detail">(Vous pouvez vérifier à nouveau les couleurs sous \"À propos d\'EVMap → FAQ\" dans le menu)</string>
<string name="donation_dialog_title">Merci d\'utiliser EVMap !</string>
<string name="welcome_1">Trouvez des chargeurs de véhicules électriques autour de vous</string>
<string name="welcome_2">La couleur d\'un chargeur sur la carte vous indique sa puissance de charge maximale</string>
<string name="welcome_2_detail">Cela peut également être vu dans \"À propos\" → \"Foire aux questions\"</string>
<string name="donation_dialog_title">Merci d\'utiliser EVMap</string>
<string name="chargeprice_donation_dialog_title">Vous êtes un vrai chasseur de bonnes affaires !</string>
<string name="deleted_filterprofile">\"%s\" supprimé</string>
<string name="undo">Annuler</string>
@@ -81,21 +81,22 @@
<string name="verified">vérifié</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d mode de paiement compatible</item>
<item quantity="many">%d modes de paiement compatibles</item>
<item quantity="other">%d modes de paiement compatibles</item>
</plurals>
<string name="verified_desc">Chargeur vérifié par un membre de la communauté %s - ne fonctionne pas forcément en ce moment.</string>
<string name="verified_desc">Le fonctionnement du chargeur a été confirmé au moins une fois par un membre de la communauté %s</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">frais de session</string>
<string name="chargeprice_per_kwh">par kWh</string>
<string name="chargeprice_per_minute">par min</string>
<string name="chargeprice_blocking_fee">Frais de blocage &gt;%s</string>
<string name="chargeprice_no_tariffs_found">Chargeprice.app n\'a trouvé aucun tarif de recharge compatible avec ce chargeur.</string>
<string name="chargeprice_no_tariffs_found">Aucun tarif de recharge pour ce chargeur sur Chargeprice.app</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Afficher les tarifs exclusifs aux clients</string>
<string name="chargeprice_battery_range">Charge de %1$.0f%% à %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Charge de</string>
<string name="chargeprice_stats">(%1$.0f kWh, approx. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_vehicle">Véhicule</string>
<string name="close">fermer</string>
<string name="close">Fermer</string>
<string name="chargeprice_title">Prix</string>
<string name="pref_chargeprice_currency">Devise</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
@@ -103,6 +104,7 @@
<string name="pref_data_source">Source des données</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d tarif sélectionné</item>
<item quantity="many">%d tarifs sélectionnés</item>
<item quantity="other">%d tarifs sélectionnés</item>
</plurals>
<string name="data_source_openchargemap">Open Charge Map</string>
@@ -119,8 +121,8 @@
<string name="pref_search_delete_recent">Supprimer les résultats de recherche récents</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Permettre une charge déséquilibrée</string>
<string name="pref_map_rotate_gestures_enabled">Activer la rotation de la carte</string>
<string name="pref_map_rotate_gestures_off">La carte reste orientée vers le nord</string>
<string name="pref_map_rotate_gestures_enabled">Rotation de la carte</string>
<string name="pref_map_rotate_gestures_off">Rotation désactivée (nord toujours en haut)</string>
<string name="refresh_live_data">rafraîchir le statut en temps réel</string>
<string name="pref_language_device_default">Utiliser la langue de l\'appareil</string>
<string name="pref_darkmode_device_default">Utiliser le réglage de l\'appareil</string>
@@ -139,22 +141,22 @@
<string name="general_info">Informations générales</string>
<string name="realtime_data_loading">Vérification du statut en temps réel…</string>
<string name="plug_ccs">CCS</string>
<string name="donation_successful">Merci ! ❤️</string>
<string name="donation_failed">Quelque chose s\'est mal passé. 😕</string>
<string name="donation_successful">Merci ❤️</string>
<string name="donation_failed">Quelque chose s\'est mal passé 😕</string>
<string name="category_supermarket">Supermarché</string>
<string name="version">Version</string>
<string name="oss_licenses">Licences Open Source</string>
<string name="oss_licenses">Licences</string>
<string name="realtime_data_source">Source du statut en temps réel (bêta) : %s</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_type_3">Type 3a</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_cee_rot">CEE Rouge</string>
<string name="all">tous</string>
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</string>
<string name="menu_report_new_charger">Signaler un nouveau chargeur</string>
<string name="menu_report_new_charger">Nouveau chargeur</string>
<string name="filter_connectors">Connecteurs</string>
<string name="copyright_summary">©2020-2022 Johan von Forstner</string>
<string name="other">Autre</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes avec lemplacement du chargeur</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes à lemplacement du chargeur</string>
<string name="settings_map">Carte</string>
<string name="fault_report">Rapport d\'anomalie</string>
<string name="filter_free">Uniquement des chargeurs gratuits</string>
@@ -177,13 +179,13 @@
<string name="number_selected">%d sélectionné</string>
<string name="cancel">Annuler</string>
<string name="filter_operators">Opérateurs</string>
<string name="chargeprice_donation_dialog_detail">Il semble que vous appréciez beaucoup la fonction de comparaison des prix. Pour accéder aux données de tarification, le développeur d\'EVMap doit payer une redevance mensuelle au fournisseur de données Chargeprice.app. Par conséquent, veuillez envisager de soutenir EVMap par un don.</string>
<string name="chargeprice_donation_dialog_detail">Vous faites bon usage de la fonction de comparaison des prix. Aidez-nous à couvrir les coûts de ces données en soutenant EVMap par un don.</string>
<string name="and_n_others">et %d autres</string>
<string name="contact">Contact</string>
<string name="pref_map_provider">Fournisseur de cartes</string>
<string name="twitter">Twitter</string>
<string name="category_petrol_station">Station-service</string>
<string name="edit_on_goingelectric_info">Si seule une page vide s\'affiche ici, veuillez d\'abord vous connecter à GoingElectric.de.</string>
<string name="edit_on_goingelectric_info">Veuillez vous connecter à GoingElectric.de si cette page est vide</string>
<string name="settings_chargeprice">Comparaison des prix</string>
<string name="category_service_on_motorway">Aire de service (sur autoroute)</string>
<string name="category_railway_station">Gare ferroviaire</string>
@@ -196,7 +198,7 @@
<string name="reorder">réorganiser</string>
<string name="delete">Supprimer</string>
<string name="save_profile_enter_name">Saisissez le nom du profil de filtrage :</string>
<string name="donation_dialog_detail">EVMap est un logiciel libre et open source que je développe pendant mon temps libre. Les contributions de codage sur GitHub sont très appréciées. Cependant, en raison de la popularité croissante de l\'application, je dois également couvrir certains coûts de fonctionnement, par exemple pour l\'accès aux sources de données. Par conséquent, veuillez envisager de soutenir l\'application par un don ou via les sponsors GitHub.</string>
<string name="donation_dialog_detail">EVMap est un logiciel libre et gratuit. Les contributions de codage sur GitHub sont très appréciées. Pour aider à couvrir les frais de fonctionnement de l\'accès aux sources de données, veuillez envisager de faire un don du montant de votre choix au développeur.</string>
<string name="charging_barrierfree">Utilisable sans enregistrement</string>
<string name="chargeprice_battery_range_to">à</string>
<string name="category_service_off_motorway">Aire de service (hors autoroute)</string>
@@ -208,23 +210,24 @@
<string name="category_holiday_home">Maison de vacances</string>
<string name="category_caravan_site">Emplacement pour caravanes</string>
<string name="filter_custom">Filtre modifié</string>
<string name="filterprofiles_empty_state">Vous n\'avez pas encore enregistré de profils de filtrage.</string>
<string name="filterprofiles_empty_state">Vous n\'avez aucun profil de filtrage enregistré</string>
<string name="welcome_to_evmap">Bienvenue sur EVMap</string>
<string name="chargeprice_provider_customer_tariff">Uniquement pour les clients du fournisseur</string>
<string name="powered_by_chargeprice">alimenté par Chargeprice</string>
<string name="pref_my_vehicle">Mes véhicules</string>
<string name="pref_my_tariffs">Mes tarifs de recharge</string>
<string name="license">Licence</string>
<string name="autocomplete_connection_error">Les suggestions n\'ont pas pu être chargées</string>
<string name="autocomplete_connection_error">Impossible de charger les suggestions</string>
<string name="chargeprice_select_connector">Choisir le connecteur</string>
<string name="chargeprice_select_car_first">Veuillez d\'abord sélectionner le modèle de votre voiture dans les paramètres.</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Certains fournisseurs offrent des tarifs moins chers exclusivement à leurs clients (par exemple, électricité domestique, gaz)</string>
<string name="pref_chargeprice_no_base_fee">Afficher uniquement les tarifs sans frais mensuels</string>
<string name="chargeprice_no_compatible_connectors">Aucun des connecteurs de cette station de charge n\'est compatible avec votre véhicule.</string>
<string name="chargeprice_select_car_first">Veuillez d\'abord sélectionner le modèle de votre voiture dans les paramètres</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Certains fournisseurs d\'énergie offrent des tarifs moins chers exclusivement à leurs clients</string>
<string name="pref_chargeprice_no_base_fee">Exclure les tarifs avec frais mensuels</string>
<string name="chargeprice_no_compatible_connectors">Pas de connecteurs compatibles dans cette station de recharge</string>
<string name="chargeprice_connection_error">Impossible de charger les prix</string>
<string name="pref_search_provider">Fournisseur de recherche de lieux</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one" tools:ignore="ImpliedQuantity">(sera mis en évidence dans la comparaison des prix)</item>
<item quantity="many">(seront mis en évidence dans la comparaison des prix)</item>
<item quantity="other">(seront mis en évidence dans la comparaison des prix)</item>
</plurals>
<string name="deleted_recent_search_results">Les résultats de recherche récents ont été supprimés</string>
@@ -235,21 +238,21 @@
<string name="got_it">J\'ai compris</string>
<string name="powered_by_mapbox">propulsé par Mapbox</string>
<string name="lets_go">Allons-y</string>
<string name="crash_report_text">Désolé, il semble que EVMap ait planté. Veuillez envoyer un rapport de plantage au développeur.</string>
<string name="crash_report_text">EVMap a planté. Veuillez envoyer un rapport de plantage au développeur.</string>
<string name="unknown_operator">Opérateur inconnu</string>
<string name="data_source_goingelectric_desc">Très bonne couverture en Allemagne, en Autriche et en Suisse et dans de nombreux pays voisins. Descriptions en allemand. Maintenu par la communauté.</string>
<string name="data_source_goingelectric_desc">Idéal dans les pays germanophones. Descriptions en allemand. Maintenu par la communauté.</string>
<string name="data_source_openchargemap_desc">Couverture mondiale avec une qualité variable. Descriptions en anglais ou dans la langue locale. Données ouvertes maintenues par la communauté et provenant de sources gouvernementales dans certains pays (par exemple, Amérique du Nord, Royaume-Uni, France, Norvège).</string>
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
<string name="settings_data_sources">Sources de données</string>
<string name="data_sources_description">EVMap supporte plusieurs sources de données pour les stations de recharge. Veuillez sélectionner celle que vous souhaitez utiliser. Vous pourrez toujours la modifier ultérieurement dans les paramètres de l\'application.</string>
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses. Si vous utilisez souvent cette fonctionnalité, veuillez envisager de faire un don via \"À propos dEVMap -&gt; Faire un don\".</string>
<string name="data_sources_description">Veuillez choisir une source de données pour les stations de recharge. Vous pourrez la modifier ultérieurement dans les paramètres de l\'application.</string>
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via \"À propos\" -&gt; \"Faire un don\".</string>
<string name="pref_chargeprice_currency_hrk">Kuna croate (HRK)</string>
<string name="help">Aide</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge avec &gt;4,5 kW aux stations AC pour les voitures avec chargeur monophasé</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge en courant alternatif monophasé de plus de 4,5 kW</string>
<string name="pref_chargeprice_currency_huf">Forint hongrois (HUF)</string>
<string name="pref_chargeprice_currency_pln">Złoty polonais (PLN)</string>
<string name="pref_map_rotate_gestures_on">La carte peut être pivotée avec un geste à deux doigts</string>
<string name="pref_map_rotate_gestures_on">Utilisez deux doigts pour faire pivoter la carte</string>
<string name="pref_chargeprice_currency_chf">Franc suisse (CHF)</string>
<string name="pref_chargeprice_currency_usd">Dollar américain (USD)</string>
<string name="pref_chargeprice_currency_sek">Couronne suédoise (SEK)</string>

View File

@@ -143,12 +143,12 @@
<string name="category_parking_underground">Parkeringsgarasje under bakken</string>
<string name="reorder">endre rekkefølge</string>
<string name="save_profile_enter_name">Skriv inn navnet på filterprofilen:</string>
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler.</string>
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler</string>
<string name="chargeprice_donation_dialog_title">Du er en sann gjerrigknark.</string>
<string name="deleted_filterprofile">Slettet «%s»</string>
<string name="charging_barrierfree">Kan brukes uten registrering</string>
<string name="welcome_1">Finn kjøretøyladere der du er.</string>
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt.</string>
<string name="welcome_1">Finn kjøretøyladere der du er</string>
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt</string>
<string name="welcome_2_detail">Dette er også å finne i «Om» → «O-S-S» i menyen</string>
<string name="verified_desc">Lader bekreftet av et medlem av %s-gemenskapen. Dette betyr ikke at den virker nå.</string>
<string name="charge_price_format">%2$s%1$.2f</string>

View File

@@ -269,13 +269,4 @@
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contributors</string>
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
<string name="chargeprice_feedback">Feedback</string>
<string name="chargeprice_feedback_missing_price">Report missing price</string>
<string name="chargeprice_feedback_wrong_price">Report incorrect price</string>
<string name="chargeprice_feedback_email">Email address for further questions</string>
<string name="chargeprice_feedback_comment">Other details</string>
<string name="chargeprice_feedback_price">Price (per kWh, minute, etc.)</string>
<string name="chargeprice_feedback_cpo">Station operator (CPO): %s</string>
<string name="chargeprice_feedback_tariff">Provider / plan</string>
<string name="chargeprice_feedback_send">Submit</string>
</resources>

View File

@@ -0,0 +1,8 @@
Verbesserungen:
- Einige Texte vereinfacht
- Unterstützung für Sprachauswahl pro App von Android 13
Fehler behoben:
- Filtermenü ließ sich nicht öffnen
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
- Abstürze unter Android Auto

View File

@@ -0,0 +1,8 @@
Improvements:
- Simplified some texts
- Support for Android 13's per-app language selector
Bugfixes:
- Filter menu could not be opened
- Crashes / inconsistencies after switching data source
- Crashes on Android Auto