Add 3 new DayViewStyles: 3D, Motion, and Micro
- **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 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ enum DayViewStyle: Int, CaseIterable {
|
|||||||
case pattern = 14 // Mood icons as repeating background pattern
|
case pattern = 14 // Mood icons as repeating background pattern
|
||||||
case leather = 15 // Skeuomorphic leather with stitching
|
case leather = 15 // Skeuomorphic leather with stitching
|
||||||
case glass = 16 // iOS 26 liquid glass with variable blur
|
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 {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -63,6 +66,9 @@ enum DayViewStyle: Int, CaseIterable {
|
|||||||
case .pattern: return "Pattern"
|
case .pattern: return "Pattern"
|
||||||
case .leather: return "Leather"
|
case .leather: return "Leather"
|
||||||
case .glass: return "Glass"
|
case .glass: return "Glass"
|
||||||
|
case .threeD: return "3D"
|
||||||
|
case .motion: return "Motion"
|
||||||
|
case .micro: return "Micro"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1153,6 +1153,86 @@ struct DayViewStylePickerCompact: View {
|
|||||||
.offset(x: -6)
|
.offset(x: -6)
|
||||||
.blur(radius: 2)
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct DayView: View {
|
|||||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
@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.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
@AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic
|
@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
|
// 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
|
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||||
@@ -158,6 +159,12 @@ extension DayView {
|
|||||||
leatherSectionHeader(month: month, year: year)
|
leatherSectionHeader(month: month, year: year)
|
||||||
case .glass:
|
case .glass:
|
||||||
glassSectionHeader(month: month, year: year)
|
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:
|
default:
|
||||||
defaultSectionHeader(month: month, year: year)
|
defaultSectionHeader(month: month, year: year)
|
||||||
}
|
}
|
||||||
@@ -745,6 +752,198 @@ extension DayView {
|
|||||||
.frame(height: 64)
|
.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] {
|
private var gridColumns: [GridItem] {
|
||||||
[
|
[
|
||||||
GridItem(.flexible(), spacing: 10),
|
GridItem(.flexible(), spacing: 10),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CoreMotion
|
||||||
|
|
||||||
struct EntryListView: View {
|
struct EntryListView: View {
|
||||||
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||||
@@ -60,6 +61,12 @@ struct EntryListView: View {
|
|||||||
leatherStyle
|
leatherStyle
|
||||||
case .glass:
|
case .glass:
|
||||||
glassStyle
|
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)
|
.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 {
|
struct EntryListView_Previews: PreviewProvider {
|
||||||
|
|||||||
Reference in New Issue
Block a user