Compare commits

...

48 Commits
1.4.3 ... 1.4.7

Author SHA1 Message Date
johan12345
086cc51dd3 Release 1.4.7 2023-02-12 18:20:36 +01:00
johan12345
0de91bc107 update CustomBottomSheetBehavior 2023-02-12 18:17:38 +01:00
johan12345
3436bcd870 update CustomBottomSheetBehavior 2023-02-12 18:04:40 +01:00
johan12345
22c150d557 upgrade dependencies 2023-02-12 17:53:21 +01:00
johan12345
675abb5011 DonateViewModel: fix possible NPE when loading products
see also https://github.com/EventFahrplan/EventFahrplan/issues/71
2023-02-12 17:40:06 +01:00
johan12345
af2a2cfcae enable Dutch locale
fixes #267
2023-02-08 21:26:44 +01:00
Hosted Weblate
f74526fdd6 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Update translation files

Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/App Store metadata
2023-02-08 21:19:47 +01:00
Hosted Weblate
c5bbca0428 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/
Translation: EVMap/Android (strings specific to F-Droid variant)
2023-02-08 21:19:26 +01:00
johan12345
6167079c0e update dependencies 2023-02-05 15:36:08 +01:00
Hosted Weblate
c3836a92ad Translated using Weblate (German)
Currently translated at 100.0% (283 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2023-02-05 15:31:48 +01:00
Hosted Weblate
dccce1a0a0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.7% (237 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2023-02-05 15:31:47 +01:00
Hosted Weblate
74d79640a8 Translated using Weblate (English)
Currently translated at 100.0% (283 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/en/
Translation: EVMap/Android
2023-02-05 15:31:47 +01:00
Hosted Weblate
0eb6ece780 Translated using Weblate (French)
Currently translated at 92.2% (261 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2023-02-05 15:31:47 +01:00
johan12345
ae15b13591 use patched mapbox-events-android version only for foss variant
to allow Google location services in google variant, even when Mapbox is selected
2023-02-04 19:40:16 +01:00
johan12345
4962eb7268 transfer project to ev-map GitHub org 2023-02-04 19:23:29 +01:00
johan12345
abe360d7c2 transfer JitPack dependencies to ev-map GitHub org 2023-02-04 19:18:42 +01:00
johan12345
2aa1fcf5bd Release 1.4.6 2023-02-01 18:56:15 +01:00
johan12345
221e5f49bc catch JsonDataExceptions from fronyx API 2023-02-01 18:54:35 +01:00
johan12345
df6f26ad56 fix import 2023-01-29 19:38:08 +01:00
johan12345
1210efd3b9 MapFragment: update map bottom padding when bottom sheet comes up 2023-01-29 18:54:02 +01:00
johan12345
097be8c92b get rid of some warnings 2023-01-28 22:33:49 +01:00
johan12345
16031884ac upgrade dependencies
Android Studio 2022.1.1
resourcesPlaceholders plugin broke - removed it for now
2023-01-28 22:00:52 +01:00
johan12345
c0b4c56eda Release 1.4.5 2023-01-20 20:10:57 +01:00
Hosted Weblate
9587ee948d Update translation files
Updated by "Squash Git commits" hook in Weblate.

Translated using Weblate (Norwegian Bokmål)

Currently translated at 83.3% (30 of 36 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/nb_NO/
Translation: EVMap/Android (strings specific to Google Play variant)
2023-01-20 19:13:16 +01:00
johan12345
890eec4419 FilterFragment: add button to reset current settings 2023-01-20 19:12:52 +01:00
johan12345
c972c871d4 add icons to filter popup menu 2023-01-20 18:38:06 +01:00
johan12345
e4da902430 GooglePlacesAutocompleteProvider: fix crash on network error 2023-01-19 22:28:13 +01:00
johan12345
7a5d4b4107 fix NPE 2023-01-08 12:48:23 +01:00
johan12345
80642b1731 fix infinite recursion in Utils.max 2023-01-08 12:45:31 +01:00
johan12345
6dab611c1b Release 1.4.4 2022-12-15 21:40:22 +01:00
johan12345
d9fc43af68 fix size of layers FAB
(fabSize=mini did not apply anymore since #135)
2022-12-11 19:01:24 +01:00
johan12345
2fd0fa7e22 update dependencies 2022-12-11 18:51:56 +01:00
johan12345
b04284fb16 AA/AAOS: fix crash when icon for plug type is not available 2022-12-11 17:58:06 +01:00
johan12345
7b3bd84d18 AA/AAOS: clear list of chargers on loading error 2022-12-11 17:49:04 +01:00
johan12345
773d052819 fix NPE in OpenChargeMapApi 2022-12-11 12:29:46 +01:00
johan12345
4e0ad98e17 AA/AAOS: implement app-driven refresh
(if supported by host)
2022-12-11 00:34:05 +01:00
johan12345
d8e572338a upgrade car app library to 1.3.0-rc01 2022-12-10 23:59:51 +01:00
johan12345
ff86eeff95 AA/AAOS: add some more useful info to developer options screen 2022-12-10 23:58:26 +01:00
johan12345
47f57992fb add extension function to abbreviate getContentLimit calls 2022-12-10 23:31:40 +01:00
johan12345
0ae59358ca AA/AAOS: implement a full "about" screen 2022-12-10 23:21:29 +01:00
johan12345
576e0b9c42 add @ExperimentalCarApi 2022-12-10 22:52:26 +01:00
johan12345
3878b27154 Revert "AAOS: CarSensorsWrapper: add experimental rotation sensor implementations"
This reverts commit e2cf332f34.
2022-12-10 22:46:31 +01:00
johan12345
2166ac076a Android Auto/Automotive: Fall back to GPS bearing if compass not available 2022-12-10 22:45:50 +01:00
johan12345
c489df2aaf Android Auto/Automotive: Fix crash when no or all prices match "my plans" 2022-12-09 21:43:43 +01:00
johan12345
56712ff1af Android Auto/Automotive: Fix crash when no prices are found 2022-12-09 21:34:45 +01:00
johan12345
e2cf332f34 AAOS: CarSensorsWrapper: add experimental rotation sensor implementations 2022-12-08 22:53:03 +01:00
johan12345
0b541d498d AA/AAOS: add developer options screen 2022-12-08 22:52:27 +01:00
johan12345
1bdc576300 AA/AAOS: enable search button while driving (#262)
Note that AA/AAOS will block access to keyboard while driving, but the search screen is still useful to access recent results. Also this enables the "clear search" button while driving.
2022-12-08 21:49:09 +01:00
80 changed files with 1209 additions and 323 deletions

View File

@@ -1,7 +1,7 @@
EVMap [![Build Status](https://github.com/johan12345/EVMap/actions/workflows/tests.yml/badge.svg)](https://github.com/johan12345/EVMap/actions)
EVMap [![Build Status](https://github.com/ev-map/EVMap/actions/workflows/tests.yml/badge.svg)](https://github.com/ev-map/EVMap/actions)
=====
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
Android app to find electric vehicle charging stations.
@@ -28,7 +28,7 @@ Features
Screenshots
-----------
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
Development setup
-----------------

View File

@@ -8,9 +8,8 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'de.timfreiheit.resourceplaceholders'
def supportedLocales = "en,de,fr,nb-rNO"
def supportedLocales = "en,de,fr,nb-rNO,nl"
android {
compileSdkVersion 33
@@ -21,11 +20,11 @@ android {
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 148
versionName "1.4.3"
versionCode 162
versionName "1.4.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")
resConfigs supportedLocales.split(',')
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
}
@@ -104,9 +103,6 @@ android {
unitTests.includeAndroidResources true
}
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
namespace 'net.vonforst.evmap'
// add API keys from environment variable if not set in apikeys.xml
@@ -159,19 +155,19 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.4"
implementation "androidx.fragment:fragment-ktx:1.5.5"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.browser:browser:1.5.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f4f641aab5'
implementation 'com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
@@ -180,7 +176,7 @@ dependencies {
implementation 'com.squareup.moshi:moshi-adapters:1.13.0'
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
implementation 'com.github.ev-map:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:4.1.0'
@@ -190,28 +186,30 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
def carAppVersion = '1.3.0-beta01'
def carAppVersion = '1.3.0-rc01'
googleImplementation "androidx.car.app:app:$carAppVersion"
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
// AnyMaps
def anyMapsVersion = 'a9b3dd7d99'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
def anyMapsVersion = '7fdcf50fc4'
implementation "com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation("com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
exclude group: 'com.google.android.gms', module: 'play-services-location'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-core'
}
// patched version of mapbox-android-core that removes build-time dependency on GMS
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
// original version of mapbox-android-core
googleImplementation 'com.mapbox.mapboxsdk:mapbox-android-core:2.0.1'
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation 'com.github.ev-map:mapbox-events-android:a21c324501'
// Google Places
googleImplementation 'com.google.android.libraries.places:places:2.7.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
googleImplementation 'com.google.android.libraries.places:places:3.0.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
// Mapbox Geocoding
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
@@ -226,13 +224,13 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// room library
def room_version = "2.4.3"
def room_version = "2.5.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// billing library
def billing_version = "4.1.0"
def billing_version = "5.1.0"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
@@ -257,12 +255,12 @@ dependencies {
testGoogleImplementation 'org.robolectric:robolectric:4.9'
testGoogleImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
}
private static String decode(String s, String key) {

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
<string name="donate_paypal">Doneer via PayPal</string>
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -2,5 +2,4 @@
<resources>
<string name="pref_search_provider_default" translatable="false">mapbox</string>
<string name="pref_map_provider_default" translatable="false">mapbox</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
</resources>

View File

@@ -11,6 +11,7 @@
<queries>
<package android:name="com.google.android.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" />
</queries>
<application>

View File

@@ -1,10 +1,5 @@
package net.vonforst.evmap.auto
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
@@ -22,7 +17,6 @@ import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
@@ -49,9 +43,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
private var prices: List<ChargePrice>? = null
private var meta: ChargepriceChargepointMeta? = null
private var chargepoint: Chargepoint? = null
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
private var errorMessage: String? = null
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
@@ -95,22 +87,32 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
val myPricesList = buildPricesList(myPrices)
val otherPricesList = buildPricesList(otherPrices)
addSectionedList(
SectionedItemList.create(
myPricesList,
(header?.let { it + "\n" } ?: "") +
carContext.getString(R.string.chargeprice_header_my_tariffs)
if (myPricesList.items.isNotEmpty() && otherPricesList.items.isNotEmpty()) {
addSectionedList(
SectionedItemList.create(
myPricesList,
(header?.let { it + "\n" } ?: "") +
carContext.getString(R.string.chargeprice_header_my_tariffs)
)
)
)
addSectionedList(
SectionedItemList.create(
otherPricesList,
carContext.getString(R.string.chargeprice_header_other_tariffs)
addSectionedList(
SectionedItemList.create(
otherPricesList,
carContext.getString(R.string.chargeprice_header_other_tariffs)
)
)
)
} else {
val list =
if (myPricesList.items.isNotEmpty()) myPricesList else otherPricesList
if (header != null) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
}
}
} else {
val list = buildPricesList(prices)
if (header != null) {
if (header != null && list.items.isNotEmpty()) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
@@ -127,38 +129,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
)
).build()
).setOnClickListener {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(
ContextCompat.getColor(
carContext,
R.color.colorPrimary
)
)
.build()
)
.build().intent
intent.data =
Uri.parse(ChargepriceApi.getPoiUrl(charger))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
}
} catch (e: ActivityNotFoundException) {
CarToast.makeText(
carContext,
R.string.no_browser_app_found,
CarToast.LENGTH_LONG
).show()
}
openUrl(carContext, ChargepriceApi.getPoiUrl(charger))
}.build()
).build()
)
@@ -235,7 +206,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
private fun loadPrices(model: Model?) {
val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
val manufacturer = model?.manufacturer?.value
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
lifecycleScope.launch {

View File

@@ -64,9 +64,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private val iconGen =
ChargerIconGenerator(carContext, null, height = imageSize)
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
} else 2
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
private val largeImageSupported =
ctx.carAppApiLevel >= 4 // since API 4, Row.setImage is supported
@@ -350,23 +348,31 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
if (i > 0) chargepointsText.append(" · ")
chargepointsText.append(
"${cp.count}× "
).append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
iconForPlugType(cp.type)
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
).append(" ").append(cp.formatPower())
chargepointsText.apply {
if (i > 0) append(" · ")
append("${cp.count}× ")
val plugIcon = iconForPlugType(cp.type)
if (plugIcon != 0) {
append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
plugIcon
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
} else {
append(nameForPlugType(carContext.stringProvider(), cp.type))
}
append(" ")
append(cp.formatPower())
}
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",

View File

@@ -31,10 +31,8 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
private var page = 0
init {
@@ -129,10 +127,14 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
)
setOnClickListener {
page -= 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
if (!supportsRefresh) {
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
} else {
invalidate()
}
}
}.build())
@@ -222,10 +224,14 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
)
setOnClickListener {
page += 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
if (!supportsRefresh) {
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
} else {
invalidate()
}
}
}.build())
@@ -243,9 +249,8 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
private val vm = FilterViewModel(carContext.applicationContext as Application)
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
private var page = 0
private var paginatedFilters = vm.filtersWithValue.map {
@@ -372,10 +377,14 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
)
setOnClickListener {
page -= 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
if (!supportsRefresh) {
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
} else {
invalidate()
}
}
}.build())
@@ -454,10 +463,14 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
)
setOnClickListener {
page += 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
if (!supportsRefresh) {
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
} else {
invalidate()
}
}
}.build())

