0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-19 19:42:17 +02:00

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.
This commit is contained in:
Ashish Yadav 2024-02-25 02:39:25 +05:30 committed by Mike Hardy
parent 2dab92e521
commit c1fbbad516
5 changed files with 172 additions and 7 deletions

View File

@ -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()

View File

@ -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

View File

@ -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<AppBarLayout>(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)
}
}
}

View File

@ -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"/>

View File

@ -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"
@ -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