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>
79 lines
2.6 KiB
Swift
79 lines
2.6 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
/// Lokales Cache-Model für Decks. Spiegelt das Server-DTO + zwei
|
|
/// computed Werte (cardCount, dueCount), die Web pro Deck als zusätzliche
|
|
/// API-Calls holt.
|
|
///
|
|
/// Offline-Read: Liste sichtbar ohne Netz. Server bleibt Wahrheit —
|
|
/// alle Edits laufen über die API, der Cache wird nur beim Re-Fetch
|
|
/// aktualisiert.
|
|
@Model
|
|
final class CachedDeck {
|
|
@Attribute(.unique) var id: String
|
|
var userId: String
|
|
var name: String
|
|
var deckDescription: String?
|
|
var color: String?
|
|
var categoryRaw: String?
|
|
var visibilityRaw: String
|
|
var contentHash: String?
|
|
var forkedFromMarketplaceDeckId: String?
|
|
var forkedFromMarketplaceVersionId: String?
|
|
var archivedAt: Date?
|
|
var createdAt: Date
|
|
var updatedAt: Date
|
|
|
|
/// Anzahl Karten im Deck (über `/api/v1/cards?deck_id=...`).
|
|
var cardCount: Int = 0
|
|
|
|
/// Anzahl fälliger Reviews (über `/api/v1/reviews/due?deck_id=...`).
|
|
var dueCount: Int = 0
|
|
|
|
/// Zeitpunkt des letzten erfolgreichen Server-Pulls für dieses Deck.
|
|
var lastFetchedAt: Date
|
|
|
|
init(deck: Deck, cardCount: Int = 0, dueCount: Int = 0) {
|
|
id = deck.id
|
|
userId = deck.userId
|
|
name = deck.name
|
|
deckDescription = deck.description
|
|
color = deck.color
|
|
categoryRaw = deck.category?.rawValue
|
|
visibilityRaw = deck.visibility.rawValue
|
|
contentHash = deck.contentHash
|
|
forkedFromMarketplaceDeckId = deck.forkedFromMarketplaceDeckId
|
|
forkedFromMarketplaceVersionId = deck.forkedFromMarketplaceVersionId
|
|
archivedAt = deck.archivedAt
|
|
createdAt = deck.createdAt
|
|
updatedAt = deck.updatedAt
|
|
self.cardCount = cardCount
|
|
self.dueCount = dueCount
|
|
lastFetchedAt = .now
|
|
}
|
|
|
|
/// Übernimmt aktualisierte Felder vom Server-DTO.
|
|
func update(from deck: Deck, cardCount: Int, dueCount: Int) {
|
|
name = deck.name
|
|
deckDescription = deck.description
|
|
color = deck.color
|
|
categoryRaw = deck.category?.rawValue
|
|
visibilityRaw = deck.visibility.rawValue
|
|
contentHash = deck.contentHash
|
|
forkedFromMarketplaceDeckId = deck.forkedFromMarketplaceDeckId
|
|
forkedFromMarketplaceVersionId = deck.forkedFromMarketplaceVersionId
|
|
archivedAt = deck.archivedAt
|
|
updatedAt = deck.updatedAt
|
|
self.cardCount = cardCount
|
|
self.dueCount = dueCount
|
|
lastFetchedAt = .now
|
|
}
|
|
|
|
var category: DeckCategory? {
|
|
categoryRaw.flatMap(DeckCategory.init(rawValue:))
|
|
}
|
|
|
|
var isFromMarketplace: Bool {
|
|
forkedFromMarketplaceDeckId != nil
|
|
}
|
|
}
|