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:
Trey t
2025-12-30 00:08:01 -06:00
parent 51c5777c03
commit bea2d3bbc9
58 changed files with 1142 additions and 967 deletions

View File

@@ -6,20 +6,15 @@
//
import SwiftUI
import SwiftData
struct YearView: View {
let months = [(0, "J"), (1, "F"), (2,"M"), (3,"A"), (4,"M"), (5, "J"), (6,"J"), (7,"A"), (8,"S"), (9,"O"), (10, "N"), (11,"D")]
@State private var toggle = true
@Query(sort: \MoodEntryModel.forDate, order: .reverse)
private var items: [MoodEntryModel]
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
@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
@EnvironmentObject var iapManager: IAPManager
@StateObject public var viewModel: YearViewModel
@@ -28,6 +23,9 @@ struct YearView: View {
@State private var trialWarningHidden = false
@State private var showSubscriptionStore = false
/// Cached sorted year keys to avoid re-sorting in ForEach on every render
@State private var cachedSortedYearKeys: [Int] = []
// Heatmap-style grid: 12 columns for months
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
@@ -39,13 +37,13 @@ struct YearView: View {
} else {
ScrollView {
VStack(spacing: 16) {
ForEach(Array(self.viewModel.data.keys.sorted(by: >)), id: \.self) { yearKey in
ForEach(cachedSortedYearKeys, id: \.self) { yearKey in
YearCard(
year: yearKey,
yearData: self.viewModel.data[yearKey]!,
yearEntries: self.viewModel.entriesByYear[yearKey] ?? [],
moodTint: moodTint,
imagePack: imagePack,
textColor: textColor,
theme: theme,
filteredDays: filteredDays.currentFilters,
onShare: { image in
@@ -57,6 +55,7 @@ struct YearView: 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
@@ -112,12 +111,12 @@ struct YearView: View {
VStack(spacing: 10) {
Text("See Your Year at a Glance")
.font(.title3.weight(.bold))
.foregroundColor(textColor)
.foregroundColor(theme.currentTheme.labelColor)
.multilineTextAlignment(.center)
Text("Discover your emotional rhythm across the year. Spot trends and celebrate how far you've come.")
.font(.subheadline)
.foregroundColor(textColor.opacity(0.7))
.foregroundColor(theme.currentTheme.labelColor.opacity(0.7))
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
@@ -168,7 +167,16 @@ struct YearView: View {
}
.onAppear(perform: {
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
})
.onChange(of: viewModel.data.keys.count) { _, _ in
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
}
.onChange(of: moodTint) { _, _ in
// Rebuild chart data when mood tint changes to update colors
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
}
.background(
theme.currentTheme.bg
.edgesIgnoringSafeArea(.all)
@@ -179,33 +187,42 @@ struct YearView: View {
}
}
.padding([.top])
.preferredColorScheme(theme.preferredColorScheme)
}
}
// MARK: - Year Card Component
struct YearCard: View {
struct YearCard: View, Equatable {
let year: Int
let yearData: [Int: [DayChartView]]
let yearEntries: [MoodEntryModel]
let moodTint: MoodTints
let imagePack: MoodImages
let textColor: Color
let theme: Theme
let filteredDays: [Int]
let onShare: (UIImage) -> Void
private var textColor: Color { theme.currentTheme.labelColor }
// Equatable conformance to prevent unnecessary re-renders
static func == (lhs: YearCard, rhs: YearCard) -> Bool {
lhs.year == rhs.year &&
lhs.yearEntries.count == rhs.yearEntries.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 months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
private var yearEntries: [MoodEntryModel] {
let firstOfYear = Calendar.current.date(from: DateComponents(year: year, month: 1, day: 1))!
let lastOfYear = Calendar.current.date(from: DateComponents(year: year + 1, month: 1, day: 1))!
return DataController.shared.getData(startDate: firstOfYear, endDate: lastOfYear, includedDays: filteredDays)
}
private var metrics: [MoodMetrics] {
return Random.createTotalPerc(fromEntries: yearEntries)
// 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 totalEntries: Int {
@@ -213,7 +230,7 @@ struct YearCard: View {
}
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 shareableView: some View {
@@ -273,7 +290,7 @@ struct YearCard: View {
// Mood breakdown with bars
VStack(spacing: 14) {
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
ForEach(displayMetrics) { metric in
HStack(spacing: 14) {
Circle()
.fill(moodTint.color(forMood: metric.mood))
@@ -366,12 +383,12 @@ struct YearCard: View {
if showStats {
HStack(spacing: 16) {
// Donut Chart
MoodDonutChart(metrics: metrics, moodTint: moodTint)
MoodDonutChart(metrics: cachedMetrics, moodTint: moodTint)
.frame(width: 100, height: 100)
// Bar Chart
VStack(spacing: 6) {
ForEach(metrics.filter { $0.total > 0 }) { metric in
ForEach(displayMetrics) { metric in
HStack(spacing: 8) {
imagePack.icon(forMood: metric.mood)
.resizable()
@@ -434,6 +451,12 @@ struct YearCard: View {
RoundedRectangle(cornerRadius: 16)
.fill(theme.currentTheme.secondaryBGColor)
)
.onAppear {
// Cache metrics calculation on first appearance
if cachedMetrics.isEmpty {
cachedMetrics = Random.createTotalPerc(fromEntries: yearEntries)
}
}
}
}
@@ -445,9 +468,16 @@ struct YearHeatmapGrid: View {
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
/// Pre-sorted month keys to avoid sorting in ForEach on every render
private var sortedMonthKeys: [Int] {
// This is computed once per yearData change, not on every body access
// since yearData is a let constant passed from parent
Array(yearData.keys.sorted(by: <))
}
var body: some View {
LazyVGrid(columns: heatmapColumns, spacing: 2) {
ForEach(Array(yearData.keys.sorted(by: <)), id: \.self) { monthKey in
ForEach(sortedMonthKeys, id: \.self) { monthKey in
if let monthData = yearData[monthKey] {
MonthColumn(
monthData: monthData,

View File

@@ -16,6 +16,8 @@ class YearViewModel: ObservableObject {
// year, month, items
@Published public private(set) var data = [Int: [Int: [DayChartView]]]()
@Published public private(set) var numberOfRatings: Int = 0
/// Entries organized by year for efficient access
@Published public private(set) var entriesByYear = [Int: [MoodEntryModel]]()
public private(set) var uncategorizedData = [MoodEntryModel]() {
didSet {
self.numberOfRatings = uncategorizedData.count
@@ -44,8 +46,16 @@ class YearViewModel: ObservableObject {
endDate: endDate,
includedDays: selectedDays)
data.removeAll()
entriesByYear.removeAll()
let filledOutData = chartViewBuilder.buildGridData(withData: filteredEntries)
data = filledOutData
uncategorizedData = filteredEntries
// Organize entries by year for efficient access in YearCard
let calendar = Calendar.current
for entry in filteredEntries {
let year = calendar.component(.year, from: entry.forDate)
entriesByYear[year, default: []].append(entry)
}
}
}