View File

@@ -71,6 +71,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var location: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var lastChargersUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private var loadingError = false
private var locationError = false
@@ -81,14 +82,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private val searchRadius = 5 // kilometers
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private val chargersUpdateThresholdDistance = 500 // meters
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
HashMap()
private val maxRows = if (ctx.carAppApiLevel >= 2) {
min(
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
25
)
} else 6
private val maxRows =
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
private var filterStatus = prefs.filterStatus
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
@@ -205,7 +205,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
).setTint(CarColor.DEFAULT).build()
)
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
screenManager.push(SettingsScreen(carContext, session))
session.mapScreen = null
}
.build())
@@ -223,11 +223,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
setOnClickListener {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pushForResult(DummyReturnScreen(carContext)) {
if (!supportsRefresh) {
screenManager.pushForResult(DummyReturnScreen(carContext)) {
chargers = null
loadChargers()
}
} else {
chargers = null
loadChargers()
}
@@ -243,7 +248,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
session.mapScreen = null
}
})
}
}.build())
.addAction(
Action.Builder()
@@ -330,7 +335,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
availabilities[charger.id]?.second?.let { av ->
val status = av.status.values.flatten()
val available = availabilityText(status)
val total = charger.chargepoints.sumBy { it.count }
val total = charger.chargepoints.sumOf { it.count }
if (text.isNotEmpty()) text.append(" · ")
text.append(
@@ -364,6 +369,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
this.location = location
if (previousLocation == null) {
loadChargers()
return
}
val now = Instant.now()
@@ -374,6 +380,23 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
// update displayed distances
invalidate()
}
// if chargers are searched around current location, consider app-driven refresh
val searchLocation =
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
val distance = searchLocation?.let {
distanceBetween(
it.latitude, it.longitude, location.latitude, location.longitude
)
} ?: 0.0
if (supportsRefresh && (lastChargersUpdateTime == null ||
Duration.between(
lastChargersUpdateTime,
now
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
) {
onContentRefreshRequested()
}
}
private fun loadChargers() {
@@ -413,13 +436,17 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
).awaitFinished()
if (response.status == Status.ERROR) {
loadingError = true
this@MapScreen.chargers = null
invalidate()
return@launch
}
chargers = headingFilter(
response.data?.filterIsInstance(ChargeLocation::class.java),
searchLocation
)
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
if (prefs.placeSearchResultAndroidAutoName == null) {
chargers = headingFilter(
chargers,
searchLocation
)
}
if (chargers == null || chargers.size >= maxRows) {
break
}
@@ -428,6 +455,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
updateCoroutine = null
lastChargersUpdateTime = Instant.now()
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
@@ -443,8 +471,12 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private fun headingFilter(
chargers: List<ChargeLocation>?,
searchLocation: LatLng
): List<ChargeLocation>? =
heading?.orientations?.value?.get(0)?.let { heading ->
): List<ChargeLocation>? {
// use compass heading if available, otherwise fall back to GPS
val location = location
val heading = heading?.orientations?.value?.get(0)
?: if (location?.hasBearing() == true) location.bearing else null
return heading?.let {
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
chargers?.filter {
@@ -458,6 +490,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
abs(diff) < 30
}
} ?: chargers
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
val isUpdate = this.energyLevel == null

View File

@@ -46,7 +46,7 @@ class PermissionScreen(
}
private fun requestPermissions() {
carContext.requestPermissions(permissions) { granted, rejected ->
carContext.requestPermissions(permissions) { granted, _ ->
if (granted.containsAll(permissions)) {
screenManager.pop()
} else {

View File

@@ -148,6 +148,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
}
private suspend fun loadNewList(query: String) {
val location = location?.let { LatLng.fromLocation(it) }
for (provider in providers) {
try {
recentResults.clear()
@@ -161,7 +162,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
}
recentResults.addAll(recentPlaces)
resultList =
recentPlaces.map { it.asAutocompletePlace(LatLng.fromLocation(location)) }
recentPlaces.map { it.asAutocompletePlace(location) }
invalidate()
// if we already have enough results or the query is short, stop here
@@ -170,7 +171,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
// then search online
val recentIds = recentPlaces.map { it.id }
resultList = withContext(Dispatchers.IO) {
(resultList!! + provider.autocomplete(query, LatLng.fromLocation(location))
(resultList!! + provider.autocomplete(query, location)
.filter { !recentIds.contains(it.id) }).take(maxItems)
}
invalidate()

View File

@@ -15,9 +15,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
protected var fullList: List<T>? = null
private var currentList: List<T> = emptyList()
private var query: String = ""
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
protected abstract val isMultiSelect: Boolean
protected abstract val shouldShowSelectAll: Boolean

View File

@@ -1,15 +1,21 @@
package net.vonforst.evmap.auto
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.hardware.Sensor
import android.hardware.SensorManager
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.*
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
@@ -18,7 +24,8 @@ import net.vonforst.evmap.storage.PreferenceDataSource
import kotlin.math.max
import kotlin.math.min
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
@@ -71,7 +78,7 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
)
.setBrowsable(true)
.setOnClickListener {
screenManager.push(VehicleDataScreen(carContext))
screenManager.push(VehicleDataScreen(carContext, session))
}
.build()
)
@@ -81,9 +88,34 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
.setToggle(Toggle.Builder {
prefs.showChargersAheadAndroidAuto = it
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_navigation
)
).setTint(CarColor.DEFAULT).build()
)
.build()
)
}
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.about))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_about
)
).setTint(CarColor.DEFAULT).build()
)
.setBrowsable(true)
.setOnClickListener {
screenManager.push(AboutScreen(carContext))
}
.build()
)
}.build())
}.build()
}
@@ -239,9 +271,7 @@ class ChooseDataSourceScreen(
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(carContext)
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
@@ -556,4 +586,165 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
)
}.build()
}
}
class AboutScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
var developerOptionsCounter = 0
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.about))
setHeaderAction(Action.BACK)
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.version))
.addText(BuildConfig.VERSION_NAME)
.addText(
carContext.getString(R.string.copyright) + " " + carContext.getString(
R.string.copyright_summary
)
)
.setBrowsable(prefs.developerModeEnabled)
.setOnClickListener {
if (!prefs.developerModeEnabled) {
developerOptionsCounter += 1
if (developerOptionsCounter >= 7) {
prefs.developerModeEnabled = true
invalidate()
CarToast.makeText(
carContext,
carContext.getString(R.string.developer_mode_enabled),
CarToast.LENGTH_SHORT
).show()
}
} else {
screenManager.pushForResult(DeveloperOptionsScreen(carContext)) {
developerOptionsCounter = 0
invalidate()
}
}
}.build()
)
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.faq))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.faq_link))
}).build()
)
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.donate))
.addText(carContext.getString(R.string.donate_desc))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
if (BuildConfig.FLAVOR_automotive == "automotive") {
// we can't open the donation page on the phone in this case
openUrl(carContext, carContext.getString(R.string.paypal_link))
} else {
val intent = Intent(carContext, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_DONATE, true)
carContext.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
}
}).build()
)
}.build(), carContext.getString(R.string.about)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.twitter))
.addText(carContext.getString(R.string.twitter_handle))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.twitter_url))
}).build()
)
if (maxRows > 6) {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.goingelectric_forum))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(
carContext,
carContext.getString(R.string.goingelectric_forum_url)
)
}).build()
)
}
}.build(), carContext.getString(R.string.contact)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.github_link_title))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.github_link))
}).build()
)
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.privacy))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link))
}).build()
)
}.build(), carContext.getString(R.string.other)))
}.build()
}
}
class DeveloperOptionsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.developer_options))
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
addItem(
Row.Builder().apply {
setTitle("Car app API Level: ${carContext.carAppApiLevel}")
val hostPackage = carContext.hostInfo?.packageName
val hostVersion = hostPackage?.let {
try {
carContext.packageManager.getPackageInfoCompat(it).versionName
} catch (e: NameNotFoundException) {
null
}
}
addText("$hostPackage $hostVersion")
if (BuildConfig.FLAVOR_automotive == "automotive") {
addText(
"Sensor list: ${
(carContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager).getSensorList(
Sensor.TYPE_ALL
).map { it.type }.joinToString(",")
}"
)
}
}.build()
)
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.disable_developer_mode))
setOnClickListener {
prefs.developerModeEnabled = false
CarToast.makeText(
carContext,
carContext.getString(R.string.developer_mode_disabled),
CarToast.LENGTH_SHORT
).show()
screenManager.pop()
}
}.build())
}.build())
}.build()
}
}

