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:
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>MealMood</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>21</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.alexandrevazquez.mealmood</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,233 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
// MARK: - Shared data model (mirrors WidgetDataStore in main app)
|
||||
|
||||
private struct TodayMealData: Codable {
|
||||
let lunch: String?
|
||||
let dinner: String?
|
||||
let lunchLabel: String
|
||||
let dinnerLabel: String
|
||||
let weekdayName: String
|
||||
let updatedAt: Date
|
||||
}
|
||||
|
||||
private enum WidgetStore {
|
||||
static let appGroupID = "group.com.alexandrevazquez.mealmood"
|
||||
static let key = "mealmood.today_meals"
|
||||
|
||||
static func read() -> TodayMealData? {
|
||||
guard let defaults = UserDefaults(suiteName: appGroupID),
|
||||
let data = defaults.data(forKey: key) else { return nil }
|
||||
return try? JSONDecoder().decode(TodayMealData.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline
|
||||
|
||||
struct TodayMealEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let lunch: String?
|
||||
let dinner: String?
|
||||
let lunchLabel: String
|
||||
let dinnerLabel: String
|
||||
let weekdayName: String
|
||||
}
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> TodayMealEntry {
|
||||
TodayMealEntry(date: Date(), lunch: "Pasta al pesto", dinner: "Salmón al horno",
|
||||
lunchLabel: "Comida", dinnerLabel: "Cena", weekdayName: "Lunes")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (TodayMealEntry) -> Void) {
|
||||
let data = WidgetStore.read()
|
||||
completion(entry(from: data))
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<TodayMealEntry>) -> Void) {
|
||||
let data = WidgetStore.read()
|
||||
let nextMidnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
||||
completion(Timeline(entries: [entry(from: data)], policy: .after(nextMidnight)))
|
||||
}
|
||||
|
||||
private func entry(from data: TodayMealData?) -> TodayMealEntry {
|
||||
TodayMealEntry(
|
||||
date: Date(),
|
||||
lunch: data?.lunch,
|
||||
dinner: data?.dinner,
|
||||
lunchLabel: data?.lunchLabel ?? "Lunch",
|
||||
dinnerLabel: data?.dinnerLabel ?? "Dinner",
|
||||
weekdayName: data?.weekdayName ?? ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Colors
|
||||
|
||||
private extension Color {
|
||||
static let mmCoral = Color(red: 0.93, green: 0.42, blue: 0.31)
|
||||
static let mmBgWarm = Color(red: 1.00, green: 0.94, blue: 0.91)
|
||||
static let mmBgMint = Color(red: 0.93, green: 0.98, blue: 0.95)
|
||||
static let mmText = Color(red: 0.13, green: 0.13, blue: 0.13)
|
||||
static let mmSubtext = Color(red: 0.55, green: 0.55, blue: 0.55)
|
||||
static let mmLunchOrange = Color(red: 0.90, green: 0.55, blue: 0.18)
|
||||
static let mmDinnerBlue = Color(red: 0.36, green: 0.48, blue: 0.78)
|
||||
}
|
||||
|
||||
// MARK: - Small widget
|
||||
|
||||
private struct SmallWidgetView: View {
|
||||
let entry: TodayMealEntry
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "fork.knife")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundColor(.mmCoral)
|
||||
Text("MealMood")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundColor(.mmCoral)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text(entry.weekdayName)
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.mmSubtext)
|
||||
.lineLimit(1)
|
||||
.padding(.top, 2)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
mealRow(icon: "sun.max.fill", label: entry.lunchLabel,
|
||||
name: entry.lunch, color: .mmLunchOrange)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
mealRow(icon: "moon.stars.fill", label: entry.dinnerLabel,
|
||||
name: entry.dinner, color: .mmDinnerBlue)
|
||||
}
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
.background(
|
||||
LinearGradient(colors: [.mmBgWarm, .mmBgMint],
|
||||
startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
)
|
||||
}
|
||||
|
||||
private func mealRow(icon: String, label: String, name: String?, color: Color) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(color)
|
||||
.frame(width: 18)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(label.uppercased())
|
||||
.font(.system(size: 8, weight: .semibold))
|
||||
.foregroundColor(.mmSubtext)
|
||||
Text(name ?? "—")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(.mmText)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Medium widget
|
||||
|
||||
private struct MediumWidgetView: View {
|
||||
let entry: TodayMealEntry
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Image(systemName: "fork.knife")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.foregroundColor(.mmCoral)
|
||||
Spacer()
|
||||
Text(entry.weekdayName)
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
.foregroundColor(.mmText)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
Text(Date(), style: .date)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(.mmSubtext)
|
||||
}
|
||||
.padding(14)
|
||||
.frame(width: 112)
|
||||
.frame(maxHeight: .infinity, alignment: .leading)
|
||||
.background(Color.mmBgWarm)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.mmCoral.opacity(0.2))
|
||||
.frame(width: 1)
|
||||
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
mediumRow(icon: "sun.max.fill", label: entry.lunchLabel,
|
||||
name: entry.lunch, color: .mmLunchOrange)
|
||||
Divider()
|
||||
mediumRow(icon: "moon.stars.fill", label: entry.dinnerLabel,
|
||||
name: entry.dinner, color: .mmDinnerBlue)
|
||||
}
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
.background(Color.white)
|
||||
}
|
||||
}
|
||||
|
||||
private func mediumRow(icon: String, label: String, name: String?, color: Color) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(color)
|
||||
.frame(width: 22)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(label.uppercased())
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(.mmSubtext)
|
||||
Text(name ?? "—")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundColor(.mmText)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget entry view
|
||||
|
||||
struct MealMoodWidgetView: View {
|
||||
var entry: Provider.Entry
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemMedium:
|
||||
MediumWidgetView(entry: entry)
|
||||
default:
|
||||
SmallWidgetView(entry: entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget definition
|
||||
|
||||
@main
|
||||
struct MealMoodWidget: Widget {
|
||||
let kind = "MealMoodWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
MealMoodWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) { Color.white }
|
||||
}
|
||||
.configurationDisplayName("MealMood")
|
||||
.description("Today's lunch and dinner at a glance.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user