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
130
Sources/Core/Domain/Deck.swift
Normal file
130
Sources/Core/Domain/Deck.swift
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue