@@ -40,6 +40,7 @@ import androidx.core.os.bundleOf
40
40
import androidx.core.view.isVisible
41
41
import androidx.core.widget.doAfterTextChanged
42
42
import androidx.fragment.app.setFragmentResult
43
+ import androidx.lifecycle.lifecycleScope
43
44
import anki.scheduler.CustomStudyDefaultsResponse
44
45
import anki.scheduler.CustomStudyRequest.Cram.CramKind
45
46
import anki.scheduler.copy
@@ -48,8 +49,8 @@ import com.ichi2.anki.CollectionManager.TR
48
49
import com.ichi2.anki.CollectionManager.withCol
49
50
import com.ichi2.anki.R
50
51
import com.ichi2.anki.analytics.AnalyticsDialogFragment
52
+ import com.ichi2.anki.asyncIO
51
53
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
52
- import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption
53
54
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.EXTEND_NEW
54
55
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.EXTEND_REV
55
56
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_AHEAD
@@ -62,6 +63,7 @@ import com.ichi2.anki.dialogs.customstudy.TagLimitFragment.Companion.KEY_INCLUDE
62
63
import com.ichi2.anki.dialogs.customstudy.TagLimitFragment.Companion.REQUEST_CUSTOM_STUDY_TAGS
63
64
import com.ichi2.anki.launchCatchingTask
64
65
import com.ichi2.anki.preferences.sharedPrefs
66
+ import com.ichi2.anki.snackbar.showSnackbar
65
67
import com.ichi2.anki.ui.internationalization.toSentenceCase
66
68
import com.ichi2.anki.utils.ext.dismissAllDialogFragments
67
69
import com.ichi2.anki.utils.ext.sharedPrefs
@@ -74,13 +76,15 @@ import com.ichi2.libanki.undoableOp
74
76
import com.ichi2.utils.BundleUtils.getNullableInt
75
77
import com.ichi2.utils.bundleOfNotNull
76
78
import com.ichi2.utils.cancelable
79
+ import com.ichi2.utils.coMeasureTime
77
80
import com.ichi2.utils.customView
78
81
import com.ichi2.utils.dp
79
82
import com.ichi2.utils.negativeButton
80
83
import com.ichi2.utils.positiveButton
81
84
import com.ichi2.utils.setPaddingRelative
82
85
import com.ichi2.utils.textAsIntOrNull
83
86
import com.ichi2.utils.title
87
+ import kotlinx.coroutines.Deferred
84
88
import kotlinx.coroutines.runBlocking
85
89
import kotlinx.parcelize.Parcelize
86
90
import timber.log.Timber
@@ -115,7 +119,8 @@ import timber.log.Timber
115
119
*
116
120
* @see TagLimitFragment
117
121
*/
118
- @KotlinCleanup(" remove 'runBlocking' calls'" )
122
+ @KotlinCleanup(" remove 'runBlocking' call'" )
123
+ @NeedsTest(" deferredDefaults" )
119
124
class CustomStudyDialog : AnalyticsDialogFragment () {
120
125
/* * ID of the [Deck] which this dialog was created for */
121
126
private val dialogDeckId: DeckId
@@ -139,9 +144,6 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
139
144
?.findViewById<EditText >(R .id.custom_study_details_edittext2)
140
145
?.textAsIntOrNull()
141
146
142
- /* * @see CustomStudyDefaults */
143
- private lateinit var defaults: CustomStudyDefaults
144
-
145
147
override fun onCreate (savedInstanceState : Bundle ? ) {
146
148
super .onCreate(savedInstanceState)
147
149
parentFragmentManager.setFragmentResultListener(REQUEST_CUSTOM_STUDY_TAGS , this ) { _, bundle ->
@@ -166,9 +168,9 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
166
168
override fun onCreateDialog (savedInstanceState : Bundle ? ): Dialog {
167
169
super .onCreate(savedInstanceState)
168
170
val option = selectedSubDialog
169
- this .defaults = runBlocking { withCol { sched.customStudyDefaults(dialogDeckId).toDomainModel() } }
170
171
return if (option == null ) {
171
172
Timber .i(" Showing Custom Study main menu" )
173
+ deferredDefaults = loadCustomStudyDefaults()
172
174
// Select the specified deck
173
175
runBlocking { withCol { decks.select(dialogDeckId) } }
174
176
buildContextMenu()
@@ -182,8 +184,18 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
182
184
* Continues the custom study process by showing an input dialog where the user can enter an
183
185
* amount specific to that type of custom study(eg. cards, days etc).
184
186
*/
185
- private fun onMenuItemSelected (item : ContextMenuOption ) {
186
- val dialog: CustomStudyDialog = createInstance(dialogDeckId, item)
187
+ private suspend fun onMenuItemSelected (item : ContextMenuOption ) {
188
+ // on a slow phone, 'extend limits' may be clicked before we know there's no new/review cards
189
+ // show 'no cards due' if this occurs
190
+ if (item.checkAvailability != null ) {
191
+ val defaults = withProgress { deferredDefaults.await() }
192
+ if (! item.checkAvailability(defaults)) {
193
+ showSnackbar(getString((R .string.studyoptions_no_cards_due)))
194
+ return
195
+ }
196
+ }
197
+
198
+ val dialog: CustomStudyDialog = createSubDialog(dialogDeckId, item)
187
199
requireActivity().showDialogFragment(dialog)
188
200
}
189
201
@@ -204,23 +216,44 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
204
216
)
205
217
val ta = TypedValue ()
206
218
requireContext().theme.resolveAttribute(android.R .attr.selectableItemBackground, ta, true )
207
- ContextMenuOption .entries
208
- .map {
209
- when (it) {
210
- EXTEND_NEW -> Pair (it, defaults.extendNew.isUsable)
211
- EXTEND_REV -> Pair (it, defaults.extendReview.isUsable)
212
- else -> Pair (it, true )
219
+
220
+ fun buildMenuItems () {
221
+ ContextMenuOption .entries
222
+ .map { option ->
223
+ Pair (
224
+ option,
225
+ // if there's no availability check, it's enabled
226
+ option.checkAvailability == null ||
227
+ // if data hasn't loaded, defer the check and assume it's enabled
228
+ ! deferredDefaults.isCompleted ||
229
+ // if unavailable, disable the item
230
+ option.checkAvailability(deferredDefaults.getCompleted()),
231
+ )
232
+ }.forEach { (menuItem, isItemEnabled) ->
233
+ (layoutInflater.inflate(android.R .layout.simple_list_item_1, container, false ) as TextView )
234
+ .apply {
235
+ text = menuItem.getTitle(requireContext().resources)
236
+ isEnabled = isItemEnabled
237
+ setBackgroundResource(ta.resourceId)
238
+ setTextAppearance(android.R .style.TextAppearance_Material_Body1 )
239
+ setOnClickListener {
240
+ launchCatchingTask { onMenuItemSelected(menuItem) }
241
+ }
242
+ }.also { container.addView(it) }
213
243
}
214
- }.forEach { (menuItem, isItemEnabled) ->
215
- (layoutInflater.inflate(android.R .layout.simple_list_item_1, container, false ) as TextView )
216
- .apply {
217
- text = menuItem.getTitle(requireContext().resources)
218
- isEnabled = isItemEnabled
219
- setBackgroundResource(ta.resourceId)
220
- setTextAppearance(android.R .style.TextAppearance_Material_Body1 )
221
- setOnClickListener { onMenuItemSelected(menuItem) }
222
- }.also { container.addView(it) }
244
+ }
245
+
246
+ buildMenuItems()
247
+
248
+ // add a continuation if 'defaults' was not loaded
249
+ if (! deferredDefaults.isCompleted) {
250
+ launchCatchingTask {
251
+ Timber .d(" awaiting 'defaults' continuation" )
252
+ deferredDefaults.await()
253
+ container.removeAllViews()
254
+ buildMenuItems()
223
255
}
256
+ }
224
257
225
258
return AlertDialog
226
259
.Builder (requireActivity())
@@ -236,6 +269,7 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
236
269
*/
237
270
@NeedsTest(" 17757: fragment not dismissed before result is output" )
238
271
private fun buildInputDialog (contextMenuOption : ContextMenuOption ): AlertDialog {
272
+ require(deferredDefaults.isCompleted || selectedSubDialog!! .checkAvailability == null )
239
273
/*
240
274
TODO: Try to change to a standard input dialog (currently the thing holding us back is having the extra
241
275
TODO: hint line for the number of cards available, and having the pre-filled text selected by default)
@@ -398,12 +432,30 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
398
432
}
399
433
}
400
434
401
- /* * Line 1 of the number entry dialog */
435
+ /* *
436
+ * Loads [CustomStudyDefaults] from the backend
437
+ *
438
+ * This method may be slow (> 1s)
439
+ */
440
+ private fun loadCustomStudyDefaults () =
441
+ lifecycleScope.asyncIO {
442
+ coMeasureTime(" loadCustomStudyDefaults" ) {
443
+ withCol { sched.customStudyDefaults(dialogDeckId).toDomainModel() }
444
+ }
445
+ }
446
+
447
+ /* *
448
+ * Line 1 of the number entry dialog
449
+ *
450
+ * e.g. "Review forgotten cards"
451
+ *
452
+ * Requires [ContextMenuOption.checkAvailability] to be null/return true
453
+ */
402
454
private val text1: String
403
455
get() =
404
456
when (selectedSubDialog) {
405
- EXTEND_NEW -> defaults .labelForNewQueueAvailable()
406
- EXTEND_REV -> defaults .labelForReviewQueueAvailable()
457
+ EXTEND_NEW -> deferredDefaults.getCompleted() .labelForNewQueueAvailable()
458
+ EXTEND_REV -> deferredDefaults.getCompleted() .labelForReviewQueueAvailable()
407
459
STUDY_FORGOT ,
408
460
STUDY_AHEAD ,
409
461
STUDY_PREVIEW ,
@@ -427,13 +479,25 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
427
479
}
428
480
}
429
481
430
- /* * Initial value of the number entry dialog */
482
+ /* *
483
+ * Initial value of the number entry dialog
484
+ *
485
+ * Requires [ContextMenuOption.checkAvailability] to be null/return true
486
+ */
431
487
private val defaultValue: String
432
488
get() {
433
489
val prefs = requireActivity().sharedPrefs()
434
490
return when (selectedSubDialog) {
435
- EXTEND_NEW -> defaults.extendNew.initialValue.toString()
436
- EXTEND_REV -> defaults.extendReview.initialValue.toString()
491
+ EXTEND_NEW ->
492
+ deferredDefaults
493
+ .getCompleted()
494
+ .extendNew.initialValue
495
+ .toString()
496
+ EXTEND_REV ->
497
+ deferredDefaults
498
+ .getCompleted()
499
+ .extendReview.initialValue
500
+ .toString()
437
501
STUDY_FORGOT -> prefs.getInt(" forgottenDays" , 1 ).toString()
438
502
STUDY_AHEAD -> prefs.getInt(" aheadDays" , 1 ).toString()
439
503
STUDY_PREVIEW -> prefs.getInt(" previewDays" , 1 ).toString()
@@ -467,16 +531,19 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
467
531
468
532
/* *
469
533
* Context menu options shown in the custom study dialog.
534
+ *
535
+ * @param checkAvailability Whether the menu option is available
470
536
*/
471
537
@VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
472
538
enum class ContextMenuOption (
473
539
val getTitle : Resources .() -> String ,
540
+ val checkAvailability : ((CustomStudyDefaults ) -> Boolean )? = null ,
474
541
) {
475
542
/* * Increase today's new card limit */
476
- EXTEND_NEW ({ TR .customStudyIncreaseTodaysNewCardLimit() }),
543
+ EXTEND_NEW ({ TR .customStudyIncreaseTodaysNewCardLimit() }, checkAvailability = { it.extendNew.isUsable } ),
477
544
478
545
/* * Increase today's review card limit */
479
- EXTEND_REV ({ TR .customStudyIncreaseTodaysReviewCardLimit() }),
546
+ EXTEND_REV ({ TR .customStudyIncreaseTodaysReviewCardLimit() }, checkAvailability = { it.extendReview.isUsable } ),
480
547
481
548
/* * Review forgotten cards */
482
549
STUDY_FORGOT ({ TR .customStudyReviewForgottenCards() }),
@@ -608,15 +675,40 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
608
675
}
609
676
610
677
companion object {
611
- fun createInstance (
678
+ /* *
679
+ * @see CustomStudyDefaults
680
+ *
681
+ * Singleton; initialized when the main screen is loaded
682
+ * This exists so we don't need to pass an unbounded object between fragments
683
+ */
684
+ private lateinit var deferredDefaults: Deferred <CustomStudyDefaults >
685
+
686
+ /* *
687
+ * Creates an instance of the Custom Study Dialog: a user can select a custom study type
688
+ */
689
+ fun createInstance (deckId : DeckId ): CustomStudyDialog =
690
+ CustomStudyDialog ().apply {
691
+ arguments =
692
+ bundleOfNotNull(
693
+ ARG_DID to deckId,
694
+ )
695
+ }
696
+
697
+ /* *
698
+ * Creates an instance of the Custom Study sub-dialog for a user to configure
699
+ * a selected custom study type
700
+ *
701
+ * e.g. After selecting "Study Ahead", entering the number of days to study ahead by
702
+ */
703
+ fun createSubDialog (
612
704
deckId : DeckId ,
613
- contextMenuAttribute : ContextMenuOption ? = null ,
705
+ contextMenuAttribute : ContextMenuOption ,
614
706
): CustomStudyDialog =
615
707
CustomStudyDialog ().apply {
616
708
arguments =
617
709
bundleOfNotNull(
618
710
ARG_DID to deckId,
619
- contextMenuAttribute?. let { ARG_SUB_DIALOG_ID to it .ordinal } ,
711
+ ARG_SUB_DIALOG_ID to contextMenuAttribute .ordinal,
620
712
)
621
713
}
622
714
0 commit comments