Skip to content

Commit 1137777

Browse files
authored
Merge branch 'main' into 18479-fix-custom-study-navigation
2 parents 93769f2 + c951b0c commit 1137777

File tree

259 files changed

+1137
-414
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

259 files changed

+1137
-414
lines changed

AnkiDroid/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ android {
8585
//
8686
// This ensures the correct ordering between the various types of releases (dev < alpha < beta < release) which is
8787
// needed for upgrades to be offered correctly.
88-
versionCode=22100118
89-
versionName="2.21alpha18"
88+
versionCode=22100122
89+
versionName="2.21alpha22"
9090
minSdk = libs.versions.minSdk.get().toInteger()
9191

9292
// After #13695: change .tests_emulator.yml

AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import android.view.MenuInflater
3333
import android.view.MenuItem
3434
import android.view.View
3535
import android.view.ViewGroup
36+
import android.widget.LinearLayout
37+
import android.widget.ScrollView
3638
import androidx.activity.OnBackPressedCallback
3739
import androidx.activity.result.ActivityResult
3840
import androidx.activity.result.contract.ActivityResultContracts
@@ -58,6 +60,7 @@ import anki.notetypes.StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_UNKNOW
5860
import anki.notetypes.notetypeId
5961
import com.google.android.material.bottomnavigation.BottomNavigationView
6062
import com.google.android.material.button.MaterialButton
63+
import com.google.android.material.card.MaterialCardView
6164
import com.google.android.material.snackbar.Snackbar
6265
import com.google.android.material.tabs.TabLayout
6366
import com.google.android.material.tabs.TabLayoutMediator
@@ -75,10 +78,12 @@ import com.ichi2.anki.dialogs.InsertFieldDialog
7578
import com.ichi2.anki.dialogs.InsertFieldDialog.Companion.REQUEST_FIELD_INSERT
7679
import com.ichi2.anki.notetype.RenameCardTemplateDialog
7780
import com.ichi2.anki.notetype.RepositionCardTemplateDialog
81+
import com.ichi2.anki.preferences.sharedPrefs
7882
import com.ichi2.anki.previewer.TemplatePreviewerArguments
7983
import com.ichi2.anki.previewer.TemplatePreviewerFragment
8084
import com.ichi2.anki.previewer.TemplatePreviewerPage
8185
import com.ichi2.anki.snackbar.showSnackbar
86+
import com.ichi2.anki.ui.ResizablePaneManager
8287
import com.ichi2.anki.utils.ext.dismissAllDialogFragments
8388
import com.ichi2.anki.utils.ext.showDialogFragment
8489
import com.ichi2.anki.utils.postDelayed
@@ -99,7 +104,6 @@ import com.ichi2.libanki.restoreNotetypeToStock
99104
import com.ichi2.libanki.undoableOp
100105
import com.ichi2.themes.Themes
101106
import com.ichi2.ui.FixedEditText
102-
import com.ichi2.ui.FixedTextView
103107
import com.ichi2.utils.copyToClipboard
104108
import com.ichi2.utils.dp
105109
import com.ichi2.utils.listItems
@@ -209,6 +213,24 @@ open class CardTemplateEditor :
209213
enableToolbar()
210214
startLoadingCollection()
211215

216+
if (fragmented) {
217+
val parentLayout = findViewById<LinearLayout>(R.id.card_template_editor_xl_view)
218+
val divider = findViewById<View>(R.id.card_template_editor_resizing_divider)
219+
val leftPane = findViewById<View>(R.id.template_editor)
220+
val rightPane = findViewById<View>(R.id.fragment_container)
221+
if (parentLayout != null && divider != null && leftPane != null && rightPane != null) {
222+
ResizablePaneManager(
223+
parentLayout = parentLayout,
224+
divider = divider,
225+
leftPane = leftPane,
226+
rightPane = rightPane,
227+
sharedPrefs = this.sharedPrefs(),
228+
leftPaneWeightKey = PREF_TEMPLATE_EDITOR_PANE_WEIGHT,
229+
rightPaneWeightKey = PREF_TEMPLATE_PREVIEWER_PANE_WEIGHT,
230+
)
231+
}
232+
}
233+
212234
// Open TemplatePreviewerFragment if in fragmented mode
213235
loadTemplatePreviewerFragmentIfFragmented()
214236
onBackPressedDispatcher.addCallback(this, displayDiscardChangesCallback)
@@ -249,6 +271,15 @@ open class CardTemplateEditor :
249271
button.layoutParams.height = 80.dp.toPx(button.context)
250272
button.requestLayout()
251273
}
274+
275+
// Adjust the top margin of the webview container to match template editor top margin
276+
val webView = fragment.view?.findViewById<MaterialCardView>(R.id.webview_container)
277+
webView?.let { container ->
278+
val params = container.layoutParams as ViewGroup.MarginLayoutParams
279+
val topMargin = resources.getDimensionPixelSize(R.dimen.reviewer_side_margin)
280+
params.topMargin = topMargin
281+
container.layoutParams = params
282+
}
252283
}
253284
}
254285
}
@@ -489,7 +520,6 @@ open class CardTemplateEditor :
489520

