feat: Include pre-set date search options (#835)

When selecting a search date range show the user a dialog with some
pre-set options, and a button that allows them to pick a custom date
range.
This commit is contained in:
Nik Clayton 2024-07-24 17:57:19 +02:00 committed by GitHub
parent bad502e6c3
commit 5d574d4d76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 230 additions and 43 deletions

View File

@ -35,7 +35,7 @@ import androidx.lifecycle.repeatOnLifecycle
import app.pachli.R
import app.pachli.components.compose.ComposeAutoCompleteAdapter
import app.pachli.components.search.SearchOperator.DateOperator
import app.pachli.components.search.SearchOperator.DateOperator.DateRange
import app.pachli.components.search.SearchOperator.DateOperator.DateChoice
import app.pachli.components.search.SearchOperator.FromOperator
import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromAccount
import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromMe
@ -94,6 +94,7 @@ import app.pachli.core.ui.extensions.reduceSwipeSensitivity
import app.pachli.core.ui.makeIcon
import app.pachli.databinding.ActivitySearchBinding
import app.pachli.databinding.SearchOperatorAttachmentDialogBinding
import app.pachli.databinding.SearchOperatorDateDialogBinding
import app.pachli.databinding.SearchOperatorFromDialogBinding
import app.pachli.databinding.SearchOperatorWhereLocationDialogBinding
import com.github.michaelbull.result.get
@ -389,30 +390,71 @@ class SearchActivity :
binding.chipDate.toggle()
lifecycleScope.launch {
val picker = MaterialDatePicker.Builder.dateRangePicker()
.setTitleText(R.string.search_operator_date_dialog_title)
.setCalendarConstraints(
CalendarConstraints.Builder()
.setValidator(DateValidatorPointBackward.now())
// Default behaviour is to show two months, with the current month
// at the top. This wastes space, as the user can't select beyond
// the current month, so open one month earlier to show this month
// and the previous month on screen.
.setOpenAt(
LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli(),
)
.build(),
val dialogBinding = SearchOperatorDateDialogBinding.inflate(layoutInflater, null, false)
val choice = viewModel.getOperator<DateOperator>()?.choice
)
.build()
.await(supportFragmentManager, "dateRangePicker")
dialogBinding.radioGroup.check(
when (choice) {
null -> R.id.radioAll
DateChoice.Today -> R.id.radioLastDay
DateChoice.Last7Days -> R.id.radioLast7Days
DateChoice.Last30Days -> R.id.radioLast30Days
DateChoice.Last6Months -> R.id.radioLast6Months
is DateChoice.DateRange -> -1
},
)
picker ?: return@launch
val dialog = AlertDialog.Builder(this@SearchActivity)
.setView(dialogBinding.root)
.setTitle(R.string.search_operator_date_dialog_title)
.create()
val after = Instant.ofEpochMilli(picker.first).atOffset(ZoneOffset.UTC).toLocalDate()
val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate()
// Wait until the dialog is shown to set up the custom range button click
// listener, as it needs a reference to the dialog to be able to dismiss
// it if appropriate.
dialog.setOnShowListener {
dialogBinding.buttonCustomRange.setOnClickListener {
launch {
val picker = MaterialDatePicker.Builder.dateRangePicker()
.setTitleText(R.string.search_operator_date_dialog_title)
.setCalendarConstraints(
CalendarConstraints.Builder()
.setValidator(DateValidatorPointBackward.now())
// Default behaviour is to show two months, with the current month
// at the top. This wastes space, as the user can't select beyond
// the current month, so open one month earlier to show this month
// and the previous month on screen.
.setOpenAt(
LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli(),
)
.build(),
)
.build()
.await(supportFragmentManager, "dateRangePicker")
viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(DateRange(after, before))))
picker ?: return@launch
val after = Instant.ofEpochMilli(picker.first).atOffset(ZoneOffset.UTC).toLocalDate()
val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate()
viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(DateChoice.DateRange(after, before))))
dialog.dismiss()
}
}
}
val button = dialog.await(android.R.string.ok, android.R.string.cancel)
if (button == AlertDialog.BUTTON_POSITIVE) {
val operator = when (dialogBinding.radioGroup.checkedRadioButtonId) {
R.id.radioLastDay -> DateChoice.Today
R.id.radioLast7Days -> DateChoice.Last7Days
R.id.radioLast30Days -> DateChoice.Last30Days
R.id.radioLast6Months -> DateChoice.Last6Months
else -> null
}
viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(operator)))
}
}
}
}

View File

