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) ) }