// // FeelsWidget.swift // FeelsWidget // // Created by Trey Tartt on 1/7/22. // import WidgetKit import SwiftUI import Intents import SwiftData import ActivityKit import AppIntents // 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" : "Don't break 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) } } } } 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) 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("Don't break your streak!") .font(.headline) Text("Tap to log your mood") .font(.caption) .foregroundColor(.secondary) } } } Spacer() } .padding() .activityBackgroundTint(Color(.systemBackground).opacity(0.8)) } } class WatchTimelineView: Identifiable { let id = UUID() let image: Image let graphic: Image let date: Date let color: Color let secondaryColor: Color init(image: Image, graphic: Image, date: Date, color: Color, secondaryColor: Color) { self.image = image self.date = date self.color = color self.graphic = graphic self.secondaryColor = secondaryColor } } struct TimeLineCreator { @MainActor static func createViews(daysBack: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let dates = Array(0...daysBack).map({ Calendar.current.date(byAdding: .day, value: -$0, to: latestDayToShow)! }) // Use WidgetDataProvider for isolated widget data access let dataProvider = WidgetDataProvider.shared for date in dates { let dayStart = Calendar.current.startOfDay(for: date) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() if let todayEntry = dataProvider.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood), graphic: moodImages.icon(forMood: todayEntry.mood), date: dayStart, color: moodTint.color(forMood: todayEntry.mood), secondaryColor: moodTint.secondary(forMood: todayEntry.mood))) } else { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing), graphic: moodImages.icon(forMood: .missing), date: dayStart, color: moodTint.color(forMood: .missing), secondaryColor: moodTint.secondary(forMood: .missing))) } } timeLineView = timeLineView.sorted(by: { $0.date > $1.date }) return timeLineView } /// Creates sample preview data for widget picker - shows what widget looks like with mood data static func createSampleViews(count: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() let sampleMoods: [Mood] = [.great, .good, .average, .good, .great, .average, .bad, .good, .great, .good, .average] let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() for i in 0.. SimpleEntry { return SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: TimeLineCreator.createSampleViews(count: 10)) } @MainActor func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { // Use sample data for widget picker preview, real data otherwise let timeLineViews: [WatchTimelineView] if context.isPreview { timeLineViews = TimeLineCreator.createSampleViews(count: 10) } else { timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) } let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus() let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: timeLineViews, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, promptText: promptText) completion(entry) } @MainActor func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus() let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!, configuration: ConfigurationIntent(), timeLineViews: nil, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, promptText: promptText) let midNightEntry = SimpleEntry(date: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())!, configuration: ConfigurationIntent(), timeLineViews: nil, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, promptText: promptText) let date = Calendar.current.date(byAdding: .second, value: 10, to: Date())! let timeline = Timeline(entries: [entry, midNightEntry], policy: .after(date)) completion(timeline) } @MainActor private func checkSubscriptionAndVoteStatus() -> (hasSubscription: Bool, hasVotedToday: Bool, promptText: String) { let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let dayStart = Calendar.current.startOfDay(for: votingDate) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart) ?? dayStart // Use WidgetDataProvider for isolated widget data access let todayEntry = WidgetDataProvider.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body return (hasSubscription, hasVotedToday, promptText) } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent let timeLineViews: [WatchTimelineView]? let showStats: Bool let hasSubscription: Bool let hasVotedToday: Bool let promptText: String init(date: Date, configuration: ConfigurationIntent, timeLineViews: [WatchTimelineView]?, showStats: Bool = false, hasSubscription: Bool = false, hasVotedToday: Bool = true, promptText: String = "") { self.date = date self.configuration = configuration self.timeLineViews = timeLineViews self.showStats = showStats self.hasSubscription = hasSubscription self.hasVotedToday = hasVotedToday self.promptText = promptText } } /**********************************************************/ struct FeelsWidgetEntryView : View { @Environment(\.sizeCategory) var sizeCategory @Environment(\.widgetFamily) var family var entry: Provider.Entry private var showVotingForToday: Bool { entry.hasSubscription && !entry.hasVotedToday } @ViewBuilder var body: some View { Group { switch family { case .systemSmall: SmallWidgetView(entry: entry) case .systemMedium: MediumWidgetView(entry: entry) case .systemLarge: LargeWidgetView(entry: entry) case .systemExtraLarge: LargeWidgetView(entry: entry) case .accessoryCircular, .accessoryRectangular, .accessoryInline: SmallWidgetView(entry: entry) @unknown default: MediumWidgetView(entry: entry) } } .containerBackground(showVotingForToday ? Color.clear : Color(UIColor.systemBackground), for: .widget) } } struct SmallWidgetView: View { var entry: Provider.Entry var todayView: WatchTimelineView? private var dayFormatter: DateFormatter { let f = DateFormatter() f.dateFormat = "EEEE" return f } private var dateFormatter: DateFormatter { let f = DateFormatter() f.dateFormat = "MMM d" return f } init(entry: Provider.Entry) { self.entry = entry let realData = TimeLineCreator.createViews(daysBack: 2) let hasRealData = realData.contains { view in let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } todayView = hasRealData ? realData.first : TimeLineCreator.createSampleViews(count: 1).first } var body: some View { if let today = todayView { VStack(spacing: 0) { Spacer() // Large mood icon today.image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 70, height: 70) .foregroundColor(today.color) Spacer() .frame(height: 12) // Date info VStack(spacing: 2) { Text(dayFormatter.string(from: today.date)) .font(.caption2.weight(.medium)) .foregroundStyle(.secondary) .textCase(.uppercase) Text(dateFormatter.string(from: today.date)) .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) } Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } } struct MediumWidgetView: View { var entry: Provider.Entry var timeLineView = [WatchTimelineView]() private var showVotingForToday: Bool { entry.hasSubscription && !entry.hasVotedToday } private var dayFormatter: DateFormatter { let f = DateFormatter() f.dateFormat = "EEE" return f } private var dateFormatter: DateFormatter { let f = DateFormatter() f.dateFormat = "d" return f } init(entry: Provider.Entry) { self.entry = entry let realData = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5)) let hasRealData = realData.contains { view in let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 5) } private var headerDateRange: String { guard let first = timeLineView.first, let last = timeLineView.last else { return "" } let formatter = DateFormatter() formatter.dateFormat = "MMM d" return "\(formatter.string(from: last.date)) - \(formatter.string(from: first.date))" } var body: some View { GeometryReader { geo in let cellHeight = geo.size.height - 36 VStack(spacing: 4) { // Header HStack { Text("Last 5 Days") .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) Text("ยท") .foregroundStyle(.secondary) Text(headerDateRange) .font(.caption) .foregroundStyle(.secondary) Spacer() } .padding(.horizontal, 14) .padding(.top, 10) // Single row of 5 days HStack(spacing: 8) { ForEach(Array(timeLineView.enumerated()), id: \.element.id) { index, item in MediumDayCell( dayLabel: dayFormatter.string(from: item.date), dateLabel: dateFormatter.string(from: item.date), image: item.image, color: item.color, isToday: index == 0, height: cellHeight ) } } .padding(.horizontal, 10) .padding(.bottom, 10) } } } } // MARK: - Day Cell for Medium Widget struct MediumDayCell: View { let dayLabel: String let dateLabel: String let image: Image let color: Color let isToday: Bool let height: CGFloat var body: some View { ZStack { RoundedRectangle(cornerRadius: 14) .fill(color.opacity(isToday ? 0.25 : 0.12)) .frame(height: height) VStack(spacing: 4) { Text(dayLabel) .font(.system(size: 10, weight: isToday ? .bold : .medium)) .foregroundStyle(isToday ? .primary : .secondary) .textCase(.uppercase) image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 36, height: 36) .foregroundColor(color) Text(dateLabel) .font(.system(size: 13, weight: isToday ? .bold : .semibold)) .foregroundStyle(isToday ? color : .secondary) } } .frame(maxWidth: .infinity) } } struct LargeWidgetView: View { var entry: Provider.Entry var timeLineView = [WatchTimelineView]() private var showVotingForToday: Bool { entry.hasSubscription && !entry.hasVotedToday } init(entry: Provider.Entry) { self.entry = entry let realData = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) let hasRealData = realData.contains { view in let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10) } private var dayFormatter: DateFormatter { let f = DateFormatter() f.dateFormat = "EEE" return f } private var dateFormatter: DateFormatter { let f = DateFormatter() f.dateFormat = "d" return f } var body: some View { GeometryReader { geo in let cellHeight = (geo.size.height - 70) / 2 // Subtract header height, divide by 2 rows VStack(spacing: 6) { // Header HStack { VStack(alignment: .leading, spacing: 2) { Text("Last 10 Days") .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) Text(headerDateRange) .font(.caption2) .foregroundStyle(.secondary) } Spacer() } .padding(.horizontal, 12) .padding(.top, 8) // Calendar grid - 2 rows of 5 VStack(spacing: 6) { // First row (most recent 5) HStack(spacing: 6) { ForEach(Array(timeLineView.prefix(5).enumerated()), id: \.element.id) { index, item in DayCell( dayLabel: dayFormatter.string(from: item.date), dateLabel: dateFormatter.string(from: item.date), image: item.image, color: item.color, isToday: index == 0, height: cellHeight ) } } // Second row (older 5) HStack(spacing: 6) { ForEach(Array(timeLineView.suffix(5).enumerated()), id: \.element.id) { _, item in DayCell( dayLabel: dayFormatter.string(from: item.date), dateLabel: dateFormatter.string(from: item.date), image: item.image, color: item.color, isToday: false, height: cellHeight ) } } } .padding(.horizontal, 10) .padding(.bottom, 8) } } } private var headerDateRange: String { guard let first = timeLineView.first, let last = timeLineView.last else { return "" } let formatter = DateFormatter() formatter.dateFormat = "MMM d" return "\(formatter.string(from: last.date)) - \(formatter.string(from: first.date))" } private var dominantMood: (image: Image, color: Color)? { guard !timeLineView.isEmpty else { return nil } // Return the most recent mood let first = timeLineView.first! return (first.image, first.color) } } // MARK: - Day Cell for Large Widget struct DayCell: View { let dayLabel: String let dateLabel: String let image: Image let color: Color let isToday: Bool let height: CGFloat var body: some View { VStack(spacing: 2) { Text(dayLabel) .font(.system(size: 10, weight: isToday ? .bold : .medium)) .foregroundStyle(isToday ? .primary : .secondary) .textCase(.uppercase) ZStack { RoundedRectangle(cornerRadius: 14) .fill(color.opacity(isToday ? 0.25 : 0.12)) .frame(height: height - 16) VStack(spacing: 6) { image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 38, height: 38) .foregroundColor(color) Text(dateLabel) .font(.system(size: 13, weight: isToday ? .bold : .semibold)) .foregroundStyle(isToday ? color : .secondary) } } } .frame(maxWidth: .infinity) } } /**********************************************************/ struct FeelsGraphicWidgetEntryView : View { @Environment(\.sizeCategory) var sizeCategory @Environment(\.widgetFamily) var family var entry: Provider.Entry @ViewBuilder var body: some View { SmallGraphicWidgetView(entry: entry) } } struct SmallGraphicWidgetView: View { var entry: Provider.Entry var timeLineView: [WatchTimelineView] init(entry: Provider.Entry) { self.entry = entry let realData = TimeLineCreator.createViews(daysBack: 2) // Check if we have any real mood data (not all missing) let hasRealData = realData.contains { view in let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 2) } private var iconViewModel: IconViewModel { if let first = timeLineView.first { return IconViewModel(backgroundImage: first.graphic, bgColor: first.color, bgOverlayColor: first.secondaryColor, centerImage: first.graphic, innerColor: first.color) } else { return IconViewModel.great } } var body: some View { Color.clear .containerBackground(for: .widget) { IconView(iconViewModel: iconViewModel) } } } /**********************************************************/ struct FeelsIconWidgetEntryView : View { @Environment(\.sizeCategory) var sizeCategory @Environment(\.widgetFamily) var family var entry: Provider.Entry @ViewBuilder var body: some View { SmallIconView(entry: entry) } } struct SmallIconView: View { var entry: Provider.Entry private var customWidget: CustomWidgetModel { UserDefaultsStore.getCustomWidgets().first(where: { $0.inUse == true }) ?? CustomWidgetModel.randomWidget } var body: some View { CustomWidgetView(customWidgetModel: customWidget) .ignoresSafeArea() .containerBackground(for: .widget) { customWidget.bgColor } } } /**********************************************************/ struct TimeHeaderView: View { let startDate: Date let endDate: Date var formatter: DateFormatter { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium return dateFormatter } var body: some View { HStack { Text(startDate, formatter: formatter) .font(.system(.footnote)) Text(" - ") .font(.system(.footnote)) Text(endDate, formatter: formatter) .font(.system(.footnote)) } } } struct TimeBodyView: View { let group: [WatchTimelineView] var showVotingForToday: Bool = false var promptText: String = "" var body: some View { if showVotingForToday { // Show voting view without extra background container InlineVotingView(promptText: promptText) .padding() } else { ZStack { Color(UIColor.secondarySystemBackground) HStack(spacing: 4) { ForEach(group) { watchView in EntryCard(timeLineView: watchView) } } .padding() } } } } // MARK: - Inline Voting View (compact mood buttons for timeline widget) struct InlineVotingView: View { let promptText: String let moods: [Mood] = [.horrible, .bad, .average, .good, .great] private var moodTint: MoodTintable.Type { UserDefaultsStore.moodTintable() } private var moodImages: MoodImagable.Type { UserDefaultsStore.moodMoodImagable() } var body: some View { VStack(spacing: 8) { Text(promptText) .font(.subheadline) .foregroundStyle(.primary) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.7) HStack(spacing: 8) { ForEach(moods, id: \.rawValue) { mood in Button(intent: VoteMoodIntent(mood: mood)) { moodImages.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 44, height: 44) .foregroundColor(moodTint.color(forMood: mood)) } .buttonStyle(.plain) } } } } } struct EntryCard: View { var timeLineView: WatchTimelineView var body: some View { timeLineView.image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 50, height: 50, alignment: .center) .foregroundColor(timeLineView.color) } } @main struct FeelsBundle: WidgetBundle { var body: some Widget { FeelsWidget() FeelsGraphicWidget() FeelsIconWidget() FeelsVoteWidget() FeelsMoodControlWidget() MoodStreakLiveActivity() } } // MARK: - Control Center Widget struct FeelsMoodControlWidget: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "FeelsMoodControl") { ControlWidgetButton(action: OpenFeelsIntent()) { Label("Log Mood", systemImage: "face.smiling") } } .displayName("Log Mood") .description("Open Feels to log your mood") } } struct OpenFeelsIntent: AppIntent { static var title: LocalizedStringResource = "Open Feels" static var description = IntentDescription("Open the Feels app to log your mood") static var openAppWhenRun: Bool = true func perform() async throws -> some IntentResult { return .result() } } struct FeelsWidget: Widget { let kind: String = "FeelsWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in FeelsWidgetEntryView(entry: entry) } .configurationDisplayName("Feels") .description("") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } struct FeelsIconWidget: Widget { let kind: String = "FeelsIconWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in FeelsIconWidgetEntryView(entry: entry) } .configurationDisplayName("Feels Icon") .description("") .supportedFamilies([.systemSmall]) .contentMarginsDisabled() } } struct FeelsGraphicWidget: Widget { let kind: String = "FeelsGraphicWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in FeelsGraphicWidgetEntryView(entry: entry) } .configurationDisplayName("Mood Graphic") .description("") .supportedFamilies([.systemSmall]) .contentMarginsDisabled() } } // MARK: - Preview Helpers private extension FeelsWidget_Previews { static func sampleTimelineViews(count: Int) -> [WatchTimelineView] { let moods: [Mood] = [.great, .good, .average, .bad, .horrible] return (0.. SimpleEntry { SimpleEntry( date: Date(), configuration: ConfigurationIntent(), timeLineViews: sampleTimelineViews(count: timelineCount), hasSubscription: true, hasVotedToday: true ) } } struct FeelsWidget_Previews: PreviewProvider { static var previews: some View { Group { // MARK: - FeelsWidget (Timeline) FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 1)) .previewContext(WidgetPreviewContext(family: .systemSmall)) .previewDisplayName("Timeline - Small") FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 5)) .previewContext(WidgetPreviewContext(family: .systemMedium)) .previewDisplayName("Timeline - Medium") FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 10)) .previewContext(WidgetPreviewContext(family: .systemLarge)) .previewDisplayName("Timeline - Large") // MARK: - FeelsGraphicWidget (Mood Graphic) FeelsGraphicWidgetEntryView(entry: sampleEntry(timelineCount: 2)) .previewContext(WidgetPreviewContext(family: .systemSmall)) .previewDisplayName("Mood Graphic - Small") // MARK: - FeelsIconWidget (Custom Icon) FeelsIconWidgetEntryView(entry: sampleEntry()) .previewContext(WidgetPreviewContext(family: .systemSmall)) .previewDisplayName("Custom Icon - Small") // MARK: - FeelsVoteWidget (Vote - Not Voted) FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling?" )) .previewContext(WidgetPreviewContext(family: .systemSmall)) .previewDisplayName("Vote - Small (Not Voted)") FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling?" )) .previewContext(WidgetPreviewContext(family: .systemMedium)) .previewDisplayName("Vote - Medium (Not Voted)") // MARK: - FeelsVoteWidget (Vote - Already Voted) FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .great, stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]), promptText: "" )) .previewContext(WidgetPreviewContext(family: .systemSmall)) .previewDisplayName("Vote - Small (Voted)") FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: MoodStats(totalEntries: 45, moodCounts: [.great: 15, .good: 18, .average: 8, .bad: 3, .horrible: 1]), promptText: "" )) .previewContext(WidgetPreviewContext(family: .systemMedium)) .previewDisplayName("Vote - Medium (Voted)") // MARK: - FeelsVoteWidget (Non-Subscriber) FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "" )) .previewContext(WidgetPreviewContext(family: .systemSmall)) .previewDisplayName("Vote - Small (Non-Subscriber)") } } }