Bug-Fixes: - ArticleDetailView: ScrollView.contentMargins(.bottom, 60) — die iOS-18+-floating Tab-Bar verdeckte sonst die letzten Zeilen - Empty-State-Copy entstaubt (kein „kommt mit π-6" mehr) - Consent-Wall-Warning: ArticleExtractPreview decodet jetzt `warning`, persistiert via ArticleSavedPayload + ArticleState, Reader rendert Banner mit „Original im Browser öffnen"-Link wenn warning == "probable_consent_wall" - AsyncImage-Placeholder von paper-cream auf Color.secondary.opacity(0.15) — sah im Dark-Mode wie helles Quadrat aus - ArticleStatus.reading.label: „Lese" → „Liest" (Imperativ → Verb) Lese-Liste-Umbau (Tills Feedback): - Statt 4-Tab-Picker jetzt ein Single-Scroll mit Sections: Lesend → Ungelesen → Gelesen. Sticky-Header via List/Section. - Archiv ist eigene Seite (ArchivedListView), erreichbar via Toolbar-Icon rechts. Aus dem Haupt-Scroll raus, damit's nicht permanent stört. - Restore-Action im Archiv (Swipe → Wiederherstellen → status=unread) Feed-Polish: - FeedItemRow ist jetzt tappbar — Tap pusht in FeedItemReaderView (rendert FeedItem.content direkt aus Pool ohne Save-Roundtrip). Toolbar-Action „In Lese-Liste speichern" macht Save explizit. - Swipe-Action „In Lese-Liste" als Quick-Save direkt in der Row. ArticleSummarySection: Empty-State jetzt kompakter Pill-Trigger statt großer Card mit leerem Header. Volle Card entsteht erst nach Generieren (oder bei Error/Loading). Toolbar: ellipsis.circle → ellipsis (iOS 18+ rendert Toolbar-Items in Glass-Capsules, .circle ergab doppelten Kreis). iOS-Sim BUILD SUCCEEDED, 38/38 Tests grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
2.7 KiB
Swift
110 lines
2.7 KiB
Swift
import Foundation
|
|
|
|
// Projected state types — Swift-Spiegel der TypeScript-State-Shapes in
|
|
// `apps/web/src/lib/event-sync-api.ts` und der Reducer in
|
|
// `mana/packages/shared-schemas/src/pageta/events/*.ts`.
|
|
//
|
|
// Diese Typen sind das Ergebnis des Reducer-Replays über alle Events
|
|
// eines Aggregates. Die UI sieht den Wire-Type `Article`/`Highlight`
|
|
// aus `Core/Domain/`; `EventSyncCoordinator` mappt State → Wire.
|
|
|
|
// MARK: - Article
|
|
|
|
/// Lese-Listen-Artikel als event-sourced State.
|
|
///
|
|
/// `content` enthält den Mozilla-Readability-Plain-Text aus dem
|
|
/// HTTP-`extract/preview`-Schritt, der weiterhin pageta-api lebt. Alle
|
|
/// User-Mutationen (Status/Progress/Favorite/Note/Delete) sind Events.
|
|
///
|
|
/// `urlHash` wird vom Native-Client als SHA-256-Hex der `originalUrl`
|
|
/// berechnet — matched dem Web-`urlHash`, damit Duplicate-Check über
|
|
/// alle Clients konsistent ist.
|
|
struct ArticleState: Codable, Equatable {
|
|
var id: String
|
|
var userId: String
|
|
var originalUrl: String
|
|
var urlHash: String
|
|
var title: String
|
|
var excerpt: String?
|
|
var content: String
|
|
var author: String?
|
|
var siteName: String?
|
|
var imageUrl: String?
|
|
var wordCount: Int?
|
|
var readingTimeMinutes: Int?
|
|
var publishedAt: String?
|
|
var status: ArticleStatus
|
|
var readingProgress: Double
|
|
var isFavorite: Bool
|
|
var userNote: String?
|
|
var tags: [String]
|
|
var savedAt: String
|
|
var readAt: String?
|
|
var deletedAt: String?
|
|
/// Vom Server-Extract gesetzt (`"probable_consent_wall"` etc.).
|
|
/// Bleibt im State, damit das UI nach Replay den Banner zeigt.
|
|
var warning: String?
|
|
|
|
static func initial() -> ArticleState {
|
|
ArticleState(
|
|
id: "",
|
|
userId: "",
|
|
originalUrl: "",
|
|
urlHash: "",
|
|
title: "",
|
|
excerpt: nil,
|
|
content: "",
|
|
author: nil,
|
|
siteName: nil,
|
|
imageUrl: nil,
|
|
wordCount: nil,
|
|
readingTimeMinutes: nil,
|
|
publishedAt: nil,
|
|
status: .unread,
|
|
readingProgress: 0,
|
|
isFavorite: false,
|
|
userNote: nil,
|
|
tags: [],
|
|
savedAt: "",
|
|
readAt: nil,
|
|
deletedAt: nil,
|
|
warning: nil
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Highlight
|
|
|
|
/// Highlight-State. Web-Client setzt echte Offsets, Native setzt 0/0
|
|
/// (Paragraph-Granularität). `text` ist führend für Anzeige + Lookup.
|
|
struct HighlightEventState: Codable, Equatable {
|
|
var id: String
|
|
var userId: String
|
|
var articleId: String
|
|
var text: String
|
|
var note: String?
|
|
var color: HighlightColor
|
|
var startOffset: Int
|
|
var endOffset: Int
|
|
var contextBefore: String?
|
|
var contextAfter: String?
|
|
var createdAt: String
|
|
var deletedAt: String?
|
|
|
|
static func initial() -> HighlightEventState {
|
|
HighlightEventState(
|
|
id: "",
|
|
userId: "",
|
|
articleId: "",
|
|
text: "",
|
|
note: nil,
|
|
color: .yellow,
|
|
startOffset: 0,
|
|
endOffset: 0,
|
|
contextBefore: nil,
|
|
contextAfter: nil,
|
|
createdAt: "",
|
|
deletedAt: nil
|
|
)
|
|
}
|
|
}
|