diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt index 9293bf0cfc00..d8246f753734 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt @@ -88,14 +88,14 @@ fun PagesTest.getStatistics(context: Context): Intent { } fun PagesTest.getCardInfo(context: Context): Intent { - return addNoteUsingBasicModel().firstCard().let { card -> + return addNoteUsingBasicModel().firstCard(col).let { card -> this.card = card CardInfoDestination(card.id).toIntent(context) } } fun PagesTest.getCongratsPage(context: Context): Intent { - return addNoteUsingBasicModel().firstCard().let { card -> + return addNoteUsingBasicModel().firstCard(col).let { card -> this.card = card CardInfoDestination(card.id).toIntent(context) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt index a974c519afac..42d9ad37b88f 100755 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt @@ -63,7 +63,7 @@ class ReviewerTest : InstrumentedTest() { customData.good.c += 1; """ val note = addNoteUsingBasicModel("foo", "bar") - val card = note.firstCard() + val card = note.firstCard(col) val deck = col.decks.get(note.notetype.did)!! card.moveToReviewQueue() col.backend.updateCards( diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt index b4d04bd3aa33..4f94fc0ba25c 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -201,7 +201,7 @@ class ContentProviderTest : InstrumentedTest() { // Check that it looks as expected assertNotNull("check note URI path", newNoteUri!!.lastPathSegment) val addedNote = Note(col, newNoteUri.lastPathSegment!!.toLong()) - addedNote.load() + addedNote.load(col) assertEquals( "Check that fields were set correctly", addedNote.fields, @@ -211,11 +211,11 @@ class ContentProviderTest : InstrumentedTest() { val model: JSONObject? = col.notetypes.get(modelId) assertNotNull("Check model", model) val expectedNumCards = model!!.getJSONArray("tmpls").length() - assertEquals("Check that correct number of cards generated", expectedNumCards, addedNote.numberOfCards()) + assertEquals("Check that correct number of cards generated", expectedNumCards, addedNote.numberOfCards(col)) // Now delete the note cr.delete(newNoteUri, null, null) try { - addedNote.load() + addedNote.load(col) fail("Expected RuntimeException to be thrown when deleting note") } catch (e: RuntimeException) { // Expect RuntimeException to be thrown when loading deleted note @@ -1300,7 +1300,7 @@ class ContentProviderTest : InstrumentedTest() { fields: Array, tag: String ): Uri { - val newNote = Note(col, col.notetypes.get(mid)!!) + val newNote = Note(col.notetypes.get(mid)!!) for (idx in fields.indices) { newNote.setField(idx, fields[idx]) } @@ -1310,7 +1310,7 @@ class ContentProviderTest : InstrumentedTest() { col.addNote(newNote), greaterThanOrEqualTo(1) ) - for (c in newNote.cards()) { + for (c in newNote.cards(col)) { c.did = did col.updateCard(c, skipUndoEntry = true) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 49e97ee95302..90989b1627a6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -2194,7 +2194,7 @@ abstract class AbstractFlashcardViewer : } protected open fun shouldDisplayMark(): Boolean { - return isMarked(currentCard!!.note(getColUnsafe)) + return isMarked(getColUnsafe, currentCard!!.note(getColUnsafe)) } val writeLock: Lock @@ -2565,8 +2565,8 @@ abstract class AbstractFlashcardViewer : if (currentCard!!.note(getColUnsafe).tags != selectedTags) { val tagString = selectedTags.joinToString(" ") val note = currentCard!!.note(getColUnsafe) - note.setTagsFromStr(tagString) - note.flush() + note.setTagsFromStr(getColUnsafe, tagString) + note.flush(getColUnsafe) // Reload current card to reflect tag changes reloadWebViewContent() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index 258eaf451523..57c0d15d1cea 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -238,7 +238,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { activity.launchCatchingTask { activity.resetCards(cardIds) } convertToByteArray(apiContract, true) } - "cardMark" -> convertToByteArray(apiContract, currentCard.note(getColUnsafe).hasTag("marked")) + "cardMark" -> convertToByteArray(apiContract, currentCard.note(getColUnsafe).hasTag(getColUnsafe, "marked")) "cardFlag" -> convertToByteArray(apiContract, currentCard.userFlag()) "cardReps" -> convertToByteArray(apiContract, currentCard.reps) "cardInterval" -> convertToByteArray(apiContract, currentCard.ivl) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index 6a728a09fc41..55f0e60f296f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -1979,7 +1979,7 @@ open class CardBrowser : 6 -> R.attr.flagTurquoise 7 -> R.attr.flagPurple else -> { - if (isMarked(card.note(col))) { + if (isMarked(col, card.note(col))) { R.attr.markedColor } else if (card.queue == Consts.QUEUE_TYPE_SUSPENDED) { R.attr.suspendedColor @@ -1994,11 +1994,11 @@ open class CardBrowser : return when (key) { CardBrowserColumn.FLAGS -> Integer.valueOf(card.userFlag()).toString() CardBrowserColumn.SUSPENDED -> if (card.queue == Consts.QUEUE_TYPE_SUSPENDED) "True" else "False" - CardBrowserColumn.MARKED -> if (isMarked(card.note(col))) "marked" else null - CardBrowserColumn.SFLD -> card.note(col).sFld() + CardBrowserColumn.MARKED -> if (isMarked(col, card.note(col))) "marked" else null + CardBrowserColumn.SFLD -> card.note(col).sFld(col) CardBrowserColumn.DECK -> col.decks.name(card.did) - CardBrowserColumn.TAGS -> card.note(col).stringTags() - CardBrowserColumn.CARD -> if (inCardMode) card.template(col).optString("name") else "${card.note(col).numberOfCards()}" + CardBrowserColumn.TAGS -> card.note(col).stringTags(col) + CardBrowserColumn.CARD -> if (inCardMode) card.template(col).optString("name") else "${card.note(col).numberOfCards(col)}" CardBrowserColumn.DUE -> card.dueString(col) CardBrowserColumn.EASE -> if (inCardMode) getEaseForCards() else getAvgEaseForNotes() CardBrowserColumn.CHANGED -> LanguageUtil.getShortDateFormatFromS(if (inCardMode) card.mod else card.note(col).mod.toLong()) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt index f67a8bd0438f..f2ede5dcb454 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt @@ -597,7 +597,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { // If we have a card for this position, send it, otherwise an empty card list signals to show a blank if (noteId != -1L) { - val cids = col.getNote(noteId).cids() + val cids = col.getNote(noteId).cids(col) if (ordinal < cids.size) { i.putExtra("cardList", longArrayOf(cids[ordinal])) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt index 10d8402aec8c..41e2a8fee342 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt @@ -239,7 +239,7 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { // loading from the note editor val toPreview = setCurrentCardFromNoteEditorBundle(col) if (toPreview != null) { - cardCount = toPreview.note(getColUnsafe).numberOfCardsEphemeral() + cardCount = toPreview.note(col).numberOfCardsEphemeral(col) if (cardCount >= 2) { previewLayout!!.showNavigationButtons() } @@ -304,7 +304,7 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { currentNote.delTag(tag) } if (tagsList != null) { - val tagsSet = currentNote.col.tags.canonify(tagsList) + val tagsSet = getColUnsafe.tags.canonify(tagsList) currentNote.addTags(tagsSet) } } @@ -425,6 +425,7 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { ord } val context = TemplateManager.TemplateRenderContext.fromCardLayout( + col, note(col), this, model(col), @@ -447,7 +448,7 @@ private class EphemeralCard(col: Collection, id: Long?) : Card(col, id) { fun fromNote(n: Note, col: Collection, cardIndex: Int = 0): EphemeralCard { val card = EphemeralCard(col, null) card.did = 1 - card.ord = n.cardIndexToOrd(cardIndex) + card.ord = n.cardIndexToOrd(col, cardIndex) Timber.v("Generating ephemeral note, idx %d ord %d", cardIndex, card.ord) val nt = n.notetype @@ -460,6 +461,7 @@ private class EphemeralCard(col: Collection, id: Long?) : Card(col, id) { template.put("ord", card.ord) val output = TemplateManager.TemplateRenderContext.fromCardLayout( + col, n, card, notetype = nt, @@ -474,7 +476,7 @@ private class EphemeralCard(col: Collection, id: Long?) : Card(col, id) { } /** returns the number of cards from a note which has not had data saved in the database */ -private fun Note.numberOfCardsEphemeral(): Int { +private fun Note.numberOfCardsEphemeral(col: Collection): Int { // We can't use note.numberOfCards() as this uses the database value return when { this.notetype.isCloze -> col.clozeNumbersInNote(this).size @@ -485,7 +487,7 @@ private fun Note.numberOfCardsEphemeral(): Int { /** * Given a card index, returns the 'ord' of the card */ -private fun Note.cardIndexToOrd(index: Int): Int { +private fun Note.cardIndexToOrd(col: Collection, index: Int): Int { // We can't use note.numberOfCards() as this uses the database value return when { this.notetype.isCloze -> col.clozeNumbersInNote(this)[index] - 1 diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index b293f77b9b58..04a9939723f8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt @@ -228,8 +228,8 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags NoteEditorActivityResultCallback { // Model can change regardless of exit type - update ourselves and CardBrowser mReloadRequired = true - mEditorNote!!.reloadModel() - if (mCurrentEditedCard == null || !mEditorNote!!.cids() + mEditorNote!!.reloadModel(getColUnsafe) + if (mCurrentEditedCard == null || !mEditorNote!!.cids(getColUnsafe) .contains(mCurrentEditedCard!!.id) ) { if (!addNote) { @@ -887,7 +887,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags // Save deck to model mEditorNote!!.notetype.put("did", deckId) // Save tags to model - mEditorNote!!.setTagsFromStr(tagsAsString(mSelectedTags!!)) + mEditorNote!!.setTagsFromStr(getColUnsafe, tagsAsString(mSelectedTags!!)) val tags = JSONArray() for (t in mSelectedTags!!) { tags.put(t) @@ -910,7 +910,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags val oldModel = if (mCurrentEditedCard == null) null else mCurrentEditedCard!!.model(getColUnsafe) if (newModel != oldModel) { mReloadRequired = true - if (mModelChangeCardMap!!.size < mEditorNote!!.numberOfCards() || mModelChangeCardMap!!.containsValue( + if (mModelChangeCardMap!!.size < mEditorNote!!.numberOfCards(getColUnsafe) || mModelChangeCardMap!!.containsValue( null ) ) { @@ -953,7 +953,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags } // added tag? for (t in mSelectedTags!!) { - modified = modified || !mEditorNote!!.hasTag(t) + modified = modified || !mEditorNote!!.hasTag(getColUnsafe, tag = t) } // removed tag? modified = modified || mEditorNote!!.tags.size > mSelectedTags!!.size @@ -963,7 +963,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags return } - mEditorNote!!.setTagsFromStr(tagsAsString(mSelectedTags!!)) + mEditorNote!!.setTagsFromStr(getColUnsafe, tagsAsString(mSelectedTags!!)) changed = true if (caller != CALLER_PREVIEWER_EDIT) { @@ -973,7 +973,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags withProgress { undoableOp { - updateNote(mCurrentEditedCard!!.note(this)) + updateNote(mCurrentEditedCard!!.note()) } closeNoteEditor() } @@ -989,9 +989,9 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags val noteId = mEditorNote!!.id undoableOp { notetypes.change(oldNotetype, noteId, newNotetype, mModelChangeFieldMap!!, mModelChangeCardMap!!) + // refresh the note object to reflect the database changes + mEditorNote!!.load() } - // refresh the note object to reflect the database changes - mEditorNote!!.load() // close note editor closeNoteEditor() } @@ -1683,7 +1683,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags // Update the field in the Note so we can run a dupe check on it. updateField(field) // 1 is empty, 2 is dupe, null is neither. - val dupeCode = mEditorNote!!.dupeOrEmpty() + val dupeCode = mEditorNote!!.dupeOrEmpty(getColUnsafe) // Change bottom line color of text field if (dupeCode == DupeOrEmpty.DUPE) { field!!.setDupeStyle() @@ -1748,7 +1748,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags private fun setNote(note: Note?, changeType: FieldChangeType) { mEditorNote = if (note == null || addNote) { val model = getColUnsafe.notetypes.current() - Note(getColUnsafe, model) + Note(model) } else { note } @@ -2126,7 +2126,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags val templatesLength = tmpls.length() mModelChangeCardMap = HashUtil.hashMapInit(templatesLength) for (i in 0 until templatesLength) { - if (i < mEditorNote!!.numberOfCards()) { + if (i < mEditorNote!!.numberOfCards(getColUnsafe)) { mModelChangeCardMap!![i] = i } else { mModelChangeCardMap!![i] = null diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index c6b57f858f0d..7c4dbd9896d1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -703,7 +703,7 @@ open class Reviewer : actionButtons.setCustomButtonsStatus(menu) val alpha = Themes.ALPHA_ICON_ENABLED_LIGHT val markCardIcon = menu.findItem(R.id.action_mark_card) - if (currentCard != null && isMarked(currentCard!!.note(getColUnsafe))) { + if (currentCard != null && isMarked(getColUnsafe, currentCard!!.note(getColUnsafe))) { markCardIcon.setTitle(R.string.menu_unmark_note).setIcon(R.drawable.ic_star_white) } else { markCardIcon.setTitle(R.string.menu_mark_note).setIcon(R.drawable.ic_star_border_white) 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 581ac063179b..d3e2cd79ac5c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt @@ -41,6 +41,7 @@ import com.ichi2.libanki.Card import com.ichi2.libanki.CardId import com.ichi2.libanki.Consts import com.ichi2.libanki.DeckId +import com.ichi2.libanki.hasTag import com.ichi2.libanki.undoableOp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow 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 a81ab7d7c984..bf7ec3ff7d0d 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.hasTag import com.ichi2.libanki.note import com.ichi2.themes.Themes import com.ichi2.utils.toRGBHex @@ -132,8 +133,8 @@ class PreviewerViewModel(private val selectedCardIds: LongArray, firstIndex: Int } private suspend fun updateMarkIcon() { - val note = withCol { currentCard.note() } - isMarked.emit(note.hasTag(MARKED_TAG)) + val isMarkedValue = withCol { currentCard.note().hasTag(MARKED_TAG) } + isMarked.emit(isMarkedValue) } private suspend fun showQuestion() { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt index 137531326fa8..fbde6b9e6413 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt @@ -227,7 +227,7 @@ class CardContentProvider : ContentProvider() { val currentNote = getNoteFromUri(uri, col) val columns = projection ?: FlashCardsContract.Card.DEFAULT_PROJECTION val rv = MatrixCursor(columns, 1) - for (currentCard: Card in currentNote.cards()) { + for (currentCard: Card in currentNote.cards(col)) { addCardToCursor(currentCard, rv, col, columns) } rv @@ -427,7 +427,7 @@ class CardContentProvider : ContentProvider() { // Update tags Timber.d("CardContentProvider: tags update...") if (tags != null) { - currentNote.setTagsFromStr(tags.toString()) + currentNote.setTagsFromStr(col, tags.toString()) } updated++ } @@ -438,7 +438,7 @@ class CardContentProvider : ContentProvider() { } } Timber.d("CardContentProvider: Saving note...") - currentNote.flush() + currentNote.flush(col) } NOTES_ID_CARDS -> throw UnsupportedOperationException("Not yet implemented") NOTES_ID_CARDS_ORD -> { @@ -718,7 +718,7 @@ class CardContentProvider : ContentProvider() { } // Create empty note - val newNote = Note(col, model!!) // for some reason we cannot pass modelId in here + val newNote = Note(model!!) // for some reason we cannot pass modelId in here // Set fields // Check that correct number of flds specified if (fldsArray.size != newNote.fields.size) { @@ -730,11 +730,11 @@ class CardContentProvider : ContentProvider() { // Set tags val tags = values.getAsString(FlashCardsContract.Note.TAGS) if (tags != null) { - newNote.setTagsFromStr(tags) + newNote.setTagsFromStr(col, tags) } // Add to collection col.addNote(newNote, deckId) - for (card: Card in newNote.cards()) { + for (card: Card in newNote.cards(col)) { card.did = deckId col.updateCard(card) } @@ -763,7 +763,7 @@ class CardContentProvider : ContentProvider() { // val allowEmpty = AllowEmpty.fromBoolean(values.getAsBoolean(FlashCardsContract.Note.ALLOW_EMPTY)) // Create empty note val model = requireNotNull(col.notetypes.get(modelId)) { "Invalid modelId: $modelId" } - val newNote = Note(col, model) + val newNote = Note(model) // Set fields val fldsArray = Utils.splitFields(flds) // Check that correct number of flds specified @@ -777,7 +777,7 @@ class CardContentProvider : ContentProvider() { } // Set tags if (tags != null) { - newNote.setTagsFromStr(tags) + newNote.setTagsFromStr(col, tags) } // Add to collection col.addNote(newNote) @@ -1176,7 +1176,7 @@ class CardContentProvider : ContentProvider() { private fun getCard(noteId: NoteId, ord: Int, col: Collection): Card { val currentNote = col.getNote(noteId) var currentCard: Card? = null - for (card in currentNote.cards()) { + for (card in currentNote.cards(col)) { if (card.ord == ord) { currentCard = card } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/NoteService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/NoteService.kt index 3ceb8b274ff9..533f14f42893 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/NoteService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/NoteService.kt @@ -22,6 +22,7 @@ package com.ichi2.anki.servicelayer import android.os.Bundle import androidx.annotation.CheckResult import androidx.annotation.VisibleForTesting +import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.CrashReportService import com.ichi2.anki.FieldEditText import com.ichi2.anki.multimediacard.IMultimediaEditableNote @@ -33,6 +34,7 @@ import com.ichi2.libanki.Consts import com.ichi2.libanki.Note import com.ichi2.libanki.NoteTypeId import com.ichi2.libanki.exception.EmptyMediaException +import com.ichi2.libanki.hasTag import com.ichi2.libanki.undoableOp import com.ichi2.utils.CollectionUtils.average import org.json.JSONException @@ -72,7 +74,7 @@ object NoteService { return null } - fun updateMultimediaNoteFromFields(col: com.ichi2.libanki.Collection, fields: Array, modelId: NoteTypeId, mmNote: MultimediaEditableNote) { + fun updateMultimediaNoteFromFields(col: Collection, fields: Array, modelId: NoteTypeId, mmNote: MultimediaEditableNote) { for (i in fields.indices) { val value = fields[i] val field: IField = if (value.startsWith(" tmpMediaPath = field.audioPath @@ -194,8 +196,10 @@ object NoteService { } } - fun isMarked(note: Note): Boolean { - return note.hasTag("marked") + suspend fun isMarked(note: Note): Boolean = withCol { isMarked(this, note) } + + fun isMarked(col: Collection, note: Note): Boolean { + return note.hasTag(col, tag = "marked") } // TODO: should make a direct SQL query to do this @@ -203,23 +207,23 @@ object NoteService { * returns the average ease of all the non-new cards in the note, * or if all the cards in the note are new, returns null */ - fun avgEase(note: Note): Int? { - val nonNewCards = note.cards().filter { it.type != Consts.CARD_TYPE_NEW } + fun avgEase(col: Collection, note: Note): Int? { + val nonNewCards = note.cards(col).filter { it.type != Consts.CARD_TYPE_NEW } return nonNewCards.average { it.factor }?.let { it / 10 }?.toInt() } // TODO: should make a direct SQL query to do this - fun totalLapses(note: Note) = note.cards().sumOf { it.lapses } + fun totalLapses(col: Collection, note: Note) = note.cards(col).sumOf { it.lapses } - fun totalReviews(note: Note) = note.cards().sumOf { it.reps } + fun totalReviews(col: Collection, note: Note) = note.cards(col).sumOf { it.reps } /** * Returns the average interval of all the non-new and non-learning cards in the note, * or if all the cards in the note are new or learning, returns null */ - fun avgInterval(note: Note): Int? { - val nonNewOrLearningCards = note.cards().filter { it.type != Consts.CARD_TYPE_NEW && it.type != Consts.CARD_TYPE_LRN } + fun avgInterval(col: Collection, note: Note): Int? { + val nonNewOrLearningCards = note.cards(col).filter { it.type != Consts.CARD_TYPE_NEW && it.type != Consts.CARD_TYPE_LRN } return nonNewOrLearningCards.average { it.ivl }?.toInt() } @@ -234,8 +238,8 @@ object NoteService { const val MARKED_TAG = "marked" -fun Card.totalLapsesOfNote(col: Collection) = NoteService.totalLapses(note(col)) +fun Card.totalLapsesOfNote(col: Collection) = NoteService.totalLapses(col, note(col)) -fun Card.totalReviewsForNote(col: Collection) = NoteService.totalReviews(note(col)) +fun Card.totalReviewsForNote(col: Collection) = NoteService.totalReviews(col, note(col)) -fun Card.avgIntervalOfNote(col: Collection) = NoteService.avgInterval(note(col)) +fun Card.avgIntervalOfNote(col: Collection) = NoteService.avgInterval(col, note(col)) diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt index c2ef6f071a01..d3bb620d7fc5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Card.kt @@ -412,7 +412,7 @@ open class Card : Cloneable { return LanguageUtil.getShortDateFormatFromS(date) } // In Anki Desktop, a card with oDue <> 0 && oDid == 0 is not marked as dynamic. - fun avgEaseOfNote(col: Collection) = avgEase(note(col)) + fun avgEaseOfNote(col: Collection) = avgEase(col, note(col)) /** Non libAnki */ val isInDynamicDeck: Boolean diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt index c09c0c21fc0c..c71a3b1fe2f1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt @@ -301,7 +301,7 @@ open class Collection( * @return The new note */ fun newNote(m: NotetypeJson): Note { - return Note(this, m) + return Note(m) } /** @@ -530,7 +530,7 @@ open class Collection( @RustCleanup("Remove this in favour of addNote() above; call addNote() inside undoableOp()") fun addNote(note: Note): Int { addNote(note, note.notetype.did) - return note.numberOfCards() + return note.numberOfCards(this) } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Note.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Note.kt index 92dda1de291d..2b543e3500e9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Note.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Note.kt @@ -29,7 +29,6 @@ import java.util.regex.Pattern @KotlinCleanup("lots to do") class Note : Cloneable { - val col: Collection /** * Should only be mutated by addNote() @@ -54,13 +53,11 @@ class Note : Cloneable { private set constructor(col: Collection, id: Long) { - this.col = col this.id = id - load() + load(col) } - constructor(col: Collection, notetype: NotetypeJson) { - this.col = col + constructor(notetype: NotetypeJson) { this.id = 0 guId = Utils.guid64() this.notetype = notetype @@ -70,12 +67,12 @@ class Note : Cloneable { fMap = Notetypes.fieldMap(this.notetype) } - fun load() { + fun load(col: Collection) { val note = col.backend.getNote(this.id) - loadFromBackendNote(note) + loadFromBackendNote(col, note) } - private fun loadFromBackendNote(note: anki.notes.Note) { + private fun loadFromBackendNote(col: Collection, note: anki.notes.Note) { this.id = note.id this.guId = note.guid this.mid = note.notetypeId @@ -88,7 +85,7 @@ class Note : Cloneable { this.fMap = Notetypes.fieldMap(notetype) } - fun reloadModel() { + fun reloadModel(col: Collection) { notetype = col.notetypes.get(mid)!! } @@ -96,21 +93,21 @@ class Note : Cloneable { * If fields or tags have changed, write changes to disk. */ @RustCleanup("code should call col.updateNote() instead, in undoableOp {}") - fun flush() { + fun flush(col: Collection) { col.updateNote(this) } - fun numberOfCards(): Int { + fun numberOfCards(col: Collection): Int { return col.db.queryLongScalar("SELECT count() FROM cards WHERE nid = ?", this.id).toInt() } - fun cids(): List { + fun cids(col: Collection): List { return col.db.queryLongList("SELECT id FROM cards WHERE nid = ? ORDER BY ord", this.id) } - fun cards(): ArrayList { - val cards = ArrayList(cids().size) - for (cid in cids()) { + fun cards(col: Collection): ArrayList { + val cards = ArrayList(cids(col).size) + for (cid in cids(col)) { // each getCard access database. This is inefficient. // Seems impossible to solve without creating a constructor of a list of card. // Not a big trouble since most note have a small number of cards. @@ -121,7 +118,7 @@ class Note : Cloneable { /** The first card, assuming it exists. */ @CheckResult - fun firstCard(): Card { + fun firstCard(col: Collection): Card { return col.getCard( col.db.queryLongScalar( "SELECT id FROM cards WHERE nid = ? ORDER BY ord LIMIT 1", @@ -184,15 +181,15 @@ class Note : Cloneable { * Tags * *********************************************************** */ - fun hasTag(tag: String?): Boolean { + fun hasTag(col: Collection, tag: String?): Boolean { return col.tags.inList(tag!!, tags) } - fun stringTags(): String { + fun stringTags(col: Collection): String { return col.tags.join(col.tags.canonify(tags)) } - fun setTagsFromStr(str: String?) { + fun setTagsFromStr(col: Collection, str: String?) { tags = col.tags.split(str!!) } @@ -233,7 +230,7 @@ class Note : Cloneable { * * @return whether it has no content, dupe first field, or nothing remarkable. */ - fun dupeOrEmpty(): DupeOrEmpty { + fun dupeOrEmpty(col: Collection): DupeOrEmpty { if (fields[0].trim { it <= ' ' }.isEmpty()) { return DupeOrEmpty.EMPTY } @@ -261,7 +258,7 @@ class Note : Cloneable { return DupeOrEmpty.CORRECT } - fun sFld(): String = col.db.queryString("SELECT sfld FROM notes WHERE id = ?", this.id) + fun sFld(col: Collection): String = col.db.queryString("SELECT sfld FROM notes WHERE id = ?", this.id) fun setField(index: Int, value: String) { fields[index] = value @@ -315,3 +312,15 @@ class Note : Cloneable { } } } + +/** @see Note.hasTag */ +context (Collection) +fun Note.hasTag(tag: String) = this.hasTag(this@Collection, tag) + +/** @see Note.setTagsFromStr */ +context (Collection) +fun Note.setTagsFromStr(str: String?) = this.setTagsFromStr(this@Collection, str) + +/** @see Note.load */ +context (Collection) +fun Note.load() = this.load(this@Collection) diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/TemplateManager.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/TemplateManager.kt index e88a726e77ab..e956a0274b5d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/TemplateManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/TemplateManager.kt @@ -135,6 +135,7 @@ class TemplateManager { } fun fromCardLayout( + col: Collection, note: Note, card: Card, notetype: NotetypeJson, @@ -142,7 +143,7 @@ class TemplateManager { fillEmpty: Boolean ): TemplateRenderContext { return TemplateRenderContext( - note.col, + col, card, note, notetype = notetype, diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt index b3021f1ee70f..414c99da4d73 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt @@ -1063,7 +1063,7 @@ class CardBrowserTest : RobolectricTest() { } private val CardCache.isMarked - get() = NoteService.isMarked(card.note(col)) + get() = NoteService.isMarked(col, card.note(col)) } fun CardBrowser.hasSelectedCardAtPosition(i: Int): Boolean = diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/services/NoteServiceTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/services/NoteServiceTest.kt index 4443ab553d3d..988e68d063bd 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/services/NoteServiceTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/services/NoteServiceTest.kt @@ -57,7 +57,7 @@ class NoteServiceTest : RobolectricTest() { multiMediaNote!!.getField(0)!!.text = "foo" multiMediaNote.getField(1)!!.text = "bar" - val basicNote = Note(col, testModel).apply { + val basicNote = Note(testModel).apply { setField(0, "this should be changed to foo") setField(1, "this should be changed to bar") } @@ -76,7 +76,7 @@ class NoteServiceTest : RobolectricTest() { // model with ID 45 testNotetype = NotetypeJson("""{"flds": [{"name": "foo bar", "ord": "1"}], "id": "45"}""") - val noteWithID45 = Note(col, testNotetype) + val noteWithID45 = Note(testNotetype) val expectedException: Throwable = assertThrows(RuntimeException::class.java) { NoteService.updateJsonNoteFromMultimediaNote(multiMediaNoteWithID42, noteWithID45) } assertEquals(expectedException.message, "Source and Destination Note ID do not match.") } @@ -245,19 +245,19 @@ class NoteServiceTest : RobolectricTest() { } } // avg ease = (3000/10 + 1500/10 + 100/10 + 750/10) / 4 = [156.25] = 156 - assertEquals(156, NoteService.avgEase(note)) + assertEquals(156, NoteService.avgEase(col, note)) // test case: one card is new note.cards()[2].update { type = Consts.CARD_TYPE_NEW } // avg ease = (3000/10 + 1500/10 + 750/10) / 3 = [175] = 175 - assertEquals(175, NoteService.avgEase(note)) + assertEquals(175, NoteService.avgEase(col, note)) // test case: all cards are new note.updateCards { type = Consts.CARD_TYPE_NEW } // no cards are rev, so avg ease cannot be calculated - assertEquals(null, NoteService.avgEase(note)) + assertEquals(null, NoteService.avgEase(col, note)) } @Test @@ -276,7 +276,7 @@ class NoteServiceTest : RobolectricTest() { } // avg interval = (3000 + 1500 + 1000 + 750) / 4 = [1562.5] = 1562 - assertEquals(1562, NoteService.avgInterval(note)) + assertEquals(1562, NoteService.avgInterval(col, note)) // case: one card is new or learning note.cards()[2].update { @@ -284,12 +284,12 @@ class NoteServiceTest : RobolectricTest() { } // avg interval = (3000 + 1500 + 750) / 3 = [1750] = 1750 - assertEquals(1750, NoteService.avgInterval(note)) + assertEquals(1750, NoteService.avgInterval(col, note)) // case: all cards are new or learning note.updateCards { type = newOrLearningList.shuffled().first() } // no cards are rev or relearning, so avg interval cannot be calculated - assertEquals(null, NoteService.avgInterval(note)) + assertEquals(null, NoteService.avgInterval(col, note)) } } diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/TestClass.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/TestClass.kt index da1aa3c72e95..73961da8e915 100644 --- a/AnkiDroid/src/test/java/com/ichi2/testutils/TestClass.kt +++ b/AnkiDroid/src/test/java/com/ichi2/testutils/TestClass.kt @@ -182,6 +182,14 @@ interface TestClass { fun Card.dueString() = this.dueString(col) fun Card.pureAnswer() = this.pureAnswer(col) + fun Note.load() = this.load(col) + fun Note.cards() = this.cards(col) + fun Note.firstCard() = this.firstCard(col) + fun Note.cids() = this.cids(col) + fun Note.numberOfCards() = this.numberOfCards(col) + fun Note.dupeOrEmpty() = this.dupeOrEmpty(col) + fun Note.flush() = this.flush(col) + /** * A wrapper around the standard [kotlinx.coroutines.test.runTest] that * takes care of updating the dispatcher used by CollectionManager as well. * * An argument could be made for using [StandardTestDispatcher] and