diff --git a/FeelsWidget2/FeelsWidget.swift b/FeelsWidget2/FeelsWidget.swift index 1755331..ffed556 100644 --- a/FeelsWidget2/FeelsWidget.swift +++ b/FeelsWidget2/FeelsWidget.swift @@ -335,34 +335,61 @@ struct FeelsWidgetEntryView : View { struct SmallWidgetView: View { var entry: Provider.Entry - var timeLineView = [WatchTimelineView]() + 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) - // 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) } - if let firstView = hasRealData ? realData.first : TimeLineCreator.createSampleViews(count: 1).first { - timeLineView = [firstView] - } + todayView = hasRealData ? realData.first : TimeLineCreator.createSampleViews(count: 1).first } var body: some View { - ZStack { - Color(UIColor.secondarySystemBackground) - HStack { - ForEach(self.timeLineView) { watchView in - EntryCard(timeLineView: watchView) + 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() } - .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) } - .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) - .frame(minHeight: 0, maxHeight: 55) - .padding() } } @@ -374,10 +401,21 @@ struct MediumWidgetView: View { 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)) - // 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) @@ -385,26 +423,90 @@ struct MediumWidgetView: View { 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 { - VStack { - Spacer() + GeometryReader { geo in + let cellHeight = geo.size.height - 36 - if !showVotingForToday, let first = timeLineView.first, let last = timeLineView.last { - TimeHeaderView(startDate: first.date, endDate: last.date) - .frame(minWidth: 0, maxWidth: .infinity) - .multilineTextAlignment(.leading) + 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) } - - TimeBodyView(group: timeLineView, showVotingForToday: showVotingForToday, promptText: entry.promptText) - .clipShape(RoundedRectangle(cornerRadius: showVotingForToday ? 0 : 25, style: .continuous)) - .frame(minHeight: 0, maxHeight: showVotingForToday ? 80 : 55) - .padding() - - Spacer() } } } +// 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]() @@ -416,7 +518,6 @@ struct LargeWidgetView: View { init(entry: Provider.Entry) { self.entry = entry let realData = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) - // 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) @@ -424,57 +525,126 @@ struct LargeWidgetView: View { timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10) } - var firstGroup: [WatchTimelineView] { - return Array(self.timeLineView.prefix(5)) + private var dayFormatter: DateFormatter { + let f = DateFormatter() + f.dateFormat = "EEE" + return f } - var secondGroup: [WatchTimelineView] { - return Array(self.timeLineView.suffix(5)) + private var dateFormatter: DateFormatter { + let f = DateFormatter() + f.dateFormat = "d" + return f } var body: some View { - VStack { - Spacer() + GeometryReader { geo in + let cellHeight = (geo.size.height - 70) / 2 // Subtract header height, divide by 2 rows - // First row (includes today - may show voting) - VStack { - Spacer() - - if !showVotingForToday, let first = firstGroup.first, let last = firstGroup.last { - TimeHeaderView(startDate: first.date, endDate: last.date) - .frame(minWidth: 0, maxWidth: .infinity) - .multilineTextAlignment(.leading) + 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) - TimeBodyView(group: firstGroup, showVotingForToday: showVotingForToday, promptText: entry.promptText) - .clipShape(RoundedRectangle(cornerRadius: showVotingForToday ? 0 : 25, style: .continuous)) - .frame(minHeight: 0, maxHeight: showVotingForToday ? 80 : 55) - .padding() + // 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 + ) + } + } - Spacer() - } - - // Second row (older entries - never show voting) - VStack { - Spacer() - - if let first = secondGroup.first, let last = secondGroup.last { - TimeHeaderView(startDate: first.date, endDate: last.date) - .frame(minWidth: 0, maxWidth: .infinity) - .multilineTextAlignment(.leading) + // 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 + ) + } + } } - - TimeBodyView(group: secondGroup, showVotingForToday: false) - .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) - .frame(minHeight: 0, maxHeight: 55) - .padding() - - Spacer() + .padding(.horizontal, 10) + .padding(.bottom, 8) } - - Spacer() } } + + 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 { @@ -486,7 +656,6 @@ struct FeelsGraphicWidgetEntryView : View { @ViewBuilder var body: some View { SmallGraphicWidgetView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) } } @@ -505,17 +674,24 @@ struct SmallGraphicWidgetView: View { timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 2) } - var body: some View { + private var iconViewModel: IconViewModel { if let first = timeLineView.first { - IconView(iconViewModel: IconViewModel(backgroundImage: first.graphic, - bgColor: first.color, - bgOverlayColor: first.secondaryColor, - centerImage: first.graphic, - innerColor: first.color)) + return IconViewModel(backgroundImage: first.graphic, + bgColor: first.color, + bgOverlayColor: first.secondaryColor, + centerImage: first.graphic, + innerColor: first.color) } else { - IconView(iconViewModel: IconViewModel.great) + return IconViewModel.great } } + + var body: some View { + Color.clear + .containerBackground(for: .widget) { + IconView(iconViewModel: iconViewModel) + } + } } /**********************************************************/ struct FeelsIconWidgetEntryView : View { @@ -527,25 +703,23 @@ struct FeelsIconWidgetEntryView : View { @ViewBuilder var body: some View { SmallIconView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) } } struct SmallIconView: View { var entry: Provider.Entry - + + private var customWidget: CustomWidgetModel { + UserDefaultsStore.getCustomWidgets().first(where: { $0.inUse == true }) + ?? CustomWidgetModel.randomWidget + } + var body: some View { - GeometryReader { geo in - if let inUseWidget = UserDefaultsStore.getCustomWidgets().first(where: { - $0.inUse == true - }) { - CustomWidgetView(customWidgetModel: inUseWidget) - .frame(width: geo.size.width, height: geo.size.height) - } else { - CustomWidgetView(customWidgetModel: CustomWidgetModel.randomWidget) - .frame(width: geo.size.width, height: geo.size.height) + CustomWidgetView(customWidgetModel: customWidget) + .ignoresSafeArea() + .containerBackground(for: .widget) { + customWidget.bgColor } - } } } /**********************************************************/ @@ -698,7 +872,7 @@ struct FeelsWidget: Widget { struct FeelsIconWidget: Widget { let kind: String = "FeelsIconWidget" - + var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, @@ -708,12 +882,13 @@ struct FeelsIconWidget: Widget { .configurationDisplayName("Feels Icon") .description("") .supportedFamilies([.systemSmall]) + .contentMarginsDisabled() } } struct FeelsGraphicWidget: Widget { let kind: String = "FeelsGraphicWidget" - + var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, @@ -723,6 +898,7 @@ struct FeelsGraphicWidget: Widget { .configurationDisplayName("Mood Graphic") .description("") .supportedFamilies([.systemSmall]) + .contentMarginsDisabled() } }