From 65c9a35a7a8f7b2eb2f3954aef49d2f77137cff2 Mon Sep 17 00:00:00 2001 From: Ashish Yadav <48384865+criticalAY@users.noreply.github.com> Date: Sun, 25 Feb 2024 02:39:25 +0530 Subject: [PATCH] feat: Allow deck selection in statistics screen A Spinner was introduced in the statistics screen top bar which can be used by the user to change the current selected deck and update the WebView with the statistics for the new selected deck. Note: the deck selection mechanism is decoupled from the general DeckSpinnerSelection/DeckSelectionDialog system so changing the deck in the statistics screen doesn't modify the selected deck for other parts of the app. --- .../com/ichi2/anki/DeckSpinnerSelection.kt | 22 +++++ .../ichi2/anki/dialogs/DeckSelectionDialog.kt | 11 ++- .../java/com/ichi2/anki/pages/Statistics.kt | 91 ++++++++++++++++++- .../src/main/res/layout/item_stats_deck.xml | 23 +++++ AnkiDroid/src/main/res/layout/statistics.xml | 26 +++++- 5 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 AnkiDroid/src/main/res/layout/item_stats_deck.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt index 5c9875af3eed..84879180b552 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt @@ -84,6 +84,28 @@ class DeckSpinnerSelection( setSpinnerListener() } + @MainThread // spinner.adapter + suspend fun initializeStatsBarDeckSpinner() { + dropDownDecks = withCol { + decks.allNamesAndIds(includeFiltered = showFilteredDecks, skipEmptyDefault = true) + }.toMutableList() + // custom implementation as DeckDropDownAdapter automatically includes a ALL_DECKS entry + + // in order for the spinner to wrap the content a row layout with wrap_content for root + // width was introduced + spinner.adapter = object : ArrayAdapter( + context, + R.layout.item_stats_deck, + dropDownDecks + ) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val rowView = super.getView(position, convertView, parent) + rowView.findViewById(R.id.title).text = getItem(position)!!.name + return rowView + } + }.apply { setDropDownViewResource(android.R.layout.simple_spinner_item) } + setSpinnerListener() + } + @MainThread // spinner.adapter fun initializeNoteEditorDeckSpinner(col: Collection, @LayoutRes layoutResource: Int = R.layout.multiline_spinner_item) { dropDownDecks = computeDropDownDecks(col, includeFiltered = false).toMutableList() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DeckSelectionDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DeckSelectionDialog.kt index e0be52a93042..f36591e44249 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DeckSelectionDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DeckSelectionDialog.kt @@ -225,8 +225,17 @@ open class DeckSelectionDialog : AnalyticsDialogFragment() { val parentFragment = parentFragment if (parentFragment is DeckSelectionListener) { return parentFragment + } else { + // try to find inside the activity an active fragment that is a DeckSelectionListener + val foundAvailableFragments = parentFragmentManager.fragments.filter { + it.isResumed && it is DeckSelectionListener + } + if (foundAvailableFragments.isNotEmpty()) { + // if we found at least one resumed candidate fragment use it + return foundAvailableFragments[0] as DeckSelectionListener + } } - throw IllegalStateException("Neither activity or parent fragment were a selection listener") + throw IllegalStateException("Neither activity or any fragment in the activity were a selection listener") } var deckCreationListener: DeckCreationListener? = null diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt index 6b921ad33c53..012194b1b57c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt @@ -21,19 +21,32 @@ import android.os.Bundle import android.print.PrintAttributes import android.print.PrintManager import android.view.View +import android.widget.Spinner import androidx.core.content.ContextCompat.getSystemService import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.ichi2.anki.CollectionManager +import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.DeckSpinnerSelection import com.ichi2.anki.R +import com.ichi2.anki.dialogs.DeckSelectionDialog +import com.ichi2.anki.launchCatchingTask +import com.ichi2.anki.requireAnkiActivity import com.ichi2.anki.utils.getTimestamp +import com.ichi2.libanki.DeckId +import com.ichi2.libanki.DeckNameId import com.ichi2.libanki.utils.TimeManager -class Statistics : PageFragment(R.layout.statistics) { +class Statistics : + PageFragment(R.layout.statistics), + DeckSelectionDialog.DeckSelectionListener { + + private lateinit var deckSpinnerSelection: DeckSpinnerSelection + private lateinit var spinner: Spinner override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - + spinner = view.findViewById(R.id.deck_selector) view.findViewById(R.id.app_bar) .addLiftOnScrollListener { _, backgroundColor -> activity?.window?.statusBarColor = backgroundColor @@ -48,6 +61,28 @@ class Statistics : PageFragment(R.layout.statistics) { true } } + deckSpinnerSelection = DeckSpinnerSelection( + requireAnkiActivity(), + spinner, + showAllDecks = false, + alwaysShowDefault = false, + showFilteredDecks = false + ) + if (savedInstanceState == null) { + requireActivity().launchCatchingTask { + deckSpinnerSelection.initializeStatsBarDeckSpinner() + val selectedDeck = withCol { decks.get(decks.selected()) } + if (selectedDeck == null) return@launchCatchingTask + select(selectedDeck.id) + changeDeck(selectedDeck.name) + } + } else { + requireActivity().launchCatchingTask { + deckSpinnerSelection.initializeStatsBarDeckSpinner() + select(savedInstanceState.getLong(KEY_DECK_ID)) + savedInstanceState.getString(KEY_DECK_NAME)?.let { changeDeck(it) } + } + } } /** Prepares and initiates a printing task for the content(stats) displayed in the WebView. @@ -65,9 +100,59 @@ class Statistics : PageFragment(R.layout.statistics) { ) } + override fun onDeckSelected(deck: DeckSelectionDialog.SelectableDeck?) { + if (deck == null) return + select(deck.deckId) + changeDeck(deck.name) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val selectedDeck = spinner.adapter.getItem(spinner.selectedItemPosition) as DeckNameId + outState.putLong(KEY_DECK_ID, selectedDeck.id) + outState.putString(KEY_DECK_NAME, selectedDeck.name) + } + + /** + * Given the [deckId] look in the decks adapter for its position and select it if found. + */ + private fun select(deckId: DeckId) { + var foundPosition = -1 + // find the selected position in the adapter and manually select it + for (i in 0 until spinner.adapter.count) { + val item = spinner.adapter.getItem(i) as DeckNameId + if (item.id == deckId) { + foundPosition = i + break + } + } + if (foundPosition >= 0) spinner.setSelection(foundPosition) + } + + /** + * This method is a workaround to change the deck in the webview by finding the text box and + * replacing the deck name with the selected deck name from the dialog and updating the stats + **/ + private fun changeDeck(selectedDeckName: String) { + val javascriptCode = """ + var textBox = [].slice.call(document.getElementsByTagName('input'), 0).filter(x => x.type == "text")[0]; + textBox.value = "deck:$selectedDeckName"; + textBox.dispatchEvent(new Event("input", { bubbles: true })); + textBox.dispatchEvent(new Event("change")); + """.trimIndent() + webView.evaluateJavascript(javascriptCode, null) + } + companion object { + private const val KEY_DECK_ID = "key_deck_id" + private const val KEY_DECK_NAME = "key_deck_name" + + /** + * Note: the title argument is set to null as the [Statistics] fragment is expected to + * handle the toolbar content(shows a deck selection spinner). + */ fun getIntent(context: Context): Intent { - return getIntent(context, "graphs", context.getString(R.string.statistics), Statistics::class) + return getIntent(context, "graphs", null, Statistics::class) } } } diff --git a/AnkiDroid/src/main/res/layout/item_stats_deck.xml b/AnkiDroid/src/main/res/layout/item_stats_deck.xml new file mode 100644 index 000000000000..2c3ffb3e0be4 --- /dev/null +++ b/AnkiDroid/src/main/res/layout/item_stats_deck.xml @@ -0,0 +1,23 @@ + + + diff --git a/AnkiDroid/src/main/res/layout/statistics.xml b/AnkiDroid/src/main/res/layout/statistics.xml index 9550c674fc8d..a67c483d97e3 100644 --- a/AnkiDroid/src/main/res/layout/statistics.xml +++ b/AnkiDroid/src/main/res/layout/statistics.xml @@ -1,9 +1,9 @@ - + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools"> + app:menu="@menu/statistics"> + + + + +