// // ReflectLiveActivity.swift // ReflectWidget // // 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) .accessibilityHidden(true) Text("\(context.state.currentStreak)") .font(.title2.bold()) } .accessibilityElement(children: .combine) .accessibilityLabel(String(localized: "\(context.state.currentStreak) day streak")) } DynamicIslandExpandedRegion(.trailing) { if context.state.hasLoggedToday { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.title2) .accessibilityLabel(String(localized: "Mood logged today")) } 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) .accessibilityHidden(true) Text("Today: \(context.state.lastMoodLogged)") .font(.subheadline) } .accessibilityElement(children: .combine) } } } compactLeading: { Image(systemName: "flame.fill") .foregroundColor(.orange) .accessibilityLabel(String(localized: "Streak")) } compactTrailing: { Text("\(context.state.currentStreak)") .font(.caption.bold()) .accessibilityLabel(String(localized: "\(context.state.currentStreak) days")) } minimal: { Image(systemName: "flame.fill") .foregroundColor(.orange) .accessibilityLabel(String(localized: "Mood streak")) } } } } // MARK: - Lock Screen View struct MoodStreakLockScreenView: View { let context: ActivityViewContext var body: some View { HStack(spacing: 16) { // Streak indicator VStack(spacing: 4) { Image(systemName: "flame.fill") .font(.title) .foregroundColor(.orange) .accessibilityHidden(true) Text("\(context.state.currentStreak)") .font(.title.bold()) Text("day streak") .font(.caption) .foregroundColor(.secondary) } .accessibilityElement(children: .combine) .accessibilityLabel(String(localized: "\(context.state.currentStreak) day streak")) 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) .accessibilityHidden(true) VStack(alignment: .leading) { Text("Today's mood") .font(.caption) .foregroundColor(.secondary) Text(context.state.lastMoodLogged) .font(.headline) } } .accessibilityElement(children: .combine) } 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 }