From 672b075a546e806e93f8e2c158749231c065a021 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:51:12 +0000 Subject: [PATCH] refactor: use Context receivers to remove `this` within `withCol` [Experimental API] (#15254) * chore: enable context receivers (experimental) Allows removal of usages of `this` in `withCol`/`undoableOp` calls Context receivers allow `this` as an implicit parameter to a method if `this` is in scope (typically due to a `with` call) This means that: * methods are only usable if `Collection` is scoped * you can't easily call these methods outside `withCol` * methods can effectively accept two context receivers * in the below, both `Card` and `Collection` are scoped to `this` Example ```kotlin context(Collection) fun Card.note() = this.note(this@Collection) ``` allows the transformation ```diff - val note = withCol { card.note(this) } + val note = withCol { card.note() } ``` Which is much more readable, as `this` was confusingly named https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md https://blog.rockthejvm.com/kotlin-context-receivers/ * chore: context receiver for Card.renderOutput() Once refactoring is complete, the `context` should be moved to the instance method, and the extension should be removed * chore: context receiver for Card.note() Once refactoring is complete, the `context` should be moved to the instance method, and the extension should be removed * chore: context receiver for AnswerTimer.resume() * chore: context receiver for CardTemplateNotetype.saveToDatabase * chore: context receiver for Sequence.toCardCache * chore: context receiver for BackupManager.repairCollection() * chore: context receiver for CollectionOperations.deleteMedia * chore: context receiver for CollectionOperations.updateValuesFromDeck * chore: context receiver for CollectionOperations.saveModel * modifies saveToDatabase, as this always required the context * chore: remove completed comment in CollectionOperations * chore: context receiver for CardsOrNotes.fromCollection * chore: context receiver for CardService.selectedNoteIds + accept CardId * chore: context receiver for CardsOrNotes.saveToCollection * chore: context receiver for BackupManager.deleteBackups() * chore: context receiver for CardSoundConfig.create() * add context receiver * inline suspend fun create * make the `col` usage explicit * review request: import 'Collection' --- .../com/ichi2/anki/AbstractFlashcardViewer.kt | 4 +-- .../main/java/com/ichi2/anki/BackupManager.kt | 13 +++---- .../main/java/com/ichi2/anki/CardBrowser.kt | 18 +++++----- .../java/com/ichi2/anki/CardTemplateEditor.kt | 4 +-- .../com/ichi2/anki/CardTemplateNotetype.kt | 6 ++-- .../main/java/com/ichi2/anki/DeckPicker.kt | 8 ++--- .../src/main/java/com/ichi2/anki/Reviewer.kt | 4 +-- .../com/ichi2/anki/StudyOptionsFragment.kt | 6 ++-- .../anki/browser/CardBrowserViewModel.kt | 6 ++-- .../ichi2/anki/cardviewer/CardSoundConfig.kt | 9 ++--- .../com/ichi2/anki/cardviewer/SoundPlayer.kt | 3 +- .../CreateCustomStudySessionListener.kt | 2 +- .../java/com/ichi2/anki/model/CardsOrNotes.kt | 10 +++--- .../anki/previewer/PreviewerViewModel.kt | 8 ++--- .../com/ichi2/anki/reviewer/AnswerTimer.kt | 6 ++++ .../ichi2/anki/servicelayer/CardService.kt | 9 ++--- .../managespace/ManageSpaceFragment.kt | 8 ++--- .../com/ichi2/async/CollectionOperations.kt | 36 +++++++------------ .../src/main/java/com/ichi2/libanki/Card.kt | 10 ++++++ .../anki/cardviewer/CardSoundConfigTest.kt | 11 +++--- build.gradle | 2 +- 21 files changed, 98 insertions(+), 85 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index a1331f2619..36549d6b46 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -432,7 +432,7 @@ abstract class AbstractFlashcardViewer : val card = editorCard!! withProgress { undoableOp { - updateNote(card.note(this)) + updateNote(card.note()) } } onCardUpdated(card) @@ -477,7 +477,7 @@ abstract class AbstractFlashcardViewer : // despite that making no sense outside of Reviewer.kt currentCard = withCol { sched.card?.apply { - renderOutput(this@withCol) + renderOutput() } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt index 44ec5a11f6..ef6909ee7b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt @@ -262,15 +262,15 @@ open class BackupManager { * Run the sqlite3 command-line-tool (if it exists) on the collection to dump to a text file * and reload as a new database. Recently this command line tool isn't available on many devices * - * @param col Collection * @return whether the repair was successful */ - fun repairCollection(col: Collection): Boolean { - val colPath = col.path + context (Collection) + fun repairCollection(): Boolean { + val colPath = this@Collection.path val colFile = File(colPath) val time = TimeManager.time Timber.i("BackupManager - RepairCollection - Closing Collection") - col.close() + this@Collection.close() // repair file val execString = "sqlite3 $colPath .dump | sqlite3 $colPath.tmp" @@ -457,9 +457,10 @@ open class BackupManager { * * @return Whether all specified backups were successfully deleted. */ + context (Collection) @Throws(IllegalArgumentException::class) - fun deleteBackups(collection: Collection, backupsToDelete: List): Boolean { - val allBackups = getBackups(File(collection.path)) + fun deleteBackups(backupsToDelete: List): Boolean { + val allBackups = getBackups(File(this@Collection.path)) val invalidBackupsToDelete = backupsToDelete.toSet() - allBackups.toSet() if (invalidBackupsToDelete.isNotEmpty()) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index f88321e22e..6840311d2a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -89,6 +89,7 @@ import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest import com.ichi2.async.* import com.ichi2.libanki.* +import com.ichi2.libanki.Collection import com.ichi2.ui.CardBrowserSearchView import com.ichi2.ui.FixedTextView import com.ichi2.utils.* @@ -515,7 +516,7 @@ open class CardBrowser : } // Finish initializing the activity after the collection has been correctly loaded - override fun onCollectionLoaded(col: com.ichi2.libanki.Collection) { + override fun onCollectionLoaded(col: Collection) { super.onCollectionLoaded(col) Timber.d("onCollectionLoaded()") registerExternalStorageListener() @@ -1523,7 +1524,7 @@ open class CardBrowser : private suspend fun editSelectedCardsTags(selectedTags: List, indeterminateTags: List) = withProgress { undoableOp { val selectedNotes = selectedCardIds - .map { cardId -> getCard(cardId).note(this) } + .map { cardId -> getCard(cardId).note() } .distinct() .onEach { note -> val previousTags: List = note.tags @@ -1563,7 +1564,7 @@ open class CardBrowser : val card = cardBrowserCard!! withProgress { undoableOp { - updateNote(card.note(this)) + updateNote(card.note()) } } updateCardInList(card) @@ -1573,7 +1574,7 @@ open class CardBrowser : * Removes cards from view. Doesn't delete them in model (database). * @param reorderCards Whether to rearrange the positions of checked items (DEFECT: Currently deselects all) */ - private fun removeNotesView(cardsIds: Collection, reorderCards: Boolean) { + private fun removeNotesView(cardsIds: List, reorderCards: Boolean) { val idToPos = viewModel.cardIdToPositionMap val idToRemove = cardsIds.filter { cId -> idToPos.containsKey(cId) } mReloadRequired = mReloadRequired || cardsIds.contains(reviewerCardId) @@ -1940,7 +1941,7 @@ open class CardBrowser : override var position: Int private val inCardMode: Boolean - constructor(id: Long, col: com.ichi2.libanki.Collection, position: Int, cardsOrNotes: CardsOrNotes) : super(col, id) { + constructor(id: Long, col: Collection, position: Int, cardsOrNotes: CardsOrNotes) : super(col, id) { this.position = position this.inCardMode = cardsOrNotes == CARDS } @@ -2259,13 +2260,14 @@ suspend fun searchForCards( ): MutableList { return withCol { (if (cardsOrNotes == CARDS) findCards(query, order) else findOneCardByNote(query, order)).asSequence() - .toCardCache(this, cardsOrNotes) + .toCardCache(cardsOrNotes) .toMutableList() } } -private fun Sequence.toCardCache(col: com.ichi2.libanki.Collection, isInCardMode: CardsOrNotes): Sequence { - return this.mapIndexed { idx, cid -> CardBrowser.CardCache(cid, col, idx, isInCardMode) } +context (Collection) +private fun Sequence.toCardCache(isInCardMode: CardsOrNotes): Sequence { + return this.mapIndexed { idx, cid -> CardBrowser.CardCache(cid, this@Collection, idx, isInCardMode) } } class Previewer2Destination(val currentIndex: Int, val selectedCardIds: LongArray) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt index 3a7c7efa7c..a389defcc3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt @@ -547,9 +547,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } launchCatchingTask(resources.getString(R.string.card_template_editor_save_error)) { requireActivity().withProgress(resources.getString(R.string.saving_model)) { - withCol { - tempModel!!.saveToDatabase(this) - } + withCol { tempModel!!.saveToDatabase() } } onModelSaved() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt index 4173fbaf3e..0f959799be 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt @@ -22,6 +22,7 @@ import androidx.core.os.bundleOf import com.ichi2.async.saveModel import com.ichi2.compat.CompatHelper.Companion.compat import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat +import com.ichi2.libanki.Collection import com.ichi2.libanki.NoteTypeId import com.ichi2.libanki.NotetypeJson import com.ichi2.utils.KotlinCleanup @@ -87,11 +88,12 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { addTemplateChange(ChangeType.DELETE, ord) } - fun saveToDatabase(collection: com.ichi2.libanki.Collection) { + context(Collection) + fun saveToDatabase() { Timber.d("saveToDatabase() called") dumpChanges() clearTempModelFiles() - return saveModel(collection, notetype, adjustedTemplateChanges) + return saveModel(notetype, adjustedTemplateChanges) } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 1965479fd9..f595a89484 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -1559,7 +1559,7 @@ open class DeckPicker : withCol { Timber.i("RepairCollection: Closing collection") close() - BackupManager.repairCollection(this) + BackupManager.repairCollection() } } if (!result) { @@ -1618,7 +1618,7 @@ open class DeckPicker : launchCatchingTask { // Number of deleted files val noOfDeletedFiles = withProgress(resources.getString(R.string.delete_media_message)) { - withCol { deleteMedia(this, unused) } + withCol { deleteMedia(unused) } } showSimpleMessageDialog( title = resources.getString(R.string.delete_media_result_title), @@ -2127,7 +2127,7 @@ open class DeckPicker : Timber.d("rebuildFiltered: doInBackground - RebuildCram") decks.select(did) sched.rebuildDyn(decks.selected()) - updateValuesFromDeck(this) + updateValuesFromDeck() } } updateDeckList() @@ -2142,7 +2142,7 @@ open class DeckPicker : withCol { Timber.d("doInBackgroundEmptyCram") sched.emptyDyn(decks.selected()) - updateValuesFromDeck(this) + updateValuesFromDeck() } } updateDeckList() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 18a109d89e..1a916f1f65 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -202,7 +202,7 @@ open class Reviewer : override fun onResume() { when { stopTimerOnAnswer && isDisplayingAnswer -> {} - else -> launchCatchingTask { withCol { answerTimer.resume(this) } } + else -> launchCatchingTask { withCol { answerTimer.resume() } } } super.onResume() if (typeAnswer?.autoFocusEditText() == true) { @@ -1015,7 +1015,7 @@ open class Reviewer : override suspend fun updateCurrentCard() { val state = withCol { sched.currentQueueState()?.apply { - topCard.renderOutput(this@withCol, true) + topCard.renderOutput(true) } } state?.timeboxReached?.let { dealWithTimeBox(it) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt index 07f8d0bcc3..ce0ab3dac4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt @@ -283,7 +283,7 @@ class StudyOptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { withCol { Timber.d("doInBackground - RebuildCram") sched.rebuildDyn(decks.selected()) - updateValuesFromDeck(this) + updateValuesFromDeck() } } rebuildUi(result, true) @@ -295,7 +295,7 @@ class StudyOptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { withCol { Timber.d("doInBackgroundEmptyCram") sched.emptyDyn(decks.selected()) - updateValuesFromDeck(this) + updateValuesFromDeck() } } rebuildUi(result, true) @@ -446,7 +446,7 @@ class StudyOptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { // Load the deck counts for the deck from Collection asynchronously updateValuesFromDeckJob = launchCatchingTask { if (CollectionManager.isOpenUnsafe()) { - val result = withCol { updateValuesFromDeck(this) } + val result = withCol { updateValuesFromDeck() } rebuildUi(result, resetDecklist) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt index 8acdf2249b..581ac06317 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt @@ -209,7 +209,7 @@ class CardBrowserViewModel( viewModelScope.launch { // PERF: slightly inefficient if the source was lastDeckId setDeckId(getInitialDeck()) - val cardsOrNotes = withCol { CardsOrNotes.fromCollection(this) } + val cardsOrNotes = withCol { CardsOrNotes.fromCollection() } cardsOrNotesFlow.update { cardsOrNotes } withCol { @@ -260,7 +260,7 @@ class CardBrowserViewModel( fun setCardsOrNotes(newValue: CardsOrNotes) = viewModelScope.launch { withCol { // Change this to only change the preference on a state change - newValue.saveToCollection(this) + newValue.saveToCollection() } cardsOrNotesFlow.update { newValue } } @@ -363,7 +363,7 @@ class CardBrowserViewModel( CARDS -> Pair(ExportDialogFragment.ExportType.Cards, selectedCardIds) NOTES -> Pair( ExportDialogFragment.ExportType.Notes, - withCol { CardService.selectedNoteIds(selectedCardIds, this) } + withCol { CardService.selectedNoteIds(selectedCardIds) } ) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardSoundConfig.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardSoundConfig.kt index 27a8e34db8..c00f005d42 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardSoundConfig.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardSoundConfig.kt @@ -18,7 +18,6 @@ package com.ichi2.anki.cardviewer import androidx.annotation.CheckResult import com.ichi2.anki.CardUtils -import com.ichi2.anki.CollectionManager.withCol import com.ichi2.libanki.Card import com.ichi2.libanki.Collection import com.ichi2.libanki.DeckId @@ -38,10 +37,11 @@ class CardSoundConfig(val replayQuestion: Boolean, val autoplay: Boolean, val de fun appliesTo(card: Card): Boolean = CardUtils.getDeckIdForCard(card) == deckId companion object { + context(Collection) @CheckResult - fun create(collection: Collection, card: Card): CardSoundConfig { + fun create(card: Card): CardSoundConfig { Timber.v("start loading SoundConfig") - val deckConfig = collection.decks.confForDid(CardUtils.getDeckIdForCard(card)) + val deckConfig = this@Collection.decks.confForDid(CardUtils.getDeckIdForCard(card)) val autoPlay = deckConfig.optBoolean("autoplay", false) @@ -51,8 +51,5 @@ class CardSoundConfig(val replayQuestion: Boolean, val autoplay: Boolean, val de Timber.d("loaded SoundConfig: %s", this) } } - - @CheckResult - suspend fun create(card: Card): CardSoundConfig = withCol { create(this, card) } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundPlayer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundPlayer.kt index 4112c8521f..7c0fb621e8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundPlayer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundPlayer.kt @@ -30,6 +30,7 @@ import com.ichi2.anki.AbstractFlashcardViewer import com.ichi2.anki.AndroidTtsError import com.ichi2.anki.AndroidTtsError.TtsErrorCode import com.ichi2.anki.AndroidTtsPlayer +import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.cardviewer.SoundErrorBehavior.CONTINUE_AUDIO import com.ichi2.anki.cardviewer.SoundErrorBehavior.RETRY_AUDIO import com.ichi2.anki.cardviewer.SoundErrorBehavior.STOP_AUDIO @@ -122,7 +123,7 @@ class SoundPlayer( this.side = side if (!this::config.isInitialized || !config.appliesTo(card)) { - config = CardSoundConfig.create(card) + config = withCol { CardSoundConfig.create(card) } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CreateCustomStudySessionListener.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CreateCustomStudySessionListener.kt index 3432671a08..67ffc45525 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CreateCustomStudySessionListener.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CreateCustomStudySessionListener.kt @@ -42,7 +42,7 @@ suspend fun rebuildCram(listener: CreateCustomStudySessionListener) { CollectionManager.withCol { Timber.d("doInBackground - rebuildCram()") sched.rebuildDyn(decks.selected()) - updateValuesFromDeck(this) + updateValuesFromDeck() } listener.onPostExecute() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/model/CardsOrNotes.kt b/AnkiDroid/src/main/java/com/ichi2/anki/model/CardsOrNotes.kt index 97f1050c3a..34e52b85ad 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/model/CardsOrNotes.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/model/CardsOrNotes.kt @@ -28,13 +28,15 @@ enum class CardsOrNotes { CARDS, NOTES; - fun saveToCollection(collection: Collection) { - collection.config.setBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, this == NOTES) + context (Collection) + fun saveToCollection() { + this@Collection.config.setBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, this == NOTES) } companion object { - fun fromCollection(col: Collection): CardsOrNotes = - when (col.config.getBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE)) { + context (Collection) + fun fromCollection(): CardsOrNotes = + when (this@Collection.config.getBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE)) { true -> NOTES false -> CARDS } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt index 14088d0c83..a81ab7d7c9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt @@ -31,6 +31,7 @@ import com.ichi2.anki.servicelayer.MARKED_TAG import com.ichi2.anki.servicelayer.NoteService import com.ichi2.libanki.Card import com.ichi2.libanki.Sound.addPlayButtons +import com.ichi2.libanki.note import com.ichi2.themes.Themes import com.ichi2.utils.toRGBHex import kotlinx.coroutines.flow.MutableSharedFlow @@ -90,8 +91,7 @@ class PreviewerViewModel(private val selectedCardIds: LongArray, firstIndex: Int fun toggleMark() { launchCatching { - // TODO: Consider a context receiver - val note = withCol { currentCard.note(this) } + val note = withCol { currentCard.note() } NoteService.toggleMark(note) isMarked.emit(NoteService.isMarked(note)) } @@ -132,7 +132,7 @@ class PreviewerViewModel(private val selectedCardIds: LongArray, firstIndex: Int } private suspend fun updateMarkIcon() { - val note = withCol { currentCard.note(this) } + val note = withCol { currentCard.note() } isMarked.emit(note.hasTag(MARKED_TAG)) } @@ -328,7 +328,7 @@ class PreviewerViewModel(private val selectedCardIds: LongArray, firstIndex: Int private suspend fun getExpectedTypeInAnswer(card: Card, field: JSONObject): String? { val fieldName = field.getString("name") - val expected = withCol { card.note(this).getItem(fieldName) } + val expected = withCol { card.note().getItem(fieldName) } return if (fieldName.startsWith("cloze:")) { val clozeIdx = card.ord + 1 withCol { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/AnswerTimer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/AnswerTimer.kt index 75a08b05fd..f0b3aa2216 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/AnswerTimer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/AnswerTimer.kt @@ -135,3 +135,9 @@ class AnswerTimer(private val cardTimer: Chronometer) { private val elapsedRealTime get() = SystemClock.elapsedRealtime() } + +/** @see AnswerTimer.resume */ +context (Collection) +fun AnswerTimer.resume() { + this@AnswerTimer.resume(this@Collection) +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/CardService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/CardService.kt index 576a205761..0e74f2c1f9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/CardService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/CardService.kt @@ -17,6 +17,7 @@ package com.ichi2.anki.servicelayer import com.ichi2.anki.CardUtils +import com.ichi2.libanki.CardId import com.ichi2.libanki.Collection object CardService { @@ -24,11 +25,11 @@ object CardService { * get unique note ids from a list of card ids * @param selectedCardIds list of card ids * can do better with performance here - * TODO: blocks the UI, should be fixed */ - fun selectedNoteIds(selectedCardIds: List, col: Collection) = + context (Collection) + fun selectedNoteIds(selectedCardIds: List) = CardUtils.getNotes( - col, - selectedCardIds.map { col.getCard(it) } + this@Collection, + selectedCardIds.map { this@Collection.getCard(it) } ).map { it.id } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/managespace/ManageSpaceFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/managespace/ManageSpaceFragment.kt index 27fac0cd28..268f971186 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/managespace/ManageSpaceFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/managespace/ManageSpaceFragment.kt @@ -84,9 +84,9 @@ class ManageSpaceViewModel(val app: Application) : AndroidViewModel(app), Collec } } - suspend fun deleteMedia(filesNamesToDelete: List) { + suspend fun deleteMediaFiles(filesNamesToDelete: List) { try { - withCol { deleteMedia(this, filesNamesToDelete) } + withCol { deleteMedia(filesNamesToDelete) } } finally { launchCalculationOfSizeOfEverything() launchCalculationOfCollectionSize() @@ -108,7 +108,7 @@ class ManageSpaceViewModel(val app: Application) : AndroidViewModel(app), Collec suspend fun deleteBackups(backupsToDelete: List) { try { - withCol { BackupManager.deleteBackups(this, backupsToDelete) } + withCol { BackupManager.deleteBackups(backupsToDelete) } } finally { launchCalculationOfBackupsSize() launchCalculationOfCollectionSize() @@ -225,7 +225,7 @@ class ManageSpaceFragment : SettingsFragment() { val filesNamesToDelete = unusedFileNames.filterIndexed { index, _ -> checkedItems[index] } withProgress(R.string.delete_media_message) { - viewModel.deleteMedia(filesNamesToDelete) + viewModel.deleteMediaFiles(filesNamesToDelete) } } } else { diff --git a/AnkiDroid/src/main/java/com/ichi2/async/CollectionOperations.kt b/AnkiDroid/src/main/java/com/ichi2/async/CollectionOperations.kt index 6b3b683b40..e6309156e3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/async/CollectionOperations.kt +++ b/AnkiDroid/src/main/java/com/ichi2/async/CollectionOperations.kt @@ -27,33 +27,23 @@ import timber.log.Timber import java.util.* /** - * This file contains functions that have been migrated from [CollectionTask] - * Remove this comment when migration has been completed - * TODO: All functions associated to Collection can be converted to extension function to avoid redundant parameter [col] in each. - */ - -/** - * Takes a list of media file names and removes them from the Collection - * @param col Collection from which media is to be deleted + * Takes a list of media file names and removes them from the [Collection] * @param unused List of media names to be deleted */ -fun deleteMedia( - col: Collection, - unused: List -): Int { +context (Collection) +fun deleteMedia(unused: List): Int { // FIXME: this provides progress info that is not currently used - col.media.removeFiles(unused) + this@Collection.media.removeFiles(unused) return unused.size } // TODO: Once [com.ichi2.async.CollectionTask.RebuildCram] and [com.ichi2.async.CollectionTask.EmptyCram] // are migrated to Coroutines, move this function to [com.ichi2.anki.StudyOptionsFragment] -fun updateValuesFromDeck( - col: Collection -): StudyOptionsFragment.DeckStudyData? { +context (Collection) +fun updateValuesFromDeck(): StudyOptionsFragment.DeckStudyData? { Timber.d("doInBackgroundUpdateValuesFromDeck") return try { - val sched = col.sched + val sched = this@Collection.sched val counts = sched.counts() val totalNewCount = sched.totalNewForCurrentDeck() val totalCount = sched.cardCount() @@ -125,13 +115,13 @@ suspend fun renderBrowserQA( * Handles everything for a model change at once - template add / deletes as well as content updates * @return Pair : (true, null) when success, (false, exceptionMessage) when failure */ +context (Collection) fun saveModel( - col: Collection, notetype: NotetypeJson, templateChanges: ArrayList> ) { Timber.d("doInBackgroundSaveModel") - val oldModel = col.notetypes.get(notetype.getLong("id")) + val oldModel = this@Collection.notetypes.get(notetype.getLong("id")) // TODO: make undoable val newTemplates = notetype.getJSONArray("tmpls") @@ -140,11 +130,11 @@ fun saveModel( when (change[1] as CardTemplateNotetype.ChangeType) { CardTemplateNotetype.ChangeType.ADD -> { Timber.d("doInBackgroundSaveModel() adding template %s", change[0]) - col.notetypes.addTemplate(oldModel, newTemplates.getJSONObject(change[0] as Int)) + this@Collection.notetypes.addTemplate(oldModel, newTemplates.getJSONObject(change[0] as Int)) } CardTemplateNotetype.ChangeType.DELETE -> { Timber.d("doInBackgroundSaveModel() deleting template currently at ordinal %s", change[0]) - col.notetypes.remTemplate(oldModel, oldTemplates.getJSONObject(change[0] as Int)) + this@Collection.notetypes.remTemplate(oldModel, oldTemplates.getJSONObject(change[0] as Int)) } } } @@ -152,6 +142,6 @@ fun saveModel( // required for Rust: the modified time can't go backwards, and we updated the model by adding fields // This could be done better notetype.put("mod", oldModel!!.getLong("mod")) - col.notetypes.save(notetype) - col.notetypes.update(notetype) + this@Collection.notetypes.save(notetype) + this@Collection.notetypes.update(notetype) } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt index 1c1a79d1bf..febada2247 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt @@ -531,3 +531,13 @@ open class Card : Cloneable { } } } + +/** @see Card.renderOutput */ +context (Collection) +fun Card.renderOutput(reload: Boolean = false, browser: Boolean = false) = + this@Card.renderOutput(this@Collection, reload, browser) + +/** @see Card.note */ +context (Collection) +fun Card.note() = + this@Card.note(this@Collection) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardSoundConfigTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardSoundConfigTest.kt index d3d8790cb1..d3f73d5742 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardSoundConfigTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardSoundConfigTest.kt @@ -17,8 +17,9 @@ package com.ichi2.anki.cardviewer import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.libanki.Card import com.ichi2.testutils.JvmTest -import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.Ignore @@ -34,7 +35,7 @@ class CardSoundConfigTest : JvmTest() { // defaults as-of Anki Desktop 23.10 (51a10f09) val note = addNoteUsingBasicModel() val card = note.firstCard() - CardSoundConfig.create(card).run { + createCardSoundConfig(card).run { assertThat("deckId", deckId, equalTo(card.did)) // Anki Desktop: "Skip question when replaying answer" -> false // our variable is reversed, so true @@ -49,7 +50,7 @@ class CardSoundConfigTest : JvmTest() { fun `cards from the same note are equal`() = runTest { val note = addNoteUsingBasicAndReversedModel() val (card1, card2) = note.cards() - CardSoundConfig.create(card1).run { + createCardSoundConfig(card1).run { assertThat("same note", this.appliesTo(card2)) } } @@ -57,7 +58,7 @@ class CardSoundConfigTest : JvmTest() { @Test fun `cards from the same deck are equal`() = runTest { val (note1, note2) = addNotes(count = 2) - CardSoundConfig.create(note1.firstCard()).run { + createCardSoundConfig(note1.firstCard()).run { assertThat("same note", this.appliesTo(note2.firstCard())) } } @@ -66,4 +67,6 @@ class CardSoundConfigTest : JvmTest() { @Test fun `cards with the same deck options are equal`() { } + + private suspend fun createCardSoundConfig(card: Card) = withCol { CardSoundConfig.create(card) } } diff --git a/build.gradle b/build.gradle index ee572ca89f..fbee5a90fe 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ subprojects { tasks.withType(KotlinCompile).configureEach { compilerOptions { allWarningsAsErrors = fatalWarnings - def compilerArgs = ['-Xjvm-default=all'] + def compilerArgs = ['-Xjvm-default=all', '-Xcontext-receivers'] if (project.name != "api") { compilerArgs += ['-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi'] }