From efada9028c4f74957d60781d442a4980aa68f77d 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] feature: Allow changing deck name in statistics screen
---
AnkiDroid/src/main/AndroidManifest.xml | 3 +
.../com/ichi2/anki/DeckSpinnerSelection.kt | 11 ++
.../java/com/ichi2/anki/StatisticsActivity.kt | 113 ++++++++++++++++++
.../java/com/ichi2/anki/pages/PageFragment.kt | 2 +-
.../java/com/ichi2/anki/pages/Statistics.kt | 53 +++++++-
.../ichi2/anki/widgets/DeckDropDownAdapter.kt | 12 ++
.../src/main/res/layout/page_fragment.xml | 16 ++-
.../java/com/ichi2/testutils/ActivityList.kt | 1 +
8 files changed, 202 insertions(+), 9 deletions(-)
create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/StatisticsActivity.kt
diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml
index f98fcc5a1df2..3ab144359145 100644
--- a/AnkiDroid/src/main/AndroidManifest.xml
+++ b/AnkiDroid/src/main/AndroidManifest.xml
@@ -431,6 +431,9 @@
android:exported="false"
android:configChanges="orientation|screenSize"
/>
+
+ *
+ * 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 .
+ */
+
+package com.ichi2.anki
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.widget.Spinner
+import androidx.activity.viewModels
+import androidx.appcompat.app.ActionBar
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.lifecycleScope
+import com.ichi2.anki.dialogs.DeckSelectionDialog
+import com.ichi2.anki.widgets.DeckDropDownAdapter
+import com.ichi2.libanki.DeckId
+import kotlinx.coroutines.launch
+import kotlin.reflect.KClass
+import kotlin.reflect.jvm.jvmName
+
+/**
+ * Handles changing of deck in Statistics webview
+ *
+ * Based in [SingleFragmentActivity], but with `configChanges="orientation|screenSize"`
+ * to avoid unwanted activity recreations
+ */
+class StatisticsActivity :
+ SingleFragmentActivity(),
+ DeckSelectionDialog.DeckSelectionListener,
+ DeckDropDownAdapter.SubtitleListener {
+
+ override val subtitleText: String
+ get() = resources.getString(R.string.statistics)
+
+ private val statisticsViewModel: StatisticsViewModel by viewModels()
+
+ private var deckSpinnerSelection: DeckSpinnerSelection? = null
+ var deckId: DeckId = 0
+
+ // menu wouldn't be visible in fragment's toolbar unless we override here
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.statistics, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ fun setupDeckSelector(spinner: Spinner, supportActionBar: ActionBar) {
+ startLoadingCollection()
+ deckSpinnerSelection = DeckSpinnerSelection(
+ this,
+ spinner,
+ showAllDecks = true,
+ alwaysShowDefault = false,
+ showFilteredDecks = false
+ ).apply {
+ lifecycleScope.launch {
+ initializeStatsBarDeckSpinner(supportActionBar)
+ }
+ launchCatchingTask {
+ selectDeckById(deckId, true)
+ }
+ }
+ }
+
+ override fun onDeckSelected(deck: DeckSelectionDialog.SelectableDeck?) {
+ if (deck == null) {
+ return
+ }
+ statisticsViewModel.setDeckName(deck.name)
+ deckId = deck.deckId
+ launchCatchingTask {
+ deckSpinnerSelection!!.selectDeckById(deckId, true)
+ }
+ }
+
+ companion object {
+
+ fun getIntent(context: Context, fragmentClass: KClass, arguments: Bundle? = null): Intent {
+ return Intent(context, StatisticsActivity::class.java).apply {
+ putExtra(FRAGMENT_NAME_EXTRA, fragmentClass.jvmName)
+ putExtra(FRAGMENT_ARGS_EXTRA, arguments)
+ }
+ }
+ }
+}
+
+/**
+ * ViewModel for the StatisticsActivity, storing and managing the current deck name.
+ */
+class StatisticsViewModel : ViewModel() {
+ private val _deckName = MutableLiveData()
+ val deckName: LiveData = _deckName
+
+ fun setDeckName(value: String) {
+ _deckName.value = value
+ }
+}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
index a9ab02c72a53..6450be34a4ba 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
@@ -32,7 +32,7 @@ import timber.log.Timber
*/
@Suppress("LeakingThis")
abstract class PageFragment : Fragment(R.layout.page_fragment), PostRequestHandler {
- abstract val title: String
+ abstract val title: String?
abstract val pageName: String
lateinit var webView: WebView
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 1d6107b3efa9..a18dce366ba8 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt
@@ -21,32 +21,75 @@ import android.os.Bundle
import android.print.PrintAttributes
import android.print.PrintManager
import android.view.View
+import android.widget.Spinner
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.getSystemService
+import androidx.core.view.isVisible
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
import com.google.android.material.appbar.MaterialToolbar
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.R
-import com.ichi2.anki.SingleFragmentActivity
+import com.ichi2.anki.StatisticsActivity
+import com.ichi2.anki.StatisticsViewModel
import com.ichi2.anki.utils.getTimestamp
import com.ichi2.libanki.utils.TimeManager
+import timber.log.Timber
class Statistics : PageFragment() {
- override val title: String
- get() = resources.getString(R.string.statistics)
+ override val title: String?
+ get() = null
override val pageName = "graphs"
+ private val statisticsViewModel: StatisticsViewModel by activityViewModels()
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- view.findViewById(R.id.toolbar)?.apply {
+
+ val toolbar = view.findViewById(R.id.toolbar)
+ (activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
+
+ toolbar.apply {
inflateMenu(R.menu.statistics)
menu.findItem(R.id.action_export_stats).title = CollectionManager.TR.statisticsSavePdf()
+
setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_export_stats) {
exportWebViewContentAsPDF()
}
true
}
+ val toolbarSpinner = findViewById(R.id.stats_toolbar_spinner)
+ toolbarSpinner.isVisible = true
+ val supportActionBar = (activity as? StatisticsActivity)?.supportActionBar
+
+ if (supportActionBar != null) {
+ (activity as? StatisticsActivity)?.setupDeckSelector(toolbarSpinner, supportActionBar)
+ } else {
+ Timber.w("SupportActionBar is null")
+ }
}
+ statisticsViewModel.deckName.observe(
+ viewLifecycleOwner,
+ Observer { deck ->
+ changeDeck(deck)
+ }
+ )
+ }
+
+ /**
+ * 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(selectedDeck: String) {
+ val javascriptCode = """
+ var textBox = [].slice.call(document.getElementsByTagName('input'), 0).filter(x => x.type == "text")[0];
+ textBox.value = "deck:$selectedDeck";
+ textBox.dispatchEvent(new Event("input", { bubbles: true }));
+ textBox.dispatchEvent(new Event("change"));
+ """.trimIndent()
+ webView.evaluateJavascript(javascriptCode, null)
}
/**Prepares and initiates a printing task for the content(stats) displayed in the WebView.
@@ -66,7 +109,7 @@ class Statistics : PageFragment() {
companion object {
fun getIntent(context: Context): Intent {
- return SingleFragmentActivity.getIntent(context, Statistics::class)
+ return StatisticsActivity.getIntent(context, Statistics::class)
}
}
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/widgets/DeckDropDownAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/widgets/DeckDropDownAdapter.kt
index b9c9e55c1469..6f207fd72cba 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/widgets/DeckDropDownAdapter.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/widgets/DeckDropDownAdapter.kt
@@ -22,8 +22,12 @@ import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
+import androidx.core.content.ContextCompat
import com.ichi2.anki.R
+import com.ichi2.anki.StatisticsActivity
import com.ichi2.libanki.DeckNameId
+import com.ichi2.themes.Theme
+import com.ichi2.themes.Themes
class DeckDropDownAdapter(private val context: Context, decks: List) : BaseAdapter() {
private val deckList = decks.toMutableList()
@@ -69,6 +73,14 @@ class DeckDropDownAdapter(private val context: Context, decks: List)
convertView = LayoutInflater.from(context).inflate(R.layout.dropdown_deck_selected_item, parent, false)
deckNameView = convertView.findViewById(R.id.dropdown_deck_name)
deckCountsView = convertView.findViewById(R.id.dropdown_deck_counts)
+ // Spinner has white text color in light theme, we change it only for StatisticsActivity
+ if (context is StatisticsActivity && (Themes.currentTheme.id == Theme.LIGHT.id)) {
+ deckNameView?.setTextColor(ContextCompat.getColor(context, android.R.color.black))
+ val drawableEnd = ContextCompat.getDrawable(context, R.drawable.ic_spinner_triangle)
+ drawableEnd?.setTint(ContextCompat.getColor(context, android.R.color.black))
+ deckNameView?.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, drawableEnd, null)
+ deckCountsView?.setTextColor(ContextCompat.getColor(context, android.R.color.black))
+ }
viewHolder = DeckDropDownViewHolder()
viewHolder.deckNameView = deckNameView
viewHolder.deckCountsView = deckCountsView
diff --git a/AnkiDroid/src/main/res/layout/page_fragment.xml b/AnkiDroid/src/main/res/layout/page_fragment.xml
index abde86eb07ac..10fb7288b66f 100644
--- a/AnkiDroid/src/main/res/layout/page_fragment.xml
+++ b/AnkiDroid/src/main/res/layout/page_fragment.xml
@@ -12,13 +12,23 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:navigationContentDescription="@string/abc_action_bar_up_description"
- app:navigationIcon="?attr/homeAsUpIndicator"
- />
+ app:navigationIcon="?attr/homeAsUpIndicator">
+
+
+
+
\ No newline at end of file
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt
index b214fadddee9..6a08b2c1046e 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt
@@ -76,6 +76,7 @@ object ActivityList {
get(PermissionsActivity::class.java),
get(SingleFragmentActivity::class.java),
get(ImageOcclusionActivity::class.java),
+ get(StatisticsActivity::class.java),
get(PreviewerActivity::class.java)
)
}