pageta-native/Sources/Core/WidgetSnapshot.swift
Till JS 934ccf5312 π-7: WidgetKit (Small/Medium/Large + App-Group-Snapshot)
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>
2026-05-18 17:09:25 +02:00

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)
}
}