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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user