mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 15:47:44 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d041513516 | ||
|
|
1effba77d1 | ||
|
|
df79f02e1d | ||
|
|
c4d44f9ddf | ||
|
|
6bec397133 | ||
|
|
474b621af0 | ||
|
|
36aeb201ca | ||
|
|
76a241d691 | ||
|
|
0f7bf7913f | ||
|
|
d11925eb33 | ||
|
|
6ac49fd84d | ||
|
|
097b7941a2 | ||
|
|
23b87e69c0 | ||
|
|
3bb5521c18 | ||
|
|
76f7b97c1f | ||
|
|
50de0009c7 | ||
|
|
f906846fcc | ||
|
|
b50225af32 | ||
|
|
8abd5219aa | ||
|
|
71f9a25c5a | ||
|
|
b5f4314795 | ||
|
|
034196b9fa | ||
|
|
72d7f7dc57 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 18
|
||||
versionName "0.2.0"
|
||||
versionCode 20
|
||||
versionName "0.2.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -114,7 +114,7 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
|
||||
// navigation library
|
||||
def nav_version = "2.3.0-rc01"
|
||||
def nav_version = "2.3.0"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
@@ -137,7 +137,7 @@ dependencies {
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.7"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
@@ -146,5 +146,5 @@ dependencies {
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package net.vonforst.evmap
|
||||
import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.google.android.libraries.places.api.Places
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
class EvMapApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
updateNightMode(PreferenceDataSource(this))
|
||||
Stetho.initializeWithDefaults(this);
|
||||
Places.initialize(getApplicationContext(), getString(R.string.google_maps_key));
|
||||
Places.initialize(applicationContext, getString(R.string.google_maps_key));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.StyleSpan
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
if (!this.containsKey(name)) return null
|
||||
@@ -14,4 +17,38 @@ fun Bundle.optLong(name: String): Long? {
|
||||
|
||||
val lng = this.getLong(name, Long.MIN_VALUE)
|
||||
return if (lng == Long.MIN_VALUE) null else lng
|
||||
}
|
||||
|
||||
fun <T> Iterable<T>.joinToSpannedString(
|
||||
separator: CharSequence = ", ",
|
||||
prefix: CharSequence = "",
|
||||
postfix: CharSequence = "",
|
||||
limit: Int = -1,
|
||||
truncated: CharSequence = "...",
|
||||
transform: ((T) -> CharSequence)? = null
|
||||
): CharSequence {
|
||||
return SpannedString(
|
||||
joinTo(
|
||||
SpannableStringBuilder(),
|
||||
separator,
|
||||
prefix,
|
||||
postfix,
|
||||
limit,
|
||||
truncated,
|
||||
transform
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
operator fun CharSequence.plus(other: CharSequence): CharSequence {
|
||||
return TextUtils.concat(this, other)
|
||||
}
|
||||
|
||||
fun String.bold(): CharSequence {
|
||||
return SpannableString(this).apply {
|
||||
setSpan(
|
||||
StyleSpan(Typeface.BOLD), 0, this.length,
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,18 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import net.vonforst.evmap.BR
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.*
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.fragment.MultiSelectDialog
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.DonationItem
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
@@ -92,121 +79,6 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
}
|
||||
|
||||
class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
|
||||
data class Detail(
|
||||
val icon: Int,
|
||||
val contentDescription: Int,
|
||||
val text: CharSequence,
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true,
|
||||
val clickable: Boolean = false,
|
||||
val hoursDays: OpeningHoursDays? = null
|
||||
) : Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
if (item.hoursDays != null) {
|
||||
return R.layout.item_detail_openinghours
|
||||
} else {
|
||||
return R.layout.item_detail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
ctx: Context
|
||||
): List<DetailAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
|
||||
return listOfNotNull(
|
||||
DetailAdapter.Detail(
|
||||
R.drawable.ic_address,
|
||||
R.string.address,
|
||||
loc.address.toString(),
|
||||
loc.locationDescription
|
||||
),
|
||||
if (loc.operator != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_operator,
|
||||
R.string.operator,
|
||||
loc.operator
|
||||
) else null,
|
||||
if (loc.network != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_network,
|
||||
R.string.network,
|
||||
loc.network
|
||||
) else null,
|
||||
if (loc.faultReport != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_fault_report,
|
||||
R.string.fault_report,
|
||||
loc.faultReport.created?.let {
|
||||
ctx.getString(
|
||||
R.string.fault_report_date,
|
||||
loc.faultReport.created
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
} ?: "",
|
||||
loc.faultReport.description?.let {
|
||||
HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
} ?: "",
|
||||
clickable = true
|
||||
) else null,
|
||||
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailAdapter.Detail(
|
||||
R.drawable.ic_hours,
|
||||
R.string.hours,
|
||||
loc.openinghours.getStatusText(ctx),
|
||||
loc.openinghours.description,
|
||||
hoursDays = loc.openinghours.days
|
||||
) else null,
|
||||
if (loc.cost != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_cost,
|
||||
R.string.cost,
|
||||
loc.cost.getStatusText(ctx),
|
||||
loc.cost.descriptionLong ?: loc.cost.descriptionShort
|
||||
)
|
||||
else null,
|
||||
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) DetailAdapter.Detail(
|
||||
R.drawable.ic_payment,
|
||||
R.string.charge_cards,
|
||||
ctx.resources.getQuantityString(
|
||||
R.plurals.charge_cards_compatible_num,
|
||||
loc.chargecards.size, loc.chargecards.size
|
||||
),
|
||||
formatChargeCards(loc.chargecards, chargeCards, ctx),
|
||||
clickable = true
|
||||
) else null,
|
||||
DetailAdapter.Detail(
|
||||
R.drawable.ic_location,
|
||||
R.string.coordinates,
|
||||
loc.coordinates.formatDMS(),
|
||||
loc.coordinates.formatDecimal(),
|
||||
links = false,
|
||||
clickable = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun formatChargeCards(
|
||||
chargecards: List<ChargeCardId>,
|
||||
chargecardData: Map<Long, ChargeCard>?,
|
||||
ctx: Context
|
||||
): String {
|
||||
if (chargecardData == null) return ""
|
||||
|
||||
val maxItems = 5
|
||||
var result = chargecards
|
||||
.take(maxItems)
|
||||
.mapNotNull { chargecardData[it.id]?.name }
|
||||
.joinToString()
|
||||
if (chargecards.size > maxItems) {
|
||||
result += " " + ctx.getString(R.string.and_n_others, chargecards.size - maxItems)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
@@ -219,194 +91,6 @@ class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
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 (val filter = getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> {
|
||||
if (filter.manyChoices) {
|
||||
R.layout.item_filter_multiple_choice_large
|
||||
} else {
|
||||
R.layout.item_filter_multiple_choice
|
||||
}
|
||||
}
|
||||
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 -> {
|
||||
setupSlider(
|
||||
holder.binding as ItemFilterSliderBinding,
|
||||
item.filter as SliderFilter, item.value
|
||||
)
|
||||
}
|
||||
is MultipleChoiceFilterValue -> {
|
||||
val filter = item.filter as MultipleChoiceFilter
|
||||
if (filter.manyChoices) {
|
||||
setupMultipleChoiceMany(
|
||||
holder.binding as ItemFilterMultipleChoiceLargeBinding,
|
||||
filter, item.value
|
||||
)
|
||||
} else {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
filter, item.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMultipleChoice(
|
||||
binding: ItemFilterMultipleChoiceBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
val inflater = LayoutInflater.from(binding.root.context)
|
||||
value.values.toList().forEach {
|
||||
// delete values that cannot be selected anymore
|
||||
if (it !in filter.choices.keys) value.values.remove(it)
|
||||
}
|
||||
|
||||
fun updateButtons() {
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.btnAll.isEnabled = !value.all
|
||||
binding.btnNone.isEnabled = value.values.isNotEmpty()
|
||||
}
|
||||
|
||||
val chips = mutableMapOf<String, Chip>()
|
||||
binding.chipGroup.children.forEach {
|
||||
if (it.id != R.id.chipMore) binding.chipGroup.removeView(it)
|
||||
}
|
||||
filter.choices.entries.sortedByDescending {
|
||||
it.key in value.values
|
||||
}.sortedByDescending {
|
||||
if (filter.commonChoices != null) it.key in filter.commonChoices else false
|
||||
}.forEach { choice ->
|
||||
val chip = inflater.inflate(
|
||||
R.layout.item_filter_multiple_choice_chip,
|
||||
binding.chipGroup,
|
||||
false
|
||||
) as Chip
|
||||
chip.text = choice.value
|
||||
chip.isChecked = choice.key in value.values || value.all
|
||||
if (value.all && choice.key !in value.values) value.values.add(choice.key)
|
||||
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
value.values.add(choice.key)
|
||||
} else {
|
||||
value.values.remove(choice.key)
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
if (filter.commonChoices != null && choice.key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.chipGroup.addView(chip, binding.chipGroup.childCount - 1)
|
||||
chips[choice.key] = chip
|
||||
}
|
||||
|
||||
binding.btnAll.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = true }
|
||||
updateButtons()
|
||||
}
|
||||
binding.btnNone.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = false }
|
||||
updateButtons()
|
||||
}
|
||||
binding.chipMore.setOnClickListener {
|
||||
binding.showingAll = !binding.showingAll
|
||||
chips.forEach { (key, chip) ->
|
||||
if (filter.commonChoices != null && key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private fun setupMultipleChoiceMany(
|
||||
binding: ItemFilterMultipleChoiceLargeBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
if (value.all) {
|
||||
value.values = filter.choices.keys.toMutableSet()
|
||||
binding.notifyPropertyChanged(BR.item)
|
||||
}
|
||||
|
||||
binding.btnEdit.setOnClickListener {
|
||||
val dialog = MultiSelectDialog.getInstance(filter.name, filter.choices, value.values)
|
||||
dialog.okListener = { selected ->
|
||||
value.values = selected.toMutableSet()
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.item = binding.item
|
||||
}
|
||||
dialog.show((binding.root.context as AppCompatActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSlider(
|
||||
binding: ItemFilterSliderBinding,
|
||||
filter: SliderFilter,
|
||||
value: SliderFilterValue
|
||||
) {
|
||||
binding.progress = filter.inverseMapping(value.value)
|
||||
binding.mappedValue = value.value
|
||||
|
||||
binding.addOnPropertyChangedCallback(object :
|
||||
Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
when (propertyId) {
|
||||
BR.progress -> {
|
||||
val mapped = filter.mapping(binding.progress)
|
||||
value.value = mapped
|
||||
binding.mappedValue = mapped
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_donation
|
||||
}
|
||||
139
app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt
Normal file
139
app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.text.HtmlCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.plus
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
class DetailsAdapter : DataBindingAdapter<DetailsAdapter.Detail>() {
|
||||
data class Detail(
|
||||
val icon: Int,
|
||||
val contentDescription: Int,
|
||||
val text: CharSequence,
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true,
|
||||
val clickable: Boolean = false,
|
||||
val hoursDays: OpeningHoursDays? = null
|
||||
) : Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
if (item.hoursDays != null) {
|
||||
return R.layout.item_detail_openinghours
|
||||
} else {
|
||||
return R.layout.item_detail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
ctx: Context
|
||||
): List<DetailsAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
|
||||
return listOfNotNull(
|
||||
DetailsAdapter.Detail(
|
||||
R.drawable.ic_address,
|
||||
R.string.address,
|
||||
loc.address.toString(),
|
||||
loc.locationDescription
|
||||
),
|
||||
if (loc.operator != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_operator,
|
||||
R.string.operator,
|
||||
loc.operator
|
||||
) else null,
|
||||
if (loc.network != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_network,
|
||||
R.string.network,
|
||||
loc.network
|
||||
) else null,
|
||||
if (loc.faultReport != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_fault_report,
|
||||
R.string.fault_report,
|
||||
loc.faultReport.created?.let {
|
||||
ctx.getString(
|
||||
R.string.fault_report_date,
|
||||
loc.faultReport.created
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
} ?: "",
|
||||
loc.faultReport.description?.let {
|
||||
HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
} ?: "",
|
||||
clickable = true
|
||||
) else null,
|
||||
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailsAdapter.Detail(
|
||||
R.drawable.ic_hours,
|
||||
R.string.hours,
|
||||
loc.openinghours.getStatusText(ctx),
|
||||
loc.openinghours.description,
|
||||
hoursDays = loc.openinghours.days
|
||||
) else null,
|
||||
if (loc.cost != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_cost,
|
||||
R.string.cost,
|
||||
loc.cost.getStatusText(ctx),
|
||||
loc.cost.descriptionLong ?: loc.cost.descriptionShort
|
||||
)
|
||||
else null,
|
||||
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) DetailsAdapter.Detail(
|
||||
R.drawable.ic_payment,
|
||||
R.string.charge_cards,
|
||||
ctx.resources.getQuantityString(
|
||||
R.plurals.charge_cards_compatible_num,
|
||||
loc.chargecards.size, loc.chargecards.size
|
||||
),
|
||||
formatChargeCards(loc.chargecards, chargeCards, filteredChargeCards, ctx),
|
||||
clickable = true
|
||||
) else null,
|
||||
DetailsAdapter.Detail(
|
||||
R.drawable.ic_location,
|
||||
R.string.coordinates,
|
||||
loc.coordinates.formatDMS(),
|
||||
loc.coordinates.formatDecimal(),
|
||||
links = false,
|
||||
clickable = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun formatChargeCards(
|
||||
chargecards: List<ChargeCardId>,
|
||||
chargecardData: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
ctx: Context
|
||||
): CharSequence {
|
||||
if (chargecardData == null) return ""
|
||||
|
||||
val maxItems = 5
|
||||
var result = chargecards
|
||||
.sortedByDescending { filteredChargeCards?.contains(it.id) }
|
||||
.take(maxItems)
|
||||
.mapNotNull {
|
||||
val name = chargecardData[it.id]?.name ?: return@mapNotNull null
|
||||
if (filteredChargeCards?.contains(it.id) == true) {
|
||||
name.bold()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}.joinToSpannedString()
|
||||
if (chargecards.size > maxItems) {
|
||||
result += " " + ctx.getString(R.string.and_n_others, chargecards.size - maxItems)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
227
app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt
Normal file
227
app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt
Normal file
@@ -0,0 +1,227 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.children
|
||||
import androidx.databinding.Observable
|
||||
import com.google.android.material.chip.Chip
|
||||
import net.vonforst.evmap.BR
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.fragment.MultiSelectDialog
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import kotlin.math.max
|
||||
|
||||
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
val itemids = mutableMapOf<String, Long>()
|
||||
var maxId = 0L
|
||||
|
||||
override fun getItemViewType(position: Int): Int =
|
||||
when (val filter = getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> {
|
||||
if (filter.manyChoices) {
|
||||
R.layout.item_filter_multiple_choice_large
|
||||
} else {
|
||||
R.layout.item_filter_multiple_choice
|
||||
}
|
||||
}
|
||||
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 -> {
|
||||
setupSlider(
|
||||
holder.binding as ItemFilterSliderBinding,
|
||||
item.filter as SliderFilter, item.value
|
||||
)
|
||||
}
|
||||
is MultipleChoiceFilterValue -> {
|
||||
val filter = item.filter as MultipleChoiceFilter
|
||||
if (filter.manyChoices) {
|
||||
setupMultipleChoiceMany(
|
||||
holder.binding as ItemFilterMultipleChoiceLargeBinding,
|
||||
filter, item.value
|
||||
)
|
||||
} else {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
filter, item.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMultipleChoice(
|
||||
binding: ItemFilterMultipleChoiceBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
// TODO: this implementation seems to be buggy
|
||||
val inflater = LayoutInflater.from(binding.root.context)
|
||||
value.values.toList().forEach {
|
||||
// delete values that cannot be selected anymore
|
||||
if (it !in filter.choices.keys) value.values.remove(it)
|
||||
}
|
||||
|
||||
fun updateButtons() {
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.btnAll.isEnabled = !value.all
|
||||
binding.btnNone.isEnabled = value.values.isNotEmpty()
|
||||
}
|
||||
|
||||
val chips = mutableMapOf<String, Chip>()
|
||||
|
||||
// reuse existing chips in layout
|
||||
val reuseChips = binding.chipGroup.children.filter {
|
||||
it.id != R.id.chipMore
|
||||
}.toMutableList()
|
||||
binding.chipGroup.children.forEach {
|
||||
if (it.id != R.id.chipMore) binding.chipGroup.removeView(it)
|
||||
}
|
||||
filter.choices.entries.sortedByDescending {
|
||||
it.key in value.values
|
||||
}.sortedByDescending {
|
||||
if (filter.commonChoices != null) it.key in filter.commonChoices else false
|
||||
}.forEach { choice ->
|
||||
var reused = false
|
||||
val chip = if (reuseChips.size > 0) {
|
||||
reused = true
|
||||
reuseChips.removeAt(0) as Chip
|
||||
} else {
|
||||
inflater.inflate(
|
||||
R.layout.item_filter_multiple_choice_chip,
|
||||
binding.chipGroup,
|
||||
false
|
||||
) as Chip
|
||||
}
|
||||
chip.text = choice.value
|
||||
chip.isChecked = choice.key in value.values || value.all
|
||||
if (value.all && choice.key !in value.values) value.values.add(choice.key)
|
||||
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
value.values.add(choice.key)
|
||||
} else {
|
||||
value.values.remove(choice.key)
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
if (filter.commonChoices != null && choice.key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (!reused) binding.chipGroup.addView(chip, binding.chipGroup.childCount - 1)
|
||||
chips[choice.key] = chip
|
||||
}
|
||||
// delete surplus reusable chips
|
||||
reuseChips.forEach {
|
||||
binding.chipGroup.removeView(it)
|
||||
}
|
||||
|
||||
binding.btnAll.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = true }
|
||||
updateButtons()
|
||||
}
|
||||
binding.btnNone.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = false }
|
||||
updateButtons()
|
||||
}
|
||||
binding.chipMore.setOnClickListener {
|
||||
binding.showingAll = !binding.showingAll
|
||||
chips.forEach { (key, chip) ->
|
||||
if (filter.commonChoices != null && key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private fun setupMultipleChoiceMany(
|
||||
binding: ItemFilterMultipleChoiceLargeBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
if (value.all) {
|
||||
value.values = filter.choices.keys.toMutableSet()
|
||||
binding.notifyPropertyChanged(BR.item)
|
||||
}
|
||||
|
||||
binding.btnEdit.setOnClickListener {
|
||||
val dialog =
|
||||
MultiSelectDialog.getInstance(
|
||||
filter.name,
|
||||
filter.choices,
|
||||
value.values,
|
||||
commonChoices = filter.commonChoices
|
||||
)
|
||||
dialog.okListener = { selected ->
|
||||
value.values = selected.toMutableSet()
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.item = binding.item
|
||||
}
|
||||
dialog.show((binding.root.context as AppCompatActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSlider(
|
||||
binding: ItemFilterSliderBinding,
|
||||
filter: SliderFilter,
|
||||
value: SliderFilterValue
|
||||
) {
|
||||
binding.progress =
|
||||
max(filter.inverseMapping(value.value) - filter.min, 0)
|
||||
binding.mappedValue = filter.mapping(binding.progress + filter.min)
|
||||
|
||||
binding.addOnPropertyChangedCallback(object :
|
||||
Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
when (propertyId) {
|
||||
BR.progress -> {
|
||||
val mapped = filter.mapping(binding.progress + filter.min)
|
||||
value.value = mapped
|
||||
binding.mappedValue = mapped
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -64,11 +64,22 @@ data class ChargeLocation(
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?
|
||||
) : ChargepointListItem(), Equatable {
|
||||
/**
|
||||
* maximum power available from this charger.
|
||||
*/
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return chargepoints.map { it.power }.max() ?: 0.0
|
||||
return maxPower()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum power available from certain connectors of this charger.
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.max() ?: 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
|
||||
@@ -55,7 +55,7 @@ import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.android.synthetic.main.fragment_map.*
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
@@ -91,6 +91,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
private var searchResultMarker: Marker? = null
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var previousChargepointIds: Set<Long>? = null
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
@@ -386,7 +387,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
markers.forEach { (m, c) ->
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c), fault = c.faultReport != null
|
||||
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -397,7 +398,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
// highlight this marker
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger), highlight = true, fault = charger.faultReport != null
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = true,
|
||||
fault = charger.faultReport != null
|
||||
)
|
||||
)
|
||||
animator.animateMarkerBounce(marker)
|
||||
@@ -407,7 +410,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
if (m != marker) {
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c), fault = c.faultReport != null
|
||||
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -471,7 +474,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
binding.detailView.details.apply {
|
||||
adapter = DetailAdapter().apply {
|
||||
adapter = DetailsAdapter().apply {
|
||||
onClickListener = {
|
||||
val charger = vm.chargerDetails.value?.data
|
||||
if (charger != null) {
|
||||
@@ -505,13 +508,22 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val activity = activity ?: return
|
||||
val chargecardData = vm.chargeCardMap.value ?: return
|
||||
val chargecards = charger.chargecards ?: return
|
||||
val filteredChargeCards = vm.filteredChargeCards.value
|
||||
|
||||
val data = chargecards.map { chargecardData[it.id] }.sortedBy { it?.name }
|
||||
val names = data.map { it?.name ?: "" }
|
||||
val data = chargecards.mapNotNull { chargecardData[it.id] }
|
||||
.sortedBy { it.name }
|
||||
.sortedByDescending { filteredChargeCards?.contains(it.id) }
|
||||
val names = data.map {
|
||||
if (filteredChargeCards?.contains(it.id) == true) {
|
||||
it.name.bold()
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.charge_cards)
|
||||
.setItems(names.toTypedArray()) { _, i ->
|
||||
val card = data[i] ?: return@setItems
|
||||
val card = data[i]
|
||||
(activity as? MapsActivity)?.openUrl("https:${card.url}")
|
||||
}.show()
|
||||
}
|
||||
@@ -635,6 +647,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateMap(chargepoints: List<ChargepointListItem>) {
|
||||
val map = this.map ?: return
|
||||
clusterMarkers.forEach { it.remove() }
|
||||
@@ -643,39 +656,54 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
|
||||
|
||||
val chargepointIds = chargers.map { it.id }.toSet()
|
||||
// remove markers that disappeared
|
||||
markers.entries.toList().forEach {
|
||||
if (!chargepointIds.contains(it.value.id)) {
|
||||
if (it.key.isVisible) {
|
||||
val tint = getMarkerTint(it.value)
|
||||
val highlight = it.value == vm.chargerSparse.value
|
||||
val fault = it.value.faultReport != null
|
||||
animator.animateMarkerDisappear(it.key, tint, highlight, fault)
|
||||
} else {
|
||||
it.key.remove()
|
||||
}
|
||||
markers.remove(it.key)
|
||||
}
|
||||
}
|
||||
// add new markers
|
||||
chargers.filter {
|
||||
!markers.containsValue(it)
|
||||
}.forEach { charger ->
|
||||
val tint = getMarkerTint(charger)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.icon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
tint, highlight = highlight,
|
||||
fault = charger.faultReport != null
|
||||
)
|
||||
)
|
||||
|
||||
// update icons of existing markers (connector filter may have changed)
|
||||
for ((marker, charger) in markers) {
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = charger == vm.chargerSparse.value,
|
||||
fault = charger.faultReport != null
|
||||
)
|
||||
)
|
||||
animator.animateMarkerAppear(marker, tint, highlight, fault)
|
||||
markers[marker] = charger
|
||||
}
|
||||
|
||||
if (chargers.toSet() != markers.values) {
|
||||
// remove markers that disappeared
|
||||
val bounds = map.projection.visibleRegion.latLngBounds
|
||||
markers.entries.toList().forEach {
|
||||
val marker = it.key
|
||||
val charger = it.value
|
||||
if (!chargepointIds.contains(charger.id)) {
|
||||
// animate marker if it is visible, otherwise remove immediately
|
||||
if (bounds.contains(marker.position)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
animator.animateMarkerDisappear(marker, tint, highlight, fault)
|
||||
} else {
|
||||
animator.deleteMarker(marker)
|
||||
}
|
||||
markers.remove(marker)
|
||||
}
|
||||
}
|
||||
// add new markers
|
||||
val map1 = markers.values.map { it.id }
|
||||
for (charger in chargers) {
|
||||
if (!map1.contains(charger.id)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.visible(false)
|
||||
)
|
||||
animator.animateMarkerAppear(marker, tint, highlight, fault)
|
||||
markers[marker] = charger
|
||||
}
|
||||
}
|
||||
previousChargepointIds = chargepointIds
|
||||
}
|
||||
clusterMarkers = clusters.map { cluster ->
|
||||
map.addMarker(
|
||||
|
||||
@@ -20,13 +20,15 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
fun getInstance(
|
||||
title: String,
|
||||
data: Map<String, String>,
|
||||
selected: Set<String>
|
||||
selected: Set<String>,
|
||||
commonChoices: Set<String>?
|
||||
): MultiSelectDialog {
|
||||
val dialog = MultiSelectDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putString("title", title)
|
||||
putSerializable("data", HashMap(data))
|
||||
putSerializable("selected", HashSet(selected))
|
||||
if (commonChoices != null) putSerializable("commonChoices", HashSet(commonChoices))
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
@@ -55,18 +57,23 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val data = requireArguments().getSerializable("data") as HashMap<String, String>
|
||||
val selected = requireArguments().getSerializable("selected") as HashSet<String>
|
||||
val title = requireArguments().getString("title")
|
||||
val args = requireArguments()
|
||||
val data = args.getSerializable("data") as HashMap<String, String>
|
||||
val selected = args.getSerializable("selected") as HashSet<String>
|
||||
val title = args.getString("title")
|
||||
val commonChoices = if (args.containsKey("commonChoices")) {
|
||||
args.getSerializable("commonChoices") as HashSet<String>
|
||||
} else null
|
||||
|
||||
dialogTitle.text = title
|
||||
val adapter = Adapter()
|
||||
list.adapter = adapter
|
||||
list.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
items = data.entries.toList().sortedBy { it.key }.map {
|
||||
MultiSelectItem(it.key, it.value, it.key in selected)
|
||||
}
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value }
|
||||
.sortedByDescending { commonChoices?.contains(it.key) == true }
|
||||
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
|
||||
adapter.submitList(items)
|
||||
|
||||
etSearch.doAfterTextChanged { text ->
|
||||
@@ -95,20 +102,20 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun search(
|
||||
items: List<MultiSelectItem>,
|
||||
text: String
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
|
||||
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
|
||||
private fun search(
|
||||
items: List<MultiSelectItem>,
|
||||
text: String
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
|
||||
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
|
||||
}
|
||||
|
||||
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable
|
||||
@@ -10,13 +10,18 @@ import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
@@ -43,6 +48,9 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
it.startActivity(it.intent);
|
||||
}
|
||||
}
|
||||
"darkmode" -> {
|
||||
updateNightMode(prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -35,13 +36,19 @@ class ChargeCardRepository(
|
||||
private suspend fun updateChargeCards() {
|
||||
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getChargeCards()
|
||||
if (!response.isSuccessful) return
|
||||
try {
|
||||
val response = api.getChargeCards()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (card in response.body()!!.result) {
|
||||
dao.insert(card)
|
||||
for (card in response.body()!!.result) {
|
||||
dao.insert(card)
|
||||
}
|
||||
|
||||
prefs.lastChargeCardUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
prefs.lastChargeCardUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -37,13 +38,19 @@ class NetworkRepository(
|
||||
private suspend fun updateNetworks() {
|
||||
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getNetworks()
|
||||
if (!response.isSuccessful) return
|
||||
try {
|
||||
val response = api.getNetworks()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Network(name))
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Network(name))
|
||||
}
|
||||
|
||||
prefs.lastNetworkUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
prefs.lastNetworkUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -37,13 +38,19 @@ class PlugRepository(
|
||||
private suspend fun updatePlugs() {
|
||||
if (Duration.between(prefs.lastPlugUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
try {
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,7 @@ class PreferenceDataSource(context: Context) {
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
|
||||
val darkmode: String
|
||||
get() = sp.getString("darkmode", "default")!!
|
||||
}
|
||||
@@ -50,12 +50,8 @@ class ChargerIconGenerator(val context: Context) {
|
||||
val fault: Boolean
|
||||
)
|
||||
|
||||
val cacheSize = 8 * 1024 * 1024; // 8MiB
|
||||
val cache = object : LruCache<BitmapData, Bitmap>(cacheSize) {
|
||||
override fun sizeOf(key: BitmapData, value: Bitmap): Int {
|
||||
return value.byteCount
|
||||
}
|
||||
}
|
||||
val cacheSize = 420; // 420 items: 21 sizes, 5 colors, highlight on/off, fault 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 highlightIcon = R.drawable.ic_map_marker_highlight
|
||||
@@ -65,7 +61,7 @@ class ChargerIconGenerator(val context: Context) {
|
||||
preloadCache()
|
||||
}
|
||||
|
||||
fun preloadCache() {
|
||||
private fun preloadCache() {
|
||||
// pre-generates images for scale from 0 to 255 for all possible tint colors
|
||||
val tints = listOf(
|
||||
R.color.charger_100kw,
|
||||
@@ -78,8 +74,7 @@ class ChargerIconGenerator(val context: Context) {
|
||||
for (highlight in listOf(false, true)) {
|
||||
for (tint in tints) {
|
||||
for (scale in 0..20) {
|
||||
val data = BitmapData(tint, scale, 255, highlight, fault)
|
||||
cache.put(data, generateBitmap(data))
|
||||
getBitmapDescriptor(tint, scale, 255, highlight, fault)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,11 +91,12 @@ class ChargerIconGenerator(val context: Context) {
|
||||
val data = BitmapData(tint, scale, alpha, highlight, fault)
|
||||
val cachedImg = cache[data]
|
||||
return if (cachedImg != null) {
|
||||
BitmapDescriptorFactory.fromBitmap(cachedImg)
|
||||
cachedImg
|
||||
} else {
|
||||
val bitmap = generateBitmap(data)
|
||||
cache.put(data, bitmap)
|
||||
BitmapDescriptorFactory.fromBitmap(bitmap)
|
||||
val bmd = BitmapDescriptorFactory.fromBitmap(bitmap)
|
||||
cache.put(data, bmd)
|
||||
bmd
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,19 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import kotlin.math.max
|
||||
|
||||
fun getMarkerTint(charger: ChargeLocation): Int = when {
|
||||
charger.maxPower >= 100 -> R.color.charger_100kw
|
||||
charger.maxPower >= 43 -> R.color.charger_43kw
|
||||
charger.maxPower >= 20 -> R.color.charger_20kw
|
||||
charger.maxPower >= 11 -> R.color.charger_11kw
|
||||
fun getMarkerTint(
|
||||
charger: ChargeLocation,
|
||||
connectors: Set<String>?
|
||||
): Int = when {
|
||||
charger.maxPower(connectors) >= 100 -> R.color.charger_100kw
|
||||
charger.maxPower(connectors) >= 43 -> R.color.charger_43kw
|
||||
charger.maxPower(connectors) >= 20 -> R.color.charger_20kw
|
||||
charger.maxPower(connectors) >= 11 -> R.color.charger_11kw
|
||||
else -> R.color.charger_low
|
||||
}
|
||||
|
||||
class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
|
||||
private val animatingMarkers = hashMapOf<String, ValueAnimator>()
|
||||
|
||||
fun animateMarkerAppear(
|
||||
marker: Marker,
|
||||
@@ -27,18 +30,15 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
highlight: Boolean,
|
||||
fault: Boolean
|
||||
) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers[marker.id]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker.id)
|
||||
}
|
||||
|
||||
val anim = ValueAnimator.ofInt(0, 20).apply {
|
||||
duration = 250
|
||||
interpolator = LinearOutSlowInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(
|
||||
@@ -48,12 +48,15 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
fault = fault
|
||||
)
|
||||
)
|
||||
marker.isVisible = true
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers.remove(marker.id)
|
||||
}, onCancel = {
|
||||
animatingMarkers.remove(marker.id)
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
animatingMarkers[marker.id] = anim
|
||||
anim.start()
|
||||
}
|
||||
|
||||
@@ -63,18 +66,15 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
highlight: Boolean,
|
||||
fault: Boolean
|
||||
) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers[marker.id]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker.id)
|
||||
}
|
||||
|
||||
val anim = ValueAnimator.ofInt(20, 0).apply {
|
||||
duration = 200
|
||||
interpolator = FastOutLinearInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(
|
||||
@@ -86,32 +86,45 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
)
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
marker.remove()
|
||||
animatingMarkers.remove(marker.id)
|
||||
}, onCancel = {
|
||||
marker.remove()
|
||||
animatingMarkers.remove(marker.id)
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
animatingMarkers[marker.id] = anim
|
||||
anim.start()
|
||||
}
|
||||
|
||||
fun deleteMarker(marker: Marker) {
|
||||
animatingMarkers[marker.id]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker.id)
|
||||
}
|
||||
marker.remove()
|
||||
}
|
||||
|
||||
fun animateMarkerBounce(marker: Marker) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers[marker.id]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker.id)
|
||||
}
|
||||
|
||||
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 700
|
||||
interpolator = BounceInterpolator()
|
||||
addUpdateListener { state ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val t = max(1f - state.animatedValue as Float, 0f) / 2
|
||||
marker.setAnchor(0.5f, 1.0f + t)
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker.id)
|
||||
}, onCancel = {
|
||||
animatingMarkers.remove(marker.id)
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
animatingMarkers[marker.id] = anim
|
||||
anim.start()
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/net/vonforst/evmap/ui/NightModeUtils.kt
Normal file
14
app/src/main/java/net/vonforst/evmap/ui/NightModeUtils.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
"on" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"off" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.vonforst.evmap.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
@@ -33,10 +32,5 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
}
|
||||
return LocaleContextWrapper(ctx)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
fun setSystemLocale(config: Configuration, locale: Locale?) {
|
||||
config.setLocale(locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,12 +76,14 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO),
|
||||
manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_networks), "networks",
|
||||
@@ -185,6 +187,7 @@ data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int,
|
||||
val min: Int = 0,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
|
||||
@@ -93,6 +93,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
}
|
||||
val filteredConnectors: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>()
|
||||
}
|
||||
val filteredChargeCards: MutableLiveData<Set<Long>> by lazy {
|
||||
MutableLiveData<Set<Long>>()
|
||||
}
|
||||
|
||||
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
|
||||
MutableLiveData<ChargeLocation>()
|
||||
@@ -200,10 +206,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
chargepointLoader?.cancel()
|
||||
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
filteredConnectors.value = null
|
||||
filteredChargeCards.value = null
|
||||
val bounds = mapPosition.bounds
|
||||
val zoom = mapPosition.zoom
|
||||
chargepointLoader = viewModelScope.launch {
|
||||
chargepoints.value = getChargepointsWithFilters(bounds, zoom, filters)
|
||||
val result = getChargepointsWithFilters(bounds, zoom, filters)
|
||||
filteredConnectors.value = result.second
|
||||
filteredChargeCards.value = result.third
|
||||
chargepoints.value = result.first
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +222,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
|
||||
val freecharging = getBooleanValue(filters, "freecharging")
|
||||
val freeparking = getBooleanValue(filters, "freeparking")
|
||||
val open247 = getBooleanValue(filters, "open_247")
|
||||
@@ -223,21 +234,24 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
return Triple(Resource.success(emptyList()), null, null)
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
|
||||
|
||||
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Resource.success(emptyList())
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, null)
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
val filteredChargeCards =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
|
||||
|
||||
val networksVal = getMultipleChoiceValue(filters, "networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Resource.success(emptyList())
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
@@ -272,14 +286,22 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), chargepoints.value?.data)
|
||||
return Triple(
|
||||
Resource.error(response.message(), chargepoints.value?.data),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, chargepoints.value?.data)
|
||||
return Triple(
|
||||
Resource.error(e.message, chargepoints.value?.data),
|
||||
filteredConnectors,
|
||||
filteredChargeCards
|
||||
)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
@@ -301,7 +323,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
|
||||
return Resource.success(result)
|
||||
return Triple(Resource.success(result), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
|
||||
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
|
||||
@@ -317,6 +339,11 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
private fun getMultipleChoiceFilter(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = filters.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
private fun getMultipleChoiceValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.DataBindingAdaptersKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.DetailsAdapterKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Resource" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
@@ -31,6 +33,10 @@
|
||||
name="chargeCards"
|
||||
type="java.util.Map<Long, ChargeCard>" />
|
||||
|
||||
<variable
|
||||
name="filteredChargeCards"
|
||||
type="java.util.Set<Long>" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
@@ -188,7 +194,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{DataBindingAdaptersKt.buildDetails(charger.data, chargeCards, context)}"
|
||||
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -86,7 +86,9 @@
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:singleLine="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -127,7 +127,8 @@
|
||||
layout="@layout/detail_view"
|
||||
app:charger="@{vm.charger}"
|
||||
app:availability="@{vm.availability}"
|
||||
app:chargeCards="@{vm.chargeCardMap}" />
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
|
||||
type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
|
||||
type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:max="@{((SliderFilter) item.filter).max}"
|
||||
android:max="@{((SliderFilter) item.filter).max - ((SliderFilter) item.filter).min}"
|
||||
android:progress="@={progress}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView18"
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
<item>Englisch</item>
|
||||
<item>Deutsch</item>
|
||||
</string-array>
|
||||
<string-array name="pref_darkmode_names">
|
||||
<item>Geräteeinstellung verwenden</item>
|
||||
<item>immer an</item>
|
||||
<item>immer aus</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -25,8 +25,8 @@
|
||||
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
|
||||
<string name="go_to_goingelectric">Quelle: goingelectric.de</string>
|
||||
<string name="search">Suche</string>
|
||||
<string name="menu_map">Map</string>
|
||||
<string name="menu_favs">Favorites</string>
|
||||
<string name="menu_map">Karte</string>
|
||||
<string name="menu_favs">Favoriten</string>
|
||||
<string name="menu_filter">Filtern</string>
|
||||
<string name="not_implemented">noch nicht implementiert</string>
|
||||
<string name="about">Über EVMap</string>
|
||||
@@ -92,6 +92,8 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Sprache</string>
|
||||
<string name="pref_language_summary">App-Sprache ändern</string>
|
||||
<string name="pref_darkmode">Dunkles Design</string>
|
||||
<string name="pref_darkmode_summary">Einstellen, wann der Nachtmodus genutzt wird</string>
|
||||
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
|
||||
<string name="retry">Wiederholen</string>
|
||||
<string name="filter_open_247">24 Stunden geöffnet</string>
|
||||
|
||||
@@ -10,4 +10,14 @@
|
||||
<item>en</item>
|
||||
<item>de</item>
|
||||
</string-array>
|
||||
<string-array name="pref_darkmode_names">
|
||||
<item>Device default</item>
|
||||
<item>always on</item>
|
||||
<item>always off</item>
|
||||
</string-array>
|
||||
<string-array name="pref_darkmode_values" tranlatable="false">
|
||||
<item>default</item>
|
||||
<item>on</item>
|
||||
<item>off</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -91,6 +91,8 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Language</string>
|
||||
<string name="pref_language_summary">Change the app language</string>
|
||||
<string name="pref_darkmode">Dark mode</string>
|
||||
<string name="pref_darkmode_summary">Set when dark mode is activated</string>
|
||||
<string name="connection_error">Could not load charging stations</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="filter_open_247">Available 24/7</string>
|
||||
|
||||
@@ -18,5 +18,13 @@
|
||||
android:defaultValue="default"
|
||||
android:summary="@string/pref_language_summary" />
|
||||
|
||||
<ListPreference
|
||||
android:key="darkmode"
|
||||
android:title="@string/pref_darkmode"
|
||||
android:entries="@array/pref_darkmode_names"
|
||||
android:entryValues="@array/pref_darkmode_values"
|
||||
android:defaultValue="default"
|
||||
android:summary="@string/pref_darkmode_summary" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user