Skip to content

Commit

Permalink
feat: Allow deck selection in statistics screen
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
criticalAY authored and lukstbit committed Jun 20, 2024
1 parent 5a13cfb commit 65c9a35
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 8 deletions.
22 changes: 22 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeckNameId>(
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<TextView>(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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 88 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppBarLayout>(R.id.app_bar)
.addLiftOnScrollListener { _, backgroundColor ->
activity?.window?.statusBarColor = backgroundColor
Expand All @@ -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.
Expand All @@ -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)
}
}
}
23 changes: 23 additions & 0 deletions AnkiDroid/src/main/res/layout/item_stats_deck.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* 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.
*
* This program 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
* this program. If not, see <http://www.gnu.org/licenses/>.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceListItem"
tools:text="Important deck"/>
26 changes: 22 additions & 4 deletions AnkiDroid/src/main/res/layout/statistics.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">

<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
Expand All @@ -20,7 +20,25 @@
app:navigationIcon="?attr/homeAsUpIndicator"
app:layout_scrollFlags="scroll|enterAlways|snap"
android:fitsSystemWindows="true"
app:menu="@menu/statistics"/>
app:menu="@menu/statistics">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<Spinner
android:id="@+id/deck_selector"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:dropDownWidth="wrap_content"/>
<TextView android:id="@+id/stats_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceListItemSecondary"
android:text="@string/statistics"
tools:text="@string/statistics"/>
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>

<androidx.core.widget.NestedScrollView
Expand Down

0 comments on commit 65c9a35

Please sign in to comment.