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 {
|
||||
|
||||
@@ -406,3 +406,7 @@
|
||||
"dish_ingredients_label" = "Zutaten";
|
||||
"dish_ingredient_placeholder" = "z. B. 200 g Spaghetti";
|
||||
"dish_ingredients_add" = "Zutat hinzufügen";
|
||||
|
||||
// Meal types (2.0)
|
||||
"breakfast" = "Frühstück";
|
||||
"snack" = "Snack";
|
||||
|
||||
@@ -406,3 +406,7 @@
|
||||
"dish_ingredients_label" = "Ingredients";
|
||||
"dish_ingredient_placeholder" = "e.g. 200 g spaghetti";
|
||||
"dish_ingredients_add" = "Add ingredient";
|
||||
|
||||
// Meal types (2.0)
|
||||
"breakfast" = "Breakfast";
|
||||
"snack" = "Snack";
|
||||
|
||||
@@ -406,3 +406,7 @@
|
||||
"dish_ingredients_label" = "Ingredientes";
|
||||
"dish_ingredient_placeholder" = "p. ej. 200 g de espaguetis";
|
||||
"dish_ingredients_add" = "Añadir ingrediente";
|
||||
|
||||
// Meal types (2.0)
|
||||
"breakfast" = "Desayuno";
|
||||
"snack" = "Merienda";
|
||||
|
||||
@@ -406,3 +406,7 @@
|
||||
"dish_ingredients_label" = "Ingrédients";
|
||||
"dish_ingredient_placeholder" = "ex. 200 g de spaghettis";
|
||||
"dish_ingredients_add" = "Ajouter un ingrédient";
|
||||
|
||||
// Meal types (2.0)
|
||||
"breakfast" = "Petit-déjeuner";
|
||||
"snack" = "Goûter";
|
||||
|
||||
@@ -406,3 +406,7 @@
|
||||
"dish_ingredients_label" = "Ingredienti";
|
||||
"dish_ingredient_placeholder" = "es. 200 g di spaghetti";
|
||||
"dish_ingredients_add" = "Aggiungi ingrediente";
|
||||
|
||||
// Meal types (2.0)
|
||||
"breakfast" = "Colazione";
|
||||
"snack" = "Merenda";
|
||||
|
||||
@@ -406,3 +406,7 @@
|
||||
"dish_ingredients_label" = "Ingredientes";
|
||||
"dish_ingredient_placeholder" = "ex.: 200 g de espaguete";
|
||||
"dish_ingredients_add" = "Adicionar ingrediente";
|
||||
|
||||
// Meal types (2.0)
|
||||
"breakfast" = "Café da manhã";
|
||||
"snack" = "Lanche";
|
||||
|
||||
@@ -37,12 +37,12 @@ final class CalendarService {
|
||||
let event = EKEvent(eventStore: eventStore)
|
||||
|
||||
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
|
||||
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
|
||||
let mealNameKey = slot.mealTypeEnum.localizedKey
|
||||
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
|
||||
event.title = "\(prefix)\(mealName): \(dishName)"
|
||||
|
||||
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
||||
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
||||
let slotTime = settings.time(for: slot.mealTypeEnum)
|
||||
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
|
||||
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
||||
event.notes = dishDescription
|
||||
@@ -82,12 +82,12 @@ final class CalendarService {
|
||||
}
|
||||
|
||||
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
|
||||
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
|
||||
let mealNameKey = slot.mealTypeEnum.localizedKey
|
||||
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
|
||||
event.title = "\(prefix)\(mealName): \(dishName)"
|
||||
|
||||
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
||||
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
||||
let slotTime = settings.time(for: slot.mealTypeEnum)
|
||||
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
|
||||
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
||||
event.notes = dishDescription
|
||||
@@ -124,7 +124,7 @@ final class CalendarService {
|
||||
event.startDate >= Date() else { continue }
|
||||
|
||||
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
||||
let newTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
||||
let newTime = settings.time(for: slot.mealTypeEnum)
|
||||
event.startDate = combineDateAndTime(date: slotDate, time: newTime)
|
||||
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
||||
|
||||
|
||||
@@ -48,13 +48,7 @@ struct DefaultDataService {
|
||||
context.insert(plan)
|
||||
|
||||
let maxDay = settings.includeWeekends ? 6 : 4
|
||||
let mealTypes: [String] = {
|
||||
switch settings.mealWindowsEnum {
|
||||
case .dinnerOnly: return ["dinner"]
|
||||
case .lunchOnly: return ["lunch"]
|
||||
case .both: return ["lunch", "dinner"]
|
||||
}
|
||||
}()
|
||||
let mealTypes: [String] = settings.activeMealTypes.map(\.rawValue)
|
||||
|
||||
for day in 0...maxDay {
|
||||
for mealType in mealTypes {
|
||||
|
||||
@@ -59,6 +59,7 @@ final class ICloudSyncService {
|
||||
settings: settings.map {
|
||||
SettingsPayload(
|
||||
mealWindows: $0.mealWindows,
|
||||
enabledMealTypesRaw: $0.enabledMealTypesRaw,
|
||||
includeWeekends: $0.includeWeekends,
|
||||
language: $0.language,
|
||||
calendarId: $0.calendarId,
|
||||
@@ -141,6 +142,7 @@ final class ICloudSyncService {
|
||||
if let settingsPayload = snapshot.settings {
|
||||
let settings = AppSettings()
|
||||
settings.mealWindows = settingsPayload.mealWindows
|
||||
settings.enabledMealTypesRaw = settingsPayload.enabledMealTypesRaw
|
||||
settings.includeWeekends = settingsPayload.includeWeekends
|
||||
settings.language = settingsPayload.language
|
||||
settings.calendarId = settingsPayload.calendarId
|
||||
@@ -227,6 +229,8 @@ private struct SyncSnapshot: Codable {
|
||||
|
||||
private struct SettingsPayload: Codable {
|
||||
let mealWindows: String
|
||||
// Optional for backward-compatibility with pre-2.0 snapshots.
|
||||
let enabledMealTypesRaw: String?
|
||||
let includeWeekends: Bool
|
||||
let language: String
|
||||
let calendarId: String?
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import Foundation
|
||||
import WidgetKit
|
||||
|
||||
/// Snapshot of today's meals for the widget. Generic over meal types (2.0):
|
||||
/// one entry per active meal type, in chronological order.
|
||||
struct TodayMealData: Codable {
|
||||
let lunch: String?
|
||||
let dinner: String?
|
||||
let lunchLabel: String
|
||||
let dinnerLabel: String
|
||||
struct Meal: Codable {
|
||||
let type: String // MealType raw value (drives icon/color in widget)
|
||||
let label: String // localized meal name
|
||||
let name: String? // dish name, nil when the slot is empty
|
||||
}
|
||||
|
||||
let meals: [Meal]
|
||||
let weekdayName: String
|
||||
let updatedAt: Date
|
||||
}
|
||||
@@ -20,11 +25,17 @@ enum WidgetDataStore {
|
||||
// weekday: 1=Sun,2=Mon..7=Sat → appDayOfWeek: 0=Mon..6=Sun
|
||||
let appDayOfWeek = (weekday + 5) % 7
|
||||
|
||||
let lunchId = plan?.slots.first { $0.dayOfWeek == appDayOfWeek && $0.mealType == MealType.lunch.rawValue }?.dishId
|
||||
let dinnerId = plan?.slots.first { $0.dayOfWeek == appDayOfWeek && $0.mealType == MealType.dinner.rawValue }?.dishId
|
||||
|
||||
let lunch = lunchId.flatMap { id in dishes.first { $0.id == id }?.name }
|
||||
let dinner = dinnerId.flatMap { id in dishes.first { $0.id == id }?.name }
|
||||
let meals: [TodayMealData.Meal] = settings.activeMealTypes.map { mealType in
|
||||
let dishId = plan?.slots.first {
|
||||
$0.dayOfWeek == appDayOfWeek && $0.mealType == mealType.rawValue
|
||||
}?.dishId
|
||||
let name = dishId.flatMap { id in dishes.first { $0.id == id }?.name }
|
||||
return TodayMealData.Meal(
|
||||
type: mealType.rawValue,
|
||||
label: String(localized: String.LocalizationValue(mealType.localizedKey)),
|
||||
name: name
|
||||
)
|
||||
}
|
||||
|
||||
let locale = Locale(identifier: settings.languageEnum.resolved().localeIdentifier)
|
||||
let formatter = DateFormatter()
|
||||
@@ -33,10 +44,7 @@ enum WidgetDataStore {
|
||||
let weekdayName = formatter.string(from: today).capitalized(with: locale)
|
||||
|
||||
let data = TodayMealData(
|
||||
lunch: lunch,
|
||||
dinner: dinner,
|
||||
lunchLabel: String(localized: "lunch"),
|
||||
dinnerLabel: String(localized: "dinner"),
|
||||
meals: meals,
|
||||
weekdayName: weekdayName,
|
||||
updatedAt: today
|
||||
)
|
||||
|
||||
@@ -377,7 +377,7 @@ final class HomeViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func mealTypeOrder(_ raw: String) -> Int {
|
||||
raw == MealType.lunch.rawValue ? 0 : 1
|
||||
MealType(rawValue: raw)?.order ?? 0
|
||||
}
|
||||
|
||||
private func applyCalendarSyncPolicy(plan: WeekPlan, settings: AppSettings, shouldNotify: Bool = true) {
|
||||
@@ -495,13 +495,7 @@ final class HomeViewModel: ObservableObject {
|
||||
|
||||
private func syncSlotsIfNeeded(plan: WeekPlan, settings: AppSettings, context: ModelContext) {
|
||||
let maxDay = settings.includeWeekends ? 6 : 4
|
||||
let mealTypes: [String] = {
|
||||
switch settings.mealWindowsEnum {
|
||||
case .dinnerOnly: return ["dinner"]
|
||||
case .lunchOnly: return ["lunch"]
|
||||
case .both: return ["lunch", "dinner"]
|
||||
}
|
||||
}()
|
||||
let mealTypes: [String] = settings.activeMealTypes.map(\.rawValue)
|
||||
|
||||
// Clean up any legacy duplicates for the same day+mealType key to keep UI mapping stable.
|
||||
var groupedByKey: [SlotKey: [MealSlot]] = [:]
|
||||
|
||||
@@ -8,7 +8,7 @@ final class OnboardingViewModel: ObservableObject {
|
||||
static let pendingAutoFillOnLaunchKey = "onboarding_pending_auto_fill_on_launch"
|
||||
|
||||
@Published var currentStep: Int = 0
|
||||
@Published var selectedMealWindows: MealWindows = .dinnerOnly
|
||||
@Published var selectedMealTypes: Set<MealType> = [.dinner]
|
||||
@Published var includeWeekends: Bool = true
|
||||
@Published var syncICloud: Bool = true
|
||||
@Published var syncCalendar: Bool = false
|
||||
@@ -130,7 +130,7 @@ final class OnboardingViewModel: ObservableObject {
|
||||
return s
|
||||
}()
|
||||
|
||||
settings.mealWindowsEnum = selectedMealWindows
|
||||
settings.activeMealTypes = MealType.allCases.filter { selectedMealTypes.contains($0) }
|
||||
settings.includeWeekends = includeWeekends
|
||||
settings.iCloudSyncEnabled = syncICloud
|
||||
settings.syncEnabled = syncCalendar
|
||||
|
||||
@@ -211,6 +211,11 @@ struct HomeView: View {
|
||||
let plan = ensureCurrentWeekPlanExists(settings: settings) else { return }
|
||||
viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context)
|
||||
}
|
||||
.onChange(of: settings?.enabledMealTypesRaw) { _, _ in
|
||||
guard let settings = settings,
|
||||
let plan = ensureCurrentWeekPlanExists(settings: settings) else { return }
|
||||
viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context)
|
||||
}
|
||||
.onChange(of: settings?.mealWindows) { _, _ in
|
||||
guard let settings = settings,
|
||||
let plan = ensureCurrentWeekPlanExists(settings: settings) else { return }
|
||||
|
||||
@@ -22,11 +22,7 @@ struct WeekCalendarView: View {
|
||||
}
|
||||
|
||||
private var mealTypes: [MealType] {
|
||||
switch settings.mealWindowsEnum {
|
||||
case .dinnerOnly: return [.dinner]
|
||||
case .lunchOnly: return [.lunch]
|
||||
case .both: return [.lunch, .dinner]
|
||||
}
|
||||
settings.activeMealTypes
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -45,11 +45,7 @@ struct WeekPlanShareView: View {
|
||||
}
|
||||
|
||||
private var mealTypes: [MealType] {
|
||||
switch settings.mealWindowsEnum {
|
||||
case .dinnerOnly: return [.dinner]
|
||||
case .lunchOnly: return [.lunch]
|
||||
case .both: return [.lunch, .dinner]
|
||||
}
|
||||
settings.activeMealTypes
|
||||
}
|
||||
|
||||
private var exportStyle: WeekExportStyle {
|
||||
@@ -389,11 +385,11 @@ struct WeekPlanShareView: View {
|
||||
HStack(spacing: 14) {
|
||||
Image(systemName: mealType.icon)
|
||||
.font(.system(size: 42, weight: .medium))
|
||||
.foregroundColor(mealType == .lunch ? Color(hex: "#C85E2A") : Color(hex: "#27604A"))
|
||||
.foregroundColor(mealTypeAccent(mealType))
|
||||
.frame(width: 56)
|
||||
Text(mealTypeLabel(mealType).uppercased(with: locale))
|
||||
.font(.system(size: 42, weight: .bold, design: .serif))
|
||||
.foregroundColor(mealType == .lunch ? Color(hex: "#C85E2A") : Color(hex: "#27604A"))
|
||||
.foregroundColor(mealTypeAccent(mealType))
|
||||
}
|
||||
Text(dishName(day: day, mealType: mealType))
|
||||
.font(.system(size: 62, weight: .medium, design: .serif))
|
||||
@@ -497,7 +493,17 @@ struct WeekPlanShareView: View {
|
||||
}
|
||||
|
||||
private func mealTypeLabel(_ mealType: MealType) -> String {
|
||||
mealType == .lunch ? String(localized: "lunch") : String(localized: "dinner")
|
||||
String(localized: String.LocalizationValue(mealType.localizedKey))
|
||||
}
|
||||
|
||||
/// Accent color per meal row in the vertical/school styles.
|
||||
private func mealTypeAccent(_ mealType: MealType) -> Color {
|
||||
switch mealType {
|
||||
case .breakfast: return Color(hex: "#B07C2B")
|
||||
case .lunch: return Color(hex: "#C85E2A")
|
||||
case .snack: return Color(hex: "#7B5EA7")
|
||||
case .dinner: return Color(hex: "#27604A")
|
||||
}
|
||||
}
|
||||
|
||||
private func dishName(day: Int, mealType: MealType) -> String {
|
||||
@@ -526,9 +532,17 @@ struct WeekPlanShareView: View {
|
||||
}
|
||||
|
||||
private func gridMealCell(_ title: String, icon: String, mealType: MealType) -> some View {
|
||||
let background = mealType == .lunch ?
|
||||
LinearGradient(colors: [Color(hex: "#FFE2C8"), Color(hex: "#FFD4B2")], startPoint: .topLeading, endPoint: .bottomTrailing) :
|
||||
LinearGradient(colors: [Color(hex: "#D7EFE7"), Color(hex: "#C2E7DB")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
let background: LinearGradient
|
||||
switch mealType {
|
||||
case .breakfast:
|
||||
background = LinearGradient(colors: [Color(hex: "#FFEFC9"), Color(hex: "#FFE5A9")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
case .lunch:
|
||||
background = LinearGradient(colors: [Color(hex: "#FFE2C8"), Color(hex: "#FFD4B2")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
case .snack:
|
||||
background = LinearGradient(colors: [Color(hex: "#EBE0F5"), Color(hex: "#DFD0EF")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
case .dinner:
|
||||
background = LinearGradient(colors: [Color(hex: "#D7EFE7"), Color(hex: "#C2E7DB")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
}
|
||||
return HStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
Text(title)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MealWindowsStepView: View {
|
||||
@Binding var selection: MealWindows
|
||||
@Binding var selection: Set<MealType>
|
||||
var onNext: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@@ -15,14 +15,21 @@ struct MealWindowsStepView: View {
|
||||
.padding(.top, 12)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
ForEach(MealWindows.allCases, id: \.self) { option in
|
||||
ForEach(MealType.allCases, id: \.self) { mealType in
|
||||
SelectionCard(
|
||||
icon: option.icon,
|
||||
title: optionTitle(option),
|
||||
isSelected: selection == option
|
||||
icon: mealType.icon,
|
||||
title: optionTitle(mealType),
|
||||
isSelected: selection.contains(mealType)
|
||||
) {
|
||||
withAnimation(.spring(response: 0.3)) {
|
||||
selection = option
|
||||
if selection.contains(mealType) {
|
||||
// Keep at least one meal selected.
|
||||
if selection.count > 1 {
|
||||
selection.remove(mealType)
|
||||
}
|
||||
} else {
|
||||
selection.insert(mealType)
|
||||
}
|
||||
}
|
||||
HapticManager.shared.selection()
|
||||
}
|
||||
@@ -38,12 +45,15 @@ struct MealWindowsStepView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
private func optionTitle(_ option: MealWindows) -> String {
|
||||
switch option {
|
||||
case .dinnerOnly: return "🌙 \(String(localized: "meal_windows_dinner_only"))"
|
||||
case .lunchOnly: return "☀️ \(String(localized: "meal_windows_lunch_only"))"
|
||||
case .both: return "🌞🌙 \(String(localized: "meal_windows_both"))"
|
||||
private func optionTitle(_ mealType: MealType) -> String {
|
||||
let emoji: String
|
||||
switch mealType {
|
||||
case .breakfast: emoji = "☕️"
|
||||
case .lunch: emoji = "🌞"
|
||||
case .snack: emoji = "🍎"
|
||||
case .dinner: emoji = "🌙"
|
||||
}
|
||||
return "\(emoji) \(String(localized: String.LocalizationValue(mealType.localizedKey)))"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ struct OnboardingView: View {
|
||||
.tag(0)
|
||||
|
||||
MealWindowsStepView(
|
||||
selection: $viewModel.selectedMealWindows,
|
||||
selection: $viewModel.selectedMealTypes,
|
||||
onNext: { viewModel.nextStep() }
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
|
||||
@@ -61,13 +61,26 @@ struct SettingsView: View {
|
||||
List {
|
||||
// Planning section
|
||||
Section {
|
||||
Picker("settings_meal_windows", selection: Binding(
|
||||
get: { settings.mealWindowsEnum },
|
||||
set: { settings.mealWindowsEnum = $0 }
|
||||
)) {
|
||||
Text("meal_windows_dinner_only").tag(MealWindows.dinnerOnly)
|
||||
Text("meal_windows_lunch_only").tag(MealWindows.lunchOnly)
|
||||
Text("meal_windows_both").tag(MealWindows.both)
|
||||
ForEach(MealType.allCases, id: \.self) { mealType in
|
||||
Toggle(isOn: Binding(
|
||||
get: { settings.activeMealTypes.contains(mealType) },
|
||||
set: { enabled in
|
||||
var types = settings.activeMealTypes
|
||||
if enabled {
|
||||
types.append(mealType)
|
||||
} else if types.count > 1 {
|
||||
// Keep at least one meal type active.
|
||||
types.removeAll { $0 == mealType }
|
||||
}
|
||||
settings.activeMealTypes = types
|
||||
}
|
||||
)) {
|
||||
Label(
|
||||
String(localized: String.LocalizationValue(mealType.localizedKey)),
|
||||
systemImage: mealType.icon
|
||||
)
|
||||
}
|
||||
.tint(.mealMoodCoral)
|
||||
}
|
||||
|
||||
Toggle("settings_include_weekends", isOn: Binding(
|
||||
|
||||
Reference in New Issue
Block a user