mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 16:17:45 -05:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086cc51dd3 | ||
|
|
0de91bc107 | ||
|
|
3436bcd870 | ||
|
|
22c150d557 | ||
|
|
675abb5011 | ||
|
|
af2a2cfcae | ||
|
|
f74526fdd6 | ||
|
|
c5bbca0428 | ||
|
|
6167079c0e | ||
|
|
c3836a92ad | ||
|
|
dccce1a0a0 | ||
|
|
74d79640a8 | ||
|
|
0eb6ece780 | ||
|
|
ae15b13591 | ||
|
|
4962eb7268 | ||
|
|
abe360d7c2 | ||
|
|
2aa1fcf5bd | ||
|
|
221e5f49bc | ||
|
|
df6f26ad56 | ||
|
|
1210efd3b9 | ||
|
|
097be8c92b | ||
|
|
16031884ac | ||
|
|
c0b4c56eda | ||
|
|
9587ee948d | ||
|
|
890eec4419 | ||
|
|
c972c871d4 | ||
|
|
e4da902430 | ||
|
|
7a5d4b4107 | ||
|
|
80642b1731 | ||
|
|
6dab611c1b | ||
|
|
d9fc43af68 | ||
|
|
2fd0fa7e22 | ||
|
|
b04284fb16 | ||
|
|
7b3bd84d18 | ||
|
|
773d052819 | ||
|
|
4e0ad98e17 | ||
|
|
d8e572338a | ||
|
|
ff86eeff95 | ||
|
|
47f57992fb | ||
|
|
0ae59358ca | ||
|
|
576e0b9c42 | ||
|
|
3878b27154 | ||
|
|
2166ac076a | ||
|
|
c489df2aaf | ||
|
|
56712ff1af | ||
|
|
e2cf332f34 | ||
|
|
0b541d498d | ||
|
|
1bdc576300 |
@@ -1,7 +1,7 @@
|
||||
EVMap [](https://github.com/johan12345/EVMap/actions)
|
||||
EVMap [](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
|
||||
-----------------
|
||||
|
||||
@@ -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) {
|
||||
|
||||
6
app/src/foss/res/values-nl/strings.xml
Normal file
6
app/src/foss/res/values-nl/strings.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
41
app/src/google/res/values-nl/strings.xml
Normal file
41
app/src/google/res/values-nl/strings.xml
Normal 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>
|
||||
5
app/src/googleAutomotive/res/values-nl/strings.xml
Normal file
5
app/src/googleAutomotive/res/values-nl/strings.xml
Normal 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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -92,4 +92,8 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetValues() {
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
|
||||
}
|
||||
}
|
||||
@@ -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)) }
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
10
app/src/main/res/drawable/ic_filter_no.xml
Normal file
10
app/src/main/res/drawable/ic_filter_no.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_manage_filter_profiles.xml
Normal file
10
app/src/main/res/drawable/ic_manage_filter_profiles.xml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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">©2020–2022 Johan von Forstner</string>
|
||||
<string name="copyright_summary">©2020–2023 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>
|
||||
@@ -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">©2020–2023 Johan von Forstner</string>
|
||||
<string name="other">Autre</string>
|
||||
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance l’application de cartes à l’emplacement du chargeur</string>
|
||||
<string name="settings_map">Carte</string>
|
||||
|
||||
@@ -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">©2020–2022 Johan von Forstner</string>
|
||||
<string name="copyright_summary">©2020–2023 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>
|
||||
299
app/src/main/res/values-nl/strings.xml
Normal file
299
app/src/main/res/values-nl/strings.xml
Normal 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” -> “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">©2020–2023 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 >%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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">©2020–2022 Johan von Forstner</string>
|
||||
<string name="copyright_summary">©2020–2023 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 >%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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
5
fastlane/metadata/android/de-DE/changelogs/156.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/156.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Verbesserungen:
|
||||
- Android Auto: Suchbutton während der Fahrt freigeschaltet (ggf. ohne Tastatur)
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
7
fastlane/metadata/android/de-DE/changelogs/158.txt
Normal file
7
fastlane/metadata/android/de-DE/changelogs/158.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Verbesserungen:
|
||||
- Neuer Knopf zum Zurücksetzen der Filtereinstellungen
|
||||
- Filtermenü mit neuen Icons
|
||||
- Übersetzungen aktualisiert
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/160.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/160.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
5
fastlane/metadata/android/de-DE/changelogs/162.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/162.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Verbesserungen:
|
||||
- Neue Übersetzung: Niederländisch
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
@@ -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.
|
||||
|
||||
|
||||
5
fastlane/metadata/android/en-US/changelogs/156.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/156.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Improvements:
|
||||
- Android Auto: Search button available while driving (possibly without keyboard)
|
||||
|
||||
Bugs fixed:
|
||||
- Fixed crashes
|
||||
7
fastlane/metadata/android/en-US/changelogs/158.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/158.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Improvements:
|
||||
- New button to reset filter setting
|
||||
- Filter menu with new icons
|
||||
- Updated translations
|
||||
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/160.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/160.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
5
fastlane/metadata/android/en-US/changelogs/162.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/162.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Improvements:
|
||||
- New translation: Dutch
|
||||
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
18
fastlane/metadata/android/nl-NL/full_description.txt
Normal file
18
fastlane/metadata/android/nl-NL/full_description.txt
Normal 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
|
||||
1
fastlane/metadata/android/nl-NL/short_description.txt
Normal file
1
fastlane/metadata/android/nl-NL/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Zoek EV laadstations
|
||||
1
fastlane/metadata/android/nl-NL/title.txt
Normal file
1
fastlane/metadata/android/nl-NL/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
EVMap - EV laadpunten
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user