mirror of
https://github.com/ev-map/EVMap.git
synced 2026-01-01 11:37:46 -05:00
Compare commits
14 Commits
1.0.0-beta
...
1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23387ae371 | ||
|
|
25f466b6d7 | ||
|
|
6692b21bf9 | ||
|
|
5959fe8be4 | ||
|
|
00f4c13fcc | ||
|
|
47054d470b | ||
|
|
d10192cae1 | ||
|
|
e1b90955c3 | ||
|
|
d249bf47c7 | ||
|
|
738dcd5f8d | ||
|
|
ad4f32ec32 | ||
|
|
4d03107ae7 | ||
|
|
0e93e310bf | ||
|
|
6cb8940696 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
versionCode 62
|
||||
versionName "1.0.0-beta04"
|
||||
versionCode 63
|
||||
versionName "1.0.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
private val client = Places.createClient(context)
|
||||
private val bold: CharacterStyle = StyleSpan(Typeface.BOLD)
|
||||
|
||||
override val id = "google"
|
||||
|
||||
override fun autocomplete(
|
||||
query: String,
|
||||
location: com.car2go.maps.model.LatLng?
|
||||
@@ -48,7 +50,7 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
it.getPrimaryText(bold),
|
||||
it.getSecondaryText(bold),
|
||||
it.placeId,
|
||||
it.distanceMeters,
|
||||
it.distanceMeters?.toDouble(),
|
||||
it.placeTypes.map { AutocompletePlaceType.valueOf(it.name) })
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -13,7 +15,10 @@ import net.vonforst.evmap.autocomplete.*
|
||||
import net.vonforst.evmap.containsAny
|
||||
import net.vonforst.evmap.databinding.ItemAutocompleteResultBinding
|
||||
import net.vonforst.evmap.isDarkMode
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.storage.RecentAutocompletePlace
|
||||
import java.time.Instant
|
||||
|
||||
class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatLng>) :
|
||||
BaseAdapter(), Filterable {
|
||||
@@ -21,7 +26,10 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
|
||||
private val providers = getAutocompleteProviders(context)
|
||||
private val typeItem = 0
|
||||
private val typeAttribution = 1
|
||||
var currentProvider: AutocompleteProvider? = null
|
||||
private val maxItems = 6
|
||||
private var currentProvider: AutocompleteProvider? = null
|
||||
private val recents = AppDatabase.getInstance(context).recentAutocompletePlaceDao()
|
||||
private var recentResults = mutableListOf<RecentAutocompletePlace>()
|
||||
|
||||
data class ViewHolder(val binding: ItemAutocompleteResultBinding)
|
||||
|
||||
@@ -103,20 +111,40 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
|
||||
}
|
||||
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterResults = FilterResults()
|
||||
val query = constraint.toString()
|
||||
if (constraint != null) {
|
||||
for (provider in providers) {
|
||||
try {
|
||||
resultList =
|
||||
provider.autocomplete(constraint.toString(), location.value)
|
||||
recentResults.clear()
|
||||
currentProvider = provider
|
||||
|
||||
// first search in recent places
|
||||
val recentPlaces = if (query.isEmpty()) {
|
||||
recents.getAll(provider.id, limit = maxItems)
|
||||
} else {
|
||||
recents.search(query, provider.id, limit = maxItems)
|
||||
}
|
||||
recentResults.addAll(recentPlaces)
|
||||
resultList = recentPlaces.map { it.asAutocompletePlace(location.value) }
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
// publish intermediate results on main thread
|
||||
publishResults(constraint, resultList.asFilterResults())
|
||||
}
|
||||
|
||||
// if we already have enough results or the query is short, stop here
|
||||
if (query.length < 3 || recentResults.size >= maxItems) break
|
||||
|
||||
// then search online
|
||||
val recentIds = recentPlaces.map { it.id }
|
||||
resultList =
|
||||
(recentPlaces.map { it.asAutocompletePlace(location.value) } +
|
||||
provider.autocomplete(query, location.value)
|
||||
.filter { !recentIds.contains(it.id) }).take(maxItems)
|
||||
break
|
||||
} catch (e: ApiUnavailableException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
filterResults.values = resultList
|
||||
filterResults.count = resultList!!.size
|
||||
}
|
||||
|
||||
|
||||
@@ -125,16 +153,42 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
|
||||
this.setDelayer { 500L }
|
||||
}
|
||||
|
||||
return filterResults
|
||||
return resultList.asFilterResults()
|
||||
}
|
||||
|
||||
private fun List<AutocompletePlace>?.asFilterResults(): FilterResults {
|
||||
val result = FilterResults()
|
||||
if (this != null) {
|
||||
result.values = this
|
||||
result.count = this.size
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDetails(id: String): PlaceWithBounds {
|
||||
val provider = currentProvider!!
|
||||
val result = resultList!!.find { it.id == id }!!
|
||||
|
||||
val recentPlace = recentResults.find { it.id == id }
|
||||
if (recentPlace != null) return recentPlace.asPlaceWithBounds()
|
||||
|
||||
val details = provider.getDetails(id)
|
||||
|
||||
recents.insert(RecentAutocompletePlace(result, details, provider.id, Instant.now()))
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun iconForPlaceType(types: List<AutocompletePlaceType>): Int =
|
||||
when {
|
||||
types.contains(
|
||||
AutocompletePlaceType.RECENT
|
||||
) -> R.drawable.ic_history
|
||||
types.containsAny(
|
||||
AutocompletePlaceType.LIGHT_RAIL_STATION,
|
||||
AutocompletePlaceType.BUS_STATION,
|
||||
@@ -153,4 +207,7 @@ fun iconForPlaceType(types: List<AutocompletePlaceType>): Int =
|
||||
}
|
||||
|
||||
fun isSpecialPlace(types: List<AutocompletePlaceType>): Boolean =
|
||||
iconForPlaceType(types) != R.drawable.ic_place_type_default
|
||||
!setOf(
|
||||
R.drawable.ic_place_type_default,
|
||||
R.drawable.ic_history
|
||||
).contains(iconForPlaceType(types))
|
||||
@@ -6,6 +6,8 @@ import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
|
||||
interface AutocompleteProvider {
|
||||
val id: String
|
||||
|
||||
fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace>
|
||||
suspend fun getDetails(id: String): PlaceWithBounds
|
||||
|
||||
@@ -20,7 +22,7 @@ data class AutocompletePlace(
|
||||
val primaryText: CharSequence,
|
||||
val secondaryText: CharSequence,
|
||||
val id: String,
|
||||
val distanceMeters: Int?,
|
||||
val distanceMeters: Double?,
|
||||
val types: List<AutocompletePlaceType>
|
||||
)
|
||||
|
||||
@@ -167,7 +169,8 @@ enum class AutocompletePlaceType {
|
||||
TRAVEL_AGENCY,
|
||||
UNIVERSITY,
|
||||
VETERINARY_CARE,
|
||||
ZOO;
|
||||
ZOO,
|
||||
RECENT;
|
||||
|
||||
companion object {
|
||||
fun valueOfOrNull(value: String): AutocompletePlaceType? {
|
||||
|
||||
@@ -17,12 +17,13 @@ import com.mapbox.geojson.BoundingBox
|
||||
import com.mapbox.geojson.Point
|
||||
import net.vonforst.evmap.R
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
private val bold: CharacterStyle = StyleSpan(Typeface.BOLD)
|
||||
private val results = HashMap<String, CarmenFeature>()
|
||||
|
||||
override val id = "mapbox"
|
||||
|
||||
override fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace> {
|
||||
val result = MapboxGeocoding.builder().apply {
|
||||
location?.let {
|
||||
@@ -58,7 +59,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
location?.let { location ->
|
||||
SphericalUtil.computeDistanceBetween(
|
||||
feature.center()!!.toLatLng(), location
|
||||
).roundToInt()
|
||||
)
|
||||
},
|
||||
getPlaceTypes(feature)
|
||||
)
|
||||
@@ -112,7 +113,8 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
|
||||
override fun getAttributionString(): Int = R.string.powered_by_mapbox
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int = R.drawable.mapbox_logo_icon
|
||||
override fun getAttributionImage(dark: Boolean): Int =
|
||||
if (dark) R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
|
||||
}
|
||||
|
||||
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
|
||||
|
||||
@@ -43,12 +43,6 @@ class FilterFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
binding.toolbar.title = "${getString(R.string.menu_filter)}: ${it.name}"
|
||||
}
|
||||
}
|
||||
|
||||
binding.filtersList.apply {
|
||||
adapter = FiltersAdapter()
|
||||
layoutManager =
|
||||
@@ -80,34 +74,52 @@ class FilterFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
R.id.menu_save_profile -> {
|
||||
showEditTextDialog(requireContext()) { dialog, input ->
|
||||
vm.filterProfile.value?.let { profile ->
|
||||
input.setText(profile.name)
|
||||
}
|
||||
|
||||
dialog.setTitle(R.string.save_as_profile)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(input.text.toString())
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
|
||||
}
|
||||
}
|
||||
saveProfile()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveProfile(error: Boolean = false) {
|
||||
showEditTextDialog(requireContext()) { dialog, input ->
|
||||
vm.filterProfile.value?.let { profile ->
|
||||
input.setText(profile.name)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
input.error = getString(R.string.required)
|
||||
}
|
||||
|
||||
dialog.setTitle(R.string.save_as_profile)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
if (input.text.isBlank()) {
|
||||
saveProfile(true)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(input.text.toString())
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
binding.toolbar.title = getString(R.string.edit_filter_profile, it.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,8 @@ class FilterProfilesFragment : Fragment() {
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val fromPos = viewHolder.adapterPosition;
|
||||
val toPos = target.adapterPosition;
|
||||
val fromPos = viewHolder.bindingAdapterPosition;
|
||||
val toPos = target.bindingAdapterPosition;
|
||||
|
||||
val list = vm.filterProfiles.value?.toMutableList()
|
||||
if (list != null) {
|
||||
|
||||
@@ -242,6 +242,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
findNavController().navigate(R.id.action_map_to_opensource_donations)
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
// when there is already another navigation going on
|
||||
} catch (ignored: IllegalStateException) {
|
||||
// "no current navigation node"
|
||||
}
|
||||
}
|
||||
/*if (!prefs.update060AndroidAutoDialogShown) {
|
||||
@@ -368,7 +370,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val place = adapter.getItem(position) ?: return@OnItemClickListener
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
vm.searchResult.value = adapter.currentProvider!!.getDetails(place.id)
|
||||
vm.searchResult.value = adapter.getDetails(place.id)
|
||||
} catch (e: ApiUnavailableException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
@@ -388,9 +390,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.search.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) {
|
||||
binding.search.keyListener = searchKeyListener
|
||||
if (binding.search.text.isNotEmpty() && isVisible) {
|
||||
binding.search.showDropDown()
|
||||
}
|
||||
} else {
|
||||
binding.search.keyListener = null
|
||||
}
|
||||
|
||||
@@ -79,7 +79,13 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
|
||||
.sortedByDescending { commonChoices?.contains(it.key) == true }
|
||||
.sortedBy {
|
||||
when {
|
||||
selected.contains(it.key) -> 0
|
||||
commonChoices?.contains(it.key) == true -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
|
||||
adapter.submitList(items)
|
||||
|
||||
|
||||
@@ -102,6 +102,12 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
return when (preference?.key) {
|
||||
"search_delete_recent" -> {
|
||||
Toast.makeText(context, R.string.deleted_recent_search_results, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
vm.deleteRecentSearchResults()
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,19 +24,21 @@ import net.vonforst.evmap.model.*
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class,
|
||||
FilterProfile::class,
|
||||
RecentAutocompletePlace::class,
|
||||
GEPlug::class,
|
||||
GENetwork::class,
|
||||
GEChargeCard::class,
|
||||
OCMConnectionType::class,
|
||||
OCMCountry::class,
|
||||
OCMOperator::class
|
||||
], version = 13
|
||||
], version = 14
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
abstract fun filterProfileDao(): FilterProfileDao
|
||||
abstract fun recentAutocompletePlaceDao(): RecentAutocompletePlaceDao
|
||||
|
||||
// GoingElectric API specific
|
||||
abstract fun geReferenceDataDao(): GEReferenceDataDao
|
||||
@@ -51,7 +53,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
.addMigrations(
|
||||
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
|
||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
||||
MIGRATION_12, MIGRATION_13
|
||||
MIGRATION_12, MIGRATION_13, MIGRATION_14
|
||||
)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
@@ -303,5 +305,12 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_14 = object : Migration(13, 14) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `RecentAutocompletePlace` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.room.*
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.autocomplete.AutocompletePlace
|
||||
import net.vonforst.evmap.autocomplete.AutocompletePlaceType
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(primaryKeys = ["id", "dataSource"])
|
||||
data class RecentAutocompletePlace(
|
||||
val id: String,
|
||||
val dataSource: String,
|
||||
var timestamp: Instant,
|
||||
val primaryText: String,
|
||||
val secondaryText: String,
|
||||
val latLng: LatLng,
|
||||
val viewport: LatLngBounds?,
|
||||
val types: List<AutocompletePlaceType>
|
||||
) {
|
||||
constructor(
|
||||
place: AutocompletePlace,
|
||||
details: PlaceWithBounds,
|
||||
dataSource: String,
|
||||
timestamp: Instant
|
||||
) : this(
|
||||
place.id, dataSource, timestamp, place.primaryText.toString(),
|
||||
place.secondaryText.toString(), details.latLng, details.viewport, place.types
|
||||
)
|
||||
|
||||
fun asAutocompletePlace(currentLocation: LatLng?): AutocompletePlace {
|
||||
return AutocompletePlace(
|
||||
primaryText,
|
||||
secondaryText,
|
||||
id,
|
||||
currentLocation?.let {
|
||||
distanceBetween(
|
||||
latLng.latitude, latLng.longitude,
|
||||
it.latitude, it.longitude
|
||||
)
|
||||
},
|
||||
types + AutocompletePlaceType.RECENT
|
||||
)
|
||||
}
|
||||
|
||||
fun asPlaceWithBounds(): PlaceWithBounds {
|
||||
return PlaceWithBounds(latLng, viewport)
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
abstract class RecentAutocompletePlaceDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg places: RecentAutocompletePlace)
|
||||
|
||||
@Query("DELETE FROM recentautocompleteplace")
|
||||
abstract suspend fun deleteAll()
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource AND primaryText LIKE '%' || :query || '%' ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract fun search(
|
||||
query: String,
|
||||
dataSource: String,
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract fun getAll(dataSource: String, limit: Int? = null): List<RecentAutocompletePlace>
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.openchargemap.OCMChargerPhotoAdapter
|
||||
import net.vonforst.evmap.autocomplete.AutocompletePlaceType
|
||||
import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
@@ -41,6 +44,12 @@ class Converters {
|
||||
val type = Types.newParameterizedType(List::class.java, String::class.java)
|
||||
moshi.adapter<List<String>>(type)
|
||||
}
|
||||
private val latLngAdapter by lazy {
|
||||
moshi.adapter<LatLng>(LatLng::class.java)
|
||||
}
|
||||
private val latLngBoundsAdapter by lazy {
|
||||
moshi.adapter<LatLngBounds>(LatLngBounds::class.java)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromChargepointList(value: List<Chargepoint>?): String {
|
||||
@@ -115,4 +124,34 @@ class Converters {
|
||||
fun toStringList(value: String): List<String>? {
|
||||
return stringListAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLatLng(value: LatLng?): String {
|
||||
return latLngAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLatLng(value: String): LatLng? {
|
||||
return latLngAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLatLngBounds(value: LatLngBounds?): String {
|
||||
return latLngBoundsAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLatLngBounds(value: String): LatLngBounds? {
|
||||
return latLngBoundsAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromAutocompletePlaceTypeList(value: List<AutocompletePlaceType>): String {
|
||||
return value.joinToString(",") { it.name }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toAutocompletePlaceTypeList(value: String): List<AutocompletePlaceType> {
|
||||
return value.split(",").map { AutocompletePlaceType.valueOf(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
|
||||
class AutocompleteTextViewWithSuggestions(ctx: Context, args: AttributeSet) :
|
||||
androidx.appcompat.widget.AppCompatAutoCompleteTextView(ctx, args) {
|
||||
override fun enoughToFilter(): Boolean = true
|
||||
|
||||
override fun onFocusChanged(
|
||||
focused: Boolean, direction: Int,
|
||||
previouslyFocusedRect: Rect?
|
||||
) {
|
||||
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
||||
if (focused && adapter != null) {
|
||||
performFiltering(text, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import java.io.IOException
|
||||
|
||||
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
@@ -49,4 +51,10 @@ class SettingsViewModel(application: Application, chargepriceApiKey: String) :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRecentSearchResults() {
|
||||
viewModelScope.launch {
|
||||
db.recentAutocompletePlaceDao().deleteAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/src/main/res/drawable/ic_history.xml
Normal file
10
app/src/main/res/drawable/ic_history.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="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/mapbox_logo.xml
Normal file
9
app/src/main/res/drawable/mapbox_logo.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="81dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="81"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="?colorControlNormal"
|
||||
android:pathData="M9.1,0.9C4.1,0.9 0,5 0,10s4.1,9.1 9.1,9.1s9.1,-4.1 9.1,-9.1S14.1,0.9 9.1,0.9zM55.7,2.9c-0.1,0 -0.2,0.1 -0.2,0.2v10.5c0,0.1 0.1,0.2 0.2,0.2h1.4c0.1,0 0.2,-0.1 0.2,-0.2v-0.7c0.7,0.7 1.7,1.2 2.7,1.2c2.1,0 3.9,-1.8 3.9,-4.1S62.1,5.9 60,5.9c-1,0 -2,0.4 -2.7,1.2V3.1c0,-0.1 -0.1,-0.2 -0.2,-0.2H55.7zM10,4.4c1.2,0 2.3,0.5 3.2,1.4c1.8,1.8 1.9,4.7 0.1,6.4c-3.1,3.1 -8.6,2.1 -8.6,2.1s-1,-5.5 2.1,-8.6C7.7,4.8 8.9,4.4 10,4.4zM26.4,5.9c-0.8,0 -1.6,0.4 -2.1,1.1V6.3c0,-0.1 -0.1,-0.2 -0.2,-0.2h-1.4c-0.1,0 -0.2,0.1 -0.2,0.2v7.4c0,0.1 0.1,0.2 0.2,0.2H24c0.1,0 0.2,-0.1 0.2,-0.2V9.3c0.1,-1 0.7,-1.8 1.6,-1.8c0.9,0 1.6,0.7 1.6,1.7v4.5c0,0.1 0.1,0.2 0.2,0.2H29c0.1,0 0.2,-0.1 0.2,-0.2l0,-4.6c0.1,-0.9 0.8,-1.6 1.6,-1.6c0.9,0 1.6,0.7 1.6,1.7v4.5c0,0.1 0.1,0.2 0.2,0.2H34c0.1,0 0.2,-0.1 0.2,-0.2l0,-5.1c0,-1.5 -1.3,-2.7 -2.8,-2.7c-1,0 -2,0.6 -2.4,1.5C28.4,6.5 27.4,5.9 26.4,5.9L26.4,5.9zM39.4,5.9c-2.1,0 -3.9,1.8 -3.9,4.1s1.7,4.1 3.9,4.1c1,0 2,-0.4 2.7,-1.2v0.7c0,0.1 0.1,0.2 0.2,0.2h1.4c0.1,0 0.2,-0.1 0.2,-0.2V6.3c0,-0.1 -0.1,-0.2 -0.2,-0.2h-1.4c-0.1,0 -0.2,0.1 -0.2,0.2V7C41.4,6.3 40.4,5.9 39.4,5.9zM50.2,5.9c-1,0 -2,0.4 -2.7,1.2V6.3c0,-0.1 -0.1,-0.2 -0.2,-0.2h-1.4c-0.1,0 -0.2,0.1 -0.2,0.2v10.5c0,0.1 0.1,0.2 0.2,0.2h1.4c0.1,0 0.2,-0.1 0.2,-0.2v-3.9c0.7,0.7 1.7,1.2 2.7,1.2c2.1,0 3.9,-1.8 3.9,-4.1S52.3,5.9 50.2,5.9zM69.1,5.9c-2.3,0 -4.2,1.8 -4.2,4.1s1.9,4.1 4.2,4.1c2.3,0 4.2,-1.8 4.2,-4.1S71.4,5.9 69.1,5.9zM73.7,6.1c-0.1,0 -0.2,0.1 -0.2,0.2c0,0 0,0.1 0,0.1l2.3,3.6l-2.4,3.6c-0.1,0.1 0,0.2 0.1,0.3c0,0 0.1,0 0.1,0h1.6c0.1,0 0.2,-0.1 0.3,-0.2l1.4,-2.4l1.4,2.4c0.1,0.1 0.2,0.2 0.3,0.2h1.6c0.1,0 0.2,-0.1 0.2,-0.2c0,0 0,-0.1 0,-0.1L78.1,10l2.3,-3.6c0.1,-0.1 0,-0.2 -0.1,-0.3c0,0 -0.1,0 -0.1,0h-1.6c-0.1,0 -0.2,0.1 -0.3,0.2L77,8.6l-1.4,-2.3c-0.1,-0.1 -0.2,-0.2 -0.3,-0.2H73.7zM10.1,6.2L9.2,8.1L7.4,8.9l1.8,0.9l0.9,1.8L11,9.8l1.8,-0.9L11,8.1L10.1,6.2zM39.7,7.5c1.3,0 2.3,1.1 2.4,2.4V10c0,1.3 -1.1,2.4 -2.4,2.4c-1.3,0 -2.4,-1.1 -2.4,-2.5S38.4,7.5 39.7,7.5L39.7,7.5zM49.9,7.5c1.3,0 2.4,1.1 2.4,2.5s-1.1,2.5 -2.4,2.5c-1.3,0 -2.3,-1.1 -2.4,-2.4V10C47.5,8.6 48.6,7.5 49.9,7.5L49.9,7.5zM59.7,7.5C61,7.5 62,8.6 62,10s-1.1,2.5 -2.4,2.5c-1.3,0 -2.3,-1.1 -2.4,-2.4V10C57.3,8.6 58.4,7.5 59.7,7.5L59.7,7.5zM69.1,7.5c1.3,0 2.4,1.1 2.4,2.5s-1.1,2.5 -2.4,2.5c-1.3,0 -2.4,-1.1 -2.4,-2.5S67.8,7.5 69.1,7.5z" />
|
||||
</vector>
|
||||
@@ -10,7 +10,8 @@
|
||||
android:name="net.vonforst.evmap.navigation.NavHostFragment"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:fitsSystemWindows="true" />
|
||||
android:fitsSystemWindows="true"
|
||||
app:defaultNavHost="true" />
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<AutoCompleteTextView
|
||||
<net.vonforst.evmap.ui.AutocompleteTextViewWithSuggestions
|
||||
android:id="@+id/search"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
|
||||
@@ -238,4 +238,10 @@
|
||||
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
|
||||
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
|
||||
<string name="unnamed_filter_profile">Unbenanntes Filterprofil</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/de/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/de/faq.html</string>
|
||||
<string name="required">erforderlich</string>
|
||||
<string name="edit_filter_profile">„%s“ bearbeiten</string>
|
||||
<string name="pref_search_delete_recent">Suchverlauf löschen</string>
|
||||
<string name="deleted_recent_search_results">Suchverlauf wurde gelöscht</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<resources>
|
||||
<string name="shared_element_picture">picture</string>
|
||||
<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>
|
||||
|
||||
@@ -223,4 +223,10 @@
|
||||
<string name="donate_desc">Support EVMap\'s development with a one-time donation</string>
|
||||
<string name="github_sponsors_desc">Support EVMap on GitHub Sponsors</string>
|
||||
<string name="unnamed_filter_profile">Unnamed filter profile</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
|
||||
<string name="required">required</string>
|
||||
<string name="edit_filter_profile">Edit “%s”</string>
|
||||
<string name="pref_search_delete_recent">Delete recent search results</string>
|
||||
<string name="deleted_recent_search_results">Recent search results have been deleted</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
android:defaultValue="@string/pref_search_provider_default"
|
||||
android:summary="%s" />
|
||||
|
||||
<Preference
|
||||
android:key="search_delete_recent"
|
||||
android:title="@string/pref_search_delete_recent" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="navigate_use_maps"
|
||||
android:title="@string/pref_navigate_use_maps"
|
||||
|
||||
12
fastlane/metadata/android/de-DE/changelogs/63.txt
Normal file
12
fastlane/metadata/android/de-DE/changelogs/63.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Neue Funktionen:
|
||||
- Android Auto: Preisvergleich mit Chargeprice
|
||||
- Android Auto: Neuer Bildschirm zur Anzeige von Fahrzeugdaten (falls verfügbar)
|
||||
- Kürzlich gesuchte Orte werden gespeichert und als Vorschläge angezeigt
|
||||
- Unterstützung für Android 12
|
||||
|
||||
Verbesserungen:
|
||||
- Android Auto: Nutzung des Fahrzeug-GPS (falls verfügbar, ab Android Auto 6.7)
|
||||
- In Auswahldialogen werden bereits ausgewählte Einträge nach oben verschoben
|
||||
- FAQ aktualisiert
|
||||
- Verbesserte Behandlung des Zurück-Buttons
|
||||
- Verschiedene Abstürze behoben
|
||||
12
fastlane/metadata/android/en-US/changelogs/63.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/63.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
New features:
|
||||
- Android Auto: Price comparison with Chargeprice (only in certain countries)
|
||||
- Android Auto: New screen displaying vehicle data (if available)
|
||||
- Recently searched places are stored and shown as suggestions
|
||||
- Android 12 support
|
||||
|
||||
Improvements:
|
||||
- Android Auto: Use vehicle GPS (if available, starting with Android Auto 6.7)
|
||||
- Move already selected entries to the top in selection dialogs
|
||||
- FAQ and privacy policy updated and now also available in English
|
||||
- Improved back button handling
|
||||
- Fixed various crashes
|
||||
Reference in New Issue
Block a user