// // WidgetSharedViews.swift // ReflectWidget // // Shared voting views used across multiple widgets // import WidgetKit import SwiftUI import AppIntents // MARK: - Voting View struct VotingView: View { let family: WidgetFamily let promptText: String let hasSubscription: Bool private var moodTint: MoodTintable.Type { UserDefaultsStore.moodTintable() } private var moodImages: MoodImagable.Type { UserDefaultsStore.moodMoodImagable() } var body: some View { if family == .systemSmall { smallLayout } else { mediumLayout } } // MARK: - Small Widget: 3 over 2 grid centered in 50%|50% vertical split private var smallLayout: some View { VStack(spacing: 0) { // Top half: Great, Good, Average HStack(spacing: 12) { ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in moodButton(for: mood, size: 40) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) // Bottom half: Bad, Horrible HStack(spacing: 12) { ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in moodButton(for: mood, size: 40) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } } // MARK: - Medium Widget: 50/50 split, both centered private var mediumLayout: some View { VStack(spacing: 0) { // Top 50%: Text left-aligned, vertically centered HStack { Text(hasSubscription ? promptText : String(localized: "Subscribe to track your mood")) .font(.title3.weight(.semibold)) .foregroundStyle(.primary) .multilineTextAlignment(.leading) .lineLimit(2) .minimumScaleFactor(0.8) Spacer() } .padding(.horizontal, 16) .frame(maxWidth: .infinity, maxHeight: .infinity) // Bottom 50%: Voting buttons centered HStack(spacing: 0) { ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in moodButtonMedium(for: mood) .frame(maxWidth: .infinity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } } @ViewBuilder private func moodButton(for mood: Mood, size: CGFloat) -> some View { // Used for small widget let touchSize = max(size, 44) if hasSubscription { Button(intent: VoteMoodIntent(mood: mood)) { moodIcon(for: mood, size: size) .frame(minWidth: touchSize, minHeight: touchSize) } .buttonStyle(.plain) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Log this mood")) .accessibilityIdentifier(AccessibilityID.Widget.voteMoodButton(mood.strValue)) } else { Link(destination: URL(string: "reflect://subscribe")!) { moodIcon(for: mood, size: size) .frame(minWidth: touchSize, minHeight: touchSize) } .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Open app to subscribe")) .accessibilityIdentifier(AccessibilityID.Widget.subscribeLink) } } @ViewBuilder private func moodButtonMedium(for mood: Mood) -> some View { // Medium widget uses icons only (accessibility labels provide screen reader support) let content = moodImages.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 54, height: 54) .foregroundColor(moodTint.color(forMood: mood)) if hasSubscription { Button(intent: VoteMoodIntent(mood: mood)) { content } .buttonStyle(.plain) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Log this mood")) .accessibilityIdentifier(AccessibilityID.Widget.voteMoodButton(mood.strValue)) } else { Link(destination: URL(string: "reflect://subscribe")!) { content } .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Open app to subscribe")) .accessibilityIdentifier(AccessibilityID.Widget.subscribeLink) } } private func moodIcon(for mood: Mood, size: CGFloat) -> some View { moodImages.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) .foregroundColor(moodTint.color(forMood: mood)) } } // MARK: - Large Voting View struct LargeVotingView: View { let promptText: String let hasSubscription: Bool private var moodTint: MoodTintable.Type { UserDefaultsStore.moodTintable() } private var moodImages: MoodImagable.Type { UserDefaultsStore.moodMoodImagable() } var body: some View { GeometryReader { geo in VStack(spacing: 0) { // Top 33%: Title centered Text(hasSubscription ? promptText : String(localized: "Subscribe to track your mood")) .font(.title2.weight(.semibold)) .foregroundStyle(.primary) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.8) .padding(.horizontal, 12) .frame(width: geo.size.width, height: geo.size.height * 0.33) // Bottom 66%: Voting buttons in two rows VStack(spacing: 0) { // Top row: Great, Good, Average HStack(spacing: 16) { ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in moodButton(for: mood) } } .frame(maxWidth: .infinity, maxHeight: .infinity) // Bottom row: Bad, Horrible HStack(spacing: 16) { ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in moodButton(for: mood) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } .frame(width: geo.size.width, height: geo.size.height * 0.67) } } } @ViewBuilder private func moodButton(for mood: Mood) -> some View { if hasSubscription { Button(intent: VoteMoodIntent(mood: mood)) { moodButtonContent(for: mood) } .buttonStyle(.plain) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Log this mood")) .accessibilityIdentifier(AccessibilityID.Widget.voteMoodButton(mood.strValue)) } else { Link(destination: URL(string: "reflect://subscribe")!) { moodButtonContent(for: mood) } .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Open app to subscribe")) .accessibilityIdentifier(AccessibilityID.Widget.subscribeLink) } } private func moodButtonContent(for mood: Mood) -> some View { // Large widget uses icons only (accessibility labels provide screen reader support) moodImages.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 53, height: 53) .foregroundColor(moodTint.color(forMood: mood)) .padding(12) .background( RoundedRectangle(cornerRadius: 14) .fill(moodTint.color(forMood: mood).opacity(0.15)) ) } } // MARK: - Inline Voting View (compact mood buttons for timeline widget) struct InlineVotingView: View { let promptText: String let hasSubscription: Bool 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(hasSubscription ? promptText : "Tap to open app") .font(.subheadline) .foregroundStyle(.primary) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.7) HStack(spacing: 8) { ForEach(moods, id: \.rawValue) { mood in moodButton(for: mood) } } } } @ViewBuilder private func moodButton(for mood: Mood) -> some View { if hasSubscription { Button(intent: VoteMoodIntent(mood: mood)) { moodIcon(for: mood) } .buttonStyle(.plain) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Log this mood")) .accessibilityIdentifier(AccessibilityID.Widget.voteMoodButton(mood.strValue)) } else { Link(destination: URL(string: "reflect://subscribe")!) { moodIcon(for: mood) } .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Open app to subscribe")) .accessibilityIdentifier(AccessibilityID.Widget.subscribeLink) } } private func moodIcon(for mood: Mood) -> some View { moodImages.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 44, height: 44) .foregroundColor(moodTint.color(forMood: mood)) } }