Initial commit: Conjuga Spanish conjugation app

Includes SwiftData dual-store architecture (local reference + CloudKit user data),
JSON-based data seeding, 20 tense guides, 20 grammar notes, SRS review system,
course vocabulary, and widget support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-09 20:58:33 -05:00
commit 4b467ec136
95 changed files with 82599 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
import CryptoKit
import Foundation
import SwiftData
public enum CourseCardStore {
private static var sortDescriptors: [SortDescriptor<VocabCard>] {
[
SortDescriptor(\VocabCard.deckId),
SortDescriptor(\VocabCard.front),
SortDescriptor(\VocabCard.back),
]
}
public static func reviewKey(for card: VocabCard) -> String {
reviewKey(
deckId: card.deckId,
front: card.front,
back: card.back,
examplesES: card.examplesES,
examplesEN: card.examplesEN
)
}
public static func reviewKey(
deckId: String,
front: String,
back: String,
examplesES: [String],
examplesEN: [String]
) -> String {
let source = [
deckId,
front,
back,
examplesES.joined(separator: "\u{1F}"),
examplesEN.joined(separator: "\u{1F}"),
].joined(separator: "\u{1E}")
let digest = SHA256.hash(data: Data(source.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
public static func fetchCardCount(context: ModelContext) -> Int {
(try? context.fetchCount(FetchDescriptor<VocabCard>())) ?? 0
}
public static func fetchCard(at index: Int, context: ModelContext) -> VocabCard? {
guard index >= 0 else { return nil }
var descriptor = FetchDescriptor<VocabCard>(sortBy: sortDescriptors)
descriptor.fetchOffset = index
descriptor.fetchLimit = 1
return (try? context.fetch(descriptor))?.first
}
public static func fetchCards(offset: Int, limit: Int, context: ModelContext) -> [VocabCard] {
guard limit > 0 else { return [] }
var descriptor = FetchDescriptor<VocabCard>(sortBy: sortDescriptors)
descriptor.fetchOffset = max(offset, 0)
descriptor.fetchLimit = limit
return (try? context.fetch(descriptor)) ?? []
}
public static func fetchWordOfDayCard(
for date: Date,
wordOffset: Int,
context: ModelContext
) -> VocabCard? {
let count = fetchCardCount(context: context)
guard count > 0 else { return nil }
let dayOfYear = Calendar.current.ordinality(of: .day, in: .year, for: date) ?? 1
let index = (dayOfYear + wordOffset) % count
return fetchCard(at: index, context: context)
}
}