1.0.5: Eating out slots, rule violations panel, Crashlytics, search bar fix
- Eating out: tap any empty slot → "Mark as eating out"; shows teal indicator, skipped by auto-assign, counts as complete, tap again to unmark - Rule violations panel: access via wand long-press context menu when conflicts exist; shows all isRuleOverridden slots with Fix (clear) or Ignore (acknowledge) actions - Firebase Crashlytics integrated: CrashlyticsService + dSYM upload build phase, isPremium property tracked per session - PremiumSyncService: extracted premium state machine, StoreKit Transaction.updates listener, isPremium no longer synced via iCloud to avoid stale state - StoreManager: analytics on purchase/restore, bundle ID fallback for product ID lookup - Search bar contrast bug fixed: TextField now has explicit foreground color for dark mode - WelcomeStepView: redesigned onboarding welcome screen with week preview - Version bump: 1.0.5 build 22 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ final class HomeViewModel: ObservableObject {
|
||||
private struct SlotSnapshot {
|
||||
let dishId: UUID?
|
||||
let isRuleOverridden: Bool
|
||||
let isEatingOut: Bool
|
||||
}
|
||||
|
||||
@Published var currentWeekStart: Date
|
||||
@@ -116,7 +117,7 @@ final class HomeViewModel: ObservableObject {
|
||||
captureUndoSnapshot(plan: plan)
|
||||
isAutoCompleting = true
|
||||
|
||||
let emptySlots = plan.slots.filter { $0.dishId == nil }
|
||||
let emptySlots = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut }
|
||||
let result = AutocompleteEngine.autocomplete(
|
||||
emptySlots: emptySlots,
|
||||
currentPlan: plan,
|
||||
@@ -160,6 +161,7 @@ final class HomeViewModel: ObservableObject {
|
||||
}
|
||||
slot.dishId = nil
|
||||
slot.isRuleOverridden = false
|
||||
slot.isEatingOut = false
|
||||
}
|
||||
plan.updatedAt = Date()
|
||||
AnalyticsService.logWeekReset()
|
||||
@@ -297,9 +299,11 @@ final class HomeViewModel: ObservableObject {
|
||||
if let snap = lastSlotsSnapshot[slot.id] {
|
||||
slot.dishId = snap.dishId
|
||||
slot.isRuleOverridden = snap.isRuleOverridden
|
||||
slot.isEatingOut = snap.isEatingOut
|
||||
} else {
|
||||
slot.dishId = nil
|
||||
slot.isRuleOverridden = false
|
||||
slot.isEatingOut = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +327,7 @@ final class HomeViewModel: ObservableObject {
|
||||
guard !isApplyingUndo else { return }
|
||||
lastWeekStartSnapshot = plan.weekStartDate
|
||||
lastSlotsSnapshot = Dictionary(uniqueKeysWithValues: plan.slots.map { slot in
|
||||
(slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden))
|
||||
(slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut))
|
||||
})
|
||||
hasUndoSnapshot = true
|
||||
}
|
||||
@@ -336,7 +340,7 @@ final class HomeViewModel: ObservableObject {
|
||||
|
||||
private func firstFreeSlot(in plan: WeekPlan) -> MealSlot? {
|
||||
plan.slots
|
||||
.filter { $0.dishId == nil }
|
||||
.filter { $0.dishId == nil && !$0.isEatingOut }
|
||||
.sorted { lhs, rhs in
|
||||
if lhs.dayOfWeek != rhs.dayOfWeek {
|
||||
return lhs.dayOfWeek < rhs.dayOfWeek
|
||||
@@ -355,7 +359,7 @@ final class HomeViewModel: ObservableObject {
|
||||
|
||||
switch settings.syncModeEnum {
|
||||
case .weekComplete:
|
||||
if plan.slots.allSatisfy({ $0.dishId != nil }) {
|
||||
if plan.slots.allSatisfy({ $0.dishId != nil || $0.isEatingOut }) {
|
||||
let descriptor = FetchDescriptor<Dish>()
|
||||
let dishes = (try? plan.modelContext?.fetch(descriptor)) ?? []
|
||||
syncAllAssignedSlotsToCalendar(plan: plan, dishes: dishes, settings: settings)
|
||||
@@ -426,6 +430,33 @@ final class HomeViewModel: ObservableObject {
|
||||
slot.isRuleOverridden = !valid
|
||||
}
|
||||
|
||||
func markEatingOut(slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
|
||||
captureUndoSnapshot(plan: plan)
|
||||
if let eventId = slot.calendarEventId {
|
||||
CalendarService.shared.deleteEvent(eventId: eventId)
|
||||
slot.calendarEventId = nil
|
||||
}
|
||||
slot.dishId = nil
|
||||
slot.isRuleOverridden = false
|
||||
slot.isEatingOut = true
|
||||
plan.updatedAt = Date()
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
|
||||
HapticManager.shared.impact(style: .medium)
|
||||
showToastMessage(localizedString("toast_eating_out_marked", language: settings.languageEnum.resolved()))
|
||||
}
|
||||
|
||||
func unmarkEatingOut(slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
|
||||
captureUndoSnapshot(plan: plan)
|
||||
slot.isEatingOut = false
|
||||
plan.updatedAt = Date()
|
||||
HapticManager.shared.impact(style: .light)
|
||||
}
|
||||
|
||||
func acknowledgeViolation(slot: MealSlot, plan: WeekPlan) {
|
||||
slot.isRuleOverridden = false
|
||||
plan.updatedAt = Date()
|
||||
}
|
||||
|
||||
func confirmInvalidDrop(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
|
||||
invalidDropContext = InvalidDropContext(dish: dish, slot: slot)
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
|
||||
Reference in New Issue
Block a user