Compare commits

...

30 Commits
0.8.0 ... 0.8.3

Author SHA1 Message Date
Johan von Forstner
14edb6f0cd release 0.8.3 2021-07-27 22:11:52 +02:00
Johan von Forstner
7726088f91 update AnyMaps 2021-07-27 22:09:18 +02:00
Johan von Forstner
cbc7c5a6d8 MapViewModel: cancel loading charger details when another charger is selected 2021-07-25 19:23:20 +02:00
Johan von Forstner
d510d81914 SettingsFragment: move appBarConfiguration to onResume to fix crash when changing dark mode setting 2021-07-25 19:14:23 +02:00
Johan von Forstner
9f5abd6c91 apparently we need @ExperimentalCarApi all classes that create a MapScreen as well 2021-07-22 13:57:31 +02:00
Johan von Forstner
966f62ac3d move @ExperimentalCarApi annotation to the whole MapScreen class 2021-07-22 13:03:40 +02:00
Johan von Forstner
91caf40bdb Android Auto: show city next to charger name
if there is enough room, the name does not already contain the city, and not all chargers on the list are in the same city
fixes #102
2021-07-22 12:41:55 +02:00
Johan von Forstner
72c0293365 update AnyMaps
New version uses Mapbox's legacy Marker API instead of the annotation plugin. This might be a fix for #91
2021-07-22 11:45:18 +02:00
johan12345
ca9dc9629f fix a coroutine crash when no internet available 2021-07-20 20:18:08 +02:00
johan12345
438e529257 fix crash in Android Auto 2021-07-20 19:43:37 +02:00
johan12345
5f69123d89 Release 0.8.2 2021-07-18 20:22:02 +02:00
johan12345
cf421b52a8 catch IOExceptions 2021-07-18 20:16:14 +02:00
johan12345
1b049d35b8 fix IndexOutOfBoundsException 2021-07-18 20:09:34 +02:00
johan12345
f6690a3566 add swipe-to-delete to favorites (fixes #75) 2021-07-18 20:02:05 +02:00
johan12345
cc97020216 adjust "report new charger" button in menu to use OpenChargeMap if chosen 2021-07-18 19:20:33 +02:00
johan12345
0e1e3ba46e use StfalconImageViewer for gallery fullscreen view
fixes #61
2021-07-17 22:22:39 +02:00
Johan von Forstner
657c209827 README.md: fix indentation 2021-07-16 22:56:38 +02:00
johan12345
6ec44bb526 fix filtering of charger status by selected connectors (fixes #100) 2021-07-16 22:40:57 +02:00
johan12345
0943505d90 Chargeprice: show charging duration (fixes #99) 2021-07-16 22:30:02 +02:00
johan12345
f155f7615f Release 0.8.1 2021-07-14 23:25:02 +02:00
johan12345
e8850575f2 avoid another Chargeprice crash 2021-07-14 23:20:29 +02:00
johan12345
d1c4d0a621 fix crash in Chargeprice window for certain chargers 2021-07-14 23:16:40 +02:00
johan12345
ecf27abdc5 remove unnecessary conversion of filter values 2021-07-14 23:05:17 +02:00
johan12345
5f5142baa6 fix bug with connectors filter in GoingElectricApi 2021-07-14 23:00:06 +02:00
johan12345
fa53a9fc5a cleaner implementation of equals check on FilterValues 2021-07-14 23:00:06 +02:00
johan12345
9a0a7b4e5f add SVG source file for Type1 connector 2021-07-14 23:00:06 +02:00
johan12345
1a43703db5 speed up database operations when saving filter values 2021-07-14 23:00:06 +02:00
Johan von Forstner
459589c51f Merge pull request #98 from johan12345/dependabot/bundler/addressable-2.8.0
Bump addressable from 2.7.0 to 2.8.0
2021-07-14 19:12:27 +02:00
dependabot[bot]
9393fe7380 Bump addressable from 2.7.0 to 2.8.0
Bumps [addressable](https://github.com/sporkmonger/addressable) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/sporkmonger/addressable/releases)
- [Changelog](https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sporkmonger/addressable/compare/addressable-2.7.0...addressable-2.8.0)

---
updated-dependencies:
- dependency-name: addressable
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 15:00:17 +00:00
Johan von Forstner
f62bd1c3c4 README: Update description with OCM support 2021-07-11 23:39:10 +02:00
40 changed files with 638 additions and 560 deletions

View File

@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
@@ -125,7 +125,7 @@ GEM
naturally (2.2.0)
os (1.1.1)
plist (3.5.0)
public_suffix (4.0.5)
public_suffix (4.0.6)
rake (13.0.1)
representable (3.0.4)
declarative (< 0.1.0)
@@ -154,6 +154,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unf_ext (0.0.7.7-x64-mingw32)
unicode-display_width (1.7.0)
word_wrap (1.0.0)
@@ -170,6 +171,7 @@ GEM
PLATFORMS
x64-mingw32
x86_64-linux
DEPENDENCIES
fastlane

View File

@@ -3,7 +3,7 @@ EVMap [![Build Status](https://travis-ci.org/johan12345/EVMap.svg?branch=master)
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
Android app to access the goingelectric.de electric vehicle charging station directory.
Android app to find electric vehicle charging stations.
<a href="https://play.google.com/store/apps/details?id=net.vonforst.evmap" target="_blank">
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="100"/></a>
@@ -14,7 +14,7 @@ Features
--------
- [Material Design](https://material.io/)
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) directory
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) and [Open Charge Map](https://openchargemap.org) directories
- Realtime availability information (beta)
- Search places
- Favorites list, also with availability information
@@ -59,6 +59,6 @@ following content:
</string>
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
</string>
</resources>
```

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_5" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:1.7;stroke-miterlimit:10;}
.st2{fill:none;stroke:#000000;stroke-width:0.5;stroke-miterlimit:10;}
</style>
<circle cx="9" cy="18.7" r="1.4" />
<circle cx="15" cy="18.7" r="1.4" />
<path class="st0" d="M8.9,16.1h6.2c1.5,0,2.7,1.2,2.7,2.7l0,0c0,1.5-1.2,2.7-2.7,2.7H8.9c-1.5,0-2.7-1.2-2.7-2.7l0,0
C6.2,17.3,7.4,16.1,8.9,16.1z" />
<g>
<circle cx="14.7" cy="6.4" r="1.3" />
<circle cx="15.3" cy="10.5" r="0.8" />
<circle cx="8.7" cy="10.5" r="0.8" />
<circle cx="9.3" cy="6.4" r="1.3" />
<circle cx="12" cy="13.1" r="1.3" />
<circle class="st1" cx="12" cy="9.1" r="6.3" />
<rect x="11" y="15.4" width="2" height="1.3" />
<line class="st2" x1="10.9" y1="1.3" x2="13.1" y2="1.3" />
<polygon points="13.1,0.9 13.1,2.4 14.5,3.1 13.8,1 " />
<polygon points="10.9,0.9 10.9,2.4 9.5,3.1 10.2,1 " />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
versionCode 48
versionName "0.8.0"
versionCode 51
versionName "0.8.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -125,7 +125,7 @@ dependencies {
implementation 'moe.banana:moshi-jsonapi:3.5.0'
implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0'
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3'
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:3.4.0'
@@ -139,7 +139,7 @@ dependencies {
googleImplementation 'androidx.car.app:app:1.0.0'
// AnyMaps
def anyMapsVersion = '1f050d860f'
def anyMapsVersion = '95ddd6c083'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"

View File

@@ -47,6 +47,7 @@ import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.viewmodel.Status
import java.io.IOException
import java.time.Duration
import java.time.ZoneId
@@ -60,6 +61,7 @@ interface LocationAwareScreen {
fun updateLocation(location: Location)
}
@androidx.car.app.annotations.ExperimentalCarApi
class CarAppService : androidx.car.app.CarAppService() {
override fun createHostValidator(): HostValidator {
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
@@ -76,6 +78,7 @@ class CarAppService : androidx.car.app.CarAppService() {
}
}
@androidx.car.app.annotations.ExperimentalCarApi
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
var mapScreen: LocationAwareScreen? = null
set(value) {
@@ -161,6 +164,7 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
/**
* Welcome screen with selection between favorites and nearby chargers
*/
@androidx.car.app.annotations.ExperimentalCarApi
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
private var location: Location? = null
@@ -221,6 +225,7 @@ class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), L
/**
* Screen to grant location permission
*/
@androidx.car.app.annotations.ExperimentalCarApi
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
override fun onGetTemplate(): Template {
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
@@ -275,6 +280,7 @@ class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx)
/**
* Main map screen showing either nearby chargers or favorites
*/
@androidx.car.app.annotations.ExperimentalCarApi
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
Screen(ctx), LocationAwareScreen {
private var updateCoroutine: Job? = null
@@ -312,8 +318,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
} ?: setLoading(true)
chargers?.take(maxRows)?.let { chargerList ->
val builder = ItemList.Builder()
// only show the city if not all chargers are in the same city
val showCity = chargerList.map { it.address.city }.distinct().size > 1
chargerList.forEach { charger ->
builder.addItem(formatCharger(charger))
builder.addItem(formatCharger(charger, showCity))
}
builder.setNoItemsMessage(
carContext.getString(
@@ -332,7 +340,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}.build()
}
private fun formatCharger(charger: ChargeLocation): Row {
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
val place =
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
@@ -344,7 +352,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
.build()
return Row.Builder().apply {
setTitle(charger.name)
// only show the city if not all chargers are in the same city (-> showCity == true)
// and the city is not already contained in the charger name
if (showCity && charger.address.city != null && charger.address.city !in charger.name) {
setTitle(CarText.Builder("${charger.name} · ${charger.address.city}")
.addVariant(charger.name)
.build())
} else {
setTitle(charger.name)
}
val text = SpannableStringBuilder()
// distance
@@ -655,8 +672,8 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private fun loadCharger() {
lifecycleScope.launch {
try {
val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id)
val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id)
if (response.status == Status.SUCCESS) {
charger = response.data!!
val photo = charger?.photos?.firstOrNull()
@@ -671,7 +688,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
availability = charger?.let { getAvailability(it).data }
invalidate()
} catch (e: IOException) {
} else {
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()

View File

@@ -22,7 +22,6 @@ import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.ui.CheckableConstraintLayout
import net.vonforst.evmap.viewmodel.FavoritesViewModel
interface Equatable {
override fun equals(other: Any?): Boolean
@@ -89,18 +88,6 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
override fun getItemViewType(position: Int): Int = R.layout.item_connector
}
class FavoritesAdapter(val vm: FavoritesViewModel) :
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
init {
setHasStableIds(true)
}
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
override fun getItemId(position: Int): Long = getItem(position).charger.id
}
class ChargepriceAdapter() :
DataBindingAdapter<ChargePrice>() {

View File

@@ -0,0 +1,39 @@
package net.vonforst.evmap.adapter
import android.annotation.SuppressLint
import android.view.animation.AccelerateInterpolator
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.ItemFavoriteBinding
import net.vonforst.evmap.viewmodel.FavoritesViewModel
class FavoritesAdapter(val onDelete: (FavoritesViewModel.FavoritesListItem) -> Unit) :
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
init {
setHasStableIds(true)
}
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
override fun getItemId(position: Int): Long = getItem(position).charger.id
@SuppressLint("ClickableViewAccessibility")
override fun bind(
holder: ViewHolder<FavoritesViewModel.FavoritesListItem>,
item: FavoritesViewModel.FavoritesListItem
) {
super.bind(holder, item)
val binding = holder.binding as ItemFavoriteBinding
binding.foreground.translationX = 0f
binding.btnDelete.setOnClickListener {
binding.foreground.animate()
.translationX(binding.foreground.width.toFloat())
.setDuration(250)
.setInterpolator(AccelerateInterpolator())
.withEndAction {
onDelete(item)
}
.start()
}
}
}

View File

@@ -2,7 +2,9 @@ package net.vonforst.evmap.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -11,19 +13,11 @@ import coil.load
import coil.memory.MemoryCache
import coil.size.OriginalSize
import coil.size.SizeResolver
import com.ortiz.touchview.TouchImageView
import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargerPhoto
class GalleryAdapter(
context: Context,
val itemClickListener: ItemClickListener? = null,
val detailView: Boolean = false,
val pageToLoad: Int? = null,
val imageCacheKey: MemoryCache.Key? = null,
val loadedListener: (() -> Unit)? = null
) :
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
ListAdapter<ChargerPhoto, GalleryAdapter.ViewHolder>(ChargerPhotoDiffCallback()) {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
@@ -38,104 +32,34 @@ class GalleryAdapter(
@SuppressLint("ClickableViewAccessibility")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: ImageView
if (detailView) {
view = inflater.inflate(R.layout.gallery_item_fullscreen, parent, false) as ImageView
view.setOnTouchListener { v, event ->
var result = true
//can scroll horizontally checks if there's still a part of the image
//that can be scrolled until you reach the edge
if (event.pointerCount >= 2 || v.canScrollHorizontally(1) && v.canScrollHorizontally(
-1
)
) {
//multi-touch event
result = when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
// Disallow RecyclerView to intercept touch events.
parent.requestDisallowInterceptTouchEvent(true)
// Disable touch on view
false
}
MotionEvent.ACTION_UP -> {
// Allow RecyclerView to intercept touch events.
parent.requestDisallowInterceptTouchEvent(false)
true
}
else -> true
}
}
result
}
} else {
view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView
}
val view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (detailView) {
(holder.view as TouchImageView).resetZoom()
}
val id = getItem(position).id
val url = if (detailView) {
getItem(position).getUrl(size = 1000)
} else {
getItem(position).getUrl(height = holder.view.height)
}
val url = getItem(position).getUrl(height = holder.view.height)
holder.view.load(
url
) {
if (pageToLoad == position && imageCacheKey != null) {
placeholderMemoryCacheKey(imageCacheKey)
}
size(SizeResolver(OriginalSize))
allowHardware(false)
listener(
onSuccess = { _, metadata ->
memoryKeys[id] = metadata.memoryCacheKey
if (pageToLoad == position) invokeLoadedListener(holder.view)
},
onError = { _, _ ->
if (!loaded && loadedListener != null && pageToLoad == position) {
loadedListener.invoke()
loaded = true
}
}
)
}
if (pageToLoad == position && imageCacheKey != null) {
// start transition immediately
if (pageToLoad == position) invokeLoadedListener(holder.view)
}
holder.view.transitionName = galleryTransitionName(position)
if (itemClickListener != null) {
holder.view.setOnClickListener {
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
}
}
}
private fun invokeLoadedListener(
view: ImageView
) {
if (!loaded && loadedListener != null) {
view.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
view.viewTreeObserver.removeOnPreDrawListener(this)
loadedListener.invoke()
return true
}
})
loaded = true
}
}
}
fun galleryTransitionName(position: Int) = "gallery_$position"
class ChargerPhotoDiffCallback : DiffUtil.ItemCallback<ChargerPhoto>() {
override fun areItemsTheSame(oldItem: ChargerPhoto, newItem: ChargerPhoto): Boolean {
return oldItem.id == newItem.id

View File

@@ -8,7 +8,8 @@ import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.viewmodel.Resource
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
@@ -117,17 +118,10 @@ data class ChargeLocationStatus(
val status: Map<Chargepoint, List<ChargepointStatus>>,
val source: String
) {
fun applyFilters(filters: FilterValues?): ChargeLocationStatus {
if (filters == null) return this
val connectorsVal = filters.getMultipleChoiceValue("connectors")
val minPower = filters.getSliderValue("min_power")
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
val statusFiltered = status.filterKeys {
(connectorsVal == null || connectorsVal.all || connectorsVal.values.map {
equivalentPlugTypes(
it
)
(connectors == null || connectors.map {
equivalentPlugTypes(it)
}.any { equivalent -> it.type in equivalent })
&& (minPower == null || it.power > minPower)
}

View File

@@ -7,6 +7,7 @@ import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
@@ -142,15 +143,10 @@ class GoingElectricApiWrapper(
val minPower = filters?.getSliderValue("min_power")
val minConnectors = filters?.getSliderValue("min_connectors")
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
if (connectorsVal != null) {
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
connectorsVal.values = connectorsVal.values.mapNotNull {
GEChargepoint.convertTypeToGE(it)
}.toMutableSet()
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
val connectors = formatMultipleChoice(connectorsVal)
@@ -241,15 +237,10 @@ class GoingElectricApiWrapper(
val minPower = filters?.getSliderValue("min_power")
val minConnectors = filters?.getSliderValue("min_connectors")
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
if (connectorsVal != null) {
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
connectorsVal.values = connectorsVal.values.mapNotNull {
GEChargepoint.convertTypeToGE(it)
}.toMutableSet()
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
val connectors = formatMultipleChoice(connectorsVal)
@@ -354,40 +345,50 @@ class GoingElectricApiWrapper(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation> {
val response = api.getChargepointDetail(id)
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
Resource.success(
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
apikey
try {
val response = api.getChargepointDetail(id)
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
Resource.success(
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
apikey
)
)
)
} else {
Resource.error(response.message(), null)
} else {
Resource.error(response.message(), null)
}
} catch (e: IOException) {
return Resource.error(e.message, null)
}
}
override suspend fun getReferenceData(): Resource<GEReferenceData> =
withContext(Dispatchers.IO) {
val plugs = async { api.getPlugs() }
val chargeCards = async { api.getChargeCards() }
val networks = async { api.getNetworks() }
supervisorScope {
try {
val plugs = async { api.getPlugs() }
val chargeCards = async { api.getChargeCards() }
val networks = async { api.getNetworks() }
val plugsResponse = plugs.await()
val chargeCardsResponse = chargeCards.await()
val networksResponse = networks.await()
val plugsResponse = plugs.await()
val chargeCardsResponse = chargeCards.await()
val networksResponse = networks.await()
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
if (responses.map { it.isSuccessful }.all { it }) {
Resource.success(
GEReferenceData(
plugsResponse.body()!!.result,
networksResponse.body()!!.result,
chargeCardsResponse.body()!!.result
)
)
} else {
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
if (responses.map { it.isSuccessful }.all { it }) {
Resource.success(
GEReferenceData(
plugsResponse.body()!!.result,
networksResponse.body()!!.result,
chargeCardsResponse.body()!!.result
)
)
} else {
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
}
} catch (e: IOException) {
Resource.error(e.message, null)
}
}
}

View File

@@ -20,6 +20,7 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import java.io.IOException
interface OpenChargeMapApi {
@GET("poi/")
@@ -137,29 +138,33 @@ class OpenChargeMapApiWrapper(
}
val operators = formatMultipleChoice(operatorsVal)
val response = api.getChargepoints(
OCMBoundingBox(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude
),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
try {
val response = api.getChargepoints(
OCMBoundingBox(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude
),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
var result = postprocessResult(
response.body()!!,
minPower,
connectorsVal,
minConnectors,
referenceData,
zoom
)
return Resource.success(result)
var result = postprocessResult(
response.body()!!,
minPower,
connectorsVal,
minConnectors,
referenceData,
zoom
)
return Resource.success(result)
} catch (e: IOException) {
return Resource.error(e.message, null)
}
}
override suspend fun getChargepointsRadius(
@@ -189,27 +194,31 @@ class OpenChargeMapApiWrapper(
}
val operators = formatMultipleChoice(operatorsVal)
val response = api.getChargepointsRadius(
location.latitude, location.longitude,
radius.toDouble(),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
try {
val response = api.getChargepointsRadius(
location.latitude, location.longitude,
radius.toDouble(),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
val result = postprocessResult(
response.body()!!,
minPower,
connectorsVal,
minConnectors,
referenceData,
zoom
)
return Resource.success(result)
val result = postprocessResult(
response.body()!!,
minPower,
connectorsVal,
minConnectors,
referenceData,
zoom
)
return Resource.success(result)
} catch (e: IOException) {
return Resource.error(e.message, null)
}
}
private fun postprocessResult(
@@ -244,20 +253,28 @@ class OpenChargeMapApiWrapper(
id: Long
): Resource<ChargeLocation> {
val referenceData = referenceData as OCMReferenceData
val response = api.getChargepointDetail(id)
if (response.isSuccessful) {
return Resource.success(response.body()!![0].convert(referenceData))
} else {
return Resource.error(response.message(), null)
try {
val response = api.getChargepointDetail(id)
if (response.isSuccessful && response.body()?.size == 1) {
return Resource.success(response.body()!![0].convert(referenceData))
} else {
return Resource.error(response.message(), null)
}
} catch (e: IOException) {
return Resource.error(e.message, null)
}
}
override suspend fun getReferenceData(): Resource<OCMReferenceData> {
val response = api.getReferenceData()
if (response.isSuccessful) {
return Resource.success(response.body()!!)
} else {
return Resource.error(response.message(), null)
try {
val response = api.getReferenceData()
if (response.isSuccessful) {
return Resource.success(response.body()!!)
} else {
return Resource.error(response.message(), null)
}
} catch (e: IOException) {
return Resource.error(e.message, null)
}
}

View File

@@ -2,10 +2,13 @@ package net.vonforst.evmap.fragment
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Canvas
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
@@ -14,20 +17,29 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.car2go.maps.model.LatLng
import com.google.android.material.snackbar.Snackbar
import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.FavoritesAdapter
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.databinding.ItemFavoriteBinding
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
private lateinit var binding: FragmentFavoritesBinding
private lateinit var locationClient: LostApiClient
private var toDelete: ChargeLocation? = null
private var deleteSnackbar: Snackbar? = null
private lateinit var adapter: FavoritesAdapter
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
viewModelFactory {
@@ -66,13 +78,15 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
(requireActivity() as MapsActivity).appBarConfiguration
)
val favAdapter = FavoritesAdapter(vm).apply {
adapter = FavoritesAdapter(onDelete = {
delete(it.charger)
}).apply {
onClickListener = {
navController.navigate(R.id.action_favs_to_map, MapFragment.showCharger(it.charger))
}
}
binding.favsList.apply {
adapter = favAdapter
adapter = this@FavoritesFragment.adapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
@@ -81,6 +95,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
)
)
}
createTouchHelper().attachToRecyclerView(binding.favsList)
locationClient.connect()
}
@@ -109,4 +124,136 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
locationClient.disconnect()
}
}
fun delete(fav: ChargeLocation) {
val position = vm.listData.value?.indexOfFirst { it.charger == fav } ?: return
// if there is already a profile to delete, delete it now
actuallyDelete()
deleteSnackbar?.dismiss()
toDelete = fav
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fav.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
toDelete = null
adapter.notifyItemChanged(position)
}.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
// if undo was not clicked, actually delete
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
actuallyDelete()
}
}
})
deleteSnackbar = snackbar
snackbar.show()
} ?: run {
actuallyDelete()
}
}
private fun actuallyDelete() {
toDelete?.let { vm.deleteFavorite(it) }
toDelete = null
}
private fun createTouchHelper(): ItemTouchHelper {
return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val fav = vm.favorites.value?.find { it.id == viewHolder.itemId }
fav?.let { delete(it) }
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (viewHolder != null && actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
getDefaultUIUtil().onSelected(binding.foreground)
} else {
super.onSelectedChanged(viewHolder, actionState)
}
}
override fun onChildDrawOver(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
getDefaultUIUtil().onDrawOver(
c, recyclerView, binding.foreground, dX, dY,
actionState, isCurrentlyActive
)
val lp = (binding.deleteIcon.layoutParams as FrameLayout.LayoutParams)
lp.gravity = Gravity.CENTER_VERTICAL or if (dX > 0) {
Gravity.START
} else {
Gravity.END
}
binding.deleteIcon.layoutParams = lp
} else {
super.onChildDrawOver(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
getDefaultUIUtil().clearView(binding.foreground)
}
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
getDefaultUIUtil().onDraw(
c, recyclerView, binding.foreground, dX, dY,
actionState, isCurrentlyActive
)
} else {
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
})
}
}

View File

@@ -1,136 +0,0 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.app.SharedElementCallback
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater
import androidx.viewpager2.widget.ViewPager2
import coil.memory.MemoryCache
import com.ortiz.touchview.TouchImageView
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.databinding.FragmentGalleryBinding
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.viewmodel.GalleryViewModel
class GalleryFragment : Fragment() {
companion object {
private const val EXTRA_POSITION = "position"
private const val EXTRA_PHOTOS = "photos"
private const val EXTRA_IMAGE_CACHE_KEY = "image_cache_key"
private const val SAVED_CURRENT_PAGE_POSITION = "current_page_position"
fun buildArgs(
photos: List<ChargerPhoto>,
position: Int,
imageCacheKey: MemoryCache.Key?
): Bundle {
return Bundle().apply {
putParcelableArrayList(EXTRA_PHOTOS, ArrayList(photos))
putInt(EXTRA_POSITION, position)
putParcelable(EXTRA_IMAGE_CACHE_KEY, imageCacheKey)
}
}
}
private lateinit var binding: FragmentGalleryBinding
private var startingPosition: Int = 0
private var currentPosition: Int = 0
private lateinit var galleryAdapter: GalleryAdapter
private var currentPage: TouchImageView? = null
private val galleryVm: GalleryViewModel by activityViewModels()
private val backPressedCallback = object :
OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val image = currentPage
if (image != null && image.currentZoom !in 0.95f..1.05f) {
image.setZoomAnimated(1f, 0.5f, 0.5f)
} else {
galleryVm.galleryPosition.value = currentPosition
findNavController().popBackStack()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_gallery, container, false
)
binding.lifecycleOwner = this
val args = requireArguments()
startingPosition = args.getInt(EXTRA_POSITION, 0)
currentPosition =
savedInstanceState?.getInt(SAVED_CURRENT_PAGE_POSITION) ?: startingPosition
galleryAdapter =
GalleryAdapter(
requireContext(), detailView = true, pageToLoad = currentPosition,
imageCacheKey = args.getParcelable(EXTRA_IMAGE_CACHE_KEY)
) {
startPostponedEnterTransition()
}
binding.gallery.setPageTransformer { page, _ ->
val v = page as TouchImageView
currentPage = v
}
binding.gallery.adapter = galleryAdapter
binding.photos = args.getParcelableArrayList(EXTRA_PHOTOS)
binding.gallery.post {
binding.gallery.setCurrentItem(currentPosition, false)
binding.gallery.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
currentPosition = position
}
})
}
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(R.transition.image_shared_element_transition)
sharedElementReturnTransition = TransitionInflater.from(context)
.inflateTransition(R.transition.image_shared_element_transition)
setEnterSharedElementCallback(enterElementCallback)
if (savedInstanceState == null) {
postponeEnterTransition();
}
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
backPressedCallback
)
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVED_CURRENT_PAGE_POSITION, currentPosition)
}
private val enterElementCallback: SharedElementCallback = object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
val currentPage = currentPage ?: return
sharedElements[names[0]] = currentPage
}
}
}

View File

@@ -10,8 +10,8 @@ import android.graphics.Color
import android.location.Geocoder
import android.location.Location
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
@@ -19,7 +19,6 @@ import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat
import androidx.core.app.SharedElementCallback
import androidx.core.content.ContextCompat
import androidx.core.view.MenuCompat
import androidx.core.view.updateLayoutParams
@@ -31,14 +30,16 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import coil.load
import coil.memory.MemoryCache
import coil.size.OriginalSize
import coil.size.SizeResolver
import com.car2go.maps.AnyMap
import com.car2go.maps.MapFragment
import com.car2go.maps.OnMapReadyCallback
@@ -57,6 +58,7 @@ import com.mapzen.android.lost.api.LocationListener
import com.mapzen.android.lost.api.LocationRequest
import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap
import kotlinx.coroutines.Dispatchers
@@ -173,7 +175,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
setHasOptionsMenu(true)
postponeEnterTransition()
binding.root.setOnApplyWindowInsetsListener { _, insets ->
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@@ -194,7 +195,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
insets
}
setExitSharedElementCallback(reenterSharedElementCallback)
exitTransition = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.map_exit_transition)
@@ -537,24 +537,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
private fun setupAdapters() {
var viewer: StfalconImageViewer<ChargerPhoto>? = null
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
override fun onItemClick(view: View, position: Int, imageCacheKey: MemoryCache.Key?) {
val photos = vm.charger.value?.data?.photos ?: return
val extras = FragmentNavigatorExtras(view to view.transitionName)
view.findNavController().navigate(
R.id.action_map_to_galleryFragment,
GalleryFragment.buildArgs(photos, position, imageCacheKey),
null,
extras
)
viewer = StfalconImageViewer.Builder(context, photos) { imageView, photo ->
imageView.load(photo.getUrl(size = 1000)) {
if (photo == photos[position] && imageCacheKey != null) {
placeholderMemoryCacheKey(imageCacheKey)
}
size(SizeResolver(OriginalSize))
allowHardware(false)
}
}
.withTransitionFrom(view as ImageView)
.withImageChangeListener {
binding.gallery.layoutManager!!.scrollToPosition(it)
binding.gallery.layoutManager!!.findViewByPosition(it)?.let {
viewer?.updateTransitionImage(it as ImageView)
}
}
.withStartPosition(position)
.show()
}
}
val galleryPosition = galleryVm.galleryPosition.value
binding.gallery.apply {
adapter = GalleryAdapter(context, galleryClickListener, pageToLoad = galleryPosition) {
startPostponedEnterTransition()
}
adapter = GalleryAdapter(context, galleryClickListener)
itemAnimator = null
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
@@ -565,41 +576,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
setDrawable(ContextCompat.getDrawable(context, R.drawable.gallery_divider)!!)
})
}
if (galleryPosition == null) {
startPostponedEnterTransition()
} else {
binding.gallery.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
v.removeOnLayoutChangeListener(this)
val layoutManager = binding.gallery.layoutManager!!
val viewAtPosition = layoutManager.findViewByPosition(galleryPosition)
if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(
viewAtPosition,
false,
true
)
) {
binding.gallery.post {
layoutManager.scrollToPosition(galleryPosition)
}
}
}
})
// make sure that the app does not freeze waiting for a picture to load
Handler().postDelayed({
startPostponedEnterTransition()
}, 100)
}
binding.detailView.connectors.apply {
adapter = ConnectorAdapter()
@@ -1111,23 +1087,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return binding.root
}
private val reenterSharedElementCallback: SharedElementCallback =
object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
// Locate the ViewHolder for the clicked position.
val position = galleryVm.galleryPosition.value ?: return
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
if (vh?.itemView == null) return
// Map the first shared element name to the child ImageView.
sharedElements[names[0]] = vh.itemView
}
}
companion object {
fun showCharger(charger: ChargeLocation): Bundle {
return Bundle().apply {

View File

@@ -36,15 +36,8 @@ class SettingsFragment : PreferenceFragmentCompat(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
prefs = PreferenceDataSource(requireContext())
val navController = findNavController()
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
vm.vehicles.observe(viewLifecycleOwner) { res ->
@@ -130,6 +123,13 @@ class SettingsFragment : PreferenceFragmentCompat(),
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
val navController = findNavController()
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
}
override fun onPause() {

View File

@@ -48,6 +48,8 @@ sealed class FilterValue : BaseObservable(), Equatable {
abstract val key: String
var dataSource: String = ""
var profile: Long = FILTERS_CUSTOM
abstract fun hasSameValueAs(other: FilterValue): Boolean
}
@Entity(
@@ -62,7 +64,11 @@ sealed class FilterValue : BaseObservable(), Equatable {
data class BooleanFilterValue(
override val key: String,
var value: Boolean
) : FilterValue()
) : FilterValue() {
override fun hasSameValueAs(other: FilterValue): Boolean {
return other is BooleanFilterValue && other.value == this.value
}
}
@Entity(
foreignKeys = [ForeignKey(
@@ -78,23 +84,14 @@ data class MultipleChoiceFilterValue(
var values: MutableSet<String>,
var all: Boolean
) : FilterValue() {
override fun equals(other: Any?): Boolean {
if (other == null || other !is MultipleChoiceFilterValue) return false
if (key != other.key) return false
return if (all) {
other.all
override fun hasSameValueAs(other: FilterValue): Boolean {
return other is MultipleChoiceFilterValue && if (other.all) {
this.all
} else {
!other.all && values == other.values
!this.all && other.values == this.values
}
}
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + all.hashCode()
result = 31 * result + if (all) 0 else values.hashCode()
return result
}
}
@Entity(
@@ -109,7 +106,11 @@ data class MultipleChoiceFilterValue(
data class SliderFilterValue(
override val key: String,
var value: Int
) : FilterValue()
) : FilterValue() {
override fun hasSameValueAs(other: FilterValue): Boolean {
return other is SliderFilterValue && other.value == this.value
}
}
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable

View File

@@ -13,11 +13,12 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
@Navigator.Name("chrome")
class ChromeCustomTabsNavigator(
@Navigator.Name("custom")
class CustomNavigator(
private val context: Context
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
) : Navigator<CustomNavigator.Destination>() {
override fun createDestination() =
Destination(this)
@@ -28,6 +29,19 @@ class ChromeCustomTabsNavigator(
navOptions: NavOptions?,
navigatorExtras: Extras?
): NavDestination? {
if (destination.destination == "report_new_charger") {
val prefs = PreferenceDataSource(context)
val url = when (prefs.dataSource) {
"goingelectric" -> "https://www.goingelectric.de/stromtankstellen/new/"
"openchargemap" -> "https://openchargemap.org/site/poi/add"
else -> throw IllegalArgumentException()
}
launchCustomTab(url)
}
return null // Do not add to the back stack, managed by Chrome Custom Tabs
}
fun launchCustomTab(url: String) {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
@@ -35,20 +49,19 @@ class ChromeCustomTabsNavigator(
.build()
)
.build()
intent.launchUrl(context, destination.url!!)
return null // Do not add to the back stack, managed by Chrome Custom Tabs
intent.launchUrl(context, Uri.parse(url))
}
override fun popBackStack() = true // Managed by Chrome Custom Tabs
@NavDestination.ClassType(Activity::class)
class Destination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {
var url: Uri? = null
lateinit var destination: String
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.withStyledAttributes(attrs, R.styleable.ChromeCustomTabsNavigator, 0, 0) {
url = Uri.parse(getString(R.styleable.ChromeCustomTabsNavigator_url))
context.withStyledAttributes(attrs, R.styleable.CustomNavigator, 0, 0) {
destination = getString(R.styleable.CustomNavigator_customDestination)!!
}
}
}

View File

@@ -7,7 +7,7 @@ class NavHostFragment : NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
ChromeCustomTabsNavigator(
CustomNavigator(
requireContext()
)
)

View File

@@ -65,7 +65,10 @@ abstract class FilterValueDao {
)
for (source in sources) {
addSource(source) {
value = sources.mapNotNull { it.value }.flatten()
val values = sources.map { it.value }
if (values.all { it != null }) {
value = values.filterNotNull().flatten()
}
}
}
}

View File

@@ -105,7 +105,12 @@ class PreferenceDataSource(val context: Context) {
}
var chargepriceMyVehicles: Set<String>
get() = sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
get() = try {
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
} catch (e: ClassCastException) {
// backwards compatibility
sp.getString("chargeprice_my_vehicle", null)?.let { setOf(it) } ?: emptySet()
}
set(value) {
sp.edit().putStringSet("chargeprice_my_vehicle", value).apply()
}

View File

@@ -22,6 +22,8 @@ import com.google.android.material.slider.RangeSlider
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.iconForPlugType
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -265,6 +267,13 @@ fun currency(currency: String): String {
}
}
fun time(value: Int): String {
val h = floor(value.toDouble() / 60).toInt();
val min = ceil(value.toDouble() % 60).toInt();
return if (h == 0 && min > 0) "$min min";
else "%d:%02d h".format(h, min);
}
@InverseBindingAdapter(attribute = "app:values")
fun getRangeSliderValue(slider: RangeSlider) = slider.values

View File

@@ -165,7 +165,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
}
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
val index = charger.value!!.chargepoints.indexOf(chargepoint)
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
return type
}
@@ -209,6 +209,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
private var loadPricesJob: Job? = null
fun loadPrices() {
chargePrices.value = Resource.loading(null)
chargePriceMeta.value = Resource.loading(null)
val charger = charger.value
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value

View File

@@ -93,11 +93,13 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
}
suspend fun saveFilterValues() {
filtersWithValue.value?.forEach {
filtersWithValue.value?.map {
val value = it.value
value.profile = FILTERS_CUSTOM
value.dataSource = prefs.dataSource
db.filterValueDao().insert(value)
value
}?.let {
db.filterValueDao().insert(*it.toTypedArray())
}
// set selected profile
@@ -113,11 +115,13 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
}
// save filter values
filtersWithValue.value?.forEach {
filtersWithValue.value?.map {
val value = it.value
value.profile = profileId
value.dataSource = prefs.dataSource
db.filterValueDao().insert(value)
value
}?.let {
db.filterValueDao().insert(*it.toTypedArray())
}
// set selected profile

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.*
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
@@ -124,7 +125,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
value = 0
addSource(filtersWithValue) { filtersWithValue ->
value = filtersWithValue.count {
it.filter.defaultValue() != it.value
!it.value.hasSameValueAs(it.filter.defaultValue())
}
}
}
@@ -143,6 +144,9 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
val filteredConnectors: MutableLiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>()
}
val filteredMinPower: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
}
val filteredChargeCards: MutableLiveData<Set<Long>> by lazy {
MutableLiveData<Set<Long>>()
}
@@ -217,13 +221,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
val av = availability.value
val filters = filtersWithValue.value
if (av?.status == Status.SUCCESS && filters != null) {
value = Resource.success(av.data!!.applyFilters(filters))
value = Resource.success(
av.data!!.applyFilters(
filteredConnectors.value,
filteredMinPower.value
)
)
} else {
value = av
}
}
addSource(availability, callback)
addSource(filtersWithValue, callback)
addSource(filteredConnectors, callback)
addSource(filteredMinPower, callback)
}
}
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
@@ -287,9 +297,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
if (filterStatus.value == FILTERS_CUSTOM) return
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
filterValues.value?.forEach {
filterValues.value?.map {
it.profile = FILTERS_CUSTOM
db.filterValueDao().insert(it)
it
}?.let {
db.filterValueDao().insert(*it.toTypedArray())
}
}
@@ -323,6 +335,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
val mapPosition = data.first
@@ -347,6 +360,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
} else if (api is OpenChargeMapApiWrapper) {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
@@ -356,6 +370,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
refData as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
}
}
@@ -364,9 +379,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
availability.value = getAvailability(charger)
}
private var chargerLoadingTask: Job? = null
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
chargerDetails.value = Resource.loading(null)
viewModelScope.launch {
chargerLoadingTask?.cancel()
chargerLoadingTask = viewModelScope.launch {
try {
chargerDetails.value = api.getChargepointDetail(referenceData, charger.id)
} catch (e: IOException) {

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -7,6 +7,7 @@
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<variable
name="vm"
@@ -99,6 +100,21 @@
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
tools:text="Charge from 20% to 80%" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="8dp"
android:text="@{@string/chargeprice_duration(BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)))}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
android:textColor="?colorPrimary"
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging &amp;&amp; vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
app:layout_constraintStart_toEndOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
tools:text="(25 min)" />
<TextView
android:id="@+id/tvVehicleHeader"
android:layout_width="wrap_content"

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.model.ChargerPhoto" />
<import type="java.util.List" />
<variable
name="photos"
type="List&lt;ChargerPhoto&gt;" />
</data>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/gallery"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:data="@{photos}" />
</layout>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.ortiz.touchview.TouchImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:fitsSystemWindows="true"
tools:src="@tools:sample/backgrounds/scenic" />

View File

@@ -6,8 +6,11 @@
<data>
<import type="net.vonforst.evmap.api.UtilsKt" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
<variable
@@ -15,21 +18,45 @@
type="net.vonforst.evmap.viewmodel.FavoritesViewModel.FavoritesListItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView15"
android:layout_width="wrap_content"
<FrameLayout
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/delete_red"> <!--Add your background color here-->
<ImageView
android:id="@+id/delete_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical|end"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:tint="@android:color/white"
app:srcCompat="@drawable/ic_delete" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/foreground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.charger.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
android:padding="16dp"
android:background="@drawable/selectable_opaque_background">
<TextView
android:id="@+id/textView15"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.charger.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
<TextView
android:id="@+id/textView2"
@@ -59,40 +86,57 @@
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
<TextView
android:id="@+id/textView16"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:goneUnless="@{item.distance != null}"
android:text="@{@string/distance_format(item.distance)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="9999,9 km" />
<TextView
android:id="@+id/textView16"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@{@string/distance_format(item.distance)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.distance != null}"
app:layout_constraintEnd_toStartOf="@id/btnDelete"
app:layout_constraintTop_toTopOf="parent"
tools:text="9999,9 km" />
<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_rect"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.available.data}"
app:goneUnless="@{item.available.status == Status.SUCCESS}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:backgroundTint="@color/available"
tools:text="80/99" />
<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="@drawable/rounded_rect"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.available.data}"
app:goneUnless="@{item.available.status == Status.SUCCESS}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnDelete"
tools:backgroundTint="@color/available"
tools:text="80/99" />
<ProgressBar
android:id="@+id/progressBar4"
style="?android:attr/progressBarStyle"
android:layout_width="24dp"
android:layout_height="24dp"
app:goneUnless="@{item.available.status == Status.LOADING}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/progressBar4"
style="?android:attr/progressBarStyle"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{item.available.status == Status.LOADING}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnDelete" />
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete"
android:contentDescription="@string/delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</layout>

View File

@@ -9,13 +9,6 @@
android:name="net.vonforst.evmap.fragment.MapFragment"
android:label="MapFragment"
tools:layout="@layout/fragment_map">
<action
android:id="@+id/action_map_to_galleryFragment"
app:destination="@id/gallery"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_filterFragment"
app:destination="@id/filter"
@@ -55,11 +48,6 @@
android:name="net.vonforst.evmap.fragment.SettingsFragment"
android:label="@string/settings"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/gallery"
android:name="net.vonforst.evmap.fragment.GalleryFragment"
android:label="GalleryFragment"
tools:layout="@layout/fragment_gallery" />
<fragment
android:id="@+id/favs"
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
@@ -102,9 +90,9 @@
android:name="net.vonforst.evmap.fragment.updatedialogs.Update060AndroidAutoDialogFramgent"
android:label="@string/welcome_to_evmap"
tools:layout="@layout/dialog_update_060_androidauto" />
<chrome
<custom
android:id="@+id/report_new_charger"
app:url="@string/report_new_charger_url" />
app:customDestination="report_new_charger" />
<fragment
android:id="@+id/onboarding"
android:name="net.vonforst.evmap.fragment.OnboardingFragment"

View File

@@ -187,6 +187,7 @@
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
<string name="chargeprice_duration">(ca. %s)</string>
<string name="chargeprice_vehicle">Fahrzeug</string>
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ChromeCustomTabsNavigator">
<attr name="url" format="reference" />
<declare-styleable name="CustomNavigator">
<attr name="customDestination" format="string" />
</declare-styleable>
<declare-styleable name="MultiSelectDialogPreference">
<attr name="showAllButton" format="boolean" />

View File

@@ -7,5 +7,4 @@
<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>
<string name="report_new_charger_url">https://www.goingelectric.de/stromtankstellen/new/</string>
</resources>

View File

@@ -186,6 +186,7 @@
<string name="pref_chargeprice_show_provider_customer_tariffs">Show customer-exclusive plans</string>
<string name="chargeprice_select_car_first">Please first select your car model in the settings.</string>
<string name="chargeprice_battery_range">Charge from %1$.0f%% to %2$.0f%%</string>
<string name="chargeprice_duration">(approx. %s)</string>
<string name="chargeprice_vehicle">Vehicle</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Some providers offer cheaper plans exclusively to their customers (e.g., household electricity, gas)</string>
<string name="close">close</string>

View File

@@ -0,0 +1,3 @@
- Abstürze behoben
- Problem mit Filter nach Anschlüssen behoben
- Wechsel zwischen Karte und Filteransicht beschleunigt

View File

@@ -0,0 +1,7 @@
Neue Features:
- Favoriten können durch Wischen gelöscht werden
Fehler behoben:
- Verbesserte Vollbildansicht von Fotos
- "Ladesäule melden" verlinkte auf GoingElectric auch wenn OpenChargeMap ausgewählt war
- Abstürze behoben

View File

@@ -0,0 +1,2 @@
- Android Auto: Anzeige des Ortsnamens (wenn genug Platz und nicht eindeutig)
- Abstürze behoben

View File

@@ -0,0 +1,3 @@
- Fixed crashes
- Fixed problem when filtering by connectors
- Improved performance when switching between map and filters view

View File

@@ -0,0 +1,7 @@
New Features:
- swipe to delete favorites
Bugs fixed:
- Improved fullscreen photo view
- "Report new charger" in menu was still linking to GoingElectric when OpenChargeMap was selected
- Fixed crashes

View File

@@ -0,0 +1,2 @@
- Android Auto: Show place name next to charger name (if enough room available)
- Fixed various crashes