diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeightForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeightForm.kt index 1fe282c8a7..2cba2d55e0 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeightForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeightForm.kt @@ -3,7 +3,6 @@ package de.westnordost.streetcomplete.quests.max_height import android.os.Bundle import android.text.InputFilter import android.view.View -import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.EditText import android.widget.Spinner @@ -22,6 +21,7 @@ import de.westnordost.streetcomplete.osm.LengthInMeters import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment import de.westnordost.streetcomplete.quests.AnswerItem import de.westnordost.streetcomplete.util.TextChangedWatcher +import de.westnordost.streetcomplete.view.OnAdapterItemSelectedListener class AddMaxHeightForm : AbstractQuestFormAnswerFragment() { @@ -68,12 +68,8 @@ class AddMaxHeightForm : AbstractQuestFormAnswerFragment() { heightUnitSelect?.isGone = lengthUnits.size == 1 heightUnitSelect?.adapter = ArrayAdapter(requireContext(), R.layout.spinner_item_centered, lengthUnits) heightUnitSelect?.setSelection(0) - heightUnitSelect?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parentView: AdapterView<*>, selectedItemView: View?, position: Int, id: Long) { - switchLayout(heightUnitSelect?.selectedItem as LengthUnit) - } - - override fun onNothingSelected(parentView: AdapterView<*>) {} + heightUnitSelect?.onItemSelectedListener = OnAdapterItemSelectedListener { + switchLayout(heightUnitSelect?.selectedItem as LengthUnit) } inchInput?.filters = arrayOf(InputFilter { source, start, end, dest, dstart, dend -> diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursForm.kt index 4599c4f623..c9840f05e8 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursForm.kt @@ -17,7 +17,7 @@ import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment import de.westnordost.streetcomplete.quests.AnswerItem import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningMonthsRow import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningWeekdaysRow -import de.westnordost.streetcomplete.quests.opening_hours.adapter.RegularOpeningHoursAdapter +import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningHoursAdapter import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -47,14 +47,16 @@ class AddOpeningHoursForm : AbstractQuestFormAnswerFragment( } ) - private lateinit var openingHoursAdapter: RegularOpeningHoursAdapter + private lateinit var openingHoursAdapter: OpeningHoursAdapter private var isDisplayingPreviousOpeningHours: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - openingHoursAdapter = RegularOpeningHoursAdapter(requireContext(), countryInfo) + openingHoursAdapter = OpeningHoursAdapter(requireContext()) + openingHoursAdapter.firstDayOfWorkweek = countryInfo.firstDayOfWorkweek + openingHoursAdapter.regularShoppingDays = countryInfo.regularShoppingDays } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/adapter/OpeningHoursAdapter.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/adapter/OpeningHoursAdapter.kt index fefd14a8f1..4d635b6c40 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/adapter/OpeningHoursAdapter.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/adapter/OpeningHoursAdapter.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.meta.CountryInfo import de.westnordost.streetcomplete.databinding.QuestTimesMonthRowBinding import de.westnordost.streetcomplete.databinding.QuestTimesOffdayRowBinding import de.westnordost.streetcomplete.databinding.QuestTimesWeekdayRowBinding @@ -30,10 +29,8 @@ data class OpeningWeekdaysRow(var weekdays: Weekdays, var timeRange: TimeRange) @Serializable data class OffDaysRow(var weekdays: Weekdays) : OpeningHoursRow() -class RegularOpeningHoursAdapter( - private val context: Context, - private val countryInfo: CountryInfo -) : RecyclerView.Adapter() { +class OpeningHoursAdapter(private val context: Context) + : RecyclerView.Adapter() { var rows: MutableList = mutableListOf() set(value) { @@ -47,6 +44,11 @@ class RegularOpeningHoursAdapter( notifyDataSetChanged() } + /** Set to change which weekdays are pre-checked in the weekday-select dialog */ + var firstDayOfWorkweek: String = "Mo" + /** Set to change which weekdays are pre-checked in the weekday-select dialog */ + var regularShoppingDays: Int = 6 + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { @@ -66,7 +68,8 @@ class RegularOpeningHoursAdapter( } is WeekdayViewHolder -> { val prevRow = if (position > 0) rows[position - 1] as? OpeningWeekdaysRow else null - holder.update(row as OpeningWeekdaysRow, prevRow, isEnabled) + val nextRow = if (rows.lastIndex > position) rows[position + 1] as? OpeningWeekdaysRow else null + holder.update(row as OpeningWeekdaysRow, prevRow, nextRow, isEnabled) } is OffDaysViewHolder -> { holder.update(row as OffDaysRow, isEnabled) @@ -233,15 +236,16 @@ class RegularOpeningHoursAdapter( } } - fun update(row: OpeningWeekdaysRow, rowBefore: OpeningWeekdaysRow?, isEnabled: Boolean) { + fun update(row: OpeningWeekdaysRow, rowBefore: OpeningWeekdaysRow?, nextRow: OpeningWeekdaysRow?, isEnabled: Boolean) { binding.weekdaysLabel.text = if (rowBefore != null && row.weekdays == rowBefore.weekdays) "" - else if (row.weekdays.isSelectionEmpty()) "(" + context.resources.getString(R.string.quest_openingHours_unspecified_range) + ")" + else if (rowBefore != null && row.weekdays.isSelectionEmpty()) "(" + context.resources.getString(R.string.quest_openingHours_unspecified_range) + ")" else row.weekdays.toLocalizedString(context.resources) + binding.weekdaysLabel.setOnClickListener { openSetWeekdaysDialog(row.weekdays) { weekdays -> row.weekdays = weekdays - notifyItemChanged(adapterPosition) + notifyItemRangeChanged(adapterPosition, if (nextRow != null) 2 else 1) } } @@ -292,9 +296,9 @@ class RegularOpeningHoursAdapter( private fun getWeekdaysSuggestion(isFirst: Boolean): Weekdays { if (isFirst) { - val firstWorkDayIdx = Weekdays.getWeekdayIndex(countryInfo.firstDayOfWorkweek) + val firstWorkDayIdx = Weekdays.getWeekdayIndex(firstDayOfWorkweek) val result = BooleanArray(Weekdays.OSM_ABBR_WEEKDAYS.size) - for (i in 0 until countryInfo.regularShoppingDays) { + for (i in 0 until regularShoppingDays) { result[(i + firstWorkDayIdx) % Weekdays.WEEKDAY_COUNT] = true } return Weekdays(result) diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt index e6c4ef13f5..45f3632c05 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt @@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType import de.westnordost.streetcomplete.data.osm.osmquests.Tags import de.westnordost.streetcomplete.data.user.achievements.QuestTypeAchievement.BICYCLIST -class AddBikeParkingFee : OsmFilterQuestType() { +class AddBikeParkingFee : OsmFilterQuestType() { // element selection logic by @DerDings in #2507 override val elementFilter = """ @@ -31,6 +31,6 @@ class AddBikeParkingFee : OsmFilterQuestType() { override fun createForm() = AddParkingFeeForm() - override fun applyAnswerTo(answer: Fee, tags: Tags, timestampEdited: Long) = + override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, timestampEdited: Long) = answer.applyTo(tags) } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt index 15f45e2eb8..ff200f9546 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt @@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType import de.westnordost.streetcomplete.data.osm.osmquests.Tags import de.westnordost.streetcomplete.data.user.achievements.QuestTypeAchievement.CAR -class AddParkingFee : OsmFilterQuestType() { +class AddParkingFee : OsmFilterQuestType() { override val elementFilter = """ nodes, ways, relations with amenity = parking @@ -25,6 +25,6 @@ class AddParkingFee : OsmFilterQuestType() { override fun createForm() = AddParkingFeeForm() - override fun applyAnswerTo(answer: Fee, tags: Tags, timestampEdited: Long) = + override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, timestampEdited: Long) = answer.applyTo(tags) } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeForm.kt index dfa3b6866f..db220fa120 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeForm.kt @@ -2,120 +2,122 @@ package de.westnordost.streetcomplete.quests.parking_fee import android.os.Bundle import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import androidx.core.view.isGone -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.databinding.QuestFeeHoursBinding +import de.westnordost.streetcomplete.databinding.QuestMaxstayBinding +import de.westnordost.streetcomplete.osm.opening_hours.parser.toOpeningHoursRules import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment import de.westnordost.streetcomplete.quests.AnswerItem -import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningHoursRow -import de.westnordost.streetcomplete.quests.opening_hours.adapter.RegularOpeningHoursAdapter -import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import de.westnordost.streetcomplete.quests.parking_fee.AddParkingFeeForm.Mode.* +import de.westnordost.streetcomplete.view.DurationUnit +import de.westnordost.streetcomplete.view.TimeRestriction.AT_ANY_TIME +import de.westnordost.streetcomplete.view.TimeRestriction.EXCEPT_AT_HOURS +import de.westnordost.streetcomplete.view.TimeRestriction.ONLY_AT_HOURS -class AddParkingFeeForm : AbstractQuestFormAnswerFragment() { +class AddParkingFeeForm : AbstractQuestFormAnswerFragment() { - override val contentLayoutResId = R.layout.quest_fee_hours - private val binding by contentViewBinding(QuestFeeHoursBinding::bind) + private var binding: ViewBinding? = null override val buttonPanelAnswers get() = - if (!isDefiningHours) listOf( - AnswerItem(R.string.quest_generic_hasFeature_no) { applyAnswer(HasNoFee) }, - AnswerItem(R.string.quest_generic_hasFeature_yes) { applyAnswer(HasFee) } + if (mode == FEE_YES_NO) listOf( + AnswerItem(R.string.quest_generic_hasFeature_no) { applyAnswer(FeeAndMaxStay(HasNoFee)) }, + AnswerItem(R.string.quest_generic_hasFeature_yes) { applyAnswer(FeeAndMaxStay(HasFee)) } ) else emptyList() override val otherAnswers = listOf( - AnswerItem(R.string.quest_fee_answer_hours) { isDefiningHours = true } + AnswerItem(R.string.quest_fee_answer_hours) { mode = FEE_AT_HOURS }, + AnswerItem(R.string.quest_fee_answer_no_but_maxstay) { mode = MAX_STAY }, ) - private lateinit var openingHoursAdapter: RegularOpeningHoursAdapter - - private var content: ViewGroup? = null - - private var isDefiningHours: Boolean = false + private var mode: Mode = FEE_YES_NO set(value) { field = value - - content?.isGone = !value + binding = when (mode) { + FEE_YES_NO -> null + FEE_AT_HOURS -> QuestFeeHoursBinding.bind(setContentView(R.layout.quest_fee_hours)) + MAX_STAY -> QuestMaxstayBinding.bind(setContentView(R.layout.quest_maxstay)) + } + onContentViewBound() updateButtonPanel() } - private var isFeeOnlyAtHours: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - openingHoursAdapter = RegularOpeningHoursAdapter(requireContext(), countryInfo) - openingHoursAdapter.rows = loadOpeningHoursData(savedInstanceState).toMutableList() - openingHoursAdapter.registerAdapterDataObserver(AdapterDataChangedWatcher { checkIsFormComplete() }) - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - content = view.findViewById(R.id.content) - - // must be read here because setting these values effects the UI - isFeeOnlyAtHours = savedInstanceState?.getBoolean(IS_FEE_ONLY_AT_HOURS, true) ?: true - isDefiningHours = savedInstanceState?.getBoolean(IS_DEFINING_HOURS) ?: false - - binding.openingHoursList.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - binding.openingHoursList.adapter = openingHoursAdapter - binding.openingHoursList.isNestedScrollingEnabled = false + mode = savedInstanceState?.getString(MODE)?.let { valueOf(it) } ?: FEE_YES_NO checkIsFormComplete() + } - binding.addTimesButton.setOnClickListener { openingHoursAdapter.addNewWeekdays() } - - val spinnerItems = listOf( - getString(R.string.quest_fee_only_at_hours), - getString(R.string.quest_fee_not_at_hours) - ) - binding.selectFeeOnlyAtHours.adapter = ArrayAdapter(requireContext(), R.layout.spinner_item_centered, spinnerItems) - binding.selectFeeOnlyAtHours.setSelection(if (isFeeOnlyAtHours) 0 else 1) - binding.selectFeeOnlyAtHours.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - isFeeOnlyAtHours = position == 0 - } - - override fun onNothingSelected(parent: AdapterView<*>) {} + private fun onContentViewBound() { + val binding = binding + if (binding is QuestFeeHoursBinding) { + binding.timesView.firstDayOfWorkweek = countryInfo.firstDayOfWorkweek + binding.timesView.regularShoppingDays = countryInfo.regularShoppingDays + binding.timesView.selectableTimeRestrictions = listOf(ONLY_AT_HOURS, EXCEPT_AT_HOURS) + binding.timesView.onInputChanged = { checkIsFormComplete() } + } else if (binding is QuestMaxstayBinding) { + binding.timesView.firstDayOfWorkweek = countryInfo.firstDayOfWorkweek + binding.timesView.regularShoppingDays = countryInfo.regularShoppingDays + binding.timesView.onInputChanged = { checkIsFormComplete() } + binding.durationInput.onInputChanged = { checkIsFormComplete() } } } override fun onDestroyView() { super.onDestroyView() - content = null + binding = null } override fun onClickOk() { - val times = openingHoursAdapter.createOpeningHours() - applyAnswer(if (isFeeOnlyAtHours) HasFeeAtHours(times) else HasFeeExceptAtHours(times)) + val binding = binding + if (binding is QuestFeeHoursBinding) { + val hours = binding.timesView.hours.toOpeningHoursRules() + val fee = when (binding.timesView.timeRestriction) { + AT_ANY_TIME -> HasFee + ONLY_AT_HOURS -> HasFeeAtHours(hours) + EXCEPT_AT_HOURS -> HasFeeExceptAtHours(hours) + } + applyAnswer(FeeAndMaxStay(fee)) + } else if (binding is QuestMaxstayBinding) { + val duration = MaxstayDuration( + binding.durationInput.durationValue, + when (binding.durationInput.durationUnit) { + DurationUnit.MINUTES -> Maxstay.Unit.MINUTES + DurationUnit.HOURS -> Maxstay.Unit.HOURS + DurationUnit.DAYS -> Maxstay.Unit.DAYS + } + ) + val hours = binding.timesView.hours.toOpeningHoursRules() + val maxstay = when (binding.timesView.timeRestriction) { + AT_ANY_TIME -> duration + ONLY_AT_HOURS -> MaxstayAtHours(duration, hours) + EXCEPT_AT_HOURS -> MaxstayExceptAtHours(duration, hours) + } + applyAnswer(FeeAndMaxStay(HasNoFee, maxstay)) + } } - private fun loadOpeningHoursData(savedInstanceState: Bundle?): List = - savedInstanceState?.let { Json.decodeFromString(it.getString(OPENING_HOURS_DATA)!!) } ?: emptyList() - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString(OPENING_HOURS_DATA, Json.encodeToString(openingHoursAdapter.rows)) - outState.putBoolean(IS_DEFINING_HOURS, isDefiningHours) - outState.putBoolean(IS_FEE_ONLY_AT_HOURS, isFeeOnlyAtHours) + outState.putString(MODE, mode.name) } - override fun isRejectingClose() = - isDefiningHours && openingHoursAdapter.rows.isEmpty() + override fun isRejectingClose() = when (val binding = binding) { + is QuestFeeHoursBinding -> binding.timesView.hours.isNotEmpty() + is QuestMaxstayBinding -> binding.timesView.isComplete || binding.durationInput.durationValue > 0.0 + else -> false + } - override fun isFormComplete() = - isDefiningHours && openingHoursAdapter.rows.isNotEmpty() + override fun isFormComplete() = when (val binding = binding) { + is QuestFeeHoursBinding -> binding.timesView.hours.isNotEmpty() + is QuestMaxstayBinding -> binding.timesView.isComplete && binding.durationInput.durationValue > 0.0 + else -> false + } companion object { - private const val OPENING_HOURS_DATA = "oh_data" - private const val IS_FEE_ONLY_AT_HOURS = "oh_fee_only_at" - private const val IS_DEFINING_HOURS = "oh" + private const val MODE = "mode" } + + private enum class Mode { FEE_YES_NO, FEE_AT_HOURS, MAX_STAY } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Fee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Fee.kt index e776f9dcae..551ed27f67 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Fee.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Fee.kt @@ -8,8 +8,8 @@ sealed class Fee object HasFee : Fee() object HasNoFee : Fee() -data class HasFeeAtHours(val openingHours: OpeningHoursRuleList) : Fee() -data class HasFeeExceptAtHours(val openingHours: OpeningHoursRuleList) : Fee() +data class HasFeeAtHours(val hours: OpeningHoursRuleList) : Fee() +data class HasFeeExceptAtHours(val hours: OpeningHoursRuleList) : Fee() fun Fee.applyTo(tags: Tags) { when (this) { @@ -23,11 +23,11 @@ fun Fee.applyTo(tags: Tags) { } is HasFeeAtHours -> { tags.updateWithCheckDate("fee", "no") - tags["fee:conditional"] = "yes @ ($openingHours)" + tags["fee:conditional"] = "yes @ ($hours)" } is HasFeeExceptAtHours -> { tags.updateWithCheckDate("fee", "yes") - tags["fee:conditional"] = "no @ ($openingHours)" + tags["fee:conditional"] = "no @ ($hours)" } } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/FeeAndMaxStay.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/FeeAndMaxStay.kt new file mode 100644 index 0000000000..4611c56f60 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/FeeAndMaxStay.kt @@ -0,0 +1,10 @@ +package de.westnordost.streetcomplete.quests.parking_fee + +import de.westnordost.streetcomplete.data.osm.osmquests.Tags + +data class FeeAndMaxStay(val fee: Fee, val maxstay: Maxstay? = null) + +fun FeeAndMaxStay.applyTo(tags: Tags) { + fee.applyTo(tags) + maxstay?.applyTo(tags) +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Maxstay.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Maxstay.kt new file mode 100644 index 0000000000..6e9ba1aa92 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/Maxstay.kt @@ -0,0 +1,44 @@ +package de.westnordost.streetcomplete.quests.parking_fee + +import de.westnordost.streetcomplete.data.meta.updateWithCheckDate +import de.westnordost.streetcomplete.data.osm.osmquests.Tags +import de.westnordost.streetcomplete.ktx.toShortString +import de.westnordost.streetcomplete.osm.opening_hours.parser.OpeningHoursRuleList +import de.westnordost.streetcomplete.quests.parking_fee.Maxstay.Unit.* + +sealed interface Maxstay { + enum class Unit { MINUTES, HOURS, DAYS } +} + +object NoMaxstay : Maxstay +data class MaxstayDuration(val value: Double, val unit: Maxstay.Unit) : Maxstay +data class MaxstayAtHours(val duration: MaxstayDuration, val hours: OpeningHoursRuleList) : Maxstay +data class MaxstayExceptAtHours(val duration: MaxstayDuration, val hours: OpeningHoursRuleList) : Maxstay + +fun MaxstayDuration.toOsmValue(): String = + value.toShortString() + " " + when (unit) { + MINUTES -> if (value != 1.0) "minutes" else "minute" + HOURS -> if (value != 1.0) "hours" else "hour" + DAYS -> if (value != 1.0) "days" else "day" + } + +fun Maxstay.applyTo(tags: Tags) { + when (this) { + is MaxstayExceptAtHours -> { + tags.updateWithCheckDate("maxstay", duration.toOsmValue()) + tags["maxstay:conditional"] = "no @ ($hours)" + } + is MaxstayAtHours -> { + tags.updateWithCheckDate("maxstay", "no") + tags["maxstay:conditional"] = "${duration.toOsmValue()} @ ($hours)" + } + is MaxstayDuration -> { + tags.updateWithCheckDate("maxstay", toOsmValue()) + tags.remove("maxstay:conditional") + } + NoMaxstay -> { + tags.updateWithCheckDate("maxstay", "no") + tags.remove("maxstay:conditional") + } + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/DurationInput.kt b/app/src/main/java/de/westnordost/streetcomplete/view/DurationInput.kt new file mode 100644 index 0000000000..abdee35512 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/view/DurationInput.kt @@ -0,0 +1,59 @@ +package de.westnordost.streetcomplete.view + +import android.content.Context +import android.content.res.Resources +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.ArrayAdapter +import android.widget.FrameLayout +import androidx.core.widget.addTextChangedListener +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.databinding.ViewDurationBinding +import de.westnordost.streetcomplete.ktx.numberOrNull +import de.westnordost.streetcomplete.view.inputfilter.InputValidator +import de.westnordost.streetcomplete.view.inputfilter.acceptDecimalDigits +import de.westnordost.streetcomplete.view.inputfilter.acceptIntDigits + +/** Allows to input a duration, in days, hours or minutes */ +class DurationInput @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding = ViewDurationBinding.inflate(LayoutInflater.from(context), this) + + var onInputChanged: (() -> Unit)? = null + + var durationUnit: DurationUnit + set(value) { binding.unitSelect.setSelection(value.ordinal) } + get() = DurationUnit.values()[binding.unitSelect.selectedItemPosition] + + var durationValue: Double + set(value) { binding.input.setText(value.toString()) } + get() = binding.input.numberOrNull ?: 0.0 + + init { + binding.unitSelect.adapter = ArrayAdapter( + context, + R.layout.spinner_item_centered, + DurationUnit.values().map { it.toLocalizedString(context.resources) } + ) + if (binding.unitSelect.selectedItemPosition < 0) binding.unitSelect.setSelection(1) + binding.unitSelect.onItemSelectedListener = OnAdapterItemSelectedListener { + + onInputChanged?.invoke() + } + binding.input.filters = arrayOf(acceptDecimalDigits(3, 1)) + binding.input.addTextChangedListener { onInputChanged?.invoke() } + } + +} + +enum class DurationUnit { MINUTES, HOURS, DAYS } + +private fun DurationUnit.toLocalizedString(resources: Resources) = when (this) { + DurationUnit.MINUTES -> resources.getString(R.string.unit_minutes) + DurationUnit.HOURS -> resources.getString(R.string.unit_hours) + DurationUnit.DAYS -> resources.getString(R.string.unit_days) +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/LengthInput.kt b/app/src/main/java/de/westnordost/streetcomplete/view/LengthInput.kt index 33d9649031..a7827ac4ed 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/view/LengthInput.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/view/LengthInput.kt @@ -3,8 +3,6 @@ package de.westnordost.streetcomplete.view import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater -import android.view.View -import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.FrameLayout import androidx.core.view.isGone @@ -34,12 +32,9 @@ class LengthInput @JvmOverloads constructor( var onInputChanged: (() -> Unit)? = null init { - binding.unitSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - updateInputFieldsVisibility() - onInputChanged?.invoke() - } - override fun onNothingSelected(parent: AdapterView<*>?) {} + binding.unitSelect.onItemSelectedListener = OnAdapterItemSelectedListener { + updateInputFieldsVisibility() + onInputChanged?.invoke() } binding.feetInput.filters = arrayOf(acceptIntDigits(4)) diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/OnAdapterItemSelectedListener.kt b/app/src/main/java/de/westnordost/streetcomplete/view/OnAdapterItemSelectedListener.kt new file mode 100644 index 0000000000..2fe47465a6 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/view/OnAdapterItemSelectedListener.kt @@ -0,0 +1,14 @@ +package de.westnordost.streetcomplete.view + +import android.view.View +import android.widget.AdapterView + +class OnAdapterItemSelectedListener(val onItemSelected: (position: Int) -> Unit) + : AdapterView.OnItemSelectedListener { + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + onItemSelected(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/TimeRestrictionSelectView.kt b/app/src/main/java/de/westnordost/streetcomplete/view/TimeRestrictionSelectView.kt new file mode 100644 index 0000000000..11c5e30524 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/view/TimeRestrictionSelectView.kt @@ -0,0 +1,123 @@ +package de.westnordost.streetcomplete.view + +import android.content.Context +import android.content.res.Resources +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.ArrayAdapter +import android.widget.FrameLayout +import androidx.annotation.Keep +import androidx.core.view.isGone +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.databinding.ViewTimeRestrictionSelectBinding +import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningHoursAdapter +import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningHoursRow +import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** Allows to input a time restriction, either inclusive or exclusive, based on opening hours. */ +class TimeRestrictionSelectView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding = ViewTimeRestrictionSelectBinding.inflate(LayoutInflater.from(context), this) + + private val hoursAdapter = OpeningHoursAdapter(context) + + private val timeRestrictionAdapter = ArrayAdapter( + context, + R.layout.spinner_item_centered, + TimeRestriction.values().map { it.toLocalizedString(context.resources) }.toMutableList() + ) + + var onInputChanged: (() -> Unit)? = null + + var firstDayOfWorkweek: String + set(value) { hoursAdapter.firstDayOfWorkweek = value } + get() = hoursAdapter.firstDayOfWorkweek + + var regularShoppingDays: Int + set(value) { hoursAdapter.regularShoppingDays = value } + get() = hoursAdapter.regularShoppingDays + + var timeRestriction: TimeRestriction + set(value) { binding.selectAtHours.setSelection(selectableTimeRestrictions.indexOf(value)) } + get() = selectableTimeRestrictions[binding.selectAtHours.selectedItemPosition] + + var selectableTimeRestrictions: List = TimeRestriction.values().toList() + set(value) { + field = value + timeRestrictionAdapter.clear() + timeRestrictionAdapter.addAll(value.map { it.toLocalizedString(context.resources) }) + } + + var hours: List + set(value) { hoursAdapter.rows = value.toMutableList() } + get() = hoursAdapter.rows + + val isComplete: Boolean get() = + timeRestriction == TimeRestriction.AT_ANY_TIME || hours.isNotEmpty() + + init { + hoursAdapter.registerAdapterDataObserver(AdapterDataChangedWatcher { onInputChanged?.invoke() }) + binding.openingHoursList.adapter = hoursAdapter + binding.addTimesButton.setOnClickListener { hoursAdapter.addNewWeekdays() } + + binding.selectAtHours.adapter = timeRestrictionAdapter + if (binding.selectAtHours.selectedItemPosition < 0) binding.selectAtHours.setSelection(0) + binding.openingHoursContainer.isGone = timeRestriction == TimeRestriction.AT_ANY_TIME + binding.selectAtHours.onItemSelectedListener = OnAdapterItemSelectedListener { + binding.openingHoursContainer.isGone = timeRestriction == TimeRestriction.AT_ANY_TIME + onInputChanged?.invoke() + } + } + + public override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + val ss = SavedState(superState) + ss.oh = hoursAdapter.rows + return ss + } + + public override fun onRestoreInstanceState(s: Parcelable) { + val ss = s as SavedState + super.onRestoreInstanceState(ss.superState) + ss.oh?.let { hoursAdapter.rows = it } + } + + internal class SavedState : BaseSavedState { + var oh: MutableList? = null + + constructor(superState: Parcelable?) : super(superState) + constructor(parcel: Parcel) : super(parcel) { + oh = parcel.readString()?.let { Json.decodeFromString(it) } + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeString(Json.encodeToString(oh)) + } + + companion object { + @JvmField @Keep + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = SavedState(parcel) + override fun newArray(size: Int) = arrayOfNulls(size) + } + } + } +} + +enum class TimeRestriction { AT_ANY_TIME, ONLY_AT_HOURS, EXCEPT_AT_HOURS } + +private fun TimeRestriction.toLocalizedString(resources: Resources) = when (this) { + TimeRestriction.AT_ANY_TIME -> resources.getString(R.string.at_any_time) + TimeRestriction.ONLY_AT_HOURS -> resources.getString(R.string.only_at_hours) + TimeRestriction.EXCEPT_AT_HOURS -> resources.getString(R.string.except_at_hours) +} diff --git a/app/src/main/res/layout/quest_fee_hours.xml b/app/src/main/res/layout/quest_fee_hours.xml index 9ec4bec1c5..d3965a6b88 100644 --- a/app/src/main/res/layout/quest_fee_hours.xml +++ b/app/src/main/res/layout/quest_fee_hours.xml @@ -1,42 +1,20 @@ - + - - - - - - - + - - + android:layout_height="wrap_content"/> -