diff --git a/app/src/main/java/app/pachli/components/search/SearchActivity.kt b/app/src/main/java/app/pachli/components/search/SearchActivity.kt index 01536aa13..ed99e7378 100644 --- a/app/src/main/java/app/pachli/components/search/SearchActivity.kt +++ b/app/src/main/java/app/pachli/components/search/SearchActivity.kt @@ -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()?.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))) + } } } } diff --git a/app/src/main/java/app/pachli/components/search/SearchOperator.kt b/app/src/main/java/app/pachli/components/search/SearchOperator.kt index 3360e9e85..12c44285b 100644 --- a/app/src/main/java/app/pachli/components/search/SearchOperator.kt +++ b/app/src/main/java/app/pachli/components/search/SearchOperator.kt @@ -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. */ diff --git a/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt b/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt index cd563ae14..37ca4c71b 100644 --- a/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt +++ b/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt @@ -185,19 +185,23 @@ sealed interface SearchOperatorViewData { data class DateOperatorViewData(override val operator: DateOperator) : SearchOperatorViewData { 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), ) } } diff --git a/app/src/main/res/layout/search_operator_date_dialog.xml b/app/src/main/res/layout/search_operator_date_dialog.xml new file mode 100644 index 000000000..31e0c6684 --- /dev/null +++ b/app/src/main/res/layout/search_operator_date_dialog.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + +