restore: original UI views from first build, keep fixed models/API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
import Foundation
|
||||
|
||||
struct FoodEntry: Codable, Identifiable, Hashable {
|
||||
let id: String
|
||||
let foodName: String
|
||||
let servingDescription: String?
|
||||
let quantity: Double
|
||||
let unit: String
|
||||
let mealType: String
|
||||
let calories: Double
|
||||
let protein: Double
|
||||
let carbs: Double
|
||||
let fat: Double
|
||||
let sugar: Double?
|
||||
let fiber: Double?
|
||||
let entryType: String?
|
||||
let method: String?
|
||||
let foodId: String?
|
||||
let imageUrl: String?
|
||||
let note: String?
|
||||
let loggedAt: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, quantity, unit, calories, protein, carbs, fat, sugar, fiber, note, method
|
||||
case foodName = "food_name"
|
||||
case servingDescription = "serving_description"
|
||||
case mealType = "meal_type"
|
||||
case entryType = "entry_type"
|
||||
case foodId = "food_id"
|
||||
case imageUrl = "image_url"
|
||||
case loggedAt = "logged_at"
|
||||
}
|
||||
|
||||
/// Alternate keys from the snapshot-based API response
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: FlexibleCodingKeys.self)
|
||||
|
||||
id = try container.decode(String.self, forKey: .init("id"))
|
||||
quantity = try container.decodeIfPresent(Double.self, forKey: .init("quantity")) ?? 1
|
||||
unit = try container.decodeIfPresent(String.self, forKey: .init("unit")) ?? "serving"
|
||||
mealType = try container.decodeIfPresent(String.self, forKey: .init("meal_type")) ?? "snack"
|
||||
|
||||
// Handle both "food_name" and "snapshot_food_name"
|
||||
if let name = try container.decodeIfPresent(String.self, forKey: .init("food_name")) {
|
||||
foodName = name
|
||||
} else if let name = try container.decodeIfPresent(String.self, forKey: .init("snapshot_food_name")) {
|
||||
foodName = name
|
||||
} else {
|
||||
foodName = "Unknown"
|
||||
}
|
||||
|
||||
servingDescription = try container.decodeIfPresent(String.self, forKey: .init("serving_description"))
|
||||
?? container.decodeIfPresent(String.self, forKey: .init("snapshot_serving_label"))
|
||||
|
||||
// Handle both direct and snapshot_ prefixed fields
|
||||
calories = try container.decodeIfPresent(Double.self, forKey: .init("calories"))
|
||||
?? container.decodeIfPresent(Double.self, forKey: .init("snapshot_calories"))
|
||||
?? 0
|
||||
protein = try container.decodeIfPresent(Double.self, forKey: .init("protein"))
|
||||
?? container.decodeIfPresent(Double.self, forKey: .init("snapshot_protein"))
|
||||
?? 0
|
||||
carbs = try container.decodeIfPresent(Double.self, forKey: .init("carbs"))
|
||||
?? container.decodeIfPresent(Double.self, forKey: .init("snapshot_carbs"))
|
||||
?? 0
|
||||
fat = try container.decodeIfPresent(Double.self, forKey: .init("fat"))
|
||||
?? container.decodeIfPresent(Double.self, forKey: .init("snapshot_fat"))
|
||||
?? 0
|
||||
sugar = try container.decodeIfPresent(Double.self, forKey: .init("sugar"))
|
||||
?? container.decodeIfPresent(Double.self, forKey: .init("snapshot_sugar"))
|
||||
fiber = try container.decodeIfPresent(Double.self, forKey: .init("fiber"))
|
||||
?? container.decodeIfPresent(Double.self, forKey: .init("snapshot_fiber"))
|
||||
|
||||
entryType = try container.decodeIfPresent(String.self, forKey: .init("entry_type"))
|
||||
method = try container.decodeIfPresent(String.self, forKey: .init("entry_method"))
|
||||
?? container.decodeIfPresent(String.self, forKey: .init("method"))
|
||||
foodId = try container.decodeIfPresent(String.self, forKey: .init("food_id"))
|
||||
imageUrl = try container.decodeIfPresent(String.self, forKey: .init("image_url"))
|
||||
?? container.decodeIfPresent(String.self, forKey: .init("food_image_path"))
|
||||
note = try container.decodeIfPresent(String.self, forKey: .init("note"))
|
||||
loggedAt = try container.decodeIfPresent(String.self, forKey: .init("logged_at"))
|
||||
?? container.decodeIfPresent(String.self, forKey: .init("created_at"))
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(foodName, forKey: .foodName)
|
||||
try container.encodeIfPresent(servingDescription, forKey: .servingDescription)
|
||||
try container.encode(quantity, forKey: .quantity)
|
||||
try container.encode(unit, forKey: .unit)
|
||||
try container.encode(mealType, forKey: .mealType)
|
||||
try container.encode(calories, forKey: .calories)
|
||||
try container.encode(protein, forKey: .protein)
|
||||
try container.encode(carbs, forKey: .carbs)
|
||||
try container.encode(fat, forKey: .fat)
|
||||
try container.encodeIfPresent(sugar, forKey: .sugar)
|
||||
try container.encodeIfPresent(fiber, forKey: .fiber)
|
||||
try container.encodeIfPresent(entryType, forKey: .entryType)
|
||||
try container.encodeIfPresent(method, forKey: .method)
|
||||
try container.encodeIfPresent(foodId, forKey: .foodId)
|
||||
try container.encodeIfPresent(imageUrl, forKey: .imageUrl)
|
||||
try container.encodeIfPresent(note, forKey: .note)
|
||||
try container.encodeIfPresent(loggedAt, forKey: .loggedAt)
|
||||
}
|
||||
|
||||
static func == (lhs: FoodEntry, rhs: FoodEntry) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Flexible coding keys for handling multiple API shapes
|
||||
struct FlexibleCodingKeys: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init(_ string: String) {
|
||||
self.stringValue = string
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
self.stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
|
||||
struct FoodItem: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let brand: String?
|
||||
let baseUnit: String?
|
||||
let caloriesPerBase: Double
|
||||
let proteinPerBase: Double?
|
||||
let carbsPerBase: Double?
|
||||
let fatPerBase: Double?
|
||||
let sugarPerBase: Double?
|
||||
let fiberPerBase: Double?
|
||||
let status: String?
|
||||
let imageUrl: String?
|
||||
let favorite: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, brand, status, favorite
|
||||
case baseUnit = "base_unit"
|
||||
case caloriesPerBase = "calories_per_base"
|
||||
case proteinPerBase = "protein_per_base"
|
||||
case carbsPerBase = "carbs_per_base"
|
||||
case fatPerBase = "fat_per_base"
|
||||
case sugarPerBase = "sugar_per_base"
|
||||
case fiberPerBase = "fiber_per_base"
|
||||
case imageUrl = "image_url"
|
||||
}
|
||||
|
||||
var displayUnit: String {
|
||||
baseUnit ?? "serving"
|
||||
}
|
||||
|
||||
var displayInfo: String {
|
||||
let parts = [brand].compactMap { $0 }
|
||||
let prefix = parts.isEmpty ? "" : "\(parts.joined(separator: " ")) - "
|
||||
return "\(prefix)\(displayUnit)"
|
||||
}
|
||||
|
||||
func scaledCalories(quantity: Double) -> Double {
|
||||
caloriesPerBase * quantity
|
||||
}
|
||||
|
||||
func scaledProtein(quantity: Double) -> Double {
|
||||
(proteinPerBase ?? 0) * quantity
|
||||
}
|
||||
|
||||
func scaledCarbs(quantity: Double) -> Double {
|
||||
(carbsPerBase ?? 0) * quantity
|
||||
}
|
||||
|
||||
func scaledFat(quantity: Double) -> Double {
|
||||
(fatPerBase ?? 0) * quantity
|
||||
}
|
||||
}
|
||||
|
||||
struct DailyGoal: Codable {
|
||||
let calories: Double
|
||||
let protein: Double
|
||||
let carbs: Double
|
||||
let fat: Double
|
||||
let sugar: Double?
|
||||
let fiber: Double?
|
||||
|
||||
static let defaultGoal = DailyGoal(
|
||||
calories: 2000, protein: 150, carbs: 200, fat: 65, sugar: 50, fiber: 30
|
||||
)
|
||||
}
|
||||
|
||||
struct MealTemplate: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let mealType: String
|
||||
let calories: Double
|
||||
let protein: Double?
|
||||
let carbs: Double?
|
||||
let fat: Double?
|
||||
let itemsCount: Int?
|
||||
|
||||
// Support flexible decoding for templates with nested items
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, calories, protein, carbs, fat, items
|
||||
case mealType = "meal_type"
|
||||
case itemsCount = "items_count"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
mealType = try container.decodeIfPresent(String.self, forKey: .mealType) ?? "snack"
|
||||
protein = try container.decodeIfPresent(Double.self, forKey: .protein)
|
||||
carbs = try container.decodeIfPresent(Double.self, forKey: .carbs)
|
||||
fat = try container.decodeIfPresent(Double.self, forKey: .fat)
|
||||
|
||||
// Try direct calories first, then compute from items
|
||||
if let directCals = try? container.decode(Double.self, forKey: .calories) {
|
||||
calories = directCals
|
||||
itemsCount = try container.decodeIfPresent(Int.self, forKey: .itemsCount)
|
||||
} else if let items = try? container.decode([TemplateItem].self, forKey: .items) {
|
||||
calories = items.reduce(0) { $0 + ($1.snapshotCalories ?? 0) * ($1.quantity ?? 1) }
|
||||
itemsCount = items.count
|
||||
} else {
|
||||
calories = 0
|
||||
itemsCount = try container.decodeIfPresent(Int.self, forKey: .itemsCount)
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(mealType, forKey: .mealType)
|
||||
try container.encode(calories, forKey: .calories)
|
||||
try container.encodeIfPresent(protein, forKey: .protein)
|
||||
try container.encodeIfPresent(carbs, forKey: .carbs)
|
||||
try container.encodeIfPresent(fat, forKey: .fat)
|
||||
try container.encodeIfPresent(itemsCount, forKey: .itemsCount)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TemplateItem: Codable {
|
||||
let snapshotCalories: Double?
|
||||
let quantity: Double?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case snapshotCalories = "snapshot_calories"
|
||||
case quantity
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateEntryRequest: Encodable {
|
||||
let foodId: String
|
||||
let quantity: Double
|
||||
let unit: String
|
||||
let mealType: String
|
||||
let entryDate: String
|
||||
let entryMethod: String
|
||||
let source: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case quantity, unit, source
|
||||
case foodId = "food_id"
|
||||
case mealType = "meal_type"
|
||||
case entryDate = "entry_date"
|
||||
case entryMethod = "entry_method"
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateEntryRequest: Encodable {
|
||||
var quantity: Double?
|
||||
var unit: String?
|
||||
var mealType: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case quantity, unit
|
||||
case mealType = "meal_type"
|
||||
}
|
||||
}
|
||||
|
||||
struct SplitFoodRequest: Encodable {
|
||||
let text: String
|
||||
let mealType: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text
|
||||
case mealType = "meal_type"
|
||||
}
|
||||
}
|
||||
|
||||
// Meals enum for type safety
|
||||
enum MealType: String, CaseIterable, Identifiable {
|
||||
case breakfast
|
||||
case lunch
|
||||
case dinner
|
||||
case snack
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
rawValue.capitalized
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .breakfast: return "sunrise.fill"
|
||||
case .lunch: return "sun.max.fill"
|
||||
case .dinner: return "moon.fill"
|
||||
case .snack: return "leaf.fill"
|
||||
}
|
||||
}
|
||||
|
||||
static func guess() -> MealType {
|
||||
let hour = Calendar.current.component(.hour, from: Date())
|
||||
switch hour {
|
||||
case 5..<11: return .breakfast
|
||||
case 11..<15: return .lunch
|
||||
case 15..<20: return .dinner
|
||||
default: return .snack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for grouping entries by meal
|
||||
struct MealGroup: Identifiable {
|
||||
let meal: MealType
|
||||
let entries: [FoodEntry]
|
||||
|
||||
var id: String { meal.rawValue }
|
||||
|
||||
var totalCalories: Double {
|
||||
entries.reduce(0) { $0 + $1.calories }
|
||||
}
|
||||
|
||||
var totalProtein: Double {
|
||||
entries.reduce(0) { $0 + $1.protein }
|
||||
}
|
||||
|
||||
var totalCarbs: Double {
|
||||
entries.reduce(0) { $0 + $1.carbs }
|
||||
}
|
||||
|
||||
var totalFat: Double {
|
||||
entries.reduce(0) { $0 + $1.fat }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user