mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 16:17:45 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d041513516 | ||
|
|
1effba77d1 | ||
|
|
df79f02e1d | ||
|
|
c4d44f9ddf | ||
|
|
6bec397133 | ||
|
|
474b621af0 | ||
|
|
36aeb201ca | ||
|
|
76a241d691 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 19
|
||||
versionName "0.2.1"
|
||||
versionCode 20
|
||||
versionName "0.2.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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.*
|
||||
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 kotlin.math.max
|
||||
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,130 +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>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
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, filteredChargeCards, 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>?,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
@@ -228,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 = 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -474,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) {
|
||||
|
||||
@@ -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.value }.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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,8 @@ 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),
|
||||
|
||||
@@ -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" />
|
||||
@@ -192,7 +194,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{DataBindingAdaptersKt.buildDetails(charger.data, chargeCards, filteredChargeCards, 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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user