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>
This commit is contained in:
Trey t
2026-02-26 11:47:16 -06:00
parent b1a54d2844
commit 0442eab1f8
380 changed files with 858 additions and 1077 deletions

View File

@@ -0,0 +1,432 @@
//
// ReflectVoteWidget.swift
// ReflectWidget
//
// Interactive widget for mood voting (iOS 17+)
//
import WidgetKit
import SwiftUI
import AppIntents
// MARK: - Widget Configuration
struct ReflectVoteWidget: Widget {
let kind: String = "ReflectVoteWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: VoteWidgetProvider()) { entry in
ReflectVoteWidgetEntryView(entry: entry)
}
.configurationDisplayName("Mood Vote")
.description("Quickly rate your mood for today")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
// MARK: - Entry View
struct ReflectVoteWidgetEntryView: View {
@Environment(\.widgetFamily) var family
var entry: VoteWidgetProvider.Entry
var body: some View {
Group {
if entry.hasVotedToday {
// Already voted today - show stats (regardless of subscription status)
VotedStatsView(entry: entry)
} else {
// Not voted yet - show voting buttons
// If subscribed/in trial: buttons record votes
// If trial expired: buttons open app
VotingView(family: family, promptText: entry.promptText, hasSubscription: entry.hasSubscription)
}
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
// MARK: - Voted Stats View (shown after voting)
struct VotedStatsView: View {
@Environment(\.widgetFamily) var family
let entry: VoteWidgetEntry
private var moodTint: MoodTintable.Type {
UserDefaultsStore.moodTintable()
}
private var moodImages: MoodImagable.Type {
UserDefaultsStore.moodMoodImagable()
}
/// Returns "Today" if the voting date is today, otherwise returns formatted date like "Sun, Dec 28th"
private var votingDateString: String {
if Calendar.current.isDateInToday(entry.votingDate) {
return String(localized: "Today")
} else {
let dayFormatter = DateFormatter()
dayFormatter.dateFormat = "EEE" // "Sun"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d" // "Dec 28"
let day = dayFormatter.string(from: entry.votingDate)
let date = dateFormatter.string(from: entry.votingDate)
return "\(day), \(date)"
}
}
var body: some View {
if family == .systemSmall {
smallLayout
} else {
mediumLayout
}
}
// MARK: - Small: Centered mood with checkmark and date
private var smallLayout: some View {
VStack(spacing: 8) {
if let mood = entry.todaysMood {
// Large centered mood icon
ZStack(alignment: .bottomTrailing) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 56, height: 56)
.foregroundColor(moodTint.color(forMood: mood))
// Checkmark badge
Image(systemName: "checkmark.circle.fill")
.font(.headline)
.foregroundColor(.green)
.background(Circle().fill(.white).frame(width: 14, height: 14))
.offset(x: 4, y: 4)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(String(localized: "Mood logged: \(mood.strValue)"))
Text(votingDateString)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
if let stats = entry.stats {
Text("\(stats.totalEntries) entries")
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(12)
}
// MARK: - Medium: Mood + stats bar
private var mediumLayout: some View {
HStack(alignment: .top, spacing: 20) {
if let mood = entry.todaysMood {
// Left: Mood display
VStack(spacing: 6) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
.foregroundColor(moodTint.color(forMood: mood))
.accessibilityLabel(mood.strValue)
Text(mood.widgetDisplayName)
.font(.subheadline.weight(.semibold))
.foregroundColor(moodTint.color(forMood: mood))
Text(votingDateString)
.font(.caption2)
.foregroundStyle(.secondary)
}
// Right: Stats with progress bar aligned under title
if let stats = entry.stats {
VStack(alignment: .leading, spacing: 10) {
Text("\(stats.totalEntries) entries")
.font(.headline.weight(.semibold))
.foregroundStyle(.primary)
// Mini mood breakdown
HStack(spacing: 6) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
let count = stats.moodCounts[mood, default: 0]
if count > 0 {
HStack(spacing: 2) {
Circle()
.fill(moodTint.color(forMood: mood))
.frame(width: 8, height: 8)
Text("\(count)")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
}
// Progress bar - aligned with title
GeometryReader { geo in
HStack(spacing: 1) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { m in
let percentage = stats.percentage(for: m)
if percentage > 0 {
RoundedRectangle(cornerRadius: 2)
.fill(moodTint.color(forMood: m))
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
}
}
}
}
.frame(height: 10)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.padding()
}
}
// MARK: - Non-Subscriber View
struct NonSubscriberView: View {
var body: some View {
Link(destination: URL(string: "reflect://subscribe")!) {
VStack(spacing: 8) {
Image(systemName: "heart.fill")
.font(.largeTitle)
.foregroundStyle(.pink)
Text("Track Your Mood")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.minimumScaleFactor(0.8)
Text("Tap to subscribe")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding(12)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.accessibilityLabel(String(localized: "Track Your Mood"))
.accessibilityHint(String(localized: "Tap to open app and subscribe"))
}
}
// MARK: - Preview Helpers
private enum VoteWidgetPreviewHelpers {
static let sampleStats = MoodStats(
totalEntries: 30,
moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]
)
static let largeStats = MoodStats(
totalEntries: 100,
moodCounts: [.great: 35, .good: 40, .average: 15, .bad: 7, .horrible: 3]
)
}
// MARK: - Small Widget Previews
#Preview("Vote Small - Not Voted", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: "How are you feeling today?"
)
}
#Preview("Vote Small - Voted Great", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .great,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Good", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .good,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Average", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .average,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Bad", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .bad,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Horrible", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .horrible,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Non-Subscriber", as: .systemSmall) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: false,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: ""
)
}
// MARK: - Medium Widget Previews
#Preview("Vote Medium - Not Voted", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: "How are you feeling today?"
)
}
#Preview("Vote Medium - Voted Great", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .great,
stats: VoteWidgetPreviewHelpers.largeStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Good", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .good,
stats: VoteWidgetPreviewHelpers.largeStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Average", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .average,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Bad", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .bad,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Horrible", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .horrible,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Medium - Non-Subscriber", as: .systemMedium) {
ReflectVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
votingDate: Date(),
hasSubscription: false,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: ""
)
}