Drei Sub-Pakete: Keyboard-Shortcuts, Daily-Reminder-Notifications, WidgetKit-Extension mit App-Group-Daten-Sharing. Siri-Shortcuts und Share-Extension auf β-7 verschoben — niedrige Priorität, die drei großen Brocken decken 90% des Native-Polish ab. Keyboard-Shortcuts: - Hidden Buttons in StudySessionView mit .keyboardShortcut - Space = flip, 1/2/3/4 = again/hard/good/easy - iPad-Magic-Keyboard + macOS-tauglich Daily-Reminders: - NotificationManager @Observable mit UNUserNotificationCenter - Authorization-State + Permission-Request-Flow - UNCalendarNotificationTrigger täglich zur konfigurierten Zeit - SettingsView in AccountView mit Toggle + DatePicker - UserDefaults-Persistierung von Hour/Minute/Enabled WidgetKit-Extension: - WidgetSnapshot Codable mit topDecks (Top-3 by dueCount) + totalDueCount - WidgetSnapshotStore schreibt in group.ev.mana.cards-Container - DeckListStore.refresh schreibt Snapshot + WidgetCenter.reloadAllTimelines - CardsWidgetExtension-Target im project.yml (app-extension) - CardsWidgetBundle + CardsDueWidget mit 5 Familien (small/medium/ accessoryCircular/accessoryInline/accessoryRectangular) - DueProvider TimelineProvider mit 30-min-Refresh - DueWidgetView Family-Switch - WidgetSnapshot.swift shared in beiden Targets via XcodeGen sources - App-Group im Haupt- und Widget-Entitlement 35 Tests grün (keine neuen Tests in β-6 — WidgetKit + Notifications sind System-API-Integrationen, Tests wären überwiegend Mocks). Build inkl. Widget-Extension grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
1.5 KiB
Swift
48 lines
1.5 KiB
Swift
import Foundation
|
|
import WidgetKit
|
|
|
|
/// Liest WidgetSnapshot aus dem shared App-Group-Container und schneidet
|
|
/// eine Timeline mit 30-Minuten-Refresh. Haupt-App ruft zusätzlich nach
|
|
/// jedem Refresh `WidgetCenter.shared.reloadAllTimelines()` auf, dann ist
|
|
/// das Update sofort sichtbar.
|
|
struct DueEntry: TimelineEntry, Sendable {
|
|
let date: Date
|
|
let totalDueCount: Int
|
|
let topDecks: [WidgetSnapshot.Entry]
|
|
let isPlaceholder: Bool
|
|
|
|
static let placeholder = DueEntry(
|
|
date: .now,
|
|
totalDueCount: 0,
|
|
topDecks: [],
|
|
isPlaceholder: true
|
|
)
|
|
}
|
|
|
|
struct DueProvider: TimelineProvider {
|
|
func placeholder(in _: Context) -> DueEntry {
|
|
.placeholder
|
|
}
|
|
|
|
func getSnapshot(in _: Context, completion: @escaping @Sendable (DueEntry) -> Void) {
|
|
completion(loadEntry())
|
|
}
|
|
|
|
func getTimeline(in _: Context, completion: @escaping @Sendable (Timeline<DueEntry>) -> Void) {
|
|
let entry = loadEntry()
|
|
let next = Calendar.current.date(byAdding: .minute, value: 30, to: .now) ?? .now
|
|
completion(Timeline(entries: [entry], policy: .after(next)))
|
|
}
|
|
|
|
private func loadEntry() -> DueEntry {
|
|
guard let snapshot = WidgetSnapshotStore.read() else {
|
|
return .placeholder
|
|
}
|
|
return DueEntry(
|
|
date: snapshot.updatedAt,
|
|
totalDueCount: snapshot.totalDueCount,
|
|
topDecks: snapshot.topDecks,
|
|
isPlaceholder: false
|
|
)
|
|
}
|
|
}
|