From 15eea92b79ae89874f7274ee797890d95ec7a888 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 20 Dec 2025 01:26:52 -0600 Subject: [PATCH] Add 3 new DayViewStyles: 3D, Motion, and Micro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **3D Style**: Cards with layered shadows and perspective depth, floating icons with highlights, and 3D text shadows - **Motion Style**: Accelerometer-driven parallax effect using CoreMotion. Floating orbs and elements shift as device tilts - **Micro Style**: Ultra compact single-line entries for maximum density. Tiny dots, abbreviated dates, and minimal spacing Each style includes: - Entry view implementation in EntryListView.swift - Section header in DayView.swift - Preview icon in CustomizeView.swift 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Shared/Models/UserDefaultsStore.swift | 6 + .../Views/CustomizeView/CustomizeView.swift | 80 ++++ Shared/Views/DayView/DayView.swift | 199 +++++++++ Shared/Views/EntryListView.swift | 400 ++++++++++++++++++ 4 files changed, 685 insertions(+) diff --git a/Shared/Models/UserDefaultsStore.swift b/Shared/Models/UserDefaultsStore.swift index 69ca313..3aec963 100644 --- a/Shared/Models/UserDefaultsStore.swift +++ b/Shared/Models/UserDefaultsStore.swift @@ -43,6 +43,9 @@ enum DayViewStyle: Int, CaseIterable { case pattern = 14 // Mood icons as repeating background pattern case leather = 15 // Skeuomorphic leather with stitching case glass = 16 // iOS 26 liquid glass with variable blur + case threeD = 17 // 3D card with perspective and depth + case motion = 18 // Accelerometer-driven parallax effect + case micro = 19 // Ultra compact single-line entries var displayName: String { switch self { @@ -63,6 +66,9 @@ enum DayViewStyle: Int, CaseIterable { case .pattern: return "Pattern" case .leather: return "Leather" case .glass: return "Glass" + case .threeD: return "3D" + case .motion: return "Motion" + case .micro: return "Micro" } } diff --git a/Shared/Views/CustomizeView/CustomizeView.swift b/Shared/Views/CustomizeView/CustomizeView.swift index 6f1e3b7..52a78a7 100644 --- a/Shared/Views/CustomizeView/CustomizeView.swift +++ b/Shared/Views/CustomizeView/CustomizeView.swift @@ -1153,6 +1153,86 @@ struct DayViewStylePickerCompact: View { .offset(x: -6) .blur(radius: 2) } + case .threeD: + // 3D depth effect + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill(Color.black.opacity(0.2)) + .frame(width: 34, height: 24) + .offset(x: 3, y: 3) + RoundedRectangle(cornerRadius: 6) + .fill(Color.black.opacity(0.1)) + .frame(width: 34, height: 24) + .offset(x: 1.5, y: 1.5) + RoundedRectangle(cornerRadius: 6) + .fill( + LinearGradient( + colors: [.white, Color(.systemGray6)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 34, height: 24) + Circle() + .fill(.blue) + .frame(width: 10, height: 10) + .offset(x: -6) + .shadow(color: .black.opacity(0.3), radius: 2, x: 1, y: 2) + } + case .motion: + // Accelerometer motion effect + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill( + LinearGradient( + colors: [.blue.opacity(0.3), .purple.opacity(0.3)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 36, height: 26) + Circle() + .fill(.purple.opacity(0.4)) + .frame(width: 16, height: 16) + .offset(x: 8, y: -4) + .blur(radius: 3) + Circle() + .fill(.blue.opacity(0.4)) + .frame(width: 12, height: 12) + .offset(x: -6, y: 4) + .blur(radius: 2) + Image(systemName: "gyroscope") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.white) + } + case .micro: + // Ultra compact micro style + VStack(spacing: 2) { + HStack(spacing: 3) { + Circle() + .fill(.green) + .frame(width: 4, height: 4) + RoundedRectangle(cornerRadius: 1) + .fill(Color.gray.opacity(0.3)) + .frame(width: 20, height: 4) + } + HStack(spacing: 3) { + Circle() + .fill(.orange) + .frame(width: 4, height: 4) + RoundedRectangle(cornerRadius: 1) + .fill(Color.gray.opacity(0.3)) + .frame(width: 20, height: 4) + } + HStack(spacing: 3) { + Circle() + .fill(.blue) + .frame(width: 4, height: 4) + RoundedRectangle(cornerRadius: 1) + .fill(Color.gray.opacity(0.3)) + .frame(width: 20, height: 4) + } + } } } } diff --git a/Shared/Views/DayView/DayView.swift b/Shared/Views/DayView/DayView.swift index f50c346..8c27484 100644 --- a/Shared/Views/DayView/DayView.swift +++ b/Shared/Views/DayView/DayView.swift @@ -24,6 +24,7 @@ struct DayView: View { @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 // 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 @@ -158,6 +159,12 @@ extension DayView { leatherSectionHeader(month: month, year: year) case .glass: glassSectionHeader(month: month, year: year) + case .threeD: + threeDSectionHeader(month: month, year: year) + case .motion: + motionSectionHeader(month: month, year: year) + case .micro: + microSectionHeader(month: month, year: year) default: defaultSectionHeader(month: month, year: year) } @@ -745,6 +752,198 @@ extension DayView { .frame(height: 64) } + // MARK: - 3D Style Section Header + private func threeDSectionHeader(month: Int, year: Int) -> some View { + ZStack { + // Deep shadow layer + RoundedRectangle(cornerRadius: 16) + .fill(Color.black.opacity(0.2)) + .offset(x: 5, y: 7) + + // Mid shadow layer + RoundedRectangle(cornerRadius: 16) + .fill(Color.black.opacity(0.1)) + .offset(x: 3, y: 4) + + // Main card + HStack(spacing: 16) { + // 3D month number + ZStack { + Text(String(format: "%02d", month)) + .font(.system(size: 32, weight: .black, design: .rounded)) + .foregroundColor(Color.black.opacity(0.15)) + .offset(x: 3, y: 3) + + Text(String(format: "%02d", month)) + .font(.system(size: 32, weight: .black, design: .rounded)) + .foregroundColor(textColor) + } + + VStack(alignment: .leading, spacing: 2) { + Text(Random.monthName(fromMonthInt: month)) + .font(.system(size: 20, weight: .bold)) + .foregroundColor(textColor) + .shadow(color: Color.black.opacity(0.1), radius: 0, x: 1, y: 1) + + Text(String(year)) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(textColor.opacity(0.5)) + } + + Spacer() + + // 3D cube icon + ZStack { + Image(systemName: "cube.fill") + .font(.system(size: 22)) + .foregroundColor(Color.black.opacity(0.15)) + .offset(x: 2, y: 2) + + Image(systemName: "cube.fill") + .font(.system(size: 22)) + .foregroundColor(textColor.opacity(0.4)) + } + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(colorScheme == .dark ? Color(.systemGray6) : .white) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke( + LinearGradient( + colors: [Color.white.opacity(0.5), Color.clear, Color.black.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 2 + ) + ) + } + .frame(height: 72) + } + + // MARK: - Motion Style Section Header + private func motionSectionHeader(month: Int, year: Int) -> some View { + ZStack { + // Animated gradient background + RoundedRectangle(cornerRadius: 18) + .fill( + LinearGradient( + colors: [ + Color.blue.opacity(0.15), + Color.purple.opacity(0.1), + Color.pink.opacity(0.1) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + + // Floating orbs + GeometryReader { geo in + Circle() + .fill( + RadialGradient( + colors: [Color.blue.opacity(0.3), Color.clear], + center: .center, + startRadius: 0, + endRadius: 30 + ) + ) + .frame(width: 60, height: 60) + .offset(x: geo.size.width * 0.7, y: -10) + + Circle() + .fill( + RadialGradient( + colors: [Color.purple.opacity(0.25), Color.clear], + center: .center, + startRadius: 0, + endRadius: 20 + ) + ) + .frame(width: 40, height: 40) + .offset(x: 30, y: geo.size.height * 0.5) + } + + HStack(spacing: 16) { + // Motion icon + ZStack { + Circle() + .fill( + LinearGradient( + colors: [Color.blue.opacity(0.5), Color.purple.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 44, height: 44) + + Image(systemName: "gyroscope") + .font(.system(size: 22, weight: .medium)) + .foregroundColor(.white) + } + .shadow(color: Color.purple.opacity(0.3), radius: 8, x: 0, y: 4) + + VStack(alignment: .leading, spacing: 2) { + Text(Random.monthName(fromMonthInt: month)) + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(textColor) + + Text(String(year)) + .font(.system(size: 13, weight: .medium)) + .foregroundColor(textColor.opacity(0.5)) + } + + Spacer() + + // Tilt indicator + Image(systemName: "iphone.gen3.radiowaves.left.and.right") + .font(.system(size: 18)) + .foregroundColor(textColor.opacity(0.3)) + } + .padding(.horizontal, 18) + } + .frame(height: 68) + .clipShape(RoundedRectangle(cornerRadius: 18)) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + } + + // MARK: - Micro Style Section Header + private func microSectionHeader(month: Int, year: Int) -> some View { + HStack(spacing: 8) { + // Minimal colored bar + RoundedRectangle(cornerRadius: 2) + .fill(textColor.opacity(0.3)) + .frame(width: 3, height: 16) + + Text("\(Random.monthName(fromMonthInt: month).prefix(3).uppercased())") + .font(.system(size: 11, weight: .bold, design: .monospaced)) + .foregroundColor(textColor.opacity(0.6)) + + Text("•") + .font(.system(size: 8)) + .foregroundColor(textColor.opacity(0.3)) + + Text(String(year)) + .font(.system(size: 11, weight: .medium, design: .monospaced)) + .foregroundColor(textColor.opacity(0.4)) + + // Thin separator line + Rectangle() + .fill(textColor.opacity(0.1)) + .frame(height: 1) + } + .padding(.horizontal, 14) + .padding(.vertical, 8) + } + private var gridColumns: [GridItem] { [ GridItem(.flexible(), spacing: 10), diff --git a/Shared/Views/EntryListView.swift b/Shared/Views/EntryListView.swift index 6efe200..86374b3 100644 --- a/Shared/Views/EntryListView.swift +++ b/Shared/Views/EntryListView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CoreMotion struct EntryListView: View { @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @@ -60,6 +61,12 @@ struct EntryListView: View { leatherStyle case .glass: glassStyle + case .threeD: + threeDStyle + case .motion: + motionStyle + case .micro: + microStyle } } @@ -1648,6 +1655,399 @@ struct EntryListView: View { ) .shadow(color: Color.black.opacity(0.1), radius: 20, x: 0, y: 10) } + + // MARK: - 3D Style + private var threeDStyle: some View { + let dayNumber = Calendar.current.component(.day, from: entry.forDate) + + return ZStack { + // Back shadow layer for depth + RoundedRectangle(cornerRadius: 20) + .fill(Color.black.opacity(0.15)) + .offset(x: 4, y: 6) + + // Middle layer + RoundedRectangle(cornerRadius: 20) + .fill( + LinearGradient( + colors: isMissing + ? [Color.gray.opacity(0.2), Color.gray.opacity(0.1)] + : [moodColor.opacity(0.3), moodColor.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .offset(x: 2, y: 3) + + // Main card with 3D transform + HStack(spacing: 16) { + // 3D floating icon + ZStack { + // Icon shadow + Circle() + .fill(Color.black.opacity(0.2)) + .frame(width: 52, height: 52) + .offset(x: 3, y: 4) + + // Icon background + Circle() + .fill( + LinearGradient( + colors: isMissing + ? [Color.gray.opacity(0.4), Color.gray.opacity(0.2)] + : [moodColor, moodColor.opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 52, height: 52) + + // Highlight + Circle() + .fill( + LinearGradient( + colors: [Color.white.opacity(0.4), Color.clear], + startPoint: .topLeading, + endPoint: .center + ) + ) + .frame(width: 52, height: 52) + + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 28, height: 28) + .foregroundColor(.white) + .shadow(color: Color.black.opacity(0.3), radius: 2, x: 1, y: 2) + } + + VStack(alignment: .leading, spacing: 6) { + // Large day number with 3D effect + Text("\(dayNumber)") + .font(.system(size: 36, weight: .black, design: .rounded)) + .foregroundColor(isMissing ? .gray : moodColor) + .shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.4), radius: 0, x: 2, y: 2) + + HStack(spacing: 4) { + Text(Random.weekdayName(fromDate: entry.forDate)) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(textColor.opacity(0.8)) + + if !isMissing { + Text("•") + .foregroundColor(textColor.opacity(0.4)) + Text(entry.moodString) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(moodColor) + } + } + } + + Spacer() + + // 3D arrow + ZStack { + Image(systemName: "chevron.right") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(Color.black.opacity(0.2)) + .offset(x: 2, y: 2) + + Image(systemName: "chevron.right") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(textColor.opacity(0.5)) + } + } + .padding(.horizontal, 18) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(colorScheme == .dark ? Color(.systemGray6) : .white) + ) + .overlay( + // Top-left highlight for 3D effect + RoundedRectangle(cornerRadius: 20) + .stroke( + LinearGradient( + colors: [Color.white.opacity(0.6), Color.clear, Color.black.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 2 + ) + ) + } + .frame(height: 88) + } + + // MARK: - Motion Style (Accelerometer Parallax) + private var motionStyle: some View { + MotionCardView( + entry: entry, + imagePack: imagePack, + moodTint: moodTint, + textColor: textColor, + colorScheme: colorScheme, + isMissing: isMissing, + moodColor: moodColor + ) + } + + // MARK: - Micro Style (Ultra Compact) + private var microStyle: some View { + HStack(spacing: 10) { + // Tiny mood indicator dot + Circle() + .fill(isMissing ? Color.gray.opacity(0.4) : moodColor) + .frame(width: 8, height: 8) + + // Compact icon + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18, height: 18) + .foregroundColor(isMissing ? .gray : moodColor) + + // Date - very compact + Text(entry.forDate, format: .dateTime.month(.abbreviated).day()) + .font(.system(size: 13, weight: .medium, design: .monospaced)) + .foregroundColor(textColor.opacity(0.7)) + + // Weekday initial + Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3))) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(textColor.opacity(0.4)) + .frame(width: 28) + + Spacer() + + // Mood as tiny pill + if !isMissing { + Text(entry.moodString) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(moodColor) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background( + Capsule() + .fill(moodColor.opacity(0.15)) + ) + } else { + Text("tap") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.gray.opacity(0.6)) + } + + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(textColor.opacity(0.25)) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(colorScheme == .dark ? Color(.systemGray6).opacity(0.6) : Color.white.opacity(0.8)) + ) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke( + isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2), + lineWidth: 0.5 + ) + ) + } +} + +// MARK: - Motion Card with Accelerometer +struct MotionCardView: View { + let entry: MoodEntryModel + let imagePack: MoodImages + let moodTint: MoodTints + let textColor: Color + let colorScheme: ColorScheme + let isMissing: Bool + let moodColor: Color + + @StateObject private var motionManager = MotionManager() + + var body: some View { + let dayNumber = Calendar.current.component(.day, from: entry.forDate) + + ZStack { + // Background with parallax offset + RoundedRectangle(cornerRadius: 22) + .fill( + LinearGradient( + colors: isMissing + ? [Color.gray.opacity(0.1), Color.gray.opacity(0.05)] + : [moodColor.opacity(0.2), moodColor.opacity(0.05)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .offset( + x: motionManager.xOffset * 0.5, + y: motionManager.yOffset * 0.5 + ) + + // Floating orbs that move with device + GeometryReader { geo in + Circle() + .fill( + RadialGradient( + colors: isMissing + ? [Color.gray.opacity(0.3), Color.clear] + : [moodColor.opacity(0.4), Color.clear], + center: .center, + startRadius: 0, + endRadius: 40 + ) + ) + .frame(width: 80, height: 80) + .offset( + x: geo.size.width * 0.7 + motionManager.xOffset * 2, + y: -10 + motionManager.yOffset * 2 + ) + + Circle() + .fill( + RadialGradient( + colors: isMissing + ? [Color.gray.opacity(0.2), Color.clear] + : [moodColor.opacity(0.3), Color.clear], + center: .center, + startRadius: 0, + endRadius: 30 + ) + ) + .frame(width: 50, height: 50) + .offset( + x: 20 - motionManager.xOffset * 1.5, + y: geo.size.height * 0.6 - motionManager.yOffset * 1.5 + ) + } + + // Main content + HStack(spacing: 16) { + // Icon with enhanced parallax + ZStack { + // Glow behind icon + Circle() + .fill(isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.4)) + .frame(width: 60, height: 60) + .blur(radius: 10) + .offset( + x: motionManager.xOffset * 0.8, + y: motionManager.yOffset * 0.8 + ) + + Circle() + .fill( + LinearGradient( + colors: isMissing + ? [Color.gray.opacity(0.5), Color.gray.opacity(0.3)] + : [moodColor.opacity(0.9), moodColor.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 54, height: 54) + .shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.4), radius: 8, x: 0, y: 4) + + imagePack.icon(forMood: entry.mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .foregroundColor(.white) + .offset( + x: -motionManager.xOffset * 0.3, + y: -motionManager.yOffset * 0.3 + ) + } + + VStack(alignment: .leading, spacing: 4) { + // Day with motion + Text("\(dayNumber)") + .font(.system(size: 32, weight: .bold, design: .rounded)) + .foregroundColor(isMissing ? .gray : moodColor) + .offset( + x: motionManager.xOffset * 0.2, + y: motionManager.yOffset * 0.2 + ) + + HStack(spacing: 6) { + Text(Random.weekdayName(fromDate: entry.forDate)) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(textColor.opacity(0.7)) + + if !isMissing { + Circle() + .fill(moodColor) + .frame(width: 4, height: 4) + Text(entry.moodString) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(moodColor) + } + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(textColor.opacity(0.3)) + .offset( + x: motionManager.xOffset * 0.5, + y: 0 + ) + } + .padding(.horizontal, 18) + .padding(.vertical, 14) + } + .frame(height: 90) + .clipShape(RoundedRectangle(cornerRadius: 22)) + .background( + RoundedRectangle(cornerRadius: 22) + .fill(colorScheme == .dark ? Color(.systemGray6) : .white) + ) + .overlay( + RoundedRectangle(cornerRadius: 22) + .stroke( + isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.3), + lineWidth: 1 + ) + ) + .shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.15), radius: 12, x: 0, y: 6) + } +} + +// MARK: - Motion Manager for Accelerometer +class MotionManager: ObservableObject { + private let motionManager = CMMotionManager() + @Published var xOffset: CGFloat = 0 + @Published var yOffset: CGFloat = 0 + + init() { + startMotionUpdates() + } + + private func startMotionUpdates() { + guard motionManager.isDeviceMotionAvailable else { return } + + motionManager.deviceMotionUpdateInterval = 1/60 + motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in + guard let motion = motion, error == nil else { return } + + withAnimation(.interactiveSpring(response: 0.15, dampingFraction: 0.8)) { + // Multiply by factor to make movement more noticeable + self?.xOffset = CGFloat(motion.attitude.roll) * 15 + self?.yOffset = CGFloat(motion.attitude.pitch) * 15 + } + } + } + + deinit { + motionManager.stopDeviceMotionUpdates() + } } struct EntryListView_Previews: PreviewProvider {