v0.2.0 — Phase β-1 Decks lesen
Deck-Liste mit Web-Parität: alle eigenen Decks aus cardecky-api, Card-/Due-Counts pro Deck (Web-Pattern: separate Calls), Pull-to- Refresh, Offline-Read via SwiftData, Inbox-Banner für Marketplace- Forks. - Deck-Codable-DTO mit snake_case-CodingKeys (DeckCategory, DeckVisibility, FsrsSettings) - ISO8601-Date-Decoder mit Fractional-Seconds-Toleranz - CardsAPI.listDecks() + cardCount() + dueCount() - CachedDeck SwiftData-Model mit lastFetchedAt - DeckListStore (API + Cache, paralleles Counts-Fetching via TaskGroup) - DeckListView mit forest-Theme, deck.color-Streifen, Inbox-Banner - AccountView mit Sign-out - DashboardView durch DeckListView ersetzt - 6 Unit-Tests + 1 UI-Test grün Phasen-Plan: mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28b20cd934
commit
f664a00b64
12 changed files with 809 additions and 85 deletions
99
Sources/Core/Sync/DeckListStore.swift
Normal file
99
Sources/Core/Sync/DeckListStore.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import Observation
|
||||
import SwiftData
|
||||
|
||||
/// Orchestriert API + SwiftData-Cache für die Deck-Liste.
|
||||
/// View bindet sich an `state` und `errorMessage`.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class DeckListStore {
|
||||
enum State: Sendable {
|
||||
case idle
|
||||
case loading
|
||||
case loaded
|
||||
case failed
|
||||
}
|
||||
|
||||
private(set) var state: State = .idle
|
||||
private(set) var errorMessage: String?
|
||||
|
||||
private let api: CardsAPI
|
||||
private let context: ModelContext
|
||||
|
||||
init(auth: AuthClient, context: ModelContext) {
|
||||
api = CardsAPI(auth: auth)
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// Holt Decks vom Server, aktualisiert Cache. Bei Netzfehler bleibt
|
||||
/// der Cache (offline-readable).
|
||||
func refresh() async {
|
||||
state = .loading
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
let decks = try await api.listDecks()
|
||||
try await applyToCache(decks: decks)
|
||||
state = .loaded
|
||||
Log.sync.info("Loaded \(decks.count, privacy: .public) decks from server")
|
||||
} catch let error as AuthError {
|
||||
errorMessage = error.errorDescription
|
||||
state = .failed
|
||||
Log.sync.error("Deck refresh failed: \(error.localizedDescription, privacy: .public)")
|
||||
} catch {
|
||||
errorMessage = String(describing: error)
|
||||
state = .failed
|
||||
Log.sync.error("Deck refresh failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private func applyToCache(decks remoteDecks: [Deck]) async throws {
|
||||
let remoteIDs = Set(remoteDecks.map(\.id))
|
||||
|
||||
// 1. Bestehende Cache-Entries finden
|
||||
let descriptor = FetchDescriptor<CachedDeck>()
|
||||
let cached = (try? context.fetch(descriptor)) ?? []
|
||||
let cachedByID = Dictionary(uniqueKeysWithValues: cached.map { ($0.id, $0) })
|
||||
|
||||
// 2. Gelöschte Decks aus Cache entfernen
|
||||
for cachedDeck in cached where !remoteIDs.contains(cachedDeck.id) {
|
||||
context.delete(cachedDeck)
|
||||
}
|
||||
|
||||
// 3. Counts parallel holen
|
||||
let counts = await withTaskGroup(of: (String, Int, Int).self) { group in
|
||||
for deck in remoteDecks {
|
||||
group.addTask { [api] in
|
||||
async let cards = api.cardCount(deckId: deck.id)
|
||||
async let due = api.dueCount(deckId: deck.id)
|
||||
let cardCount = (try? await cards) ?? 0
|
||||
let dueCount = (try? await due) ?? 0
|
||||
return (deck.id, cardCount, dueCount)
|
||||
}
|
||||
}
|
||||
var result: [String: (cardCount: Int, dueCount: Int)] = [:]
|
||||
for await (id, c, d) in group {
|
||||
result[id] = (c, d)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 4. Neue/aktualisierte Decks einarbeiten
|
||||
for deck in remoteDecks {
|
||||
let counts = counts[deck.id] ?? (0, 0)
|
||||
if let existing = cachedByID[deck.id] {
|
||||
existing.update(from: deck, cardCount: counts.cardCount, dueCount: counts.dueCount)
|
||||
} else {
|
||||
let cachedDeck = CachedDeck(
|
||||
deck: deck,
|
||||
cardCount: counts.cardCount,
|
||||
dueCount: counts.dueCount
|
||||
)
|
||||
context.insert(cachedDeck)
|
||||
}
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue