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:
@@ -11,10 +11,12 @@ import CoreMotion
|
||||
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
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
public let entry: MoodEntryModel
|
||||
|
||||
private var moodColor: Color {
|
||||
@@ -30,6 +32,21 @@ struct EntryListView: View {
|
||||
entry.moodValue == Mood.missing.rawValue
|
||||
}
|
||||
|
||||
// MARK: - Cached Date Strings (avoids repeated ICU/Calendar operations)
|
||||
private var dateCache: DateFormattingCache { DateFormattingCache.shared }
|
||||
private var cachedDay: String { dateCache.string(for: entry.forDate, format: .day) }
|
||||
private var cachedWeekdayWide: String { dateCache.string(for: entry.forDate, format: .weekdayWide) }
|
||||
private var cachedWeekdayAbbrev: String { dateCache.string(for: entry.forDate, format: .weekdayAbbreviated) }
|
||||
private var cachedWeekdayWideDay: String { dateCache.string(for: entry.forDate, format: .weekdayWideDay) }
|
||||
private var cachedMonthWide: String { dateCache.string(for: entry.forDate, format: .monthWide) }
|
||||
private var cachedMonthAbbrev: String { dateCache.string(for: entry.forDate, format: .monthAbbreviated) }
|
||||
private var cachedMonthAbbrevDay: String { dateCache.string(for: entry.forDate, format: .monthAbbreviatedDay) }
|
||||
private var cachedMonthAbbrevYear: String { dateCache.string(for: entry.forDate, format: .monthAbbreviatedYear) }
|
||||
private var cachedMonthWideYear: String { dateCache.string(for: entry.forDate, format: .monthWideYear) }
|
||||
private var cachedWeekdayAbbrevMonthAbbrev: String { dateCache.string(for: entry.forDate, format: .weekdayAbbrevMonthAbbrev) }
|
||||
private var cachedWeekdayWideMonthAbbrev: String { dateCache.string(for: entry.forDate, format: .weekdayWideMonthAbbrev) }
|
||||
private var cachedYearMonthDayDigits: String { dateCache.string(for: entry.forDate, format: .yearMonthDayDigits) }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch dayViewStyle {
|
||||
@@ -82,9 +99,7 @@ struct EntryListView: View {
|
||||
}
|
||||
|
||||
private var accessibilityDescription: String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .full
|
||||
let dateString = dateFormatter.string(from: entry.forDate)
|
||||
let dateString = DateFormattingCache.shared.string(for: entry.forDate, format: .dateFull)
|
||||
|
||||
if isMissing {
|
||||
return String(localized: "\(dateString), no mood logged")
|
||||
@@ -196,7 +211,7 @@ struct EntryListView: View {
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||
Text(cachedWeekdayWideDay)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
@@ -233,10 +248,10 @@ struct EntryListView: View {
|
||||
|
||||
// Date column
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
Text(cachedWeekdayAbbrev)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
@@ -293,7 +308,7 @@ struct EntryListView: View {
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||
Text(cachedWeekdayWideDay)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(isMissing ? textColor : moodContrastingTextColor)
|
||||
|
||||
@@ -361,12 +376,12 @@ struct EntryListView: View {
|
||||
)
|
||||
|
||||
// Day number
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Weekday abbreviation
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
Text(cachedWeekdayAbbrev)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
@@ -383,7 +398,7 @@ struct EntryListView: View {
|
||||
private var auraStyle: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Giant day number - the visual hero
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.largeTitle.weight(.black))
|
||||
.foregroundStyle(
|
||||
isMissing
|
||||
@@ -396,7 +411,7 @@ struct EntryListView: View {
|
||||
// Content area with glowing aura
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Weekday with elegant typography
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.caption.weight(.semibold))
|
||||
.textCase(.uppercase)
|
||||
.tracking(2)
|
||||
@@ -449,7 +464,7 @@ struct EntryListView: View {
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Month context
|
||||
Text(entry.forDate, format: .dateTime.month(.wide))
|
||||
Text(cachedMonthWide)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
@@ -522,12 +537,12 @@ struct EntryListView: View {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
// Left column: Giant day number in serif
|
||||
VStack(alignment: .trailing, spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.largeTitle.weight(.regular))
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 80)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated).month(.abbreviated))
|
||||
Text(cachedWeekdayAbbrevMonthAbbrev)
|
||||
.font(.caption2.weight(.regular))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
@@ -641,13 +656,14 @@ struct EntryListView: View {
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
// Scanline overlay for CRT effect
|
||||
VStack(spacing: 0) {
|
||||
ForEach(0..<30, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.white.opacity(0.015))
|
||||
.frame(height: 1)
|
||||
Spacer().frame(height: 2)
|
||||
// Scanline overlay for CRT effect - simplified with gradient stripes
|
||||
Canvas { context, size in
|
||||
let lineHeight: CGFloat = 3
|
||||
var y: CGFloat = 0
|
||||
while y < size.height {
|
||||
let rect = CGRect(x: 0, y: y, width: size.width, height: 1)
|
||||
context.fill(Path(rect), with: .color(.white.opacity(0.015)))
|
||||
y += lineHeight
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
@@ -697,7 +713,7 @@ struct EntryListView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
// Date in cyan monospace
|
||||
Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits))
|
||||
Text(cachedYearMonthDayDigits)
|
||||
.font(.system(.caption, design: .monospaced).weight(.semibold))
|
||||
.foregroundColor(neonCyan)
|
||||
.shadow(color: neonCyan.opacity(0.5), radius: 4, x: 0, y: 0)
|
||||
@@ -722,7 +738,7 @@ struct EntryListView: View {
|
||||
}
|
||||
|
||||
// Weekday in magenta
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.system(.caption2, design: .monospaced).weight(.medium))
|
||||
.foregroundColor(neonMagenta.opacity(0.7))
|
||||
.textCase(.uppercase)
|
||||
@@ -836,18 +852,18 @@ struct EntryListView: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Day number with brush-like weight variation
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.title.weight(.thin))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.month(.wide))
|
||||
Text(cachedMonthWide)
|
||||
.font(.caption2.weight(.light))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
.tracking(2)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.caption2.weight(.light))
|
||||
.foregroundColor(textColor.opacity(0.35))
|
||||
}
|
||||
@@ -984,12 +1000,12 @@ struct EntryListView: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||
Text(cachedMonthAbbrevDay)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
@@ -1037,7 +1053,7 @@ struct EntryListView: View {
|
||||
HStack(spacing: 0) {
|
||||
// Track number column
|
||||
VStack {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.title2.weight(.bold).monospaced())
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
}
|
||||
@@ -1067,7 +1083,7 @@ struct EntryListView: View {
|
||||
|
||||
// Track info
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).month(.abbreviated))
|
||||
Text(cachedWeekdayWideMonthAbbrev)
|
||||
.font(.caption2.weight(.medium).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
@@ -1194,15 +1210,15 @@ struct EntryListView: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Date with organic flow
|
||||
HStack(spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.title.weight(.light))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||
Text(cachedMonthAbbrev)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
Text(cachedWeekdayAbbrev)
|
||||
.font(.caption2.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
@@ -1246,12 +1262,14 @@ struct EntryListView: View {
|
||||
|
||||
// Main note
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Torn paper edge effect
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<20, id: \.self) { i in
|
||||
Rectangle()
|
||||
.fill(isMissing ? Color.gray.opacity(0.3) : moodColor.opacity(0.6))
|
||||
.frame(width: .infinity, height: CGFloat.random(in: 3...6))
|
||||
// Torn paper edge effect - simplified with Canvas
|
||||
Canvas { context, size in
|
||||
let segmentWidth = size.width / 10
|
||||
let color = (isMissing ? Color.gray.opacity(0.3) : moodColor.opacity(0.6))
|
||||
for i in 0..<10 {
|
||||
let height = CGFloat(3 + (i * 7) % 4) // Deterministic pseudo-random heights
|
||||
let rect = CGRect(x: CGFloat(i) * segmentWidth, y: 0, width: segmentWidth, height: height)
|
||||
context.fill(Path(rect), with: .color(color))
|
||||
}
|
||||
}
|
||||
.frame(height: 6)
|
||||
@@ -1259,11 +1277,11 @@ struct EntryListView: View {
|
||||
HStack(spacing: 16) {
|
||||
// Handwritten-style date
|
||||
VStack(alignment: .center, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.title.weight(.light))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
Text(cachedWeekdayAbbrev)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
@@ -1278,7 +1296,7 @@ struct EntryListView: View {
|
||||
// Content area
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Lined paper effect
|
||||
Text(entry.forDate, format: .dateTime.month(.wide).year())
|
||||
Text(cachedMonthWideYear)
|
||||
.font(.caption.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
@@ -1330,11 +1348,11 @@ struct EntryListView: View {
|
||||
HStack(spacing: 0) {
|
||||
// Date column - minimal
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.title.weight(.thin))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
Text(cachedWeekdayAbbrev)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.textCase(.uppercase)
|
||||
@@ -1401,7 +1419,7 @@ struct EntryListView: View {
|
||||
Spacer()
|
||||
|
||||
// Month indicator
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||
Text(cachedMonthAbbrev)
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundColor(isMissing ? .gray : moodContrastingTextColor.opacity(0.7))
|
||||
.padding(.trailing, 16)
|
||||
@@ -1436,7 +1454,7 @@ struct EntryListView: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||
Text(cachedWeekdayWideDay)
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
@@ -1456,7 +1474,7 @@ struct EntryListView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||
Text(cachedMonthAbbrev)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.padding(.horizontal, 10)
|
||||
@@ -1466,35 +1484,30 @@ struct EntryListView: View {
|
||||
.padding(16)
|
||||
.frame(height: 86)
|
||||
.background {
|
||||
// Repeating mood icon pattern background
|
||||
GeometryReader { geo in
|
||||
// Simplified pattern background using Canvas for better performance
|
||||
Canvas { context, size in
|
||||
let iconSize: CGFloat = 20
|
||||
let spacing: CGFloat = 28
|
||||
let cols = Int(geo.size.width / spacing) + 2
|
||||
let rows = Int(geo.size.height / spacing) + 2
|
||||
let patternColor = (isMissing ? Color.gray : moodColor).opacity(0.15)
|
||||
|
||||
ZStack {
|
||||
// Base background color
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
|
||||
|
||||
// Pattern overlay
|
||||
ForEach(0..<rows, id: \.self) { row in
|
||||
ForEach(0..<cols, id: \.self) { col in
|
||||
imagePack.icon(forMood: entry.mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize, height: iconSize)
|
||||
.foregroundColor(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2))
|
||||
.accessibilityHidden(true)
|
||||
.position(
|
||||
x: CGFloat(col) * spacing + (row.isMultiple(of: 2) ? spacing/2 : 0),
|
||||
y: CGFloat(row) * spacing
|
||||
)
|
||||
}
|
||||
// Draw simple circles as pattern instead of complex icons
|
||||
var row = 0
|
||||
var y: CGFloat = 0
|
||||
while y < size.height + spacing {
|
||||
var x: CGFloat = row.isMultiple(of: 2) ? spacing / 2 : 0
|
||||
while x < size.width + spacing {
|
||||
let rect = CGRect(x: x - iconSize/2, y: y - iconSize/2, width: iconSize, height: iconSize)
|
||||
context.fill(Circle().path(in: rect), with: .color(patternColor))
|
||||
x += spacing
|
||||
}
|
||||
y += spacing
|
||||
row += 1
|
||||
}
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.overlay(
|
||||
@@ -1596,12 +1609,12 @@ struct EntryListView: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
|
||||
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||
Text(cachedMonthAbbrevDay)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
|
||||
|
||||
@@ -1728,12 +1741,12 @@ struct EntryListView: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||
Text(cachedMonthAbbrevDay)
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
@@ -1805,7 +1818,7 @@ struct EntryListView: View {
|
||||
entry: entry,
|
||||
imagePack: imagePack,
|
||||
moodTint: moodTint,
|
||||
textColor: textColor,
|
||||
theme: theme,
|
||||
colorScheme: colorScheme,
|
||||
isMissing: isMissing,
|
||||
moodColor: moodColor
|
||||
@@ -1829,7 +1842,7 @@ struct EntryListView: View {
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
// Date - very compact
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||
Text(cachedMonthAbbrevDay)
|
||||
.font(.caption.weight(.medium).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
|
||||
@@ -1883,7 +1896,7 @@ struct EntryListView: View {
|
||||
entry: entry,
|
||||
imagePack: imagePack,
|
||||
moodColor: moodColor,
|
||||
textColor: textColor,
|
||||
theme: theme,
|
||||
colorScheme: colorScheme,
|
||||
isMissing: isMissing
|
||||
)
|
||||
@@ -1895,10 +1908,17 @@ struct OrbitEntryView: View {
|
||||
let entry: MoodEntryModel
|
||||
let imagePack: MoodImages
|
||||
let moodColor: Color
|
||||
let textColor: Color
|
||||
let theme: Theme
|
||||
let colorScheme: ColorScheme
|
||||
let isMissing: Bool
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
// Cached date strings
|
||||
private var cachedDay: String { DateFormattingCache.shared.string(for: entry.forDate, format: .day) }
|
||||
private var cachedWeekdayWide: String { DateFormattingCache.shared.string(for: entry.forDate, format: .weekdayWide) }
|
||||
private var cachedMonthAbbrevYear: String { DateFormattingCache.shared.string(for: entry.forDate, format: .monthAbbreviatedYear) }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Orbital system on left
|
||||
@@ -1970,7 +1990,7 @@ struct OrbitEntryView: View {
|
||||
.frame(width: 28, height: 28)
|
||||
.shadow(color: .black.opacity(0.15), radius: 4)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
Text(cachedDay)
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundColor(.black.opacity(0.7))
|
||||
}
|
||||
@@ -1979,11 +1999,11 @@ struct OrbitEntryView: View {
|
||||
|
||||
private var dateDisplay: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
Text(cachedWeekdayWide)
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).year())
|
||||
Text(cachedMonthAbbrevYear)
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
@@ -2040,15 +2060,19 @@ struct MotionCardView: View {
|
||||
let entry: MoodEntryModel
|
||||
let imagePack: MoodImages
|
||||
let moodTint: MoodTints
|
||||
let textColor: Color
|
||||
let theme: Theme
|
||||
let colorScheme: ColorScheme
|
||||
let isMissing: Bool
|
||||
let moodColor: Color
|
||||
|
||||
@StateObject private var motionManager = MotionManager()
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
// Cached date string
|
||||
private var cachedDay: String { DateFormattingCache.shared.string(for: entry.forDate, format: .day) }
|
||||
|
||||
@ObservedObject private var motionManager = MotionManager.shared
|
||||
|
||||
var body: some View {
|
||||
let dayNumber = Calendar.current.component(.day, from: entry.forDate)
|
||||
|
||||
ZStack {
|
||||
// Background with parallax offset
|
||||
@@ -2145,7 +2169,7 @@ struct MotionCardView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Day with motion
|
||||
Text("\(dayNumber)")
|
||||
Text(cachedDay)
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.offset(
|
||||
@@ -2196,37 +2220,45 @@ struct MotionCardView: View {
|
||||
)
|
||||
)
|
||||
.shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.15), radius: 12, x: 0, y: 6)
|
||||
.onAppear {
|
||||
motionManager.startIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Motion Manager for Accelerometer
|
||||
// MARK: - Motion Manager for Accelerometer (Shared Singleton)
|
||||
class MotionManager: ObservableObject {
|
||||
/// Shared singleton - avoids creating per-cell instances
|
||||
static let shared = MotionManager()
|
||||
|
||||
private let motionManager = CMMotionManager()
|
||||
@Published var xOffset: CGFloat = 0
|
||||
@Published var yOffset: CGFloat = 0
|
||||
|
||||
init() {
|
||||
startMotionUpdates()
|
||||
}
|
||||
private var isRunning = false
|
||||
|
||||
private func startMotionUpdates() {
|
||||
// Respect Reduce Motion preference - skip parallax effect entirely
|
||||
guard motionManager.isDeviceMotionAvailable,
|
||||
private init() {}
|
||||
|
||||
func startIfNeeded() {
|
||||
guard !isRunning,
|
||||
motionManager.isDeviceMotionAvailable,
|
||||
!UIAccessibility.isReduceMotionEnabled else { return }
|
||||
|
||||
motionManager.deviceMotionUpdateInterval = 1/60
|
||||
isRunning = true
|
||||
motionManager.deviceMotionUpdateInterval = 1/30 // Reduced from 60fps to 30fps
|
||||
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 {
|
||||
func stop() {
|
||||
guard isRunning else { return }
|
||||
isRunning = false
|
||||
motionManager.stopDeviceMotionUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user