mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c59ec9e895 | ||
|
|
fd288e653a | ||
|
|
dec7e6bdc9 | ||
|
|
6276bef1e0 | ||
|
|
5c72ee718b | ||
|
|
810338ba38 | ||
|
|
53a9af8226 | ||
|
|
e5dd0e19ab | ||
|
|
78421ec79f | ||
|
|
12329f82b3 | ||
|
|
528790b570 | ||
|
|
89af31c684 | ||
|
|
6c8efed96a | ||
|
|
6b8e87a6c7 |
11
.travis.yml
11
.travis.yml
@@ -8,8 +8,11 @@ env:
|
||||
global:
|
||||
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=
|
||||
- secure: Hoko50vP+Mwm/O4CWvPvjMxd1gGhi+Bultjyy1WpludSmZFCfKz45Mj1EqzeYk6MeMLvGOEkSLB5wjdXdgJ9j5gEOF5K34k4vESJA7+DqDO3I7Xw9cnWgOXdFqB0qGHar0TVP3Dfg7ZcRgtmeX6t2uoELFLiS9GvTnbZXk4PCUybd979Xi8XHjEQV7+3EZbSjtsI4GFeIK1rrjd0I+UM88zYrWnz1KhdCjWvQb4iZjo+ib6NmGGEMqR7jJPRZz3KB01Y+n5h21qq9/Nv31zQIqt2B5nRxy3vBvvqKapgprIk+hVOpNnBU8w89uWUU6tYUeFk0t7z1TYWjgaBrMmGCM+aKkQ2q5F/ygNzDwB+KkpJx709Yhf0ZX8g2yvkdz+Ok7moYuvmrOPOf4E/U3BlfZxbGtRD2bGYbDgHLFYlTn6v5J2kJHDJAz31yvF5jvJejDaPp2IBVfoMRy7ZJFmGUNHGd9Se6bRwxS++AoobP5WDrBiUXNe2KKDMs3e7vbaO+hLbZ9XHpjeWIJGUfvtTee8EHZqF/8A3ju53V4/R0ehlOv2UZbpYNqcmwrsy9/R4pgMfDkG3Q054LmYrxD8DIC9b8excVMwWRP5aQ1TqZnxO2B1/vJU87RcnGl3jekeHTdHXbRq9BMV4dAdPfB9X3nGIi2GgV0iTBk+24xccOc0=
|
||||
before_install:
|
||||
- openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
|
||||
script:
|
||||
- "./gradlew lintDebug testDebugUnitTest"
|
||||
- "./gradlew assembleRelease"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
@@ -18,3 +21,11 @@ cache:
|
||||
- "$HOME/.gradle/caches/"
|
||||
- "$HOME/.gradle/wrapper/"
|
||||
- "$HOME/.android/build-cache"
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: B+V5Fz8k9HbpecyMjpJuLr8aVBrdwtDBDkQh4YQ8nu+Da4AiYwEJZseWXhOWs+oms0gNen9bBxsakQQKu7GKYDs8gIXZZtANWsc0gse8xo+cYT7NqEM3jP6mM3ytAv7VNRX3N2cdL7xazELK3/5+mghfORAAdXXYKUFGG5eTKoML8zgdPVN8E9QFqiusLXqoKhxOMCSE4NS+Di7CGlUmnidRTWg6yxhE085zljmYv2owS0NRbr5a4/zW6Z9xZPALGAqsOvIvpZHuOC2s0eMJWMmYGkK/Ws/LAVxfj4U+YkFp9hlZC0zEg/JoS19Gf57QmEu+vsoQ3uOBYBFv9NPI/R9kVH6o0hcOxId3J0u+ewSGWuceGLRpizXuMxKIvLTS5j6GWkxdSieWjwh/OuVB+ciAHNM31B7GP4FWnfz0ZaEVxI/tPenNipZdl9oXdyyBQQ00vPlYp0jT80XhaMh5rDwWMUPaEjRafvymcNyqZ0iVOr0rq1CbdT92STMSmA1U3/rmhtCMD5IGD0b+gQl+VpPKe1QXViYftVxCGL+s4ke4DUZD7HR20fGs8zu61Elnwci1HufbetKFL5TmxoKSLkWFSkzrtBaJnEruZIxhNUMkUL2UPynaOcPNzLoumjHXrUb3m3s0yE4OFelmJ6mJfXswP38sS8kj3wB7R/gC4rw=
|
||||
file: app/build/outputs/apk/release/app-release.apk
|
||||
on:
|
||||
repo: johan12345/EVMap
|
||||
skip_cleanup: 'true'
|
||||
|
||||
46
README.md
46
README.md
@@ -1,8 +1,48 @@
|
||||
EVMap [](https://travis-ci.org/johan12345/EVMap)
|
||||
=====
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/appicon.svg?sanitize=true" width=250 alt="Logo"/>
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/appicon_cropped.svg?sanitize=true" width=80 alt="Logo"/>
|
||||
|
||||
Android app to access the goingelectric.de electric vehicle charging station directory
|
||||
Android app to access the goingelectric.de electric vehicle charging station directory.
|
||||
|
||||
Work in progress
|
||||
<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>
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- [Material Design](https://material.io/)
|
||||
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) directory
|
||||
- Realtime availability information (beta)
|
||||
- Search places
|
||||
- Favorites list, also with availability information
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/01_main.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/02_detail.png" width=250 alt="Screenshot 2"/>
|
||||
|
||||
Development setup
|
||||
-----------------
|
||||
|
||||
The App is developed using Android Studio.
|
||||
|
||||
For testing the app, you need to obtain API Keys for the
|
||||
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/)
|
||||
as well as for [Google APIs](https://console.developers.google.com/)
|
||||
("Maps SDK for Android" and "Places API" need to be activated). These APIs need to be put into the
|
||||
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
|
||||
following content:
|
||||
|
||||
```xml
|
||||
<resources>
|
||||
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">
|
||||
insert your Google Maps key here
|
||||
</string>
|
||||
<string name="goingelectric_key" translatable="false">
|
||||
insert your GoingElectric key here
|
||||
</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
BIN
_ci/keystore.jks.enc
Normal file
BIN
_ci/keystore.jks.enc
Normal file
Binary file not shown.
40
_img/appicon_cropped.svg
Normal file
40
_img/appicon_cropped.svg
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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 75.4 104" style="enable-background:new 0 0 75.4 104;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFB300;}
|
||||
.st1{fill:#90A4AE;}
|
||||
.st2{fill:#546E7A;}
|
||||
.st3{fill:#00E676;}
|
||||
.st4{fill:#FFFFFF;fill-opacity:0.2;}
|
||||
.st5{fill:#3E2723;fill-opacity:0.2;}
|
||||
.st6{opacity:0.45;enable-background:new ;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0"
|
||||
d="M9.2,76.5L7.3,59.9l-2.9,0.3l1.9,16.6L9.2,76.5z M19.5,75.3l-1.9-16.6L14.7,59l1.9,16.6L19.5,75.3z" />
|
||||
<path class="st1" d="M24.9,97.9c-0.9,1.1-1.6,1.8-1.7,1.9c-2.6,2.1-4.7,2.7-6.4,1.9c-3-1.5-2.8-7.1-2.7-7.7l2.1,0.1
|
||||
c-0.1,1.6,0.2,5,1.6,5.7c0.8,0.4,2.2-0.1,4-1.6l0,0c0,0,5.8-5.8,4.6-10.4c-1.4-5.5,5-13.4,7.1-16.1l0.3-0.3l1.7,1.3l-0.3,0.4
|
||||
c-6.5,8-7.2,12.1-6.7,14.2C29.5,91.3,26.8,95.6,24.9,97.9z" />
|
||||
<path class="st1" d="M2.8,76.3l0.8,6.8l6.3,4.2l8.5-0.9l5.2-5.5l-0.8-6.8L2.8,76.3z" />
|
||||
<g>
|
||||
<path class="st2"
|
||||
d="M18.3,86.4l-8.5,0.9l1.8,7.5l6.7-0.8V86.4L18.3,86.4z M24.4,68.4l0.7,6.2L0.7,77.4L0,71.2L24.4,68.4z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M43.5,0C26,0,11.8,14.2,11.8,31.7c0,23.9,26.7,36.4,29.9,70.5c0.1,1,0.9,1.7,1.9,1.7s1.8-0.7,1.9-1.7
|
||||
c3.2-34.1,29.9-46.6,29.9-70.5C75.2,14.1,61,0,43.5,0z" />
|
||||
<path class="st4" d="M43.5,0.7c17.4,0,31.5,14,31.7,31.3c0-0.1,0-0.2,0-0.3C75.2,14.2,61,0,43.5,0S11.8,14.1,11.8,31.7
|
||||
c0,0.1,0,0.2,0,0.3C12,14.7,26.1,0.7,43.5,0.7L43.5,0.7z" />
|
||||
<path class="st5" d="M45.4,101.4c-0.1,1-0.9,1.7-1.9,1.7s-1.8-0.7-1.9-1.7c-3.1-34-29.6-46.5-29.8-70.1c0,0.2,0,0.3,0,0.5
|
||||
c0,23.9,26.7,36.4,29.9,70.5c0.1,1,0.9,1.7,1.9,1.7s1.8-0.7,1.9-1.7c3.2-34.1,29.9-46.6,29.9-70.5c0-0.2,0-0.3,0-0.5
|
||||
C75,54.9,48.5,67.4,45.4,101.4L45.4,101.4z" />
|
||||
</g>
|
||||
<path class="st6"
|
||||
d="M36.2,16.2v19.2h5.2v15.7l12.2-21h-7l7-14C53.7,16.2,36.2,16.2,36.2,16.2z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -13,19 +13,33 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 3
|
||||
versionName "0.0.3"
|
||||
versionCode 4
|
||||
versionName "0.0.4"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
def isRunningOnTravis = System.getenv("CI") == "true"
|
||||
if (isRunningOnTravis) {
|
||||
// configure keystore
|
||||
signingConfigs.release.storeFile = file("../_ci/keystore.jks")
|
||||
signingConfigs.release.storePassword = System.getenv("keystore_password")
|
||||
signingConfigs.release.keyAlias = System.getenv("keystore_alias")
|
||||
signingConfigs.release.keyPassword = System.getenv("keystore_alias_password")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
@@ -82,7 +96,7 @@ dependencies {
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
|
||||
// navigation library
|
||||
def nav_version = "2.3.0-alpha04"
|
||||
def nav_version = "2.3.0-alpha05"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const val REQUEST_LOCATION_PERMISSION = 1
|
||||
class MapsActivity : AppCompatActivity() {
|
||||
interface FragmentCallback {
|
||||
fun getRootView(): View
|
||||
fun goBack(): Boolean
|
||||
}
|
||||
|
||||
private var reenterState: Bundle? = null
|
||||
@@ -48,11 +47,6 @@ class MapsActivity : AppCompatActivity() {
|
||||
prefs = PreferenceDataSource(this)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val didGoBack = fragmentCallback?.goBack() ?: false
|
||||
if (!didGoBack) super.onBackPressed()
|
||||
}
|
||||
|
||||
fun navigateTo(charger: ChargeLocation) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val coord = charger.coordinates
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.adapter
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
@@ -13,7 +14,8 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
@@ -132,4 +134,60 @@ class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
}
|
||||
|
||||
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
val itemids = mutableMapOf<String, Long>()
|
||||
var maxId = 0L
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> R.layout.item_filter_boolean
|
||||
is SliderFilter -> R.layout.item_filter_slider
|
||||
}
|
||||
|
||||
override fun bind(
|
||||
holder: ViewHolder<FilterWithValue<FilterValue>>,
|
||||
item: FilterWithValue<FilterValue>
|
||||
) {
|
||||
super.bind(holder, item)
|
||||
when (item.value) {
|
||||
is SliderFilterValue -> {
|
||||
val binding = holder.binding as ItemFilterSliderBinding
|
||||
binding.progress = item.value.value
|
||||
binding.seekBar.setOnSeekBarChangeListener(object :
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
item.value.value = progress
|
||||
binding.progress = progress
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val key = getItem(position).filter.key
|
||||
var value = itemids[key]
|
||||
if (value == null) {
|
||||
maxId++
|
||||
value = maxId
|
||||
itemids[key] = maxId
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class ChargecloudAvailabilityDetector(
|
||||
if (chargepoint == null) {
|
||||
// find corresponding chargepoint from goingelectric to get correct power
|
||||
val geChargepoint =
|
||||
getCorrespondingChargepoint(location.chargepoints, type, power)
|
||||
getCorrespondingChargepoint(location.chargepointsMerged, type, power)
|
||||
?: throw AvailabilityDetectorException(
|
||||
"Chargepoints from chargecloud API and goingelectric do not match."
|
||||
)
|
||||
@@ -70,7 +70,7 @@ class ChargecloudAvailabilityDetector(
|
||||
|
||||
|
||||
|
||||
if (chargepointStatus.keys == location.chargepoints.toSet()) {
|
||||
if (chargepointStatus.keys == location.chargepointsMerged.toSet()) {
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"chargecloud.de"
|
||||
|
||||
@@ -145,6 +145,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
"Domestic" -> Chargepoint.SCHUKO
|
||||
"Type2Combo" -> Chargepoint.CCS
|
||||
"TepcoCHAdeMO" -> Chargepoint.CHADEMO
|
||||
"Unspecified" -> "unspecified"
|
||||
else -> throw IllegalArgumentException("unrecognized type ${connector.connectorType}")
|
||||
}
|
||||
val status = when (statusStr) {
|
||||
@@ -158,7 +159,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
nmStatus.put(id, status)
|
||||
}
|
||||
|
||||
val match = matchChargepoints(nmConnectors, location.chargepoints)
|
||||
val match = matchChargepoints(nmConnectors, location.chargepointsMerged)
|
||||
val chargepointStatus = match.mapValues { entry ->
|
||||
entry.value.map { nmStatus[it]!! }
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ interface GoingElectricApi {
|
||||
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
|
||||
@Query("clustering") clustering: Boolean,
|
||||
@Query("zoom") zoom: Float,
|
||||
@Query("cluster_distance") clusterDistance: Int
|
||||
@Query("cluster_distance") clusterDistance: Int,
|
||||
@Query("freecharging") freecharging: Boolean,
|
||||
@Query("freeparking") freeparking: Boolean,
|
||||
@Query("min_power") minPower: Int
|
||||
): Call<ChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
|
||||
@@ -53,8 +53,25 @@ data class ChargeLocation(
|
||||
return chargepoints.map { it.power }.max() ?: 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
* This occurs e.g. for Type2 sockets and plugs, which are distinct on the GE website, but not
|
||||
* separable in the API
|
||||
*/
|
||||
val chargepointsMerged: List<Chargepoint>
|
||||
get() {
|
||||
val variants = chargepoints.distinctBy { it.power to it.type }
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatChargepoints(): String {
|
||||
return chargepoints.map {
|
||||
return chargepointsMerged.map {
|
||||
"${it.count} × ${it.type} ${it.formatPower()}"
|
||||
}.joinToString(" · ")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,7 +12,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
@@ -77,17 +77,15 @@ class FavoritesFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
vm.favorites.observe(viewLifecycleOwner, Observer {
|
||||
print(it.toString())
|
||||
})
|
||||
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? ->
|
||||
if (location != null) {
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
app/src/main/java/net/vonforst/evmap/fragment/FilterFragment.kt
Normal file
108
app/src/main/java/net/vonforst/evmap/fragment/FilterFragment.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.FiltersAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFilterBinding
|
||||
import net.vonforst.evmap.ui.exitCircularReveal
|
||||
import net.vonforst.evmap.ui.startCircularReveal
|
||||
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 = {
|
||||
viewModelFactory {
|
||||
FilterViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
|
||||
OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
exitAfterTransition()
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
binding.filtersList.apply {
|
||||
adapter = FiltersAdapter()
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
context, LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
view.startCircularReveal()
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
exitAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.filter, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_apply -> {
|
||||
lifecycleScope.launch {
|
||||
vm.saveFilterValues()
|
||||
}
|
||||
exitAfterTransition()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun exitAfterTransition() {
|
||||
view?.exitCircularReveal {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,23 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.adapter.galleryTransitionName
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import net.vonforst.evmap.databinding.FragmentGalleryBinding
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
|
||||
|
||||
class GalleryFragment : Fragment(), MapsActivity.FragmentCallback {
|
||||
class GalleryFragment : Fragment() {
|
||||
companion object {
|
||||
private const val EXTRA_POSITION = "position"
|
||||
private const val EXTRA_PHOTOS = "photos"
|
||||
@@ -42,6 +42,20 @@ class GalleryFragment : Fragment(), MapsActivity.FragmentCallback {
|
||||
private var currentPage: TouchImageView? = null
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
|
||||
private val backPressedCallback = object :
|
||||
OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val image = currentPage
|
||||
if (image != null && image.currentZoom !in 0.95f..1.05f) {
|
||||
image.setZoomAnimated(1f, 0.5f, 0.5f)
|
||||
} else {
|
||||
isReturning = true
|
||||
galleryVm.galleryPosition.value = currentPosition
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -88,6 +102,11 @@ class GalleryFragment : Fragment(), MapsActivity.FragmentCallback {
|
||||
postponeEnterTransition();
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
backPressedCallback
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -103,39 +122,9 @@ class GalleryFragment : Fragment(), MapsActivity.FragmentCallback {
|
||||
) {
|
||||
if (isReturning) {
|
||||
val currentPage = currentPage ?: return
|
||||
val index = binding.gallery.currentItem
|
||||
|
||||
if (startingPosition != currentPosition) {
|
||||
names.clear()
|
||||
names.add(galleryTransitionName(index))
|
||||
|
||||
sharedElements.clear()
|
||||
sharedElements[galleryTransitionName(index)] = currentPage
|
||||
}
|
||||
sharedElements[names[0]] = currentPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRootView(): View {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun goBack(): Boolean {
|
||||
val image = currentPage
|
||||
if (image != null && image.currentZoom !in 0.95f..1.05f) {
|
||||
image.setZoomAnimated(1f, 0.5f, 0.5f)
|
||||
return true
|
||||
} else {
|
||||
isReturning = true
|
||||
galleryVm.galleryPosition.value = currentPosition
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val hostActivity = activity as? MapsActivity ?: return
|
||||
hostActivity.fragmentCallback = this
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@@ -24,6 +26,7 @@ import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.maps.CameraUpdateFactory
|
||||
@@ -34,8 +37,9 @@ import com.google.android.gms.maps.model.*
|
||||
import com.google.android.libraries.places.api.model.Place
|
||||
import com.google.android.libraries.places.widget.Autocomplete
|
||||
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import kotlinx.android.synthetic.main.fragment_map.*
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
@@ -81,6 +85,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
private lateinit var animator: MarkerAnimator
|
||||
private lateinit var favToggle: MenuItem
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
val state = bottomSheetBehavior.state
|
||||
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
} else if (state == STATE_COLLAPSED) {
|
||||
vm.chargerSparse.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -106,6 +120,15 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
insets
|
||||
}
|
||||
|
||||
setExitSharedElementCallback(exitElementCallback)
|
||||
exitTransition = TransitionInflater.from(requireContext())
|
||||
.inflateTransition(R.transition.map_exit_transition)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
backPressedCallback
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -178,7 +201,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
imm.toggleSoftInput(0, 0)
|
||||
}
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
}
|
||||
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
@@ -217,18 +240,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
vm.bottomSheetState.value = newState
|
||||
backPressedCallback.isEnabled = newState != STATE_HIDDEN
|
||||
}
|
||||
})
|
||||
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
|
||||
if (it != null) {
|
||||
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
}
|
||||
binding.fabDirections.show()
|
||||
detailAppBarBehavior.setToolbarTitle(it.name)
|
||||
updateFavoriteToggle()
|
||||
} else {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
bottomSheetBehavior.state = STATE_HIDDEN
|
||||
}
|
||||
})
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer {
|
||||
@@ -456,12 +480,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_filter -> {
|
||||
Snackbar.make(root, R.string.not_implemented, Snackbar.LENGTH_SHORT).show()
|
||||
return true
|
||||
requireView().findNavController().navigate(
|
||||
R.id.action_map_to_filterFragment
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,21 +504,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
override fun goBack(): Boolean {
|
||||
return if (bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED &&
|
||||
bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
) {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
true
|
||||
} else if (bottomSheetBehavior.state == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED) {
|
||||
vm.chargerSparse.value = null
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRootView(): View {
|
||||
return root
|
||||
}
|
||||
|
||||
private val exitElementCallback: SharedElementCallback = object : SharedElementCallback() {
|
||||
override fun onMapSharedElements(
|
||||
names: MutableList<String>,
|
||||
sharedElements: MutableMap<String, View>
|
||||
) {
|
||||
// Locate the ViewHolder for the clicked position.
|
||||
val position = galleryVm.galleryPosition.value ?: return
|
||||
|
||||
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
|
||||
if (vh?.itemView == null) return
|
||||
|
||||
// Map the first shared element name to the child ImageView.
|
||||
sharedElements[names[0]] = vh.itemView
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,46 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.viewmodel.BooleanFilterValue
|
||||
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
|
||||
import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
|
||||
@Database(entities = [ChargeLocation::class], version = 1)
|
||||
@Database(
|
||||
entities = [
|
||||
ChargeLocation::class,
|
||||
BooleanFilterValue::class,
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class
|
||||
], version = 2
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
|
||||
companion object {
|
||||
private lateinit var context: Context
|
||||
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db").build()
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
|
||||
.addMigrations(MIGRATION_2)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): AppDatabase {
|
||||
this.context = context.applicationContext
|
||||
return database
|
||||
}
|
||||
|
||||
private val MIGRATION_2 = object : Migration(1, 2) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// SQL for creating tables copied from build/generated/source/kapt/debug/net/vonforst/evmap/storage/AppDatbase_Impl
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `BooleanFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `MultipleChoiceFilterValue` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, PRIMARY KEY(`key`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
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
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
@Query("SELECT * FROM booleanfiltervalue")
|
||||
protected abstract fun getBooleanFilterValues(): LiveData<List<BooleanFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue")
|
||||
protected abstract fun getMultipleChoiceFilterValues(): LiveData<List<MultipleChoiceFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue")
|
||||
protected abstract fun getSliderFilterValues(): LiveData<List<SliderFilterValue>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: MultipleChoiceFilterValue)
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun insert(vararg values: FilterValue) {
|
||||
values.forEach {
|
||||
when (it) {
|
||||
is BooleanFilterValue -> insert(it)
|
||||
is MultipleChoiceFilterValue -> insert(it)
|
||||
is SliderFilterValue -> insert(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ class Converters {
|
||||
val type = Types.newParameterizedType(List::class.java, ChargerPhoto::class.java)
|
||||
moshi.adapter<List<ChargerPhoto>>(type)
|
||||
}
|
||||
private val stringSetAdapter by lazy {
|
||||
val type = Types.newParameterizedType(Set::class.java, String::class.java)
|
||||
moshi.adapter<Set<String>>(type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromChargepointList(value: List<Chargepoint>?): String {
|
||||
@@ -49,4 +53,14 @@ class Converters {
|
||||
LocalTime.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringSet(value: Set<String>?): String {
|
||||
return stringSetAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toStringSet(value: String): Set<String>? {
|
||||
return stringSetAdapter.fromJson(value)
|
||||
}
|
||||
}
|
||||
55
app/src/main/java/net/vonforst/evmap/ui/AnimationUtils.kt
Normal file
55
app/src/main/java/net/vonforst/evmap/ui/AnimationUtils.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import kotlin.math.hypot
|
||||
|
||||
fun View.startCircularReveal() {
|
||||
addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
|
||||
override fun onLayoutChange(
|
||||
v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int,
|
||||
oldRight: Int, oldBottom: Int
|
||||
) {
|
||||
v.removeOnLayoutChangeListener(this)
|
||||
val cx = v.right
|
||||
val cy = v.top
|
||||
val radius = hypot(right.toDouble(), bottom.toDouble()).toInt()
|
||||
ViewAnimationUtils.createCircularReveal(v, cx, cy, 0f, radius.toFloat()).apply {
|
||||
interpolator = DecelerateInterpolator(2f)
|
||||
duration = 1000
|
||||
start()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun View.exitCircularReveal(block: () -> Unit) {
|
||||
val startRadius = hypot(this.width.toDouble(), this.height.toDouble())
|
||||
ViewAnimationUtils.createCircularReveal(this, this.width, 0, startRadius.toFloat(), 0f).apply {
|
||||
duration = 350
|
||||
interpolator = DecelerateInterpolator(1f)
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
visibility = View.INVISIBLE
|
||||
block()
|
||||
super.onAnimationEnd(animation)
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the position of the current [View]'s center in the screen
|
||||
*/
|
||||
fun View.findLocationOfCenterOnTheScreen(): IntArray {
|
||||
val positions = intArrayOf(0, 0)
|
||||
getLocationInWindow(positions)
|
||||
// Get the center of the view
|
||||
positions[0] = positions[0] + width / 2
|
||||
positions[1] = positions[1] + height / 2
|
||||
return positions
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
fun getFilters(application: Application): List<Filter<FilterValue>> {
|
||||
return listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
SliderFilter(application.getString(R.string.filter_min_power), "min_power", 350)
|
||||
)
|
||||
}
|
||||
|
||||
class FilterViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
private val filters = getFilters(application)
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues()
|
||||
}
|
||||
|
||||
val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
addSource(filterValues) { values ->
|
||||
value = if (values != null) {
|
||||
filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveFilterValues() {
|
||||
filtersWithValue.value?.forEach {
|
||||
db.filterValueDao().insert(it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Filter<out T : FilterValue> : Equatable {
|
||||
abstract val name: String
|
||||
abstract val key: String
|
||||
abstract val valueClass: KClass<out T>
|
||||
abstract fun defaultValue(): T
|
||||
}
|
||||
|
||||
data class BooleanFilter(override val name: String, override val key: String) :
|
||||
Filter<BooleanFilterValue>() {
|
||||
override val valueClass: KClass<BooleanFilterValue> = BooleanFilterValue::class
|
||||
override fun defaultValue() = BooleanFilterValue(key, false)
|
||||
}
|
||||
|
||||
data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>
|
||||
) : Filter<MultipleChoiceFilterValue>() {
|
||||
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, emptySet(), true)
|
||||
}
|
||||
|
||||
data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, 0)
|
||||
}
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class BooleanFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue()
|
||||
|
||||
@Entity
|
||||
data class MultipleChoiceFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
var values: Set<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue()
|
||||
|
||||
@Entity
|
||||
data class SliderFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
@@ -14,6 +14,7 @@ import net.vonforst.evmap.storage.AppDatabase
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
|
||||
@@ -28,12 +29,37 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val mapPosition: MutableLiveData<MapPosition> by lazy {
|
||||
MutableLiveData<MapPosition>()
|
||||
}
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues()
|
||||
}
|
||||
private val filters = getFilters(application)
|
||||
|
||||
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
addSource(filterValues) {
|
||||
val values = filterValues.value
|
||||
if (values != null) {
|
||||
value = filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val chargepoints: MediatorLiveData<Resource<List<ChargepointListItem>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargepointListItem>>>()
|
||||
.apply {
|
||||
value = Resource.loading(emptyList())
|
||||
addSource(mapPosition) {
|
||||
mapPosition.value?.let { pos -> loadChargepoints(pos) }
|
||||
listOf(mapPosition, filtersWithValue).forEach {
|
||||
addSource(it) {
|
||||
val pos = mapPosition.value ?: return@addSource
|
||||
val filters = filtersWithValue.value ?: return@addSource
|
||||
loadChargepoints(pos, filters)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,16 +123,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargepoints(mapPosition: MapPosition) {
|
||||
private fun loadChargepoints(
|
||||
mapPosition: MapPosition,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
) {
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
val bounds = mapPosition.bounds
|
||||
val zoom = mapPosition.zoom
|
||||
api.getChargepoints(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude,
|
||||
clustering = zoom < 13, zoom = zoom,
|
||||
clusterDistance = 70
|
||||
).enqueue(object : Callback<ChargepointList> {
|
||||
getChargepointsWithFilters(bounds, zoom, filters).enqueue(object :
|
||||
Callback<ChargepointList> {
|
||||
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
|
||||
chargepoints.value = Resource.error(t.message, chargepoints.value?.data)
|
||||
t.printStackTrace()
|
||||
@@ -126,6 +151,27 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
})
|
||||
}
|
||||
|
||||
private fun getChargepointsWithFilters(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
): Call<ChargepointList> {
|
||||
val freecharging =
|
||||
(filters.find { it.value.key == "freecharging" }!!.value as BooleanFilterValue).value
|
||||
val freeparking =
|
||||
(filters.find { it.value.key == "freeparking" }!!.value as BooleanFilterValue).value
|
||||
val minPower =
|
||||
(filters.find { it.value.key == "min_power" }!!.value as SliderFilterValue).value
|
||||
|
||||
return api.getChargepoints(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude,
|
||||
clustering = zoom < 13, zoom = zoom,
|
||||
clusterDistance = 70, freecharging = freecharging, minPower = minPower,
|
||||
freeparking = freeparking
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
|
||||
10
app/src/main/res/drawable/ic_check.xml
Normal file
10
app/src/main/res/drawable/ic_check.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="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
</vector>
|
||||
@@ -83,7 +83,7 @@
|
||||
android:id="@+id/connectors"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:data="@{DataBindingAdaptersKt.chargepointWithAvailability(charger.data.chargepoints, availability.data.status)}"
|
||||
app:data="@{DataBindingAdaptersKt.chargepointWithAvailability(charger.data.chargepointsMerged, availability.data.status)}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView7"
|
||||
|
||||
41
app/src/main/res/layout/fragment_filter.xml
Normal file
41
app/src/main/res/layout/fragment_filter.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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.FilterViewModel" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="FilterViewModel" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/filters_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:data="@{vm.filtersWithValue}"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_filter_boolean" />
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
52
app/src/main/res/layout/item_filter_boolean.xml
Normal file
52
app/src/main/res/layout/item_filter_boolean.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.BooleanFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.BooleanFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="FilterWithValue<BooleanFilterValue>" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView17"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@{item.filter.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/switch1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Free charging" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switch1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:checked="@={item.value.value}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
68
app/src/main/res/layout/item_filter_slider.xml
Normal file
68
app/src/main/res/layout/item_filter_slider.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.SliderFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.SliderFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="FilterWithValue<SliderFilterValue>" />
|
||||
|
||||
<variable
|
||||
name="progress"
|
||||
type="int" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView17"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@{item.filter.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Minimum power" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekBar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:max="@{((SliderFilter) item.filter).max}"
|
||||
android:progress="@={item.value.value}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView18"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView17" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView18"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{progress + " kW"}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/seekBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/seekBar"
|
||||
tools:text="0 kW" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
9
app/src/main/res/menu/filter.xml
Normal file
9
app/src/main/res/menu/filter.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?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_apply"
|
||||
android:title="@string/menu_filter"
|
||||
android:icon="@drawable/ic_check"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -17,6 +17,13 @@
|
||||
app:exitAnim="@anim/fragment_fade_exit"
|
||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
||||
app:popExitAnim="@anim/fragment_fade_exit" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
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"
|
||||
@@ -38,4 +45,9 @@
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites" />
|
||||
<fragment
|
||||
android:id="@+id/filter"
|
||||
android:name="net.vonforst.evmap.fragment.FilterFragment"
|
||||
android:label="@string/menu_filter"
|
||||
tools:layout="@layout/fragment_filter" />
|
||||
</navigation>
|
||||
12
app/src/main/res/transition/map_exit_transition.xml
Normal file
12
app/src/main/res/transition/map_exit_transition.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="375"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:startDelay="25">
|
||||
<fade>
|
||||
<targets>
|
||||
<target android:targetId="@id/bottom_sheet" />
|
||||
<target android:targetId="@id/fab_directions" />
|
||||
</targets>
|
||||
</fade>
|
||||
</transitionSet>
|
||||
@@ -44,4 +44,7 @@
|
||||
<string name="pref_navigate_use_maps_off">Navigationsbutton startet Karten-App mit Position der Ladesäule</string>
|
||||
<string name="coordinates">Koordinaten</string>
|
||||
<string name="share">Teilen</string>
|
||||
<string name="filter_free">Nur kostenlose Ladesäulen</string>
|
||||
<string name="filter_min_power">Minimale Leistung</string>
|
||||
<string name="filter_free_parking">Nur Ladesäulen mit kostenlosem Parkplatz</string>
|
||||
</resources>
|
||||
@@ -43,4 +43,7 @@
|
||||
<string name="pref_navigate_use_maps_off">Navigation button launches maps app with charger location</string>
|
||||
<string name="coordinates">Coordinates</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="filter_free">Only free chargers</string>
|
||||
<string name="filter_min_power">Minimum power</string>
|
||||
<string name="filter_free_parking">Only chargers with free parking</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,4 +17,9 @@
|
||||
|
||||
<style name="AppTheme" parent="AppTheme.Base" />
|
||||
|
||||
<style name="FullScreenDialogStyle" parent="AppTheme">
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user