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:
parent
bad502e6c3
commit
5d574d4d76
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue