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:
parent
2dab92e521
commit
c1fbbad516
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
throw IllegalStateException("Neither activity or parent fragment were a selection listener")
|
||||
if (foundAvailableFragments.isNotEmpty()) {
|
||||
// if we found at least one resumed candidate fragment use it
|
||||
return foundAvailableFragments[0] as DeckSelectionListener
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("Neither activity or any fragment in the activity were a selection listener")
|
||||
}
|
||||
|
||||
var deckCreationListener: DeckCreationListener? = null
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
AnkiDroid/src/main/res/layout/item_stats_deck.xml
Normal file
23
AnkiDroid/src/main/res/layout/item_stats_deck.xml
Normal 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"/>
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user