mirror of
https://github.com/ev-map/EVMap.git
synced 2026-04-23 23:57:08 -04:00
@@ -1,25 +1,31 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.app.Application
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.viewmodel.FilterViewModel
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(ctx)
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
private val maxRows = 6
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val checkedIcon =
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
@@ -47,11 +53,10 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val filterStatus =
|
||||
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
|
||||
?: FILTERS_DISABLED
|
||||
prefs.filterStatus.takeUnless { it == FILTERS_FAVORITES } ?: FILTERS_DISABLED
|
||||
return ListTemplate.Builder().apply {
|
||||
filterProfiles.value?.let {
|
||||
setSingleList(buildFilterProfilesList(it.take(maxRows), filterStatus))
|
||||
setSingleList(buildFilterProfilesList(it, filterStatus))
|
||||
} ?: setLoading(true)
|
||||
setTitle(carContext.getString(R.string.menu_filter))
|
||||
setHeaderAction(Action.BACK)
|
||||
@@ -62,6 +67,8 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
profiles: List<FilterProfile>,
|
||||
filterStatus: Long
|
||||
): ItemList {
|
||||
val extraRows = if (FILTERS_CUSTOM == filterStatus) 3 else 2
|
||||
val profilesToShow = profiles.take(maxRows - extraRows)
|
||||
return ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.no_filters))
|
||||
@@ -71,11 +78,10 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setImage(uncheckedIcon)
|
||||
}
|
||||
setOnClickListener {
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
screenManager.pop()
|
||||
onItemClick(FILTERS_DISABLED)
|
||||
}
|
||||
}.build())
|
||||
profiles.forEach {
|
||||
profilesToShow.forEach {
|
||||
addItem(Row.Builder().apply {
|
||||
val name =
|
||||
it.name.ifEmpty { carContext.getString(R.string.unnamed_filter_profile) }
|
||||
@@ -86,12 +92,188 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setImage(uncheckedIcon)
|
||||
}
|
||||
setOnClickListener {
|
||||
prefs.filterStatus = it.id
|
||||
screenManager.pop()
|
||||
onItemClick(it.id)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
setNoItemsMessage(carContext.getString(R.string.filterprofiles_empty_state))
|
||||
if (FILTERS_CUSTOM == filterStatus) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.filter_custom))
|
||||
setImage(checkedIcon)
|
||||
setOnClickListener {
|
||||
onItemClick(FILTERS_CUSTOM)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.menu_edit_filters))
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
lifecycleScope.launch {
|
||||
db.filterValueDao().copyFiltersToCustom(filterStatus, prefs.dataSource)
|
||||
screenManager.push(EditFiltersScreen(carContext))
|
||||
}
|
||||
})
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun onItemClick(id: Long) {
|
||||
prefs.filterStatus = id
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
private val vm = FilterViewModel(carContext.applicationContext as Application)
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
|
||||
init {
|
||||
vm.filtersWithValue.observe(this) {
|
||||
vm.filterProfile.observe(this) {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val currentProfileName = vm.filterProfile.value?.name
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
vm.filtersWithValue.value?.let { filtersWithValue ->
|
||||
setSingleList(buildFiltersList(filtersWithValue.take(maxRows)))
|
||||
} ?: setLoading(true)
|
||||
|
||||
setTitle(currentProfileName?.let {
|
||||
carContext.getString(
|
||||
R.string.edit_filter_profile,
|
||||
it
|
||||
)
|
||||
} ?: carContext.getString(R.string.menu_filter))
|
||||
|
||||
setHeaderAction(Action.BACK)
|
||||
setActionStrip(ActionStrip.Builder().apply {
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_check
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
vm.saveFilterValues()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_save
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
val textPromptScreen = TextPromptScreen(
|
||||
carContext,
|
||||
R.string.save_as_profile,
|
||||
R.string.save_profile_enter_name,
|
||||
currentProfileName
|
||||
)
|
||||
screenManager.pushForResult(textPromptScreen) { name ->
|
||||
if (name == null) return@pushForResult
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(name as String)
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildFiltersList(filters: List<FilterWithValue<out FilterValue>>): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
filters.forEach {
|
||||
val filter = it.filter
|
||||
val value = it.value
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(filter.name)
|
||||
when (filter) {
|
||||
is BooleanFilter -> {
|
||||
setToggle(Toggle.Builder {
|
||||
(value as BooleanFilterValue).value = it
|
||||
}.setChecked((value as BooleanFilterValue).value).build())
|
||||
}
|
||||
is MultipleChoiceFilter -> {
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
MultipleChoiceFilterScreen(
|
||||
carContext,
|
||||
filter,
|
||||
value as MultipleChoiceFilterValue
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is SliderFilter -> {
|
||||
// TODO: toggle through possible options on click?
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleChoiceFilterScreen(
|
||||
ctx: CarContext,
|
||||
val filter: MultipleChoiceFilter,
|
||||
val value: MultipleChoiceFilterValue
|
||||
) : MultiSelectSearchScreen<Pair<String, String>>(ctx) {
|
||||
override val isMultiSelect = true
|
||||
override val shouldShowSelectAll = true
|
||||
|
||||
override fun isSelected(it: Pair<String, String>): Boolean =
|
||||
value.all || value.values.contains(it.first)
|
||||
|
||||
override fun toggleSelected(item: Pair<String, String>) {
|
||||
if (isSelected(item)) {
|
||||
val values = if (value.all) filter.choices.keys else value.values
|
||||
value.values = values.minus(item.first).toMutableSet()
|
||||
value.all = false
|
||||
} else {
|
||||
value.values.add(item.first)
|
||||
if (value.values == filter.choices.keys) {
|
||||
value.all = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectAll() {
|
||||
value.all = true
|
||||
}
|
||||
|
||||
override fun selectNone() {
|
||||
value.all = false
|
||||
value.values = mutableSetOf()
|
||||
}
|
||||
|
||||
override fun getLabel(it: Pair<String, String>): String = it.second
|
||||
|
||||
override suspend fun loadData(): List<Pair<String, String>> {
|
||||
return filter.choices.entries.map { it.toPair() }
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
@@ -48,6 +47,10 @@ import kotlin.math.roundToInt
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
|
||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
||||
ItemList.OnItemVisibilityChangedListener {
|
||||
companion object {
|
||||
val MARKER = "map"
|
||||
}
|
||||
|
||||
private var updateCoroutine: Job? = null
|
||||
private var availabilityUpdateCoroutine: Job? = null
|
||||
|
||||
@@ -73,8 +76,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
private val filterStatus = MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
|
||||
?: FILTERS_DISABLED
|
||||
value = prefs.filterStatus.takeUnless { it == FILTERS_FAVORITES } ?: FILTERS_DISABLED
|
||||
}
|
||||
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
|
||||
private val filters =
|
||||
@@ -99,6 +101,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
filtersWithValue.observe(this) {
|
||||
loadChargers()
|
||||
}
|
||||
marker = MARKER
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -160,7 +163,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
screenManager.pushForResult(FilterScreen(carContext)) {
|
||||
chargers = null
|
||||
filterStatus.value =
|
||||
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
|
||||
prefs.filterStatus.takeUnless { it == FILTERS_FAVORITES }
|
||||
?: FILTERS_DISABLED
|
||||
}
|
||||
session.mapScreen = null
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.InputCallback
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.signin.InputSignInMethod
|
||||
import androidx.car.app.model.signin.SignInTemplate
|
||||
|
||||
class TextPromptScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val prompt: Int,
|
||||
val initialValue: String? = null
|
||||
) : Screen(ctx),
|
||||
InputCallback {
|
||||
override fun onGetTemplate(): Template {
|
||||
val signInMethod = InputSignInMethod.Builder(this).apply {
|
||||
initialValue?.let { setDefaultValue(it) }
|
||||
setShowKeyboardByDefault(true)
|
||||
}.build()
|
||||
return SignInTemplate.Builder(signInMethod).apply {
|
||||
setHeaderAction(Action.BACK)
|
||||
setInstructions(carContext.getString(prompt))
|
||||
setTitle(carContext.getString(title))
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onInputSubmitted(text: String) {
|
||||
setResult(text)
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.await
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Dao
|
||||
@@ -92,4 +93,15 @@ abstract class FilterValueDao {
|
||||
deleteSliderFilterValuesForProfile(profile, dataSource)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun copyFiltersToCustom(filterStatus: Long, dataSource: String) {
|
||||
if (filterStatus == FILTERS_CUSTOM) return
|
||||
|
||||
deleteFilterValuesForProfile(FILTERS_CUSTOM, dataSource)
|
||||
val values = getFilterValues(filterStatus, dataSource).await().onEach {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
}
|
||||
insert(*values.toTypedArray())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -270,15 +270,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
|
||||
suspend fun copyFiltersToCustom() {
|
||||
if (filterStatus.value == FILTERS_CUSTOM) return
|
||||
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
|
||||
filterValues.value?.map {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
it
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
filterStatus.value?.let { db.filterValueDao().copyFiltersToCustom(it, prefs.dataSource) }
|
||||
}
|
||||
|
||||
fun setMapType(type: AnyMap.Type) {
|
||||
|
||||
Reference in New Issue
Block a user