Add interactive voting to all timeline widgets

- SmallWidgetView now shows VotingView when vote needed
- MediumWidgetView now shows VotingView when vote needed
- LargeWidgetView now shows LargeVotingView when vote needed
- Add new LargeVotingView with larger buttons and mood labels
- All voting buttons use VoteMoodIntent for interactive voting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-21 20:51:58 -06:00
parent 7c280146de
commit f8ec6962de

View File

@@ -337,6 +337,10 @@ struct SmallWidgetView: View {
var entry: Provider.Entry var entry: Provider.Entry
var todayView: WatchTimelineView? var todayView: WatchTimelineView?
private var showVotingForToday: Bool {
entry.hasSubscription && !entry.hasVotedToday
}
private var dayFormatter: DateFormatter { private var dayFormatter: DateFormatter {
let f = DateFormatter() let f = DateFormatter()
f.dateFormat = "EEEE" f.dateFormat = "EEEE"
@@ -360,7 +364,10 @@ struct SmallWidgetView: View {
} }
var body: some View { var body: some View {
if let today = todayView { if showVotingForToday {
// Show interactive voting buttons
VotingView(family: .systemSmall, promptText: entry.promptText)
} else if let today = todayView {
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer() Spacer()
@@ -431,40 +438,45 @@ struct MediumWidgetView: View {
} }
var body: some View { var body: some View {
GeometryReader { geo in if showVotingForToday {
let cellHeight = geo.size.height - 36 // Show interactive voting buttons
VotingView(family: .systemMedium, promptText: entry.promptText)
} else {
GeometryReader { geo in
let cellHeight = geo.size.height - 36
VStack(spacing: 4) { VStack(spacing: 4) {
// Header // Header
HStack { HStack {
Text("Last 5 Days") Text("Last 5 Days")
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
.foregroundStyle(.primary) .foregroundStyle(.primary)
Text("·") Text("·")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(headerDateRange) Text(headerDateRange)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Spacer() 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, 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)
} }
.padding(.horizontal, 10)
.padding(.bottom, 10)
} }
} }
} }
@@ -538,57 +550,62 @@ struct LargeWidgetView: View {
} }
var body: some View { var body: some View {
GeometryReader { geo in if showVotingForToday {
let cellHeight = (geo.size.height - 70) / 2 // Subtract header height, divide by 2 rows // Show interactive voting buttons for large widget
LargeVotingView(promptText: entry.promptText)
} else {
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) { VStack(spacing: 6) {
// First row (most recent 5) // Header
HStack(spacing: 6) { HStack {
ForEach(Array(timeLineView.prefix(5).enumerated()), id: \.element.id) { index, item in VStack(alignment: .leading, spacing: 2) {
DayCell( Text("Last 10 Days")
dayLabel: dayFormatter.string(from: item.date), .font(.subheadline.weight(.semibold))
dateLabel: dateFormatter.string(from: item.date), .foregroundStyle(.primary)
image: item.image, Text(headerDateRange)
color: item.color, .font(.caption2)
isToday: index == 0, .foregroundStyle(.secondary)
height: cellHeight
)
} }
Spacer()
} }
.padding(.horizontal, 12)
.padding(.top, 8)
// Second row (older 5) // Calendar grid - 2 rows of 5
HStack(spacing: 6) { VStack(spacing: 6) {
ForEach(Array(timeLineView.suffix(5).enumerated()), id: \.element.id) { _, item in // First row (most recent 5)
DayCell( HStack(spacing: 6) {
dayLabel: dayFormatter.string(from: item.date), ForEach(Array(timeLineView.prefix(5).enumerated()), id: \.element.id) { index, item in
dateLabel: dateFormatter.string(from: item.date), DayCell(
image: item.image, dayLabel: dayFormatter.string(from: item.date),
color: item.color, dateLabel: dateFormatter.string(from: item.date),
isToday: false, image: item.image,
height: cellHeight 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)
} }
.padding(.horizontal, 10)
.padding(.bottom, 8)
} }
} }
} }
@@ -646,6 +663,63 @@ struct DayCell: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
// MARK: - Large Voting View
struct LargeVotingView: View {
let promptText: String
private var moodTint: MoodTintable.Type {
UserDefaultsStore.moodTintable()
}
private var moodImages: MoodImagable.Type {
UserDefaultsStore.moodMoodImagable()
}
var body: some View {
VStack(spacing: 24) {
Spacer()
Text(promptText)
.font(.title2.weight(.semibold))
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
.lineLimit(2)
.minimumScaleFactor(0.8)
// Large mood buttons in a row
HStack(spacing: 20) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
Button(intent: VoteMoodIntent(mood: mood)) {
VStack(spacing: 8) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 56, height: 56)
.foregroundColor(moodTint.color(forMood: mood))
Text(mood.strValue)
.font(.caption.weight(.medium))
.foregroundColor(moodTint.color(forMood: mood))
}
.padding(.vertical, 12)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(moodTint.color(forMood: mood).opacity(0.15))
)
}
.buttonStyle(.plain)
}
}
Spacer()
}
.padding()
}
}
/**********************************************************/ /**********************************************************/
struct FeelsGraphicWidgetEntryView : View { struct FeelsGraphicWidgetEntryView : View {
@Environment(\.sizeCategory) var sizeCategory @Environment(\.sizeCategory) var sizeCategory