Add interactive widget voting and fix warnings/bugs
Widget Features: - Add inline voting to timeline widget when no entry exists for today - Show random prompt from notification strings in voting mode - Update vote widget to use simple icon style for selection - Make stats bar full width in voted state view - Add Localizable.strings to widget extension target Bug Fixes: - Fix inverted date calculation in InsightsViewModel streak logic - Replace force unwraps with safe optional handling in widgets - Replace fatalError calls with graceful error handling - Fix CSV import safety in SettingsView Warning Fixes: - Add @retroactive to Color and Date extension conformances - Update deprecated onChange(of:perform:) to new syntax - Replace deprecated applicationIconBadgeNumber with setBadgeCount - Replace deprecated UIApplication.shared.windows API - Add @preconcurrency for Swift 6 protocol conformances - Add missing widget family cases to switch statement - Remove unused variables and #warning directives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,14 +52,16 @@ struct VoteWidgetProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> VoteWidgetEntry {
|
||||
// Show sample "already voted" state for widget picker preview
|
||||
let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])
|
||||
return VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats)
|
||||
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
|
||||
return VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (VoteWidgetEntry) -> Void) {
|
||||
// Show sample data for widget picker preview
|
||||
if context.isPreview {
|
||||
let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])
|
||||
let entry = VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats)
|
||||
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
|
||||
let entry = VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText)
|
||||
completion(entry)
|
||||
return
|
||||
}
|
||||
@@ -115,12 +117,16 @@ struct VoteWidgetProvider: TimelineProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Get random prompt text for voting view
|
||||
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
|
||||
|
||||
return VoteWidgetEntry(
|
||||
date: Date(),
|
||||
hasSubscription: hasSubscription,
|
||||
hasVotedToday: hasVotedToday,
|
||||
todaysMood: todaysMood,
|
||||
stats: stats
|
||||
stats: stats,
|
||||
promptText: promptText
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -145,6 +151,7 @@ struct VoteWidgetEntry: TimelineEntry {
|
||||
let hasVotedToday: Bool
|
||||
let todaysMood: Mood?
|
||||
let stats: MoodStats?
|
||||
let promptText: String
|
||||
}
|
||||
|
||||
// MARK: - Widget Views
|
||||
@@ -161,7 +168,7 @@ struct FeelsVoteWidgetEntryView: View {
|
||||
VotedStatsView(entry: entry)
|
||||
} else {
|
||||
// Show voting buttons
|
||||
VotingView(family: family)
|
||||
VotingView(family: family, promptText: entry.promptText)
|
||||
}
|
||||
} else {
|
||||
// Non-subscriber view - tap to open app
|
||||
@@ -176,39 +183,9 @@ struct FeelsVoteWidgetEntryView: View {
|
||||
|
||||
struct VotingView: View {
|
||||
let family: WidgetFamily
|
||||
let promptText: String
|
||||
let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
Text("How are you feeling?")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if family == .systemSmall {
|
||||
// Compact layout for small widget
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
|
||||
ForEach(moods, id: \.rawValue) { mood in
|
||||
MoodButton(mood: mood, isCompact: true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Horizontal layout for medium/large
|
||||
HStack(spacing: 4) {
|
||||
ForEach(moods, id: \.rawValue) { mood in
|
||||
MoodButton(mood: mood, isCompact: false)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct MoodButton: View {
|
||||
let mood: Mood
|
||||
let isCompact: Bool
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
@@ -218,23 +195,28 @@ struct MoodButton: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||
VStack(spacing: 4) {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: isCompact ? 28 : 36, height: isCompact ? 28 : 36)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
VStack(spacing: 12) {
|
||||
Text(promptText)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
|
||||
if !isCompact {
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.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: family == .systemSmall ? 36 : 44, height: family == .systemSmall ? 36 : 44)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,17 +266,19 @@ struct VotedStatsView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
let percentage = stats.percentage(for: mood)
|
||||
if percentage > 0 {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(moodTint.color(forMood: mood))
|
||||
.frame(width: max(4, CGFloat(percentage) * 0.8))
|
||||
GeometryReader { geo in
|
||||
HStack(spacing: 2) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
let percentage = stats.percentage(for: mood)
|
||||
if percentage > 0 {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(moodTint.color(forMood: mood))
|
||||
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
.frame(height: 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,7 +331,7 @@ struct FeelsVoteWidget: Widget {
|
||||
#Preview(as: .systemSmall) {
|
||||
FeelsVoteWidget()
|
||||
} timeline: {
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil)
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .great, stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]))
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil)
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling today?")
|
||||
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: "")
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "")
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ struct TimeLineCreator {
|
||||
}
|
||||
}
|
||||
|
||||
struct Provider: IntentTimelineProvider {
|
||||
struct Provider: @preconcurrency IntentTimelineProvider {
|
||||
let timeLineCreator = TimeLineCreator()
|
||||
|
||||
/*
|
||||
@@ -107,25 +107,52 @@ struct Provider: IntentTimelineProvider {
|
||||
} else {
|
||||
timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))
|
||||
}
|
||||
let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus()
|
||||
let entry = SimpleEntry(date: Date(),
|
||||
configuration: ConfigurationIntent(),
|
||||
timeLineViews: timeLineViews)
|
||||
timeLineViews: timeLineViews,
|
||||
hasSubscription: hasSubscription,
|
||||
hasVotedToday: hasVotedToday,
|
||||
promptText: promptText)
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
|
||||
@MainActor func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus()
|
||||
let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!,
|
||||
configuration: ConfigurationIntent(),
|
||||
timeLineViews: nil)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
let todayEntry = DataController.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 {
|
||||
@@ -133,12 +160,18 @@ struct SimpleEntry: TimelineEntry {
|
||||
let configuration: ConfigurationIntent
|
||||
let timeLineViews: [WatchTimelineView]?
|
||||
let showStats: Bool
|
||||
|
||||
init(date: Date, configuration: ConfigurationIntent, timeLineViews: [WatchTimelineView]?, showStats: Bool = false) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,13 +179,16 @@ struct SimpleEntry: TimelineEntry {
|
||||
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 {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
Group {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
SmallWidgetView(entry: entry)
|
||||
@@ -162,10 +198,13 @@ struct FeelsWidgetEntryView : View {
|
||||
LargeWidgetView(entry: entry)
|
||||
case .systemExtraLarge:
|
||||
LargeWidgetView(entry: entry)
|
||||
case .accessoryCircular, .accessoryRectangular, .accessoryInline:
|
||||
SmallWidgetView(entry: entry)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
MediumWidgetView(entry: entry)
|
||||
}
|
||||
}
|
||||
.containerBackground(showVotingForToday ? Color.clear : Color(UIColor.systemBackground), for: .widget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +220,9 @@ struct SmallWidgetView: View {
|
||||
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
|
||||
return view.color != moodTint.color(forMood: .missing)
|
||||
}
|
||||
timeLineView = hasRealData ? [realData.first!] : [TimeLineCreator.createSampleViews(count: 1).first!]
|
||||
if let firstView = hasRealData ? realData.first : TimeLineCreator.createSampleViews(count: 1).first {
|
||||
timeLineView = [firstView]
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -204,6 +245,10 @@ struct MediumWidgetView: 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: 6).prefix(5))
|
||||
@@ -219,13 +264,15 @@ struct MediumWidgetView: View {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
TimeHeaderView(startDate: timeLineView.first!.date, endDate: timeLineView.last!.date)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.multilineTextAlignment(.leading)
|
||||
if !showVotingForToday, let first = timeLineView.first, let last = timeLineView.last {
|
||||
TimeHeaderView(startDate: first.date, endDate: last.date)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
TimeBodyView(group: timeLineView)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
.frame(minHeight: 0, maxHeight: 55)
|
||||
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()
|
||||
@@ -237,6 +284,10 @@ 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))
|
||||
@@ -248,33 +299,52 @@ struct LargeWidgetView: View {
|
||||
timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10)
|
||||
}
|
||||
|
||||
var firstGroup: ([WatchTimelineView], String) {
|
||||
return (Array(self.timeLineView.prefix(5)), UUID().uuidString)
|
||||
var firstGroup: [WatchTimelineView] {
|
||||
return Array(self.timeLineView.prefix(5))
|
||||
}
|
||||
|
||||
var secondGroup: ([WatchTimelineView], String) {
|
||||
return (Array(self.timeLineView.suffix(5)), UUID().uuidString)
|
||||
var secondGroup: [WatchTimelineView] {
|
||||
return Array(self.timeLineView.suffix(5))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
ForEach([firstGroup, secondGroup], id: \.1) { group in
|
||||
VStack {
|
||||
Spacer()
|
||||
// First row (includes today - may show voting)
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
TimeHeaderView(startDate: group.0.first!.date, endDate: group.0.last!.date)
|
||||
if !showVotingForToday, let first = firstGroup.first, let last = firstGroup.last {
|
||||
TimeHeaderView(startDate: first.date, endDate: last.date)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
TimeBodyView(group: group.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
.frame(minHeight: 0, maxHeight: 55)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
TimeBodyView(group: firstGroup, showVotingForToday: showVotingForToday, promptText: entry.promptText)
|
||||
.clipShape(RoundedRectangle(cornerRadius: showVotingForToday ? 0 : 25, style: .continuous))
|
||||
.frame(minHeight: 0, maxHeight: showVotingForToday ? 80 : 55)
|
||||
.padding()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
TimeBodyView(group: secondGroup, showVotingForToday: false)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
|
||||
.frame(minHeight: 0, maxHeight: 55)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -376,16 +446,63 @@ struct TimeHeaderView: View {
|
||||
|
||||
struct TimeBodyView: View {
|
||||
let group: [WatchTimelineView]
|
||||
|
||||
var showVotingForToday: Bool = false
|
||||
var promptText: String = ""
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(UIColor.secondarySystemBackground)
|
||||
HStack {
|
||||
ForEach(group) { watchView in
|
||||
EntryCard(timeLineView: watchView)
|
||||
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)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user