View File

@@ -1,16 +1,25 @@
package net.vonforst.evmap.auto
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.*
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.getPackageInfoCompat
import java.util.*
import kotlin.math.roundToInt
@@ -33,6 +42,23 @@ fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
val CarContext.constraintManager
get() = getCarService(CarContext.CONSTRAINT_SERVICE) as ConstraintManager
fun CarContext.getContentLimit(id: Int) = if (carAppApiLevel >= 2) {
constraintManager.getContentLimit(id)
} else {
when (id) {
ConstraintManager.CONTENT_LIMIT_TYPE_GRID -> 6
ConstraintManager.CONTENT_LIMIT_TYPE_LIST -> 6
ConstraintManager.CONTENT_LIMIT_TYPE_PANE -> 4
ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST -> 6
ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST -> 3
else -> throw IllegalArgumentException("unknown limit ID")
}
}
val CarContext.isAppDrivenRefreshSupported
@androidx.car.app.annotations.ExperimentalCarApi
get() = if (carAppApiLevel >= 6) constraintManager.isAppDrivenRefreshEnabled else false
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
val emptyCarIcon: CarIcon by lazy {
@@ -171,7 +197,7 @@ fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): Li
}
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")
}
@@ -190,6 +216,40 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
return true
}
fun openUrl(carContext: CarContext, url: String) {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(
ContextCompat.getColor(
carContext,
R.color.colorPrimary
)
)
.build()
)
.build().intent
intent.data = Uri.parse(url)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
}
} catch (e: ActivityNotFoundException) {
CarToast.makeText(
carContext,
R.string.no_browser_app_found,
CarToast.LENGTH_LONG
).show()
}
}
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
/*
Dummy screen to get around template refresh limitations.

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap.auto
import android.content.pm.PackageManager
import android.location.Location
import android.os.Handler
import android.os.Looper
import androidx.car.app.CarContext
@@ -16,10 +17,13 @@ import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.CompassNeedle
import net.vonforst.evmap.ui.Gauge
import net.vonforst.evmap.utils.formatDecimal
import kotlin.math.min
import kotlin.math.roundToInt
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver {
@androidx.car.app.annotations.ExperimentalCarApi
class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
LocationAwareScreen, DefaultLifecycleObserver {
private val carInfo =
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
private val carSensors = carContext.patchedCarSensors
@@ -27,6 +31,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
private var energyLevel: EnergyLevel? = null
private var speed: Speed? = null
private var heading: Compass? = null
private var location: Location? = null
private var gauge = Gauge((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
private var compass =
CompassNeedle((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
@@ -70,7 +75,11 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
val energyLevel = energyLevel
val model = model
val speed = speed
val heading = heading
val location = location
val compassHeading = heading?.orientations?.value?.get(0)
val gpsHeading = if (location?.hasBearing() == true) location.bearing else null
val heading = compassHeading ?: gpsHeading
return GridTemplate.Builder().apply {
setTitle(
@@ -192,17 +201,30 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
if (heading == null) {
setLoading(true)
} else {
val heading = heading.orientations.value
if (heading != null) {
setText(
"${heading[0].roundToInt()}°"
val headingSource =
if (compassHeading != null) carContext.getString(R.string.compass) else carContext.getString(
R.string.gps
)
} else {
setText(carContext.getString(R.string.auto_no_data))
}
setText("${heading.roundToInt()}° ($headingSource)")
setImage(
compass.draw(heading?.get(0)).asCarIcon()
compass.draw(heading).asCarIcon()
)
}
}.build())
addItem(GridItem.Builder().apply {
setTitle(carContext.getString(R.string.coordinates))
if (location == null) {
setLoading(true)
} else {
val dms = location.formatDecimal(4)
setText(dms)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_location
)
).setTint(CarColor.DEFAULT).build()
)
}
}.build())
@@ -229,6 +251,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
override fun onResume(owner: LifecycleOwner) {
setupListeners()
session.mapScreen = this
}
private fun setupListeners() {
@@ -253,6 +276,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
override fun onPause(owner: LifecycleOwner) {
removeListeners()
session.mapScreen = null
}
private fun removeListeners() {
@@ -269,4 +293,8 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
it
) == PackageManager.PERMISSION_GRANTED
}
override fun updateLocation(location: Location) {
this.location = location
}
}

View File

@@ -7,6 +7,7 @@ import android.text.style.StyleSpan
import com.car2go.maps.google.adapter.AnyMapAdapter
import com.car2go.maps.util.SphericalUtil
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.tasks.Tasks.await
@@ -19,6 +20,7 @@ import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRe
import com.google.android.libraries.places.api.net.PlacesStatusCodes
import kotlinx.coroutines.tasks.await
import net.vonforst.evmap.R
import java.io.IOException
import java.util.concurrent.ExecutionException
import kotlin.math.sqrt
@@ -58,6 +60,13 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
if (cause is ApiException) {
if (cause.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
throw ApiUnavailableException()
} else if (cause.statusCode in listOf(
CommonStatusCodes.NETWORK_ERROR,
CommonStatusCodes.TIMEOUT, CommonStatusCodes.RECONNECTION_TIMED_OUT,
CommonStatusCodes.RECONNECTION_TIMED_OUT_DURING_UPDATE
)
) {
throw IOException(cause)
}
}
throw e

View File

@@ -5,6 +5,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.ProductType
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.adapter.Equatable
@@ -14,6 +15,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
.setListener(this)
.enablePendingPurchases()
.build()
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
MutableLiveData<Resource<List<DonationItem>>>().apply {
value = Resource.loading(null)
}
}
init {
billingClient.startConnection(object : BillingClientStateListener {
@@ -24,10 +31,15 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
loadProducts()
// consume pending purchases
val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
purchases.purchasesList?.forEach {
if (!it.isAcknowledged) {
consumePurchase(it.purchaseToken, false)
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(ProductType.INAPP)
.build()
) { _, purchasesList ->
purchasesList.forEach {
if (!it.isAcknowledged) {
consumePurchase(it.purchaseToken, false)
}
}
}
}
@@ -36,26 +48,26 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
}
private fun loadProducts() {
val params = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.INAPP)
.setSkusList(
listOf(
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
) +
if (BuildConfig.DEBUG) {
listOf(
"android.test.purchased", "android.test.canceled",
"android.test.item_unavailable"
)
} else {
emptyList()
}
val productIds = listOf(
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
) + if (BuildConfig.DEBUG) {
listOf(
"android.test.purchased", "android.test.canceled",
"android.test.item_unavailable"
)
} else {
emptyList()
}
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productIds.map {
QueryProductDetailsParams.Product.newBuilder().setProductType(ProductType.INAPP)
.setProductId(it).build()
})
.build()
billingClient.querySkuDetailsAsync(params) { result, details ->
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
billingClient.queryProductDetailsAsync(params) { result, details ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
products.postValue(Resource.success(details
.sortedBy { it.priceAmountMicros }
.sortedBy { it.oneTimePurchaseOfferDetails!!.priceAmountMicros }
.map { DonationItem(it) }
))
} else {
@@ -64,12 +76,6 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
}
}
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
MutableLiveData<Resource<List<DonationItem>>>().apply {
value = Resource.loading(null)
}
}
val purchaseSuccessful = SingleLiveEvent<Nothing>()
val purchaseFailed = SingleLiveEvent<Nothing>()
@@ -97,7 +103,13 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
fun startPurchase(it: DonationItem, activity: Activity) {
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(it.sku)
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(it.product)
.build()
)
)
.build()
val response = billingClient.launchBillingFlow(activity, flowParams)
if (response.responseCode != BillingClient.BillingResponseCode.OK) {
@@ -110,4 +122,4 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
}
}
data class DonationItem(val sku: SkuDetails) : Equatable
data class DonationItem(val product: ProductDetails) : Equatable

View File

@@ -28,7 +28,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@{item.sku.title}"
android:text="@{item.product.title}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/textView21"
@@ -41,7 +41,7 @@
android:id="@+id/textView21"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.sku.price}"
android:text="@{item.product.oneTimePurchaseOfferDetails.formattedPrice}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -36,4 +36,6 @@
<string name="sounds_cool">den er grei</string>
<string name="auto_chargers_ahead">Kun ladere i kjøreretningen</string>
<string name="loading">Laster inn …</string>
<string name="auto_multipage_goto">Side %d</string>
<string name="auto_multipage">(%d/%d)</string>
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auto_chargeprice_vehicle_ambiguous">Meerdere voertuigen geselecteerd in de app komen overeen met dit voertuig (%1$s %2$s).</string>
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
\n
\nGoogle houdt 15% in van elke donatie.</string>
<string name="auto_location_service">EVMap draait op Android Auto en gebruikt jouw locatie.</string>
<string name="auto_no_chargers_found">Geen laadpunten gevonden in de omgeving</string>
<string name="auto_no_favorites_found">Geen favorieten gevonden</string>
<string name="open_in_app">Open in de app</string>
<string name="opened_on_phone">Geopend op de telefoon</string>
<string name="auto_location_permission_needed">Om EVMap op Android Auto te gebruiken, moet je toegang geven tot je locatie.</string>
<string name="auto_vehicle_data_permission_needed">Voor deze functie heeft EVMap toegang nodig tot de gegevens van je voertuig.</string>
<string name="grant_on_phone">Geef toestemming op telefoon</string>
<string name="auto_chargers_closeby">Oplaadpunten in de buurt</string>
<string name="auto_favorites">Favorieten</string>
<string name="auto_chargers_near_location">Nabij %s</string>
<string name="auto_fault_report_date">⚠️ Foutrapport (%s)</string>
<string name="auto_no_refresh_possible">Verdere updates zijn niet mogelijk. Ga terug en herbegin.</string>
<string name="auto_prices">Prijzen</string>
<string name="auto_vehicle_data">Voertuiggegevens</string>
<string name="auto_charging_level">Laadniveau (SoC)</string>
<string name="auto_no_data">Niet beschikbaar</string>
<string name="auto_range">Reikwijdte</string>
<string name="auto_speed">Snelheid</string>
<string name="auto_heading">Richting</string>
<string name="auto_settings">Instellingen</string>
<string name="welcome_android_auto">Android Auto support</string>
<string name="welcome_android_auto_detail">Je kan EVMap ook gebruiken in Android Auto op ondersteunde voertuigen. Selecteer gewoon de EVMap app in het Android Auto menu.</string>
<string name="sounds_cool">klinkt cool</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap kon je voertuigtype niet bepalen.</string>
<string name="auto_chargers_ahead">Alleen laadpunten in rijrichting</string>
<string name="settings_android_auto_chargeprice_range">Laadbereik voor prijsvergelijking</string>
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap (Mapbox) voor de kaartgegevens.</string>
<string name="selecting_all">alle items geselecteerd</string>
<string name="selecting_none">alle items gedeselecteerd</string>
<string name="loading">Laden…</string>
<string name="auto_multipage_goto">Pagina %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="auto_chargeprice_vehicle_unknown">Geen enkel voertuig geselecteerd in de app komt overeen met dit voertuig (%1$s %2$s).</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="grant_on_phone">Toestaan</string>
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
</resources>

View File

@@ -40,6 +40,7 @@ const val EXTRA_CHARGER_ID = "chargerId"
const val EXTRA_LAT = "lat"
const val EXTRA_LON = "lon"
const val EXTRA_FAVORITES = "favorites"
const val EXTRA_DONATE = "donate"
class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
@@ -75,7 +76,7 @@ class MapsActivity : AppCompatActivity(),
val navView = findViewById<NavigationView>(R.id.nav_view)
navView.setupWithNavController(navController)
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
ViewCompat.setOnApplyWindowInsetsListener(navView) { _, insets ->
val header = navView.getHeaderView(0)
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
insets
@@ -188,6 +189,11 @@ class MapsActivity : AppCompatActivity(),
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent()
}
deepLink?.send()

View File

@@ -1,8 +1,11 @@
package net.vonforst.evmap
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
import android.text.*
import android.text.style.StyleSpan
@@ -72,7 +75,7 @@ fun max(a: Int?, b: Int?): Int? {
* otherwise the non-null value or null
*/
return if (a != null && b != null) {
max(a, b)
kotlin.math.max(a, b)
} else {
a ?: b
}
@@ -88,4 +91,11 @@ const val meterPerFt = 0.3048
fun shouldUseImperialUnits(): Boolean {
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
}
}
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
} else {
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
}

