From 6adef2d6fc40ef78d4af88bd91fa4f50cddcd9bd Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 13 Dec 2025 09:18:57 -0600 Subject: [PATCH] different home screen layouts --- .claude/settings.local.json | 7 +- Shared/Models/UserDefaultsStore.swift | 23 ++ .../Views/CustomizeView/CustomizeView.swift | 107 +++++++++ Shared/Views/DayView/DayView.swift | 41 +++- Shared/Views/EntryListView.swift | 219 +++++++++++++++++- 5 files changed, 382 insertions(+), 15 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cc6b530..83d8af4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,12 @@ "allow": [ "Bash(find:*)", "Bash(xcodebuild:*)", - "Bash(cat:*)" + "Bash(cat:*)", + "Bash(grep:*)", + "WebSearch", + "WebFetch(domain:swiftwithmajid.com)", + "WebFetch(domain:azamsharp.com)", + "WebFetch(domain:www.createwithswift.com)" ] } } diff --git a/Shared/Models/UserDefaultsStore.swift b/Shared/Models/UserDefaultsStore.swift index f08a934..896d560 100644 --- a/Shared/Models/UserDefaultsStore.swift +++ b/Shared/Models/UserDefaultsStore.swift @@ -23,6 +23,28 @@ enum VotingLayoutStyle: Int, CaseIterable { } } +enum DayViewStyle: Int, CaseIterable { + case classic = 0 // Current card style with gradient icons + case minimal = 1 // Clean, simple flat cards + case compact = 2 // Dense timeline view + case bubble = 3 // Colorful full-width bubbles + case grid = 4 // 3 entries per row grid + + var displayName: String { + switch self { + case .classic: return "Classic" + case .minimal: return "Minimal" + case .compact: return "Compact" + case .bubble: return "Bubble" + case .grid: return "Grid" + } + } + + var isGridLayout: Bool { + self == .grid + } +} + class UserDefaultsStore { enum Keys: String { case savedOnboardingData @@ -45,6 +67,7 @@ class UserDefaultsStore { case hasActiveSubscription case lastVotedDate case votingLayoutStyle + case dayViewStyle case contentViewCurrentSelectedHeaderViewBackDays case contentViewHeaderTag diff --git a/Shared/Views/CustomizeView/CustomizeView.swift b/Shared/Views/CustomizeView/CustomizeView.swift index 4005e88..ccce795 100644 --- a/Shared/Views/CustomizeView/CustomizeView.swift +++ b/Shared/Views/CustomizeView/CustomizeView.swift @@ -64,6 +64,13 @@ struct CustomizeView: View { Divider() + // Day View Style + SettingsRow(title: "Entry Style") { + DayViewStylePickerCompact() + } + + Divider() + // Voting Layout SettingsRow(title: "Voting Layout") { VotingLayoutPickerCompact() @@ -749,6 +756,106 @@ struct SubscriptionBannerView: View { } } +// MARK: - Day View Style Picker +struct DayViewStylePickerCompact: View { + @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic + @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + HStack(spacing: 10) { + ForEach(DayViewStyle.allCases, id: \.rawValue) { style in + Button(action: { + withAnimation(.easeInOut(duration: 0.2)) { + dayViewStyle = style + } + let impactMed = UIImpactFeedbackGenerator(style: .medium) + impactMed.impactOccurred() + EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName]) + }) { + VStack(spacing: 6) { + styleIcon(for: style) + .frame(width: 44, height: 44) + .foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4)) + + Text(style.displayName) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(dayViewStyle == style + ? Color.accentColor.opacity(0.1) + : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) + ) + } + .buttonStyle(.plain) + } + } + } + + @ViewBuilder + private func styleIcon(for style: DayViewStyle) -> some View { + switch style { + case .classic: + // Card with gradient circle and text + HStack(spacing: 6) { + Circle() + .fill(LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .topLeading, endPoint: .bottomTrailing)) + .frame(width: 16, height: 16) + VStack(alignment: .leading, spacing: 2) { + RoundedRectangle(cornerRadius: 1).frame(width: 18, height: 4) + RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3).opacity(0.5) + } + } + case .minimal: + // Simple flat card + HStack(spacing: 6) { + Circle() + .strokeBorder(lineWidth: 1.5) + .frame(width: 14, height: 14) + VStack(alignment: .leading, spacing: 2) { + RoundedRectangle(cornerRadius: 1).frame(width: 18, height: 4) + RoundedRectangle(cornerRadius: 1).frame(width: 10, height: 3).opacity(0.5) + } + } + case .compact: + // Timeline dots with bars + HStack(spacing: 4) { + VStack(spacing: 3) { + Circle().frame(width: 6, height: 6) + Circle().frame(width: 6, height: 6) + Circle().frame(width: 6, height: 6) + } + VStack(spacing: 3) { + RoundedRectangle(cornerRadius: 2).frame(width: 24, height: 8) + RoundedRectangle(cornerRadius: 2).frame(width: 24, height: 8) + RoundedRectangle(cornerRadius: 2).frame(width: 24, height: 8) + } + } + case .bubble: + // Full-width colored bars + VStack(spacing: 4) { + RoundedRectangle(cornerRadius: 4).fill(.green).frame(width: 34, height: 10) + RoundedRectangle(cornerRadius: 4).fill(.yellow).frame(width: 34, height: 10) + RoundedRectangle(cornerRadius: 4).fill(.blue).frame(width: 34, height: 10) + } + case .grid: + // 3x3 grid of circles + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) { + Circle().fill(.green).frame(width: 10, height: 10) + Circle().fill(.yellow).frame(width: 10, height: 10) + Circle().fill(.blue).frame(width: 10, height: 10) + Circle().fill(.orange).frame(width: 10, height: 10) + Circle().fill(.green).frame(width: 10, height: 10) + Circle().fill(.yellow).frame(width: 10, height: 10) + } + } + } +} + struct CustomizeView_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/Shared/Views/DayView/DayView.swift b/Shared/Views/DayView/DayView.swift index 48b6b86..49f0b36 100644 --- a/Shared/Views/DayView/DayView.swift +++ b/Shared/Views/DayView/DayView.swift @@ -22,6 +22,7 @@ struct DayView: View { @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @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 + @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic // 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 @@ -162,13 +163,23 @@ extension DayView { .background(.ultraThinMaterial) } + private var gridColumns: [GridItem] { + [ + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10) + ] + } + + @ViewBuilder private func monthListView(month: Int, year: Int, entries: [MoodEntryModel]) -> some View { - VStack(spacing: 12) { - // for reach all entries - ForEach(entries.sorted(by: { - return $0.forDate > $1.forDate - }), id: \.self) { entry in - if filteredDays.currentFilters.contains(entry.weekDay) { + let filteredEntries = entries.sorted(by: { $0.forDate > $1.forDate }) + .filter { filteredDays.currentFilters.contains($0.weekDay) } + + if dayViewStyle.isGridLayout { + // Grid layout - 3 per row + LazyVGrid(columns: gridColumns, spacing: 10) { + ForEach(filteredEntries, id: \.self) { entry in EntryListView(entry: entry) .contentShape(Rectangle()) .onTapGesture(perform: { @@ -177,9 +188,23 @@ extension DayView { }) } } + .padding(.horizontal, 12) + .padding(.top, 8) + } else { + // Standard vertical layout + VStack(spacing: 12) { + ForEach(filteredEntries, id: \.self) { entry in + EntryListView(entry: entry) + .contentShape(Rectangle()) + .onTapGesture(perform: { + selectedEntry = entry + showUpdateEntryAlert = true + }) + } + } + .padding(.horizontal, 12) + .padding(.top, 8) } - .padding(.horizontal, 12) - .padding(.top, 8) } } diff --git a/Shared/Views/EntryListView.swift b/Shared/Views/EntryListView.swift index f953146..85a76e2 100644 --- a/Shared/Views/EntryListView.swift +++ b/Shared/Views/EntryListView.swift @@ -11,6 +11,7 @@ struct EntryListView: View { @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @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 + @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic @Environment(\.colorScheme) private var colorScheme public let entry: MoodEntryModel @@ -24,10 +25,25 @@ struct EntryListView: View { } var body: some View { + switch dayViewStyle { + case .classic: + classicStyle + case .minimal: + minimalStyle + case .compact: + compactStyle + case .bubble: + bubbleStyle + case .grid: + gridStyle + } + } + + // MARK: - Classic Style (Original) + private var classicStyle: some View { HStack(spacing: 16) { // Large mood icon with gradient background ZStack { - // Gradient circle background Circle() .fill( LinearGradient( @@ -40,7 +56,6 @@ struct EntryListView: View { ) .frame(width: 56, height: 56) - // Icon imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) @@ -49,9 +64,7 @@ struct EntryListView: View { } .shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4) - // Content VStack(alignment: .leading, spacing: 6) { - // Date row HStack(spacing: 6) { Text(Random.weekdayName(fromDate: entry.forDate)) .font(.system(size: 17, weight: .semibold)) @@ -65,7 +78,6 @@ struct EntryListView: View { .foregroundColor(textColor.opacity(0.8)) } - // Mood label with colored badge if isMissing { Text(String(localized: "mood_value_missing_tap_to_add")) .font(.system(size: 14, weight: .medium)) @@ -85,7 +97,6 @@ struct EntryListView: View { Spacer() - // Chevron indicator Image(systemName: "chevron.right") .font(.system(size: 14, weight: .semibold)) .foregroundColor(textColor.opacity(0.3)) @@ -112,6 +123,202 @@ struct EntryListView: View { ) ) } + + // MARK: - Minimal Style + private var minimalStyle: some View { + HStack(spacing: 14) { + // Simple flat circle with icon + Circle() + .fill(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.15)) + .frame(width: 44, height: 44) + .overlay( + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 22, height: 22) + .foregroundColor(isMissing ? .gray : moodColor) + ) + + VStack(alignment: .leading, spacing: 3) { + Text(entry.forDate, format: .dateTime.weekday(.wide).day()) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(textColor) + + if isMissing { + Text(String(localized: "mood_value_missing_tap_to_add")) + .font(.system(size: 13)) + .foregroundColor(.gray) + } else { + Text(entry.moodString) + .font(.system(size: 13, weight: .medium)) + .foregroundColor(moodColor) + } + } + + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? Color(.systemGray6) : .white) + ) + } + + // MARK: - Compact Style (Timeline) + private var compactStyle: some View { + HStack(spacing: 12) { + // Timeline indicator + VStack(spacing: 0) { + Circle() + .fill(isMissing ? Color.gray.opacity(0.4) : moodColor) + .frame(width: 12, height: 12) + } + + // Date column + VStack(alignment: .leading, spacing: 0) { + Text(entry.forDate, format: .dateTime.day()) + .font(.system(size: 20, weight: .bold, design: .rounded)) + .foregroundColor(textColor) + Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(textColor.opacity(0.5)) + .textCase(.uppercase) + } + .frame(width: 36) + + // Mood indicator bar + if isMissing { + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray.opacity(0.15)) + .frame(height: 32) + .overlay( + Text(String(localized: "mood_value_missing_tap_to_add")) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.gray) + ) + } else { + RoundedRectangle(cornerRadius: 6) + .fill(moodColor.opacity(0.2)) + .frame(height: 32) + .overlay( + HStack(spacing: 6) { + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 16, height: 16) + .foregroundColor(moodColor) + + Text(entry.moodString) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(moodColor) + } + ) + } + + Spacer(minLength: 0) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + } + + // MARK: - Bubble Style + private var bubbleStyle: some View { + HStack(spacing: 0) { + // Full-width colored background + HStack(spacing: 14) { + // Icon + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 28, height: 28) + .foregroundColor(isMissing ? .gray : .white) + + VStack(alignment: .leading, spacing: 2) { + Text(entry.forDate, format: .dateTime.weekday(.wide).day()) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(isMissing ? textColor : .white) + + if isMissing { + Text(String(localized: "mood_value_missing_tap_to_add")) + .font(.system(size: 13)) + .foregroundColor(.gray) + } else { + Text(entry.moodString) + .font(.system(size: 13, weight: .medium)) + .foregroundColor(.white.opacity(0.85)) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(isMissing ? textColor.opacity(0.3) : .white.opacity(0.6)) + } + .padding(.horizontal, 18) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: 16) + .fill( + isMissing + ? (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)) + : moodColor + ) + ) + .shadow( + color: isMissing ? .clear : moodColor.opacity(0.35), + radius: 8, + x: 0, + y: 4 + ) + } + } + + // MARK: - Grid Style (3 per row) + private var gridStyle: some View { + VStack(spacing: 6) { + // Mood icon circle + ZStack { + Circle() + .fill( + isMissing + ? Color.gray.opacity(0.2) + : moodColor + ) + .aspectRatio(1, contentMode: .fit) + + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(16) + .foregroundColor(isMissing ? .gray : .white) + } + .shadow( + color: isMissing ? .clear : moodColor.opacity(0.3), + radius: 6, + x: 0, + y: 3 + ) + + // Day number + Text(entry.forDate, format: .dateTime.day()) + .font(.system(size: 16, weight: .bold, design: .rounded)) + .foregroundColor(textColor) + + // Weekday abbreviation + Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(textColor.opacity(0.5)) + .textCase(.uppercase) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(colorScheme == .dark ? Color(.systemGray6) : .white) + ) + } } struct EntryListView_Previews: PreviewProvider {