Home-Screen-Widgets für Pageta — drei Größen, jede liest aus dem
App-Group-Snapshot den die App nach jedem Login/onAppear refreshed.
Widget-Process macht keine Netzwerk-Calls (kein JWT-Refresh in einer
Extension mit 30s-Limit). Pattern aus herbatrium-native + seepuls-
native: App fetched + persistiert, Widget rendert read-only.
- Sources/Core/WidgetSnapshot.swift: WidgetSnapshot + WidgetFeedItem +
SavedCounts. Mirror von server-side /api/v1/snapshot/widget.
WidgetSnapshotStore mit UserDefaults(suiteName: group.ev.mana.pageta).
- Sources/Core/Sync/SnapshotSync.swift: ruft fetchWidgetSnapshot,
persistiert, triggert WidgetCenter.reloadAllTimelines(). Soft-Fail
bei Network — letzter Snapshot bleibt sichtbar.
- PagetaAPI.fetchWidgetSnapshot() neu.
- PagetaNativeApp.onAppear: zusätzlich zu flushPendingShares jetzt
syncWidgetSnapshot.
- Widgets/PagetaWidget/:
- PagetaWidgetBundle: @main, FeedWidget mit kind "ev.mana.pageta.feed",
3 system-Families, paper-Cream Container-Background.
- FeedProvider: getTimeline alle 1h Fallback, App überschreibt via
WidgetCenter. Preview-Snapshot mit 3 realistischen Items.
- SmallFeedView: Top-1 Item mit Site-Caption + Serif-Title, widgetURL
pageta://article/{id}.
- MediumFeedView: Top-3 mit Header + ungelesen-Count + Color-Bar.
- LargeFeedView: Top-3 als Link (pageta://) + CountsRow (unread/lese
als Pills oben rechts).
- WidgetTheme.swift: paper-Brand-Tokens dupliziert (Widget-Bundle
kann App-Modul nicht importieren).
- EmptyState: Fallback wenn kein Snapshot da.
- project.yml: PagetaWidgetExtension-Target (Bundle
ev.mana.pageta.widget, App-Group, embed in main App). Shared
WidgetSnapshot.swift in beiden Targets — DTO-Drift wird so
unmöglich.
2 neue Tests (WidgetSnapshot-Wire-Format-Decode + Store-Round-Trip),
25/25 grün, iOS+macOS Build green.
Apple-Dev-Portal-Pre für Real-Run:
- Bundle ev.mana.pageta.widget registrieren
- App-Group group.ev.mana.pageta für alle 3 Targets aktivieren
Phase-2 jetzt 4/4 (π-5..π-7+π-6) — nur noch π-8 Spotlight + π-9
macOS-Polish + π-10 ASC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
1.7 KiB
Swift
60 lines
1.7 KiB
Swift
import Foundation
|
|
|
|
/// Snapshot-DTO für die Widget-Surfaces. Spiegelt das Server-Response-
|
|
/// Shape von `GET /api/v1/snapshot/widget` (siehe
|
|
/// `pageta/apps/api/src/routes/snapshot.ts`).
|
|
///
|
|
/// **Architektur**: App fetched + schreibt nach App-Group-UserDefaults,
|
|
/// Widget liest read-only. Widget redet NIE direkt mit dem Server —
|
|
/// hätte sonst JWT-Refresh-Komplexität in einer Extension mit 30s-Limit.
|
|
struct WidgetSnapshot: Codable, Sendable, Equatable {
|
|
let feed: [WidgetFeedItem]
|
|
let saved: SavedCounts
|
|
let lastSyncAt: String
|
|
|
|
struct SavedCounts: Codable, Sendable, Equatable {
|
|
let unread: Int
|
|
let reading: Int
|
|
let finished: Int
|
|
let archived: Int
|
|
}
|
|
}
|
|
|
|
struct WidgetFeedItem: Codable, Sendable, Equatable, Identifiable {
|
|
let id: String
|
|
let title: String
|
|
let siteName: String
|
|
let topic: String
|
|
let imageUrl: String?
|
|
let publishedAt: String?
|
|
let url: String
|
|
}
|
|
|
|
/// Read/Write-Wrapper über UserDefaults im App-Group. Identische
|
|
/// Pattern wie `PendingShareStore`.
|
|
struct WidgetSnapshotStore: @unchecked Sendable {
|
|
// UserDefaults ist thread-safe (Apple Docs), aber nicht Sendable.
|
|
private let defaults: UserDefaults
|
|
private let key: String
|
|
|
|
static let storageKey = "pageta.widgetSnapshot.v1"
|
|
|
|
init(appGroup: String = "group.ev.mana.pageta", key: String = WidgetSnapshotStore.storageKey) {
|
|
self.defaults = UserDefaults(suiteName: appGroup) ?? .standard
|
|
self.key = key
|
|
}
|
|
|
|
func load() -> WidgetSnapshot? {
|
|
guard let data = defaults.data(forKey: key) else { return nil }
|
|
return try? JSONDecoder().decode(WidgetSnapshot.self, from: data)
|
|
}
|
|
|
|
func save(_ snapshot: WidgetSnapshot) throws {
|
|
let data = try JSONEncoder().encode(snapshot)
|
|
defaults.set(data, forKey: key)
|
|
}
|
|
|
|
func clear() {
|
|
defaults.removeObject(forKey: key)
|
|
}
|
|
}
|