From e644452d64e0e22a3907a043a62187a1fc5d5ff6 Mon Sep 17 00:00:00 2001 From: mcliquid Date: Fri, 13 Feb 2026 23:45:59 +0100 Subject: [PATCH 1/5] WIP First draft for Charging Station Socket Type quest So far running and tagging is working, UI is not beautiful, but works. --- .../streetcomplete/quests/QuestsModule.kt | 2 + .../AddChargingStationSocket.kt | 69 ++++++++++++++++ .../AddChargingStationSocketForm.kt | 80 +++++++++++++++++++ .../res/drawable/quest_charger_socket.xml | 41 ++++++++++ app/src/androidMain/res/values/strings.xml | 7 ++ .../drawable/socket_chademo.xml | 39 +++++++++ .../drawable/socket_domestic.xml | 15 ++++ .../drawable/socket_type2.xml | 33 ++++++++ .../drawable/socket_type2_cable.xml | 36 +++++++++ .../drawable/socket_type2_combo.xml | 42 ++++++++++ .../composeResources/values/strings.xml | 7 ++ .../streetcomplete/data/meta/CountryInfo.kt | 3 + .../charging_station_socket/SocketType.kt | 19 +++++ .../charging_station_socket/SocketTypeItem.kt | 22 +++++ .../charging station socket/chademo.svg | 26 ++++++ .../charging station socket/domestic.svg | 10 +++ .../charging station socket/type2.svg | 17 ++++ .../charging station socket/type2_cable.svg | 20 +++++ .../charging station socket/type2_combo.svg | 29 +++++++ res/graphics/quest/charger_socket.svg | 18 +++++ 20 files changed, 535 insertions(+) create mode 100644 app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt create mode 100644 app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt create mode 100644 app/src/androidMain/res/drawable/quest_charger_socket.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_chademo.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_domestic.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_type2.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_type2_cable.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_type2_combo.xml create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt create mode 100644 res/graphics/charging station socket/chademo.svg create mode 100644 res/graphics/charging station socket/domestic.svg create mode 100644 res/graphics/charging station socket/type2.svg create mode 100644 res/graphics/charging station socket/type2_cable.svg create mode 100644 res/graphics/charging station socket/type2_combo.svg create mode 100644 res/graphics/quest/charger_socket.svg diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt index 7f812c6600d..b2fefd15506 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt @@ -69,6 +69,7 @@ import de.westnordost.streetcomplete.quests.charging_station_bicycles.AddChargin import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationBicycleCapacity import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationCapacity import de.westnordost.streetcomplete.quests.charging_station_operator.AddChargingStationOperator +import de.westnordost.streetcomplete.quests.charging_station_socket.AddChargingStationSocket import de.westnordost.streetcomplete.quests.clothing_bin_operator.AddClothingBinOperator import de.westnordost.streetcomplete.quests.construction.MarkCompletedBuildingConstruction import de.westnordost.streetcomplete.quests.construction.MarkCompletedHighwayConstruction @@ -429,6 +430,7 @@ fun questTypeRegistry( 87 to AddChargingStationCapacity(), // after question for bicycles because user has possibility to answer that it is only for bicycles 179 to AddChargingStationBicycleCapacity(), 88 to AddChargingStationOperator(), + 197 to AddChargingStationSocket(), // postboxes (collection times are further up, see comment) 89 to AddPostboxRoyalCypher(), // can be glanced across the road (if postbox facing the right way) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt new file mode 100644 index 00000000000..f02ac30c94e --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt @@ -0,0 +1,69 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.Element +import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry +import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType +import de.westnordost.streetcomplete.data.quest.AndroidQuest +import de.westnordost.streetcomplete.data.quest.NoCountriesExcept +import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement +import de.westnordost.streetcomplete.osm.Tags + +class AddChargingStationSocket : + OsmElementQuestType>, + AndroidQuest { + + private val filter by lazy { + """ + nodes, ways with + amenity = charging_station + and bicycle != yes + and motorcar != no + """.toElementFilterExpression() + } + + override val enabledInCountries = NoCountriesExcept( + "AT","BE","BG","HR","CY","CZ","DE","DK","EE","ES","FI","FR","GB", + "GR","HU","IE","IS","IT","LI","LT","LU","LV","MT","NL","NO", + "PL","PT","RO","SE","SI","SK" + ) + + override val changesetComment = "Add charging station sockets" + override val wikiLink = "Key:socket" + override val icon = R.drawable.quest_charger_socket + override val achievements = listOf(EditTypeAchievement.CAR) + + override fun getTitle(tags: Map) = + R.string.quest_charging_station_socket_title + + override fun createForm() = AddChargingStationSocketForm() + + override fun isApplicableTo(element: Element): Boolean { + if (!filter.matches(element)) return false + + // skip if any socket:* tag already exists + if (element.tags.keys.any { it.startsWith("socket:") }) return false + + return true + } + + override fun getApplicableElements( + mapData: MapDataWithGeometry + ): Iterable = + mapData.filter { element -> + isApplicableTo(element) + } + + override fun applyAnswerTo( + answer: Set, + tags: Tags, + geometry: ElementGeometry, + timestampEdited: Long + ) { + answer.forEach { + tags["socket:${it.osmKey}"] = "yes" + } + } +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt new file mode 100644 index 00000000000..75627bdc7c0 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt @@ -0,0 +1,80 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import android.os.Bundle +import android.view.View +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.databinding.ComposeViewBinding +import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm +import de.westnordost.streetcomplete.ui.common.item_select.ItemsSelectGrid +import de.westnordost.streetcomplete.ui.common.item_select.ImageWithLabel +import de.westnordost.streetcomplete.ui.util.content +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +class AddChargingStationSocketForm : + AbstractOsmQuestForm>() { + + override val contentLayoutResId = R.layout.compose_view + private val binding by contentViewBinding(ComposeViewBinding::bind) + override val defaultExpanded = false + + private val selectedItems = mutableStateOf(emptySet()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.composeViewBase.content { Surface { + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + + CompositionLocalProvider( + LocalContentAlpha provides ContentAlpha.medium, + LocalTextStyle provides MaterialTheme.typography.body2 + ) { + Text(stringResource(R.string.quest_multiselect_hint)) + } + + ItemsSelectGrid( + columns = com.cheonjaeung.compose.grid.SimpleGridCells.Fixed(3), + items = SocketType.selectableValues, + selectedItems = selectedItems.value, + onSelect = { item, selected -> + if (!selected) { + selectedItems.value = selectedItems.value - item + } else { + selectedItems.value = selectedItems.value + item + } + checkIsFormComplete() + }, + modifier = androidx.compose.ui.Modifier.fillMaxWidth() + ) { + ImageWithLabel( + painterResource(it.icon), + stringResource(it.title) + ) + } + } + }} + } + + override fun onClickOk() { + applyAnswer(selectedItems.value) + } + + override fun isFormComplete() = selectedItems.value.isNotEmpty() +} diff --git a/app/src/androidMain/res/drawable/quest_charger_socket.xml b/app/src/androidMain/res/drawable/quest_charger_socket.xml new file mode 100644 index 00000000000..e0ca2b17cf2 --- /dev/null +++ b/app/src/androidMain/res/drawable/quest_charger_socket.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/app/src/androidMain/res/values/strings.xml b/app/src/androidMain/res/values/strings.xml index 7faad547e69..9897db29b9d 100644 --- a/app/src/androidMain/res/values/strings.xml +++ b/app/src/androidMain/res/values/strings.xml @@ -1056,6 +1056,13 @@ A level counts as a roof level when its windows are in the roof. Subsequently, r Who is the operator of this charging station? + Which sockets does this charging station have? + Type 2 (Mennekes) + Type 2 (Mennekes) Cable + Type 2 Combo (CCS) + CHAdeMO + Household-plug + Who accepts donations for this clothing bin? Is this building completed? diff --git a/app/src/commonMain/composeResources/drawable/socket_chademo.xml b/app/src/commonMain/composeResources/drawable/socket_chademo.xml new file mode 100644 index 00000000000..8378c3ee720 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_chademo.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_domestic.xml b/app/src/commonMain/composeResources/drawable/socket_domestic.xml new file mode 100644 index 00000000000..e3c4323f26b --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_domestic.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_type2.xml b/app/src/commonMain/composeResources/drawable/socket_type2.xml new file mode 100644 index 00000000000..c59048ae418 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_type2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_type2_cable.xml b/app/src/commonMain/composeResources/drawable/socket_type2_cable.xml new file mode 100644 index 00000000000..a1fa6545156 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_type2_cable.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_type2_combo.xml b/app/src/commonMain/composeResources/drawable/socket_type2_combo.xml new file mode 100644 index 00000000000..9b5e7e1b2f8 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_type2_combo.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml index 7faad547e69..9897db29b9d 100644 --- a/app/src/commonMain/composeResources/values/strings.xml +++ b/app/src/commonMain/composeResources/values/strings.xml @@ -1056,6 +1056,13 @@ A level counts as a roof level when its windows are in the roof. Subsequently, r Who is the operator of this charging station? + Which sockets does this charging station have? + Type 2 (Mennekes) + Type 2 (Mennekes) Cable + Type 2 Combo (CCS) + CHAdeMO + Household-plug + Who accepts donations for this clothing bin? Is this building completed? diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt index aad1232b4a6..588f529500b 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt @@ -41,6 +41,7 @@ data class IncompleteCountryInfo( val atmOperators: List? = null, val centerLineStyle: String? = null, val chargingStationOperators: List? = null, + val chargingStationSocket: List? = null, val clothesContainerOperators: List? = null, val edgeLineStyle: String? = null, val exclusiveCycleLaneStyle: String? = null, @@ -159,6 +160,8 @@ data class CountryInfo(private val infos: List) { get() = infos.firstNotNullOfOrNull { it.atmOperators } val chargingStationOperators: List? get() = infos.firstNotNullOfOrNull { it.chargingStationOperators } + val chargingStationSocket: List? + get() = infos.firstNotNullOfOrNull { it.chargingStationSocket } val clothesContainerOperators: List? get() = infos.firstNotNullOfOrNull { it.clothesContainerOperators } val livingStreetSignStyle: String? diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt new file mode 100644 index 00000000000..83ea2f7b76f --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt @@ -0,0 +1,19 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +enum class SocketType(val osmKey: String) { + TYPE2("type2"), + TYPE2_CABLE("type2_cable"), + TYPE2_COMBO("type2_combo"), + CHADEMO("chademo"), + DOMESTIC("domestic"); + + companion object { + val selectableValues = listOf( + TYPE2, + TYPE2_CABLE, + TYPE2_COMBO, + CHADEMO, + DOMESTIC + ) + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt new file mode 100644 index 00000000000..9ac6dd6c026 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt @@ -0,0 +1,22 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.* +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource + +val SocketType.icon: DrawableResource get() = when (this) { + SocketType.TYPE2 -> Res.drawable.socket_type2 + SocketType.TYPE2_CABLE -> Res.drawable.socket_type2_cable + SocketType.TYPE2_COMBO -> Res.drawable.socket_type2_combo + SocketType.CHADEMO -> Res.drawable.socket_chademo + SocketType.DOMESTIC -> Res.drawable.socket_domestic +} + +val SocketType.title: StringResource get() = when (this) { + SocketType.TYPE2 -> Res.string.quest_charging_station_socket_type2 + SocketType.TYPE2_CABLE -> Res.string.quest_charging_station_socket_type2_cable + SocketType.TYPE2_COMBO -> Res.string.quest_charging_station_socket_type2_combo + SocketType.CHADEMO -> Res.string.quest_charging_station_socket_chademo + SocketType.DOMESTIC -> Res.string.quest_charging_station_socket_domestic +} diff --git a/res/graphics/charging station socket/chademo.svg b/res/graphics/charging station socket/chademo.svg new file mode 100644 index 00000000000..7e147384c5d --- /dev/null +++ b/res/graphics/charging station socket/chademo.svg @@ -0,0 +1,26 @@ + + + + + + + + M + + + + + N + + + + + + + + + + + + + diff --git a/res/graphics/charging station socket/domestic.svg b/res/graphics/charging station socket/domestic.svg new file mode 100644 index 00000000000..8ee54acadca --- /dev/null +++ b/res/graphics/charging station socket/domestic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/graphics/charging station socket/type2.svg b/res/graphics/charging station socket/type2.svg new file mode 100644 index 00000000000..4fab45b0b66 --- /dev/null +++ b/res/graphics/charging station socket/type2.svg @@ -0,0 +1,17 @@ + + + + C + + + + + + + + + + + + + diff --git a/res/graphics/charging station socket/type2_cable.svg b/res/graphics/charging station socket/type2_cable.svg new file mode 100644 index 00000000000..8433fb5a569 --- /dev/null +++ b/res/graphics/charging station socket/type2_cable.svg @@ -0,0 +1,20 @@ + + + + + + + C + + + + + + + + + + + + + diff --git a/res/graphics/charging station socket/type2_combo.svg b/res/graphics/charging station socket/type2_combo.svg new file mode 100644 index 00000000000..7befc6cb4d2 --- /dev/null +++ b/res/graphics/charging station socket/type2_combo.svg @@ -0,0 +1,29 @@ + + + + + + L + + + + + + K + + + + + + + + + + + + + + + + + diff --git a/res/graphics/quest/charger_socket.svg b/res/graphics/quest/charger_socket.svg new file mode 100644 index 00000000000..17332c8545c --- /dev/null +++ b/res/graphics/quest/charger_socket.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From b3c41053f7b4ef4cb8b1a9ef2fbf4e7bb00845a7 Mon Sep 17 00:00:00 2001 From: mcliquid Date: Sun, 15 Feb 2026 10:45:35 +0100 Subject: [PATCH 2/5] Rebuild ChargingSocket-Quest to MultiStepForm (based on maxweight-Quest) --- .../AddChargingStationSocket.kt | 38 +++---- .../AddChargingStationSocketForm.kt | 103 +++++++++--------- .../ChargingSocketMultiStepForm.kt | 63 +++++++++++ .../charging_station_socket/SocketCount.kt | 14 +++ .../charging_station_socket/SocketType.kt | 11 +- 5 files changed, 148 insertions(+), 81 deletions(-) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt index f02ac30c94e..20bcd0bd8e2 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt @@ -8,11 +8,11 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType import de.westnordost.streetcomplete.data.quest.AndroidQuest import de.westnordost.streetcomplete.data.quest.NoCountriesExcept -import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement +import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR import de.westnordost.streetcomplete.osm.Tags class AddChargingStationSocket : - OsmElementQuestType>, + OsmElementQuestType>, AndroidQuest { private val filter by lazy { @@ -30,40 +30,30 @@ class AddChargingStationSocket : "PL","PT","RO","SE","SI","SK" ) - override val changesetComment = "Add charging station sockets" + override val changesetComment = "Specify charging station sockets" override val wikiLink = "Key:socket" override val icon = R.drawable.quest_charger_socket - override val achievements = listOf(EditTypeAchievement.CAR) + override val achievements = listOf(CAR) override fun getTitle(tags: Map) = R.string.quest_charging_station_socket_title override fun createForm() = AddChargingStationSocketForm() - override fun isApplicableTo(element: Element): Boolean { - if (!filter.matches(element)) return false - - // skip if any socket:* tag already exists - if (element.tags.keys.any { it.startsWith("socket:") }) return false - - return true - } - - override fun getApplicableElements( - mapData: MapDataWithGeometry - ): Iterable = - mapData.filter { element -> - isApplicableTo(element) - } - override fun applyAnswerTo( - answer: Set, + answer: List, tags: Tags, geometry: ElementGeometry, timestampEdited: Long ) { - answer.forEach { - tags["socket:${it.osmKey}"] = "yes" - } + answer.forEach { it.applyTo(tags) } + } + + override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable = + mapData.filter { isApplicableTo(it) } + + override fun isApplicableTo(element: Element): Boolean { + if (!filter.matches(element)) return false + return true } } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt index 75627bdc7c0..34a305bc7c1 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt @@ -2,79 +2,82 @@ package de.westnordost.streetcomplete.quests.charging_station_socket import android.os.Bundle import android.view.View -import androidx.compose.material.ContentAlpha -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme +import androidx.appcompat.app.AlertDialog import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.lifecycleScope import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.databinding.ComposeViewBinding import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm -import de.westnordost.streetcomplete.ui.common.item_select.ItemsSelectGrid -import de.westnordost.streetcomplete.ui.common.item_select.ImageWithLabel import de.westnordost.streetcomplete.ui.util.content -import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringResource +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach class AddChargingStationSocketForm : - AbstractOsmQuestForm>() { + AbstractOsmQuestForm>() { override val contentLayoutResId = R.layout.compose_view private val binding by contentViewBinding(ComposeViewBinding::bind) - override val defaultExpanded = false - private val selectedItems = mutableStateOf(emptySet()) + private var selectedTypes = mutableStateListOf() + private var socketCounts = mutableStateListOf() + + private val maxSockets = 50 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.composeViewBase.content { Surface { - - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + snapshotFlow { socketCounts.toList() } + .onEach { checkIsFormComplete() } + .launchIn(lifecycleScope) - CompositionLocalProvider( - LocalContentAlpha provides ContentAlpha.medium, - LocalTextStyle provides MaterialTheme.typography.body2 - ) { - Text(stringResource(R.string.quest_multiselect_hint)) - } - - ItemsSelectGrid( - columns = com.cheonjaeung.compose.grid.SimpleGridCells.Fixed(3), - items = SocketType.selectableValues, - selectedItems = selectedItems.value, - onSelect = { item, selected -> - if (!selected) { - selectedItems.value = selectedItems.value - item - } else { - selectedItems.value = selectedItems.value + item + binding.composeViewBase.content { + Surface { + ChargingSocketMultiStepForm( + selectedTypes = selectedTypes, + socketCounts = socketCounts, + onTypeSelected = { type -> + if (!selectedTypes.contains(type)) { + selectedTypes.add(type) } - checkIsFormComplete() }, - modifier = androidx.compose.ui.Modifier.fillMaxWidth() - ) { - ImageWithLabel( - painterResource(it.icon), - stringResource(it.title) - ) - } + onTypeDeselected = { type -> + selectedTypes.remove(type) + socketCounts.removeAll { it.type == type } + }, + onCountChanged = { type, count -> + socketCounts.removeAll { it.type == type } + socketCounts.add(SocketCount(type, count)) + } + ) } - }} + } } override fun onClickOk() { - applyAnswer(selectedItems.value) + if (socketCounts.any { it.count <= 0 || it.count > maxSockets }) { + confirmUnusualInput { + applyAnswer(socketCounts) + } + } else { + applyAnswer(socketCounts) + } } - override fun isFormComplete() = selectedItems.value.isNotEmpty() + override fun isFormComplete(): Boolean = + socketCounts.isNotEmpty() && + socketCounts.all { it.count > 0 } + + private fun confirmUnusualInput(callback: () -> Unit) { + activity?.let { + AlertDialog.Builder(it) + .setTitle(R.string.quest_generic_confirmation_title) + .setMessage(R.string.quest_maxweight_unusualInput_confirmation_description) + .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> callback() } + .setNegativeButton(R.string.quest_generic_confirmation_no, null) + .show() + } + } } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt new file mode 100644 index 00000000000..0eed2a79567 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt @@ -0,0 +1,63 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.cheonjaeung.compose.grid.SimpleGridCells +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.quest_multiselect_hint +import de.westnordost.streetcomplete.ui.common.item_select.ItemsSelectGrid +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun ChargingSocketMultiStepForm( + selectedTypes: List, + socketCounts: List, + onTypeSelected: (SocketType) -> Unit, + onTypeDeselected: (SocketType) -> Unit, + onCountChanged: (SocketType, Int) -> Unit +) { + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + + Text(stringResource(Res.string.quest_multiselect_hint)) + + ItemsSelectGrid( + columns = SimpleGridCells.Fixed(3), + items = SocketType.selectableValues, + selectedItems = selectedTypes.toSet(), + onSelect = { type, selected -> + if (selected) onTypeSelected(type) + else onTypeDeselected(type) + } + ) { + Icon(painterResource(it.icon), null) + Text(stringResource(it.title)) + } + + selectedTypes.forEach { type -> + var value by remember { mutableStateOf(1) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(type.title)) + TextField( + value = value.toString(), + onValueChange = { + val int = it.toIntOrNull() + if (int != null) { + value = int + onCountChanged(type, int) + } + }, + modifier = Modifier.width(80.dp) + ) + } + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt new file mode 100644 index 00000000000..66a2ae03094 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt @@ -0,0 +1,14 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import de.westnordost.streetcomplete.osm.Tags +import kotlinx.serialization.Serializable + +@Serializable +data class SocketCount( + val type: SocketType, + val count: Int +) + +fun SocketCount.applyTo(tags: Tags) { + tags["socket:${type.osmKey}"] = count.toString() +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt index 83ea2f7b76f..d7b8140cac9 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt @@ -1,5 +1,8 @@ package de.westnordost.streetcomplete.quests.charging_station_socket +import kotlinx.serialization.Serializable + +@Serializable enum class SocketType(val osmKey: String) { TYPE2("type2"), TYPE2_CABLE("type2_cable"), @@ -8,12 +11,6 @@ enum class SocketType(val osmKey: String) { DOMESTIC("domestic"); companion object { - val selectableValues = listOf( - TYPE2, - TYPE2_CABLE, - TYPE2_COMBO, - CHADEMO, - DOMESTIC - ) + val selectableValues = entries } } From 4ad28c705b33afe19f805b50e2b37dae7c0a8567 Mon Sep 17 00:00:00 2001 From: mcliquid Date: Mon, 16 Feb 2026 21:50:53 +0100 Subject: [PATCH 3/5] Rebuild UI from MultiStepForm to Grid Form with Icons --- .../AddChargingStationSocket.kt | 38 +++++++--- .../AddChargingStationSocketForm.kt | 68 +++++++++-------- .../drawable/socket_eu_chademo.xml | 15 ++++ .../drawable/socket_eu_domestic.xml | 15 ++++ .../drawable/socket_eu_type2.xml | 15 ++++ .../drawable/socket_eu_type2_combo.xml | 15 ++++ .../ChargingSocketMultiStepForm.kt | 63 ---------------- .../charging_station_socket/SocketCount.kt | 14 ---- .../charging_station_socket/SocketType.kt | 45 +++++++++++- .../SocketTypeAndCountForm.kt | 73 +++++++++++++++++++ .../charging_station_socket/SocketTypeItem.kt | 22 ------ .../socket_eu_type2.svg | 8 ++ 12 files changed, 252 insertions(+), 139 deletions(-) create mode 100644 app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_eu_type2.xml create mode 100644 app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml delete mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt delete mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt delete mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt create mode 100644 res/graphics/charging station socket/socket_eu_type2.svg diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt index 20bcd0bd8e2..b0fadce7dbb 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt @@ -12,7 +12,7 @@ import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement. import de.westnordost.streetcomplete.osm.Tags class AddChargingStationSocket : - OsmElementQuestType>, + OsmElementQuestType>, AndroidQuest { private val filter by lazy { @@ -40,20 +40,40 @@ class AddChargingStationSocket : override fun createForm() = AddChargingStationSocketForm() + override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable = + mapData.filter { isApplicableTo(it) } + + override fun isApplicableTo(element: Element): Boolean { + if (!filter.matches(element)) return false + + // Only show if NO socket:* exists + if (element.tags.keys.any { it.startsWith("socket:") }) { + return false + } + + return true + } + override fun applyAnswerTo( - answer: List, + answer: Map, tags: Tags, geometry: ElementGeometry, timestampEdited: Long ) { - answer.forEach { it.applyTo(tags) } - } - override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable = - mapData.filter { isApplicableTo(it) } + answer.forEach { (type, count) -> + if (count > 0) { + tags["socket:${type.osmKey}"] = count.toString() + } + } - override fun isApplicableTo(element: Element): Boolean { - if (!filter.matches(element)) return false - return true + // Special rule: + // If type2 > 0 AND type2_cable == 0 → explicitly tag no + val type2 = answer[SocketType.TYPE2] ?: 0 + val cable = answer[SocketType.TYPE2_CABLE] ?: 0 + + if (type2 > 0 && cable == 0) { + tags["socket:type2_cable"] = "no" + } } } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt index 34a305bc7c1..0ac8be83fae 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt @@ -3,10 +3,16 @@ package de.westnordost.streetcomplete.quests.charging_station_socket import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Surface -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf +import androidx.compose.material.Text +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.databinding.ComposeViewBinding @@ -14,42 +20,42 @@ import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm import de.westnordost.streetcomplete.ui.util.content import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource class AddChargingStationSocketForm : - AbstractOsmQuestForm>() { + AbstractOsmQuestForm>() { override val contentLayoutResId = R.layout.compose_view private val binding by contentViewBinding(ComposeViewBinding::bind) - private var selectedTypes = mutableStateListOf() - private var socketCounts = mutableStateListOf() + private val socketCounts = mutableStateMapOf() - private val maxSockets = 50 + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + SocketType.selectableValues.forEach { + socketCounts[it] = 0 + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - snapshotFlow { socketCounts.toList() } + snapshotFlow { socketCounts.values.sum() } .onEach { checkIsFormComplete() } .launchIn(lifecycleScope) binding.composeViewBase.content { Surface { - ChargingSocketMultiStepForm( - selectedTypes = selectedTypes, - socketCounts = socketCounts, - onTypeSelected = { type -> - if (!selectedTypes.contains(type)) { - selectedTypes.add(type) - } - }, - onTypeDeselected = { type -> - selectedTypes.remove(type) - socketCounts.removeAll { it.type == type } + SocketTypeAndCountForm( + counts = socketCounts, + onIncrement = { type -> + val current = socketCounts[type] ?: 0 + if (current < 50) socketCounts[type] = current + 1 }, - onCountChanged = { type, count -> - socketCounts.removeAll { it.type == type } - socketCounts.add(SocketCount(type, count)) + onDecrement = { type -> + val current = socketCounts[type] ?: 0 + if (current > 0) socketCounts[type] = current - 1 } ) } @@ -57,18 +63,16 @@ class AddChargingStationSocketForm : } override fun onClickOk() { - if (socketCounts.any { it.count <= 0 || it.count > maxSockets }) { - confirmUnusualInput { - applyAnswer(socketCounts) - } + if (hasUnusualInput()) { + confirmUnusualInput { applyAnswer(socketCounts.toMap()) } } else { - applyAnswer(socketCounts) + applyAnswer(socketCounts.toMap()) } } - override fun isFormComplete(): Boolean = - socketCounts.isNotEmpty() && - socketCounts.all { it.count > 0 } + private fun hasUnusualInput(): Boolean { + return socketCounts.values.any { it == 0 || it > 50 } + } private fun confirmUnusualInput(callback: () -> Unit) { activity?.let { @@ -80,4 +84,10 @@ class AddChargingStationSocketForm : .show() } } + + override fun isFormComplete(): Boolean = + socketCounts.values.any { it > 0 } + + override fun isRejectingClose(): Boolean = + socketCounts.values.any { it > 0 } } diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml b/app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml b/app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_type2.xml b/app/src/commonMain/composeResources/drawable/socket_eu_type2.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_type2.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml b/app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt deleted file mode 100644 index 0eed2a79567..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/ChargingSocketMultiStepForm.kt +++ /dev/null @@ -1,63 +0,0 @@ -package de.westnordost.streetcomplete.quests.charging_station_socket - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.cheonjaeung.compose.grid.SimpleGridCells -import de.westnordost.streetcomplete.resources.Res -import de.westnordost.streetcomplete.resources.quest_multiselect_hint -import de.westnordost.streetcomplete.ui.common.item_select.ItemsSelectGrid -import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringResource - -@Composable -fun ChargingSocketMultiStepForm( - selectedTypes: List, - socketCounts: List, - onTypeSelected: (SocketType) -> Unit, - onTypeDeselected: (SocketType) -> Unit, - onCountChanged: (SocketType, Int) -> Unit -) { - - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - - Text(stringResource(Res.string.quest_multiselect_hint)) - - ItemsSelectGrid( - columns = SimpleGridCells.Fixed(3), - items = SocketType.selectableValues, - selectedItems = selectedTypes.toSet(), - onSelect = { type, selected -> - if (selected) onTypeSelected(type) - else onTypeDeselected(type) - } - ) { - Icon(painterResource(it.icon), null) - Text(stringResource(it.title)) - } - - selectedTypes.forEach { type -> - var value by remember { mutableStateOf(1) } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(stringResource(type.title)) - TextField( - value = value.toString(), - onValueChange = { - val int = it.toIntOrNull() - if (int != null) { - value = int - onCountChanged(type, int) - } - }, - modifier = Modifier.width(80.dp) - ) - } - } - } -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt deleted file mode 100644 index 66a2ae03094..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketCount.kt +++ /dev/null @@ -1,14 +0,0 @@ -package de.westnordost.streetcomplete.quests.charging_station_socket - -import de.westnordost.streetcomplete.osm.Tags -import kotlinx.serialization.Serializable - -@Serializable -data class SocketCount( - val type: SocketType, - val count: Int -) - -fun SocketCount.applyTo(tags: Tags) { - tags["socket:${type.osmKey}"] = count.toString() -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt index d7b8140cac9..96c93d1d3ba 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt @@ -1,8 +1,9 @@ package de.westnordost.streetcomplete.quests.charging_station_socket -import kotlinx.serialization.Serializable +import de.westnordost.streetcomplete.resources.* +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource -@Serializable enum class SocketType(val osmKey: String) { TYPE2("type2"), TYPE2_CABLE("type2_cable"), @@ -14,3 +15,43 @@ enum class SocketType(val osmKey: String) { val selectableValues = entries } } + +/* ---------------------------------------------------------- + Primary socket icon + ---------------------------------------------------------- */ + +val SocketType.icon: DrawableResource + get() = when (this) { + SocketType.TYPE2 -> Res.drawable.socket_type2 + SocketType.TYPE2_CABLE -> Res.drawable.socket_type2_cable + SocketType.TYPE2_COMBO -> Res.drawable.socket_type2_combo + SocketType.CHADEMO -> Res.drawable.socket_chademo + SocketType.DOMESTIC -> Res.drawable.socket_domestic + } + +/* ---------------------------------------------------------- + EU compatibility label (hexagon symbol) + Each socket gets its own EU-label icon. + ---------------------------------------------------------- */ + +val SocketType.euLabel: DrawableResource + get() = when (this) { + SocketType.TYPE2 -> Res.drawable.socket_eu_type2 + SocketType.TYPE2_CABLE -> Res.drawable.socket_eu_type2 + SocketType.TYPE2_COMBO -> Res.drawable.socket_eu_type2_combo + SocketType.CHADEMO -> Res.drawable.socket_eu_chademo + SocketType.DOMESTIC -> Res.drawable.socket_eu_domestic + } + +/* ---------------------------------------------------------- + Localized title + ---------------------------------------------------------- */ + +val SocketType.title: StringResource + get() = when (this) { + SocketType.TYPE2 -> Res.string.quest_charging_station_socket_type2 + SocketType.TYPE2_CABLE -> Res.string.quest_charging_station_socket_type2_cable + SocketType.TYPE2_COMBO -> Res.string.quest_charging_station_socket_type2_combo + SocketType.CHADEMO -> Res.string.quest_charging_station_socket_chademo + SocketType.DOMESTIC -> Res.string.quest_charging_station_socket_domestic + } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt new file mode 100644 index 00000000000..4e0921f2b8d --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt @@ -0,0 +1,73 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun SocketTypeAndCountForm( + counts: Map, + onIncrement: (SocketType) -> Unit, + onDecrement: (SocketType) -> Unit, + modifier: Modifier = Modifier +) { + + Column( + modifier = modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + SocketType.selectableValues.forEach { type -> + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + + Row(verticalAlignment = Alignment.CenterVertically) { + + Icon( + painter = painterResource(type.icon), + contentDescription = null, + modifier = Modifier.size(32.dp) + ) + + Spacer(Modifier.width(8.dp)) + + Icon( + painter = painterResource(type.euLabel), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + + Spacer(Modifier.width(12.dp)) + + Text(stringResource(type.title)) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + + IconButton(onClick = { onDecrement(type) }) { + Text("-") + } + + Text( + text = (counts[type] ?: 0).toString(), + modifier = Modifier.width(32.dp), + ) + + IconButton(onClick = { onIncrement(type) }) { + Text("+") + } + } + } + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt deleted file mode 100644 index 9ac6dd6c026..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeItem.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.westnordost.streetcomplete.quests.charging_station_socket - -import de.westnordost.streetcomplete.resources.Res -import de.westnordost.streetcomplete.resources.* -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.StringResource - -val SocketType.icon: DrawableResource get() = when (this) { - SocketType.TYPE2 -> Res.drawable.socket_type2 - SocketType.TYPE2_CABLE -> Res.drawable.socket_type2_cable - SocketType.TYPE2_COMBO -> Res.drawable.socket_type2_combo - SocketType.CHADEMO -> Res.drawable.socket_chademo - SocketType.DOMESTIC -> Res.drawable.socket_domestic -} - -val SocketType.title: StringResource get() = when (this) { - SocketType.TYPE2 -> Res.string.quest_charging_station_socket_type2 - SocketType.TYPE2_CABLE -> Res.string.quest_charging_station_socket_type2_cable - SocketType.TYPE2_COMBO -> Res.string.quest_charging_station_socket_type2_combo - SocketType.CHADEMO -> Res.string.quest_charging_station_socket_chademo - SocketType.DOMESTIC -> Res.string.quest_charging_station_socket_domestic -} diff --git a/res/graphics/charging station socket/socket_eu_type2.svg b/res/graphics/charging station socket/socket_eu_type2.svg new file mode 100644 index 00000000000..5b5eb67ed0b --- /dev/null +++ b/res/graphics/charging station socket/socket_eu_type2.svg @@ -0,0 +1,8 @@ + + + + + + C + + \ No newline at end of file From a0640565b8f8cb766de4f9ecdcb58320ea1f4f09 Mon Sep 17 00:00:00 2001 From: mcliquid Date: Mon, 16 Feb 2026 22:19:20 +0100 Subject: [PATCH 4/5] Getting StepperButton from HouseNumber-Quest --- .../AddChargingStationSocket.kt | 33 +++-- .../AddChargingStationSocketForm.kt | 63 +++------ .../SocketTypeAndCountForm.kt | 124 +++++++++++------- 3 files changed, 117 insertions(+), 103 deletions(-) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt index b0fadce7dbb..a2d1c03e139 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt @@ -46,12 +46,12 @@ class AddChargingStationSocket : override fun isApplicableTo(element: Element): Boolean { if (!filter.matches(element)) return false - // Only show if NO socket:* exists - if (element.tags.keys.any { it.startsWith("socket:") }) { - return false + // Exclude if any valid socket:* with numeric value exists + val hasValidSocket = element.tags.any { + it.key.startsWith("socket:") && it.value.toIntOrNull() != null } - return true + return !hasValidSocket } override fun applyAnswerTo( @@ -61,18 +61,25 @@ class AddChargingStationSocket : timestampEdited: Long ) { - answer.forEach { (type, count) -> - if (count > 0) { - tags["socket:${type.osmKey}"] = count.toString() - } + // Cleanup deprecated keys + tags.keys + .filter { it.startsWith("socket:tesla") || it == "socket:css" } + .forEach { tags.remove(it) } + + // Remove old supported socket keys + SocketType.selectableValues.forEach { + tags.remove("socket:${it.osmKey}") } - // Special rule: - // If type2 > 0 AND type2_cable == 0 → explicitly tag no - val type2 = answer[SocketType.TYPE2] ?: 0 - val cable = answer[SocketType.TYPE2_CABLE] ?: 0 + // Apply new values + answer.forEach { (type, count) -> + tags["socket:${type.osmKey}"] = count.toString() + } - if (type2 > 0 && cable == 0) { + // type2_cable=no logic + if (answer.containsKey(SocketType.TYPE2) && + !answer.containsKey(SocketType.TYPE2_CABLE) + ) { tags["socket:type2_cable"] = "no" } } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt index 0ac8be83fae..2a599b96d7b 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt @@ -3,25 +3,12 @@ package de.westnordost.streetcomplete.quests.charging_station_socket import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.lifecycleScope import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.databinding.ComposeViewBinding import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm import de.westnordost.streetcomplete.ui.util.content -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringResource class AddChargingStationSocketForm : AbstractOsmQuestForm>() { @@ -29,33 +16,19 @@ class AddChargingStationSocketForm : override val contentLayoutResId = R.layout.compose_view private val binding by contentViewBinding(ComposeViewBinding::bind) - private val socketCounts = mutableStateMapOf() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - SocketType.selectableValues.forEach { - socketCounts[it] = 0 - } - } + private val counts = mutableStateMapOf() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - snapshotFlow { socketCounts.values.sum() } - .onEach { checkIsFormComplete() } - .launchIn(lifecycleScope) - binding.composeViewBase.content { Surface { SocketTypeAndCountForm( - counts = socketCounts, - onIncrement = { type -> - val current = socketCounts[type] ?: 0 - if (current < 50) socketCounts[type] = current + 1 - }, - onDecrement = { type -> - val current = socketCounts[type] ?: 0 - if (current > 0) socketCounts[type] = current - 1 + counts = counts, + onCountsChanged = { + counts.clear() + counts.putAll(it) + checkIsFormComplete() } ) } @@ -63,31 +36,29 @@ class AddChargingStationSocketForm : } override fun onClickOk() { - if (hasUnusualInput()) { - confirmUnusualInput { applyAnswer(socketCounts.toMap()) } + if (counts.values.any { it !in 1..50 }) { + confirmUnusualInput() } else { - applyAnswer(socketCounts.toMap()) + applyAnswer(counts.toMap()) } } - private fun hasUnusualInput(): Boolean { - return socketCounts.values.any { it == 0 || it > 50 } - } - - private fun confirmUnusualInput(callback: () -> Unit) { + private fun confirmUnusualInput() { activity?.let { AlertDialog.Builder(it) .setTitle(R.string.quest_generic_confirmation_title) .setMessage(R.string.quest_maxweight_unusualInput_confirmation_description) - .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> callback() } + .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> + applyAnswer(counts.toMap()) + } .setNegativeButton(R.string.quest_generic_confirmation_no, null) .show() } } - override fun isFormComplete(): Boolean = - socketCounts.values.any { it > 0 } + override fun isFormComplete() = + counts.isNotEmpty() - override fun isRejectingClose(): Boolean = - socketCounts.values.any { it > 0 } + override fun isRejectingClose() = + counts.isNotEmpty() } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt index 4e0921f2b8d..5d32d660b6e 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt @@ -1,72 +1,108 @@ package de.westnordost.streetcomplete.quests.charging_station_socket import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.common.StepperButton import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Arrangement @Composable fun SocketTypeAndCountForm( counts: Map, - onIncrement: (SocketType) -> Unit, - onDecrement: (SocketType) -> Unit, + onCountsChanged: (Map) -> Unit, modifier: Modifier = Modifier ) { - Column( - modifier = modifier.padding(16.dp), + modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { SocketType.selectableValues.forEach { type -> + SocketRow( + type = type, + count = counts[type] ?: 0, + onIncrease = { + val newCount = (counts[type] ?: 0) + 1 + if (newCount <= 50) { + val newMap = counts.toMutableMap() + newMap[type] = newCount + onCountsChanged(newMap) + } + }, + onDecrease = { + val current = counts[type] ?: 0 + val newMap = counts.toMutableMap() + if (current <= 1) { + newMap.remove(type) + } else { + newMap[type] = current - 1 + } + onCountsChanged(newMap) + } + ) + } + } +} +@Composable +private fun SocketRow( + type: SocketType, + count: Int, + onIncrease: () -> Unit, + onDecrease: () -> Unit +) { + Surface { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + + // LEFT: ICON + EU HEX + LABEL Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.weight(1f) ) { + Icon( + painter = painterResource(type.icon), + contentDescription = null + ) + Icon( + painter = painterResource(type.euLabel), + contentDescription = null + ) + Text( + text = stringResource(type.title), + style = MaterialTheme.typography.body1 + ) + } - Row(verticalAlignment = Alignment.CenterVertically) { - - Icon( - painter = painterResource(type.icon), - contentDescription = null, - modifier = Modifier.size(32.dp) - ) - - Spacer(Modifier.width(8.dp)) - - Icon( - painter = painterResource(type.euLabel), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - - Spacer(Modifier.width(12.dp)) - - Text(stringResource(type.title)) - } - - Row(verticalAlignment = Alignment.CenterVertically) { - - IconButton(onClick = { onDecrement(type) }) { - Text("-") - } - - Text( - text = (counts[type] ?: 0).toString(), - modifier = Modifier.width(32.dp), - ) + // RIGHT: COUNTER + STEPPER + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = count.toString(), + style = MaterialTheme.typography.h6 + ) - IconButton(onClick = { onIncrement(type) }) { - Text("+") - } - } + StepperButton( + onIncrease = onIncrease, + onDecrease = onDecrease, + increaseEnabled = count < 50, + decreaseEnabled = count > 0 + ) } } } From 62d9c862d169b01a649d210a585afa682a579c43 Mon Sep 17 00:00:00 2001 From: mcliquid Date: Tue, 17 Feb 2026 15:17:35 +0100 Subject: [PATCH 5/5] Still WIP but working Grid Version of the UI --- .../AddChargingStationSocket.kt | 79 +++++++++++++++---- app/src/androidMain/res/values/strings.xml | 2 +- .../composeResources/values/strings.xml | 2 +- .../SocketTypeAndCountForm.kt | 68 +++++++++++----- 4 files changed, 113 insertions(+), 38 deletions(-) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt index a2d1c03e139..3d0b22dfe2b 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt @@ -5,11 +5,13 @@ import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpressio import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry import de.westnordost.streetcomplete.data.osm.mapdata.Element import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.Way import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType import de.westnordost.streetcomplete.data.quest.AndroidQuest import de.westnordost.streetcomplete.data.quest.NoCountriesExcept import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR import de.westnordost.streetcomplete.osm.Tags +import de.westnordost.streetcomplete.util.math.contains class AddChargingStationSocket : OsmElementQuestType>, @@ -40,18 +42,57 @@ class AddChargingStationSocket : override fun createForm() = AddChargingStationSocketForm() - override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable = - mapData.filter { isApplicableTo(it) } + override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable { + return mapData + .filter { element -> filter.matches(element) } + .filter { element -> isApplicableTo(element, mapData) } + .asIterable() + } + + override fun isApplicableTo(element: Element): Boolean? { + // This variant must exist because OsmElementQuestType requires it. + // But we delegate real logic to the overloaded version. + return null + } + + private fun isApplicableTo( + element: Element, + mapData: MapDataWithGeometry + ): Boolean { - override fun isApplicableTo(element: Element): Boolean { if (!filter.matches(element)) return false - // Exclude if any valid socket:* with numeric value exists - val hasValidSocket = element.tags.any { - it.key.startsWith("socket:") && it.value.toIntOrNull() != null + // Skip charge_point nodes completely + if (element.tags["man_made"] == "charge_point") return false + + // Skip charging_station areas that contain charge_points + if (element is Way) { + + val geometry = mapData.getGeometry(element.type, element.id) + ?: return true + + val bounds = geometry.bounds + + val hasChargePointsInside = mapData + .filter { it.tags["man_made"] == "charge_point" } + .any { cp -> + val cpGeometry = mapData.getGeometry(cp.type, cp.id) + ?: return@any false + + bounds.contains(cpGeometry.center) + } + + if (hasChargePointsInside) return false } - return !hasValidSocket + val socketTags = element.tags + .filterKeys { it.startsWith("socket:") } + + if (socketTags.isEmpty()) return true + if (socketTags.keys.any { isDeprecatedSocketKey(it) }) return true + if (socketTags.values.any { it == "yes" }) return true + + return false } override fun applyAnswerTo( @@ -63,24 +104,32 @@ class AddChargingStationSocket : // Cleanup deprecated keys tags.keys - .filter { it.startsWith("socket:tesla") || it == "socket:css" } + .filter { isDeprecatedSocketKey(it) } + .toList() .forEach { tags.remove(it) } - // Remove old supported socket keys - SocketType.selectableValues.forEach { - tags.remove("socket:${it.osmKey}") - } + // Remove old socket:* keys + tags.keys + .filter { it.startsWith("socket:") } + .toList() + .forEach { tags.remove(it) } // Apply new values answer.forEach { (type, count) -> tags["socket:${type.osmKey}"] = count.toString() } - // type2_cable=no logic - if (answer.containsKey(SocketType.TYPE2) && - !answer.containsKey(SocketType.TYPE2_CABLE) + // type2/type2_cable=no logic + if (answer.containsKey(SocketType.TYPE2) + && !answer.containsKey(SocketType.TYPE2_CABLE) ) { tags["socket:type2_cable"] = "no" } } + + private fun isDeprecatedSocketKey(key: String): Boolean = + key.startsWith("socket:tesla") || + key == "socket:css" || + key == "socket:unknown" || + key == "socket:type" } diff --git a/app/src/androidMain/res/values/strings.xml b/app/src/androidMain/res/values/strings.xml index 9897db29b9d..145dc0761c9 100644 --- a/app/src/androidMain/res/values/strings.xml +++ b/app/src/androidMain/res/values/strings.xml @@ -1056,7 +1056,7 @@ A level counts as a roof level when its windows are in the roof. Subsequently, r Who is the operator of this charging station? - Which sockets does this charging station have? + How many sockets does this charging station have? Type 2 (Mennekes) Type 2 (Mennekes) Cable Type 2 Combo (CCS) diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml index 9897db29b9d..145dc0761c9 100644 --- a/app/src/commonMain/composeResources/values/strings.xml +++ b/app/src/commonMain/composeResources/values/strings.xml @@ -1056,7 +1056,7 @@ A level counts as a roof level when its windows are in the roof. Subsequently, r Who is the operator of this charging station? - Which sockets does this charging station have? + How many sockets does this charging station have? Type 2 (Mennekes) Type 2 (Mennekes) Cable Type 2 Combo (CCS) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt index 5d32d660b6e..10f76f3d3d3 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt @@ -1,18 +1,20 @@ package de.westnordost.streetcomplete.quests.charging_station_socket +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.common.StepperButton import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import androidx.compose.material.Icon -import androidx.compose.foundation.layout.Arrangement @Composable fun SocketTypeAndCountForm( @@ -25,11 +27,14 @@ fun SocketTypeAndCountForm( verticalArrangement = Arrangement.spacedBy(12.dp) ) { SocketType.selectableValues.forEach { type -> + + val count = counts[type] ?: 0 + SocketRow( type = type, - count = counts[type] ?: 0, + count = count, onIncrease = { - val newCount = (counts[type] ?: 0) + 1 + val newCount = count + 1 if (newCount <= 50) { val newMap = counts.toMutableMap() newMap[type] = newCount @@ -37,12 +42,11 @@ fun SocketTypeAndCountForm( } }, onDecrease = { - val current = counts[type] ?: 0 val newMap = counts.toMutableMap() - if (current <= 1) { + if (count <= 1) { newMap.remove(type) } else { - newMap[type] = current - 1 + newMap[type] = count - 1 } onCountsChanged(newMap) } @@ -62,40 +66,62 @@ private fun SocketRow( Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + .padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically ) { - // LEFT: ICON + EU HEX + LABEL + // LEFT: SOCKET ICON + EU HEX + LABEL Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically ) { + + // Socket icon (bigger) Icon( painter = painterResource(type.icon), - contentDescription = null + contentDescription = null, + modifier = Modifier.size(60.dp) ) + + Spacer(Modifier.width(4.dp)) + + // EU Hex (smaller) Icon( painter = painterResource(type.euLabel), - contentDescription = null + contentDescription = null, + modifier = Modifier.size(32.dp) ) + + Spacer(Modifier.width(12.dp)) + Text( text = stringResource(type.title), style = MaterialTheme.typography.body1 ) } - // RIGHT: COUNTER + STEPPER + // RIGHT: COUNT + STEPPER Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - text = count.toString(), - style = MaterialTheme.typography.h6 - ) + + // Number with Border + Box( + modifier = Modifier + .border( + width = 1.dp, + color = if (count > 0) MaterialTheme.colors.primary else Color.DarkGray, + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 16.dp, vertical = 20.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = count.toString(), + style = MaterialTheme.typography.h6 + ) + } StepperButton( onIncrease = onIncrease,