Files
Reflect/ReflectWidget/WidgetSharedViews.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

281 lines
9.3 KiB
Swift

//
// 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 : "Subscribe to track your mood")
.font(.system(size: 20, 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"))
} 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"))
}
}
@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"))
} else {
Link(destination: URL(string: "reflect://subscribe")!) {
content
}
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
}
}
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 : "Subscribe to track your mood")
.font(.system(size: 24, 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"))
} else {
Link(destination: URL(string: "reflect://subscribe")!) {
moodButtonContent(for: mood)
}
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
}
}
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"))
} else {
Link(destination: URL(string: "reflect://subscribe")!) {
moodIcon(for: mood)
}
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
}
}
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))
}
}