Files
Reflect/FeelsWidget2/FeelsLiveActivity.swift
Trey t 74dc289a3d Fix Live Activity streak messaging and mislabeled widget text
Show "Start your streak!" instead of "Don't break your streak!" when
streak count is zero, and fix small widget incorrectly labeling total
entries as "day streak".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 23:03:09 -06:00

266 lines
9.4 KiB
Swift

//
// FeelsLiveActivity.swift
// FeelsWidget
//
// Live Activity for mood streak tracking (Dynamic Island + Lock Screen)
//
import WidgetKit
import SwiftUI
import ActivityKit
// MARK: - Live Activity Widget
// Note: MoodStreakAttributes is defined in MoodStreakActivity.swift (Shared folder)
struct MoodStreakLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: MoodStreakAttributes.self) { context in
// Lock Screen / StandBy view
MoodStreakLockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded view
DynamicIslandExpandedRegion(.leading) {
HStack(spacing: 8) {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
Text("\(context.state.currentStreak)")
.font(.title2.bold())
}
}
DynamicIslandExpandedRegion(.trailing) {
if context.state.hasLoggedToday {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.title2)
} else {
Text("Log now")
.font(.caption)
.foregroundColor(.secondary)
}
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.hasLoggedToday ? "Streak: \(context.state.currentStreak) days" : (context.state.currentStreak > 0 ? "Don't break your streak!" : "Start your streak!"))
.font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
if !context.state.hasLoggedToday {
Text("Voting closes at midnight")
.font(.caption)
.foregroundColor(.secondary)
} else {
HStack {
Circle()
.fill(Color(hex: context.state.lastMoodColor))
.frame(width: 20, height: 20)
Text("Today: \(context.state.lastMoodLogged)")
.font(.subheadline)
}
}
}
} compactLeading: {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
} compactTrailing: {
Text("\(context.state.currentStreak)")
.font(.caption.bold())
} minimal: {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
}
}
}
}
// MARK: - Lock Screen View
struct MoodStreakLockScreenView: View {
let context: ActivityViewContext<MoodStreakAttributes>
var body: some View {
HStack(spacing: 16) {
// Streak indicator
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("\(context.state.currentStreak)")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
// Status
VStack(alignment: .leading, spacing: 8) {
if context.state.hasLoggedToday {
HStack(spacing: 8) {
Circle()
.fill(Color(hex: context.state.lastMoodColor))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text(context.state.lastMoodLogged)
.font(.headline)
}
}
} else {
VStack(alignment: .leading) {
Text(context.state.currentStreak > 0 ? "Don't break your streak!" : "Start your streak!")
.font(.headline)
Text("Tap to log your mood")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
Spacer()
}
.padding()
.activityBackgroundTint(Color(.systemBackground).opacity(0.8))
}
}
// MARK: - Preview Sample Data
extension MoodStreakAttributes {
static var preview: MoodStreakAttributes {
MoodStreakAttributes(startDate: Date())
}
}
extension MoodStreakAttributes.ContentState {
static var notLogged: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 7,
lastMoodLogged: "None",
lastMoodColor: "#888888",
hasLoggedToday: false,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedGreat: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 15,
lastMoodLogged: "Great",
lastMoodColor: MoodTints.Default.color(forMood: .great).toHex() ?? "#4CAF50",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedGood: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 30,
lastMoodLogged: "Good",
lastMoodColor: MoodTints.Default.color(forMood: .good).toHex() ?? "#8BC34A",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedAverage: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 10,
lastMoodLogged: "Average",
lastMoodColor: MoodTints.Default.color(forMood: .average).toHex() ?? "#FFC107",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedBad: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 5,
lastMoodLogged: "Bad",
lastMoodColor: MoodTints.Default.color(forMood: .bad).toHex() ?? "#FF9800",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedHorrible: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 3,
lastMoodLogged: "Horrible",
lastMoodColor: MoodTints.Default.color(forMood: .horrible).toHex() ?? "#F44336",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
}
// MARK: - Live Activity Previews
#Preview("Lock Screen - Not Logged", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.notLogged
}
#Preview("Lock Screen - Great", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}
#Preview("Lock Screen - Good", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGood
}
#Preview("Lock Screen - Average", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedAverage
}
#Preview("Lock Screen - Bad", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedBad
}
#Preview("Lock Screen - Horrible", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedHorrible
}
// MARK: - Dynamic Island Previews
#Preview("Dynamic Island Expanded - Not Logged", as: .dynamicIsland(.expanded), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.notLogged
}
#Preview("Dynamic Island Expanded - Logged", as: .dynamicIsland(.expanded), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}
#Preview("Dynamic Island Compact", as: .dynamicIsland(.compact), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}
#Preview("Dynamic Island Minimal", as: .dynamicIsland(.minimal), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}