Compare commits

...

9 Commits
0.8.1 ... 0.8.2

Author SHA1 Message Date
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
28 changed files with 509 additions and 497 deletions

View File

@@ -59,6 +59,6 @@ following content:
</string>
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
</string>
</resources>
```

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
versionCode 49
versionName "0.8.1"
versionCode 50
versionName "0.8.2"
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'

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

@@ -344,40 +344,48 @@ 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() }
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
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)
} 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

@@ -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

@@ -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

@@ -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

@@ -143,6 +143,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 +220,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 {
@@ -325,6 +334,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
@@ -349,6 +359,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 =
@@ -358,6 +369,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
refData as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
}
}

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,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,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