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>
81 lines
1.8 KiB
Swift
81 lines
1.8 KiB
Swift
import SwiftUI
|
||
import WidgetKit
|
||
|
||
/// Large-Widget — bis zu 9 Favoriten als 3×3-Grid + Total-Count im
|
||
/// Footer. Tap auf Kachel öffnet App im Player.
|
||
struct LargeMoodsView: View {
|
||
let entry: MoodsEntry
|
||
|
||
private var moods: [WidgetMood] {
|
||
Array((entry.snapshot?.favorites ?? []).prefix(9))
|
||
}
|
||
|
||
private var totalCount: Int {
|
||
entry.snapshot?.favorites.count ?? 0
|
||
}
|
||
|
||
var body: some View {
|
||
if moods.isEmpty {
|
||
MediumMoodsView(entry: entry) // Reuse empty state
|
||
} else {
|
||
content
|
||
}
|
||
}
|
||
|
||
private var content: some View {
|
||
VStack(spacing: 6) {
|
||
LazyVGrid(
|
||
columns: Array(repeating: GridItem(.flexible(), spacing: 6), count: 3),
|
||
spacing: 6
|
||
) {
|
||
ForEach(moods) { mood in
|
||
Link(destination: URL(string: "moodlit://play/\(mood.id)")!) {
|
||
tile(mood)
|
||
}
|
||
}
|
||
}
|
||
footer
|
||
}
|
||
.padding(6)
|
||
}
|
||
|
||
private func tile(_ mood: WidgetMood) -> some View {
|
||
ZStack(alignment: .bottomLeading) {
|
||
LinearGradient(
|
||
colors: mood.colors.map { Color(hex: $0) },
|
||
startPoint: .topLeading,
|
||
endPoint: .bottomTrailing
|
||
)
|
||
LinearGradient(
|
||
colors: [.black.opacity(0.55), .clear],
|
||
startPoint: .bottom,
|
||
endPoint: .center
|
||
)
|
||
Text(mood.name)
|
||
.font(.caption2.weight(.semibold))
|
||
.foregroundStyle(.white)
|
||
.shadow(radius: 3)
|
||
.lineLimit(1)
|
||
.padding(.horizontal, 6)
|
||
.padding(.bottom, 4)
|
||
}
|
||
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
||
.aspectRatio(1, contentMode: .fit)
|
||
}
|
||
|
||
private var footer: some View {
|
||
HStack {
|
||
Image(systemName: "heart.fill")
|
||
.font(.caption2)
|
||
.foregroundStyle(.red.opacity(0.85))
|
||
Text("\(totalCount) Favoriten")
|
||
.font(.caption2)
|
||
.foregroundStyle(.white.opacity(0.7))
|
||
Spacer()
|
||
Text("Moodlit")
|
||
.font(.caption2.weight(.medium))
|
||
.foregroundStyle(.white.opacity(0.85))
|
||
}
|
||
.padding(.horizontal, 6)
|
||
}
|
||
}
|