diff --git a/Shared/Views/MonthView/MonthView.swift b/Shared/Views/MonthView/MonthView.swift index c367d25..8c0b4d2 100644 --- a/Shared/Views/MonthView/MonthView.swift +++ b/Shared/Views/MonthView/MonthView.swift @@ -9,40 +9,36 @@ import SwiftUI struct MonthView: View { @AppStorage(UserDefaultsStore.Keys.needsOnboarding.rawValue, store: GroupUserDefaults.groupDefaults) private var needsOnboarding = true - + @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 @AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle - + @StateObject private var shareImage = StupidAssShareObservableObject() // 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 = StupidAssDetailViewObservableObject() @State private var showingSheet = false @StateObject private var onboardingData = OnboardingDataDataManager.shared @StateObject private var filteredDays = DaysFilterClass.shared - + class StupidAssDetailViewObservableObject: ObservableObject { @Published var fuckingWrapped: MonthDetailView? = nil @Published var showFuckingSheet = false } - - let columns = [ - GridItem(.flexible(minimum: 5, maximum: 400)), - GridItem(.flexible(minimum: 5, maximum: 400)), - GridItem(.flexible(minimum: 5, maximum: 400)), - GridItem(.flexible(minimum: 5, maximum: 400)), - GridItem(.flexible(minimum: 5, maximum: 400)), - GridItem(.flexible(minimum: 5, maximum: 400)), - GridItem(.flexible(minimum: 5, maximum: 400)) - ] - + + // Heatmap-style grid with tight spacing + private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 7) + + private let weekdayLabels = ["S", "M", "T", "W", "T", "F", "S"] + @ObservedObject var viewModel: DayViewViewModel @State private var trialWarningHidden = false @State private var showSubscriptionStore = false @@ -54,26 +50,35 @@ struct MonthView: View { .padding() } else { ScrollView { - VStack(spacing: 5) { - ForEach(viewModel.grouped.sorted(by: { $0.key < $1.key }), id: \.key) { year, months in - - // for reach month - ForEach(months.sorted(by: { $0.key < $1.key }), id: \.key) { month, entries in - Section() { - homeViewTwoMonthListView(month: month, year: year, entries: entries) - } - } - .padding(.bottom) - } - .padding() - .background( - RoundedRectangle(cornerRadius: 10) - .foregroundColor( - theme.currentTheme.secondaryBGColor + VStack(spacing: 16) { + ForEach(viewModel.grouped.sorted(by: { $0.key > $1.key }), id: \.key) { year, months in + // for each month + ForEach(months.sorted(by: { $0.key > $1.key }), id: \.key) { month, entries in + MonthCard( + month: month, + year: year, + entries: entries, + moodTint: moodTint, + imagePack: imagePack, + textColor: textColor, + theme: theme, + filteredDays: filteredDays.currentFilters, + onTap: { + let detailView = MonthDetailView( + monthInt: month, + yearInt: year, + entries: entries, + parentViewModel: viewModel + ) + selectedDetail.fuckingWrapped = detailView + selectedDetail.showFuckingSheet = true + } ) - ) + } + } } - .padding([.leading, .trailing]) + .padding(.horizontal) + .padding(.bottom, 100) .background( GeometryReader { proxy in let offset = proxy.frame(in: .named("scroll")).minY @@ -84,6 +89,10 @@ struct MonthView: View { .disabled(iapManager.shouldShowPaywall) } + // Hidden text to trigger updates when custom tint changes + Text(String(customMoodTintUpdateNumber)) + .hidden() + if iapManager.shouldShowPaywall { // Paywall overlay - tap to show subscription store Color.black.opacity(0.3) @@ -141,14 +150,173 @@ struct MonthView: View { } } } - - + + func didDismiss() { selectedDetail.showFuckingSheet = false selectedDetail.fuckingWrapped = nil } } +// MARK: - Month Card Component +struct MonthCard: View { + let month: Int + let year: Int + let entries: [MoodEntry] + let moodTint: MoodTints + let imagePack: MoodImages + let textColor: Color + let theme: Theme + let filteredDays: [Int] + let onTap: () -> Void + + @State private var showStats = true + + 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 = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7]) + return Random.createTotalPerc(fromEntries: monthEntries) + } + + var body: some View { + VStack(spacing: 0) { + // Month Header + Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) { + HStack { + Text("\(Random.monthName(fromMonthInt: month)) \(String(year))") + .font(.title3.bold()) + .foregroundColor(textColor) + + Spacer() + + Image(systemName: showStats ? "chevron.up" : "chevron.down") + .font(.caption.weight(.semibold)) + .foregroundColor(textColor.opacity(0.5)) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .buttonStyle(.plain) + + // Weekday Labels + HStack(spacing: 2) { + ForEach(weekdayLabels, id: \.self) { day in + Text(day) + .font(.caption2.weight(.medium)) + .foregroundColor(textColor.opacity(0.5)) + .frame(maxWidth: .infinity) + } + } + .padding(.horizontal, 16) + .padding(.bottom, 6) + + // Heatmap Grid + LazyVGrid(columns: heatmapColumns, spacing: 2) { + ForEach(entries, id: \.self) { entry in + HeatmapCell( + entry: entry, + moodTint: moodTint, + isFiltered: filteredDays.contains(Int(entry.weekDay)) + ) + } + } + .padding(.horizontal, 16) + .padding(.bottom, 12) + + // Bar Chart Stats (collapsible) + if showStats { + Divider() + .padding(.horizontal, 16) + + MoodBarChart(metrics: metrics, moodTint: moodTint, imagePack: imagePack) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .transition(.opacity.combined(with: .move(edge: .top))) + } + } + .background( + RoundedRectangle(cornerRadius: 16) + .fill(theme.currentTheme.secondaryBGColor) + ) + .contentShape(Rectangle()) + .onTapGesture { + onTap() + } + } +} + +// MARK: - Heatmap Cell +struct HeatmapCell: View { + let entry: MoodEntry + let moodTint: MoodTints + let isFiltered: Bool + + var body: some View { + RoundedRectangle(cornerRadius: 4) + .fill(cellColor) + .aspectRatio(1, contentMode: .fit) + } + + private var cellColor: Color { + if entry.mood == .placeholder { + return Color.gray.opacity(0.1) + } else if entry.mood == .missing { + return Color.gray.opacity(0.25) + } else if !isFiltered { + return Color.gray.opacity(0.1) + } else { + return moodTint.color(forMood: entry.mood) + } + } +} + +// MARK: - Mini Bar Chart +struct MoodBarChart: View { + let metrics: [MoodMetrics] + let moodTint: MoodTints + let imagePack: MoodImages + + var body: some View { + VStack(spacing: 8) { + ForEach(metrics.filter { $0.total > 0 }) { metric in + HStack(spacing: 10) { + // Mood icon + imagePack.icon(forMood: metric.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18, height: 18) + .foregroundColor(moodTint.color(forMood: metric.mood)) + + // Bar + GeometryReader { geo in + ZStack(alignment: .leading) { + // Background track + RoundedRectangle(cornerRadius: 4) + .fill(Color.gray.opacity(0.15)) + + // Filled bar + RoundedRectangle(cornerRadius: 4) + .fill(moodTint.color(forMood: metric.mood)) + .frame(width: max(4, geo.size.width * CGFloat(metric.percent / 100))) + } + } + .frame(height: 10) + + // Count and percentage + Text("\(metric.total)") + .font(.caption.weight(.semibold)) + .foregroundColor(moodTint.color(forMood: metric.mood)) + .frame(width: 28, alignment: .trailing) + } + } + } + } +} + +// MARK: - Legacy support for settings button extension MonthView { private var settingsButtonView: some View { HStack { @@ -171,104 +339,6 @@ extension MonthView { } } -// view that make up the list body -extension MonthView { - private func monthCountView(forMonth month: Int, year: Int) -> [MoodMetrics] { - let (startDate, endDate) = Date.dateRange(monthInt: month, yearInt: year) - let entries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7]) - return Random.createTotalPerc(fromEntries: entries) - } - - - private func homeViewTwoSectionHeaderView(month: Int, year: Int) -> some View { - ZStack { - HStack { - Text("\(Random.monthName(fromMonthInt: month)) \(String(year))") - .font(.body) - .foregroundColor(textColor) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - - ForEach(monthCountView(forMonth: month, year: year)) { - Text("\($0.total)") - .font(.body) - .fontWeight(.bold) - .foregroundColor($0.mood.color) - } - } - Text(String(customMoodTintUpdateNumber)) - .hidden() - } - } - - private func shareViewImage(month: Int, year: Int, entries: [MoodEntry]) -> some View { - ZStack { - VStack { - HStack { - homeViewTwoSectionHeaderView(month: month, year: year) - } - Divider() - LazyVGrid(columns: columns, spacing: 15) { - ForEach(entries, id: \.self) { entry in - shape.view(withText: Text(""), bgColor: entry.mood == .placeholder ? .clear : moodTint.color(forMood: entry.mood), - textColor: .clear) - .frame(minHeight: 25, idealHeight: 25, maxHeight: 50, alignment: .center) - } - } - Spacer() - } - .padding() - .background( - RoundedRectangle(cornerRadius: 10) - .foregroundColor( - theme.currentTheme.secondaryBGColor - ) - ) - .padding() - } - .background( - theme.currentTheme.bg - ) - .padding(.bottom, 55) - } - - private func homeViewTwoMonthListView(month: Int, year: Int, entries: [MoodEntry]) -> some View { - VStack { - HStack { - homeViewTwoSectionHeaderView(month: month, year: year) - } - Divider() - LazyVGrid(columns: columns, spacing: 15) { - ForEach(entries, id: \.self) { entry in - if filteredDays.currentFilters.contains(Int(entry.weekDay)) { - shape.view(withText: Text(""), - bgColor: entry.mood == .placeholder ? .clear : moodTint.color(forMood: entry.mood), - textColor: .clear) - .frame(minHeight: 25, idealHeight: 25, maxHeight: 50, alignment: .center) - } else { - shape.view(withText: Text(""), - bgColor: .clear, - textColor: .clear) - .frame(minHeight: 25, idealHeight: 25, maxHeight: 50, alignment: .center) - } - } - } - } - .contentShape(Rectangle()) - .onTapGesture{ - let deailView = MonthDetailView(monthInt: month, - yearInt: year, - entries: entries, - parentViewModel: viewModel) - - selectedDetail.fuckingWrapped = deailView - selectedDetail.showFuckingSheet = true - } - } -} - struct MonthView_Previews: PreviewProvider { static var previews: some View { MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true))