View File

@@ -1,7 +1,6 @@
package net.vonforst.evmap.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
@@ -166,7 +165,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
root.setOnCheckedChangeListener { _, checked: Boolean ->
if (checked) {
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
root.post {
@@ -205,7 +204,7 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
root.setOnCheckedChangeListener { _, checked: Boolean ->
if (checked && item != checkedItem) {
checkedItem = item
root.post {

View File

@@ -10,6 +10,8 @@ import net.vonforst.evmap.model.ChargeCardId
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.OpeningHoursDays
import net.vonforst.evmap.plus
import net.vonforst.evmap.utils.formatDMS
import net.vonforst.evmap.utils.formatDecimal
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle

View File

@@ -25,7 +25,7 @@ class FilterProfilesAdapter(
super.bind(holder, item)
val binding = holder.binding as ItemFilterProfileBinding
binding.handle.setOnTouchListener { v, event ->
binding.handle.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_DOWN) {
dragHelper.startDrag(holder)
}

View File

@@ -71,19 +71,19 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
connectors: Map<Long, Pair<Double, String>>,
chargepoints: List<Chargepoint>
): Map<Chargepoint, Set<Long>> {
var chargepoints = chargepoints
var cpts = chargepoints
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
var geTypes = chargepoints.map { it.type }.distinct().toSet()
var geTypes = cpts.map { it.type }.distinct().toSet()
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
Chargepoint.SCHUKO
)) {
// If charger has household plugs and other plugs, try removing the household plugs
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
cpts = cpts.filter { it.type != Chargepoint.SCHUKO }
}
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
@@ -93,14 +93,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
val powers = connsOfType.map { it.value.first }.distinct().sorted()
// find corresponding powers in GE data
val gePowers =
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
cpts.filter { equivalentPlugTypes(it.type).any { it == type } }
.mapNotNull { it.power }.distinct().sorted()
// if the distinct number of powers is the same, try to match.
if (powers.size == gePowers.size) {
gePowers.zip(powers).map { (gePower, power) ->
val chargepoint =
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
cpts.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
val ids = connsOfType.filter { it.value.first == power }.keys
if (chargepoint.count != ids.size) {
throw AvailabilityDetectorException("chargepoints do not match")
@@ -108,7 +108,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
chargepoint to ids
}
} else if (powers.size == 1 && gePowers.size == 2
&& chargepoints.sumOf { it.count } == connsOfType.size
&& cpts.sumOf { it.count } == connsOfType.size
) {
// special case: dual charger(s) with load balancing
// GoingElectric shows 2 different powers, NewMotion just one
@@ -116,7 +116,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
var i = 0
gePowers.map { gePower ->
val chargepoint =
chargepoints.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
cpts.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
val ids = allIds.subList(i, i + chargepoint.count).toSet()
i += chargepoint.count
chargepoint to ids

View File

@@ -51,7 +51,7 @@ interface ChargecloudApi {
)
companion object {
fun create(client: OkHttpClient, baseUrl: String? = null): ChargecloudApi {
fun create(client: OkHttpClient, baseUrl: String): ChargecloudApi {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())

View File

@@ -140,7 +140,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
connectorStatus.forEach { (connector, statusStr, evseId) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3
"type2" -> Chargepoint.TYPE_2_UNKNOWN
"type1" -> Chargepoint.TYPE_1

View File

@@ -163,7 +163,7 @@ interface ChargepriceApi {
"Spanien",
"Großbritannien",
"Irland",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
// additional countries found 2022/09/17, https://github.com/ev-map/EVMap/issues/234
"Finnland",
"Lettland",
"Litauen",
@@ -202,7 +202,7 @@ interface ChargepriceApi {
"ES",
"GB",
"IE",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
// additional countries found 2022/09/17, https://github.com/ev-map/EVMap/issues/234
"FI",
"LV",
"LT",

View File

@@ -71,10 +71,10 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
private val clazz: Class<*>
) : JsonAdapter<T>() {
class Factory() : JsonAdapter.Factory {
class Factory : JsonAdapter.Factory {
override fun create(
type: Type,
annotations: Set<Annotation>?,
annotations: Set<Annotation>,
moshi: Moshi
): JsonAdapter<Any>? {
val clazz = Types.getRawType(type)

View File

@@ -399,10 +399,10 @@ class GoingElectricApiWrapper(
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as GEReferenceData
val plugs = referenceData.plugs
val networks = referenceData.networks
val chargeCards = referenceData.chargecards
val refData = referenceData as GEReferenceData
val plugs = refData.plugs
val networks = refData.networks
val chargeCards = refData.chargecards
val plugMap = plugs.map { plug ->
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))

View File

@@ -120,7 +120,7 @@ class OpenChargeMapApiWrapper(
zoom: Float,
filters: FilterValues?,
): Resource<List<ChargepointListItem>> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
val minPower = filters?.getSliderValue("min_power")?.toDouble()
val minConnectors = filters?.getSliderValue("min_connectors")
@@ -133,7 +133,7 @@ class OpenChargeMapApiWrapper(
}
val connectors = formatMultipleChoice(connectorsVal)
val operatorsVal = filters?.getMultipleChoiceValue("operators")!!
val operatorsVal = filters?.getMultipleChoiceValue("operators")
if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) {
// no operators chosen
return Resource.success(emptyList())
@@ -160,7 +160,7 @@ class OpenChargeMapApiWrapper(
minPower,
connectorsVal,
minConnectors,
referenceData,
refData,
zoom
)
return Resource.success(result)
@@ -176,7 +176,7 @@ class OpenChargeMapApiWrapper(
zoom: Float,
filters: FilterValues?
): Resource<List<ChargepointListItem>> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
val minPower = filters?.getSliderValue("min_power")?.toDouble()
val minConnectors = filters?.getSliderValue("min_connectors")
@@ -214,7 +214,7 @@ class OpenChargeMapApiWrapper(
minPower,
connectorsVal,
minConnectors,
referenceData,
refData,
zoom
)
return Resource.success(result)
@@ -254,11 +254,11 @@ class OpenChargeMapApiWrapper(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
try {
val response = api.getChargepointDetail(id)
if (response.isSuccessful && response.body()?.size == 1) {
return Resource.success(response.body()!![0].convert(referenceData, true))
return Resource.success(response.body()!![0].convert(refData, true))
} else {
return Resource.error(response.message(), null)
}
@@ -284,10 +284,10 @@ class OpenChargeMapApiWrapper(
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
val operatorsMap = referenceData.operators.map { it.id.toString() to it.title }.toMap()
val plugMap = referenceData.connectionTypes.map { it.id.toString() to it.title }.toMap()
val operatorsMap = refData.operators.map { it.id.toString() to it.title }.toMap()
val plugMap = refData.connectionTypes.map { it.id.toString() to it.title }.toMap()
return listOf(
// supported by OCM API

View File

@@ -44,8 +44,7 @@ class FavoritesFragment : Fragment() {
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
viewModelFactory {
FavoritesViewModel(
requireActivity().application,
getString(R.string.goingelectric_key)
requireActivity().application
)
}
})

View File

@@ -98,6 +98,12 @@ class FilterFragment : Fragment(), MenuProvider {
saveProfile()
true
}
R.id.menu_reset -> {
lifecycleScope.launch {
vm.resetValues()
}
true
}
else -> false
}
}
@@ -114,7 +120,7 @@ class FilterFragment : Fragment(), MenuProvider {
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
.setPositiveButton(R.string.ok) { _, _ ->
if (input.text.isBlank()) {
saveProfile(true)
} else {
@@ -124,7 +130,7 @@ class FilterFragment : Fragment(), MenuProvider {
}
}
}
.setNegativeButton(R.string.cancel) { di, button ->
.setNegativeButton(R.string.cancel) { _, _ ->
}
}

View File

@@ -188,12 +188,12 @@ class FilterProfilesFragment : Fragment() {
dialog.setTitle(R.string.rename)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
vm.update(fp.copy(name = input.text.toString()))
}
}
.setNegativeButton(R.string.cancel) { di, button ->
.setNegativeButton(R.string.cancel) { _, _ ->
}
}

View File

@@ -91,6 +91,7 @@ import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.contains
import kotlin.collections.set
import kotlin.math.min
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider {
@@ -197,7 +198,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { v, insets ->
) { _, insets ->
ViewCompat.onApplyWindowInsets(binding.root, insets)
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
@@ -465,7 +466,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
)
}
binding.search.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
binding.search.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
binding.search.keyListener = searchKeyListener
binding.search.text = binding.search.text // workaround to fix copy/paste
@@ -553,7 +554,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (bottomSheetBehavior.state == STATE_HIDDEN) {
map?.setPadding(0, mapTopPadding, 0, 0)
} else {
val height = binding.root.height - bottomSheet.top
map?.setPadding(
0,
mapTopPadding,
0,
min(bottomSheetBehavior.peekHeight, height)
)
}
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
@@ -561,9 +572,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
updateBackPressedCallback()
if (vm.layersMenuOpen.value!! && newState !in listOf(
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING,
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN,
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
STATE_SETTLING,
STATE_HIDDEN,
STATE_COLLAPSED
)
) {
closeLayersMenu()
@@ -1189,6 +1200,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
)
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
MenuCompat.setGroupDividerEnabled(popup.menu, true)
popup.setForceShowIcon(true)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_edit_filters -> {

View File

@@ -71,7 +71,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
items = data.entries.toList()
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
.sortedBy { it.value.lowercase(Locale.getDefault()) }
.sortedBy {
when {
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
@@ -117,7 +117,7 @@ private fun search(
): List<MultiSelectItem> {
return items.filter { item ->
// search for string within name
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
text.lowercase(Locale.getDefault()) in item.name.lowercase(Locale.getDefault())
}
}

View File

@@ -19,8 +19,6 @@ import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.math.abs
import kotlin.math.floor
sealed class ChargepointListItem
@@ -113,7 +111,7 @@ data class ChargeLocation(
// check if there is more than one plug for any connector type
val chargepointsPerConnector =
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumOf { it.count } }
return chargepointsPerConnector.any { it > 1 }
}
@@ -129,13 +127,13 @@ data class ChargeLocation(
return variants.map { variant ->
val count = chargepoints
.filter { it.type == variant.type && it.power == variant.power }
.sumBy { it.count }
.sumOf { it.count }
Chargepoint(variant.type, variant.power, count)
}
}
val totalChargepoints: Int
get() = chargepoints.sumBy { it.count }
get() = chargepoints.sumOf { it.count }
fun formatChargepoints(sp: StringProvider): String {
return chargepointsMerged.map {
@@ -343,28 +341,7 @@ data class ChargeLocationCluster(
) : ChargepointListItem()
@Parcelize
data class Coordinate(val lat: Double, val lng: Double) : Parcelable {
fun formatDMS(): String {
return "${dms(lat, false)}, ${dms(lng, true)}"
}
private fun dms(value: Double, lon: Boolean): String {
val hemisphere = if (lon) {
if (value >= 0) "E" else "W"
} else {
if (value >= 0) "N" else "S"
}
val d = abs(value)
val degrees = floor(d).toInt()
val minutes = floor((d - degrees) * 60).toInt()
val seconds = ((d - degrees) * 60 - minutes) * 60
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
}
fun formatDecimal(): String {
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
}
}
data class Coordinate(val lat: Double, val lng: Double) : Parcelable
@Parcelize
data class Address(

View File

@@ -1,29 +1,49 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.liveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.*
import net.vonforst.evmap.model.*
@Dao
abstract class FilterValueDao {
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract suspend fun getBooleanFilterValues(
protected abstract suspend fun getBooleanFilterValuesAsync(
profile: Long,
dataSource: String
): List<BooleanFilterValue>
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract suspend fun getMultipleChoiceFilterValues(
protected abstract suspend fun getMultipleChoiceFilterValuesAsync(
profile: Long,
dataSource: String
): List<MultipleChoiceFilterValue>
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract suspend fun getSliderFilterValues(
protected abstract suspend fun getSliderFilterValuesAsync(
profile: Long,
dataSource: String
): List<SliderFilterValue>
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getBooleanFilterValues(
profile: Long,
dataSource: String
): LiveData<List<BooleanFilterValue>>
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getMultipleChoiceFilterValues(
profile: Long,
dataSource: String
): LiveData<List<MultipleChoiceFilterValue>>
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getSliderFilterValues(
profile: Long,
dataSource: String
): LiveData<List<SliderFilterValue>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
@@ -58,15 +78,32 @@ abstract class FilterValueDao {
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
emptyList()
} else {
getBooleanFilterValues(filterStatus, dataSource) +
getMultipleChoiceFilterValues(filterStatus, dataSource) +
getSliderFilterValues(filterStatus, dataSource)
getBooleanFilterValuesAsync(filterStatus, dataSource) +
getMultipleChoiceFilterValuesAsync(filterStatus, dataSource) +
getSliderFilterValuesAsync(filterStatus, dataSource)
}
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
emit(null)
emit(getFilterValuesAsync(filterStatus, dataSource))
}
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>?> =
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
MutableLiveData(emptyList())
} else {
MediatorLiveData<List<FilterValue>?>().apply {
value = null
val sources = listOf(
getBooleanFilterValues(filterStatus, dataSource),
getMultipleChoiceFilterValues(filterStatus, dataSource),
getSliderFilterValues(filterStatus, dataSource)
)
for (source in sources) {
addSource(source) {
val values = sources.map { it.value }
if (values.all { it != null }) {
value = values.filterNotNull().flatten()
}
}
}
}
}
@Transaction
open suspend fun insert(vararg values: FilterValue) {

View File

@@ -255,4 +255,10 @@ class PreferenceDataSource(val context: Context) {
val predictionEnabled: Boolean
get() = sp.getBoolean("prediction_enabled", true)
var developerModeEnabled: Boolean
get() = sp.getBoolean("dev_mode_enabled", false)
set(value) {
sp.edit().putBoolean("dev_mode_enabled", value).apply()
}
}

View File

@@ -211,12 +211,12 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
val bubbleBounds = bubbleBounds ?: return
val graphBounds = graphBounds ?: return
val data = data.toList()
val d = data.toList()
if (data.size <= selectedBar) return
if (d.size <= selectedBar) return
canvas.apply {
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
val (t, v) = data[selectedBar]
val (t, v) = d[selectedBar]
val tformat = context.getString(
R.string.prediction_time_colon,
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)

View File

@@ -121,6 +121,7 @@ private fun activeTint(
}
@BindingAdapter("data")
@Suppress("UNCHECKED_CAST")
fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
@@ -128,6 +129,7 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
}
@BindingAdapter("data")
@Suppress("UNCHECKED_CAST")
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
@@ -325,10 +327,10 @@ fun distance(meters: Number?): String? {
}
}
@InverseBindingAdapter(attribute = "app:values")
@InverseBindingAdapter(attribute = "values")
fun getRangeSliderValue(slider: RangeSlider) = slider.values
@BindingAdapter("app:valuesAttrChanged")
@BindingAdapter("valuesAttrChanged")
fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) {
slider.addOnChangeListener { _, _, _ ->
attrChange.onChange()
@@ -348,7 +350,7 @@ fun colorEnabled(ctx: Context, enabled: Boolean): Int {
return color
}
@BindingAdapter("app:tint")
@BindingAdapter("tint")
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}

View File

@@ -89,12 +89,12 @@ class RangeSliderPreference(context: Context, attrs: AttributeSet) : Preference(
slider.valueTo = valueTo
stepSize?.let { slider.stepSize = it }
slider.addOnChangeListener { slider, value, fromUser ->
slider.addOnChangeListener { slider, _, fromUser ->
if (fromUser && (updatesContinuously || !dragging)) {
syncValueInternal(slider)
}
}
slider.setOnTouchListener { v, event ->
slider.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> dragging = true
MotionEvent.ACTION_UP -> dragging = false

View File

@@ -8,6 +8,8 @@ import android.location.Location
import androidx.core.content.ContextCompat
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.model.Coordinate
import java.util.*
import kotlin.math.*
/**
@@ -48,7 +50,7 @@ fun distanceBetween(
fun bearingBetween(startLat: Double, startLng: Double, endLat: Double, endLng: Double): Double {
val dLon = Math.toRadians(-endLng) - Math.toRadians(-startLng)
val dLon = Math.toRadians(endLng) - Math.toRadians(startLng)
val originLat = Math.toRadians(startLat)
val destinationLat = Math.toRadians(endLat)
@@ -111,4 +113,33 @@ fun Context.checkAnyLocationPermission() = ContextCompat.checkSelfPermission(
fun Context.checkFineLocationPermission() = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) == PackageManager.PERMISSION_GRANTED
fun Coordinate.formatDMS(): String {
return "${dms(lat, false)}, ${dms(lng, true)}"
}
fun Location.formatDMS(): String {
return "${dms(latitude, false)}, ${dms(longitude, true)}"
}
private fun dms(value: Double, lon: Boolean): String {
val hemisphere = if (lon) {
if (value >= 0) "E" else "W"
} else {
if (value >= 0) "N" else "S"
}
val d = abs(value)
val degrees = floor(d).toInt()
val minutes = floor((d - degrees) * 60).toInt()
val seconds = ((d - degrees) * 60 - minutes) * 60
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
}
fun Coordinate.formatDecimal(accuracy: Int = 6): String {
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng)
}
fun Location.formatDecimal(accuracy: Int = 6): String {
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude)
}

View File

@@ -16,7 +16,7 @@ import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.utils.distanceBetween
class FavoritesViewModel(application: Application, geApiKey: String) :
class FavoritesViewModel(application: Application) :
AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
@@ -69,7 +69,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
FavoritesListItem(
favorite,
totalAvailable(charger.id),
charger.chargepoints.sumBy { it.count },
charger.chargepoints.sumOf { it.count },
location.value.let { loc ->
if (loc == null) null else {
distanceBetween(

View File

@@ -92,4 +92,8 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
prefs.filterStatus = FILTERS_DISABLED
}
}
suspend fun resetValues() {
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
}
}

View File

@@ -7,6 +7,7 @@ import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.squareup.moshi.JsonDataException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -265,6 +266,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
} catch (e: AvailabilityDetectorException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: JsonDataException) {
// malformed JSON response from fronyx API
emit(Resource.error(e.message, null))
e.printStackTrace()
}
}
} ?: liveData { emit(Resource.success(null)) }

View File

@@ -13,7 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean
@Suppress("UNCHECKED_CAST")
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
override fun <T : ViewModel> create(modelClass: Class<T>): T = f() as T
}
@Suppress("UNCHECKED_CAST")
@@ -106,7 +106,8 @@ fun <T> throttleLatest(
}
}
public suspend fun <T> LiveData<T>.await(): T {
@ExperimentalCoroutinesApi
suspend fun <T> LiveData<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T?) {
@@ -124,7 +125,8 @@ public suspend fun <T> LiveData<T>.await(): T {
}
}
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
@ExperimentalCoroutinesApi
suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<Resource<T>> {
override fun onChanged(value: Resource<T>) {

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M10.83,8H21V6H8.83L10.83,8zM15.83,13H18v-2h-4.17L15.83,13zM14,16.83V18h-4v-2h3.17l-3,-3H6v-2h2.17l-3,-3H3V6h0.17L1.39,4.22l1.41,-1.41l18.38,18.38l-1.41,1.41L14,16.83z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z" />
</vector>

View File

@@ -230,6 +230,7 @@
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.Material3.FloatingActionButton.Small.Surface"
android:id="@+id/fab_layers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -239,9 +240,7 @@
android:layout_marginEnd="20dp"
android:layout_marginTop="@dimen/layers_fab_top_padding"
app:tint="?android:colorControlNormal"
app:backgroundTint="?android:colorBackground"
app:borderWidth="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_layers"
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
android:theme="@style/NoElevationOverlay" />

View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_reset"
android:title="@string/menu_reset"
android:icon="@drawable/ic_filter_no"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_save_profile"
android:title="@string/menu_save_profile"

View File

@@ -8,9 +8,11 @@
<item
android:id="@+id/menu_edit_filters"
android:title="@string/menu_edit_filters"
android:menuCategory="secondary" />
android:menuCategory="secondary"
android:icon="@drawable/ic_edit" />
<item
android:id="@+id/menu_manage_filter_profiles"
android:title="@string/menu_manage_filter_profiles"
android:menuCategory="secondary" />
android:menuCategory="secondary"
android:icon="@drawable/ic_manage_filter_profiles" />
</menu>

View File

@@ -42,7 +42,7 @@
<string name="settings_ui">Oberfläche</string>
<string name="settings_map">Karte</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="copyright_summary">©20202023 Johan von Forstner</string>
<string name="other">Sonstiges</string>
<string name="privacy">Datenschutzerklärung</string>
<string name="fav_add">Als Favorit speichern</string>
@@ -143,6 +143,7 @@
<string name="category_caravan_site">Wohnmobilstellplatz</string>
<string name="menu_apply">Filter anwenden</string>
<string name="menu_save_profile">Als Profil speichern</string>
<string name="menu_reset">Filter zurücksetzen</string>
<string name="no_filters">Keine Filter</string>
<string name="filter_custom">Verändertes Filterprofil</string>
<string name="filter_favorites">Favoriten</string>
@@ -288,4 +289,10 @@
<string name="pref_applink_associate_summary">von goingelectric.de und openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">Meine Tarife</string>
<string name="chargeprice_header_other_tariffs">Andere Tarife</string>
<string name="developer_mode_enabled">Entwicklermodus aktiviert</string>
<string name="developer_options">Entwicklereinstellungen</string>
<string name="disable_developer_mode">Entwicklermodus deaktivieren</string>
<string name="developer_mode_disabled">Entwicklermodus deaktiviert</string>
<string name="gps">GPS</string>
<string name="compass">Kompass</string>
</resources>

View File

@@ -154,7 +154,7 @@
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</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="copyright_summary">©20202023 Johan von Forstner</string>
<string name="other">Autre</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>

View File

@@ -93,7 +93,7 @@
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
<string name="other">Andre</string>
<string name="cost_detail"><b>Lading:</b> %1$s · <b>Parkering:</b> %2$s</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="copyright_summary">©20202023 Johan von Forstner</string>
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter ruteveiledning på Google Maps</string>
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
<string name="filter_min_power">Min. effekt</string>
@@ -284,4 +284,16 @@
<string name="pref_prediction_enabled">Vis bruksprognoser</string>
<string name="pref_prediction_enabled_summary">for støttede ladere
\n(foreløpig kun for likestrøm i Tyskland)</string>
<string name="chargeprice_price_not_available">Pris ikke tilgjengelig</string>
<string name="developer_mode_disabled">Utviklermodus avslått</string>
<string name="gps">GPS</string>
<string name="compass">Kompass</string>
<string name="pref_applink_associate">Åpne støttede lenker</string>
<string name="pref_applink_associate_summary">fra goingelectric.de og openchargemap.org</string>
<string name="chargeprice_header_other_tariffs">Andre ladeabonnementer</string>
<string name="disable_developer_mode">Skru av utviklermodus</string>
<string name="chargeprice_header_my_tariffs">Mine ladeabonnementer</string>
<string name="developer_options">Utvikleralternativer</string>
<string name="data_source_switched_to">Datakilde byttet til %s</string>
<string name="developer_mode_enabled">Utviklermodus påslått</string>
</resources>

View File

@@ -0,0 +1,299 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="data_source_goingelectric_desc">Ideaal in Duitstalige landen. Beschrijvingen in het Duits. Onderhouden door de gebruikersgemeenschap.</string>
<string name="crash_report_text">EVMap is afgebroken. Stuur een crash rapport naar de ontwikkelaar.</string>
<string name="pref_search_provider_info">Gegevens opzoeken is duur, vooral via Google Maps. Overweeg aub om een donatie te doen via “Over” -&gt; “Doneer”.</string>
<string name="data_source_openchargemap_desc">Werelddekkend, met variabele kwaliteit. Beschrijving in Engels of lokale taal. Onderhouden door de gebruikers. Ook open overheidswege eens in sommige landen (bv. Noord-Amerika, UK, Frankrijk, Noorwegen).</string>
<string name="pref_darkmode_always_off">altijd uit</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="chargeprice_select_car_first">Kiest eerst je voertuig model in de instellingen</string>
<string name="chargeprice_no_compatible_connectors">Geen compatibele connectoren aan dit laadstation</string>
<string name="license">Licentie</string>
<string name="data_sources_description">Kies een gegevensbron voor laadstations. Dit kan later worden aangepast in de app-instellingen.</string>
<string name="category_church">Kerk</string>
<string name="welcome_2">Elk laadpunt heeft een kleur die het maximale laadvermogen weergeeft</string>
<string name="donation_dialog_detail">EVMap is open source en gratis. Via GitHub kan iedereen bijdragen aan de app. Om de vaste kosten te helpen dragen, kan je overwegen een donatie te schenken aan de ontwikkelaar.</string>
<string name="charging_barrierfree">Te gebruiken zonder registratie</string>
<string name="verified_desc">Laadpunt is minstens 1x bevestigd als werkend door een lid van de %s gemeenschap</string>
<string name="chargeprice_no_tariffs_found">Geen tarieven voor dit laadpunt op Chargeprice.app</string>
<string name="category_hospital">Ziekenhuis</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connectoren</string>
<string name="no_browser_app_found">Installeer eerst een web browser</string>
<string name="address">Adres</string>
<string name="operator">Operator</string>
<string name="network">Netwerk</string>
<string name="open_247"><b>24/7 open</b></string>
<string name="closed"><b>Gesloten</b></string>
<string name="open_closesat"><b>Open</b> · Sluit om %s</string>
<string name="closed_opensat"><b>Gesloten</b> · Opent om %s</string>
<string name="closed_unfmt">Gesloten</string>
<string name="holiday">Feestdag</string>
<string name="cost">Kostprijs</string>
<string name="cost_detail"><b>Laden:</b> %1$s · <b>Parkeren:</b> %2$s</string>
<string name="cost_detail_charging"><b>%s laden</b></string>
<string name="cost_detail_parking"><b>%s parkeren</b></string>
<string name="charging_free">Gratis</string>
<string name="parking_free">Gratis</string>
<string name="amenities">Voorzieningen</string>
<string name="general_info">Algemene informatie</string>
<string name="realtime_data_unavailable">Real-time status niet beschikbaar</string>
<string name="realtime_data_loading">Real-time status opvragen…</string>
<string name="source">Bron: %s</string>
<string name="search">Zoek</string>
<string name="menu_map">Kaart</string>
<string name="menu_favs">Favorieten</string>
<string name="menu_filter">Filter</string>
<string name="not_implemented">nog niet geïmplementeerd</string>
<string name="about">Over</string>
<string name="version">Versie</string>
<string name="github_link_title">Broncode</string>
<string name="oss_licenses">Licenties</string>
<string name="settings">Instellingen</string>
<string name="settings_ui">Interface</string>
<string name="settings_map">Kaart</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©20202023 Johan von Forstner</string>
<string name="other">Andere</string>
<string name="privacy">Privacy</string>
<string name="pref_navigate_use_maps_off">Navigatieknop opent de kaart app met de locatie van het laadstation</string>
<string name="coordinates">Coördinaten</string>
<string name="share">Deel</string>
<string name="filter_free">Allen gratis laadpunten</string>
<string name="filter_min_power">Minimaal vermogen</string>
<string name="filter_free_parking">Alleen laadpunten met gratis parking</string>
<string name="filter_min_connectors">Minimaal aantal connecteren</string>
<string name="filter_connectors">Connectoren</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_cee_rot">CEE Red</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="all">allemaal</string>
<string name="none">geen</string>
<string name="show_more">meer…</string>
<string name="favorites_empty_state">Opgeslagen laadpunten verschijnen hier</string>
<string name="donate">Doneer</string>
<string name="donation_successful">Dank u ❤️</string>
<string name="donation_failed">Er ging iets mis 😕</string>
<string name="map_type_normal">Default</string>
<string name="map_type_satellite">Satelliet</string>
<string name="map_type_terrain">Terrein</string>
<string name="map_traffic">Verkeer</string>
<string name="faq">Veelgestelde vragen</string>
<string name="menu_filters_active">Actieve filters</string>
<string name="filters_activated">Filters geactiveerd</string>
<string name="filters_deactivated">Filters gedeactiveerd</string>
<string name="menu_edit_filters">Pas filters aan</string>
<string name="menu_manage_filter_profiles">Beheer filterprofielen</string>
<string name="go_to_chargeprice">Vergelijk prijzen</string>
<string name="fault_report">Foutenrapport</string>
<string name="fault_report_date">Foutenrapport (laatste update: %s)</string>
<string name="filter_networks">Netwerken</string>
<string name="filter_operators">Operatoren</string>
<string name="filter_chargecards">Betaalmethoden</string>
<string name="all_selected">Alle geselecteerd</string>
<string name="number_selected">%d geselecteerd</string>
<string name="edit">aanpassen</string>
<string name="cancel">Afbreken</string>
<string name="ok">OK</string>
<string name="pref_language">App-taal</string>
<string name="pref_darkmode">Donkere modus</string>
<string name="connection_error">Laadstations konden niet worden geladen</string>
<string name="location_error">Kon locatie niet bepalen. Controleer de instellingen</string>
<string name="retry">Opnieuw</string>
<string name="filter_open_247">24/7 beschikbaar</string>
<string name="filter_barrierfree">Te gebruiken zonder registratie</string>
<string name="filter_exclude_faults">Sluit laadstations uit met gerapporteerde fouten</string>
<string name="categories">Categorieën</string>
<string name="category_car_dealership">Autoverdeler</string>
<string name="category_service_on_motorway">Herstelzone (op snelweg)</string>
<string name="category_service_off_motorway">Herstelzone (niet langs de snelweg)</string>
<string name="category_railway_station">Treinstation</string>
<string name="category_shopping_mall">Winkelcentrum</string>
<string name="category_holiday_home">Vakantiewoning</string>
<string name="category_airport">Luchthaven</string>
<string name="category_amusement_park">Attractiepark</string>
<string name="category_hotel">Hotel</string>
<string name="category_cinema">Bioscoop</string>
<string name="category_museum">Museum</string>
<string name="category_parking_multi">Parkeergarage</string>
<string name="category_parking">Parking</string>
<string name="category_private_charger">Privé-laadpunt</string>
<string name="category_rest_area">Rustplaats</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_swimming_pool">Zwembad</string>
<string name="category_supermarket">Supermarkt</string>
<string name="category_petrol_station">Benzinestation</string>
<string name="category_parking_underground">Ondergrondse parking</string>
<string name="category_zoo">Zoo</string>
<string name="category_caravan_site">Staanplaats voor caravans</string>
<string name="menu_apply">Pas filters toe</string>
<string name="menu_save_profile">Bewaar als profiel</string>
<string name="menu_reset">Reset filters</string>
<string name="no_filters">Geen filters</string>
<string name="filter_custom">Aangepaste filter</string>
<string name="filter_favorites">Favorieten</string>
<string name="reorder">herorden</string>
<string name="delete">Verwijder</string>
<string name="save_as_profile">Bewaar als profiel</string>
<string name="save_profile_enter_name">Geef de naam van het filterprofiel:</string>
<string name="filterprofiles_empty_state">Je hebt geen bewaarde filterprofielen</string>
<string name="welcome_to_evmap">Welkom bij EVMap</string>
<string name="welcome_1">Zoek EV laadpunten in je omgeving</string>
<string name="welcome_2_title">Een kwestie van power</string>
<string name="welcome_2_detail">Dit vind je ook in “Over” → “Veelgestelde vragen”</string>
<string name="donation_dialog_title">Bedankt om EVMap te gebruiken</string>
<string name="chargeprice_donation_dialog_title">Jij bent een echte koopjeszoeker!</string>
<string name="chargeprice_donation_dialog_detail">Blijkbaar maak je dankbaar gebruik van de prijsvergelijkingen. Met een donatie kan je de kosten voor deze data helpen dragen.</string>
<string name="deleted_filterprofile">“%s” verwijderd</string>
<string name="undo">Ongedaan maken</string>
<string name="rename">Hernoem</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d compatibele betaalmethode</item>
<item quantity="other">%d compatibele betaalmethodes</item>
</plurals>
<string name="navigate">Navigeer naar hier</string>
<string name="verified">geverifieerd</string>
<string name="charge_price_format">%1$.2f %2$s</string>
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
<string name="chargeprice_select_connector">Kies connector</string>
<string name="chargeprice_provider_customer_tariff">Alleen voor eigen klanten</string>
<string name="edit_on_goingelectric_info">Log aub in op GoingElectric.de als deze pagina leeg is</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">kostprijs  sessie</string>
<string name="chargeprice_per_kwh">per kWh</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_blocking_fee">Kostprijs blokkeren &gt;%s</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="settings_chargeprice">Prijsvergelijking</string>
<string name="pref_my_vehicle">Mijn voertuigen</string>
<string name="pref_chargeprice_no_base_fee">Sluit plannen uit met maandelijkse kost</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Neem plannen op die enkel voor klanten gelden</string>
<string name="chargeprice_battery_range_from">Laden vanaf</string>
<string name="chargeprice_battery_range_to">tot</string>
<string name="chargeprice_vehicle">Voertuig</string>
<string name="chargeprice_price_not_available">Prijs niet beschikbaar</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Sommige energieleveranciers bieden speciale plannen voor hun klanten</string>
<string name="close">Sluiten</string>
<string name="chargeprice_title">Prijzen</string>
<string name="chargeprice_connection_error">Kon prijzen niet laden</string>
<string name="pref_chargeprice_currency">Valuta</string>
<string name="pref_my_tariffs">Mijn laadplannen</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one">(wordt aangeduid in de prijsvergelijking)</item>
<item quantity="other">(worden aangeduid in de prijsvergelijking)</item>
</plurals>
<string name="chargeprice_all_tariffs_selected">alle plannen geselecteerd</string>
<string name="settings_charger_data">Laadstations</string>
<string name="pref_data_source">Databron</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plan geselecteerd</item>
<item quantity="other">%d plannen geselecteerd</item>
</plurals>
<string name="unknown_operator">Onbekende operator</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap">OpenChargeMap</string>
<string name="chargeprice_base_fee">Abonnementskost: %1$.2f %2$s/maand</string>
<string name="chargeprice_min_spend">Minimale kost: %1$.2f %2$s/maand</string>
<string name="chargeprice_battery_range">Laden van %1$.0f%% tot %2$.0f%%</string>
<string name="chargeprice_stats">(%1$.0f kWh, ca. %2$s, ⌀ %3$.0f kW)</string>
<string name="next">volgende</string>
<string name="get_started">Starten</string>
<string name="got_it">Begrepen</string>
<string name="lets_go">Laten we beginnen</string>
<string name="crash_report_comment_prompt">Je kan hieronder commentaar geven:</string>
<string name="powered_by_mapbox">powered by Mapbox</string>
<string name="pref_search_provider">Zoekprovider</string>
<string name="donate_desc">Ondersteun de EVMap ontwikkeling via een eenmalige donatie</string>
<string name="github_sponsors_desc">Ondersteun EVMap op GitHub Spinsors</string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="unnamed_filter_profile">Naamloos filterprofiel</string>
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</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="required">verplicht</string>
<string name="pref_search_delete_recent">Verwijder recente zoekresultaten</string>
<string name="deleted_recent_search_results">Recente zoekresultaten zijn verwijderd</string>
<string name="settings_data_sources">Gegevensbronnen</string>
<string name="help">Help</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Ongebalanceerd laden toelaten</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Eenfasig AC laden toelaten met meer dan 4.5 kW</string>
<string name="pref_map_rotate_gestures_enabled">Kaartrotatie</string>
<string name="pref_map_rotate_gestures_on">Gebruik twee vingers om de kaart te draaien</string>
<string name="pref_map_rotate_gestures_off">Rotatie afzetten (noorden naar boven)</string>
<string name="refresh_live_data">vernieuw de real-time status</string>
<string name="autocomplete_connection_error">Suggesties konden niet worden geladen</string>
<string name="pref_language_device_default">Standaardtaal van toestel</string>
<string name="pref_darkmode_device_default">Standaardinstelling van toestel</string>
<string name="pref_darkmode_always_on">altijd aan</string>
<string name="pref_chargeprice_currency_chf">Zwitserse Frank (CHF)</string>
<string name="pref_chargeprice_currency_czk">Tsjechische koruna (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Deense kroon (DKK)</string>
<string name="pref_chargeprice_currency_gbp">Britse Pond (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Hongaarse Forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">IJslandse Kroon (ISK)</string>
<string name="pref_chargeprice_currency_nok">Noorse Kroon (NOK)</string>
<string name="pref_chargeprice_currency_pln">Poolse Złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Zweedse Kroon (SEK)</string>
<string name="pref_chargeprice_currency_usd">Amerikaanse Dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="edit_filter_profile">“%s” editeren</string>
<string name="compass">Kompas</string>
<string name="gps">GPS</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Bijdragers</string>
<string name="about_contributors_text">Dank aan iedereen die heeft bijgedragen aan de code en vertaling van EVMap:</string>
<string name="utilization_prediction">Voorspeld verbruik</string>
<string name="prediction_help">De voorspelling is gebaseerd op factoren zoals weekdag, tijdstip en gebruik in het verleden, zodat je zwaar bezette laders kan vermijden. Geen garantie, uiteraard.</string>
<string name="prediction_time_colon">%s:</string>
<string name="pref_prediction_enabled">Toon voorspeld gebruik</string>
<string name="pref_prediction_enabled_summary">voor ondersteunde laders
\n(momenteel enkel DC in Duitsland)</string>
<string name="prediction_only">(enkel %s)</string>
<string name="prediction_dc_plugs_only">DC aansluitingen</string>
<string name="data_source_switched_to">Gegevensbron gewijzigd naar %s</string>
<string name="pref_applink_associate">Open ondersteunde links</string>
<string name="pref_applink_associate_summary">van going electric.de en openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">Mijn plannen</string>
<string name="chargeprice_header_other_tariffs">Andere plannen</string>
<string name="developer_mode_enabled">Ontwillekaarsmodus geactiveerd</string>
<string name="developer_options">Ontwikkelaarsopties</string>
<string name="disable_developer_mode">Ontwikkelaarsmodus uitzetten</string>
<string name="developer_mode_disabled">Ontwikkelaarsmodus uitgezet</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d beschkbaar</item>
<item quantity="other">%1$d/%2$d beschikbaar</item>
</plurals>
<string name="app_name">EVMap</string>
<string name="no_maps_app_found">Installeer eerst een navigatie-app</string>
<string name="hours">Openingsuren</string>
<string name="charging_paid">Betalend</string>
<string name="parking_paid">Betalend</string>
<string name="realtime_data_source">Real-time status bron (beta): %s</string>
<string name="pref_navigate_use_maps">Onmiddellijke navigatie</string>
<string name="fav_remove">Verwijder uit favorieten</string>
<string name="pref_navigate_use_maps_on">Navigatieknop start routebegeleiding met Google Maps</string>
<string name="fav_add">Bewaar als favoriet</string>
<string name="goingelectric_forum">Forumthread op GoingElectric.de</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_supercharger">Tesla Supercharger</string>
<string name="plug_cee_blau">CEE Blue</string>
<string name="plug_ccs">CCS</string>
<string name="plug_type_1">Type 1</string>
<string name="menu_report_new_charger">Nieuw laadpunt</string>
<string name="show_less">minder…</string>
<string name="map_type">Kaarttype</string>
<string name="map_details">Kaartdetails</string>
<string name="edit_at_datasource">aanpassen op %s</string>
<string name="charge_cards">Betaalmethoden</string>
<string name="pref_map_provider">Kaartaanbieder</string>
<string name="twitter">Twitter</string>
<string name="contact">Contact</string>
<string name="and_n_others">en %d andere</string>
<string name="category_camping">Kampeerplaats</string>
<string name="category_public_authorities">Publieke instanties</string>
</resources>

View File

@@ -6,6 +6,7 @@
<item>@string/pref_language_de</item>
<item>@string/pref_language_fr</item>
<item>@string/pref_language_nb_rNO</item>
<item>@string/pref_language_nl</item>
</string-array>
<string-array name="pref_language_values" translatable="false">
<item>default</item>
@@ -13,6 +14,7 @@
<item>de</item>
<item>fr</item>
<item>nb-NO</item>
<item>nl</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>@string/pref_darkmode_device_default</item>

View File

@@ -2,7 +2,7 @@
<resources>
<string name="shared_element_picture">picture</string>
<string name="shared_element_chargeprice">chargeprice</string>
<string name="github_link">https://github.com/johan12345/EVMap</string>
<string name="github_link">https://github.com/ev-map/EVMap</string>
<string name="twitter_handle">\@ev_map</string>
<string name="twitter_url">https://twitter.com/ev_map</string>
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
@@ -13,6 +13,7 @@
<string name="pref_language_de">Deutsch</string>
<string name="pref_language_fr">Français</string>
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
<string name="pref_language_nl">Nederlands</string>
<string name="about_contributors_list">
Danilo Bargen\n
Altonss\n
@@ -23,4 +24,5 @@
nautilusx
</string>
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
</resources>

View File

@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
@@ -41,7 +42,7 @@
<string name="settings_ui">Interface</string>
<string name="settings_map">Map</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="copyright_summary">©20202023 Johan von Forstner</string>
<string name="other">Other</string>
<string name="privacy">Privacy</string>
<string name="fav_add">Save as favorite</string>
@@ -142,6 +143,7 @@
<string name="category_caravan_site">Caravan site</string>
<string name="menu_apply">Apply filters</string>
<string name="menu_save_profile">Save as profile</string>
<string name="menu_reset">Reset filter settings</string>
<string name="no_filters">No filters</string>
<string name="filter_custom">Modified filter</string>
<string name="filter_favorites">Favorites</string>
@@ -156,7 +158,7 @@
<string name="welcome_2">Each charger\'s color corresponds to its max charging power</string>
<string name="welcome_2_detail">This can also be seen in “About” → “Frequently Asked Questions”</string>
<string name="donation_dialog_title">Thank you for using EVMap</string>
<string name="donation_dialog_detail">EVMap is libre and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
<string name="donation_dialog_detail">EVMap is open source and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
<string name="chargeprice_donation_dialog_title">You\'re a real bargain hunter!</string>
<string name="chargeprice_donation_dialog_detail">You make great use of the price comparison feature. Please help cover the costs for this data by supporting EVMap with a donation.</string>
<string name="deleted_filterprofile">Deleted “%s”</string>
@@ -180,7 +182,7 @@
<string name="chargeprice_session_fee">session fee</string>
<string name="chargeprice_per_kwh">per kWh</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_blocking_fee">Blocking fee >%s</string>
<string name="chargeprice_blocking_fee">Blocking fee &gt;%s</string>
<string name="chargeprice_no_tariffs_found">No charging plans for this charger on Chargeprice.app</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="chargeprice_base_fee">Base fee: %2$s%1$.2f/month</string>
@@ -287,4 +289,10 @@
<string name="pref_applink_associate_summary">from goingelectric.de and openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">My plans</string>
<string name="chargeprice_header_other_tariffs">Other plans</string>
</resources>
<string name="developer_mode_enabled">Developer mode enabled</string>
<string name="developer_options">Developer options</string>
<string name="disable_developer_mode">Disable developer mode</string>
<string name="developer_mode_disabled">Developer mode disabled</string>
<string name="gps">GPS</string>
<string name="compass">Compass</string>
</resources>

View File

@@ -9,7 +9,7 @@
(e.g. in the debug version). -->
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="${applicationId}"
android:targetPackage="net.vonforst.evmap"
android:targetClass="net.vonforst.evmap.MapsActivity">
<extra
android:name="favorites"

View File

@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.7.21'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.3'
repositories {
@@ -10,11 +10,10 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.android.tools.build:gradle:7.4.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
classpath "de.timfreiheit.resourceplaceholders:placeholders:0.4"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@@ -47,7 +47,7 @@ Below you find a list of all the services and how to obtain the API keys.
Map providers
-------------
The different Map SDKs are wrapped by our [fork](https://github.com/johan12345/AnyMaps) of the
The different Map SDKs are wrapped by our [fork](https://github.com/ev-map/AnyMaps) of the
[AnyMaps](https://github.com/sharenowTech/AnyMaps) library to provide a common API. The `google`
build flavor of the app includes both Google Maps and Mapbox and allows the user to switch between
the two, while the `foss` flavor only includes the Mapbox SDK.
@@ -118,7 +118,7 @@ in German.
- website (*Webseite*, optional)
- phone number (*Telefonnummer*, optional)
- name of the app (*Name der App*): EVMap
- app website (*Webseite der App*): https://github.com/johan12345/EVMap
- app website (*Webseite der App*): https://github.com/ev-map/EVMap
- description (*kurze Beschreibung der App*): please explain that you would like to contribute to
the development of EVMap and therefore need access to the GoingElectric.de API.
- Referrer (*Herkunft*): leave this field blank!
@@ -137,7 +137,7 @@ in German.
1. [Sign up](https://openchargemap.org/site/loginprovider/register) for an account at OpenChargeMap
2. Go to the [My Apps](https://openchargemap.org/site/profile/applications) page and click
*Register an application*
3. Enter the name of the app (EVMap) and website (https://github.com/johan12345/EVMap), and in the
3. Enter the name of the app (EVMap) and website (https://github.com/ev-map/EVMap), and in the
description field describe that you would like to contribute to the development of EVMap and
therefore need access to the OpenChargeMap API. Do not tick the *List App in Public Showcase*
box. Then, click *save*.
@@ -160,7 +160,7 @@ Since February 2022, the Chargeprice API is no longer available for free to new
you can use their
[staging API](https://github.com/chargeprice/chargeprice-api-docs/blob/master/test_the_api.md)
for free to test the Chargeprice features. This is already
[configured](https://github.com/johan12345/EVMap/blob/master/app/src/debug/res/values/donottranslate.xml)
[configured](https://github.com/ev-map/EVMap/blob/master/app/src/debug/res/values/donottranslate.xml)
by default for the debug version of the app, so you can leave the `chargeprice_key` field in your
new `app/src/main/res/values/apikeys.xml` file blank. Note that the staging API contains only a
limited dataset, so it only outputs prices for certain charge point operators and payment plans (see

View File

@@ -0,0 +1,5 @@
Verbesserungen:
- Android Auto: Suchbutton während der Fahrt freigeschaltet (ggf. ohne Tastatur)
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,7 @@
Verbesserungen:
- Neuer Knopf zum Zurücksetzen der Filtereinstellungen
- Filtermenü mit neuen Icons
- Übersetzungen aktualisiert
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,5 @@
Verbesserungen:
- Neue Übersetzung: Niederländisch
Fehler behoben:
- Abstürze behoben

View File

@@ -11,7 +11,7 @@ Funktionen:
- Favoritenliste, auch mit Anzeige der Verfügbarkeit
- Keine nervige Werbung
EVMap ist ein Open-Source-Projekt und unter https://github.com/johan12345/EVMap zu finden.
EVMap ist ein Open-Source-Projekt und unter https://github.com/ev-map/EVMap zu finden.
Die App ist kein offizielles Angebot von GoingElectric.de oder Open Charge Map, sondern nutzt die öffentlichen APIs dieser Seiten.

View File

@@ -0,0 +1,5 @@
Improvements:
- Android Auto: Search button available while driving (possibly without keyboard)
Bugs fixed:
- Fixed crashes

View File

@@ -0,0 +1,7 @@
Improvements:
- New button to reset filter setting
- Filter menu with new icons
- Updated translations
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,5 @@
Improvements:
- New translation: Dutch
Bugfixes:
- Fixed crashes

View File

@@ -11,7 +11,7 @@ Features:
- Favorites list, also with availability information
- No ads, fully open source
EVMap is an open source project and can be found at https://github.com/johan12345/EVMap.
EVMap is an open source project and can be found at https://github.com/ev-map/EVMap.
This app is not an official product of GoingElectric.de or Open Charge Map, it only uses their public APIs.

View File

@@ -11,7 +11,7 @@ Caractéristiques :
- Liste de favoris, avec également des informations sur la disponibilité
- Pas de publicité, entièrement open source
EVMap est un projet open source et peut être trouvé sur https://github.com/johan12345/EVMap.
EVMap est un projet open source et peut être trouvé sur https://github.com/ev-map/EVMap.
Cette application n'est pas un produit officiel de GoingElectric.de ou Open Charge Map, elle utilise uniquement leurs API publiques.

View File

@@ -11,7 +11,7 @@ Du finner info om ladestasjoner i hele verden og sanntidsinfo for mange av dem s
- Favorittliste, som også har tilgjengelighetsinfo
- Ingen reklame
Du finner kildekoden på https://github.com/johan12345/EVMap.
Du finner kildekoden på https://github.com/ev-map/EVMap.
Dette kartet er ikke et offisielt program fra hverken GoingElectric.de eller Open Charge Map.
Kun de offentlige API-ene derfra benyttes.

View File

@@ -0,0 +1,18 @@
Met EVMap vind je op een eenvoudige manier EV laders op je Android toestel. Het geeft mobiele toegang tot de door de community onderhouden databases van GoingElectric.de en Open Charge Map, die informatie bevatten over laadstations over de hele wereld. Voor heel wat laadpunten in Europa kan je de real-time status zien.
Kenmerken:
- Material Design
- Toont alle laadpunten van de GoingElectric.de en Open Charge Map databanken
- Real-time status (enkel in Europa)
- Geïntegreerde prijsvergelijking via Chargeprice.app (enkel in Europe)
- Kaartgegevens van OpenStreetMap (Mapbox)
- Zoek naar locaties
- Geavanceerde filtermogelijkheden, inclusief bewaarde filterprofielen
- Lijst van favorieten, met statusinformatie
- Geen advertenties, volledig open source
EVMap is een open source project en is te vinden op https://github.com/ev-map/EVMap.
Deze app is geen officieel product van GoingElectric.de of Open Charge Map; ze gebruikt enkel hun publieke APIs.
Een lijst van noodzakelijke permissies is hier te vinden: https://evmap.vonforst.net/en/permissions.html

View File

@@ -0,0 +1 @@
Zoek EV laadstations

View File

@@ -0,0 +1 @@
EVMap - EV laadpunten

View File

@@ -1,6 +1,6 @@
#Sat Aug 06 15:33:46 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME