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>
130 lines
3.8 KiB
Swift
130 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
/// Deck-DTO. Wire-Format aus `cards/apps/api/src/lib/dto.ts:toDeckDto`.
|
|
/// snake_case-Felder via `CodingKeys`, Optionals explizit nullable.
|
|
struct Deck: Codable, Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let userId: String
|
|
let name: String
|
|
let description: String?
|
|
let color: String?
|
|
let category: DeckCategory?
|
|
let visibility: DeckVisibility
|
|
let fsrsSettings: FsrsSettings
|
|
let contentHash: String?
|
|
let forkedFromMarketplaceDeckId: String?
|
|
let forkedFromMarketplaceVersionId: String?
|
|
let archivedAt: Date?
|
|
let createdAt: Date
|
|
let updatedAt: Date
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case userId = "user_id"
|
|
case name
|
|
case description
|
|
case color
|
|
case category
|
|
case visibility
|
|
case fsrsSettings = "fsrs_settings"
|
|
case contentHash = "content_hash"
|
|
case forkedFromMarketplaceDeckId = "forked_from_marketplace_deck_id"
|
|
case forkedFromMarketplaceVersionId = "forked_from_marketplace_version_id"
|
|
case archivedAt = "archived_at"
|
|
case createdAt = "created_at"
|
|
case updatedAt = "updated_at"
|
|
}
|
|
|
|
/// Geforkt aus dem Cardecky-Marketplace?
|
|
var isFromMarketplace: Bool {
|
|
forkedFromMarketplaceDeckId != nil
|
|
}
|
|
}
|
|
|
|
enum DeckVisibility: String, Codable, Sendable {
|
|
case `private`
|
|
case space
|
|
case `public`
|
|
}
|
|
|
|
/// Aus `cards/packages/cards-domain/src/schemas/deck.ts:DECK_CATEGORY_IDS`.
|
|
enum DeckCategory: String, Codable, Sendable, CaseIterable {
|
|
case language
|
|
case medicine
|
|
case science
|
|
case math
|
|
case history
|
|
case law
|
|
case technology
|
|
case arts
|
|
case music
|
|
case sport
|
|
case other
|
|
|
|
/// Deutsche Labels aus `DECK_CATEGORY_LABELS`.
|
|
var label: String {
|
|
switch self {
|
|
case .language: "Sprache"
|
|
case .medicine: "Medizin"
|
|
case .science: "Wissenschaft"
|
|
case .math: "Mathematik"
|
|
case .history: "Geschichte"
|
|
case .law: "Recht"
|
|
case .technology: "Technik"
|
|
case .arts: "Kunst"
|
|
case .music: "Musik"
|
|
case .sport: "Sport"
|
|
case .other: "Sonstiges"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// FSRS-Settings — Native bleibt schematisch agnostisch, FSRS rechnet
|
|
/// nur der Server. Wir behalten die Felder als roh-JSON, damit eine
|
|
/// neue Setting auf dem Server uns nicht bricht.
|
|
struct FsrsSettings: Codable, Sendable, Hashable {
|
|
let requestRetention: Double?
|
|
let maximumInterval: Int?
|
|
let enableFuzz: Bool?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case requestRetention = "request_retention"
|
|
case maximumInterval = "maximum_interval"
|
|
case enableFuzz = "enable_fuzz"
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
requestRetention = try container.decodeIfPresent(Double.self, forKey: .requestRetention)
|
|
maximumInterval = try container.decodeIfPresent(Int.self, forKey: .maximumInterval)
|
|
enableFuzz = try container.decodeIfPresent(Bool.self, forKey: .enableFuzz)
|
|
}
|
|
|
|
static let empty = FsrsSettings()
|
|
|
|
private init(
|
|
requestRetention: Double? = nil,
|
|
maximumInterval: Int? = nil,
|
|
enableFuzz: Bool? = nil
|
|
) {
|
|
self.requestRetention = requestRetention
|
|
self.maximumInterval = maximumInterval
|
|
self.enableFuzz = enableFuzz
|
|
}
|
|
}
|
|
|
|
/// Server-Response von `GET /api/v1/decks`.
|
|
struct DeckListResponse: Decodable, Sendable {
|
|
let decks: [Deck]
|
|
let total: Int
|
|
}
|
|
|
|
/// Server-Response von `GET /api/v1/cards?deck_id=...`.
|
|
struct CardListResponse: Decodable, Sendable {
|
|
let total: Int
|
|
}
|
|
|
|
/// Server-Response von `GET /api/v1/reviews/due?deck_id=...`.
|
|
struct DueReviewsResponse: Decodable, Sendable {
|
|
let total: Int
|
|
}
|