@ -19,7 +19,9 @@ package app.pachli.components.search
import app.pachli.BuildConfig
import app.pachli.util.modernLanguageCode
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
@ -111,26 +113,58 @@ sealed interface SearchOperator {
}
/** The date-range operator. Creates `after:... before:...`. */
class DateOperator(override val choice: DateRange? = null) : SearchOperator {
/**
* The date range to search.
*
* @param startDate Earliest date to search (inclusive)
* @param endDate Latest date to search (inclusive)
*/
data class DateRange(val startDate: LocalDate, val endDate: LocalDate) {
// This class treats the date range as **inclusive** of the start and
// end dates, Mastodon's search treats the dates as exclusive, so the
// range must be expanded by one day in each direction when creating
// the search string.
fun fmt() = "after:${formatter.format(startDate.minusDays(1))} before:${formatter.format(endDate.plusDays(1))}"
class DateOperator(override val choice: DateChoice? = null) : SearchOperator {
sealed interface DateChoice {
fun fmt(): String
companion object {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
data object Today : DateChoice {
override fun fmt(): String {
val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate()
return "after:${now.minusDays(1)}"
}
}
data object Last7Days : DateChoice {
override fun fmt(): String {
val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate()
return "after:${now.minusDays(7)}"
}
}
data object Last30Days : DateChoice {
override fun fmt(): String {
val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate()
return "after:${now.minusDays(30)}"
}
}
data object Last6Months : DateChoice {
override fun fmt(): String {
val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate()
return "after:${now.minusMonths(6)}"
}
}
/**
* The date range to search.
*
* @param startDate Earliest date to search (inclusive)
* @param endDate Latest date to search (inclusive)
*/
data class DateRange(val startDate: LocalDate, val endDate: LocalDate) : DateChoice {
// This class treats the date range as **inclusive** of the start and
// end dates, Mastodon's search treats the dates as exclusive, so the
// range must be expanded by one day in each direction when creating
// the search string.
override fun fmt() = "after:${formatter.format(startDate.minusDays(1))} before:${formatter.format(endDate.plusDays(1))}"
}
}
override fun query() = choice?.fmt()
companion object {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
}
}
/** The `from:...` operator. */

View File

@ -185,19 +185,23 @@ sealed interface SearchOperatorViewData<out T : SearchOperator> {
data class DateOperatorViewData(override val operator: DateOperator) : SearchOperatorViewData<DateOperator> {
private val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
override fun chipLabel(context: Context) = when (operator.choice) {
override fun chipLabel(context: Context) = when (val choice = operator.choice) {
null -> context.getString(R.string.search_operator_date_all)
else -> {
if (operator.choice.startDate == operator.choice.endDate) {
DateOperator.DateChoice.Today -> context.getString(R.string.search_operator_date_dialog_today)
DateOperator.DateChoice.Last7Days -> context.getString(R.string.search_operator_date_dialog_last_7_days)
DateOperator.DateChoice.Last30Days -> context.getString(R.string.search_operator_date_dialog_last_30_days)
DateOperator.DateChoice.Last6Months -> context.getString(R.string.search_operator_date_dialog_last_6_months)
is DateOperator.DateChoice.DateRange -> {
if (choice.startDate == choice.endDate) {
context.getString(
R.string.search_operator_date_checked_same_day,
formatter.format(operator.choice.startDate),
formatter.format(choice.startDate),
)
} else {
context.getString(
R.string.search_operator_date_checked,
formatter.format(operator.choice.startDate),
formatter.format(operator.choice.endDate),
formatter.format(choice.startDate),
formatter.format(choice.endDate),
)
}
}

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="4dp"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RadioButton
android:id="@+id/radioAll"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_all"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />
<RadioButton
android:id="@+id/radioLastDay"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_today"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />
<RadioButton
android:id="@+id/radioLast7Days"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_last_7_days"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />
<RadioButton
android:id="@+id/radioLast30Days"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_last_30_days"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />
<RadioButton
android:id="@+id/radioLast6Months"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_last_6_months"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />
</RadioGroup>
<Button
android:id="@+id/buttonCustomRange"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/search_operator_date_dialog_custom_range"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radioGroup" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -788,7 +788,13 @@
<string name="search_operator_date_all">Dates ▾</string>
<string name="search_operator_date_checked">%1$s - %2$s</string>
<string name="search_operator_date_checked_same_day">On %1$s</string>
<string name="search_operator_date_dialog_title">Limit to posts between…</string>
<string name="search_operator_date_dialog_title">Limit to posts sent…</string>
<string name="search_operator_date_dialog_all">Any time</string>
<string name="search_operator_date_dialog_today">Today</string>
<string name="search_operator_date_dialog_last_7_days">Last 7 days</string>
<string name="search_operator_date_dialog_last_30_days">Last 30 days</string>
<string name="search_operator_date_dialog_last_6_months">Last 6 months</string>
<string name="search_operator_date_dialog_custom_range">Custom range</string>
<string name="search_operator_language_dialog_title">Limit to posts written in…</string>
<string name="search_operator_language_all">Any language ▾</string>