490521
class CardTemplateFragment : Fragment() {
491522
private val refreshFragmentHandler = Handler(Looper.getMainLooper())
492-
private var currentEditorTitle: FixedTextView? = null
493523
private lateinit var editorEditText: FixedEditText
494524

495525
var currentEditorViewId = 0
@@ -518,25 +548,57 @@ open class CardTemplateEditor :
518548
return mainView
519549
}
520550

521-
currentEditorTitle = mainView.findViewById(R.id.title_edit)
522551
editorEditText = mainView.findViewById(R.id.editor_editText)
523552
cursorPosition = requireArguments().getInt(CURSOR_POSITION_KEY)
524553

525554
editorEditText.customInsertionActionModeCallback = ActionModeCallback()
526555

527556
bottomNavigation = mainView.findViewById(R.id.card_template_editor_bottom_navigation)
557+
558+
// If in fragmented mode, wrap the edit area in a MaterialCardView
559+
if (templateEditor.fragmented) {
560+
val mainLayout = mainView.findViewById<LinearLayout>(R.id.main_layout)
561+
562+
// Set the background color of the main layout to match the previewer
563+
mainLayout.setBackgroundColor(Themes.getColorFromAttr(requireContext(), R.attr.alternativeBackgroundColor))
564+
565+
// Create a MaterialCardView to wrap the editorEditText
566+
val cardView =
567+
MaterialCardView(requireContext()).apply {
568+
layoutParams =
569+
LinearLayout
570+
.LayoutParams(
571+
LinearLayout.LayoutParams.MATCH_PARENT,
572+
0,
573+
1f,
574+
).apply {
575+
val sideMargin = resources.getDimensionPixelSize(R.dimen.reviewer_side_margin)
576+
setMargins(sideMargin, 0, sideMargin, 0)
577+
}
578+
}
579+
580+
// Remove the ScrollView from the main layout and add it to the cardView
581+
val editScrollView = mainLayout.findViewById<ScrollView>(R.id.card_template_editor_scroll_view)
582+
mainLayout.removeViewInLayout(editScrollView)
583+
584+
cardView.addView(
585+
editScrollView,
586+
ViewGroup.LayoutParams(
587+
ViewGroup.LayoutParams.MATCH_PARENT,
588+
ViewGroup.LayoutParams.MATCH_PARENT,
589+
),
590+
)
591+
592+
mainLayout.addView(cardView, 0)
593+
}
594+
528595
bottomNavigation.setOnItemSelectedListener { item: MenuItem ->
529596
val currentSelectedId = item.itemId
530597
templateEditor.tabToViewId[cardIndex] = currentSelectedId
531598
when (currentSelectedId) {
532-
R.id.styling_edit -> setCurrentEditorView(currentSelectedId, tempModel.css, R.string.card_template_editor_styling)
533-
R.id.back_edit ->
534-
setCurrentEditorView(
535-
currentSelectedId,
536-
template.afmt,
537-
R.string.card_template_editor_back,
538-
)
539-
else -> setCurrentEditorView(currentSelectedId, template.qfmt, R.string.card_template_editor_front)
599+
R.id.styling_edit -> setCurrentEditorView(currentSelectedId, tempModel.css)
600+
R.id.back_edit -> setCurrentEditorView(currentSelectedId, template.afmt)
601+
else -> setCurrentEditorView(currentSelectedId, template.qfmt)
540602
}
541603
// contents of menu have changed and menu should be redrawn
542604
templateEditor.invalidateOptionsMenu()
@@ -606,6 +668,14 @@ open class CardTemplateEditor :
606668
insets
607669
}
608670

671+
/**
672+
* We focus on the editText to indicate it's editable, but we don't automatically
673+
* show the keyboard. This is intentional - the keyboard should only appear
674+
* when the user taps on the edit field, not every time the fragment loads.
675+
*/
676+
editorEditText.post {
677+
editorEditText.requestFocus()
678+
}
609679
return mainView
610680
}
611681

