2.0: breakfast and snack meal types across the app
- MealType gains breakfast/snack (chronological order drives slot/row order). - AppSettings.activeMealTypes: single source of truth for enabled meals, backed by new optional enabledMealTypesRaw with legacy mealWindows fallback — additive migration, keeps pre-2.0 installs and iCloud snapshots working. Legacy field kept coherent by the setter. time(for:) resolves per-meal event times (breakfast 8:00 / snack 17:00 defaults on old stores). - Replaced the 5 duplicated mealWindows→[MealType] derivations (slot sync, default plan creation, week calendar, export view) with activeMealTypes. - CalendarService: per-type event names and times. - Export styles: per-type accent colors and gradients. - Widget: generic meals list (N rows) instead of hardcoded lunch/dinner; compact families fall back to main meals when >2 are active. - Settings: 4 meal toggles (min 1) replace the 3-option picker. - Onboarding: meal step is now multi-select over the 4 types. - iCloud sync: enabledMealTypesRaw synced (optional, pre-2.0 compatible). - Localized breakfast/snack in all 6 languages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
This commit is contained in:
@@ -7,11 +7,17 @@ final class AppSettings {
|
||||
var includeWeekends: Bool = true
|
||||
var language: String // "spanish", "english"
|
||||
|
||||
/// Comma-separated MealType raw values, in chronological order (2.0).
|
||||
/// nil = derive from the legacy `mealWindows` field (pre-2.0 installs).
|
||||
var enabledMealTypesRaw: String?
|
||||
|
||||
var calendarId: String?
|
||||
var syncEnabled: Bool = false
|
||||
var syncMode: String? // "weekComplete", "manual"
|
||||
var lunchTime: Date
|
||||
var dinnerTime: Date
|
||||
var breakfastTime: Date? // optional: pre-2.0 stores lack it
|
||||
var snackTime: Date?
|
||||
var eventDuration: Int
|
||||
var eventPrefix: String
|
||||
var reminderMinutesBefore: Int?
|
||||
@@ -71,6 +77,57 @@ final class AppSettings {
|
||||
get { WeekExportStyle(rawValue: weekExportStyle ?? "") ?? .defaultStyle }
|
||||
set { weekExportStyle = newValue.rawValue }
|
||||
}
|
||||
|
||||
/// The meal types the user plans, in chronological order. Single source of
|
||||
/// truth for slot generation, calendar rows, exports and the widget.
|
||||
/// Falls back to the legacy `mealWindows` for pre-2.0 installs.
|
||||
var activeMealTypes: [MealType] {
|
||||
get {
|
||||
if let raw = enabledMealTypesRaw {
|
||||
let types = raw.split(separator: ",").compactMap { MealType(rawValue: String($0)) }
|
||||
if !types.isEmpty {
|
||||
return MealType.allCases.filter { types.contains($0) }
|
||||
}
|
||||
}
|
||||
switch mealWindowsEnum {
|
||||
case .dinnerOnly: return [.dinner]
|
||||
case .lunchOnly: return [.lunch]
|
||||
case .both: return [.lunch, .dinner]
|
||||
}
|
||||
}
|
||||
set {
|
||||
let ordered = MealType.allCases.filter { newValue.contains($0) }
|
||||
enabledMealTypesRaw = ordered.isEmpty
|
||||
? MealType.dinner.rawValue
|
||||
: ordered.map(\.rawValue).joined(separator: ",")
|
||||
// Keep the legacy field roughly coherent for pre-2.0 code paths.
|
||||
if ordered.contains(.lunch) && ordered.contains(.dinner) {
|
||||
mealWindowsEnum = .both
|
||||
} else if ordered.contains(.lunch) {
|
||||
mealWindowsEnum = .lunchOnly
|
||||
} else {
|
||||
mealWindowsEnum = .dinnerOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Event start time for a meal type (calendar sync). Breakfast/snack get
|
||||
/// sensible defaults on pre-2.0 stores that never set them.
|
||||
func time(for mealType: MealType) -> Date {
|
||||
switch mealType {
|
||||
case .breakfast: return breakfastTime ?? Self.defaultTime(hour: 8)
|
||||
case .lunch: return lunchTime
|
||||
case .snack: return snackTime ?? Self.defaultTime(hour: 17)
|
||||
case .dinner: return dinnerTime
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultTime(hour: Int) -> Date {
|
||||
var components = DateComponents()
|
||||
components.hour = hour
|
||||
components.minute = 0
|
||||
return Calendar.current.date(from: components) ?? Date()
|
||||
}
|
||||
}
|
||||
|
||||
enum MealWindows: String, CaseIterable {
|
||||
@@ -142,15 +199,34 @@ enum AppLanguage: String, CaseIterable {
|
||||
}
|
||||
|
||||
enum MealType: String, Codable, CaseIterable {
|
||||
// Declaration order is chronological — allCases drives row/slot ordering.
|
||||
case breakfast
|
||||
case lunch
|
||||
case snack
|
||||
case dinner
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .breakfast: return "cup.and.saucer.fill"
|
||||
case .lunch: return "sun.max.fill"
|
||||
case .snack: return "carrot.fill"
|
||||
case .dinner: return "moon.stars.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var localizedKey: String {
|
||||
switch self {
|
||||
case .breakfast: return "breakfast"
|
||||
case .lunch: return "lunch"
|
||||
case .snack: return "snack"
|
||||
case .dinner: return "dinner"
|
||||
}
|
||||
}
|
||||
|
||||
/// Chronological position, used to order slots within a day.
|
||||
var order: Int {
|
||||
MealType.allCases.firstIndex(of: self) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
enum CalendarSyncMode: String, CaseIterable {
|
||||
|
||||
Reference in New Issue
Block a user