Skip to content

Commit 9f845f0

Browse files
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 Fixes 14134 Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
1 parent c7f5425 commit 9f845f0

File tree

32 files changed

+2488
-23
lines changed

32 files changed

+2488
-23
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
@@ -55,6 +55,7 @@ import com.ichi2.anki.browser.CardBrowserLaunchOptions
5555
import com.ichi2.anki.browser.CardBrowserViewModel
5656
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode
5757
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode.SingleSelectCause
58+
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeNoteTypeResponse
5859
import com.ichi2.anki.browser.CardBrowserViewModel.SearchState
5960
import com.ichi2.anki.browser.CardBrowserViewModel.SearchState.Initializing
6061
import com.ichi2.anki.browser.CardBrowserViewModel.SearchState.Searching
@@ -67,6 +68,7 @@ import com.ichi2.anki.browser.toCardBrowserLaunchOptions
6768
import com.ichi2.anki.common.annotations.NeedsTest
6869
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
6970
import com.ichi2.anki.databinding.ActivityCardBrowserBinding
71+
import com.ichi2.anki.dialogs.ChangeNoteTypeDialog
7072
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
7173
import com.ichi2.anki.dialogs.DiscardChangesDialog
7274
import com.ichi2.anki.dialogs.GradeNowDialog
@@ -546,6 +548,19 @@ open class CardBrowser :
546548
showDialogFragment(dialog)
547549
}
548550

551+
fun onChangeNoteType(result: ChangeNoteTypeResponse) {
552+
when (result) {
553+
ChangeNoteTypeResponse.NoSelection -> {
554+
Timber.w("change note type: no selection")
555+
}
556+
ChangeNoteTypeResponse.MixedSelection -> showSnackbar(R.string.different_note_types_selected)
557+
is ChangeNoteTypeResponse.ChangeNoteType -> {
558+
val dialog = ChangeNoteTypeDialog.newInstance(result.noteIds)
559+
showDialogFragment(dialog)
560+
}
561+
}
562+
}
563+
549564
viewModel.flowOfSearchQueryExpanded.launchCollectionInLifecycleScope(::onSearchQueryExpanded)
550565
viewModel.flowOfSelectedRows.launchCollectionInLifecycleScope(::onSelectedRowsChanged)
551566
viewModel.flowOfFilterQuery.launchCollectionInLifecycleScope(::onFilterQueryChanged)
@@ -555,6 +570,7 @@ open class CardBrowser :
555570
viewModel.flowOfSearchState.launchCollectionInLifecycleScope(::searchStateChanged)
556571
viewModel.cardSelectionEventFlow.launchCollectionInLifecycleScope(::onSelectedCardUpdated)
557572
viewModel.flowOfSaveSearchNamePrompt.launchCollectionInLifecycleScope(::onSaveSearchNamePrompt)
573+
viewModel.flowOfChangeNoteType.launchCollectionInLifecycleScope(::onChangeNoteType)
558574
}
559575

560576
fun isKeyboardVisible(view: View?): Boolean =
@@ -665,6 +681,13 @@ open class CardBrowser :
665681
return true
666682
}
667683
}
684+
KeyEvent.KEYCODE_M -> {
685+
if (event.isCtrlPressed && event.isShiftPressed) {
686+
Timber.i("Ctrl+Shift+M: Change Note Type")
687+
viewModel.requestChangeNoteType()
688+
return true
689+
}
690+
}
668691
KeyEvent.KEYCODE_Z -> {
669692
if (event.isCtrlPressed) {
670693
Timber.i("Ctrl+Z: Undo")
@@ -854,6 +877,7 @@ open class CardBrowser :
854877
isVisible = isFindReplaceEnabled
855878
title = TR.browsingFindAndReplace().toSentenceCase(this@CardBrowser, R.string.sentence_find_and_replace)
856879
}
880+
857881
previewItem = menu.findItem(R.id.action_preview)
858882
onSelectionChanged()
859883
refreshMenuItems()
@@ -1016,6 +1040,11 @@ open class CardBrowser :
10161040
showSavedSearches()
10171041
return true
10181042
}
1043+
R.id.action_change_note_type -> {
1044+
Timber.i("Menu: Change note type")
1045+
viewModel.requestChangeNoteType()
1046+
return true
1047+
}
10191048
R.id.action_undo -> {
10201049
Timber.w("CardBrowser:: Undo pressed")
10211050
onUndo()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2495,7 +2495,7 @@ class OneWaySyncDialog(
24952495
/**
24962496
* [launchCatchingTask], showing a one-way sync dialog: [R.string.full_sync_confirmation]
24972497
*/
2498-
private fun AnkiActivity.launchCatchingRequiringOneWaySync(block: suspend () -> Unit) =
2498+
fun AnkiActivity.launchCatchingRequiringOneWaySync(block: suspend () -> Unit) =
24992499
launchCatchingTask {
25002500
try {
25012501
block()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,7 @@ class NoteEditorFragment :
14361436
* Change the note type from oldNoteType to newNoteType, handling the case where a full sync will be required
14371437
*/
14381438
@NeedsTest("test changing note type")
1439+
@Suppress("Deprecation") // Replace with ChangeNoteTypeDialog
14391440
private fun changeNoteType(
14401441
oldNotetype: NotetypeJson,
14411442
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
@@ -267,6 +268,8 @@ class CardBrowserViewModel(
267268
*/
268269
val flowOfCardStateChanged = MutableSharedFlow<Unit>()
269270

271+
val flowOfChangeNoteType = MutableSharedFlow<ChangeNoteTypeResponse>()
272+
270273
/**
271274
* Opens a prompt for the user to input a saved search name
272275
*
@@ -284,6 +287,19 @@ class CardBrowserViewModel(
284287

285288
suspend fun queryAllSelectedNoteIds() = selectedRows.queryNoteIds(this.cardsOrNotes)
286289

290+
fun requestChangeNoteType() =
291+
viewModelScope.launch {
292+
val noteIds = queryAllSelectedNoteIds()
293+
Timber.i("requestChangeNoteType: querying %d selected notes", noteIds.size)
294+
flowOfChangeNoteType.emit(
295+
when {
296+
noteIds.isEmpty() -> ChangeNoteTypeResponse.NoSelection
297+
!noteIds.allOfSameNoteType() -> ChangeNoteTypeResponse.MixedSelection
298+
else -> ChangeNoteTypeResponse.ChangeNoteType.from(noteIds)
299+
},
300+
)
301+
}
302+
287303
@VisibleForTesting
288304
internal suspend fun queryAllCardIds() = cards.queryCardIds()
289305

@@ -1351,6 +1367,25 @@ class CardBrowserViewModel(
13511367
SELECT_NONE,
13521368
}
13531369

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

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

1553+
/**
1554+
* Whether the provided notes all have the same the same [note type][com.ichi2.anki.libanki.NoteTypeId]
1555+
*/
1556+
private suspend fun List<NoteId>.allOfSameNoteType(): Boolean {
1557+
val noteIds = this
1558+
return withCol { notetypes.nids(getNote(noteIds.first()).noteTypeId) }.toSet().let { set ->
1559+
noteIds.all { set.contains(it) }
1560+
}
1561+
}
1562+
15181563
@Parcelize
15191564
data class ColumnHeading(
15201565
val label: String,

0 commit comments

Comments
 (0)