WidgetSnapshot-Bridge App ↔ Widget via App-Group-UserDefaults (`group.ev.mana.moodlit`). MoodStore.refreshWidgetSnapshot läuft nach loadAll + toggleFavorite und pingt WidgetCenter. Widget-Extension (`ev.mana.moodlit.widget`, iOS-only app-extension): - Small: Last-Played oder erstes Favorit als Glow-Tile + Name + Animation-Slug - Medium: 2×2-Grid, bis zu 4 Favoriten, jede Kachel hat eigene Link-Destination zum App-Player - Large: 3×3-Grid, bis zu 9 Favoriten + Footer mit Total-Count - Empty-State, wenn keine Favoriten gesetzt sind Deep-Links: - `moodlit://play/<id>` (Custom-Scheme aus Widget-Tap): `url.host == "play"`, ID aus pathComponents - `https://moodlit.mana.how/play/<id>` (Universal-Link via AASA): pathComponents == ["/", "play", "<id>"] Beide öffnen MoodPlayerView als fullScreenCover direkt auf RootView (unabhängig vom aktiven Tab). Wegen Widget-Target-Sharing: `Mood.swiftUIColors` aus HexColor.swift nach Mood+SwiftUI.swift extrahiert (Widget kennt den Mood-Type nicht). xcodebuild iOS-Sim + macOS beide BUILD SUCCEEDED. Widget-Extension korrekt eingebettet in `MoodlitNative.app/PlugIns/`. 11 Unit-Tests weiter grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.9 KiB
Swift
87 lines
2.9 KiB
Swift
import SwiftUI
|
||
import WidgetKit
|
||
|
||
/// Widget-Bundle für Moodlit. Ein Widget mit drei Größen:
|
||
/// - small: zuletzt gespieltes Mood oder erstes Favorit als Glow-Tile
|
||
/// - medium: bis zu 4 Favoriten als Grid
|
||
/// - large: bis zu 9 Favoriten als 3×3-Grid + Count
|
||
///
|
||
/// Tap-Aktion: `widgetURL(moodlit://play/<id>)` öffnet die App im
|
||
/// MoodPlayerView für das jeweilige Mood.
|
||
@main
|
||
struct MoodlitWidgetBundle: WidgetBundle {
|
||
var body: some Widget {
|
||
MoodsWidget()
|
||
}
|
||
}
|
||
|
||
struct MoodsWidget: Widget {
|
||
let kind: String = "ev.mana.moodlit.moods"
|
||
|
||
var body: some WidgetConfiguration {
|
||
StaticConfiguration(kind: kind, provider: MoodsProvider()) { entry in
|
||
MoodsEntryView(entry: entry)
|
||
.containerBackground(.black, for: .widget)
|
||
.widgetURL(URL(string: "moodlit://"))
|
||
}
|
||
.configurationDisplayName("Meine Moods")
|
||
.description("Schnellzugriff auf deine Lieblings-Stimmungen.")
|
||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||
}
|
||
}
|
||
|
||
struct MoodsEntry: TimelineEntry {
|
||
let date: Date
|
||
let snapshot: WidgetSnapshot?
|
||
}
|
||
|
||
struct MoodsProvider: TimelineProvider {
|
||
func placeholder(in context: Context) -> MoodsEntry {
|
||
MoodsEntry(date: Date(), snapshot: previewSnapshot())
|
||
}
|
||
|
||
func getSnapshot(in context: Context, completion: @escaping (MoodsEntry) -> Void) {
|
||
let snap = WidgetSnapshotStore.read() ?? (context.isPreview ? previewSnapshot() : nil)
|
||
completion(MoodsEntry(date: Date(), snapshot: snap))
|
||
}
|
||
|
||
func getTimeline(in context: Context, completion: @escaping (Timeline<MoodsEntry>) -> Void) {
|
||
let snap = WidgetSnapshotStore.read()
|
||
let entry = MoodsEntry(date: Date(), snapshot: snap)
|
||
// Refresh-Heartbeat alle 4h; App ruft `WidgetCenter.shared.
|
||
// reloadAllTimelines()` bei jedem `loadAll`/`toggleFavorite` —
|
||
// das überschreibt diesen Heartbeat in der Praxis.
|
||
let next = Calendar.current.date(byAdding: .hour, value: 4, to: Date()) ?? Date()
|
||
completion(Timeline(entries: [entry], policy: .after(next)))
|
||
}
|
||
|
||
private func previewSnapshot() -> WidgetSnapshot {
|
||
WidgetSnapshot(
|
||
favorites: [
|
||
WidgetMood(id: "fire", name: "Fire",
|
||
colors: ["#ff6b35", "#ff4500", "#dc143c", "#8b0000"], animation: "candle"),
|
||
WidgetMood(id: "ocean", name: "Ocean",
|
||
colors: ["#48dbfb", "#0abde3", "#10ac84", "#1dd1a1"], animation: "wave"),
|
||
WidgetMood(id: "midnight", name: "Midnight",
|
||
colors: ["#0c0c0c", "#1a1a2e", "#16213e", "#0f3460"], animation: "breath"),
|
||
],
|
||
lastPlayed: WidgetMood(id: "fire", name: "Fire",
|
||
colors: ["#ff6b35", "#ff4500", "#dc143c", "#8b0000"], animation: "candle"),
|
||
updatedAt: Date()
|
||
)
|
||
}
|
||
}
|
||
|
||
struct MoodsEntryView: View {
|
||
@Environment(\.widgetFamily) private var family
|
||
let entry: MoodsEntry
|
||
|
||
var body: some View {
|
||
switch family {
|
||
case .systemSmall: SmallMoodsView(entry: entry)
|
||
case .systemMedium: MediumMoodsView(entry: entry)
|
||
case .systemLarge: LargeMoodsView(entry: entry)
|
||
default: SmallMoodsView(entry: entry)
|
||
}
|
||
}
|
||
}
|