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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user