Code + Identity-Rename zur Vorbereitung auf Apple-Dev-Portal-Aktion (Bundle ev.mana.wordeck, App-Group group.ev.mana.wordeck, AASA applinks:wordeck.com). Build bleibt funktional, aber gegen die neue text-only-API können image-occlusion-Creates 422 zurückgeben — das wird mit der Wordeck-Native v1.0-Welle (parallele Apple-Aktion) sauber gemacht. Umbenennung: - 41 Files: cardecky/Cardecky → wordeck/Wordeck (Display, Strings, Kommentare) - 57 Files: CardsNative → WordeckNative, CardsAPI → WordeckAPI, CardsTheme → WordeckTheme, CardsBrand → WordeckBrand, CardsWidget → WordeckWidget, CardsDueWidget → WordeckDueWidget - Bundle-ID ev.mana.cardecky → ev.mana.wordeck (project.yml, Info.plist, entitlements, Keychain-Service, App-Group) - AASA applinks:cardecky.mana.how → applinks:wordeck.com - API-Base cardecky-api.mana.how → api.wordeck.com - 10 Files renamed (App-Entry, API-Extensions, Theme, Widget, Entitlements, Tests) - xcodeproj regenerated via xcodegen → WordeckNative.xcodeproj - MaskRegionsTests.swift gelöscht (image-occlusion entfällt mit Wordeck text-only) Forgejo-Repo git.mana.how/till/cards-native → wordeck-native umbenannt (Auto-Redirect aktiv). Lokales Verzeichnis Code/cards-native/ bleibt vorerst — wird beim nächsten Apple-Setup mit Bundle-Test umbenannt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3 KiB
Swift
94 lines
3 KiB
Swift
import Foundation
|
|
import ManaCore
|
|
import Observation
|
|
import SwiftData
|
|
|
|
/// Persistente Offline-Queue für Grade-Aktionen. Drain-Loop kann
|
|
/// vom UI ausgelöst werden (bei Reconnect oder App-Foreground).
|
|
@MainActor
|
|
@Observable
|
|
final class GradeQueue {
|
|
private(set) var isDraining = false
|
|
private(set) var lastDrainError: String?
|
|
|
|
private let api: WordeckAPI
|
|
private let context: ModelContext
|
|
|
|
init(api: WordeckAPI, context: ModelContext) {
|
|
self.api = api
|
|
self.context = context
|
|
}
|
|
|
|
/// Enqueue + sofort versuchen zu senden. Bei Fehler bleibt der
|
|
/// Eintrag in der Queue.
|
|
func submit(cardId: String, subIndex: Int, rating: Rating, reviewedAt: Date = .now) async {
|
|
let grade = PendingGrade(
|
|
cardId: cardId,
|
|
subIndex: subIndex,
|
|
rating: rating,
|
|
reviewedAt: reviewedAt
|
|
)
|
|
context.insert(grade)
|
|
try? context.save()
|
|
let rawRating = rating.rawValue
|
|
Log.study.info(
|
|
"Queued grade \(cardId, privacy: .public)/\(subIndex, privacy: .public): \(rawRating, privacy: .public)"
|
|
)
|
|
await drain()
|
|
}
|
|
|
|
/// Schickt alle pending grades in FIFO-Reihenfolge ab. Bei Server-
|
|
/// Erfolg: aus Queue löschen. Bei Netzfehler: Loop abbrechen
|
|
/// (kommender Drain probiert es nochmal).
|
|
func drain() async {
|
|
guard !isDraining else { return }
|
|
isDraining = true
|
|
defer { isDraining = false }
|
|
|
|
let descriptor = FetchDescriptor<PendingGrade>(
|
|
sortBy: [SortDescriptor(\.queuedAt, order: .forward)]
|
|
)
|
|
let pending = (try? context.fetch(descriptor)) ?? []
|
|
guard !pending.isEmpty else {
|
|
lastDrainError = nil
|
|
return
|
|
}
|
|
|
|
for grade in pending {
|
|
guard let rating = grade.rating else {
|
|
context.delete(grade)
|
|
continue
|
|
}
|
|
do {
|
|
_ = try await api.gradeReview(
|
|
cardId: grade.cardId,
|
|
subIndex: grade.subIndex,
|
|
rating: rating,
|
|
reviewedAt: grade.reviewedAt
|
|
)
|
|
context.delete(grade)
|
|
try? context.save()
|
|
} catch {
|
|
let msg = (error as? LocalizedError)?.errorDescription ?? String(describing: error)
|
|
grade.lastTryAt = .now
|
|
grade.lastError = msg
|
|
try? context.save()
|
|
lastDrainError = msg
|
|
let cid = grade.cardId
|
|
let sub = grade.subIndex
|
|
Log.study.notice(
|
|
"Drain stopped \(cid, privacy: .public)/\(sub, privacy: .public): \(msg, privacy: .public)"
|
|
)
|
|
return
|
|
}
|
|
}
|
|
lastDrainError = nil
|
|
Log.study.info("Drain complete")
|
|
}
|
|
|
|
/// Wie viele Grades hängen aktuell offline?
|
|
func pendingCount() -> Int {
|
|
let descriptor = FetchDescriptor<PendingGrade>()
|
|
return (try? context.fetchCount(descriptor)) ?? 0
|
|
}
|
|
}
|