Add Extra Study — star cards during review, study just the marked ones

Per-week "Extra Study (N)" row appears at the end of each LanGo week
section when at least one card is marked. Cards are marked from inside
VocabFlashcardView via a star next to the speaker on reveal. Marks are
keyed by the same SHA256 hash CourseReviewCard uses, so a mark and its
SRS state describe the same logical card.

ExtraStudyMark is CloudKit-synced (private DB), with uniqueness enforced
by fetch-or-create on id since CloudKit forbids @Attribute(.unique).
Skipped for textbook courses: DeckStudyView nils out the mark context
when the deck's courseName matches a TextbookChapter, and CourseView
hides the row when the active course is a textbook — so there are no
orphan marks the user can't reach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-04 22:38:39 -05:00
parent 05a0cc0d17
commit ade091f108
8 changed files with 334 additions and 11 deletions
@@ -0,0 +1,44 @@
import Foundation
import SwiftData
/// User mark on a vocab card for "extra study" focus. Cloud-synced so the set
/// follows the user across devices. Denormalises card identity (deckId/front/
/// back) so the Course tab can resolve a marked-cards view without joining
/// against the local-only `VocabCard` store.
///
/// CloudKit forbids `@Attribute(.unique)`, so callers must fetch-or-create
/// by `id` to maintain uniqueness in code.
@Model
public final class ExtraStudyMark {
/// Stable hash of (deckId, front, back, examplesES, examplesEN). Same
/// shape as `CourseCardStore.reviewKey(for:)` so a mark and a review
/// card describe the same logical card.
public var id: String = ""
public var deckId: String = ""
public var courseName: String = ""
public var weekNumber: Int = 0
public var front: String = ""
public var back: String = ""
public var markedAt: Date = Date()
public init(
id: String,
deckId: String,
courseName: String,
weekNumber: Int,
front: String,
back: String,
markedAt: Date = Date()
) {
self.id = id
self.deckId = deckId
self.courseName = courseName
self.weekNumber = weekNumber
self.front = front
self.back = back
self.markedAt = markedAt
}
}