Skip to content

Commit 251382b

Browse files
committed
Introduce ChangeNoteType Dialog
This dialog allows the bulk remapping of either fields or card templates to a different note type A full sync is required for this operation inputs: * output note type * a map of fields (based on the output) * a map of templates (based on the output) * only if both input and output are non-cloze NOTE: This is added as a developer option Fixes 14134
1 parent 4de0391 commit 251382b

File tree

31 files changed

+2436
-22
lines changed

31 files changed

+2436
-22
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import com.ichi2.anki.browser.CardBrowserLaunchOptions
5757
import com.ichi2.anki.browser.CardBrowserViewModel
5858
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode
5959
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode.SingleSelectCause
60+
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeNoteTypeResponse
6061
import com.ichi2.anki.browser.CardBrowserViewModel.SearchState
6162
import com.ichi2.anki.browser.CardBrowserViewModel.SearchState.Initializing
6263
import com.ichi2.anki.browser.CardBrowserViewModel.SearchState.Searching
@@ -69,6 +70,7 @@ import com.ichi2.anki.browser.toCardBrowserLaunchOptions
6970
import com.ichi2.anki.common.annotations.NeedsTest
7071
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
7172
import com.ichi2.anki.databinding.CardBrowserBinding
73+
import com.ichi2.anki.dialogs.ChangeNoteTypeDialog
7274
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
7375
import com.ichi2.anki.dialogs.DiscardChangesDialog
7476
import com.ichi2.anki.dialogs.GradeNowDialog
@@ -557,6 +559,19 @@ open class CardBrowser :
557559
showDialogFragment(dialog)
558560
}
559561

562+
fun onChangeNoteType(result: ChangeNoteTypeResponse) {
563+
when (result) {
564+
ChangeNoteTypeResponse.NoSelection -> {
565+
Timber.w("change note type: no selection")
566+
}
567+
ChangeNoteTypeResponse.MixedSelection -> showSnackbar(R.string.different_note_types_selected)
568+
is ChangeNoteTypeResponse.ChangeNoteType -> {
569+
val dialog = ChangeNoteTypeDialog.newInstance(result.nodeIds)
570+
showDialogFragment(dialog)
571+
}
572+
}
573+
}
574+
560575
viewModel.flowOfSearchQueryExpanded.launchCollectionInLifecycleScope(::onSearchQueryExpanded)
561576
viewModel.flowOfSelectedRows.launchCollectionInLifecycleScope(::onSelectedRowsChanged)
562577
viewModel.flowOfFilterQuery.launchCollectionInLifecycleScope(::onFilterQueryChanged)
@@ -567,6 +582,7 @@ open class CardBrowser :
567582
viewModel.flowOfSearchState.launchCollectionInLifecycleScope(::searchStateChanged)
568583
viewModel.cardSelectionEventFlow.launchCollectionInLifecycleScope(::onSelectedCardUpdated)
569584
viewModel.flowOfSaveSearchNamePrompt.launchCollectionInLifecycleScope(::onSaveSearchNamePrompt)
585+
viewModel.flowOfChangeNoteType.launchCollectionInLifecycleScope(::onChangeNoteType)
570586
}
571587

