From d1bc2b9deb818de4b56e1803eeff1d910ff780c6 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 22 May 2024 14:43:31 -0300 Subject: [PATCH] feat: start Reviewer port the style and features are going to be implemented incrementally --- .../main/java/com/ichi2/anki/DeckPicker.kt | 9 +- .../ui/windows/reviewer/ReviewerFragment.kt | 137 +++++++++++++++++ .../ui/windows/reviewer/ReviewerViewModel.kt | 119 +++++++++++++++ AnkiDroid/src/main/res/layout/reviewer2.xml | 141 ++++++++++++++++++ AnkiDroid/src/main/res/menu/reviewer2.xml | 29 ++++ AnkiDroid/src/main/res/values/colors.xml | 9 ++ AnkiDroid/src/main/res/values/styles.xml | 5 + 7 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt create mode 100644 AnkiDroid/src/main/res/layout/reviewer2.xml create mode 100644 AnkiDroid/src/main/res/menu/reviewer2.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 6f61c04ddaec..782137d20ff0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -117,6 +117,7 @@ import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.ui.dialogs.storageMigrationFailedDialogIsShownOrPending +import com.ichi2.anki.ui.windows.reviewer.ReviewerFragment import com.ichi2.anki.utils.SECONDS_PER_DAY import com.ichi2.anki.widgets.DeckAdapter import com.ichi2.anki.worker.SyncMediaWorker @@ -2305,8 +2306,12 @@ open class DeckPicker : } private fun openReviewer() { - val reviewer = Intent(this, Reviewer::class.java) - reviewLauncher.launch(reviewer) + val intent = if (sharedPrefs().getBoolean("newReviewer", true)) { + ReviewerFragment.getIntent(this) + } else { + Intent(this, Reviewer::class.java) + } + reviewLauncher.launch(intent) } override fun onCreateCustomStudySession() { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt new file mode 100644 index 000000000000..4a30002e34c4 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * 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.ui.windows.reviewer + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.webkit.WebView +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.ThemeUtils +import androidx.appcompat.widget.Toolbar +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.ichi2.anki.AbstractFlashcardViewer.Companion.RESULT_NO_MORE_CARDS +import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.CardMediaPlayer +import com.ichi2.anki.previewer.CardViewerActivity +import com.ichi2.anki.previewer.CardViewerFragment +import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider +import com.ichi2.anki.snackbar.SnackbarBuilder +import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.utils.ext.collectIn +import com.ichi2.anki.utils.ext.collectLatestIn +import com.ichi2.anki.utils.navBarNeedsScrim +import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons + +class ReviewerFragment : + CardViewerFragment(R.layout.reviewer2), + BaseSnackbarBuilderProvider, + Toolbar.OnMenuItemClickListener { + + override val viewModel: ReviewerViewModel by viewModels { + ReviewerViewModel.factory(CardMediaPlayer()) + } + + override val webView: WebView + get() = requireView().findViewById(R.id.webview) + + override val baseSnackbarBuilder: SnackbarBuilder = { + anchorView = this@ReviewerFragment.view?.findViewById(R.id.buttons_area) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupAnswerButtons(view) + + view.findViewById(R.id.toolbar).apply { + setOnMenuItemClickListener(this@ReviewerFragment) + setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } + (menu as? MenuBuilder)?.let { + it.setOptionalIconsVisible(true) + requireContext().increaseHorizontalPaddingOfOverflowMenuIcons(it) + } + } + + with(requireActivity()) { + if (!navBarNeedsScrim) { + window.navigationBarColor = + ThemeUtils.getThemeAttrColor(this, R.attr.alternativeBackgroundColor) + } + } + + viewModel.isQueueFinishedFlow.collectIn(lifecycleScope) { isQueueFinished -> + if (isQueueFinished) { + requireActivity().run { + setResult(RESULT_NO_MORE_CARDS) + finish() + } + } + } + } + + // TODO + override fun onMenuItemClick(item: MenuItem?): Boolean { + showSnackbar("Not implemented yet") + return true + } + + private fun setupAnswerButtons(view: View) { + view.findViewById(R.id.again_button).setOnClickListener { + viewModel.answerAgain() + } + view.findViewById(R.id.hard_button).setOnClickListener { + viewModel.answerHard() + } + view.findViewById(R.id.good_button).setOnClickListener { + viewModel.answerGood() + } + view.findViewById(R.id.easy_button).setOnClickListener { + viewModel.answerEasy() + } + + val showAnswerButton = view.findViewById(R.id.show_answer).apply { + setOnClickListener { + viewModel.showAnswer() + } + } + val answerButtonsLayout = view.findViewById(R.id.answer_buttons) + + // TODO add some kind of feedback/animation after tapping show answer or the answer buttons + viewModel.showingAnswer.collectLatestIn(lifecycleScope) { shouldShowAnswer -> + if (shouldShowAnswer) { + showAnswerButton.isVisible = false + answerButtonsLayout.isVisible = true + } else { + showAnswerButton.isVisible = true + answerButtonsLayout.isVisible = false + } + } + } + + companion object { + fun getIntent(context: Context): Intent { + return CardViewerActivity.getIntent(context, ReviewerFragment::class) + } + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt new file mode 100644 index 000000000000..9a40483eabe9 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * 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.ui.windows.reviewer + +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.Ease +import com.ichi2.anki.asyncIO +import com.ichi2.anki.cardviewer.CardMediaPlayer +import com.ichi2.anki.launchCatchingIO +import com.ichi2.anki.previewer.CardViewerViewModel +import com.ichi2.anki.reviewer.CardSide +import com.ichi2.libanki.sched.CurrentQueueState +import com.ichi2.libanki.undoableOp +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.flow.MutableSharedFlow + +class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) : CardViewerViewModel(cardMediaPlayer) { + + private var queueState: Deferred = asyncIO { + withCol { sched.currentQueueState() } + } + override var currentCard = asyncIO { + // this assumes that the Reviewer won't be launched if there isn't a queueState + queueState.await()!!.topCard + } + var isQueueFinishedFlow = MutableSharedFlow() + + /* ********************************************************************************************* + ************************ Public methods: meant to be used by the View ************************** + ********************************************************************************************* */ + + override fun onPageFinished(isAfterRecreation: Boolean) { + if (isAfterRecreation) { + launchCatchingIO { + // TODO handle "Don't keep activities" + if (showingAnswer.value) showAnswerInternal() else showQuestion() + } + } else { + launchCatchingIO { + updateCurrentCard() + } + } + } + + fun showAnswer() { + launchCatchingIO { + showAnswerInternal() + loadAndPlaySounds(CardSide.ANSWER) + } + } + + fun answerAgain() = answerCard(Ease.AGAIN) + fun answerHard() = answerCard(Ease.HARD) + fun answerGood() = answerCard(Ease.GOOD) + fun answerEasy() = answerCard(Ease.EASY) + + /* ********************************************************************************************* + *************************************** Internal methods *************************************** + ********************************************************************************************* */ + + private fun answerCard(ease: Ease) { + launchCatchingIO { + queueState.await()?.let { + undoableOp { sched.answerCard(it, ease.value) } + updateCurrentCard() + } + } + } + + private suspend fun loadAndPlaySounds(side: CardSide) { + cardMediaPlayer.loadCardSounds(currentCard.await()) + cardMediaPlayer.playAllSoundsForSide(side) + } + + private suspend fun updateCurrentCard() { + queueState = asyncIO { + withCol { + sched.currentQueueState() + } + } + queueState.await()?.let { + currentCard = CompletableDeferred(it.topCard) + showQuestion() + loadAndPlaySounds(CardSide.QUESTION) + } ?: isQueueFinishedFlow.emit(true) + } + + // TODO + override suspend fun typeAnsFilter(text: String): String { + return text + } + + companion object { + fun factory(soundPlayer: CardMediaPlayer): ViewModelProvider.Factory { + return viewModelFactory { + initializer { + ReviewerViewModel(soundPlayer) + } + } + } + } +} diff --git a/AnkiDroid/src/main/res/layout/reviewer2.xml b/AnkiDroid/src/main/res/layout/reviewer2.xml new file mode 100644 index 000000000000..d7dfefa837c5 --- /dev/null +++ b/AnkiDroid/src/main/res/layout/reviewer2.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/menu/reviewer2.xml b/AnkiDroid/src/main/res/menu/reviewer2.xml new file mode 100644 index 000000000000..ea7c0d8099cb --- /dev/null +++ b/AnkiDroid/src/main/res/menu/reviewer2.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values/colors.xml b/AnkiDroid/src/main/res/values/colors.xml index 53625c8a0d62..f3cc206a9326 100644 --- a/AnkiDroid/src/main/res/values/colors.xml +++ b/AnkiDroid/src/main/res/values/colors.xml @@ -126,5 +126,14 @@ #FF4747 #6FF06F #444444 + + #ffFFCDD2 + @color/material_red_900 + #FFE0B2 + #B33800 + #C8E6C9 + @color/material_green_900 + @color/material_light_blue_100 + @color/material_light_blue_900 diff --git a/AnkiDroid/src/main/res/values/styles.xml b/AnkiDroid/src/main/res/values/styles.xml index 317d60e53b03..d2da90259c0b 100644 --- a/AnkiDroid/src/main/res/values/styles.xml +++ b/AnkiDroid/src/main/res/values/styles.xml @@ -29,6 +29,11 @@ @drawable/bg_popup + +