@@ -713,11 +783,9 @@ open class CardTemplateEditor :
713783
fun setCurrentEditorView(
714784
id: Int,
715785
editorContent: String,
716-
editorTitleId: Int,
717786
) {
718787
currentEditorViewId = id
719788
editorEditText.setText(editorContent)
720-
currentEditorTitle!!.text = resources.getString(editorTitleId)
721789
editorEditText.setSelection(cursorPosition)
722790
editorEditText.requestFocus()
723791
}
@@ -1415,6 +1483,10 @@ open class CardTemplateEditor :
14151483
private const val EDITOR_START_ORD_ID = "ordId"
14161484
private const val CARD_INDEX = "card_ord"
14171485

1486+
// Keys for saving pane weights in SharedPreferences
1487+
private const val PREF_TEMPLATE_EDITOR_PANE_WEIGHT = "cardTemplateEditorPaneWeight"
1488+
private const val PREF_TEMPLATE_PREVIEWER_PANE_WEIGHT = "cardTemplatePreviewerPaneWeight"
1489+
14181490
// Time to wait before refreshing the previewer
14191491
private val REFRESH_PREVIEW_DELAY = 1.seconds
14201492

AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ fun <T> ViewModel.asyncIO(block: suspend CoroutineScope.() -> T): Deferred<T> =
137137
*/
138138
suspend fun <T> FragmentActivity.runCatching(
139139
errorMessage: String? = null,
140+
skipCrashReport: ((Exception) -> Boolean)? = null,
140141
block: suspend () -> T?,
141142
): T? {
142143
// appends the pre-coroutine stack to the error message. Example:
@@ -158,6 +159,11 @@ suspend fun <T> FragmentActivity.runCatching(
158159
try {
159160
return block()
160161
} catch (exc: Exception) {
162+
if (skipCrashReport?.invoke(exc) == true) {
163+
Timber.i("Showing error dialog but not sending a crash report.")
164+
showError(this, exc.localizedMessage!!, exc, false, enableEnterKeyHandler = true)
165+
return null
166+
}
161167
when (exc) {
162168
is CancellationException -> {
163169
throw exc // CancellationException should be re-thrown to propagate it to the parent coroutine
@@ -225,19 +231,21 @@ fun getCoroutineExceptionHandler(
225231
*/
226232
fun FragmentActivity.launchCatchingTask(
227233
errorMessage: String? = null,
234+
skipCrashReport: ((Exception) -> Boolean)? = null,
228235
block: suspend CoroutineScope.() -> Unit,
229236
): Job =
230237
lifecycle.coroutineScope.launch {
231-
runCatching(errorMessage) { block() }
238+
runCatching(errorMessage, skipCrashReport = skipCrashReport) { block() }
232239
}
233240

234241
/** See [FragmentActivity.launchCatchingTask] */
235242
fun Fragment.launchCatchingTask(
236243
errorMessage: String? = null,
244+
skipCrashReport: ((Exception) -> Boolean)? = null,
237245
block: suspend CoroutineScope.() -> Unit,
238246
): Job =
239247
lifecycle.coroutineScope.launch {
240-
requireActivity().runCatching(errorMessage) { block() }
248+
requireActivity().runCatching(errorMessage, skipCrashReport = skipCrashReport) { block() }
241249
}
242250

243251
fun showError(
@@ -273,6 +281,7 @@ fun showError(
273281
message(text = msg)
274282
positiveButton(R.string.dialog_ok)
275283
if (crashReport) {
284+
Timber.w("sending crash report on close")
276285
setOnDismissListener {
277286
CrashReportService.sendExceptionReport(
278287
exception,

AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ object CrashReportService {
6161
"ActivityManager:I",
6262
"SQLiteLog:W",
6363
AnkiDroidApp.TAG + ":D",
64+
"rsdroid:E",
6465
"*:S",
6566
)
6667

AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ import androidx.core.content.edit
6666
import androidx.core.content.pm.ShortcutInfoCompat
6767
import androidx.core.content.pm.ShortcutManagerCompat
6868
import androidx.core.graphics.drawable.IconCompat
69-
import androidx.core.net.toUri
7069
import androidx.core.os.bundleOf
7170
import androidx.core.util.component1
7271
import androidx.core.util.component2
@@ -133,6 +132,7 @@ import com.ichi2.anki.dialogs.EmptyCardsDialogFragment
133132
import com.ichi2.anki.dialogs.ImportDialog.ImportDialogListener
134133
import com.ichi2.anki.dialogs.ImportFileSelectionFragment.ApkgImportResultLauncherProvider
135134
import com.ichi2.anki.dialogs.ImportFileSelectionFragment.CsvImportResultLauncherProvider
135+
import com.ichi2.anki.dialogs.SchedulerUpgradeDialog
136136
import com.ichi2.anki.dialogs.SyncErrorDialog
137137
import com.ichi2.anki.dialogs.SyncErrorDialog.Companion.newInstance
138138
import com.ichi2.anki.dialogs.SyncErrorDialog.SyncErrorDialogListener
@@ -190,7 +190,6 @@ import com.ichi2.utils.customView
190190
import com.ichi2.utils.dp
191191
import com.ichi2.utils.message
192192
import com.ichi2.utils.negativeButton
193-
import com.ichi2.utils.neutralButton
194193
import com.ichi2.utils.positiveButton
195194
import com.ichi2.utils.show
196195
import com.ichi2.utils.title
@@ -479,6 +478,12 @@ open class DeckPicker :
479478
Timber.i("notification permission: %b", it)
480479
}
481480

481+
/**
482+
* Tracks the scheduler version for which the upgrade dialog was last shown,
483+
* to avoid repeatedly prompting the user for the same collection version.
484+
*/
485+
private var schedulerUpgradeDialogShownForVersion: Long? = null
486+
482487
// ----------------------------------------------------------------------------
483488
// ANDROID ACTIVITY METHODS
484489
// ----------------------------------------------------------------------------
@@ -523,7 +528,7 @@ open class DeckPicker :
523528
if (fragmented && !startupError) {
524529
loadStudyOptionsFragment(false)
525530

526-
val resizingDivider = findViewById<View>(R.id.resizing_divider)
531+
val resizingDivider = findViewById<View>(R.id.homescreen_resizing_divider)
527532
val parentLayout = findViewById<LinearLayout>(R.id.deckpicker_xl_view)
528533

529534
// Get references to the panes
@@ -2093,25 +2098,18 @@ open class DeckPicker :
20932098
}
20942099

20952100
private fun promptUserToUpdateScheduler() {
2096-
AlertDialog.Builder(this).show {
2097-
message(text = TR.schedulingUpdateRequired())
2098-
positiveButton(R.string.dialog_ok) {
2099-
launchCatchingTask {
2100-
if (!userAcceptsSchemaChange(getColUnsafe)) {
2101-
return@launchCatchingTask
2102-
}
2103-
withProgress { withCol { sched.upgradeToV2() } }
2101+
SchedulerUpgradeDialog(
2102+
activity = this,
2103+
onUpgrade = {
2104+
this@DeckPicker.launchCatchingTask {
2105+
this@DeckPicker.withProgress { withCol { sched.upgradeToV2() } }
21042106
showThemedToast(this@DeckPicker, TR.schedulingUpdateDone(), false)
2105-
refreshState()
2106-
}
2107-
}
2108-
negativeButton(R.string.dialog_cancel)
2109-
if (AdaptionUtil.hasWebBrowser(this@DeckPicker)) {
2110-
neutralButton(text = TR.schedulingUpdateMoreInfoButton()) {
2111-
this@DeckPicker.openUrl("https://faqs.ankiweb.net/the-anki-2.1-scheduler.html#updating".toUri())
21122107
}
2113-
}
2114-
}
2108+
},
2109+
onCancel = {
2110+
onBackPressedDispatcher.onBackPressed()
2111+
},
2112+
).showDialog()
21152113
}
21162114

21172115
@NeedsTest("14608: Ensure that the deck options refer to the selected deck")
@@ -2143,10 +2141,6 @@ open class DeckPicker :
21432141
}
21442142
}
21452143

2146-
if (withCol { ((config.get("schedVer") ?: 1L) == 1L) }) {
2147-
promptUserToUpdateScheduler()
2148-
return
2149-
}
21502144
withCol { decks.select(did) }
21512145
deckListAdapter.updateSelectedDeck(did)
21522146
// Also forget the last deck used by the Browser
@@ -2226,6 +2220,19 @@ open class DeckPicker :
22262220
}
22272221
onDecksLoaded(deckDueTree, collectionHasNoCards)
22282222

2223+
/**
2224+
* Checks the current scheduler version and prompts the upgrade dialog if using the legacy version.
2225+
* Ensures the dialog is only shown once per collection load, even if [updateDeckList()] is called multiple times.
2226+
*/
2227+
val currentSchedulerVersion = withCol { config.get("schedVer") as? Long ?: 1L }
2228+
2229+
if (currentSchedulerVersion == 1L && schedulerUpgradeDialogShownForVersion != 1L) {
2230+
schedulerUpgradeDialogShownForVersion = 1L
2231+
promptUserToUpdateScheduler()
2232+
} else {
2233+
schedulerUpgradeDialogShownForVersion = currentSchedulerVersion
2234+
}
2235+
22292236
updateUndoMenuState()
22302237
}
22312238
}

0 commit comments

Comments
 (0)