diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt index 07fadb2c1df1..1eff07e757da 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 ae011845cdb9..5ca3900f42b7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DeckSelectionDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DeckSelectionDialog.kt @@ -239,8 +239,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 6c82f4b60cbc..5fbe3602e294 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt @@ -21,20 +21,36 @@ import android.os.Bundle import android.print.PrintAttributes import android.print.PrintManager import android.view.View +import android.widget.AdapterView.INVALID_POSITION +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 import com.ichi2.themes.setTransparentStatusBar +import com.ichi2.utils.BundleUtils.getNullableLong -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) requireActivity().setTransparentStatusBar() + spinner = view.findViewById(R.id.deck_selector) view.findViewById(R.id.app_bar) .addLiftOnScrollListener { _, backgroundColor -> activity?.window?.statusBarColor = backgroundColor @@ -49,6 +65,29 @@ 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 { + val savedDeckId = savedInstanceState.getNullableLong(KEY_DECK_ID) ?: return + requireActivity().launchCatchingTask { + deckSpinnerSelection.initializeStatsBarDeckSpinner() + select(savedDeckId) + savedInstanceState.getString(KEY_DECK_NAME)?.let { changeDeck(it) } + } + } } /** Prepares and initiates a printing task for the content(stats) displayed in the WebView. @@ -66,9 +105,63 @@ 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 selectedPosition = spinner.selectedItemPosition + if (selectedPosition != INVALID_POSITION) { + val selectedDeck = spinner.adapter.getItem(selectedPosition) as DeckNameId + outState.putLong(KEY_DECK_ID, selectedDeck.id) + outState.putString(KEY_DECK_NAME, selectedDeck.name) + } + } + + private val decksAdapterSequence + get() = sequence { + for (i in 0 until spinner.adapter.count) { + yield(spinner.adapter.getItem(i) as DeckNameId) + } + } + + /** + * Given the [deckId] look in the decks adapter for its position and select it if found. + */ + private fun select(deckId: DeckId) { + val itemToSelect = decksAdapterSequence.withIndex().firstOrNull { it.value.id == deckId } ?: return + spinner.setSelection(itemToSelect.index) + } + + /** + * 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 + * + * See issue #3394 in the Anki repository + **/ + 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"> + + + + +