Skip to content

Commit 1f914bc

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 b825f75 commit 1f914bc

File tree

30 files changed

+2417
-22
lines changed

30 files changed

+2417
-22
lines changed

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

Lines changed: 29 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.noteIds)
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,7 @@ open class CardBrowser :
866889
isVisible = isFindReplaceEnabled
867890
title = TR.browsingFindAndReplace().toSentenceCase(this@CardBrowser, R.string.sentence_find_and_replace)
868891
}
892+
869893
previewItem = menu.findItem(R.id.action_preview)
870894
onSelectionChanged()
871895
refreshMenuItems()
@@ -1039,6 +1063,11 @@ open class CardBrowser :
10391063
showSavedSearches()
10401064
return true
10411065
}
1066+
R.id.action_change_note_type -> {
1067+
Timber.i("Menu: Change note type")
1068+
viewModel.requestChangeNoteType()
1069+
return true
1070+
}
10421071
R.id.action_undo -> {
10431072
Timber.w("CardBrowser:: Undo pressed")
10441073
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
@@ -265,6 +266,8 @@ class CardBrowserViewModel(
265266
*/
266267
val flowOfCardStateChanged = MutableSharedFlow<Unit>()
267268

269+
val flowOfChangeNoteType = MutableSharedFlow<ChangeNoteTypeResponse>()
270+
268271
/**
269272
* Opens a prompt for the user to input a saved search name
270273
*
@@ -282,6 +285,19 @@ class CardBrowserViewModel(
282285

283286
suspend fun queryAllSelectedNoteIds() = selectedRows.queryNoteIds(this.cardsOrNotes)
284287

288+
fun requestChangeNoteType() =
289+
viewModelScope.launch {
290+
Timber.i("launchChangeNoteType")
291+
val noteIds = queryAllSelectedNoteIds()
292+
flowOfChangeNoteType.emit(
293+
when {
294+
noteIds.isEmpty() -> ChangeNoteTypeResponse.NoSelection
295+
!noteIds.allOfSameNoteType() -> ChangeNoteTypeResponse.MixedSelection
296+
else -> ChangeNoteTypeResponse.ChangeNoteType.from(noteIds)
297+
},
298+
)
299+
}
300+
285301
@VisibleForTesting
286302
internal suspend fun queryAllCardIds() = cards.queryCardIds()
287303

@@ -1354,6 +1370,25 @@ class CardBrowserViewModel(
13541370
SELECT_NONE,
13551371
}
13561372

1373+
sealed interface ChangeNoteTypeResponse {
1374+
data object NoSelection : ChangeNoteTypeResponse
1375+
1376+
data object MixedSelection : ChangeNoteTypeResponse
1377+
1378+
@ConsistentCopyVisibility
1379+
data class ChangeNoteType private constructor(
1380+
val noteIds: List<NoteId>,
1381+
) : ChangeNoteTypeResponse {
1382+
companion object {
1383+
@CheckResult
1384+
fun from(ids: List<NoteId>): ChangeNoteType {
1385+
require(ids.isNotEmpty()) { "a non-empty list must be provided" }
1386+
return ChangeNoteType(ids.distinct())
1387+
}
1388+
}
1389+
}
1390+
}
1391+
13571392
/**
13581393
* @param wasBuried `true` if all cards were buried, `false` if unburied
13591394
* @param count the number of affected cards
@@ -1524,6 +1559,16 @@ sealed class RepositionCardsRequest {
15241559

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

1562+
/**
1563+
* Whether the provided notes all have the same the same [note type][com.ichi2.anki.libanki.NoteTypeId]
1564+
*/
1565+
private suspend fun List<NoteId>.allOfSameNoteType(): Boolean {
1566+
val noteIds = this
1567+
return withCol { notetypes.nids(getNote(noteIds.first()).noteTypeId) }.toSet().let { set ->
1568+
noteIds.all { set.contains(it) }
1569+
}
1570+
}
1571+
15271572
@Parcelize
15281573
data class ColumnHeading(
15291574
val label: String,

0 commit comments

Comments
 (0)