From c5c7e3d2e4d437f2940c1f6bf4a4f54fdeaafc6f Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Thu, 26 Feb 2026 10:27:09 +0100 Subject: [PATCH] Release 1.0.0 baseline --- .DS_Store | Bin 6148 -> 6148 bytes MealMood.xcodeproj/project.pbxproj | 4 + .../UserInterfaceState.xcuserstate | Bin 41384 -> 45796 bytes .../xcschemes/MealMood-IAP.xcscheme | 2 +- .../xcshareddata/xcschemes/MealMood.xcscheme | 2 +- MealMood/.DS_Store | Bin 6148 -> 8196 bytes MealMood/Components/AdBannerView.swift | 2 + MealMood/Configuration/aaaa.storekit | 76 +++++ MealMood/ContentView.swift | 3 - .../Resources/en.lproj/Localizable.strings | 2 + .../Resources/es.lproj/Localizable.strings | 2 + MealMood/Services/CalendarService.swift | 19 +- MealMood/Services/StoreManager.swift | 20 +- MealMood/ViewModels/HomeViewModel.swift | 5 +- MealMood/ViewModels/OnboardingViewModel.swift | 11 +- MealMood/ViewModels/SettingsViewModel.swift | 61 +++- MealMood/Views/Home/HomeView.swift | 105 +++--- MealMood/Views/Home/WeekCalendarView.swift | 33 +- MealMood/Views/Home/WeekPlanShareView.swift | 299 ++++++++++++++---- .../Views/Onboarding/CalendarStepView.swift | 25 ++ .../Onboarding/FirstDishesStepView.swift | 56 +++- .../Views/Onboarding/OnboardingView.swift | 29 ++ MealMood/Views/Premium/PremiumView.swift | 5 +- MealMood/Views/Settings/SettingsView.swift | 7 +- 24 files changed, 600 insertions(+), 168 deletions(-) create mode 100644 MealMood/Configuration/aaaa.storekit diff --git a/.DS_Store b/.DS_Store index 30d7846f36615c9eb97279e651fd4373cb892303..94eb701d5a4d991b4492c993aa65bf748319923b 100644 GIT binary patch delta 57 zcmZoMXffFEg^8)_#N>TU8k6Oid05x1n_%NJS)bV)&eEPdjhT<}!sd<4VJs6Hcs8?h H{N)D#UX>It delta 57 zcmZoMXffFEg^8*B-{gHv8k6Oid06!s#V>eI)@L?{v$Q8qW9DN#uz4eM7|X;4p3UqW GfB69&;1VJL diff --git a/MealMood.xcodeproj/project.pbxproj b/MealMood.xcodeproj/project.pbxproj index 7810967..9b7e9f5 100644 --- a/MealMood.xcodeproj/project.pbxproj +++ b/MealMood.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 016D78C0DC1B1F6E671F36F7 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */; }; + 0EC555FE2F4498780005513D /* aaaa.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 0EC555FD2F4498780005513D /* aaaa.storekit */; }; 0F91C8611D3BBE1FBD51D1E6 /* MealSlot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797A41D292142240FBDF9F2A /* MealSlot.swift */; }; 19330C6ECE7A60110A3A679D /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB00DA865F6FE86952BFC3 /* TagListView.swift */; }; 203DDFF14C0688DEDB6EE83E /* Localization+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0164A33496A304E40685EA6D /* Localization+Helpers.swift */; }; @@ -88,6 +89,7 @@ 049AE68D5B577514ADD6BACB /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 050EA827CA951AB9E847E2CA /* AdBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdBannerView.swift; sourceTree = ""; }; 065982B284538632951F1C3A /* MealMoodLogo.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = MealMoodLogo.svg; sourceTree = ""; }; + 0EC555FD2F4498780005513D /* aaaa.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = aaaa.storekit; sourceTree = ""; }; 0F46C32F97EAD52F8F3BC0B7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 103374A7D91B4F884278DA72 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = ""; }; 10594EA55F6BEFF4241D364C /* FirstDishesStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstDishesStepView.swift; sourceTree = ""; }; @@ -193,6 +195,7 @@ 4B4ACE1D0F2968B284A1FA7E /* Configuration */ = { isa = PBXGroup; children = ( + 0EC555FD2F4498780005513D /* aaaa.storekit */, ); path = Configuration; sourceTree = ""; @@ -455,6 +458,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0EC555FE2F4498780005513D /* aaaa.storekit in Resources */, EA4EC6244EEBE6D568BCA5E5 /* Assets.xcassets in Resources */, 87EB371FD3B6C241AC721C55 /* Localizable.strings in Resources */, 7CBE113B28462C6C41C35533 /* MealMoodLogo.svg in Resources */, diff --git a/MealMood.xcodeproj/project.xcworkspace/xcuserdata/alexandrev.xcuserdatad/UserInterfaceState.xcuserstate b/MealMood.xcodeproj/project.xcworkspace/xcuserdata/alexandrev.xcuserdatad/UserInterfaceState.xcuserstate index 9387c096aeedfb904e06e4f01d5aae565b43267b..6d010367431d4073906d8e0ba90ae34006e16f69 100644 GIT binary patch delta 23086 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 H4^968z?I9r delta 20210 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 + identifier = "../../../MealMood/Configuration/MealMood.storekit"> + identifier = "../../MealMood/Configuration/aaaa.storekit"> aqvop7NLq$#C5Obdd!eJMrS0&VSWMasQ*rtRQ#rreo( z%gfl9xKY5k()f0zk;uY;8aJA#h%OZu7~_Jd@v*>#8FwK@UW+t5aiF8#YR}3SDb=H87~|AJUc-EOVlJ+_zlEd&targhN`nH|kDwWL;-Z&J^SQ zS=Y{wIo^;~kxkp#yq)&VX4~$uj+xX~?qzeXZSQeCOH4D6^35Z@7@IYe8uZ+p-x^Fd zq-lB2+-K5UR>5qe8NHN6OA5!z8N?uVwrtso)pxJo)VMv_-qE@DQn^;KWND?U4qArk z_|~vx=K3Zq!yg?;ds)*lEN7&@V0l)?HhZ1)m|20kSnN`H)$;1Q;<`SUX)2$w4(BcZ zs8*vW!5WE`y1L(>i|cd^v5KLcS7;tuea%W;e^`vP=1i)M&|zJzuJ(F#4_Z1jtlyxj zy^mUsp?TsG$aV7;RqwOY8PnE`m|Ul6tE%@GB>&ojVdsuYRZnHzu`#+uFYIe>(e(pj z+1+W+?;bZDx}=NY+V|7gpf{S%ncWkPnG1V&QE%`_sb4s?8;G@1y`0-|Fz$%fEGVLIEy z+SqP(kiJ13XQ$Zn>OCe%lRr^$Jg_E-pHHyHhwSf;@v#O)BF&3`MA7(9&T;yk??*(;793X ze({Y<@$s`a7GL~-Txc$T`rSyyW`Gfi8ZwwwlwW%z8;n`j;BKRpMnTfFiksw z3W6w^5y6+FF4YxNnw=@oK9Y}lL=h_17D($=>S~4Lqm{_!T6LX5!qG})vo2n*kW@7u zG}Vpq%`pxWCM>9;=8J407YtcZ_ejj$Ci{f2Gdmj#92x(+sU@GWWJ7yLRSt~;m4HGZEzt_wXm$9A0& zvYJxH^a-NY%`s3U-K#|Y-#+*E|C?JR@{SmY7`UYwKyh17TMJ#{T(4P?YbWS9NQWrG zZc<|4LRFBC69(xx;q^ZZ={!NE!Y3Kxq(thW@}GYQ5WisMW~)d0f3*JxL3kThe*@`W B9`yhK delta 121 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGqg=C6q~50$jCS`z?zY9@&;kK$yx$Do41Mu zF>Y-5&$^hMgF}!Rq!6f{8%VeUaofhi@640=Wpp_ifxH7i%rM!8N1BHTVlK!khRyLj GGnfHaQWaSM diff --git a/MealMood/Components/AdBannerView.swift b/MealMood/Components/AdBannerView.swift index 2bdfa02..1ad19cc 100644 --- a/MealMood/Components/AdBannerView.swift +++ b/MealMood/Components/AdBannerView.swift @@ -29,6 +29,7 @@ struct AdBannerView: View { } } .frame(height: 56) + .clipped() } .frame(maxWidth: .infinity) .background(Color.mealMoodSurface) @@ -52,6 +53,7 @@ private struct BannerBridge: UIViewRepresentable { func makeUIView(context: Context) -> UIView { let container = UIView() container.backgroundColor = UIColor(Color.mealMoodSurface) + container.clipsToBounds = true let width = max(UIScreen.main.bounds.width, 320) let adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(width) diff --git a/MealMood/Configuration/aaaa.storekit b/MealMood/Configuration/aaaa.storekit new file mode 100644 index 0000000..41fbd7d --- /dev/null +++ b/MealMood/Configuration/aaaa.storekit @@ -0,0 +1,76 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "8A126AD4", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_askToBuyEnabled" : false, + "_billingGracePeriodEnabled" : false, + "_billingIssuesEnabled" : false, + "_disableDialogs" : false, + "_failTransactionsEnabled" : false, + "_locale" : "en_US", + "_renewalBillingIssuesEnabled" : false, + "_storefront" : "USA", + "_storeKitErrors" : [ + + ], + "_timeRate" : 0 + }, + "subscriptionGroups" : [ + { + "id" : "6E6036BC", + "localizations" : [ + + ], + "name" : "Test", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "2.99", + "familyShareable" : true, + "groupNumber" : 1, + "internalID" : "375D53F5", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "com.mealmood.premium.monthly", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "com.mealmood.premium.monthly", + "subscriptionGroupID" : "6E6036BC", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + } + ] + } + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} diff --git a/MealMood/ContentView.swift b/MealMood/ContentView.swift index 6988a5d..2684c40 100644 --- a/MealMood/ContentView.swift +++ b/MealMood/ContentView.swift @@ -44,9 +44,6 @@ struct ContentView: View { Task { switch phase { case .active: - if settings?.iCloudSyncEnabledResolved ?? true { - await ICloudSyncService.shared.pullRemoteIfNeeded(context: context) - } await syncPremiumStatus() case .inactive, .background: if settings?.iCloudSyncEnabledResolved ?? true { diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings index 8c61f72..aee7272 100644 --- a/MealMood/Resources/en.lproj/Localizable.strings +++ b/MealMood/Resources/en.lproj/Localizable.strings @@ -242,3 +242,5 @@ "reminder_30" = "30 min before"; "reminder_60" = "1 hour before"; "reminder_120" = "2 hours before"; + +"first_dishes_limit_reached" = "You reached 10 dishes. Tap Finish when you are ready."; diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings index afa23b5..beb1c91 100644 --- a/MealMood/Resources/es.lproj/Localizable.strings +++ b/MealMood/Resources/es.lproj/Localizable.strings @@ -242,3 +242,5 @@ "reminder_30" = "30 min antes"; "reminder_60" = "1 hora antes"; "reminder_120" = "2 horas antes"; + +"first_dishes_limit_reached" = "Has llegado a 10 platos. Pulsa Finalizar cuando quieras."; diff --git a/MealMood/Services/CalendarService.swift b/MealMood/Services/CalendarService.swift index 0b305cd..57a54b4 100644 --- a/MealMood/Services/CalendarService.swift +++ b/MealMood/Services/CalendarService.swift @@ -28,8 +28,10 @@ final class CalendarService { weekStartDate: Date, settings: AppSettings ) -> String? { - guard let calendarId = settings.calendarId, - let calendar = eventStore.calendar(withIdentifier: calendarId) else { return nil } + guard let calendar = resolvedCalendar(for: settings) else { + print("Calendar sync skipped: no writable calendar available") + return nil + } let event = EKEvent(eventStore: eventStore) @@ -81,4 +83,17 @@ final class CalendarService { try? eventStore.save(event, span: .thisEvent) } } + + private func resolvedCalendar(for settings: AppSettings) -> EKCalendar? { + if let calendarId = settings.calendarId, + let calendar = eventStore.calendar(withIdentifier: calendarId) { + return calendar + } + + if let defaultCalendar = eventStore.defaultCalendarForNewEvents { + return defaultCalendar + } + + return eventStore.calendars(for: .event).first + } } diff --git a/MealMood/Services/StoreManager.swift b/MealMood/Services/StoreManager.swift index bcb5794..18b2042 100644 --- a/MealMood/Services/StoreManager.swift +++ b/MealMood/Services/StoreManager.swift @@ -28,10 +28,6 @@ final class StoreManager: ObservableObject { @Published var debugLoadedProducts: [String] = [] static let monthlyProductId = "com.mealmood.premium.monthly" - static let monthlyProductIdAlt = "com.alexandrevazquez.mealmood.premium.monthly" - static let monthlyProductIdLegacy = "com.alexandrev.mealmood.premium.monthly" - static let monthlyProductIdShort = "com.alexandrevazquez.mealmood.premium" - private var productIds: [String] { var ids = [Self.monthlyProductId] @@ -114,20 +110,13 @@ final class StoreManager: ObservableObject { } var monthlyProduct: Product? { - let prioritizedIds = [ - Self.monthlyProductIdAlt, - Self.monthlyProductId, - Self.monthlyProductIdLegacy, - Self.monthlyProductIdShort - ] - if let match = products.first(where: { product in - prioritizedIds.contains(product.id) && product.type == .autoRenewable + product.id == Self.monthlyProductId && product.type == .autoRenewable }) { return match } - if let match = products.first(where: { prioritizedIds.contains($0.id) }) { + if let match = products.first(where: { $0.id == Self.monthlyProductId }) { return match } @@ -145,10 +134,7 @@ final class StoreManager: ObservableObject { var debugProductIds: [String] { productIds } static func hasActiveSubscription(productIds: [String] = [ - StoreManager.monthlyProductId, - StoreManager.monthlyProductIdAlt, - StoreManager.monthlyProductIdLegacy, - StoreManager.monthlyProductIdShort + StoreManager.monthlyProductId ]) async -> Bool { for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result, diff --git a/MealMood/ViewModels/HomeViewModel.swift b/MealMood/ViewModels/HomeViewModel.swift index c038b18..55400fd 100644 --- a/MealMood/ViewModels/HomeViewModel.swift +++ b/MealMood/ViewModels/HomeViewModel.swift @@ -68,13 +68,16 @@ final class HomeViewModel: ObservableObject { ) if let existing = try? context.fetch(descriptor).first { - syncSlotsIfNeeded(plan: existing, settings: settings, context: context) return existing } return DefaultDataService.createWeekPlan(for: currentWeekStart, settings: settings, context: context) } + func reconcileSlotsIfNeeded(plan: WeekPlan, settings: AppSettings, context: ModelContext) { + syncSlotsIfNeeded(plan: plan, settings: settings, context: context) + } + func assignDish(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings, isOverride: Bool = false) { captureUndoSnapshot(plan: plan) diff --git a/MealMood/ViewModels/OnboardingViewModel.swift b/MealMood/ViewModels/OnboardingViewModel.swift index fe62488..66499b2 100644 --- a/MealMood/ViewModels/OnboardingViewModel.swift +++ b/MealMood/ViewModels/OnboardingViewModel.swift @@ -6,6 +6,7 @@ final class OnboardingViewModel: ObservableObject { @Published var currentStep: Int = 0 @Published var selectedMealWindows: MealWindows = .dinnerOnly @Published var includeWeekends: Bool = true + @Published var syncICloud: Bool = true @Published var syncCalendar: Bool = false @Published var selectedCalendarId: String? @Published var lunchTime: Date = { @@ -21,6 +22,7 @@ final class OnboardingViewModel: ObservableObject { @Published var newDishName: String = "" @Published var newDishTags: Set = [] @Published var addedDishes: [(name: String, tagIds: [UUID])] = [] + let maxOnboardingDishes = 10 let totalSteps = 5 @@ -36,10 +38,15 @@ final class OnboardingViewModel: ObservableObject { } var canAddDish: Bool { - !newDishName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + !newDishName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && canAddMoreDishes + } + + var canAddMoreDishes: Bool { + addedDishes.count < maxOnboardingDishes } func addDish(defaultTagId: UUID? = nil) { + guard canAddMoreDishes else { return } guard canAddDish else { return } let normalizedName = newDishName.trimmingCharacters(in: .whitespacesAndNewlines) guard !addedDishes.contains(where: { $0.name.lowercased() == normalizedName.lowercased() }) else { return } @@ -64,6 +71,7 @@ final class OnboardingViewModel: ObservableObject { } func addSuggestedDish(name: String, preferredTagNames: [String], availableTags: [Tag]) { + guard canAddMoreDishes else { return } let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines) guard !normalizedName.isEmpty else { return } guard !addedDishes.contains(where: { $0.name.lowercased() == normalizedName.lowercased() }) else { return } @@ -115,6 +123,7 @@ final class OnboardingViewModel: ObservableObject { settings.mealWindowsEnum = selectedMealWindows settings.includeWeekends = includeWeekends + settings.iCloudSyncEnabled = syncICloud settings.syncEnabled = syncCalendar settings.calendarId = selectedCalendarId settings.lunchTime = lunchTime diff --git a/MealMood/ViewModels/SettingsViewModel.swift b/MealMood/ViewModels/SettingsViewModel.swift index c3b7e3e..7095ac5 100644 --- a/MealMood/ViewModels/SettingsViewModel.swift +++ b/MealMood/ViewModels/SettingsViewModel.swift @@ -6,11 +6,33 @@ import EventKit final class SettingsViewModel: ObservableObject { @Published var availableCalendars: [EKCalendar] = [] @Published var showCalendarPermissionAlert: Bool = false + @Published var showToast: Bool = false + @Published var toastMessage: String = "" func loadCalendars() { availableCalendars = CalendarService.shared.availableCalendars() } + @discardableResult + func ensureValidCalendarSelection(settings: AppSettings) -> Bool { + loadCalendars() + + guard !availableCalendars.isEmpty else { + settings.calendarId = nil + settings.syncEnabled = false + showToastMessage(String(localized: "toast_calendar_unavailable")) + return false + } + + if let selected = settings.calendarId, + availableCalendars.contains(where: { $0.calendarIdentifier == selected }) { + return true + } + + settings.calendarId = availableCalendars.first?.calendarIdentifier + return true + } + func requestCalendarAccess() async -> Bool { let granted = await CalendarService.shared.requestAccess() if !granted { @@ -42,10 +64,19 @@ final class SettingsViewModel: ObservableObject { try deleteAll(of: WeekPlan.self, in: context) try deleteAll(of: Dish.self, in: context) try deleteAll(of: Tag.self, in: context) - try deleteAll(of: AppSettings.self, in: context) - - DefaultDataService.createDefaultSettings(context: context) DefaultDataService.createDefaultTags(context: context) + + let settingsDescriptor = FetchDescriptor() + if let existingSettings = try context.fetch(settingsDescriptor).first { + resetSettings(existingSettings) + } else { + DefaultDataService.createDefaultSettings(context: context) + if let createdSettings = try context.fetch(settingsDescriptor).first { + resetSettings(createdSettings) + } + } + + try context.save() } catch { #if DEBUG print("Failed to reset all data: \(error)") @@ -61,4 +92,28 @@ final class SettingsViewModel: ObservableObject { } try context.save() } + + private func showToastMessage(_ message: String) { + toastMessage = message + withAnimation(.easeInOut(duration: 0.2)) { + showToast = true + } + } + + private func resetSettings(_ settings: AppSettings) { + let defaults = AppSettings() + settings.mealWindows = defaults.mealWindows + settings.includeWeekends = defaults.includeWeekends + settings.language = defaults.language + settings.calendarId = defaults.calendarId + settings.syncEnabled = defaults.syncEnabled + settings.syncMode = defaults.syncMode + settings.lunchTime = defaults.lunchTime + settings.dinnerTime = defaults.dinnerTime + settings.eventDuration = defaults.eventDuration + settings.eventPrefix = defaults.eventPrefix + settings.reminderMinutesBefore = defaults.reminderMinutesBefore + settings.iCloudSyncEnabled = false + settings.onboardingCompleted = false + } } diff --git a/MealMood/Views/Home/HomeView.swift b/MealMood/Views/Home/HomeView.swift index 680c831..9bc9392 100644 --- a/MealMood/Views/Home/HomeView.swift +++ b/MealMood/Views/Home/HomeView.swift @@ -26,9 +26,11 @@ struct HomeView: View { ZStack { Color.mealMoodBackground.ignoresSafeArea() - if let settings = settings, - let plan = viewModel.getOrCreateWeekPlan(context: context, settings: settings) { - mainContent(plan: plan, settings: settings) + if let settings = settings { + let plan = viewModel.getOrCreateWeekPlan(context: context, settings: settings) + if let plan { + mainContent(plan: plan, settings: settings) + } } } .toast(isShowing: $viewModel.showToast, message: viewModel.toastMessage) @@ -151,6 +153,26 @@ struct HomeView: View { } } } + .onAppear { + guard let settings = settings, + let plan = viewModel.getOrCreateWeekPlan(context: context, settings: settings) else { return } + viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) + } + .onChange(of: settings?.includeWeekends) { _, _ in + guard let settings = settings, + let plan = viewModel.getOrCreateWeekPlan(context: context, settings: settings) else { return } + viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) + } + .onChange(of: settings?.mealWindows) { _, _ in + guard let settings = settings, + let plan = viewModel.getOrCreateWeekPlan(context: context, settings: settings) else { return } + viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) + } + .onChange(of: viewModel.currentWeekStart) { _, _ in + guard let settings = settings, + let plan = viewModel.getOrCreateWeekPlan(context: context, settings: settings) else { return } + viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) + } } @ViewBuilder @@ -167,69 +189,37 @@ struct HomeView: View { .padding(.bottom, 8) } - if horizontalSizeClass == .regular { - HStack(alignment: .top, spacing: 20) { - VStack(spacing: 10) { - WeekCalendarView( - plan: plan, - settings: settings, - dishes: dishes, - tags: tags, - viewModel: viewModel, - onTapEmptySlot: { slot in - selectedEmptySlotId = slot.id - } - ) - - if isWeekComplete(plan: plan) { - exportCallout(plan: plan, settings: settings) - } - + VStack(spacing: 10) { + WeekCalendarView( + plan: plan, + settings: settings, + dishes: dishes, + tags: tags, + viewModel: viewModel, + onTapEmptySlot: { slot in + selectedEmptySlotId = slot.id } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + ) - VStack(spacing: 12) { - ScrollView(showsIndicators: true) { - dishDrawer(plan: plan, settings: settings) - .padding(.bottom, 6) - } - } - .frame(width: 360) - .frame(maxHeight: .infinity, alignment: .top) + if isWeekComplete(plan: plan) { + exportCallout(plan: plan, settings: settings) } - .padding(.horizontal, 16) - .padding(.bottom, 0) - } else { - VStack(spacing: 10) { - WeekCalendarView( - plan: plan, - settings: settings, - dishes: dishes, - tags: tags, - viewModel: viewModel, - onTapEmptySlot: { slot in - selectedEmptySlotId = slot.id - } - ) - if isWeekComplete(plan: plan) { - exportCallout(plan: plan, settings: settings) - } - - ScrollView(showsIndicators: true) { - dishDrawer(plan: plan, settings: settings) - .padding(.bottom, 0) - } - .frame(maxHeight: .infinity) + ScrollView(showsIndicators: true) { + dishDrawer(plan: plan, settings: settings) + .padding(.bottom, 0) } - .padding(.bottom, 0) + .frame(maxHeight: .infinity) } + .frame(maxWidth: horizontalSizeClass == .regular ? 1160 : .infinity) + .frame(maxWidth: .infinity, alignment: .top) + .padding(.horizontal, horizontalSizeClass == .regular ? 14 : 0) + .padding(.bottom, 0) } .frame(maxHeight: .infinity, alignment: .top) .safeAreaInset(edge: .bottom, spacing: 0) { if !settings.isPremium { AdBannerView() - .ignoresSafeArea(.container, edges: .bottom) } } .alert("reset_title", isPresented: $viewModel.showResetAlert) { @@ -596,7 +586,10 @@ struct HomeView: View { private func renderWeekShareImage(plan: WeekPlan, settings: AppSettings) -> UIImage? { let renderer = ImageRenderer(content: WeekPlanShareView(plan: plan, settings: settings, dishes: dishes, tags: tags)) - renderer.proposedSize = ProposedViewSize(width: 2400, height: 1700) + renderer.proposedSize = ProposedViewSize( + width: WeekPlanShareView.a4LandscapeWidth, + height: WeekPlanShareView.a4LandscapeHeight + ) renderer.scale = 1 return renderer.uiImage } diff --git a/MealMood/Views/Home/WeekCalendarView.swift b/MealMood/Views/Home/WeekCalendarView.swift index d52ed1c..8619508 100644 --- a/MealMood/Views/Home/WeekCalendarView.swift +++ b/MealMood/Views/Home/WeekCalendarView.swift @@ -44,7 +44,7 @@ struct WeekCalendarView: View { ForEach(Array(days.enumerated()), id: \.element) { index, day in dayHeaderCell(day: day, width: 96, height: 24) if index < days.count - 1 { - calendarDivider + calendarDivider() } } } @@ -59,7 +59,7 @@ struct WeekCalendarView: View { draggableSlotView(slot: slot, mealType: mealType) .frame(width: 96) if index < days.count - 1 { - calendarDivider + calendarDivider() } } } @@ -78,16 +78,25 @@ struct WeekCalendarView: View { let dayCount = CGFloat(dayRange.count) let spacing: CGFloat = 10 let rowLabelWidth: CGFloat = 86 + let headerHeight: CGFloat = 26 + let slotRowHeight: CGFloat = 96 + let verticalInset: CGFloat = 2 let contentWidth = proxy.size.width - rowLabelWidth - (dayCount * spacing) let dayWidth = max(92, contentWidth / max(dayCount, 1)) + let totalHeight = + (verticalInset * 2) + + headerHeight + + 1 + + (CGFloat(mealTypes.count) * slotRowHeight) + + CGFloat(max(0, mealTypes.count - 1)) VStack(spacing: 0) { HStack(spacing: 0) { - Color.clear.frame(width: rowLabelWidth, height: 24) + Color.clear.frame(width: rowLabelWidth, height: headerHeight) ForEach(Array(days.enumerated()), id: \.element) { index, day in - dayHeaderCell(day: day, width: dayWidth, height: 24) + dayHeaderCell(day: day, width: dayWidth, height: headerHeight) if index < days.count - 1 { - calendarDivider + calendarDivider(height: headerHeight) .padding(.horizontal, spacing / 2) } } @@ -98,15 +107,15 @@ struct WeekCalendarView: View { ForEach(Array(mealTypes.enumerated()), id: \.element) { rowIndex, mealType in HStack(spacing: 0) { mealTypeLabel(mealType) - .frame(width: rowLabelWidth) + .frame(width: rowLabelWidth, height: slotRowHeight) .padding(.trailing, spacing) ForEach(Array(days.enumerated()), id: \.element) { index, day in let slot = slotFor(day: day, mealType: mealType) draggableSlotView(slot: slot, mealType: mealType) - .frame(width: dayWidth) + .frame(width: dayWidth, height: slotRowHeight) if index < days.count - 1 { - calendarDivider + calendarDivider(height: slotRowHeight) .padding(.horizontal, spacing / 2) } } @@ -118,15 +127,17 @@ struct WeekCalendarView: View { } } .padding(.horizontal, 16) - .padding(.vertical, 6) + .padding(.vertical, verticalInset) + .frame(height: totalHeight, alignment: .top) } - .frame(minHeight: CGFloat(mealTypes.count) * 142 + 54) + .frame(height: CGFloat(mealTypes.count) * 98 + 44) } - private var calendarDivider: some View { + private func calendarDivider(height: CGFloat? = nil) -> some View { Rectangle() .fill(Color.mealMoodTextSecondary.opacity(0.22)) .frame(width: 1) + .frame(height: height) } private var calendarHorizontalDivider: some View { diff --git a/MealMood/Views/Home/WeekPlanShareView.swift b/MealMood/Views/Home/WeekPlanShareView.swift index abbe198..fcc2ce1 100644 --- a/MealMood/Views/Home/WeekPlanShareView.swift +++ b/MealMood/Views/Home/WeekPlanShareView.swift @@ -1,4 +1,5 @@ import SwiftUI +import CoreImage.CIFilterBuiltins struct WeekPlanShareView: View { let plan: WeekPlan @@ -6,6 +7,13 @@ struct WeekPlanShareView: View { let dishes: [Dish] let tags: [Tag] + static let a4LandscapeWidth: CGFloat = 3508 + static let a4LandscapeHeight: CGFloat = 2480 + + private let shareURL = "https://mealmood.app" + private let qrContext = CIContext() + private let qrFilter = CIFilter.qrCodeGenerator() + private var dayRange: ClosedRange { settings.includeWeekends ? 0...6 : 0...4 } @@ -17,65 +25,64 @@ struct WeekPlanShareView: View { case .both: return [.lunch, .dinner] } } - + + private var exportDateString: String { + Date.now.formatted(date: .abbreviated, time: .omitted) + } + + private var qrImage: UIImage? { + qrFilter.setValue(Data(shareURL.utf8), forKey: "inputMessage") + qrFilter.setValue("M", forKey: "inputCorrectionLevel") + guard let outputImage = qrFilter.outputImage else { return nil } + let transformed = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10)) + guard let cgImage = qrContext.createCGImage(transformed, from: transformed.extent) else { return nil } + return UIImage(cgImage: cgImage) + } + var body: some View { ZStack { - LinearGradient( - colors: [ - Color(hex: "#FFF6F0"), - Color(hex: "#F6FCFA") - ], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .ignoresSafeArea() + backgroundLayer - VStack(alignment: .leading, spacing: 28) { - HStack(spacing: 16) { - AppIconPlaceholder(size: 72) - VStack(alignment: .leading, spacing: 6) { - Text("MealMood") - .font(.system(size: 44, weight: .bold)) - .foregroundColor(.mealMoodTextPrimary) - Text(plan.weekStartDate.formattedWeekRange()) - .font(.system(size: 26, weight: .medium)) - .foregroundColor(.mealMoodTextSecondary) - } - Spacer() - Text(Date.now.formatted(date: .abbreviated, time: .omitted)) - .font(.system(size: 20, weight: .medium)) - .foregroundColor(.mealMoodTextSecondary) - } + VStack(alignment: .leading, spacing: 34) { + headerBlock - Grid(horizontalSpacing: 10, verticalSpacing: 10) { - GridRow { - gridHeaderCell("") - .frame(minWidth: 220) - ForEach(Array(dayRange), id: \.self) { day in - gridHeaderCell(dayTitle(for: day)) - } - } + ZStack { + RoundedRectangle(cornerRadius: 30) + .fill(Color.white.opacity(0.62)) + RoundedRectangle(cornerRadius: 30) + .stroke(Color.white.opacity(0.75), lineWidth: 2) - ForEach(mealTypes, id: \.self) { mealType in + Grid(horizontalSpacing: 10, verticalSpacing: 10) { GridRow { - gridMealCell(mealTypeLabel(mealType), icon: mealType.icon) + gridHeaderCell("", day: nil) .frame(minWidth: 220) ForEach(Array(dayRange), id: \.self) { day in - gridDishCell(dishName(day: day, mealType: mealType)) + gridHeaderCell(dayTitle(for: day), day: day) + } + } + + ForEach(mealTypes, id: \.self) { mealType in + GridRow { + gridMealCell(mealTypeLabel(mealType), icon: mealType.icon, mealType: mealType) + .frame(minWidth: 220) + ForEach(Array(dayRange), id: \.self) { day in + gridDishCell(dishName(day: day, mealType: mealType), day: day) + } } } } + .padding(18) } + .shadow(color: Color(hex: "#DDAA99").opacity(0.18), radius: 26, x: 0, y: 16) Spacer() - Text("share_week_title") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(.mealMoodTextSecondary) + footerBlock } - .padding(56) + .padding(.horizontal, 84) + .padding(.vertical, 72) } - .frame(width: 2400, height: 1700) + .frame(width: Self.a4LandscapeWidth, height: Self.a4LandscapeHeight) } private func dayTitle(for day: Int) -> String { @@ -89,7 +96,7 @@ struct WeekPlanShareView: View { private func mealTypeLabel(_ mealType: MealType) -> String { mealType == .lunch ? String(localized: "lunch") : String(localized: "dinner") } - + private func dishName(day: Int, mealType: MealType) -> String { let slot = plan.slots.first { $0.dayOfWeek == day && $0.mealType == mealType.rawValue } guard let slot, let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else { @@ -97,32 +104,160 @@ struct WeekPlanShareView: View { } return dish.name } - - private func gridHeaderCell(_ title: String) -> some View { - Text(title) - .font(.system(size: 24, weight: .semibold)) + + private var backgroundLayer: some View { + ZStack { + LinearGradient( + colors: [ + Color(hex: "#FFEFE6"), + Color(hex: "#F1FBF5") + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + Circle() + .fill(Color(hex: "#FFD6E2").opacity(0.55)) + .frame(width: 760, height: 760) + .blur(radius: 20) + .offset(x: -980, y: -760) + + Circle() + .fill(Color(hex: "#CFF2E8").opacity(0.55)) + .frame(width: 680, height: 680) + .blur(radius: 20) + .offset(x: 1050, y: 760) + + GeometryReader { geo in + Canvas { context, size in + let spacing: CGFloat = 80 + var x: CGFloat = -size.height + while x < size.width { + var path = Path() + path.move(to: CGPoint(x: x, y: 0)) + path.addLine(to: CGPoint(x: x + size.height, y: size.height)) + context.stroke(path, with: .color(Color(hex: "#FFFFFF").opacity(0.26)), lineWidth: 1.2) + x += spacing + } + } + .frame(width: geo.size.width, height: geo.size.height) + } + } + } + + private var headerBlock: some View { + HStack(alignment: .center, spacing: 24) { + AppIconPlaceholder(size: 84) + + VStack(alignment: .leading, spacing: 8) { + Text("MealMood Premium") + .font(.system(size: 50, weight: .bold, design: .rounded)) + .foregroundColor(.mealMoodTextPrimary) + Text(String(localized: "share_week_title")) + .font(.system(size: 28, weight: .semibold, design: .rounded)) + .foregroundColor(Color(hex: "#5D645F")) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 8) { + Text(plan.weekStartDate.formattedWeekRange()) + .font(.system(size: 36, weight: .bold, design: .rounded)) + .foregroundColor(Color(hex: "#4A4F4B")) + Text(exportDateString) + .font(.system(size: 24, weight: .medium, design: .rounded)) + .foregroundColor(.mealMoodTextSecondary) + } + } + .padding(.horizontal, 30) + .padding(.vertical, 24) + .background(Color.white.opacity(0.76)) + .clipShape(RoundedRectangle(cornerRadius: 24)) + .overlay( + RoundedRectangle(cornerRadius: 24) + .stroke(Color(hex: "#F3C4B5").opacity(0.7), lineWidth: 1) + ) + .shadow(color: Color(hex: "#DDAA99").opacity(0.15), radius: 20, x: 0, y: 12) + } + + private var footerBlock: some View { + HStack(alignment: .center, spacing: 20) { + VStack(alignment: .leading, spacing: 6) { + Text("Powered by MealMood") + .font(.system(size: 30, weight: .bold, design: .rounded)) + .foregroundColor(Color(hex: "#3A3A3A")) + Text(shareURL) + .font(.system(size: 24, weight: .semibold, design: .rounded)) + .foregroundColor(Color(hex: "#588A77")) + } + + Spacer() + + if let qrImage { + VStack(spacing: 6) { + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 162, height: 162) + .padding(10) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 18)) + Text("Scan me") + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundColor(.mealMoodTextSecondary) + } + } + } + .padding(.horizontal, 26) + .padding(.vertical, 18) + .background(Color.white.opacity(0.8)) + .clipShape(RoundedRectangle(cornerRadius: 22)) + .overlay( + RoundedRectangle(cornerRadius: 22) + .stroke(Color(hex: "#DDEEE6"), lineWidth: 1) + ) + .shadow(color: Color(hex: "#7F9E90").opacity(0.14), radius: 16, x: 0, y: 8) + } + + private func gridHeaderCell(_ title: String, day: Int?) -> some View { + let background = dayHeaderBackground(day: day) + return Text(title) + .font(.system(size: 25, weight: .bold, design: .rounded)) .foregroundColor(.mealMoodTextPrimary) .frame(maxWidth: .infinity, minHeight: 92) .padding(.horizontal, 10) - .background(Color.white.opacity(0.95)) + .background(background) .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color.white.opacity(0.6), lineWidth: 1) + ) } - - private func gridMealCell(_ title: String, icon: String) -> some View { - HStack(spacing: 8) { + + 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) + return HStack(spacing: 8) { Image(systemName: icon) Text(title) } - .font(.system(size: 24, weight: .semibold)) - .foregroundColor(.mealMoodTextPrimary) + .font(.system(size: 25, weight: .bold, design: .rounded)) + .foregroundColor(Color(hex: "#365548")) .frame(maxWidth: .infinity, minHeight: 130) .padding(.horizontal, 14) - .background(Color.white.opacity(0.95)) + .background(background) .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color.white.opacity(0.6), lineWidth: 1) + ) } - - private func gridDishCell(_ title: String) -> some View { - Text(title) + + private func gridDishCell(_ title: String, day: Int) -> some View { + let tint = dayCellTint(day: day) + return Text(title) .font(.system(size: 26, weight: .medium)) .foregroundColor(.mealMoodTextPrimary) .multilineTextAlignment(.center) @@ -130,7 +265,55 @@ struct WeekPlanShareView: View { .minimumScaleFactor(0.6) .frame(maxWidth: .infinity, minHeight: 130) .padding(.horizontal, 10) - .background(Color.white.opacity(0.92)) + .background( + LinearGradient( + colors: [Color.white.opacity(0.95), tint.opacity(0.32)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color(hex: "#F2E5DE"), lineWidth: 1) + ) + } + + private func dayHeaderBackground(day: Int?) -> LinearGradient { + guard let day else { + return LinearGradient( + colors: [Color(hex: "#F6E7DE"), Color(hex: "#F2DED2")], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + } + let palettes: [(String, String)] = [ + ("#FFD5D9", "#FFC8D2"), + ("#FFE1C8", "#FFD3AF"), + ("#FFF0C8", "#FFE6A8"), + ("#DFF2CC", "#CFEAB6"), + ("#D3F0E7", "#BFE6DA"), + ("#D9E8FF", "#C9DDFF"), + ("#E6D9FF", "#D9C8FF") + ] + let index = max(0, min(day, palettes.count - 1)) + return LinearGradient( + colors: [Color(hex: palettes[index].0), Color(hex: palettes[index].1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + } + + private func dayCellTint(day: Int) -> Color { + let colors = [ + Color(hex: "#FFD0D8"), + Color(hex: "#FFE1C6"), + Color(hex: "#FFF0C3"), + Color(hex: "#DFF1CA"), + Color(hex: "#D5EFE6"), + Color(hex: "#D7E7FF"), + Color(hex: "#E7DCFF") + ] + return colors[max(0, min(day, colors.count - 1))] } } diff --git a/MealMood/Views/Onboarding/CalendarStepView.swift b/MealMood/Views/Onboarding/CalendarStepView.swift index b786c5f..2a6908f 100644 --- a/MealMood/Views/Onboarding/CalendarStepView.swift +++ b/MealMood/Views/Onboarding/CalendarStepView.swift @@ -2,10 +2,12 @@ import SwiftUI import EventKit struct CalendarStepView: View { + @Binding var iCloudSyncEnabled: Bool @Binding var syncEnabled: Bool @Binding var selectedCalendarId: String? @Binding var lunchTime: Date @Binding var dinnerTime: Date + var onEnableICloud: () -> Void var onNext: () -> Void var onSkip: () -> Void @@ -23,6 +25,29 @@ struct CalendarStepView: View { .padding(.top, 32) VStack(spacing: 16) { + // iCloud sync toggle + HStack { + Text("settings_icloud_sync") + .font(.mealMoodBody) + .foregroundColor(.mealMoodTextPrimary) + Spacer() + Toggle("", isOn: $iCloudSyncEnabled) + .tint(.mealMoodCoral) + .labelsHidden() + .onChange(of: iCloudSyncEnabled) { _, newValue in + if newValue { + onEnableICloud() + } + } + } + .padding(16) + .background(Color.mealMoodSurface) + .cornerRadius(14) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color(hex: "#F0F0F0"), lineWidth: 1) + ) + // Sync toggle HStack { Text("calendar_sync") diff --git a/MealMood/Views/Onboarding/FirstDishesStepView.swift b/MealMood/Views/Onboarding/FirstDishesStepView.swift index 985f399..1749700 100644 --- a/MealMood/Views/Onboarding/FirstDishesStepView.swift +++ b/MealMood/Views/Onboarding/FirstDishesStepView.swift @@ -29,6 +29,8 @@ struct FirstDishesStepView: View { ?? availableTags.sorted(by: { $0.sortOrder < $1.sortOrder }).first?.id } + private let maxVisibleSuggestions = 3 + var body: some View { ScrollView { VStack(spacing: 24) { @@ -165,12 +167,13 @@ struct FirstDishesStepView: View { .foregroundColor(.mealMoodTextSecondary) .padding(.horizontal, 24) - ForEach(suggestedDishes(), id: \.name) { suggestion in - let isAdded = viewModel.addedDishes.contains(where: { - $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == suggestion.name.lowercased() - }) + Text("Added \(viewModel.addedDishes.count)/\(viewModel.maxOnboardingDishes)") + .font(.mealMoodCaption) + .foregroundColor(.mealMoodTextSecondary) + .padding(.horizontal, 24) + + ForEach(visibleSuggestions(), id: \.name) { suggestion in Button { - guard !isAdded else { return } viewModel.addSuggestedDish( name: suggestion.name, preferredTagNames: suggestion.tagHints, @@ -182,21 +185,28 @@ struct FirstDishesStepView: View { Text(suggestion.name) .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) - Text(isAdded ? "first_dishes_suggestion_added" : "first_dishes_suggestion_add") + Text("first_dishes_suggestion_add") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) } Spacer() - Image(systemName: isAdded ? "checkmark.circle.fill" : "plus.circle.fill") - .foregroundColor(isAdded ? .mealMoodSuccess : .mealMoodCoral) + Image(systemName: "plus.circle.fill") + .foregroundColor(.mealMoodCoral) } .padding(12) .background(Color.mealMoodSurface) .cornerRadius(12) } - .disabled(isAdded) + .disabled(!viewModel.canAddMoreDishes) .padding(.horizontal, 24) } + + if !viewModel.canAddMoreDishes { + Text("first_dishes_limit_reached") + .font(.mealMoodCaption) + .foregroundColor(.mealMoodTextSecondary) + .padding(.horizontal, 24) + } } Spacer(minLength: 40) @@ -236,17 +246,41 @@ struct FirstDishesStepView: View { return [ (name: "Roast chicken", tagHints: ["Meat"]), (name: "Lentil stew", tagHints: ["Legumes"]), - (name: "Veggie pasta", tagHints: ["Pasta/Rice", "Vegetables"]) + (name: "Veggie pasta", tagHints: ["Pasta/Rice", "Vegetables"]), + (name: "Chicken curry", tagHints: ["Meat"]), + (name: "Salmon with rice", tagHints: ["Fish", "Pasta/Rice"]), + (name: "Turkey meatballs", tagHints: ["Meat"]), + (name: "Chickpea salad", tagHints: ["Legumes", "Vegetables"]), + (name: "Beef tacos", tagHints: ["Meat"]), + (name: "Vegetable soup", tagHints: ["Vegetables"]), + (name: "Baked cod", tagHints: ["Fish"]) ] } return [ (name: "Pollo asado", tagHints: ["Carne"]), (name: "Lentejas estofadas", tagHints: ["Legumbres"]), - (name: "Pasta con verduras", tagHints: ["Pasta/Arroz", "Verduras"]) + (name: "Pasta con verduras", tagHints: ["Pasta/Arroz", "Verduras"]), + (name: "Pollo al curry", tagHints: ["Carne"]), + (name: "Salmón con arroz", tagHints: ["Pescado", "Pasta/Arroz"]), + (name: "Albóndigas de pavo", tagHints: ["Carne"]), + (name: "Ensalada de garbanzos", tagHints: ["Legumbres", "Verduras"]), + (name: "Tacos de ternera", tagHints: ["Carne"]), + (name: "Crema de verduras", tagHints: ["Verduras"]), + (name: "Merluza al horno", tagHints: ["Pescado"]) ] } + private func visibleSuggestions() -> [(name: String, tagHints: [String])] { + let addedNames = Set( + viewModel.addedDishes.map { + $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + } + ) + let remaining = suggestedDishes().filter { !addedNames.contains($0.name.lowercased()) } + return Array(remaining.prefix(maxVisibleSuggestions)) + } + private func ensureDefaultTagsIfNeeded() { let descriptor = FetchDescriptor() if (try? context.fetch(descriptor))?.isEmpty ?? true { diff --git a/MealMood/Views/Onboarding/OnboardingView.swift b/MealMood/Views/Onboarding/OnboardingView.swift index ca2d5dd..bab6574 100644 --- a/MealMood/Views/Onboarding/OnboardingView.swift +++ b/MealMood/Views/Onboarding/OnboardingView.swift @@ -42,10 +42,14 @@ struct OnboardingView: View { .tag(2) CalendarStepView( + iCloudSyncEnabled: $viewModel.syncICloud, syncEnabled: $viewModel.syncCalendar, selectedCalendarId: $viewModel.selectedCalendarId, lunchTime: $viewModel.lunchTime, dinnerTime: $viewModel.dinnerTime, + onEnableICloud: { + Task { await restoreFromICloudAndFinishIfNeeded() } + }, onNext: { viewModel.nextStep() }, onSkip: { viewModel.nextStep() } ) @@ -68,6 +72,7 @@ struct OnboardingView: View { } .onAppear { ensureDefaultTagsIfNeeded() + Task { await restoreFromICloudAndFinishIfNeeded() } } } @@ -77,4 +82,28 @@ struct OnboardingView: View { DefaultDataService.createDefaultTags(context: context) } } + + private func restoreFromICloudAndFinishIfNeeded() async { + let settingsDescriptor = FetchDescriptor() + let currentSettings = (try? context.fetch(settingsDescriptor))?.first + let iCloudEnabled = currentSettings?.iCloudSyncEnabledResolved ?? viewModel.syncICloud + guard iCloudEnabled else { return } + + await ICloudSyncService.shared.pullRemoteIfNeeded(context: context) + + guard shouldAutoCompleteOnboarding() else { return } + if let settings = (try? context.fetch(settingsDescriptor))?.first, !settings.onboardingCompleted { + settings.onboardingCompleted = true + try? context.save() + } + onComplete() + } + + private func shouldAutoCompleteOnboarding() -> Bool { + let dishes = (try? context.fetch(FetchDescriptor())) ?? [] + if !dishes.isEmpty { return true } + + let plans = (try? context.fetch(FetchDescriptor())) ?? [] + return !plans.isEmpty + } } diff --git a/MealMood/Views/Premium/PremiumView.swift b/MealMood/Views/Premium/PremiumView.swift index 680b314..2c73390 100644 --- a/MealMood/Views/Premium/PremiumView.swift +++ b/MealMood/Views/Premium/PremiumView.swift @@ -116,12 +116,15 @@ struct PremiumView: View { .foregroundColor(.mealMoodTextSecondary) .multilineTextAlignment(.center) .padding(.horizontal, 24) - .padding(.bottom, 40) + .padding(.bottom, 24) } + .safeAreaPadding(.bottom, 20) } } .navigationTitle("settings_premium") .navigationBarTitleDisplayMode(.inline) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) .task { await storeManager.loadProducts() } diff --git a/MealMood/Views/Settings/SettingsView.swift b/MealMood/Views/Settings/SettingsView.swift index a3d1bd4..7bb9447 100644 --- a/MealMood/Views/Settings/SettingsView.swift +++ b/MealMood/Views/Settings/SettingsView.swift @@ -23,6 +23,7 @@ struct SettingsView: View { .toolbarBackground(Color.mealMoodBackground, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .environment(\.isEnabled, true) + .toast(isShowing: $viewModel.showToast, message: viewModel.toastMessage) } @ViewBuilder @@ -64,7 +65,8 @@ struct SettingsView: View { Task { let granted = await viewModel.requestCalendarAccess() if granted { - settings.syncEnabled = true + let hasCalendar = viewModel.ensureValidCalendarSelection(settings: settings) + settings.syncEnabled = hasCalendar } } } else { @@ -214,7 +216,8 @@ struct SettingsView: View { .tint(.mealMoodCoral) .onAppear { if settings.syncEnabled { - viewModel.loadCalendars() + let hasCalendar = viewModel.ensureValidCalendarSelection(settings: settings) + settings.syncEnabled = hasCalendar } } .alert("calendar_permission_title", isPresented: $viewModel.showCalendarPermissionAlert) {