import Foundation import SwiftData // CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere. @Model final class AppSettings { var mealWindows: String = "dinnerOnly" // legacy: "dinnerOnly", "lunchOnly", "both" var includeWeekends: Bool = true var language: String = "system" /// 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 = AppSettings.defaultTime(hour: 14) var dinnerTime: Date = AppSettings.defaultTime(hour: 21) var breakfastTime: Date? // optional: pre-2.0 stores lack it var snackTime: Date? var eventDuration: Int = 60 var eventPrefix: String = "🍽️" var reminderMinutesBefore: Int? var iCloudSyncEnabled: Bool? var weekExportStyle: String? /// Show dish photos as planner card backgrounds (2.0). Optional for /// CloudKit/migration compatibility; resolves to true. var showDishPhotosInPlanner: Bool? var isPremium: Bool = false var onboardingCompleted: Bool = false init() { self.mealWindows = "dinnerOnly" self.includeWeekends = true self.language = "system" self.calendarId = nil self.syncEnabled = false self.syncMode = CalendarSyncMode.weekComplete.rawValue // Default lunch time 14:00 var lunchComponents = DateComponents() lunchComponents.hour = 14 lunchComponents.minute = 0 self.lunchTime = Calendar.current.date(from: lunchComponents) ?? Date() // Default dinner time 21:00 var dinnerComponents = DateComponents() dinnerComponents.hour = 21 dinnerComponents.minute = 0 self.dinnerTime = Calendar.current.date(from: dinnerComponents) ?? Date() self.eventDuration = 60 self.eventPrefix = "🍽️" self.reminderMinutesBefore = nil self.iCloudSyncEnabled = true self.weekExportStyle = nil self.isPremium = false self.onboardingCompleted = false } var mealWindowsEnum: MealWindows { get { MealWindows(rawValue: mealWindows) ?? .dinnerOnly } set { mealWindows = newValue.rawValue } } var languageEnum: AppLanguage { get { AppLanguage(rawValue: language) ?? .system } set { language = newValue.rawValue } } var syncModeEnum: CalendarSyncMode { get { CalendarSyncMode(rawValue: syncMode ?? "") ?? .weekComplete } set { syncMode = newValue.rawValue } } var iCloudSyncEnabledResolved: Bool { get { iCloudSyncEnabled ?? true } set { iCloudSyncEnabled = newValue } } var showDishPhotosInPlannerResolved: Bool { get { showDishPhotosInPlanner ?? true } set { showDishPhotosInPlanner = newValue } } var weekExportStyleEnum: WeekExportStyle { 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 { case dinnerOnly case lunchOnly case both var localizedKey: String { switch self { case .dinnerOnly: return "meal_windows_dinner_only" case .lunchOnly: return "meal_windows_lunch_only" case .both: return "meal_windows_both" } } var icon: String { switch self { case .dinnerOnly: return "moon.stars.fill" case .lunchOnly: return "sun.max.fill" case .both: return "sun.and.horizon.fill" } } } enum AppLanguage: String, CaseIterable { case system case spanish case english case french case german case italian case portugueseBrazil var displayName: String { switch self { case .system: return "System" case .spanish: return "Español" case .english: return "English" case .french: return "Français" case .german: return "Deutsch" case .italian: return "Italiano" case .portugueseBrazil: return "Português (Brasil)" } } var localeIdentifier: String { switch self { case .system: return resolved().localeIdentifier case .spanish: return "es" case .english: return "en" case .french: return "fr" case .german: return "de" case .italian: return "it" case .portugueseBrazil: return "pt-BR" } } func resolved() -> AppLanguage { if self != .system { return self } let localeIdentifier = Locale.current.identifier.lowercased() if localeIdentifier.hasPrefix("es") { return .spanish } if localeIdentifier.hasPrefix("fr") { return .french } if localeIdentifier.hasPrefix("de") { return .german } if localeIdentifier.hasPrefix("it") { return .italian } if localeIdentifier.hasPrefix("pt") { return .portugueseBrazil } return .english } } 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 { case weekComplete case manual var localizedKey: String { switch self { case .weekComplete: return "settings_sync_mode_week_complete" case .manual: return "settings_sync_mode_manual" } } } enum WeekExportStyle: String, CaseIterable { case defaultStyle case schoolTimetable case vertical var localizedKey: String { switch self { case .defaultStyle: return "share_export_style_default" case .schoolTimetable: return "share_export_style_school" case .vertical: return "share_export_style_vertical" } } }