moodlit-native/Sources/Core/Sync/WidgetSnapshot.swift
till 03dca7d84d μ-7.3: Widget (Small/Medium/Large) + Deep-Link-Handling
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>
2026-05-18 15:21:55 +02:00

64 lines
2.2 KiB
Swift

import Foundation
/// Schreib-Lese-Bridge zwischen App und Widget-Extension via shared
/// App-Group-UserDefaults.
///
/// Die App schreibt nach jedem `MoodStore.loadAll`-Refresh und nach
/// `toggleFavorite`; das Widget liest in seinem Timeline-Provider.
///
/// Bewusst flach gehalten: kein SwiftData / kein Core Data ein
/// Codable-Dokument in einem Key reicht für ~10 Favoriten + Last-
/// Played. Größenlimit App-Group-UserDefaults ist locker genug.
public struct WidgetSnapshot: Codable, Equatable, Sendable {
public let favorites: [WidgetMood]
public let lastPlayed: WidgetMood?
public let updatedAt: Date
public static let appGroup = "group.ev.mana.moodlit"
public static let storageKey = "moodlit.widgetSnapshot"
public init(favorites: [WidgetMood], lastPlayed: WidgetMood?, updatedAt: Date = Date()) {
self.favorites = favorites
self.lastPlayed = lastPlayed
self.updatedAt = updatedAt
}
}
/// Minimal-Repräsentation für Widgets. Hex-Strings statt SwiftUI
/// Color (Widget-Process kann keine `Color` aus App-Bundle-Resources
/// dereferenzieren) und nur der Animation-Slug.
public struct WidgetMood: Codable, Equatable, Identifiable, Sendable {
public let id: String
public let name: String
public let colors: [String]
public let animation: String
public init(id: String, name: String, colors: [String], animation: String) {
self.id = id
self.name = name
self.colors = colors
self.animation = animation
}
}
/// Lesen/Schreiben des Snapshots aus dem App-Group-Container.
/// Beide Seiten (App + Widget) konsumieren `WidgetSnapshot.appGroup`.
public enum WidgetSnapshotStore {
private static var defaults: UserDefaults? {
UserDefaults(suiteName: WidgetSnapshot.appGroup)
}
public static func read() -> WidgetSnapshot? {
guard let data = defaults?.data(forKey: WidgetSnapshot.storageKey) else { return nil }
return try? JSONDecoder().decode(WidgetSnapshot.self, from: data)
}
public static func write(_ snapshot: WidgetSnapshot) {
guard let data = try? JSONEncoder().encode(snapshot) else { return }
defaults?.set(data, forKey: WidgetSnapshot.storageKey)
}
public static func clear() {
defaults?.removeObject(forKey: WidgetSnapshot.storageKey)
}
}