1717package com.ichi2.anki.browser.search
1818
1919import android.content.Context
20+ import android.os.Bundle
21+ import android.view.LayoutInflater
22+ import android.view.View
23+ import android.view.ViewGroup
2024import androidx.annotation.CheckResult
2125import androidx.annotation.StringRes
26+ import androidx.annotation.VisibleForTesting
27+ import androidx.core.content.ContextCompat
28+ import androidx.core.os.bundleOf
29+ import androidx.core.view.isVisible
30+ import androidx.fragment.app.FragmentManager
31+ import androidx.fragment.app.activityViewModels
32+ import androidx.lifecycle.ViewModelProvider
33+ import androidx.recyclerview.widget.RecyclerView
2234import anki.search.BrowserColumns.Sorting
35+ import com.google.android.material.bottomsheet.BottomSheetBehavior
2336import com.google.android.material.bottomsheet.BottomSheetDialogFragment
2437import com.ichi2.anki.R
2538import com.ichi2.anki.browser.BrowserColumnKey
@@ -28,15 +41,76 @@ import com.ichi2.anki.browser.CardBrowserViewModel
2841import com.ichi2.anki.browser.ColumnType
2942import com.ichi2.anki.browser.getLabel
3043import com.ichi2.anki.browser.humanReadableExplanation
44+ import com.ichi2.anki.browser.search.SortOrderBottomSheetFragment.ColumnUiModel.AnkiColumn
45+ import com.ichi2.anki.browser.search.ui.SortDirection
46+ import com.ichi2.anki.browser.search.ui.toSortDirection
47+ import com.ichi2.anki.databinding.FragmentBottomSheetListBinding
48+ import com.ichi2.anki.databinding.ViewBrowserSortOrderBottomSheetItemBinding
49+ import com.ichi2.anki.databinding.ViewBrowserSortOrderSectionHeaderBinding
3150import com.ichi2.anki.model.CardsOrNotes
3251import com.ichi2.anki.model.SortType
52+ import com.ichi2.anki.utils.ext.behavior
53+ import com.ichi2.anki.utils.ext.requireParcelable
54+ import dev.androidbroadcast.vbpd.viewBinding
55+ import timber.log.Timber
3356
3457private typealias Reverse = Boolean?
3558
3659/* *
3760 * A [BottomSheetDialogFragment] allowing selection of the sort order of the Card Browser
61+ *
62+ * @param viewModelProviderFactory A factory producing a [CardBrowserViewModel]
3863 */
39- class SortOrderBottomSheetFragment {
64+ class SortOrderBottomSheetFragment (
65+ private val viewModelProviderFactory : ViewModelProvider .Factory = ViewModelProvider .NewInstanceFactory (),
66+ ) : BottomSheetDialogFragment(R .layout.fragment_bottom_sheet_list) {
67+ @VisibleForTesting
68+ val viewModel: CardBrowserViewModel by activityViewModels { viewModelProviderFactory }
69+
70+ @VisibleForTesting
71+ val binding by viewBinding(FragmentBottomSheetListBinding ::bind)
72+
73+ @VisibleForTesting
74+ val currentSortType: SortType
75+ get() = requireArguments().requireParcelable<SortType >(ARG_CURRENT_SORT_TYPE )
76+
77+ override fun onViewCreated (
78+ view : View ,
79+ savedInstanceState : Bundle ? ,
80+ ) {
81+ super .onViewCreated(view, savedInstanceState)
82+
83+ with (binding.title) {
84+ isVisible = true
85+ text = getString(R .string.card_browser_change_display_order_title)
86+ }
87+
88+ with (this .behavior) {
89+ state = BottomSheetBehavior .STATE_EXPANDED
90+ skipCollapsed = true
91+ isDraggable = false
92+ }
93+
94+ binding.list.adapter =
95+ SortOrderHolderAdapter (
96+ columns = ColumnUiModel .buildList(viewModel),
97+ currentlySelectedSort = currentSortType,
98+ onItemClickedListener = { sortType ->
99+ viewModel.setSortType(sortType)
100+ this @SortOrderBottomSheetFragment.dismiss()
101+ },
102+ )
103+ }
104+
105+ /* *
106+ * Display the dialog, adding the fragment to the given [FragmentManager].
107+ *
108+ * @param manager The [FragmentManager] this fragment will be added to.
109+ *
110+ * @see BottomSheetDialogFragment.show
111+ */
112+ fun show (manager : FragmentManager ) = this .show(manager, TAG )
113+
40114 /* *
41115 * An item displayed in the sort order list
42116 *
@@ -197,16 +271,16 @@ class SortOrderBottomSheetFragment {
197271 )
198272 }
199273
200- // three groups: shown in the browser, sortable but hidden, unsortable
201- val selected = ankiColumnsList.filter { it.isShownInUI && it.canBeSorted }
274+ // three groups: browser columns , sortable but hidden, unavailable
275+ val active = ankiColumnsList.filter { it.isShownInUI && it.canBeSorted }
202276 val available = ankiColumnsList.filter { ! it.isShownInUI && it.canBeSorted }
203277 val unavailable = ankiColumnsList.filter { ! it.canBeSorted }
204278
205279 return buildList {
206280 add(NoOrdering )
207- if (selected .isNotEmpty()) {
281+ if (active .isNotEmpty()) {
208282 add(SectionHeader (R .string.user_active_columns))
209- addAll(selected )
283+ addAll(active )
210284 }
211285 if (available.isNotEmpty()) {
212286 add(SectionHeader (R .string.user_potential_columns))
@@ -221,7 +295,176 @@ class SortOrderBottomSheetFragment {
221295 }
222296 }
223297
298+ /* *
299+ * @see ViewBrowserSortOrderBottomSheetItemBinding
300+ */
301+ @VisibleForTesting
302+ inner class SortOrderHolderAdapter (
303+ @VisibleForTesting
304+ val columns : List <SortListItem >,
305+ private val currentlySelectedSort : SortType ,
306+ private val onItemClickedListener : ((SortType ) -> Unit ) = { },
307+ ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
308+ override fun getItemViewType (position : Int ): Int =
309+ when (columns[position]) {
310+ is SectionHeader -> VIEW_TYPE_HEADER
311+ is ColumnUiModel -> VIEW_TYPE_COLUMN
312+ }
313+
314+ override fun onCreateViewHolder (
315+ parent : ViewGroup ,
316+ viewType : Int ,
317+ ): RecyclerView .ViewHolder {
318+ val inflater = LayoutInflater .from(parent.context)
319+ return when (viewType) {
320+ VIEW_TYPE_HEADER ->
321+ HeaderHolder (ViewBrowserSortOrderSectionHeaderBinding .inflate(inflater, parent, false ))
322+ else ->
323+ Holder (ViewBrowserSortOrderBottomSheetItemBinding .inflate(inflater, parent, false ))
324+ }
325+ }
326+
327+ override fun onBindViewHolder (
328+ holder : RecyclerView .ViewHolder ,
329+ position : Int ,
330+ ) {
331+ when (holder) {
332+ is HeaderHolder -> {
333+ val header = columns[position] as SectionHeader
334+ holder.binding.sectionHeader.setText(header.titleRes)
335+ }
336+ is Holder -> {
337+ val column = this .columns[position] as ColumnUiModel
338+ bindColumnHolder(holder, column)
339+ }
340+ }
341+ }
342+
343+ private fun bindColumnHolder (
344+ holder : Holder ,
345+ column : ColumnUiModel ,
346+ ) {
347+ val context = holder.binding.root.context
348+
349+ setupAvailability(holder, available = column.available)
350+
351+ // highlight the current row
352+ holder.binding.root.background =
353+ if (column.isCurrentSortOrder()) {
354+ ContextCompat .getDrawable(context, R .drawable.background_sort_order_selected_row)
355+ } else {
356+ null
357+ }
358+
359+ // setup title/subtitle
360+ holder.binding.text.text = column.getLabel(context)
361+ column.getSubtitle(context, currentlySelectedSort as ? SortType .CollectionOrdering ).let {
362+ holder.binding.subtitle.isVisible = it != null
363+ holder.binding.subtitle.text = it
364+ }
365+
366+ setupSortControls(column, holder)
367+ }
368+
369+ private fun setupSortControls (
370+ column : ColumnUiModel ,
371+ holder : Holder ,
372+ ) {
373+ val pill = holder.binding.sortPill
374+ val root = holder.binding.root
375+
376+ when (column) {
377+ is AnkiColumn -> {
378+ pill.isVisible = true
379+
380+ pill.columnType = column.type
381+ val sort = currentlySelectedSort as ? SortType .CollectionOrdering
382+ val activeDirection: SortDirection ? =
383+ if (sort != null && sort.key == column.key) sort.reverse.toSortDirection() else null
384+ pill.activeDirection = activeDirection
385+ pill.isEnabled = column.available
386+ pill.onDirectionClicked = { direction ->
387+ if (direction == activeDirection) {
388+ Timber .i(" clicked active direction for %s; dismissing dialog" , column)
389+ this @SortOrderBottomSheetFragment.dismiss()
390+ } else {
391+ Timber .i(" sort direction clicked: %s for column %s" , direction, column)
392+ onItemClickedListener(column.toSortType(direction.isReverse))
393+ }
394+ }
395+
396+ // when tapped, select the left pill if unselected, otherwise flip the selection
397+ root.isClickable = column.available
398+ if (column.available) {
399+ root.setOnClickListener {
400+ val next = activeDirection?.flipped() ? : SortDirection .Ascending
401+ Timber .i(" row tap: column=%s next=%s" , column, next)
402+ onItemClickedListener(column.toSortType(next.isReverse))
403+ }
404+ } else {
405+ root.setOnClickListener(null )
406+ }
407+ }
408+ is ColumnUiModel .NoOrdering -> {
409+ pill.isVisible = false
410+ root.isClickable = true
411+ root.setOnClickListener {
412+ Timber .i(" NoOrdering row clicked" )
413+ onItemClickedListener(column.toSortType(null ))
414+ }
415+ }
416+ }
417+ }
418+
419+ /* *
420+ * Updates visibility/enabled status of a row
421+ */
422+ private fun setupAvailability (
423+ holder : Holder ,
424+ available : Boolean ,
425+ ) {
426+ holder.binding.root.isEnabled = available
427+ val disabledAlpha = if (available) 1.0f else 0.4f
428+ holder.binding.text.alpha = disabledAlpha
429+ holder.binding.sortPill.alpha = disabledAlpha
430+ // the reason for unavailability, so keep it visible
431+ holder.binding.subtitle.alpha = 1.0f
432+ }
433+
434+ private fun ColumnUiModel.isCurrentSortOrder () =
435+ when (this ) {
436+ is ColumnUiModel .NoOrdering -> currentlySelectedSort is SortType .NoOrdering
437+ is AnkiColumn ->
438+ currentlySelectedSort is SortType .CollectionOrdering &&
439+ this .key == currentlySelectedSort.key
440+ }
441+
442+ override fun getItemCount () = columns.size
443+
444+ inner class Holder (
445+ val binding : ViewBrowserSortOrderBottomSheetItemBinding ,
446+ ) : RecyclerView.ViewHolder(binding.root)
447+
448+ inner class HeaderHolder (
449+ val binding : ViewBrowserSortOrderSectionHeaderBinding ,
450+ ) : RecyclerView.ViewHolder(binding.root)
451+ }
452+
224453 companion object {
225454 const val TAG = " SortOrderBottomSheetFragment"
455+
456+ const val ARG_CURRENT_SORT_TYPE = " currentSortType"
457+
458+ private const val VIEW_TYPE_COLUMN = 0
459+ private const val VIEW_TYPE_HEADER = 1
460+
461+ suspend fun createInstance (cardsOrNotes : CardsOrNotes ) =
462+ SortOrderBottomSheetFragment ().apply {
463+ val sortData = SortType .build(cardsOrNotes)
464+
465+ Timber .i(" creating SortOrderBottomSheetFragment with %s" , sortData)
466+
467+ arguments = bundleOf(ARG_CURRENT_SORT_TYPE to sortData)
468+ }
226469 }
227470}
0 commit comments