Update Neon colors and show color circles in theme picker
- Update NeonMoodTint to use synthwave colors matching Neon voting style (cyan, lime, yellow, orange, magenta) - Replace text label with 5 color circles in theme preview Colors row - Remove unused textColor customization code and picker views - Add .id(moodTint) to Month/Year views for color refresh - Clean up various unused color-related code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,8 @@ struct MonthDetailView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@StateObject private var shareImage = ShareImageStateViewModel()
|
||||
|
||||
|
||||
@@ -12,17 +12,15 @@ struct MonthView: View {
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var labelColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
@StateObject private var shareImage = ShareImageStateViewModel()
|
||||
|
||||
// store a value that gets changed when user updates custom colors to update the view since the moodTint doesn't change
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@StateObject private var selectedDetail = DetailViewStateViewModel()
|
||||
@State private var showingSheet = false
|
||||
@@ -43,8 +41,11 @@ struct MonthView: View {
|
||||
@State private var trialWarningHidden = false
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
/// Cached sorted year/month data to avoid recalculating in ForEach
|
||||
@State private var cachedSortedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] = []
|
||||
|
||||
/// Filters month data to only current month when subscription/trial expired
|
||||
private var filteredMonthData: [Int: [Int: [MoodEntryModel]]] {
|
||||
private func computeFilteredMonthData() -> [Int: [Int: [MoodEntryModel]]] {
|
||||
guard iapManager.shouldShowPaywall else {
|
||||
return viewModel.grouped
|
||||
}
|
||||
@@ -61,6 +62,13 @@ struct MonthView: View {
|
||||
return filtered
|
||||
}
|
||||
|
||||
/// Sorts the filtered month data - called only when source data changes
|
||||
private func computeSortedYearMonthData() -> [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
|
||||
computeFilteredMonthData()
|
||||
.sorted { $0.key > $1.key }
|
||||
.map { (year: $0.key, months: $0.value.sorted { $0.key > $1.key }.map { (month: $0.key, entries: $0.value) }) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if viewModel.hasNoData {
|
||||
@@ -69,23 +77,22 @@ struct MonthView: View {
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(filteredMonthData.sorted(by: { $0.key > $1.key }), id: \.key) { year, months in
|
||||
ForEach(cachedSortedData, id: \.year) { yearData in
|
||||
// for each month
|
||||
ForEach(months.sorted(by: { $0.key > $1.key }), id: \.key) { month, entries in
|
||||
ForEach(yearData.months, id: \.month) { monthData in
|
||||
MonthCard(
|
||||
month: month,
|
||||
year: year,
|
||||
entries: entries,
|
||||
month: monthData.month,
|
||||
year: yearData.year,
|
||||
entries: monthData.entries,
|
||||
moodTint: moodTint,
|
||||
imagePack: imagePack,
|
||||
textColor: textColor,
|
||||
theme: theme,
|
||||
filteredDays: filteredDays.currentFilters,
|
||||
onTap: {
|
||||
let detailView = MonthDetailView(
|
||||
monthInt: month,
|
||||
yearInt: year,
|
||||
entries: entries,
|
||||
monthInt: monthData.month,
|
||||
yearInt: yearData.year,
|
||||
entries: monthData.entries,
|
||||
parentViewModel: viewModel
|
||||
)
|
||||
selectedDetail.selectedItem = detailView
|
||||
@@ -101,6 +108,7 @@ struct MonthView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 100)
|
||||
.id(moodTint) // Force complete refresh when mood tint changes
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
let offset = proxy.frame(in: .named("scroll")).minY
|
||||
@@ -126,10 +134,6 @@ struct MonthView: View {
|
||||
)
|
||||
}
|
||||
|
||||
// Hidden text to trigger updates when custom tint changes
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
|
||||
if iapManager.shouldShowPaywall {
|
||||
// Premium month history prompt - bottom half
|
||||
VStack(spacing: 20) {
|
||||
@@ -160,12 +164,12 @@ struct MonthView: View {
|
||||
VStack(spacing: 10) {
|
||||
Text("Explore Your Mood History")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("See your complete monthly journey. Track patterns and understand what shapes your days.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.foregroundColor(labelColor.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
@@ -231,6 +235,18 @@ struct MonthView: View {
|
||||
trialWarningHidden = value < 0
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
}
|
||||
.onChange(of: viewModel.numberOfItems) { _, _ in
|
||||
// Use numberOfItems as a lightweight proxy for data changes
|
||||
// instead of comparing the entire grouped dictionary
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
}
|
||||
.onChange(of: iapManager.shouldShowPaywall) { _, _ in
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
}
|
||||
.preferredColorScheme(theme.preferredColorScheme)
|
||||
}
|
||||
|
||||
|
||||
@@ -241,31 +257,43 @@ struct MonthView: View {
|
||||
}
|
||||
|
||||
// MARK: - Month Card Component
|
||||
struct MonthCard: View {
|
||||
struct MonthCard: View, Equatable {
|
||||
let month: Int
|
||||
let year: Int
|
||||
let entries: [MoodEntryModel]
|
||||
let moodTint: MoodTints
|
||||
let imagePack: MoodImages
|
||||
let textColor: Color
|
||||
let theme: Theme
|
||||
let filteredDays: [Int]
|
||||
let onTap: () -> Void
|
||||
let onShare: (UIImage) -> Void
|
||||
|
||||
private var labelColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
// Equatable conformance to prevent unnecessary re-renders
|
||||
static func == (lhs: MonthCard, rhs: MonthCard) -> Bool {
|
||||
lhs.month == rhs.month &&
|
||||
lhs.year == rhs.year &&
|
||||
lhs.entries.count == rhs.entries.count &&
|
||||
lhs.moodTint == rhs.moodTint &&
|
||||
lhs.imagePack == rhs.imagePack &&
|
||||
lhs.filteredDays == rhs.filteredDays &&
|
||||
lhs.theme == rhs.theme
|
||||
}
|
||||
|
||||
@State private var showStats = true
|
||||
@State private var cachedMetrics: [MoodMetrics] = []
|
||||
|
||||
private let weekdayLabels = ["S", "M", "T", "W", "T", "F", "S"]
|
||||
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 7)
|
||||
|
||||
private var metrics: [MoodMetrics] {
|
||||
let (startDate, endDate) = Date.dateRange(monthInt: month, yearInt: year)
|
||||
let monthEntries = DataController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7])
|
||||
return Random.createTotalPerc(fromEntries: monthEntries)
|
||||
// Cached filtered/sorted metrics to avoid recalculating in ForEach
|
||||
private var displayMetrics: [MoodMetrics] {
|
||||
cachedMetrics.filter { $0.total > 0 }.sorted { $0.mood.rawValue > $1.mood.rawValue }
|
||||
}
|
||||
|
||||
private var topMood: Mood? {
|
||||
metrics.filter { $0.total > 0 }.max(by: { $0.total < $1.total })?.mood
|
||||
displayMetrics.max(by: { $0.total < $1.total })?.mood
|
||||
}
|
||||
|
||||
private var totalTrackedDays: Int {
|
||||
@@ -277,13 +305,13 @@ struct MonthCard: View {
|
||||
// Header with month/year
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
|
||||
.font(.title.weight(.heavy))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Monthly Mood Wrap")
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(labelColor.opacity(0.6))
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// Top mood highlight
|
||||
@@ -304,7 +332,7 @@ struct MonthCard: View {
|
||||
|
||||
Text("Top Mood")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
|
||||
Text(topMood.strValue.uppercased())
|
||||
.font(.title3.weight(.bold))
|
||||
@@ -318,10 +346,10 @@ struct MonthCard: View {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(totalTrackedDays)")
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
Text("Days Tracked")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
@@ -329,7 +357,7 @@ struct MonthCard: View {
|
||||
|
||||
// Mood breakdown with bars
|
||||
VStack(spacing: 12) {
|
||||
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
|
||||
ForEach(displayMetrics) { metric in
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
@@ -357,7 +385,7 @@ struct MonthCard: View {
|
||||
|
||||
Text("\(Int(metric.percent))%")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
.frame(width: 40, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
@@ -368,7 +396,7 @@ struct MonthCard: View {
|
||||
// App branding
|
||||
Text("ifeel")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.foregroundColor(labelColor.opacity(0.3))
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.frame(width: 400)
|
||||
@@ -389,11 +417,11 @@ struct MonthCard: View {
|
||||
HStack {
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.title3.bold())
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
|
||||
Image(systemName: showStats ? "chevron.up" : "chevron.down")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -406,7 +434,7 @@ struct MonthCard: View {
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(labelColor.opacity(0.6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -418,7 +446,7 @@ struct MonthCard: View {
|
||||
ForEach(weekdayLabels.indices, id: \.self) { index in
|
||||
Text(weekdayLabels[index])
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -443,7 +471,7 @@ struct MonthCard: View {
|
||||
Divider()
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
MoodBarChart(metrics: metrics, moodTint: moodTint, imagePack: imagePack)
|
||||
MoodBarChart(metrics: cachedMetrics, moodTint: moodTint, imagePack: imagePack)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
@@ -457,6 +485,12 @@ struct MonthCard: View {
|
||||
.onTapGesture {
|
||||
onTap()
|
||||
}
|
||||
.onAppear {
|
||||
// Cache metrics calculation on first appearance
|
||||
if cachedMetrics.isEmpty {
|
||||
cachedMetrics = Random.createTotalPerc(fromEntries: entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,9 +521,7 @@ struct HeatmapCell: View {
|
||||
}
|
||||
|
||||
private var formattedDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter.string(from: entry.forDate)
|
||||
DateFormattingCache.shared.string(for: entry.forDate, format: .dateMedium)
|
||||
}
|
||||
|
||||
private var cellColor: Color {
|
||||
|
||||
Reference in New Issue
Block a user