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:
@@ -13,9 +13,10 @@ struct AddMoodHeaderView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@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.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@State var onboardingData = OnboardingDataDataManager.shared.savedOnboardingData
|
||||
|
||||
// Celebration animation state
|
||||
|
||||
@@ -10,8 +10,9 @@ import SwiftUI
|
||||
struct CreateWidgetView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@StateObject private var customWidget: CustomWidgetModel
|
||||
|
||||
@State private var mouth: CustomWidgetMouthOptions = CustomWidgetMouthOptions.defaultOption
|
||||
|
||||
@@ -65,18 +65,8 @@ struct CustomizeContentView: View {
|
||||
|
||||
// APPEARANCE
|
||||
SettingsSection(title: "Appearance") {
|
||||
VStack(spacing: 16) {
|
||||
// Theme
|
||||
SettingsRow(title: "Theme") {
|
||||
ThemePickerCompact()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Text Color
|
||||
SettingsRow(title: "Text Color") {
|
||||
TextColorPickerCompact()
|
||||
}
|
||||
SettingsRow(title: "Theme") {
|
||||
ThemePickerCompact()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,13 +80,6 @@ struct CustomizeContentView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
// Mood Colors
|
||||
SettingsRow(title: "Colors") {
|
||||
TintPickerCompact()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Day View Style
|
||||
SettingsRow(title: "Entry Style") {
|
||||
DayViewStylePickerCompact()
|
||||
@@ -143,7 +126,6 @@ struct CustomizeView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -157,18 +139,8 @@ struct CustomizeView: View {
|
||||
|
||||
// APPEARANCE
|
||||
SettingsSection(title: "Appearance") {
|
||||
VStack(spacing: 16) {
|
||||
// Theme
|
||||
SettingsRow(title: "Theme") {
|
||||
ThemePickerCompact()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Text Color
|
||||
SettingsRow(title: "Text Color") {
|
||||
TextColorPickerCompact()
|
||||
}
|
||||
SettingsRow(title: "Theme") {
|
||||
ThemePickerCompact()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,13 +154,6 @@ struct CustomizeView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
// Mood Colors
|
||||
SettingsRow(title: "Colors") {
|
||||
TintPickerCompact()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Day View Style
|
||||
SettingsRow(title: "Entry Style") {
|
||||
DayViewStylePickerCompact()
|
||||
@@ -237,7 +202,7 @@ struct CustomizeView: View {
|
||||
HStack {
|
||||
Text("Customize")
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@@ -251,13 +216,13 @@ struct SettingsSection<Content: View>: View {
|
||||
@ViewBuilder let content: Content
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(title.uppercased())
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.4))
|
||||
.tracking(0.5)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
@@ -277,13 +242,13 @@ struct SettingsRow<Content: View>: View {
|
||||
let title: String
|
||||
@ViewBuilder let content: Content
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.7))
|
||||
|
||||
content
|
||||
}
|
||||
@@ -294,14 +259,12 @@ struct SettingsRow<Content: View>: View {
|
||||
struct ThemePickerCompact: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 20) {
|
||||
ForEach(Theme.allCases, id: \.rawValue) { aTheme in
|
||||
Button(action: {
|
||||
theme = aTheme
|
||||
changeTextColor(forTheme: aTheme)
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": aTheme.rawValue])
|
||||
}) {
|
||||
VStack(spacing: 8) {
|
||||
@@ -323,7 +286,7 @@ struct ThemePickerCompact: View {
|
||||
|
||||
Text(aTheme.title)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
|
||||
.foregroundColor(theme == aTheme ? .accentColor : theme.currentTheme.labelColor.opacity(0.6))
|
||||
}
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
@@ -331,33 +294,6 @@ struct ThemePickerCompact: View {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private func changeTextColor(forTheme theme: Theme) {
|
||||
if [Theme.iFeel, Theme.system].contains(theme) {
|
||||
let currentSystemScheme = UITraitCollection.current.userInterfaceStyle
|
||||
textColor = currentSystemScheme == .dark ? .white : .black
|
||||
}
|
||||
if theme == Theme.dark { textColor = .white }
|
||||
if theme == Theme.light { textColor = .black }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Text Color Picker
|
||||
struct TextColorPickerCompact: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
ColorPicker("", selection: $textColor)
|
||||
.labelsHidden()
|
||||
|
||||
Text("Sample Text")
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Pack Picker
|
||||
@@ -412,111 +348,10 @@ struct ImagePackPickerCompact: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tint Picker
|
||||
struct TintPickerCompact: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
@StateObject private var customMoodTint = UserDefaultsStore.getCustomMoodTint()
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
ForEach(MoodTints.defaultOptions, id: \.rawValue) { tint in
|
||||
Button(action: {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
moodTint = tint
|
||||
EventLogger.log(event: "change_mood_tint_id", withData: ["id": tint.rawValue])
|
||||
}) {
|
||||
HStack {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
Circle()
|
||||
.fill(tint.color(forMood: mood))
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if moodTint == tint {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint == tint
|
||||
? Color.accentColor.opacity(0.08)
|
||||
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// Custom colors
|
||||
Button(action: {
|
||||
moodTint = .Custom
|
||||
}) {
|
||||
HStack {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
ColorPicker("", selection: colorBinding(for: index))
|
||||
.labelsHidden()
|
||||
.onChange(of: colorBinding(for: index).wrappedValue) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if moodTint == .Custom {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Text("Custom")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint == .Custom
|
||||
? Color.accentColor.opacity(0.08)
|
||||
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
private func colorBinding(for index: Int) -> Binding<Color> {
|
||||
switch index {
|
||||
case 0: return $customMoodTint.colorOne
|
||||
case 1: return $customMoodTint.colorTwo
|
||||
case 2: return $customMoodTint.colorThree
|
||||
case 3: return $customMoodTint.colorFour
|
||||
default: return $customMoodTint.colorFive
|
||||
}
|
||||
}
|
||||
|
||||
private func saveCustomMoodTint() {
|
||||
UserDefaultsStore.saveCustomMoodTint(customTint: customMoodTint)
|
||||
moodTint = .Custom
|
||||
EventLogger.log(event: "change_mood_tint_id", withData: ["id": MoodTints.Custom.rawValue])
|
||||
customMoodTintUpdateNumber += 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Voting Layout Picker
|
||||
struct VotingLayoutPickerCompact: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private var currentLayout: VotingLayoutStyle {
|
||||
@@ -540,11 +375,11 @@ struct VotingLayoutPickerCompact: View {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : theme.currentTheme.labelColor.opacity(0.4))
|
||||
|
||||
Text(layout.displayName)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : theme.currentTheme.labelColor.opacity(0.5))
|
||||
}
|
||||
.frame(width: 70)
|
||||
.padding(.vertical, 12)
|
||||
@@ -669,7 +504,6 @@ struct VotingLayoutPickerCompact: View {
|
||||
// MARK: - Custom Widget Section
|
||||
struct CustomWidgetSection: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@StateObject private var selectedWidget = CustomWidgetStateViewModel()
|
||||
|
||||
var body: some View {
|
||||
@@ -728,7 +562,7 @@ struct CustomWidgetSection: View {
|
||||
struct PersonalityPackPickerCompact: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.personalityPack.rawValue, store: GroupUserDefaults.groupDefaults) private var personalityPack: PersonalityPack = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.showNSFW.rawValue, store: GroupUserDefaults.groupDefaults) private var showNSFW: Bool = false
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@State private var showOver18Alert = false
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@@ -751,12 +585,12 @@ struct PersonalityPackPickerCompact: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(String(aPack.title()))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
let strings = aPack.randomPushNotificationStrings()
|
||||
Text(strings.body)
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
@@ -798,7 +632,7 @@ struct PersonalityPackPickerCompact: View {
|
||||
// MARK: - Day Filter Picker
|
||||
struct DayFilterPickerCompact: View {
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let weekdays = [(Calendar.current.shortWeekdaySymbols[0], 1),
|
||||
@@ -828,7 +662,7 @@ struct DayFilterPickerCompact: View {
|
||||
}) {
|
||||
Text(day.prefix(2).uppercased())
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(isActive ? .white : textColor.opacity(0.5))
|
||||
.foregroundColor(isActive ? .white : theme.currentTheme.labelColor.opacity(0.5))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
@@ -842,7 +676,7 @@ struct DayFilterPickerCompact: View {
|
||||
|
||||
Text(String(localized: "day_picker_view_text"))
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
@@ -953,7 +787,7 @@ 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
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
@@ -975,11 +809,11 @@ struct DayViewStylePickerCompact: View {
|
||||
VStack(spacing: 6) {
|
||||
styleIcon(for: style)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4))
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : theme.currentTheme.labelColor.opacity(0.4))
|
||||
|
||||
Text(style.displayName)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : theme.currentTheme.labelColor.opacity(0.5))
|
||||
}
|
||||
.frame(width: 70)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
@@ -288,12 +288,10 @@ struct AppThemePreviewSheet: View {
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
ThemeComponentRow(
|
||||
ThemeColorRow(
|
||||
icon: "paintpalette.fill",
|
||||
title: "Colors",
|
||||
value: theme.colorTint == .Default ? "Default" :
|
||||
theme.colorTint == .Neon ? "Neon" :
|
||||
theme.colorTint == .Pastel ? "Pastel" : "Custom",
|
||||
moodTint: theme.colorTint,
|
||||
color: .orange
|
||||
)
|
||||
|
||||
@@ -404,6 +402,48 @@ struct ThemeComponentRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Color Row with Circles
|
||||
|
||||
struct ThemeColorRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let moodTint: MoodTints
|
||||
let color: Color
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private let moods: [Mood] = [.great, .good, .average, .bad, .horrible]
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 14) {
|
||||
Image(systemName: icon)
|
||||
.font(.title3)
|
||||
.foregroundColor(color)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(color.opacity(0.15))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 6) {
|
||||
ForEach(moods, id: \.rawValue) { mood in
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: mood))
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
struct AppThemePickerView_Previews: PreviewProvider {
|
||||
|
||||
@@ -9,9 +9,10 @@ import SwiftUI
|
||||
|
||||
struct CustomWigetView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@StateObject private var selectedWidget = CustomWidgetStateViewModel()
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
@@ -9,8 +9,9 @@ import SwiftUI
|
||||
|
||||
struct DayFilterPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let weekdays = [(Calendar.current.shortWeekdaySymbols[0], 1),
|
||||
(Calendar.current.shortWeekdaySymbols[1], 2),
|
||||
|
||||
@@ -12,8 +12,9 @@ struct PersonalityPackPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.personalityPack.rawValue, store: GroupUserDefaults.groupDefaults) private var personalityPack: PersonalityPack = .Default
|
||||
@State private var showOver18Alert = false
|
||||
@AppStorage(UserDefaultsStore.Keys.showNSFW.rawValue, store: GroupUserDefaults.groupDefaults) private var showNSFW: Bool = false
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
@@ -10,10 +10,11 @@ import SwiftUI
|
||||
struct ShapePickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@State var shapeRefreshToggleThing: Bool = false
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// TextColorPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TextColorPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
ColorPicker(String(localized: "customize_view_view_text_color"), selection: $textColor)
|
||||
.padding()
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct TextColorPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextColorPickerView()
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ struct ThemePickerView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@State private var selectedTheme: Theme = UserDefaultsStore.theme()
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { selectedTheme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -70,31 +71,8 @@ struct ThemePickerView: View {
|
||||
selectedTheme = theme
|
||||
}
|
||||
|
||||
changeTextColor(forTheme: theme)
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": theme.rawValue])
|
||||
}
|
||||
|
||||
private func changeTextColor(forTheme theme: Theme) {
|
||||
if [Theme.iFeel, Theme.system].contains(theme) {
|
||||
let currentSystemScheme = UITraitCollection.current.userInterfaceStyle
|
||||
switch currentSystemScheme {
|
||||
case .unspecified:
|
||||
textColor = .black
|
||||
case .light:
|
||||
textColor = .black
|
||||
case .dark:
|
||||
textColor = .white
|
||||
@unknown default:
|
||||
textColor = .black
|
||||
}
|
||||
}
|
||||
if theme == Theme.dark {
|
||||
textColor = .white
|
||||
}
|
||||
if theme == Theme.light {
|
||||
textColor = .black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemePickerView_Previews: PreviewProvider {
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
//
|
||||
// TintPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TintPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@StateObject private var customMoodTint = UserDefaultsStore.getCustomMoodTint()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
ForEach(MoodTints.defaultOptions, id: \.rawValue) { tint in
|
||||
HStack {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
Circle()
|
||||
.frame(width: 35, height: 35)
|
||||
.foregroundColor(
|
||||
tint.color(forMood: mood)
|
||||
)
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(moodTint == tint ? theme.currentTheme.bgColor : .clear)
|
||||
.padding([.top, .bottom], -3)
|
||||
|
||||
)
|
||||
.onTapGesture {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
moodTint = tint
|
||||
EventLogger.log(event: "change_mood_tint_id", withData: ["id": tint.rawValue])
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 35)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.foregroundColor(.clear)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
moodTint = .Custom
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
}
|
||||
|
||||
HStack {
|
||||
ColorPicker("", selection: $customMoodTint.colorOne)
|
||||
.onChange(of: customMoodTint.colorOne) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorTwo)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorTwo) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorThree)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorThree) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorFour)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorFour) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorFive)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorFive) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Color.clear
|
||||
)
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(moodTint == .Custom ? theme.currentTheme.bgColor : .clear)
|
||||
.padding([.top, .bottom], -3)
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private func saveCustomMoodTint() {
|
||||
UserDefaultsStore.saveCustomMoodTint(customTint: customMoodTint)
|
||||
moodTint = .Custom
|
||||
EventLogger.log(event: "change_mood_tint_id", withData: ["id": MoodTints.Custom.rawValue])
|
||||
customMoodTintUpdateNumber += 1
|
||||
}
|
||||
}
|
||||
|
||||
struct TintPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TintPickerView()
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,10 @@ import SwiftUI
|
||||
|
||||
struct VotingLayoutPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
private var currentLayout: VotingLayoutStyle {
|
||||
VotingLayoutStyle(rawValue: votingLayoutStyle) ?? .horizontal
|
||||
}
|
||||
|
||||
@@ -20,13 +20,9 @@ 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
|
||||
@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
|
||||
|
||||
// MARK: edit row properties
|
||||
@State private var showingSheet = false
|
||||
@State private var selectedEntry: MoodEntryModel?
|
||||
@@ -42,9 +38,6 @@ struct DayView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
|
||||
mainView
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_home_view")
|
||||
@@ -65,6 +58,7 @@ struct DayView: View {
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
.preferredColorScheme(theme.preferredColorScheme)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +77,6 @@ struct DayView: View {
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
DataController.shared.fillInMissingDates()
|
||||
viewModel.updateData()
|
||||
}
|
||||
.background(
|
||||
@@ -102,19 +95,20 @@ struct DayView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached sorted year/month data to avoid sorting dictionaries in ForEach
|
||||
private var sortedGroupedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
|
||||
viewModel.grouped
|
||||
.sorted { $0.key > $1.key }
|
||||
.map { (year: $0.key, months: $0.value.sorted { $0.key > $1.key }.map { (month: $0.key, entries: $0.value) }) }
|
||||
}
|
||||
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8, pinnedViews: [.sectionHeaders]) {
|
||||
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(header: SectionHeaderView(month: month, year: year, entries: entries)) {
|
||||
monthListView(month: month, year: year, entries: entries)
|
||||
ForEach(sortedGroupedData, id: \.year) { yearData in
|
||||
ForEach(yearData.months, id: \.month) { monthData in
|
||||
Section(header: SectionHeaderView(month: monthData.month, year: yearData.year, entries: monthData.entries)) {
|
||||
monthListView(month: monthData.month, year: yearData.year, entries: monthData.entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,12 +166,12 @@ extension DayView {
|
||||
// Calendar icon
|
||||
Image(systemName: "calendar")
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.6))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@@ -196,7 +190,7 @@ extension DayView {
|
||||
.font(.largeTitle.weight(.black))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [textColor, textColor.opacity(0.4)],
|
||||
colors: [theme.currentTheme.labelColor, theme.currentTheme.labelColor.opacity(0.4)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
@@ -207,18 +201,18 @@ extension DayView {
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.subheadline.weight(.bold))
|
||||
.tracking(3)
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Decorative element
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.1))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.1))
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
@@ -232,7 +226,7 @@ extension DayView {
|
||||
// Subtle gradient accent
|
||||
LinearGradient(
|
||||
colors: [
|
||||
textColor.opacity(0.03),
|
||||
theme.currentTheme.labelColor.opacity(0.03),
|
||||
Color.clear
|
||||
],
|
||||
startPoint: .leading,
|
||||
@@ -246,34 +240,34 @@ extension DayView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Thick editorial rule
|
||||
Rectangle()
|
||||
.fill(textColor)
|
||||
.fill(theme.currentTheme.labelColor)
|
||||
.frame(height: 4)
|
||||
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
// Large serif month name
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.title.weight(.regular))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
// Year in lighter weight
|
||||
Text(String(year))
|
||||
.font(.body.weight(.light))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
|
||||
Spacer()
|
||||
|
||||
// Decorative flourish
|
||||
Text("§")
|
||||
.font(.title3.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 16)
|
||||
|
||||
// Thin bottom rule
|
||||
Rectangle()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.2))
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
@@ -306,13 +300,14 @@ extension DayView {
|
||||
ZStack {
|
||||
Color.black
|
||||
|
||||
// Scanlines
|
||||
VStack(spacing: 3) {
|
||||
ForEach(0..<8, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.white.opacity(0.02))
|
||||
.frame(height: 1)
|
||||
Spacer().frame(height: 3)
|
||||
// Scanlines - simplified with Canvas for performance
|
||||
Canvas { context, size in
|
||||
let lineHeight: CGFloat = 4
|
||||
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.02)))
|
||||
y += lineHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,18 +318,18 @@ extension DayView {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
// Brush stroke accent
|
||||
Capsule()
|
||||
.fill(textColor.opacity(0.15))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.15))
|
||||
.frame(width: 40, height: 3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.headline.weight(.thin))
|
||||
.tracking(4)
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.caption2.weight(.ultraLight))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.4))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -342,7 +337,7 @@ extension DayView {
|
||||
// Zen circle ornament
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.7)
|
||||
.stroke(textColor.opacity(0.2), style: StrokeStyle(lineWidth: 2, lineCap: .round))
|
||||
.stroke(theme.currentTheme.labelColor.opacity(0.2), style: StrokeStyle(lineWidth: 2, lineCap: .round))
|
||||
.frame(width: 20, height: 20)
|
||||
.rotationEffect(.degrees(-60))
|
||||
}
|
||||
@@ -371,15 +366,15 @@ extension DayView {
|
||||
HStack(spacing: 12) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Capsule()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.2))
|
||||
.frame(width: 4, height: 4)
|
||||
|
||||
Text(String(year))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.6))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@@ -395,21 +390,21 @@ extension DayView {
|
||||
// Tape reel icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(textColor.opacity(0.3), lineWidth: 2)
|
||||
.stroke(theme.currentTheme.labelColor.opacity(0.3), lineWidth: 2)
|
||||
.frame(width: 24, height: 24)
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.2))
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("SIDE A")
|
||||
.font(.caption2.weight(.bold).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.4))
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))")
|
||||
.font(.body.weight(.black))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
.tracking(1)
|
||||
}
|
||||
|
||||
@@ -418,7 +413,7 @@ extension DayView {
|
||||
// Track counter
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.title3.weight(.bold).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 14)
|
||||
@@ -433,7 +428,7 @@ extension DayView {
|
||||
// Organic blob background
|
||||
HStack {
|
||||
Ellipse()
|
||||
.fill(textColor.opacity(0.08))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.08))
|
||||
.frame(width: 120, height: 60)
|
||||
.blur(radius: 15)
|
||||
.offset(x: -20)
|
||||
@@ -443,17 +438,17 @@ extension DayView {
|
||||
HStack(spacing: 16) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.title2.weight(.light))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.subheadline.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.4))
|
||||
|
||||
Spacer()
|
||||
|
||||
// Blob indicator
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.15))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.15))
|
||||
.frame(width: 12, height: 12)
|
||||
.blur(radius: 2)
|
||||
}
|
||||
@@ -465,12 +460,13 @@ extension DayView {
|
||||
|
||||
private func stackSectionHeader(month: Int, year: Int) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Torn edge
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<30, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.frame(height: CGFloat.random(in: 2...4))
|
||||
// Torn edge - simplified with Canvas for performance
|
||||
Canvas { context, size in
|
||||
let segmentWidth = size.width / 15
|
||||
for i in 0..<15 {
|
||||
let height = CGFloat(2 + (i * 5) % 3) // Deterministic pseudo-random heights
|
||||
let rect = CGRect(x: CGFloat(i) * segmentWidth, y: 0, width: segmentWidth, height: height)
|
||||
context.fill(Path(rect), with: .color(Color(uiColor: UIColor.label).opacity(0.2)))
|
||||
}
|
||||
}
|
||||
.frame(height: 4)
|
||||
@@ -483,12 +479,12 @@ extension DayView {
|
||||
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.headline.weight(.regular))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.subheadline.weight(.light))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@@ -516,7 +512,7 @@ extension DayView {
|
||||
}
|
||||
}()
|
||||
|
||||
let barColor = hasData ? moodTint.color(forMood: moodForColor) : textColor.opacity(0.2)
|
||||
let barColor = hasData ? moodTint.color(forMood: moodForColor) : theme.currentTheme.labelColor.opacity(0.2)
|
||||
// Width percentage based on average (0=20%, 4=100%)
|
||||
let widthPercent = hasData ? 0.2 + (averageMood / 4.0) * 0.8 : 0.2
|
||||
|
||||
@@ -524,7 +520,7 @@ extension DayView {
|
||||
// Month number
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.title.weight(.thin))
|
||||
.foregroundColor(hasData ? barColor.opacity(0.6) : textColor.opacity(0.3))
|
||||
.foregroundColor(hasData ? barColor.opacity(0.6) : theme.currentTheme.labelColor.opacity(0.3))
|
||||
.frame(width: 50)
|
||||
|
||||
// Gradient bar sized by average mood
|
||||
@@ -532,7 +528,7 @@ extension DayView {
|
||||
ZStack(alignment: .leading) {
|
||||
// Background track
|
||||
Capsule()
|
||||
.fill(textColor.opacity(0.1))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.1))
|
||||
.frame(height: 8)
|
||||
|
||||
// Colored bar based on average
|
||||
@@ -554,7 +550,7 @@ extension DayView {
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
if hasData {
|
||||
Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5
|
||||
@@ -563,7 +559,7 @@ extension DayView {
|
||||
} else {
|
||||
Text(String(year))
|
||||
.font(.caption2.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,11 +572,11 @@ extension DayView {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.6))
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@@ -726,17 +722,17 @@ extension DayView {
|
||||
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.headline.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.caption.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -818,11 +814,11 @@ extension DayView {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -830,7 +826,7 @@ extension DayView {
|
||||
// Tilt indicator
|
||||
Image(systemName: "iphone.gen3.radiowaves.left.and.right")
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
}
|
||||
@@ -847,24 +843,24 @@ extension DayView {
|
||||
HStack(spacing: 8) {
|
||||
// Minimal colored bar
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(textColor.opacity(0.3))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.3))
|
||||
.frame(width: 3, height: 16)
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month).prefix(3).uppercased())")
|
||||
.font(.caption2.weight(.bold).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.6))
|
||||
|
||||
Text("•")
|
||||
.font(.caption2)
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.3))
|
||||
|
||||
Text(String(year))
|
||||
.font(.caption2.weight(.medium).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.4))
|
||||
|
||||
// Thin separator line
|
||||
Rectangle()
|
||||
.fill(textColor.opacity(0.1))
|
||||
.fill(theme.currentTheme.labelColor.opacity(0.1))
|
||||
.frame(height: 1)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
|
||||
@@ -37,9 +37,8 @@ class DayViewViewModel: ObservableObject {
|
||||
|
||||
DataController.shared.addNewDataListener { [weak self] in
|
||||
guard let self = self else { return }
|
||||
withAnimation {
|
||||
self.updateData()
|
||||
}
|
||||
// Avoid withAnimation for bulk data updates - it causes expensive view diffing
|
||||
self.updateData()
|
||||
}
|
||||
updateData()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import SwiftUI
|
||||
|
||||
struct EmptyHomeView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let showVote: Bool
|
||||
let viewModel: DayViewViewModel?
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import SwiftUI
|
||||
struct ExportView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@State private var selectedFormat: ExportFormat = .csv
|
||||
@State private var selectedRange: DateRange = .allTime
|
||||
|
||||
@@ -14,11 +14,11 @@ enum PercViewType {
|
||||
|
||||
struct HeaderPercView: 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.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@State private var entries = [MoodMetrics]()
|
||||
let backDays: Int
|
||||
let type: PercViewType
|
||||
|
||||
@@ -9,7 +9,8 @@ import SwiftUI
|
||||
|
||||
struct IAPWarningView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@ObservedObject var iapManager: IAPManager
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ struct ImagePickerGridView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@State var column = Array(repeating: GridItem(.flexible(), spacing: 10), count: 7)
|
||||
let pickedImageClosure: ((CustomWidgeImageOptions) -> Void)
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let imageOptions = CustomWidgeImageOptions.allCases.sorted(by: {
|
||||
$0.rawValue < $1.rawValue
|
||||
})
|
||||
|
||||
@@ -9,11 +9,12 @@ import SwiftUI
|
||||
|
||||
struct InsightsView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@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
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@StateObject private var viewModel = InsightsViewModel()
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
@@ -11,7 +11,8 @@ struct MainTabView: 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.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
|
||||
let onboardingData = OnboardingDataDataManager.shared.savedOnboardingData
|
||||
|
||||
@@ -11,7 +11,8 @@ struct MonthDetailView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
@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
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@StateObject private var shareImage = ShareImageStateViewModel()
|
||||
|
||||
|
||||
@@ -12,17 +12,15 @@ struct MonthView: View {
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var labelColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@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 = ShareImageStateViewModel()
|
||||
|
||||
// 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 = DetailViewStateViewModel()
|
||||
@State private var showingSheet = false
|
||||
@@ -43,8 +41,11 @@ struct MonthView: View {
|
||||
@State private var trialWarningHidden = false
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
/// Cached sorted year/month data to avoid recalculating in ForEach
|
||||
@State private var cachedSortedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] = []
|
||||
|
||||
/// Filters month data to only current month when subscription/trial expired
|
||||
private var filteredMonthData: [Int: [Int: [MoodEntryModel]]] {
|
||||
private func computeFilteredMonthData() -> [Int: [Int: [MoodEntryModel]]] {
|
||||
guard iapManager.shouldShowPaywall else {
|
||||
return viewModel.grouped
|
||||
}
|
||||
@@ -61,6 +62,13 @@ struct MonthView: View {
|
||||
return filtered
|
||||
}
|
||||
|
||||
/// Sorts the filtered month data - called only when source data changes
|
||||
private func computeSortedYearMonthData() -> [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
|
||||
computeFilteredMonthData()
|
||||
.sorted { $0.key > $1.key }
|
||||
.map { (year: $0.key, months: $0.value.sorted { $0.key > $1.key }.map { (month: $0.key, entries: $0.value) }) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if viewModel.hasNoData {
|
||||
@@ -69,23 +77,22 @@ struct MonthView: View {
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(filteredMonthData.sorted(by: { $0.key > $1.key }), id: \.key) { year, months in
|
||||
ForEach(cachedSortedData, id: \.year) { yearData in
|
||||
// for each month
|
||||
ForEach(months.sorted(by: { $0.key > $1.key }), id: \.key) { month, entries in
|
||||
ForEach(yearData.months, id: \.month) { monthData in
|
||||
MonthCard(
|
||||
month: month,
|
||||
year: year,
|
||||
entries: entries,
|
||||
month: monthData.month,
|
||||
year: yearData.year,
|
||||
entries: monthData.entries,
|
||||
moodTint: moodTint,
|
||||
imagePack: imagePack,
|
||||
textColor: textColor,
|
||||
theme: theme,
|
||||
filteredDays: filteredDays.currentFilters,
|
||||
onTap: {
|
||||
let detailView = MonthDetailView(
|
||||
monthInt: month,
|
||||
yearInt: year,
|
||||
entries: entries,
|
||||
monthInt: monthData.month,
|
||||
yearInt: yearData.year,
|
||||
entries: monthData.entries,
|
||||
parentViewModel: viewModel
|
||||
)
|
||||
selectedDetail.selectedItem = detailView
|
||||
@@ -101,6 +108,7 @@ struct MonthView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 100)
|
||||
.id(moodTint) // Force complete refresh when mood tint changes
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
let offset = proxy.frame(in: .named("scroll")).minY
|
||||
@@ -126,10 +134,6 @@ struct MonthView: View {
|
||||
)
|
||||
}
|
||||
|
||||
// Hidden text to trigger updates when custom tint changes
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
|
||||
if iapManager.shouldShowPaywall {
|
||||
// Premium month history prompt - bottom half
|
||||
VStack(spacing: 20) {
|
||||
@@ -160,12 +164,12 @@ struct MonthView: View {
|
||||
VStack(spacing: 10) {
|
||||
Text("Explore Your Mood History")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("See your complete monthly journey. Track patterns and understand what shapes your days.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.foregroundColor(labelColor.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
@@ -231,6 +235,18 @@ struct MonthView: View {
|
||||
trialWarningHidden = value < 0
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
}
|
||||
.onChange(of: viewModel.numberOfItems) { _, _ in
|
||||
// Use numberOfItems as a lightweight proxy for data changes
|
||||
// instead of comparing the entire grouped dictionary
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
}
|
||||
.onChange(of: iapManager.shouldShowPaywall) { _, _ in
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
}
|
||||
.preferredColorScheme(theme.preferredColorScheme)
|
||||
}
|
||||
|
||||
|
||||
@@ -241,31 +257,43 @@ struct MonthView: View {
|
||||
}
|
||||
|
||||
// MARK: - Month Card Component
|
||||
struct MonthCard: View {
|
||||
struct MonthCard: View, Equatable {
|
||||
let month: Int
|
||||
let year: Int
|
||||
let entries: [MoodEntryModel]
|
||||
let moodTint: MoodTints
|
||||
let imagePack: MoodImages
|
||||
let textColor: Color
|
||||
let theme: Theme
|
||||
let filteredDays: [Int]
|
||||
let onTap: () -> Void
|
||||
let onShare: (UIImage) -> Void
|
||||
|
||||
private var labelColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
// Equatable conformance to prevent unnecessary re-renders
|
||||
static func == (lhs: MonthCard, rhs: MonthCard) -> Bool {
|
||||
lhs.month == rhs.month &&
|
||||
lhs.year == rhs.year &&
|
||||
lhs.entries.count == rhs.entries.count &&
|
||||
lhs.moodTint == rhs.moodTint &&
|
||||
lhs.imagePack == rhs.imagePack &&
|
||||
lhs.filteredDays == rhs.filteredDays &&
|
||||
lhs.theme == rhs.theme
|
||||
}
|
||||
|
||||
@State private var showStats = true
|
||||
@State private var cachedMetrics: [MoodMetrics] = []
|
||||
|
||||
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 = DataController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7])
|
||||
return Random.createTotalPerc(fromEntries: monthEntries)
|
||||
// Cached filtered/sorted metrics to avoid recalculating in ForEach
|
||||
private var displayMetrics: [MoodMetrics] {
|
||||
cachedMetrics.filter { $0.total > 0 }.sorted { $0.mood.rawValue > $1.mood.rawValue }
|
||||
}
|
||||
|
||||
private var topMood: Mood? {
|
||||
metrics.filter { $0.total > 0 }.max(by: { $0.total < $1.total })?.mood
|
||||
displayMetrics.max(by: { $0.total < $1.total })?.mood
|
||||
}
|
||||
|
||||
private var totalTrackedDays: Int {
|
||||
@@ -277,13 +305,13 @@ struct MonthCard: View {
|
||||
// Header with month/year
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
|
||||
.font(.title.weight(.heavy))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Monthly Mood Wrap")
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(labelColor.opacity(0.6))
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// Top mood highlight
|
||||
@@ -304,7 +332,7 @@ struct MonthCard: View {
|
||||
|
||||
Text("Top Mood")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
|
||||
Text(topMood.strValue.uppercased())
|
||||
.font(.title3.weight(.bold))
|
||||
@@ -318,10 +346,10 @@ struct MonthCard: View {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(totalTrackedDays)")
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
Text("Days Tracked")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
@@ -329,7 +357,7 @@ struct MonthCard: View {
|
||||
|
||||
// Mood breakdown with bars
|
||||
VStack(spacing: 12) {
|
||||
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
|
||||
ForEach(displayMetrics) { metric in
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
@@ -357,7 +385,7 @@ struct MonthCard: View {
|
||||
|
||||
Text("\(Int(metric.percent))%")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
.frame(width: 40, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
@@ -368,7 +396,7 @@ struct MonthCard: View {
|
||||
// App branding
|
||||
Text("ifeel")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.foregroundColor(labelColor.opacity(0.3))
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.frame(width: 400)
|
||||
@@ -389,11 +417,11 @@ struct MonthCard: View {
|
||||
HStack {
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.title3.bold())
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(labelColor)
|
||||
|
||||
Image(systemName: showStats ? "chevron.up" : "chevron.down")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -406,7 +434,7 @@ struct MonthCard: View {
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.foregroundColor(labelColor.opacity(0.6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -418,7 +446,7 @@ struct MonthCard: View {
|
||||
ForEach(weekdayLabels.indices, id: \.self) { index in
|
||||
Text(weekdayLabels[index])
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.foregroundColor(labelColor.opacity(0.5))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -443,7 +471,7 @@ struct MonthCard: View {
|
||||
Divider()
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
MoodBarChart(metrics: metrics, moodTint: moodTint, imagePack: imagePack)
|
||||
MoodBarChart(metrics: cachedMetrics, moodTint: moodTint, imagePack: imagePack)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
@@ -457,6 +485,12 @@ struct MonthCard: View {
|
||||
.onTapGesture {
|
||||
onTap()
|
||||
}
|
||||
.onAppear {
|
||||
// Cache metrics calculation on first appearance
|
||||
if cachedMetrics.isEmpty {
|
||||
cachedMetrics = Random.createTotalPerc(fromEntries: entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,9 +521,7 @@ struct HeatmapCell: View {
|
||||
}
|
||||
|
||||
private var formattedDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter.string(from: entry.forDate)
|
||||
DateFormattingCache.shared.string(for: entry.forDate, format: .dateMedium)
|
||||
}
|
||||
|
||||
private var cellColor: Color {
|
||||
|
||||
@@ -11,7 +11,9 @@ import PhotosUI
|
||||
struct NoteEditorView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let entry: MoodEntryModel
|
||||
@State private var noteText: String
|
||||
@@ -137,9 +139,11 @@ struct EntryDetailView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@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.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let entry: MoodEntryModel
|
||||
let onMoodUpdate: (Mood) -> Void
|
||||
let onDelete: () -> Void
|
||||
|
||||
@@ -10,7 +10,8 @@ import StoreKit
|
||||
|
||||
struct PurchaseButtonView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@ObservedObject var iapManager: IAPManager
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
struct SampleEntryView: View {
|
||||
@State private var sampleListEntry = DataController.shared.generateObjectNotInArray(forDate: Date(), withMood: Mood.great)
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
@@ -14,7 +14,8 @@ struct DebugAnimationSettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@State private var selectedAnimation: CelebrationAnimationType = .vortexCheckmark
|
||||
@State private var isAnimating = false
|
||||
|
||||
@@ -22,7 +22,8 @@ struct SettingsTabView: View {
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -85,7 +86,9 @@ struct UpgradeBannerView: View {
|
||||
let trialExpirationDate: Date?
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
|
||||
@@ -26,7 +26,8 @@ struct SettingsContentView: View {
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -48,6 +49,8 @@ struct SettingsContentView: View {
|
||||
eulaButton
|
||||
privacyButton
|
||||
|
||||
addTestDataButton
|
||||
|
||||
#if DEBUG
|
||||
// Debug section
|
||||
debugSectionHeader
|
||||
@@ -55,7 +58,7 @@ struct SettingsContentView: View {
|
||||
animationLabButton
|
||||
paywallPreviewButton
|
||||
tipsPreviewButton
|
||||
addTestDataButton
|
||||
|
||||
clearDataButton
|
||||
#endif
|
||||
|
||||
@@ -379,36 +382,6 @@ struct SettingsContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var addTestDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
DataController.shared.populateTestData()
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "plus.square.on.square")
|
||||
.font(.title2)
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Add Test Data")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Populate with sample mood entries")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var clearDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
@@ -494,6 +467,36 @@ struct SettingsContentView: View {
|
||||
|
||||
@ObservedObject private var healthKitManager = HealthKitManager.shared
|
||||
|
||||
private var addTestDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
DataController.shared.populateTestData()
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "plus.square.on.square")
|
||||
.font(.title2)
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Add Test Data")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Populate with sample mood entries")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var healthKitToggle: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 12) {
|
||||
@@ -795,10 +798,11 @@ struct SettingsView: View {
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
||||
@AppStorage(UserDefaultsStore.Keys.privacyLockEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var privacyLockEnabled = false
|
||||
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
|
||||
@@ -24,7 +24,8 @@ struct WrappedSharable: Hashable, Equatable {
|
||||
|
||||
struct SharingListView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
class ShareStateViewModel: ObservableObject {
|
||||
@Published var selectedItem: WrappedSharable? = nil
|
||||
|
||||
@@ -20,7 +20,8 @@ struct AllMoodsTotalTemplate: View, SharingTemplate {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@StateObject private var shareImage = ShareImageStateViewModel()
|
||||
private var entries = [MoodMetrics]()
|
||||
|
||||
@@ -23,7 +23,8 @@ struct CurrentStreakTemplate: View, SharingTemplate {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
|
||||
@@ -32,7 +32,8 @@ struct LongestStreakTemplate: View, SharingTemplate {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
|
||||
@@ -25,7 +25,8 @@ struct MonthTotalTemplate: View, SharingTemplate {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
private var moodMetrics = [MoodMetrics]()
|
||||
private var moodEntries = [MoodEntryModel]()
|
||||
|
||||
@@ -10,10 +10,10 @@ import SwiftUI
|
||||
struct SmallRollUpHeaderView: View {
|
||||
@Binding var viewType: MainSwitchableViewType
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
let entries: [MoodEntryModel]
|
||||
private var moodMetrics = [MoodMetrics]()
|
||||
|
||||
@@ -29,11 +29,9 @@ struct SwitchableView: View {
|
||||
|
||||
@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.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
init(daysBack: Int, viewType: Binding<MainSwitchableViewType>, headerTypeChanged: @escaping ((MainSwitchableViewType) -> Void)) {
|
||||
self.daysBack = daysBack
|
||||
self.headerTypeChanged = headerTypeChanged
|
||||
@@ -66,9 +64,6 @@ struct SwitchableView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
|
||||
mainViews
|
||||
.padding([.top, .bottom])
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ struct TipModalView: View {
|
||||
let onDismiss: () -> Void
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@State private var appeared = false
|
||||
|
||||
|
||||
@@ -6,20 +6,15 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct YearView: View {
|
||||
let months = [(0, "J"), (1, "F"), (2,"M"), (3,"A"), (4,"M"), (5, "J"), (6,"J"), (7,"A"), (8,"S"), (9,"O"), (10, "N"), (11,"D")]
|
||||
|
||||
@State private var toggle = true
|
||||
|
||||
@Query(sort: \MoodEntryModel.forDate, order: .reverse)
|
||||
private var items: [MoodEntryModel]
|
||||
|
||||
@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
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@StateObject public var viewModel: YearViewModel
|
||||
@@ -28,6 +23,9 @@ struct YearView: View {
|
||||
@State private var trialWarningHidden = false
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
/// Cached sorted year keys to avoid re-sorting in ForEach on every render
|
||||
@State private var cachedSortedYearKeys: [Int] = []
|
||||
|
||||
// Heatmap-style grid: 12 columns for months
|
||||
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
|
||||
|
||||
@@ -39,13 +37,13 @@ struct YearView: View {
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(Array(self.viewModel.data.keys.sorted(by: >)), id: \.self) { yearKey in
|
||||
ForEach(cachedSortedYearKeys, id: \.self) { yearKey in
|
||||
YearCard(
|
||||
year: yearKey,
|
||||
yearData: self.viewModel.data[yearKey]!,
|
||||
yearEntries: self.viewModel.entriesByYear[yearKey] ?? [],
|
||||
moodTint: moodTint,
|
||||
imagePack: imagePack,
|
||||
textColor: textColor,
|
||||
theme: theme,
|
||||
filteredDays: filteredDays.currentFilters,
|
||||
onShare: { image in
|
||||
@@ -57,6 +55,7 @@ struct YearView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 100)
|
||||
.id(moodTint) // Force complete refresh when mood tint changes
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
let offset = proxy.frame(in: .named("scroll")).minY
|
||||
@@ -112,12 +111,12 @@ struct YearView: View {
|
||||
VStack(spacing: 10) {
|
||||
Text("See Your Year at a Glance")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(theme.currentTheme.labelColor)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("Discover your emotional rhythm across the year. Spot trends and celebrate how far you've come.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
@@ -168,7 +167,16 @@ struct YearView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
|
||||
})
|
||||
.onChange(of: viewModel.data.keys.count) { _, _ in
|
||||
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
|
||||
}
|
||||
.onChange(of: moodTint) { _, _ in
|
||||
// Rebuild chart data when mood tint changes to update colors
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
@@ -179,33 +187,42 @@ struct YearView: View {
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
.preferredColorScheme(theme.preferredColorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Year Card Component
|
||||
struct YearCard: View {
|
||||
struct YearCard: View, Equatable {
|
||||
let year: Int
|
||||
let yearData: [Int: [DayChartView]]
|
||||
let yearEntries: [MoodEntryModel]
|
||||
let moodTint: MoodTints
|
||||
let imagePack: MoodImages
|
||||
let textColor: Color
|
||||
let theme: Theme
|
||||
let filteredDays: [Int]
|
||||
let onShare: (UIImage) -> Void
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
// Equatable conformance to prevent unnecessary re-renders
|
||||
static func == (lhs: YearCard, rhs: YearCard) -> Bool {
|
||||
lhs.year == rhs.year &&
|
||||
lhs.yearEntries.count == rhs.yearEntries.count &&
|
||||
lhs.moodTint == rhs.moodTint &&
|
||||
lhs.imagePack == rhs.imagePack &&
|
||||
lhs.filteredDays == rhs.filteredDays &&
|
||||
lhs.theme == rhs.theme
|
||||
}
|
||||
|
||||
@State private var showStats = true
|
||||
@State private var cachedMetrics: [MoodMetrics] = []
|
||||
|
||||
private let months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]
|
||||
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
|
||||
|
||||
private var yearEntries: [MoodEntryModel] {
|
||||
let firstOfYear = Calendar.current.date(from: DateComponents(year: year, month: 1, day: 1))!
|
||||
let lastOfYear = Calendar.current.date(from: DateComponents(year: year + 1, month: 1, day: 1))!
|
||||
return DataController.shared.getData(startDate: firstOfYear, endDate: lastOfYear, includedDays: filteredDays)
|
||||
}
|
||||
|
||||
private var metrics: [MoodMetrics] {
|
||||
return Random.createTotalPerc(fromEntries: yearEntries)
|
||||
// Cached filtered/sorted metrics to avoid recalculating in ForEach
|
||||
private var displayMetrics: [MoodMetrics] {
|
||||
cachedMetrics.filter { $0.total > 0 }.sorted { $0.mood.rawValue > $1.mood.rawValue }
|
||||
}
|
||||
|
||||
private var totalEntries: Int {
|
||||
@@ -213,7 +230,7 @@ struct YearCard: View {
|
||||
}
|
||||
|
||||
private var topMood: Mood? {
|
||||
metrics.filter { $0.total > 0 }.max(by: { $0.total < $1.total })?.mood
|
||||
displayMetrics.max(by: { $0.total < $1.total })?.mood
|
||||
}
|
||||
|
||||
private var shareableView: some View {
|
||||
@@ -273,7 +290,7 @@ struct YearCard: View {
|
||||
|
||||
// Mood breakdown with bars
|
||||
VStack(spacing: 14) {
|
||||
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
|
||||
ForEach(displayMetrics) { metric in
|
||||
HStack(spacing: 14) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
@@ -366,12 +383,12 @@ struct YearCard: View {
|
||||
if showStats {
|
||||
HStack(spacing: 16) {
|
||||
// Donut Chart
|
||||
MoodDonutChart(metrics: metrics, moodTint: moodTint)
|
||||
MoodDonutChart(metrics: cachedMetrics, moodTint: moodTint)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
// Bar Chart
|
||||
VStack(spacing: 6) {
|
||||
ForEach(metrics.filter { $0.total > 0 }) { metric in
|
||||
ForEach(displayMetrics) { metric in
|
||||
HStack(spacing: 8) {
|
||||
imagePack.icon(forMood: metric.mood)
|
||||
.resizable()
|
||||
@@ -434,6 +451,12 @@ struct YearCard: View {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(theme.currentTheme.secondaryBGColor)
|
||||
)
|
||||
.onAppear {
|
||||
// Cache metrics calculation on first appearance
|
||||
if cachedMetrics.isEmpty {
|
||||
cachedMetrics = Random.createTotalPerc(fromEntries: yearEntries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,9 +468,16 @@ struct YearHeatmapGrid: View {
|
||||
|
||||
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
|
||||
|
||||
/// Pre-sorted month keys to avoid sorting in ForEach on every render
|
||||
private var sortedMonthKeys: [Int] {
|
||||
// This is computed once per yearData change, not on every body access
|
||||
// since yearData is a let constant passed from parent
|
||||
Array(yearData.keys.sorted(by: <))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: heatmapColumns, spacing: 2) {
|
||||
ForEach(Array(yearData.keys.sorted(by: <)), id: \.self) { monthKey in
|
||||
ForEach(sortedMonthKeys, id: \.self) { monthKey in
|
||||
if let monthData = yearData[monthKey] {
|
||||
MonthColumn(
|
||||
monthData: monthData,
|
||||
|
||||
@@ -16,6 +16,8 @@ class YearViewModel: ObservableObject {
|
||||
// year, month, items
|
||||
@Published public private(set) var data = [Int: [Int: [DayChartView]]]()
|
||||
@Published public private(set) var numberOfRatings: Int = 0
|
||||
/// Entries organized by year for efficient access
|
||||
@Published public private(set) var entriesByYear = [Int: [MoodEntryModel]]()
|
||||
public private(set) var uncategorizedData = [MoodEntryModel]() {
|
||||
didSet {
|
||||
self.numberOfRatings = uncategorizedData.count
|
||||
@@ -44,8 +46,16 @@ class YearViewModel: ObservableObject {
|
||||
endDate: endDate,
|
||||
includedDays: selectedDays)
|
||||
data.removeAll()
|
||||
entriesByYear.removeAll()
|
||||
let filledOutData = chartViewBuilder.buildGridData(withData: filteredEntries)
|
||||
data = filledOutData
|
||||
uncategorizedData = filteredEntries
|
||||
|
||||
// Organize entries by year for efficient access in YearCard
|
||||
let calendar = Calendar.current
|
||||
for entry in filteredEntries {
|
||||
let year = calendar.component(.year, from: entry.forDate)
|
||||
entriesByYear[year, default: []].append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user