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.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,30 +390,71 @@ class SearchActivity :
|
||||||
binding.chipDate.toggle()
|
binding.chipDate.toggle()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val picker = MaterialDatePicker.Builder.dateRangePicker()
|
val dialogBinding = SearchOperatorDateDialogBinding.inflate(layoutInflater, null, false)
|
||||||
.setTitleText(R.string.search_operator_date_dialog_title)
|
val choice = viewModel.getOperator<DateOperator>()?.choice
|
||||||
.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(),
|
|
||||||
|
|
||||||
)
|
dialogBinding.radioGroup.check(
|
||||||
.build()
|
when (choice) {
|
||||||
.await(supportFragmentManager, "dateRangePicker")
|
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()
|
// Wait until the dialog is shown to set up the custom range button click
|
||||||
val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate()
|
// 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.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 {
|
||||||
* The date range to search.
|
fun fmt(): String
|
||||||
*
|
|
||||||
* @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))}"
|
|
||||||
|
|
||||||
companion object {
|
data object Today : DateChoice {
|
||||||
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
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()
|
override fun query() = choice?.fmt()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The `from:...` operator. */
|
/** The `from:...` operator. */
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_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>
|
||||||
|
|
Loading…
Reference in New Issue