0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-19 19:42:17 +02:00

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<CardId>.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'
This commit is contained in:
David Allison 2024-01-22 00:51:12 +00:00 committed by GitHub
parent da41ac1cb1
commit 672b075a54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 98 additions and 85 deletions

View File

@ -432,7 +432,7 @@ abstract class AbstractFlashcardViewer :
val card = editorCard!! val card = editorCard!!
withProgress { withProgress {
undoableOp { undoableOp {
updateNote(card.note(this)) updateNote(card.note())
} }
} }
onCardUpdated(card) onCardUpdated(card)
@ -477,7 +477,7 @@ abstract class AbstractFlashcardViewer :
// despite that making no sense outside of Reviewer.kt // despite that making no sense outside of Reviewer.kt
currentCard = withCol { currentCard = withCol {
sched.card?.apply { sched.card?.apply {
renderOutput(this@withCol) renderOutput()
} }
} }
} }

View File

@ -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 * 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 * 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 * @return whether the repair was successful
*/ */
fun repairCollection(col: Collection): Boolean { context (Collection)
val colPath = col.path fun repairCollection(): Boolean {
val colPath = this@Collection.path
val colFile = File(colPath) val colFile = File(colPath)
val time = TimeManager.time val time = TimeManager.time
Timber.i("BackupManager - RepairCollection - Closing Collection") Timber.i("BackupManager - RepairCollection - Closing Collection")
col.close() this@Collection.close()
// repair file // repair file
val execString = "sqlite3 $colPath .dump | sqlite3 $colPath.tmp" val execString = "sqlite3 $colPath .dump | sqlite3 $colPath.tmp"
@ -457,9 +457,10 @@ open class BackupManager {
* *
* @return Whether all specified backups were successfully deleted. * @return Whether all specified backups were successfully deleted.
*/ */
context (Collection)
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun deleteBackups(collection: Collection, backupsToDelete: List<File>): Boolean { fun deleteBackups(backupsToDelete: List<File>): Boolean {
val allBackups = getBackups(File(collection.path)) val allBackups = getBackups(File(this@Collection.path))
val invalidBackupsToDelete = backupsToDelete.toSet() - allBackups.toSet() val invalidBackupsToDelete = backupsToDelete.toSet() - allBackups.toSet()
if (invalidBackupsToDelete.isNotEmpty()) { if (invalidBackupsToDelete.isNotEmpty()) {

View File

@ -89,6 +89,7 @@ import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener
import com.ichi2.annotations.NeedsTest import com.ichi2.annotations.NeedsTest
import com.ichi2.async.* import com.ichi2.async.*
import com.ichi2.libanki.* import com.ichi2.libanki.*
import com.ichi2.libanki.Collection
import com.ichi2.ui.CardBrowserSearchView import com.ichi2.ui.CardBrowserSearchView
import com.ichi2.ui.FixedTextView import com.ichi2.ui.FixedTextView
import com.ichi2.utils.* import com.ichi2.utils.*
@ -515,7 +516,7 @@ open class CardBrowser :
} }
// Finish initializing the activity after the collection has been correctly loaded // 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) super.onCollectionLoaded(col)
Timber.d("onCollectionLoaded()") Timber.d("onCollectionLoaded()")
registerExternalStorageListener() registerExternalStorageListener()
@ -1523,7 +1524,7 @@ open class CardBrowser :
private suspend fun editSelectedCardsTags(selectedTags: List<String>, indeterminateTags: List<String>) = withProgress { private suspend fun editSelectedCardsTags(selectedTags: List<String>, indeterminateTags: List<String>) = withProgress {
undoableOp { undoableOp {
val selectedNotes = selectedCardIds val selectedNotes = selectedCardIds
.map { cardId -> getCard(cardId).note(this) } .map { cardId -> getCard(cardId).note() }
.distinct() .distinct()
.onEach { note -> .onEach { note ->
val previousTags: List<String> = note.tags val previousTags: List<String> = note.tags
@ -1563,7 +1564,7 @@ open class CardBrowser :
val card = cardBrowserCard!! val card = cardBrowserCard!!
withProgress { withProgress {
undoableOp { undoableOp {
updateNote(card.note(this)) updateNote(card.note())
} }
} }
updateCardInList(card) updateCardInList(card)
@ -1573,7 +1574,7 @@ open class CardBrowser :
* Removes cards from view. Doesn't delete them in model (database). * 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) * @param reorderCards Whether to rearrange the positions of checked items (DEFECT: Currently deselects all)
*/ */
private fun removeNotesView(cardsIds: Collection<Long>, reorderCards: Boolean) { private fun removeNotesView(cardsIds: List<Long>, reorderCards: Boolean) {
val idToPos = viewModel.cardIdToPositionMap val idToPos = viewModel.cardIdToPositionMap
val idToRemove = cardsIds.filter { cId -> idToPos.containsKey(cId) } val idToRemove = cardsIds.filter { cId -> idToPos.containsKey(cId) }
mReloadRequired = mReloadRequired || cardsIds.contains(reviewerCardId) mReloadRequired = mReloadRequired || cardsIds.contains(reviewerCardId)
@ -1940,7 +1941,7 @@ open class CardBrowser :
override var position: Int override var position: Int
private val inCardMode: Boolean 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.position = position
this.inCardMode = cardsOrNotes == CARDS this.inCardMode = cardsOrNotes == CARDS
} }
@ -2259,13 +2260,14 @@ suspend fun searchForCards(
): MutableList<CardBrowser.CardCache> { ): MutableList<CardBrowser.CardCache> {
return withCol { return withCol {
(if (cardsOrNotes == CARDS) findCards(query, order) else findOneCardByNote(query, order)).asSequence() (if (cardsOrNotes == CARDS) findCards(query, order) else findOneCardByNote(query, order)).asSequence()
.toCardCache(this, cardsOrNotes) .toCardCache(cardsOrNotes)
.toMutableList() .toMutableList()
} }
} }
private fun Sequence<CardId>.toCardCache(col: com.ichi2.libanki.Collection, isInCardMode: CardsOrNotes): Sequence<CardBrowser.CardCache> { context (Collection)
return this.mapIndexed { idx, cid -> CardBrowser.CardCache(cid, col, idx, isInCardMode) } private fun Sequence<CardId>.toCardCache(isInCardMode: CardsOrNotes): Sequence<CardBrowser.CardCache> {
return this.mapIndexed { idx, cid -> CardBrowser.CardCache(cid, this@Collection, idx, isInCardMode) }
} }
class Previewer2Destination(val currentIndex: Int, val selectedCardIds: LongArray) class Previewer2Destination(val currentIndex: Int, val selectedCardIds: LongArray)

View File

@ -547,9 +547,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener {
} }
launchCatchingTask(resources.getString(R.string.card_template_editor_save_error)) { launchCatchingTask(resources.getString(R.string.card_template_editor_save_error)) {
requireActivity().withProgress(resources.getString(R.string.saving_model)) { requireActivity().withProgress(resources.getString(R.string.saving_model)) {
withCol { withCol { tempModel!!.saveToDatabase() }
tempModel!!.saveToDatabase(this)
}
} }
onModelSaved() onModelSaved()
} }

View File

@ -22,6 +22,7 @@ import androidx.core.os.bundleOf
import com.ichi2.async.saveModel import com.ichi2.async.saveModel
import com.ichi2.compat.CompatHelper.Companion.compat import com.ichi2.compat.CompatHelper.Companion.compat
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
import com.ichi2.libanki.Collection
import com.ichi2.libanki.NoteTypeId import com.ichi2.libanki.NoteTypeId
import com.ichi2.libanki.NotetypeJson import com.ichi2.libanki.NotetypeJson
import com.ichi2.utils.KotlinCleanup import com.ichi2.utils.KotlinCleanup
@ -87,11 +88,12 @@ class CardTemplateNotetype(val notetype: NotetypeJson) {
addTemplateChange(ChangeType.DELETE, ord) addTemplateChange(ChangeType.DELETE, ord)
} }
fun saveToDatabase(collection: com.ichi2.libanki.Collection) { context(Collection)
fun saveToDatabase() {
Timber.d("saveToDatabase() called") Timber.d("saveToDatabase() called")
dumpChanges() dumpChanges()
clearTempModelFiles() clearTempModelFiles()
return saveModel(collection, notetype, adjustedTemplateChanges) return saveModel(notetype, adjustedTemplateChanges)
} }
/** /**

View File

@ -1559,7 +1559,7 @@ open class DeckPicker :
withCol { withCol {
Timber.i("RepairCollection: Closing collection") Timber.i("RepairCollection: Closing collection")
close() close()
BackupManager.repairCollection(this) BackupManager.repairCollection()
} }
} }
if (!result) { if (!result) {
@ -1618,7 +1618,7 @@ open class DeckPicker :
launchCatchingTask { launchCatchingTask {
// Number of deleted files // Number of deleted files
val noOfDeletedFiles = withProgress(resources.getString(R.string.delete_media_message)) { val noOfDeletedFiles = withProgress(resources.getString(R.string.delete_media_message)) {
withCol { deleteMedia(this, unused) } withCol { deleteMedia(unused) }
} }
showSimpleMessageDialog( showSimpleMessageDialog(
title = resources.getString(R.string.delete_media_result_title), title = resources.getString(R.string.delete_media_result_title),
@ -2127,7 +2127,7 @@ open class DeckPicker :
Timber.d("rebuildFiltered: doInBackground - RebuildCram") Timber.d("rebuildFiltered: doInBackground - RebuildCram")
decks.select(did) decks.select(did)
sched.rebuildDyn(decks.selected()) sched.rebuildDyn(decks.selected())
updateValuesFromDeck(this) updateValuesFromDeck()
} }
} }
updateDeckList() updateDeckList()
@ -2142,7 +2142,7 @@ open class DeckPicker :
withCol { withCol {
Timber.d("doInBackgroundEmptyCram") Timber.d("doInBackgroundEmptyCram")
sched.emptyDyn(decks.selected()) sched.emptyDyn(decks.selected())
updateValuesFromDeck(this) updateValuesFromDeck()
} }
} }
updateDeckList() updateDeckList()

View File

@ -202,7 +202,7 @@ open class Reviewer :
override fun onResume() { override fun onResume() {
when { when {
stopTimerOnAnswer && isDisplayingAnswer -> {} stopTimerOnAnswer && isDisplayingAnswer -> {}
else -> launchCatchingTask { withCol { answerTimer.resume(this) } } else -> launchCatchingTask { withCol { answerTimer.resume() } }
} }
super.onResume() super.onResume()
if (typeAnswer?.autoFocusEditText() == true) { if (typeAnswer?.autoFocusEditText() == true) {
@ -1015,7 +1015,7 @@ open class Reviewer :
override suspend fun updateCurrentCard() { override suspend fun updateCurrentCard() {
val state = withCol { val state = withCol {
sched.currentQueueState()?.apply { sched.currentQueueState()?.apply {
topCard.renderOutput(this@withCol, true) topCard.renderOutput(true)
} }
} }
state?.timeboxReached?.let { dealWithTimeBox(it) } state?.timeboxReached?.let { dealWithTimeBox(it) }

View File

@ -283,7 +283,7 @@ class StudyOptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
withCol { withCol {
Timber.d("doInBackground - RebuildCram") Timber.d("doInBackground - RebuildCram")
sched.rebuildDyn(decks.selected()) sched.rebuildDyn(decks.selected())
updateValuesFromDeck(this) updateValuesFromDeck()
} }
} }
rebuildUi(result, true) rebuildUi(result, true)
@ -295,7 +295,7 @@ class StudyOptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
withCol { withCol {
Timber.d("doInBackgroundEmptyCram") Timber.d("doInBackgroundEmptyCram")
sched.emptyDyn(decks.selected()) sched.emptyDyn(decks.selected())
updateValuesFromDeck(this) updateValuesFromDeck()
} }
} }
rebuildUi(result, true) rebuildUi(result, true)
@ -446,7 +446,7 @@ class StudyOptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
// Load the deck counts for the deck from Collection asynchronously // Load the deck counts for the deck from Collection asynchronously
updateValuesFromDeckJob = launchCatchingTask { updateValuesFromDeckJob = launchCatchingTask {
if (CollectionManager.isOpenUnsafe()) { if (CollectionManager.isOpenUnsafe()) {
val result = withCol { updateValuesFromDeck(this) } val result = withCol { updateValuesFromDeck() }
rebuildUi(result, resetDecklist) rebuildUi(result, resetDecklist)
} }
} }

View File

@ -209,7 +209,7 @@ class CardBrowserViewModel(
viewModelScope.launch { viewModelScope.launch {
// PERF: slightly inefficient if the source was lastDeckId // PERF: slightly inefficient if the source was lastDeckId
setDeckId(getInitialDeck()) setDeckId(getInitialDeck())
val cardsOrNotes = withCol { CardsOrNotes.fromCollection(this) } val cardsOrNotes = withCol { CardsOrNotes.fromCollection() }
cardsOrNotesFlow.update { cardsOrNotes } cardsOrNotesFlow.update { cardsOrNotes }
withCol { withCol {
@ -260,7 +260,7 @@ class CardBrowserViewModel(
fun setCardsOrNotes(newValue: CardsOrNotes) = viewModelScope.launch { fun setCardsOrNotes(newValue: CardsOrNotes) = viewModelScope.launch {
withCol { withCol {
// Change this to only change the preference on a state change // Change this to only change the preference on a state change
newValue.saveToCollection(this) newValue.saveToCollection()
} }
cardsOrNotesFlow.update { newValue } cardsOrNotesFlow.update { newValue }
} }
@ -363,7 +363,7 @@ class CardBrowserViewModel(
CARDS -> Pair(ExportDialogFragment.ExportType.Cards, selectedCardIds) CARDS -> Pair(ExportDialogFragment.ExportType.Cards, selectedCardIds)
NOTES -> Pair( NOTES -> Pair(
ExportDialogFragment.ExportType.Notes, ExportDialogFragment.ExportType.Notes,
withCol { CardService.selectedNoteIds(selectedCardIds, this) } withCol { CardService.selectedNoteIds(selectedCardIds) }
) )
} }
} }

View File

@ -18,7 +18,6 @@ package com.ichi2.anki.cardviewer
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import com.ichi2.anki.CardUtils import com.ichi2.anki.CardUtils
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.libanki.Card import com.ichi2.libanki.Card
import com.ichi2.libanki.Collection import com.ichi2.libanki.Collection
import com.ichi2.libanki.DeckId 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 fun appliesTo(card: Card): Boolean = CardUtils.getDeckIdForCard(card) == deckId
companion object { companion object {
context(Collection)
@CheckResult @CheckResult
fun create(collection: Collection, card: Card): CardSoundConfig { fun create(card: Card): CardSoundConfig {
Timber.v("start loading SoundConfig") 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) 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) Timber.d("loaded SoundConfig: %s", this)
} }
} }
@CheckResult
suspend fun create(card: Card): CardSoundConfig = withCol { create(this, card) }
} }
} }

View File

@ -30,6 +30,7 @@ import com.ichi2.anki.AbstractFlashcardViewer
import com.ichi2.anki.AndroidTtsError import com.ichi2.anki.AndroidTtsError
import com.ichi2.anki.AndroidTtsError.TtsErrorCode import com.ichi2.anki.AndroidTtsError.TtsErrorCode
import com.ichi2.anki.AndroidTtsPlayer 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.CONTINUE_AUDIO
import com.ichi2.anki.cardviewer.SoundErrorBehavior.RETRY_AUDIO import com.ichi2.anki.cardviewer.SoundErrorBehavior.RETRY_AUDIO
import com.ichi2.anki.cardviewer.SoundErrorBehavior.STOP_AUDIO import com.ichi2.anki.cardviewer.SoundErrorBehavior.STOP_AUDIO
@ -122,7 +123,7 @@ class SoundPlayer(
this.side = side this.side = side
if (!this::config.isInitialized || !config.appliesTo(card)) { if (!this::config.isInitialized || !config.appliesTo(card)) {
config = CardSoundConfig.create(card) config = withCol { CardSoundConfig.create(card) }
} }
} }

View File

@ -42,7 +42,7 @@ suspend fun rebuildCram(listener: CreateCustomStudySessionListener) {
CollectionManager.withCol { CollectionManager.withCol {
Timber.d("doInBackground - rebuildCram()") Timber.d("doInBackground - rebuildCram()")
sched.rebuildDyn(decks.selected()) sched.rebuildDyn(decks.selected())
updateValuesFromDeck(this) updateValuesFromDeck()
} }
listener.onPostExecute() listener.onPostExecute()
} }

View File

@ -28,13 +28,15 @@ enum class CardsOrNotes {
CARDS, CARDS,
NOTES; NOTES;
fun saveToCollection(collection: Collection) { context (Collection)
collection.config.setBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, this == NOTES) fun saveToCollection() {
this@Collection.config.setBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, this == NOTES)
} }
companion object { companion object {
fun fromCollection(col: Collection): CardsOrNotes = context (Collection)
when (col.config.getBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE)) { fun fromCollection(): CardsOrNotes =
when (this@Collection.config.getBool(ConfigKey.Bool.BROWSER_TABLE_SHOW_NOTES_MODE)) {
true -> NOTES true -> NOTES
false -> CARDS false -> CARDS
} }

View File

@ -31,6 +31,7 @@ import com.ichi2.anki.servicelayer.MARKED_TAG
import com.ichi2.anki.servicelayer.NoteService import com.ichi2.anki.servicelayer.NoteService
import com.ichi2.libanki.Card import com.ichi2.libanki.Card
import com.ichi2.libanki.Sound.addPlayButtons import com.ichi2.libanki.Sound.addPlayButtons
import com.ichi2.libanki.note
import com.ichi2.themes.Themes import com.ichi2.themes.Themes
import com.ichi2.utils.toRGBHex import com.ichi2.utils.toRGBHex
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -90,8 +91,7 @@ class PreviewerViewModel(private val selectedCardIds: LongArray, firstIndex: Int
fun toggleMark() { fun toggleMark() {
launchCatching { launchCatching {
// TODO: Consider a context receiver val note = withCol { currentCard.note() }
val note = withCol { currentCard.note(this) }
NoteService.toggleMark(note) NoteService.toggleMark(note)
isMarked.emit(NoteService.isMarked(note)) isMarked.emit(NoteService.isMarked(note))
} }
@ -132,7 +132,7 @@ class PreviewerViewModel(private val selectedCardIds: LongArray, firstIndex: Int
} }
private suspend fun updateMarkIcon() { private suspend fun updateMarkIcon() {
val note = withCol { currentCard.note(this) } val note = withCol { currentCard.note() }
isMarked.emit(note.hasTag(MARKED_TAG)) 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? { private suspend fun getExpectedTypeInAnswer(card: Card, field: JSONObject): String? {
val fieldName = field.getString("name") 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:")) { return if (fieldName.startsWith("cloze:")) {
val clozeIdx = card.ord + 1 val clozeIdx = card.ord + 1
withCol { withCol {

View File

@ -135,3 +135,9 @@ class AnswerTimer(private val cardTimer: Chronometer) {
private val elapsedRealTime private val elapsedRealTime
get() = SystemClock.elapsedRealtime() get() = SystemClock.elapsedRealtime()
} }
/** @see AnswerTimer.resume */
context (Collection)
fun AnswerTimer.resume() {
this@AnswerTimer.resume(this@Collection)
}

View File

@ -17,6 +17,7 @@
package com.ichi2.anki.servicelayer package com.ichi2.anki.servicelayer
import com.ichi2.anki.CardUtils import com.ichi2.anki.CardUtils
import com.ichi2.libanki.CardId
import com.ichi2.libanki.Collection import com.ichi2.libanki.Collection
object CardService { object CardService {
@ -24,11 +25,11 @@ object CardService {
* get unique note ids from a list of card ids * get unique note ids from a list of card ids
* @param selectedCardIds list of card ids * @param selectedCardIds list of card ids
* can do better with performance here * can do better with performance here
* TODO: blocks the UI, should be fixed
*/ */
fun selectedNoteIds(selectedCardIds: List<Long>, col: Collection) = context (Collection)
fun selectedNoteIds(selectedCardIds: List<CardId>) =
CardUtils.getNotes( CardUtils.getNotes(
col, this@Collection,
selectedCardIds.map { col.getCard(it) } selectedCardIds.map { this@Collection.getCard(it) }
).map { it.id } ).map { it.id }
} }

View File

@ -84,9 +84,9 @@ class ManageSpaceViewModel(val app: Application) : AndroidViewModel(app), Collec
} }
} }
suspend fun deleteMedia(filesNamesToDelete: List<String>) { suspend fun deleteMediaFiles(filesNamesToDelete: List<String>) {
try { try {
withCol { deleteMedia(this, filesNamesToDelete) } withCol { deleteMedia(filesNamesToDelete) }
} finally { } finally {
launchCalculationOfSizeOfEverything() launchCalculationOfSizeOfEverything()
launchCalculationOfCollectionSize() launchCalculationOfCollectionSize()
@ -108,7 +108,7 @@ class ManageSpaceViewModel(val app: Application) : AndroidViewModel(app), Collec
suspend fun deleteBackups(backupsToDelete: List<File>) { suspend fun deleteBackups(backupsToDelete: List<File>) {
try { try {
withCol { BackupManager.deleteBackups(this, backupsToDelete) } withCol { BackupManager.deleteBackups(backupsToDelete) }
} finally { } finally {
launchCalculationOfBackupsSize() launchCalculationOfBackupsSize()
launchCalculationOfCollectionSize() launchCalculationOfCollectionSize()
@ -225,7 +225,7 @@ class ManageSpaceFragment : SettingsFragment() {
val filesNamesToDelete = unusedFileNames.filterIndexed { index, _ -> checkedItems[index] } val filesNamesToDelete = unusedFileNames.filterIndexed { index, _ -> checkedItems[index] }
withProgress(R.string.delete_media_message) { withProgress(R.string.delete_media_message) {
viewModel.deleteMedia(filesNamesToDelete) viewModel.deleteMediaFiles(filesNamesToDelete)
} }
} }
} else { } else {

View File

@ -27,33 +27,23 @@ import timber.log.Timber
import java.util.* import java.util.*
/** /**
* This file contains functions that have been migrated from [CollectionTask] * Takes a list of media file names and removes them from the [Collection]
* 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
* @param unused List of media names to be deleted * @param unused List of media names to be deleted
*/ */
fun deleteMedia( context (Collection)
col: Collection, fun deleteMedia(unused: List<String>): Int {
unused: List<String>
): Int {
// FIXME: this provides progress info that is not currently used // FIXME: this provides progress info that is not currently used
col.media.removeFiles(unused) this@Collection.media.removeFiles(unused)
return unused.size return unused.size
} }
// TODO: Once [com.ichi2.async.CollectionTask.RebuildCram] and [com.ichi2.async.CollectionTask.EmptyCram] // 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] // are migrated to Coroutines, move this function to [com.ichi2.anki.StudyOptionsFragment]
fun updateValuesFromDeck( context (Collection)
col: Collection fun updateValuesFromDeck(): StudyOptionsFragment.DeckStudyData? {
): StudyOptionsFragment.DeckStudyData? {
Timber.d("doInBackgroundUpdateValuesFromDeck") Timber.d("doInBackgroundUpdateValuesFromDeck")
return try { return try {
val sched = col.sched val sched = this@Collection.sched
val counts = sched.counts() val counts = sched.counts()
val totalNewCount = sched.totalNewForCurrentDeck() val totalNewCount = sched.totalNewForCurrentDeck()
val totalCount = sched.cardCount() 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 * Handles everything for a model change at once - template add / deletes as well as content updates
* @return Pair<Boolean, String> : (true, null) when success, (false, exceptionMessage) when failure * @return Pair<Boolean, String> : (true, null) when success, (false, exceptionMessage) when failure
*/ */
context (Collection)
fun saveModel( fun saveModel(
col: Collection,
notetype: NotetypeJson, notetype: NotetypeJson,
templateChanges: ArrayList<Array<Any>> templateChanges: ArrayList<Array<Any>>
) { ) {
Timber.d("doInBackgroundSaveModel") Timber.d("doInBackgroundSaveModel")
val oldModel = col.notetypes.get(notetype.getLong("id")) val oldModel = this@Collection.notetypes.get(notetype.getLong("id"))
// TODO: make undoable // TODO: make undoable
val newTemplates = notetype.getJSONArray("tmpls") val newTemplates = notetype.getJSONArray("tmpls")
@ -140,11 +130,11 @@ fun saveModel(
when (change[1] as CardTemplateNotetype.ChangeType) { when (change[1] as CardTemplateNotetype.ChangeType) {
CardTemplateNotetype.ChangeType.ADD -> { CardTemplateNotetype.ChangeType.ADD -> {
Timber.d("doInBackgroundSaveModel() adding template %s", change[0]) 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 -> { CardTemplateNotetype.ChangeType.DELETE -> {
Timber.d("doInBackgroundSaveModel() deleting template currently at ordinal %s", change[0]) 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 // required for Rust: the modified time can't go backwards, and we updated the model by adding fields
// This could be done better // This could be done better
notetype.put("mod", oldModel!!.getLong("mod")) notetype.put("mod", oldModel!!.getLong("mod"))
col.notetypes.save(notetype) this@Collection.notetypes.save(notetype)
col.notetypes.update(notetype) this@Collection.notetypes.update(notetype)
} }

View File

@ -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)

View File

@ -17,8 +17,9 @@
package com.ichi2.anki.cardviewer package com.ichi2.anki.cardviewer
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.libanki.Card
import com.ichi2.testutils.JvmTest import com.ichi2.testutils.JvmTest
import kotlinx.coroutines.test.runTest
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.equalTo
import org.junit.Ignore import org.junit.Ignore
@ -34,7 +35,7 @@ class CardSoundConfigTest : JvmTest() {
// defaults as-of Anki Desktop 23.10 (51a10f09) // defaults as-of Anki Desktop 23.10 (51a10f09)
val note = addNoteUsingBasicModel() val note = addNoteUsingBasicModel()
val card = note.firstCard() val card = note.firstCard()
CardSoundConfig.create(card).run { createCardSoundConfig(card).run {
assertThat("deckId", deckId, equalTo(card.did)) assertThat("deckId", deckId, equalTo(card.did))
// Anki Desktop: "Skip question when replaying answer" -> false // Anki Desktop: "Skip question when replaying answer" -> false
// our variable is reversed, so true // our variable is reversed, so true
@ -49,7 +50,7 @@ class CardSoundConfigTest : JvmTest() {
fun `cards from the same note are equal`() = runTest { fun `cards from the same note are equal`() = runTest {
val note = addNoteUsingBasicAndReversedModel() val note = addNoteUsingBasicAndReversedModel()
val (card1, card2) = note.cards() val (card1, card2) = note.cards()
CardSoundConfig.create(card1).run { createCardSoundConfig(card1).run {
assertThat("same note", this.appliesTo(card2)) assertThat("same note", this.appliesTo(card2))
} }
} }
@ -57,7 +58,7 @@ class CardSoundConfigTest : JvmTest() {
@Test @Test
fun `cards from the same deck are equal`() = runTest { fun `cards from the same deck are equal`() = runTest {
val (note1, note2) = addNotes(count = 2) val (note1, note2) = addNotes(count = 2)
CardSoundConfig.create(note1.firstCard()).run { createCardSoundConfig(note1.firstCard()).run {
assertThat("same note", this.appliesTo(note2.firstCard())) assertThat("same note", this.appliesTo(note2.firstCard()))
} }
} }
@ -66,4 +67,6 @@ class CardSoundConfigTest : JvmTest() {
@Test @Test
fun `cards with the same deck options are equal`() { fun `cards with the same deck options are equal`() {
} }
private suspend fun createCardSoundConfig(card: Card) = withCol { CardSoundConfig.create(card) }
} }

View File

@ -96,7 +96,7 @@ subprojects {
tasks.withType(KotlinCompile).configureEach { tasks.withType(KotlinCompile).configureEach {
compilerOptions { compilerOptions {
allWarningsAsErrors = fatalWarnings allWarningsAsErrors = fatalWarnings
def compilerArgs = ['-Xjvm-default=all'] def compilerArgs = ['-Xjvm-default=all', '-Xcontext-receivers']
if (project.name != "api") { if (project.name != "api") {
compilerArgs += ['-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi'] compilerArgs += ['-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi']
} }