572588
fun isKeyboardVisible(view: View?): Boolean =
@@ -677,6 +693,13 @@ open class CardBrowser :
677693
return true
678694
}
679695
}
696+
KeyEvent.KEYCODE_M -> {
697+
if (event.isCtrlPressed && event.isShiftPressed) {
698+
Timber.i("Ctrl+Shift+M: Change Note Type")
699+
viewModel.requestChangeNoteType()
700+
return true
701+
}
702+
}
680703
KeyEvent.KEYCODE_Z -> {
681704
if (event.isCtrlPressed) {
682705
Timber.i("Ctrl+Z: Undo")
@@ -866,6 +889,12 @@ open class CardBrowser :
866889
isVisible = isFindReplaceEnabled
867890
title = TR.browsingFindAndReplace().toSentenceCase(this@CardBrowser, R.string.sentence_find_and_replace)
868891
}
892+
893+
val isChangeNoteTypeEnabled = sharedPrefs().getBoolean(getString(R.string.pref_change_note_type), false)
894+
menu.findItem(R.id.action_change_note_type)?.apply {
895+
isVisible = isChangeNoteTypeEnabled
896+
title = TR.browsingChangeNoteType().toSentenceCase(this@CardBrowser, R.string.sentence_change_note_type)
897+
}
869898
previewItem = menu.findItem(R.id.action_preview)
870899
onSelectionChanged()
871900
refreshMenuItems()
@@ -1039,6 +1068,11 @@ open class CardBrowser :
10391068
showSavedSearches()
10401069
return true
10411070
}
1071+
R.id.action_change_note_type -> {
1072+
Timber.i("Menu: Change note type")
1073+
viewModel.requestChangeNoteType()
1074+
return true
1075+
}
10421076
R.id.action_undo -> {
10431077
Timber.w("CardBrowser:: Undo pressed")
10441078
onUndo()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2519,7 +2519,7 @@ class OneWaySyncDialog(
25192519
/**
25202520
* [launchCatchingTask], showing a one-way sync dialog: [R.string.full_sync_confirmation]
25212521
*/
2522-
private fun AnkiActivity.launchCatchingRequiringOneWaySync(block: suspend () -> Unit) =
2522+
fun AnkiActivity.launchCatchingRequiringOneWaySync(block: suspend () -> Unit) =
25232523
launchCatchingTask {
25242524
try {
25252525
block()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ class NoteEditorFragment :
13981398
* Change the note type from oldNoteType to newNoteType, handling the case where a full sync will be required
13991399
*/
14001400
@NeedsTest("test changing note type")
1401+
@Suppress("Deprecation") // Replace with ChangeNoteTypeDialog
14011402
private fun changeNoteType(
14021403
oldNotetype: NotetypeJson,
14031404
newNotetype: NotetypeJson,

AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import com.ichi2.anki.libanki.Card
5656
import com.ichi2.anki.libanki.CardId
5757
import com.ichi2.anki.libanki.CardType
5858
import com.ichi2.anki.libanki.DeckId
59+
import com.ichi2.anki.libanki.NoteId
5960
import com.ichi2.anki.libanki.QueueType
6061
import com.ichi2.anki.libanki.QueueType.ManuallyBuried
6162
import com.ichi2.anki.libanki.QueueType.SiblingBuried
@@ -260,6 +261,8 @@ class CardBrowserViewModel(
260261
*/
261262
val flowOfCardStateChanged = MutableSharedFlow<Unit>()
262263

264+
val flowOfChangeNoteType = MutableSharedFlow<ChangeNoteTypeResponse>()
265+
263266
/**
264267
* Opens a prompt for the user to input a saved search name
265268
*
@@ -277,6 +280,19 @@ class CardBrowserViewModel(
277280

278281
suspend fun queryAllSelectedNoteIds() = selectedRows.queryNoteIds(this.cardsOrNotes)
279282

283+
fun requestChangeNoteType() =
284+
viewModelScope.launch {
285+
Timber.i("launchChangeNoteType")
286+
val noteIds = queryAllSelectedNoteIds()
287+
flowOfChangeNoteType.emit(
288+
when {
289+
noteIds.isEmpty() -> ChangeNoteTypeResponse.NoSelection
290+
!noteIds.allOfSameNoteType() -> ChangeNoteTypeResponse.MixedSelection
291+
else -> ChangeNoteTypeResponse.ChangeNoteType.from(noteIds)
292+
},
293+
)
294+
}
295+
280296
@VisibleForTesting
281297
internal suspend fun queryAllCardIds() = cards.queryCardIds()
282298

@@ -1331,6 +1347,25 @@ class CardBrowserViewModel(
13311347
SELECT_NONE,
13321348
}
13331349

1350+
sealed interface ChangeNoteTypeResponse {
1351+
data object NoSelection : ChangeNoteTypeResponse
1352+
1353+
data object MixedSelection : ChangeNoteTypeResponse
1354+
1355+
@ConsistentCopyVisibility
1356+
data class ChangeNoteType private constructor(
1357+
val nodeIds: List<NoteId>,
1358+
) : ChangeNoteTypeResponse {
1359+
companion object {
1360+
@CheckResult
1361+
fun from(ids: List<NoteId>): ChangeNoteType {
1362+
require(ids.isNotEmpty()) { "a non-empty list must be provided" }
1363+
return ChangeNoteType(ids.distinct())
1364+
}
1365+
}
1366+
}
1367+
}
1368+
13341369
/**
13351370
* @param wasBuried `true` if all cards were buried, `false` if unburied
13361371
* @param count the number of affected cards
@@ -1501,6 +1536,16 @@ sealed class RepositionCardsRequest {
15011536

15021537
fun BrowserColumns.Column.getLabel(cardsOrNotes: CardsOrNotes): String = if (cardsOrNotes == CARDS) cardsModeLabel else notesModeLabel
15031538

1539+
/**
1540+
* Whether the provided notes all have the same the same [note type][com.ichi2.anki.libanki.NoteTypeId]
1541+
*/
1542+
private suspend fun List<NoteId>.allOfSameNoteType(): Boolean {
1543+
val noteIds = this
1544+
return withCol { notetypes.nids(getNote(noteIds.first()).noteTypeId) }.toSet().let { set ->
1545+
noteIds.all { set.contains(it) }
1546+
}
1547+
}
1548+
15041549
@Parcelize
15051550
data class ColumnHeading(
15061551
val label: String,

0 commit comments

Comments
 (0)