diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index 64fde378..65b0ed24 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -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() { - 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?, - filteredChargeCards: Set?, - ctx: Context -): List { - 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, - chargecardData: Map?, - filteredChargeCards: Set?, - 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() { @@ -228,194 +91,6 @@ class FavoritesAdapter(val vm: FavoritesViewModel) : override fun getItemId(position: Int): Long = getItem(position).charger.id } -class FiltersAdapter : DataBindingAdapter>() { - init { - setHasStableIds(true) - } - - val itemids = mutableMapOf() - 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>, - item: FilterWithValue - ) { - 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() - 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() { override fun getItemViewType(position: Int): Int = R.layout.item_donation } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt b/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt new file mode 100644 index 00000000..b3cfca34 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt @@ -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() { + 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?, + filteredChargeCards: Set?, + ctx: Context +): List { + 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, + chargecardData: Map?, + filteredChargeCards: Set?, + 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 +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt b/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt new file mode 100644 index 00000000..10dbb18f --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt @@ -0,0 +1,210 @@ +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>() { + init { + setHasStableIds(true) + } + + val itemids = mutableMapOf() + 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>, + item: FilterWithValue + ) { + 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() + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index 60c8f409..67ff6e01 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -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) { diff --git a/app/src/main/res/layout/item_detail.xml b/app/src/main/res/layout/item_detail.xml index f10c6f66..a77eab27 100644 --- a/app/src/main/res/layout/item_detail.xml +++ b/app/src/main/res/layout/item_detail.xml @@ -8,7 +8,7 @@ + type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" /> + type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />