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.R
import app.pachli.components.compose.ComposeAutoCompleteAdapter import app.pachli.components.compose.ComposeAutoCompleteAdapter
import app.pachli.components.search.SearchOperator.DateOperator 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
import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromAccount import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromAccount
import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromMe 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.core.ui.makeIcon
import app.pachli.databinding.ActivitySearchBinding import app.pachli.databinding.ActivitySearchBinding
import app.pachli.databinding.SearchOperatorAttachmentDialogBinding import app.pachli.databinding.SearchOperatorAttachmentDialogBinding
import app.pachli.databinding.SearchOperatorDateDialogBinding
import app.pachli.databinding.SearchOperatorFromDialogBinding import app.pachli.databinding.SearchOperatorFromDialogBinding
import app.pachli.databinding.SearchOperatorWhereLocationDialogBinding import app.pachli.databinding.SearchOperatorWhereLocationDialogBinding
import com.github.michaelbull.result.get import com.github.michaelbull.result.get
@ -389,6 +390,31 @@ class SearchActivity :
binding.chipDate.toggle() binding.chipDate.toggle()
lifecycleScope.launch { lifecycleScope.launch {
val dialogBinding = SearchOperatorDateDialogBinding.inflate(layoutInflater, null, false)
val choice = viewModel.getOperator<DateOperator>()?.choice
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
},
)
val dialog = AlertDialog.Builder(this@SearchActivity)
.setView(dialogBinding.root)
.setTitle(R.string.search_operator_date_dialog_title)
.create()
// 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() val picker = MaterialDatePicker.Builder.dateRangePicker()
.setTitleText(R.string.search_operator_date_dialog_title) .setTitleText(R.string.search_operator_date_dialog_title)
.setCalendarConstraints( .setCalendarConstraints(
@ -402,7 +428,6 @@ class SearchActivity :
LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli(), LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli(),
) )
.build(), .build(),
) )
.build() .build()
.await(supportFragmentManager, "dateRangePicker") .await(supportFragmentManager, "dateRangePicker")
@ -412,7 +437,24 @@ class SearchActivity :
val after = Instant.ofEpochMilli(picker.first).atOffset(ZoneOffset.UTC).toLocalDate() val after = Instant.ofEpochMilli(picker.first).atOffset(ZoneOffset.UTC).toLocalDate()
val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate() val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate()
viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(DateRange(after, before)))) 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.BuildConfig
import app.pachli.util.modernLanguageCode import app.pachli.util.modernLanguageCode
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
@ -111,26 +113,58 @@ sealed interface SearchOperator {
} }
/** The date-range operator. Creates `after:... before:...`. */ /** The date-range operator. Creates `after:... before:...`. */
class DateOperator(override val choice: DateRange? = null) : SearchOperator { class DateOperator(override val choice: DateChoice? = null) : SearchOperator {
sealed interface DateChoice {
fun fmt(): String
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. * The date range to search.
* *
* @param startDate Earliest date to search (inclusive) * @param startDate Earliest date to search (inclusive)
* @param endDate Latest date to search (inclusive) * @param endDate Latest date to search (inclusive)
*/ */
data class DateRange(val startDate: LocalDate, val endDate: LocalDate) { data class DateRange(val startDate: LocalDate, val endDate: LocalDate) : DateChoice {
// This class treats the date range as **inclusive** of the start and // This class treats the date range as **inclusive** of the start and
// end dates, Mastodon's search treats the dates as exclusive, so the // end dates, Mastodon's search treats the dates as exclusive, so the
// range must be expanded by one day in each direction when creating // range must be expanded by one day in each direction when creating
// the search string. // the search string.
fun fmt() = "after:${formatter.format(startDate.minusDays(1))} before:${formatter.format(endDate.plusDays(1))}" override fun fmt() = "after:${formatter.format(startDate.minusDays(1))} before:${formatter.format(endDate.plusDays(1))}"
companion object {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
} }
} }
override fun query() = choice?.fmt() override fun query() = choice?.fmt()
companion object {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
}
} }
/** The `from:...` operator. */ /** The `from:...` operator. */

View File

@ -185,19 +185,23 @@ sealed interface SearchOperatorViewData<out T : SearchOperator> {
data class DateOperatorViewData(override val operator: DateOperator) : SearchOperatorViewData<DateOperator> { data class DateOperatorViewData(override val operator: DateOperator) : SearchOperatorViewData<DateOperator> {
private val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) 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) null -> context.getString(R.string.search_operator_date_all)
else -> { DateOperator.DateChoice.Today -> context.getString(R.string.search_operator_date_dialog_today)
if (operator.choice.startDate == operator.choice.endDate) { 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( context.getString(
R.string.search_operator_date_checked_same_day, R.string.search_operator_date_checked_same_day,
formatter.format(operator.choice.startDate), formatter.format(choice.startDate),
) )
} else { } else {
context.getString( context.getString(
R.string.search_operator_date_checked, R.string.search_operator_date_checked,
formatter.format(operator.choice.startDate), formatter.format(choice.startDate),
formatter.format(operator.choice.endDate), 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_all">Dates ▾</string>
<string name="search_operator_date_checked">%1$s - %2$s</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_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_dialog_title">Limit to posts written in…</string>
<string name="search_operator_language_all">Any language ▾</string> <string name="search_operator_language_all">Any language ▾</string>