mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7fcb35a4e | ||
|
|
56348905a6 | ||
|
|
3336faa953 | ||
|
|
e22e1521a4 | ||
|
|
e974acac4e | ||
|
|
8a13bfcd9e | ||
|
|
1e04d6e98a | ||
|
|
a0045fc6bb | ||
|
|
ec10b51387 | ||
|
|
b054464280 | ||
|
|
1a32159526 | ||
|
|
c6cc7102e6 | ||
|
|
6a5dc93fd8 | ||
|
|
a85966bb1d | ||
|
|
bf3c401c37 | ||
|
|
4da7e0b50d | ||
|
|
d78f2f08cb | ||
|
|
d2952766e4 | ||
|
|
40503b6bd2 | ||
|
|
e875e0ee42 | ||
|
|
6f9ea6c6e3 | ||
|
|
a79d013179 | ||
|
|
4b75389a31 | ||
|
|
1039251d63 | ||
|
|
2cd9e9d642 | ||
|
|
7d495468ea | ||
|
|
e47a82a4bc | ||
|
|
87421e450a | ||
|
|
479917fad1 | ||
|
|
dfaf841160 | ||
|
|
c18ea5b15d | ||
|
|
62116473c8 | ||
|
|
bc8106bd81 | ||
|
|
7bd89b9ecb | ||
|
|
898b61945e | ||
|
|
38e022b547 | ||
|
|
b8c438503c | ||
|
|
2ca6a8e3e8 | ||
|
|
0ae201e363 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,4 +11,5 @@ apikeys.xml
|
||||
/app/**/*.aab
|
||||
/app/**/*.apk
|
||||
/_img/connectors/*.ai
|
||||
api-7125266970515251116-798419-8e2dda660c80.json
|
||||
api-7125266970515251116-798419-8e2dda660c80.json
|
||||
output-metadata.json
|
||||
@@ -7,6 +7,8 @@ Android app to access the goingelectric.de electric vehicle charging station dir
|
||||
|
||||
<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>
|
||||
<a href="https://f-droid.org/repository/browse/?fdid=net.vonforst.evmap" target="_blank">
|
||||
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"/></a>
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
27
_img/map_marker_charging_multiple.svg
Normal file
27
_img/map_marker_charging_multiple.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#B5B5B5;}
|
||||
.st2{fill:#808080;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
|
||||
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
|
||||
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6C6.6,68.6,17.6,49.1,32.8,34
|
||||
C53.3,14,81.1,1.8,109.8,0z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st1"
|
||||
points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
|
||||
<path class="st1"
|
||||
d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path class="st2" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
|
||||
l43.4-74.4h-24.8L138.9,57.3z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +1,6 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 24
|
||||
versionName "0.3.3"
|
||||
versionCode 27
|
||||
versionName "0.4.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -68,6 +68,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
@@ -78,7 +79,7 @@ android {
|
||||
variant.resValue "string", "goingelectric_key", goingelectricKey
|
||||
}
|
||||
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
|
||||
if (googleMapsKey != null) {
|
||||
if (googleMapsKey != null && variant.flavorName == 'google') {
|
||||
variant.resValue "string", "google_maps_key", googleMapsKey
|
||||
}
|
||||
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
|
||||
@@ -92,20 +93,22 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.1'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation "androidx.activity:activity-ktx:1.1.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.5"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.core:core:1.3.1'
|
||||
implementation 'androidx.core:core:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.7.2'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3'
|
||||
@@ -114,9 +117,11 @@ dependencies {
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
implementation 'io.michaelrocks:bimap:1.0.2'
|
||||
implementation 'com.mapzen.android:lost:3.0.2'
|
||||
implementation 'com.google.guava:guava:29.0-android'
|
||||
implementation 'com.github.pengrad:mapscaleview:1.6.0'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = 'e6e014dd11'
|
||||
def anyMapsVersion = '631708a156'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
@@ -125,21 +130,24 @@ dependencies {
|
||||
googleImplementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
|
||||
googleImplementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
|
||||
googleImplementation 'com.android.volley:volley:1.1.1'
|
||||
googleImplementation 'com.google.android.gms:play-services-base:17.3.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-basement:17.3.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-base:17.5.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-basement:17.5.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-tasks:17.1.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-location:17.1.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-tasks:17.2.0'
|
||||
googleImplementation 'com.google.auto.value:auto-value-annotations:1.6.3'
|
||||
googleImplementation 'com.google.code.gson:gson:2.8.6'
|
||||
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.3'
|
||||
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.5'
|
||||
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
|
||||
// Mapbox places (autocomplete)
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0'
|
||||
implementation('com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0') {
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
|
||||
}
|
||||
|
||||
// navigation library
|
||||
def nav_version = "2.3.0"
|
||||
def nav_version = "2.3.2"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
@@ -149,13 +157,13 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.2.5"
|
||||
def room_version = "2.2.6"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "3.0.0"
|
||||
def billing_version = "3.0.2"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
@@ -163,13 +171,13 @@ dependencies {
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.7"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EV Map (debug)</string>
|
||||
<string name="app_name">EVMap (debug)</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EV Map (debug)</string>
|
||||
<string name="app_name">EVMap (debug)</string>
|
||||
</resources>
|
||||
@@ -8,29 +8,32 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import kotlinx.android.synthetic.foss.fragment_donate.*
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.FragmentDonateBinding
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
private lateinit var binding: FragmentDonateBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_donate, container, false)
|
||||
binding = FragmentDonateBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
binding.toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
btnDonate.setOnClickListener {
|
||||
binding.btnDonate.setOnClickListener {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
|
||||
}
|
||||
}
|
||||
|
||||
10
app/src/google/AndroidManifest.xml
Normal file
10
app/src/google/AndroidManifest.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.vonforst.evmap">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -17,7 +17,7 @@ fun checkPlayServices(activity: Activity): Boolean {
|
||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
|
||||
if (resultCode != ConnectionResult.SUCCESS) {
|
||||
if (apiAvailability.isUserResolvableError(resultCode)) {
|
||||
apiAvailability.getErrorDialog(activity, resultCode, request).show()
|
||||
apiAvailability.getErrorDialog(activity, resultCode, request)?.show()
|
||||
} else {
|
||||
Log.d("EVMap", "This device is not supported.")
|
||||
}
|
||||
|
||||
@@ -14,17 +14,6 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!--
|
||||
The API key for Google Maps-based APIs is defined as a string resource.
|
||||
(See the file "res/values/apikeys.xml").
|
||||
Note that the API key is linked to the encryption key used to sign the APK.
|
||||
You need a different API key for each encryption key, including the release key that is used to
|
||||
sign the APK for publishing.
|
||||
You can define the keys for the debug and release targets in src/debug/ and src/release/.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key" />
|
||||
<meta-data
|
||||
android:name="com.mapbox.ACCESS_TOKEN"
|
||||
android:value="@string/mapbox_key" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
@@ -53,7 +54,8 @@ class MapsActivity : AppCompatActivity() {
|
||||
setOf(
|
||||
R.id.map,
|
||||
R.id.favs,
|
||||
R.id.about
|
||||
R.id.about,
|
||||
R.id.settings
|
||||
),
|
||||
findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
)
|
||||
@@ -95,7 +97,11 @@ class MapsActivity : AppCompatActivity() {
|
||||
|
||||
fun openUrl(url: String) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
intent.launchUrl(this, Uri.parse(url))
|
||||
}
|
||||
|
||||
@@ -79,8 +79,11 @@ fun buildDetails(
|
||||
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailsAdapter.Detail(
|
||||
R.drawable.ic_hours,
|
||||
R.string.hours,
|
||||
loc.openinghours.getStatusText(ctx),
|
||||
loc.openinghours.description,
|
||||
if (loc.openinghours.days != null || loc.openinghours.twentyfourSeven)
|
||||
loc.openinghours.getStatusText(ctx)
|
||||
else
|
||||
loc.openinghours.description ?: "",
|
||||
if (loc.openinghours.days != null) loc.openinghours.description else null,
|
||||
hoursDays = loc.openinghours.days
|
||||
) else null,
|
||||
if (loc.cost != null) DetailsAdapter.Detail(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.MotionEvent
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.ItemFilterProfileBinding
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
|
||||
class FilterProfilesAdapter(val dragHelper: ItemTouchHelper) : DataBindingAdapter<FilterProfile>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun bind(
|
||||
holder: ViewHolder<FilterProfile>,
|
||||
item: FilterProfile
|
||||
) {
|
||||
super.bind(holder, item)
|
||||
|
||||
val binding = holder.binding as ItemFilterProfileBinding
|
||||
binding.handle.setOnTouchListener { v, event ->
|
||||
if (event?.action == MotionEvent.ACTION_DOWN) {
|
||||
dragHelper.startDrag(holder)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItem(position).id
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_filter_profile
|
||||
}
|
||||
@@ -37,11 +37,11 @@ class GalleryAdapter(
|
||||
val view: ImageView
|
||||
if (detailView) {
|
||||
view = inflater.inflate(R.layout.gallery_item_fullscreen, parent, false) as ImageView
|
||||
view.setOnTouchListener { view, event ->
|
||||
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 || view.canScrollHorizontally(1) && view.canScrollHorizontally(
|
||||
if (event.pointerCount >= 2 || v.canScrollHorizontally(1) && v.canScrollHorizontally(
|
||||
-1
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
|
||||
class RateLimitInterceptor : Interceptor {
|
||||
private val rateLimiter = RateLimiter.create(3.0)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (request.url.host == "my.newmotion.com") {
|
||||
// limit requests sent to NewMotion to 3 per second
|
||||
rateLimiter.acquire(1)
|
||||
|
||||
var response: Response = chain.proceed(request)
|
||||
// 403 is how the NewMotion API indicates a rate limit error
|
||||
if (!response.isSuccessful && response.code == 403) {
|
||||
response.close()
|
||||
// wait & retry
|
||||
try {
|
||||
Thread.sleep(1000)
|
||||
} catch (e: InterruptedException) {
|
||||
}
|
||||
response = chain.proceed(request)
|
||||
}
|
||||
return response
|
||||
} else {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@ import java.io.IOException
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
operator fun <T> JSONArray.iterator(): Iterator<T> =
|
||||
(0 until length()).asSequence().map { get(it) as T }.iterator()
|
||||
(0 until length()).asSequence().map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
get(it) as T
|
||||
}.iterator()
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
suspend fun Call.await(): Response {
|
||||
|
||||
@@ -2,21 +2,27 @@ package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface AvailabilityDetector {
|
||||
suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
|
||||
protected val radius = 150 // max radius in meters
|
||||
|
||||
@@ -24,9 +30,9 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = client.newCall(request).await()
|
||||
|
||||
if (!response.isSuccessful) throw IOException(response.message())
|
||||
if (!response.isSuccessful) throw IOException(response.message)
|
||||
|
||||
val str = response.body()!!.string()
|
||||
val str = response.body!!.string()
|
||||
return str
|
||||
}
|
||||
|
||||
@@ -115,10 +121,16 @@ enum class ChargepointStatus {
|
||||
|
||||
class AvailabilityDetectorException(message: String) : Exception(message)
|
||||
|
||||
private val cookieManager = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.build()
|
||||
val availabilityDetectors = listOf(
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
|
||||
@@ -8,6 +8,7 @@ import okhttp3.OkHttpClient
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class ChargecloudAvailabilityDetector(
|
||||
client: OkHttpClient,
|
||||
private val operatorId: String
|
||||
|
||||
@@ -95,7 +95,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
// find nearest station to this position
|
||||
var markers =
|
||||
api.getMarkers(lng - coordRange, lng + coordRange, lat - coordRange, lat + coordRange)
|
||||
val nearest = markers.minBy { marker ->
|
||||
val nearest = markers.minByOrNull { marker ->
|
||||
distanceBetween(marker.coordinates.latitude, marker.coordinates.longitude, lat, lng)
|
||||
} ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ interface GoingElectricApi {
|
||||
@Query("plugs") plugs: String? = null,
|
||||
@Query("chargecards") chargecards: String? = null,
|
||||
@Query("networks") networks: String? = null,
|
||||
@Query("categories") categories: String? = null,
|
||||
@Query("startkey") startkey: Int? = null,
|
||||
@Query("open_twentyfourseven") open247: Boolean = false,
|
||||
@Query("barrierfree") barrierfree: Boolean = false,
|
||||
@@ -46,7 +47,7 @@ interface GoingElectricApi {
|
||||
suspend fun getChargeCards(): Response<ChargeCardList>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024; // 10MB
|
||||
private val cacheSize = 10L * 1024 * 1024 // 10MB
|
||||
|
||||
fun create(
|
||||
apikey: String,
|
||||
@@ -57,7 +58,7 @@ interface GoingElectricApi {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
var original = chain.request()
|
||||
val url = original.url().newBuilder().addQueryParameter("key", apikey).build()
|
||||
val url = original.url.newBuilder().addQueryParameter("key", apikey).build()
|
||||
original = original.newBuilder().url(url).build()
|
||||
chain.proceed(original)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.time.DayOfWeek
|
||||
@@ -77,7 +77,7 @@ data class ChargeLocation(
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.max() ?: 0.0
|
||||
.map { it.power }.maxOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,6 +277,7 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
|
||||
const val SUPERCHARGER = "Tesla Supercharger"
|
||||
const val CEE_BLAU = "CEE Blau"
|
||||
const val CEE_ROT = "CEE Rot"
|
||||
const val TESLA_ROADSTER_HPC = "Tesla HPC"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,14 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
findNavController().navigate(R.id.action_about_to_donateFragment)
|
||||
true
|
||||
}
|
||||
"twitter" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.twitter_url))
|
||||
true
|
||||
}
|
||||
"goingelectric" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.goingelectric_forum_url))
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@@ -20,6 +27,7 @@ import net.vonforst.evmap.databinding.FragmentFilterBinding
|
||||
import net.vonforst.evmap.viewmodel.FilterViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
|
||||
class FilterFragment : Fragment() {
|
||||
private lateinit var binding: FragmentFilterBinding
|
||||
private val vm: FilterViewModel by viewModels(factoryProducer = {
|
||||
@@ -35,13 +43,15 @@ class FilterFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -85,6 +95,58 @@ class FilterFragment : Fragment() {
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.menu_save_profile -> {
|
||||
val container = FrameLayout(requireContext())
|
||||
container.setPadding(
|
||||
(16 * resources.displayMetrics.density).toInt(), 0,
|
||||
(16 * resources.displayMetrics.density).toInt(), 0
|
||||
)
|
||||
val input = EditText(requireContext())
|
||||
input.isSingleLine = true
|
||||
vm.filterProfile.value?.let { profile ->
|
||||
input.setText(profile.name)
|
||||
}
|
||||
container.addView(input)
|
||||
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.save_as_profile)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setView(container)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(input.text.toString())
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
|
||||
}.show()
|
||||
|
||||
// move dialog to top
|
||||
val attrs = dialog.window?.attributes?.apply {
|
||||
gravity = Gravity.TOP
|
||||
}
|
||||
dialog.window?.attributes = attrs
|
||||
|
||||
// focus and show keyboard
|
||||
input.requestFocus()
|
||||
input.postDelayed({
|
||||
val imm =
|
||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 100)
|
||||
input.setOnEditorActionListener { _, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
val text = input.text
|
||||
if (text != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
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.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
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 net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.FilterProfilesAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFilterProfilesBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterProfileBinding
|
||||
import net.vonforst.evmap.viewmodel.FilterProfilesViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
|
||||
class FilterProfilesFragment : Fragment() {
|
||||
private lateinit var binding: FragmentFilterProfilesBinding
|
||||
private val vm: FilterProfilesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
FilterProfilesViewModel(requireActivity().application)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentFilterProfilesBinding.inflate(inflater, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
|
||||
val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
|
||||
) {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val fromPos = viewHolder.adapterPosition;
|
||||
val toPos = target.adapterPosition;
|
||||
|
||||
val list = vm.filterProfiles.value?.toMutableList()
|
||||
if (list != null) {
|
||||
val item = list[fromPos]
|
||||
list.removeAt(fromPos)
|
||||
list.add(toPos, item)
|
||||
list.forEachIndexed { index, filterProfile ->
|
||||
filterProfile.order = index
|
||||
}
|
||||
vm.reorderProfiles(list)
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
vm.delete(viewHolder.itemId)
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
if (viewHolder != null && actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
val binding =
|
||||
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFilterProfileBinding
|
||||
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 ItemFilterProfileBinding
|
||||
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 ItemFilterProfileBinding
|
||||
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 ItemFilterProfileBinding
|
||||
getDefaultUIUtil().onDraw(
|
||||
c, recyclerView, binding.foreground, dX, dY,
|
||||
actionState, isCurrentlyActive
|
||||
)
|
||||
} else {
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val adapter = FilterProfilesAdapter(touchHelper)
|
||||
binding.filterProfilesList.apply {
|
||||
this.adapter = adapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
context, LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
touchHelper.attachToRecyclerView(binding.filterProfilesList)
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class GalleryFragment : Fragment() {
|
||||
GalleryAdapter(requireContext(), detailView = true, pageToLoad = currentPosition) {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
binding.gallery.setPageTransformer { page, position ->
|
||||
binding.gallery.setPageTransformer { page, _ ->
|
||||
val v = page as TouchImageView
|
||||
currentPage = v
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -25,6 +26,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
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
|
||||
@@ -51,7 +53,7 @@ import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.android.synthetic.main.fragment_map.*
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
@@ -165,10 +167,22 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
setHasOptionsMenu(true)
|
||||
postponeEnterTransition()
|
||||
|
||||
binding.root.setOnApplyWindowInsetsListener { v, insets ->
|
||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.systemWindowInsetTop
|
||||
}
|
||||
|
||||
// margin of layers button
|
||||
val density = resources.displayMetrics.density
|
||||
// status bar height + toolbar height + margin
|
||||
val margin =
|
||||
insets.systemWindowInsetTop + (48 * density).toInt() + (24 * density).toInt()
|
||||
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -208,6 +222,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
super.onResume()
|
||||
val hostActivity = activity as? MapsActivity ?: return
|
||||
hostActivity.fragmentCallback = this
|
||||
vm.reloadPrefs()
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
@@ -269,6 +284,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.openUrl("https:${charger.url}edit/")
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -421,7 +443,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
markers.forEach { (m, c) ->
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
|
||||
getMarkerTint(c, vm.filteredConnectors.value),
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = getMarkerMulti(c, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -434,7 +459,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = true,
|
||||
fault = charger.faultReport != null
|
||||
fault = charger.faultReport != null,
|
||||
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
animator.animateMarkerBounce(marker)
|
||||
@@ -444,13 +470,31 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
if (m != marker) {
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
|
||||
getMarkerTint(c, vm.filteredConnectors.value),
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = getMarkerMulti(c, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMarkerMulti(charger: ChargeLocation, filteredConnectors: Set<String>?): Boolean {
|
||||
var chargepoints = charger.chargepointsMerged
|
||||
.filter { filteredConnectors?.contains(it.type) ?: true }
|
||||
if (charger.maxPower(filteredConnectors) >= 43) {
|
||||
// fast charger -> only count fast chargers
|
||||
chargepoints = chargepoints.filter { it.power >= 43 }
|
||||
}
|
||||
val connectors = chargepoints.map { it.type }.distinct().toSet()
|
||||
|
||||
// check if there is more than one plug for any connector type
|
||||
val chargepointsPerConnector =
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
|
||||
return chargepointsPerConnector.any { it > 1 }
|
||||
}
|
||||
|
||||
private fun updateFavoriteToggle() {
|
||||
val favs = vm.favorites.value ?: return
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
@@ -573,6 +617,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.mapPosition.value = MapPosition(
|
||||
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
|
||||
)
|
||||
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
|
||||
}
|
||||
map.setOnCameraMoveListener {
|
||||
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
|
||||
}
|
||||
map.setOnMarkerClickListener { marker ->
|
||||
when (marker) {
|
||||
@@ -678,6 +726,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
if (location != null) {
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
vm.location.value = latLng
|
||||
val camUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 13f)
|
||||
if (animate) {
|
||||
map.animateCamera(camUpdate)
|
||||
@@ -703,7 +752,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = charger == vm.chargerSparse.value,
|
||||
fault = charger.faultReport != null
|
||||
fault = charger.faultReport != null,
|
||||
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -720,7 +770,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
animator.animateMarkerDisappear(marker, tint, highlight, fault)
|
||||
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi)
|
||||
} else {
|
||||
animator.deleteMarker(marker)
|
||||
}
|
||||
@@ -734,6 +785,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
@@ -743,12 +795,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
0,
|
||||
255,
|
||||
highlight,
|
||||
fault
|
||||
fault,
|
||||
multi
|
||||
)
|
||||
)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
animator.animateMarkerAppear(marker, tint, highlight, fault)
|
||||
animator.animateMarkerAppear(marker, tint, highlight, fault, multi)
|
||||
markers[marker] = charger
|
||||
}
|
||||
}
|
||||
@@ -801,34 +854,94 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
})
|
||||
}
|
||||
filterView?.setOnClickListener {
|
||||
var profilesMap: MutableBiMap<Long, MenuItem> = HashBiMap()
|
||||
|
||||
val popup = PopupMenu(requireContext(), it, Gravity.END)
|
||||
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_edit_filters -> {
|
||||
lifecycleScope.launch {
|
||||
vm.copyFiltersToCustom()
|
||||
requireView().findNavController().navigate(
|
||||
R.id.action_map_to_filterFragment
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.menu_manage_filter_profiles -> {
|
||||
requireView().findNavController().navigate(
|
||||
R.id.action_map_to_filterFragment
|
||||
R.id.action_map_to_filterProfilesFragment
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.menu_filters_active -> {
|
||||
vm.filtersActive.value = !vm.filtersActive.value!!
|
||||
else -> {
|
||||
val profileId = profilesMap.inverse[it]
|
||||
if (profileId != null) {
|
||||
vm.filterStatus.value = profileId
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val checkItem = popup.menu.findItem(R.id.menu_filters_active)
|
||||
vm.filtersActive.observe(viewLifecycleOwner, Observer {
|
||||
checkItem.isChecked = it
|
||||
vm.filterProfiles.observe(viewLifecycleOwner, { profiles ->
|
||||
popup.menu.removeGroup(R.id.menu_group_filter_profiles)
|
||||
|
||||
val noFiltersItem = popup.menu.add(
|
||||
R.id.menu_group_filter_profiles,
|
||||
Menu.NONE, Menu.NONE, R.string.no_filters
|
||||
)
|
||||
profiles.forEach { profile ->
|
||||
val item = popup.menu.add(
|
||||
R.id.menu_group_filter_profiles,
|
||||
Menu.NONE,
|
||||
Menu.NONE,
|
||||
profile.name
|
||||
)
|
||||
profilesMap[profile.id] = item
|
||||
}
|
||||
val customItem = popup.menu.add(
|
||||
R.id.menu_group_filter_profiles,
|
||||
Menu.NONE, Menu.NONE, R.string.filter_custom
|
||||
)
|
||||
|
||||
profilesMap[FILTERS_DISABLED] = noFiltersItem
|
||||
profilesMap[FILTERS_CUSTOM] = customItem
|
||||
|
||||
popup.menu.setGroupCheckable(R.id.menu_group_filter_profiles, true, true);
|
||||
|
||||
val manageFiltersItem = popup.menu.findItem(R.id.menu_manage_filter_profiles)
|
||||
manageFiltersItem.isVisible = !profiles.isEmpty()
|
||||
|
||||
vm.filterStatus.observe(viewLifecycleOwner, Observer { id ->
|
||||
when (id) {
|
||||
FILTERS_DISABLED -> {
|
||||
customItem.isVisible = false
|
||||
noFiltersItem.isChecked = true
|
||||
}
|
||||
FILTERS_CUSTOM -> {
|
||||
customItem.isVisible = true
|
||||
customItem.isChecked = true
|
||||
}
|
||||
else -> {
|
||||
customItem.isVisible = false
|
||||
val item = profilesMap[id]
|
||||
if (item != null) {
|
||||
item.isChecked = true
|
||||
}
|
||||
// else unknown ID -> wait for filterProfiles to update
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
popup.show()
|
||||
}
|
||||
|
||||
filterView?.setOnLongClickListener {
|
||||
// enable/disable filters
|
||||
vm.filtersActive.value = !vm.filtersActive.value!!
|
||||
vm.toggleFilters()
|
||||
// haptic feedback
|
||||
filterView.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
@@ -836,7 +949,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
// show snackbar
|
||||
Snackbar.make(
|
||||
requireView(), if (vm.filtersActive.value!!) {
|
||||
requireView(), if (vm.filterStatus.value != FILTERS_DISABLED) {
|
||||
R.string.filters_activated
|
||||
} else {
|
||||
R.string.filters_deactivated
|
||||
@@ -858,7 +971,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
override fun getRootView(): View {
|
||||
return root
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private val exitElementCallback: SharedElementCallback = object : SharedElementCallback() {
|
||||
|
||||
@@ -7,10 +7,10 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.dialog_multi_select.*
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.databinding.DialogMultiSelectBinding
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.collections.HashSet
|
||||
@@ -37,13 +37,15 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
var okListener: ((Set<String>) -> Unit)? = null
|
||||
var cancelListener: (() -> Unit)? = null
|
||||
private lateinit var items: List<MultiSelectItem>
|
||||
private lateinit var binding: DialogMultiSelectBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.dialog_multi_select, container)
|
||||
): View {
|
||||
binding = DialogMultiSelectBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -65,41 +67,41 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
args.getSerializable("commonChoices") as HashSet<String>
|
||||
} else null
|
||||
|
||||
dialogTitle.text = title
|
||||
binding.dialogTitle.text = title
|
||||
val adapter = Adapter()
|
||||
list.adapter = adapter
|
||||
list.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value }
|
||||
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
|
||||
.sortedByDescending { commonChoices?.contains(it.key) == true }
|
||||
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
|
||||
adapter.submitList(items)
|
||||
|
||||
etSearch.doAfterTextChanged { text ->
|
||||
binding.etSearch.doAfterTextChanged { text ->
|
||||
adapter.submitList(search(items, text.toString()))
|
||||
}
|
||||
|
||||
btnCancel.setOnClickListener {
|
||||
binding.btnCancel.setOnClickListener {
|
||||
cancelListener?.let { listener ->
|
||||
listener()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
btnOK.setOnClickListener {
|
||||
binding.btnOK.setOnClickListener {
|
||||
okListener?.let { listener ->
|
||||
val result = items.filter { it.selected }.map { it.key }.toSet()
|
||||
listener(result)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
btnAll.setOnClickListener {
|
||||
binding.btnAll.setOnClickListener {
|
||||
items = items.map { MultiSelectItem(it.key, it.name, true) }
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
adapter.submitList(search(items, binding.etSearch.text.toString()))
|
||||
}
|
||||
btnNone.setOnClickListener {
|
||||
binding.btnNone.setOnClickListener {
|
||||
items = items.map { MultiSelectItem(it.key, it.name, false) }
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
adapter.submitList(search(items, binding.etSearch.text.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package net.vonforst.evmap.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
@Navigator.Name("chrome")
|
||||
class ChromeCustomTabsNavigator(
|
||||
private val context: Context
|
||||
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
|
||||
|
||||
override fun createDestination() =
|
||||
Destination(this)
|
||||
|
||||
override fun navigate(
|
||||
destination: Destination,
|
||||
args: Bundle?,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?
|
||||
): NavDestination? {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
intent.launchUrl(context, destination.url!!)
|
||||
return null // Do not add to the back stack, managed by Chrome Custom Tabs
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.vonforst.evmap.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
||||
class NavHostFragment : NavHostFragment() {
|
||||
override fun onCreateNavController(navController: NavController) {
|
||||
super.onCreateNavController(navController)
|
||||
navController.navigatorProvider.addNavigator(
|
||||
ChromeCustomTabsNavigator(
|
||||
requireContext()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.viewmodel.BooleanFilterValue
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
|
||||
import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
|
||||
@@ -19,15 +20,17 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
BooleanFilterValue::class,
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class,
|
||||
FilterProfile::class,
|
||||
Plug::class,
|
||||
Network::class,
|
||||
ChargeCard::class
|
||||
], version = 8
|
||||
], version = 10
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
abstract fun filterProfileDao(): FilterProfileDao
|
||||
abstract fun plugDao(): PlugDao
|
||||
abstract fun networkDao(): NetworkDao
|
||||
abstract fun chargeCardDao(): ChargeCardDao
|
||||
@@ -38,8 +41,14 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
|
||||
.addMigrations(
|
||||
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
|
||||
MIGRATION_7, MIGRATION_8
|
||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10
|
||||
)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
// create default filter profile
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`name`, `id`, `order`) VALUES ('FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -120,5 +129,46 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargecards` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_9 = object : Migration(8, 9) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.beginTransaction()
|
||||
try {
|
||||
// create filter profiles table
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `FilterProfile` (`name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_name` ON `FilterProfile` (`name`)")
|
||||
|
||||
// create default filter profile
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`name`, `id`) VALUES ('FILTERS_CUSTOM', $FILTERS_CUSTOM)")
|
||||
|
||||
// add profile column to existing filtervalue tables
|
||||
db.execSQL("CREATE TABLE `BooleanFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE `MultipleChoiceFilterValueNew` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
|
||||
|
||||
for (table in listOf(
|
||||
"BooleanFilterValue",
|
||||
"MultipleChoiceFilterValue",
|
||||
"SliderFilterValue"
|
||||
)) {
|
||||
db.execSQL("ALTER TABLE `$table` ADD COLUMN `profile` INTEGER NOT NULL DEFAULT $FILTERS_CUSTOM")
|
||||
db.execSQL("INSERT INTO `${table}New` SELECT * FROM `$table`")
|
||||
db.execSQL("DROP TABLE `$table`")
|
||||
db.execSQL("ALTER TABLE `${table}New` RENAME TO `$table`")
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val MIGRATION_10 = object : Migration(9, 10) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE `FilterProfile` ADD `order` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
|
||||
@Entity(
|
||||
indices = [Index(value = ["name"], unique = true)]
|
||||
)
|
||||
data class FilterProfile(
|
||||
val name: String,
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
var order: Int = 0
|
||||
) : Equatable
|
||||
|
||||
@Dao
|
||||
interface FilterProfileDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(profile: FilterProfile): Long
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg profiles: FilterProfile)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg profiles: FilterProfile)
|
||||
|
||||
@Query("SELECT * FROM filterProfile WHERE id != $FILTERS_CUSTOM ORDER BY `order` ASC, `name` ASC")
|
||||
fun getProfiles(): LiveData<List<FilterProfile>>
|
||||
|
||||
@Query("SELECT * FROM filterProfile WHERE name = :name")
|
||||
suspend fun getProfileByName(name: String): FilterProfile?
|
||||
|
||||
@Query("SELECT * FROM filterProfile WHERE id = :id")
|
||||
suspend fun getProfileById(id: Long): FilterProfile?
|
||||
}
|
||||
@@ -2,22 +2,20 @@ package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.viewmodel.BooleanFilterValue
|
||||
import net.vonforst.evmap.viewmodel.FilterValue
|
||||
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
|
||||
import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
@Query("SELECT * FROM booleanfiltervalue")
|
||||
protected abstract fun getBooleanFilterValues(): LiveData<List<BooleanFilterValue>>
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile")
|
||||
protected abstract fun getBooleanFilterValues(profile: Long): LiveData<List<BooleanFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue")
|
||||
protected abstract fun getMultipleChoiceFilterValues(): LiveData<List<MultipleChoiceFilterValue>>
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile")
|
||||
protected abstract fun getMultipleChoiceFilterValues(profile: Long): LiveData<List<MultipleChoiceFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue")
|
||||
protected abstract fun getSliderFilterValues(): LiveData<List<SliderFilterValue>>
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile")
|
||||
protected abstract fun getSliderFilterValues(profile: Long): LiveData<List<SliderFilterValue>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
|
||||
@@ -28,16 +26,29 @@ abstract class FilterValueDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: SliderFilterValue)
|
||||
|
||||
open fun getFilterValues(): LiveData<List<FilterValue>> =
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(),
|
||||
getMultipleChoiceFilterValues(),
|
||||
getSliderFilterValues()
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
value = sources.mapNotNull { it.value }.flatten()
|
||||
@Query("DELETE FROM booleanfiltervalue WHERE profile = :profile")
|
||||
protected abstract suspend fun deleteBooleanFilterValuesForProfile(profile: Long)
|
||||
|
||||
@Query("DELETE FROM multiplechoicefiltervalue WHERE profile = :profile")
|
||||
protected abstract suspend fun deleteMultipleChoiceFilterValuesForProfile(profile: Long)
|
||||
|
||||
@Query("DELETE FROM sliderfiltervalue WHERE profile = :profile")
|
||||
protected abstract suspend fun deleteSliderFilterValuesForProfile(profile: Long)
|
||||
|
||||
open fun getFilterValues(filterStatus: Long): LiveData<List<FilterValue>> =
|
||||
if (filterStatus == FILTERS_DISABLED) {
|
||||
MutableLiveData(emptyList())
|
||||
} else {
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(filterStatus),
|
||||
getMultipleChoiceFilterValues(filterStatus),
|
||||
getSliderFilterValues(filterStatus)
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
value = sources.mapNotNull { it.value }.flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,4 +63,12 @@ abstract class FilterValueDao {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun deleteFilterValuesForProfile(profile: Long) {
|
||||
deleteBooleanFilterValuesForProfile(profile)
|
||||
deleteMultipleChoiceFilterValuesForProfile(profile)
|
||||
deleteSliderFilterValuesForProfile(profile)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package net.vonforst.evmap.storage
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_DISABLED
|
||||
import java.time.Instant
|
||||
|
||||
class PreferenceDataSource(val context: Context) {
|
||||
@@ -32,12 +34,33 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var filtersActive: Boolean
|
||||
get() = sp.getBoolean("filters_active", true)
|
||||
/**
|
||||
* Stores the current filtering status, which is either the ID of a filter profile or
|
||||
* one of FILTERS_DISABLED, FILTERS_CUSTOM
|
||||
*/
|
||||
var filterStatus: Long
|
||||
get() =
|
||||
sp.getLong(
|
||||
"filter_status",
|
||||
// migration from versions before filter profiles were implemented
|
||||
if (sp.getBoolean("filters_active", true))
|
||||
FILTERS_CUSTOM else FILTERS_DISABLED
|
||||
)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("filters_active", value).apply()
|
||||
sp.edit().putLong("filter_status", value).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the last filter profile which was selected
|
||||
* (excluding FILTERS_DISABLED, but including FILTERS_CUSTOM)
|
||||
*/
|
||||
var lastFilterProfile: Long
|
||||
get() = sp.getLong("last_filter_profile", FILTERS_CUSTOM)
|
||||
set(value) {
|
||||
sp.edit().putLong("last_filter_profile", value).apply()
|
||||
}
|
||||
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
|
||||
|
||||
@@ -47,14 +47,17 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
|
||||
val scale: Int,
|
||||
val alpha: Int,
|
||||
val highlight: Boolean,
|
||||
val fault: Boolean
|
||||
val fault: Boolean,
|
||||
val multi: Boolean
|
||||
)
|
||||
|
||||
val cacheSize = 420; // 420 items: 21 sizes, 5 colors, highlight on/off, fault on/off
|
||||
val cacheSize = 840; // 840 items: 21 sizes, 5 colors, highlight, fault, multi on/off
|
||||
val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
|
||||
val oversize = 1.4f // increase to add padding for fault icon or scale > 1
|
||||
val icon = R.drawable.ic_map_marker_charging
|
||||
val multiIcon = R.drawable.ic_map_marker_charging_multiple
|
||||
val highlightIcon = R.drawable.ic_map_marker_highlight
|
||||
val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
|
||||
val faultIcon = R.drawable.ic_map_marker_fault
|
||||
|
||||
init {
|
||||
@@ -72,9 +75,11 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
|
||||
)
|
||||
for (fault in listOf(false, true)) {
|
||||
for (highlight in listOf(false, true)) {
|
||||
for (tint in tints) {
|
||||
for (scale in 0..20) {
|
||||
getBitmapDescriptor(tint, scale, 255, highlight, fault)
|
||||
for (multi in listOf(false, true)) {
|
||||
for (tint in tints) {
|
||||
for (scale in 0..20) {
|
||||
getBitmapDescriptor(tint, scale, 255, highlight, fault, multi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,9 +91,10 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
|
||||
scale: Int = 20,
|
||||
alpha: Int = 255,
|
||||
highlight: Boolean = false,
|
||||
fault: Boolean = false
|
||||
fault: Boolean = false,
|
||||
multi: Boolean = false
|
||||
): BitmapDescriptor? {
|
||||
val data = BitmapData(tint, scale, alpha, highlight, fault)
|
||||
val data = BitmapData(tint, scale, alpha, highlight, fault, multi)
|
||||
val cachedImg = cache[data]
|
||||
return if (cachedImg != null) {
|
||||
cachedImg
|
||||
@@ -101,13 +107,14 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
|
||||
}
|
||||
|
||||
private fun generateBitmap(data: BitmapData): Bitmap {
|
||||
val vd: Drawable = context.getDrawable(icon)!!
|
||||
val icon = if (data.multi) multiIcon else icon
|
||||
val vd: Drawable = ContextCompat.getDrawable(context, icon)!!
|
||||
|
||||
DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint));
|
||||
DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
val leftPadding = vd.intrinsicWidth * (oversize - 1) / 2
|
||||
val topPadding = vd.intrinsicWidth * (oversize - 1)
|
||||
val topPadding = vd.intrinsicHeight * (oversize - 1)
|
||||
vd.setBounds(
|
||||
leftPadding.toInt(), topPadding.toInt(),
|
||||
leftPadding.toInt() + vd.intrinsicWidth,
|
||||
@@ -132,7 +139,8 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
|
||||
vd.draw(canvas)
|
||||
|
||||
if (data.highlight) {
|
||||
val highlightDrawable = context.getDrawable(highlightIcon)!!
|
||||
val hIcon = if (data.multi) highlightIconMulti else highlightIcon
|
||||
val highlightDrawable = ContextCompat.getDrawable(context, hIcon)!!
|
||||
highlightDrawable.setBounds(
|
||||
leftPadding.toInt(), topPadding.toInt(),
|
||||
leftPadding.toInt() + vd.intrinsicWidth,
|
||||
@@ -143,7 +151,7 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
|
||||
}
|
||||
|
||||
if (data.fault) {
|
||||
val faultDrawable = context.getDrawable(faultIcon)!!
|
||||
val faultDrawable = ContextCompat.getDrawable(context, faultIcon)!!
|
||||
val faultSize = 0.75
|
||||
val faultShift = 0.25
|
||||
val base = vd.intrinsicWidth
|
||||
|
||||
@@ -28,7 +28,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
marker: Marker,
|
||||
tint: Int,
|
||||
highlight: Boolean,
|
||||
fault: Boolean
|
||||
fault: Boolean,
|
||||
multi: Boolean
|
||||
) {
|
||||
animatingMarkers[marker]?.let {
|
||||
it.cancel()
|
||||
@@ -45,7 +46,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
tint,
|
||||
scale = scale,
|
||||
highlight = highlight,
|
||||
fault = fault
|
||||
fault = fault,
|
||||
multi = multi
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -63,7 +65,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
marker: Marker,
|
||||
tint: Int,
|
||||
highlight: Boolean,
|
||||
fault: Boolean
|
||||
fault: Boolean,
|
||||
multi: Boolean
|
||||
) {
|
||||
animatingMarkers[marker]?.let {
|
||||
it.cancel()
|
||||
@@ -80,7 +83,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
tint,
|
||||
scale = scale,
|
||||
highlight = highlight,
|
||||
fault = fault
|
||||
fault = fault,
|
||||
multi = multi
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
fun wrap(context: Context, language: String): ContextWrapper {
|
||||
val sysConfig: Configuration = context.applicationContext.resources.configuration
|
||||
val appConfig: Configuration = context.resources.configuration
|
||||
var ctx = context
|
||||
|
||||
|
||||
if (language == "" || language == "default") {
|
||||
// set default locale
|
||||
@@ -37,8 +35,7 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx = context.createConfigurationContext(appConfig)
|
||||
return LocaleContextWrapper(ctx)
|
||||
return LocaleContextWrapper(context.createConfigurationContext(appConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
) / 1000
|
||||
}
|
||||
})
|
||||
}
|
||||
}?.sortedBy { it.distance }
|
||||
}
|
||||
addSource(favorites, callback)
|
||||
addSource(location, callback)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
class FilterProfilesViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles()
|
||||
}
|
||||
|
||||
fun delete(itemId: Long) {
|
||||
viewModelScope.launch {
|
||||
val profile = db.filterProfileDao().getProfileById(itemId)
|
||||
profile?.let { db.filterProfileDao().delete(it) }
|
||||
if (prefs.filterStatus == profile?.id) {
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderProfiles(list: List<FilterProfile>) {
|
||||
viewModelScope.launch {
|
||||
db.filterProfileDao().update(*list.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,11 @@ package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.CASCADE
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
@@ -22,7 +21,7 @@ val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300,
|
||||
internal fun mapPower(i: Int) = powerSteps[i]
|
||||
internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
.minBy { it.first }?.second ?: 0
|
||||
.minByOrNull { it.first }?.second ?: 0
|
||||
|
||||
internal fun getFilters(
|
||||
application: Application,
|
||||
@@ -40,7 +39,8 @@ internal fun getFilters(
|
||||
Chargepoint.CHADEMO to application.getString(R.string.plug_chademo),
|
||||
Chargepoint.SUPERCHARGER to application.getString(R.string.plug_supercharger),
|
||||
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
|
||||
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot)
|
||||
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot),
|
||||
Chargepoint.TESLA_ROADSTER_HPC to application.getString(R.string.plug_roadster_hpc)
|
||||
)
|
||||
listOf(plugs, networks, chargeCards).forEach { source ->
|
||||
addSource(source) { _ ->
|
||||
@@ -62,6 +62,34 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
}?.toMap() ?: return
|
||||
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
|
||||
val chargecardMap = chargeCards.value?.map { it.id.toString() to it.name }?.toMap() ?: return
|
||||
val categoryMap = mapOf(
|
||||
"Autohaus" to application.getString(R.string.category_car_dealership),
|
||||
"Autobahnraststätte" to application.getString(R.string.category_service_on_motorway),
|
||||
"Autohof" to application.getString(R.string.category_service_off_motorway),
|
||||
"Bahnhof" to application.getString(R.string.category_railway_station),
|
||||
"Behörde" to application.getString(R.string.category_public_authorities),
|
||||
"Campingplatz" to application.getString(R.string.category_camping),
|
||||
"Einkaufszentrum" to application.getString(R.string.category_shopping_mall),
|
||||
"Ferienwohnung" to application.getString(R.string.category_holiday_home),
|
||||
"Flughafen" to application.getString(R.string.category_airport),
|
||||
"Freizeitpark" to application.getString(R.string.category_amusement_park),
|
||||
"Hotel" to application.getString(R.string.category_hotel),
|
||||
"Kino" to application.getString(R.string.category_cinema),
|
||||
"Kirche" to application.getString(R.string.category_church),
|
||||
"Krankenhaus" to application.getString(R.string.category_hospital),
|
||||
"Museum" to application.getString(R.string.category_museum),
|
||||
"Parkhaus" to application.getString(R.string.category_parking_multi),
|
||||
"Parkplatz" to application.getString(R.string.category_parking),
|
||||
"Privater Ladepunkt" to application.getString(R.string.category_private_charger),
|
||||
"Rastplatz" to application.getString(R.string.category_rest_area),
|
||||
"Restaurant" to application.getString(R.string.category_restaurant),
|
||||
"Schwimmbad" to application.getString(R.string.category_swimming_pool),
|
||||
"Supermarkt" to application.getString(R.string.category_supermarket),
|
||||
"Tankstelle" to application.getString(R.string.category_petrol_station),
|
||||
"Tiefgarage" to application.getString(R.string.category_parking_underground),
|
||||
"Tierpark" to application.getString(R.string.category_zoo),
|
||||
"Wohnmobilstellplatz" to application.getString(R.string.category_caravan_site)
|
||||
)
|
||||
value = listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
@@ -89,6 +117,11 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
application.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.categories), "categories",
|
||||
categoryMap,
|
||||
manyChoices = true
|
||||
),
|
||||
BooleanFilter(application.getString(R.string.filter_barrierfree), "barrierfree"),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_chargecards), "chargecards",
|
||||
@@ -101,25 +134,17 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>,
|
||||
active: LiveData<Boolean>? = null
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
listOf(filters, filterValues, active).forEach {
|
||||
if (it == null) return@forEach
|
||||
listOf(filters, filterValues).forEach {
|
||||
addSource(it) {
|
||||
val filters = filters.value ?: return@addSource
|
||||
value = if (active != null && !active.value!!) {
|
||||
filters.map { filter ->
|
||||
FilterWithValue(filter, filter.defaultValue())
|
||||
}
|
||||
} else {
|
||||
val values = filterValues.value ?: return@addSource
|
||||
filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
val f = filters.value ?: return@addSource
|
||||
val values = filterValues.value ?: return@addSource
|
||||
value = f.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,17 +170,59 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
}
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues()
|
||||
db.filterValueDao().getFilterValues(FILTERS_CUSTOM)
|
||||
}
|
||||
|
||||
val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
private val filterStatus: LiveData<Long> by lazy {
|
||||
MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus
|
||||
}
|
||||
}
|
||||
|
||||
val filterProfile: LiveData<FilterProfile> by lazy {
|
||||
MediatorLiveData<FilterProfile>().apply {
|
||||
addSource(filterStatus) { id ->
|
||||
when (id) {
|
||||
FILTERS_CUSTOM, FILTERS_DISABLED -> value = null
|
||||
else -> viewModelScope.launch {
|
||||
value = db.filterProfileDao().getProfileById(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveFilterValues() {
|
||||
filtersWithValue.value?.forEach {
|
||||
db.filterValueDao().insert(it.value)
|
||||
val value = it.value
|
||||
value.profile = FILTERS_CUSTOM
|
||||
db.filterValueDao().insert(value)
|
||||
}
|
||||
|
||||
// set selected profile
|
||||
prefs.filterStatus = FILTERS_CUSTOM
|
||||
}
|
||||
|
||||
suspend fun saveAsProfile(name: String) {
|
||||
// get or create profile
|
||||
var profileId = db.filterProfileDao().getProfileByName(name)?.id
|
||||
if (profileId == null) {
|
||||
profileId = db.filterProfileDao().insert(FilterProfile(name))
|
||||
}
|
||||
|
||||
// save filter values
|
||||
filtersWithValue.value?.forEach {
|
||||
val value = it.value
|
||||
value.profile = profileId
|
||||
db.filterValueDao().insert(value)
|
||||
}
|
||||
|
||||
// set selected profile
|
||||
prefs.filterStatus = profileId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,17 +265,34 @@ data class SliderFilter(
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
var profile: Long = FILTERS_CUSTOM
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class BooleanFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue()
|
||||
|
||||
@Entity
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class MultipleChoiceFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
override val key: String,
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue() {
|
||||
@@ -222,12 +306,30 @@ data class MultipleChoiceFilterValue(
|
||||
!other.all && values == other.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
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class SliderFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
const val FILTERS_DISABLED = -2L
|
||||
const val FILTERS_CUSTOM = -1L
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.distanceBetween
|
||||
import net.vonforst.evmap.api.goingelectric.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
@@ -46,7 +47,16 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
MutableLiveData<MapPosition>()
|
||||
}
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues()
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
var source: LiveData<List<FilterValue>>? = null
|
||||
addSource(filterStatus) { status ->
|
||||
source?.let { removeSource(it) }
|
||||
source = db.filterValueDao().getFilterValues(status)
|
||||
addSource(source!!) { result ->
|
||||
value = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
@@ -60,7 +70,11 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
private val filters = getFilters(application, plugs, networks, chargeCards)
|
||||
|
||||
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
filtersWithValue(filters, filterValues, filtersActive)
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles()
|
||||
}
|
||||
|
||||
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
|
||||
@@ -128,6 +142,28 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
}
|
||||
val chargerDistance: MediatorLiveData<Double> by lazy {
|
||||
MediatorLiveData<Double>().apply {
|
||||
val callback = { _: Any? ->
|
||||
val loc = location.value
|
||||
val charger = chargerSparse.value
|
||||
value = if (loc != null && charger != null && myLocationEnabled.value == true) {
|
||||
distanceBetween(
|
||||
loc.latitude,
|
||||
loc.longitude,
|
||||
charger.coordinates.lat,
|
||||
charger.coordinates.lng
|
||||
) / 1000
|
||||
} else null
|
||||
}
|
||||
addSource(chargerSparse, callback)
|
||||
addSource(location, callback)
|
||||
addSource(myLocationEnabled, callback)
|
||||
}
|
||||
}
|
||||
val location: MutableLiveData<LatLng> by lazy {
|
||||
MutableLiveData<LatLng>()
|
||||
}
|
||||
val availability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
|
||||
addSource(chargerSparse) { charger ->
|
||||
@@ -170,15 +206,38 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
|
||||
val filtersActive: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = prefs.filtersActive
|
||||
val filterStatus: MutableLiveData<Long> by lazy {
|
||||
MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus
|
||||
observeForever {
|
||||
prefs.filtersActive = it
|
||||
prefs.filterStatus = it
|
||||
if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadPrefs() {
|
||||
filterStatus.value = prefs.filterStatus
|
||||
}
|
||||
|
||||
fun toggleFilters() {
|
||||
if (filterStatus.value == FILTERS_DISABLED) {
|
||||
filterStatus.value = prefs.lastFilterProfile
|
||||
} else {
|
||||
filterStatus.value = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun copyFiltersToCustom() {
|
||||
if (filterStatus.value == FILTERS_CUSTOM) return
|
||||
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM)
|
||||
filterValues.value?.forEach {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
db.filterValueDao().insert(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMapType(type: AnyMap.Type) {
|
||||
mapType.value = type
|
||||
}
|
||||
@@ -257,6 +316,13 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = getMultipleChoiceValue(filters, "categories")
|
||||
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
|
||||
// no categories chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val categories = formatMultipleChoice(categoriesVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors <= 1
|
||||
@@ -285,6 +351,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
|
||||
object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
|
||||
|
||||
10
app/src/main/res/drawable/ic_add.xml
Normal file
10
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_delete.xml
Normal file
10
app/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="44.11976dp"
|
||||
android:viewportWidth="233.8"
|
||||
android:viewportHeight="368.4">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.8"
|
||||
android:pathData="M143.2,109.4l-19.7,33.8l0,38.1l43.4,-74.4l-22.2,0z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.8"
|
||||
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector android:height="44.11976dp"
|
||||
android:viewportHeight="368.4"
|
||||
android:viewportWidth="233.8"
|
||||
android:width="28dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6C6.6,68.6 17.6,49.1 32.8,34C53.3,14 81.1,1.8 109.8,0z" />
|
||||
<path
|
||||
android:fillColor="#B5B5B5"
|
||||
android:pathData="M143.2,109.4l-19.7,33.8l0,38.1l43.4,-74.4l-22.2,0z" />
|
||||
<path
|
||||
android:fillColor="#B5B5B5"
|
||||
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_reorder.xml
Normal file
10
app/src/main/res/drawable/ic_reorder.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,15h18v-2L3,13v2zM3,19h18v-2L3,17v2zM3,11h18L21,9L3,9v2zM3,5v2h18L21,5L3,5z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_save.xml
Normal file
10
app/src/main/res/drawable/ic_save.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
|
||||
</vector>
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:name="net.vonforst.evmap.navigation.NavHostFragment"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
name="charger"
|
||||
type="Resource<ChargeLocation>" />
|
||||
|
||||
<variable
|
||||
name="distance"
|
||||
type="Double" />
|
||||
|
||||
<variable
|
||||
name="availability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
@@ -80,15 +84,31 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:text="Beispielstraße 10, 12345 Berlin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView27"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="end"
|
||||
android:maxLines="1"
|
||||
android:minWidth="50dp"
|
||||
android:text="@{@string/distance_format(distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView3"
|
||||
tools:text="10 km" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{charger.data.formatChargepoints()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView27"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="2x Typ 2 22 kW" />
|
||||
@@ -139,6 +159,8 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{charger.data.amenities}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
app:goneUnless="@{charger.data.amenities != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
@@ -167,6 +189,8 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{charger.data.generalInformation}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
app:goneUnless="@{charger.data.generalInformation != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
|
||||
68
app/src/main/res/layout/fragment_filter_profiles.xml
Normal file
68
app/src/main/res/layout/fragment_filter_profiles.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterProfilesViewModel" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="FilterProfilesViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/filter_profiles_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/filter_profiles_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:data="@{vm.filterProfiles}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_filter_boolean" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView19"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/filterprofiles_empty_state"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:goneUnless="@{vm.filterProfiles.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -24,6 +24,20 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@id/fab_locate"
|
||||
app:layout_anchorGravity="start|center_vertical"
|
||||
android:layout_gravity="start|center_vertical">
|
||||
|
||||
<com.github.pengrad.mapscaleview.MapScaleView
|
||||
android:id="@+id/scaleView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
@@ -118,7 +132,7 @@
|
||||
app:behavior_peekHeight="@dimen/peek_height"
|
||||
app:bottomsheetbehavior_defaultState="stateHidden"
|
||||
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
|
||||
tools:bottomsheetbehavior_defaultState="stateHidden">
|
||||
tools:bottomsheetbehavior_defaultState="stateCollapsed">
|
||||
|
||||
<include
|
||||
android:id="@+id/detail_view"
|
||||
@@ -126,7 +140,8 @@
|
||||
app:charger="@{vm.charger}"
|
||||
app:availability="@{vm.availability}"
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}" />
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}"
|
||||
app:distance="@{vm.chargerDistance}" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
73
app/src/main/res/layout/item_filter_profile.xml
Normal file
73
app/src/main/res/layout/item_filter_profile.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.storage.FilterProfile" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="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="?listPreferredItemHeight"
|
||||
android:background="?android:colorBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{item.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/handle"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.511"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/handle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/reorder"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_reorder"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
</layout>
|
||||
@@ -2,6 +2,12 @@
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_edit"
|
||||
android:icon="@drawable/ic_edit"
|
||||
android:title="@string/edit_on_goingelectric"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_share"
|
||||
android:icon="@drawable/ic_share"
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
android:icon="@drawable/ic_fav"
|
||||
android:title="@string/menu_favs" />
|
||||
</group>
|
||||
<group
|
||||
android:id="@+id/nav_group_links"
|
||||
android:checkableBehavior="none">
|
||||
<item
|
||||
android:id="@+id/report_new_charger"
|
||||
android:icon="@drawable/ic_add"
|
||||
android:title="@string/menu_report_new_charger" />
|
||||
</group>
|
||||
<group
|
||||
android:id="@+id/nav_group_settings"
|
||||
android:checkableBehavior="single">
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/menu_save_profile"
|
||||
android:title="@string/menu_save_profile"
|
||||
android:icon="@drawable/ic_save"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_apply"
|
||||
android:title="@string/menu_filter"
|
||||
android:title="@string/menu_apply"
|
||||
android:icon="@drawable/ic_check"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -1,11 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/menu_filters_active"
|
||||
android:title="@string/menu_filters_active"
|
||||
android:checkable="true"
|
||||
android:checked="true" />
|
||||
<group
|
||||
android:checkableBehavior="single"
|
||||
android:id="@+id/menu_group_filter_profiles">
|
||||
|
||||
</group>
|
||||
<item
|
||||
android:id="@+id/menu_edit_filters"
|
||||
android:title="@string/menu_edit_filters" />
|
||||
android:title="@string/menu_edit_filters"
|
||||
android:menuCategory="secondary" />
|
||||
<item
|
||||
android:id="@+id/menu_manage_filter_profiles"
|
||||
android:title="@string/menu_manage_filter_profiles"
|
||||
android:menuCategory="secondary" />
|
||||
</menu>
|
||||
@@ -24,6 +24,13 @@
|
||||
app:enterAnim="@anim/fragment_fade_enter"
|
||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
||||
app:popExitAnim="@anim/fragment_fade_exit" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterProfilesFragment"
|
||||
app:destination="@id/filter_profiles"
|
||||
app:exitAnim="@anim/fragment_fade_exit"
|
||||
app:enterAnim="@anim/fragment_fade_enter"
|
||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
||||
app:popExitAnim="@anim/fragment_fade_exit" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/about"
|
||||
@@ -58,9 +65,17 @@
|
||||
android:name="net.vonforst.evmap.fragment.FilterFragment"
|
||||
android:label="@string/menu_filter"
|
||||
tools:layout="@layout/fragment_filter" />
|
||||
<fragment
|
||||
android:id="@+id/filter_profiles"
|
||||
android:name="net.vonforst.evmap.fragment.FilterProfilesFragment"
|
||||
android:label="@string/menu_manage_filter_profiles"
|
||||
tools:layout="@layout/fragment_filter_profiles" />
|
||||
<fragment
|
||||
android:id="@+id/donate"
|
||||
android:name="net.vonforst.evmap.fragment.DonateFragment"
|
||||
android:label="@string/donate"
|
||||
tools:layout="@layout/fragment_donate" />
|
||||
<chrome
|
||||
android:id="@+id/report_new_charger"
|
||||
app:url="@string/report_new_charger_url" />
|
||||
</navigation>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EV Map</string>
|
||||
<string name="title_activity_maps">EV Map</string>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Anschlüsse</string>
|
||||
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
|
||||
<string name="address">Adresse</string>
|
||||
@@ -62,6 +62,7 @@
|
||||
<string name="plug_supercharger">Tesla Supercharger</string>
|
||||
<string name="plug_cee_blau">CEE Blau</string>
|
||||
<string name="plug_cee_rot">CEE Rot</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="all">alle</string>
|
||||
<string name="none">keine</string>
|
||||
<string name="show_more">mehr…</string>
|
||||
@@ -80,7 +81,8 @@
|
||||
<string name="menu_filters_active">Filter aktiv</string>
|
||||
<string name="filters_activated">Filter aktiviert</string>
|
||||
<string name="filters_deactivated">Filter deaktiviert</string>
|
||||
<string name="menu_edit_filters">Filter bearbeiten…</string>
|
||||
<string name="menu_edit_filters">Filter bearbeiten</string>
|
||||
<string name="menu_manage_filter_profiles">Filterprofile verwalten</string>
|
||||
<string name="go_to_chargeprice">Preisvergleich</string>
|
||||
<string name="fault_report">Störungsmeldung</string>
|
||||
<string name="fault_report_date">Störungsmeldung (Letztes Update: %s)</string>
|
||||
@@ -103,6 +105,47 @@
|
||||
<string name="charge_cards">Ladetarife</string>
|
||||
<string name="and_n_others">und %d weitere</string>
|
||||
<string name="pref_map_provider">Kartenanbieter</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="goingelectric_forum">Forenthread bei GoingElectric.de</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="menu_report_new_charger">Ladesäule melden</string>
|
||||
<string name="edit_on_goingelectric">bei GoingElectric.de bearbeiten</string>
|
||||
<string name="categories">Kategorien</string>
|
||||
<string name="category_car_dealership">Autohaus</string>
|
||||
<string name="category_service_on_motorway">Autobahnraststätte</string>
|
||||
<string name="category_service_off_motorway">Autohof</string>
|
||||
<string name="category_railway_station">Bahnhof</string>
|
||||
<string name="category_public_authorities">Behörde</string>
|
||||
<string name="category_camping">Campingplatz</string>
|
||||
<string name="category_shopping_mall">Einkaufszentrum</string>
|
||||
<string name="category_holiday_home">Ferienwohnung</string>
|
||||
<string name="category_airport">Flughafen</string>
|
||||
<string name="category_amusement_park">Freizeitpark</string>
|
||||
<string name="category_hotel">Hotel</string>
|
||||
<string name="category_cinema">Kino</string>
|
||||
<string name="category_church">Kirche</string>
|
||||
<string name="category_hospital">Krankenhaus</string>
|
||||
<string name="category_museum">Museum</string>
|
||||
<string name="category_parking_multi">Parkhaus</string>
|
||||
<string name="category_parking">Parkplatz</string>
|
||||
<string name="category_private_charger">Privater Ladepunkt</string>
|
||||
<string name="category_rest_area">Rastplatz</string>
|
||||
<string name="category_restaurant">Restaurant</string>
|
||||
<string name="category_swimming_pool">Schwimmbad</string>
|
||||
<string name="category_supermarket">Supermarkt</string>
|
||||
<string name="category_petrol_station">Tankstelle</string>
|
||||
<string name="category_parking_underground">Tiefgarage</string>
|
||||
<string name="category_zoo">Tierpark</string>
|
||||
<string name="category_caravan_site">Wohnmobilstellplatz</string>
|
||||
<string name="menu_apply">Filter anwenden</string>
|
||||
<string name="menu_save_profile">Als Profil speichern</string>
|
||||
<string name="no_filters">Keine Filter</string>
|
||||
<string name="filter_custom">Verändertes Filterprofil</string>
|
||||
<string name="reorder">Reihenfolge ändern</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="save_as_profile">Als Profil speichern</string>
|
||||
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
|
||||
<string name="filterprofiles_empty_state">Du hast noch keine Filterprofile gespeichert.</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d kompatibler Ladetarif</item>
|
||||
<item quantity="other">%d kompatible Ladetarife</item>
|
||||
|
||||
6
app/src/main/res/values/attrs.xml
Normal file
6
app/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="ChromeCustomTabsNavigator">
|
||||
<attr name="url" format="reference" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -13,4 +13,5 @@
|
||||
<color name="unavailable">#f44336</color>
|
||||
<color name="unknown">#9e9e9e</color>
|
||||
<color name="status_bar_scrim">#C3000000</color>
|
||||
<color name="delete_red">#f44336</color>
|
||||
</resources>
|
||||
|
||||
@@ -4,4 +4,8 @@
|
||||
<string name="github_link">https://github.com/johan12345/EVMap</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/faq.html</string>
|
||||
<string name="twitter_handle">\@ev_map</string>
|
||||
<string name="twitter_url">https://twitter.com/ev_map</string>
|
||||
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
|
||||
<string name="report_new_charger_url">https://www.goingelectric.de/stromtankstellen/new/</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">EV Map</string>
|
||||
<string name="title_activity_maps">EV Map</string>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Connectors</string>
|
||||
<string name="no_maps_app_found">No navigation app found</string>
|
||||
<string name="address">Address</string>
|
||||
@@ -61,6 +61,7 @@
|
||||
<string name="plug_supercharger">Tesla Supercharger</string>
|
||||
<string name="plug_cee_blau">CEE Blue</string>
|
||||
<string name="plug_cee_rot">CEE Red</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="all">all</string>
|
||||
<string name="none">none</string>
|
||||
<string name="show_more">more…</string>
|
||||
@@ -79,7 +80,8 @@
|
||||
<string name="menu_filters_active">Filters active</string>
|
||||
<string name="filters_activated">Filters activated</string>
|
||||
<string name="filters_deactivated">Filters deactivated</string>
|
||||
<string name="menu_edit_filters">Edit filters…</string>
|
||||
<string name="menu_edit_filters">Edit filters</string>
|
||||
<string name="menu_manage_filter_profiles">Manage filter profiles</string>
|
||||
<string name="go_to_chargeprice">Compare prices</string>
|
||||
<string name="fault_report">Fault report</string>
|
||||
<string name="fault_report_date">Fault report (last update: %s)</string>
|
||||
@@ -102,6 +104,47 @@
|
||||
<string name="charge_cards">Payment methods</string>
|
||||
<string name="and_n_others">and %d others</string>
|
||||
<string name="pref_map_provider">Map provider</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="goingelectric_forum">Forum thread at GoingElectric.de</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="menu_report_new_charger">Report new charger</string>
|
||||
<string name="edit_on_goingelectric">edit on GoingElectric.de</string>
|
||||
<string name="categories">Categories</string>
|
||||
<string name="category_car_dealership">Car Dealership</string>
|
||||
<string name="category_service_on_motorway">Service area (on motorway)</string>
|
||||
<string name="category_service_off_motorway">Service area (off motorway)</string>
|
||||
<string name="category_railway_station">Railway station</string>
|
||||
<string name="category_public_authorities">Public authorities</string>
|
||||
<string name="category_camping">Camping site</string>
|
||||
<string name="category_shopping_mall">Shopping mall</string>
|
||||
<string name="category_holiday_home">Holiday home</string>
|
||||
<string name="category_airport">Airport</string>
|
||||
<string name="category_amusement_park">Amusement park</string>
|
||||
<string name="category_hotel">Hotel</string>
|
||||
<string name="category_cinema">Cinema</string>
|
||||
<string name="category_church">Church</string>
|
||||
<string name="category_hospital">Hospital</string>
|
||||
<string name="category_museum">Museum</string>
|
||||
<string name="category_parking_multi">Multi-storey car park</string>
|
||||
<string name="category_parking">Car park</string>
|
||||
<string name="category_private_charger">Private charger</string>
|
||||
<string name="category_rest_area">Rest area</string>
|
||||
<string name="category_restaurant">Restaurant</string>
|
||||
<string name="category_swimming_pool">Swimming pool</string>
|
||||
<string name="category_supermarket">Supermarket</string>
|
||||
<string name="category_petrol_station">Petrol station</string>
|
||||
<string name="category_parking_underground">Underground car park</string>
|
||||
<string name="category_zoo">Zoo</string>
|
||||
<string name="category_caravan_site">Caravan site</string>
|
||||
<string name="menu_apply">Apply filters</string>
|
||||
<string name="menu_save_profile">Save as profile</string>
|
||||
<string name="no_filters">No filters</string>
|
||||
<string name="filter_custom">Modified filter</string>
|
||||
<string name="reorder">reorder</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="save_as_profile">Save as profile</string>
|
||||
<string name="save_profile_enter_name">Enter the name of the filter profile:</string>
|
||||
<string name="filterprofiles_empty_state">You have not yet saved any filter profiles.</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d compatible payment method</item>
|
||||
<item quantity="other">%d compatible payment methods</item>
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
<Preference
|
||||
android:key="donate"
|
||||
android:title="@string/donate" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/contact">
|
||||
|
||||
<Preference
|
||||
android:key="twitter"
|
||||
android:title="@string/twitter"
|
||||
android:summary="@string/twitter_handle" />
|
||||
|
||||
<Preference
|
||||
android:key="goingelectric"
|
||||
android:title="@string/goingelectric_forum" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ class NewMotionAvailabilityDetectorTest {
|
||||
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val segments = request.requestUrl.pathSegments()
|
||||
val segments = request.requestUrl!!.pathSegments
|
||||
val urlHead = segments.subList(0, 2).joinToString("/")
|
||||
when (urlHead) {
|
||||
"ge/chargepoints" -> {
|
||||
val id = request.requestUrl.queryParameter("ge_id")
|
||||
val id = request.requestUrl!!.queryParameter("ge_id")
|
||||
return okResponse("/chargers/$id.json")
|
||||
}
|
||||
"nm/markers" -> {
|
||||
|
||||
@@ -24,18 +24,18 @@ class GoingElectricApiTest {
|
||||
|
||||
webServer.dispatcher = object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val segments = request.requestUrl.pathSegments()
|
||||
val segments = request.requestUrl!!.pathSegments
|
||||
val urlHead = segments.subList(0, 2).joinToString("/")
|
||||
when (urlHead) {
|
||||
"ge/chargepoints" -> {
|
||||
val id = request.requestUrl.queryParameter("ge_id")
|
||||
val id = request.requestUrl!!.queryParameter("ge_id")
|
||||
if (id != null) {
|
||||
return okResponse("/chargers/$id.json")
|
||||
} else {
|
||||
val freeparking =
|
||||
request.requestUrl.queryParameter("freeparking")!!.toBoolean()
|
||||
request.requestUrl!!.queryParameter("freeparking")!!.toBoolean()
|
||||
val freecharging =
|
||||
request.requestUrl.queryParameter("freecharging")!!.toBoolean()
|
||||
request.requestUrl!!.queryParameter("freecharging")!!.toBoolean()
|
||||
return if (freeparking && freecharging) {
|
||||
okResponse("/chargers/list-empty.json")
|
||||
} else if (freecharging) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.72'
|
||||
ext.kotlin_version = '1.4.21'
|
||||
ext.about_libs_version = '8.1.1'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
|
||||
|
||||
6
fastlane/metadata/android/de-DE/changelogs/25.txt
Normal file
6
fastlane/metadata/android/de-DE/changelogs/25.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Verbesserungen:
|
||||
- Sortierung in Filterdialogen unabhängig von Groß- und Kleinschreibung
|
||||
- Vertikale Position der Marker auf der Karte korrigiert
|
||||
- Neues Icon für Ladestationen mit mehr als einem Anschluss (bei Schnelladern: mehr als ein schnelladefähiger Anschluss)
|
||||
- "Über EVMap": Links zu Twitter-Account @ev_map und Thread im GoingElectric-Forum hinzugefügt
|
||||
- Button "Ladesäule melden" im Hauptmenü hinzugefügt
|
||||
6
fastlane/metadata/android/en-US/changelogs/25.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/25.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Improvements:
|
||||
- Sorting of filter dialogs is case-insensitive
|
||||
- Vertical positions of map markers fixed
|
||||
- New icon for chargers with more than one connector (for fast chargers: more than one fast-charging connector)
|
||||
- "About EVMap": Added links to Twitter account @ev_map and GoingElectric.de forum thread
|
||||
- Added button "rerport new charger" in main menu
|
||||
2
fastlane/metadata/android/en-US/changelogs/26.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/26.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Crash fixed
|
||||
Fix wrong position of layers button on devices with camera cutout
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sun Mar 29 18:50:22 CEST 2020
|
||||
#Wed Dec 23 14:54:49 CET 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
rootProject.name='EV Map'
|
||||
rootProject.name='EVMap'
|
||||
include ':app'
|
||||
|
||||
Reference in New Issue
Block a user