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>
102 lines
3.1 KiB
Swift
102 lines
3.1 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
|
|
/// Family-Switch für das Cards-Due-Widget.
|
|
struct DueWidgetView: View {
|
|
let entry: DueEntry
|
|
|
|
@Environment(\.widgetFamily) private var family
|
|
|
|
var body: some View {
|
|
Group {
|
|
switch family {
|
|
case .systemSmall:
|
|
smallView
|
|
case .systemMedium:
|
|
mediumView
|
|
case .accessoryCircular:
|
|
circularView
|
|
case .accessoryInline:
|
|
inlineView
|
|
case .accessoryRectangular:
|
|
rectangularView
|
|
default:
|
|
smallView
|
|
}
|
|
}
|
|
}
|
|
|
|
private var smallView: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("\(entry.totalDueCount)")
|
|
.font(.system(size: 48, weight: .bold))
|
|
Text(entry.totalDueCount == 1 ? "Karte fällig" : "Karten fällig")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
if let top = entry.topDecks.first {
|
|
Text(top.name)
|
|
.font(.caption2)
|
|
.lineLimit(1)
|
|
.foregroundStyle(.primary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private var mediumView: some View {
|
|
HStack(alignment: .top, spacing: 16) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("\(entry.totalDueCount)")
|
|
.font(.system(size: 40, weight: .bold))
|
|
Text(entry.totalDueCount == 1 ? "Karte fällig" : "Karten fällig")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Divider()
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
ForEach(entry.topDecks.prefix(3)) { deck in
|
|
HStack {
|
|
Text(deck.name)
|
|
.font(.caption.weight(.medium))
|
|
.lineLimit(1)
|
|
Spacer()
|
|
Text("\(deck.dueCount)")
|
|
.font(.caption.weight(.bold))
|
|
}
|
|
}
|
|
if entry.topDecks.isEmpty {
|
|
Text("Keine Decks")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
private var circularView: some View {
|
|
ZStack {
|
|
Circle()
|
|
.fill(.tint.opacity(0.2))
|
|
Text("\(entry.totalDueCount)")
|
|
.font(.headline.bold())
|
|
}
|
|
}
|
|
|
|
private var inlineView: some View {
|
|
Text("Cards: \(entry.totalDueCount) fällig")
|
|
}
|
|
|
|
private var rectangularView: some View {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("\(entry.totalDueCount) fällig")
|
|
.font(.headline)
|
|
if let top = entry.topDecks.first {
|
|
Text(top.name)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|