Improvements to FD MealPlanner integration

- Detailed nutrition facts are now shown on the menu item view
- Fixed The College Grind not having a "View Menu" button (IDs were mapped incorrectly)
- Menu items are now sorted alphabetically
- "Allergens" section now hidden on the menu item view if the item contains no allergens
- Duplicate items will no longer be added to the menu item list, so no more issues with items randomly appearing and disappearing (thanks FD MealPlanner for having duplicates in the first place)
This commit is contained in:
Campbell 2025-11-10 01:58:23 -05:00
parent c7639de06b
commit 85aa9e636d
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
6 changed files with 95 additions and 12 deletions

View File

@ -265,7 +265,7 @@
CODE_SIGN_ENTITLEMENTS = "RIT Dining/RIT Dining.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@ -300,7 +300,7 @@
CODE_SIGN_ENTITLEMENTS = "RIT Dining/RIT Dining.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;

View File

@ -16,6 +16,11 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] {
// will only be a single index to operate on.
if let allMenuRecipes = menu.result[0].allMenuRecipes {
for recipe in allMenuRecipes {
// Prevent duplicate items from being added, because for some reason the exact same item with the exact same information
// might be included in FD MealPlanner more than once.
if menuItems.contains(where: { $0.id == recipe.componentId }) {
continue
}
// englishAlternateName holds the proper name of the item, but it's blank for some items for some reason. If that's the
// case, then we should fall back on componentName, which is less user-friendly but works as a backup.
let realName = if recipe.englishAlternateName != "" {
@ -23,7 +28,7 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] {
} else {
recipe.componentName
}
let allergens = recipe.allergenName.components(separatedBy: ",")
let allergens = recipe.allergenName != "" ? recipe.allergenName.components(separatedBy: ",") : []
// Get the list of dietary markers (Vegan, Vegetarian, Pork, Beef), and drop "Vegetarian" if "Vegan" is also included since
// that's kinda redundant.
var dietaryMarkers = recipe.recipeProductDietaryName != "" ? recipe.recipeProductDietaryName.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } : []
@ -31,6 +36,23 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] {
dietaryMarkers.remove(at: dietaryMarkers.firstIndex(of: "Vegetarian")!)
}
let calories = Int(Double(recipe.calories)!.rounded())
// Collect and organize all the nutritional entries. I ordered them based off how they were ordered in the nutritional
// facts panel on the side of the bag of goldfish that lives on my desk, so presumably they're ordered correctly.
let nutritionalEntries = [
FDNutritionalEntry(type: "Total Fat", amount: Double(recipe.fat) ?? 0.0, unit: recipe.fatUOM),
FDNutritionalEntry(type: "Saturated Fat", amount: Double(recipe.saturatedFat) ?? 0.0, unit: recipe.saturatedFatUOM),
FDNutritionalEntry(type: "Trans Fat", amount: Double(recipe.transFattyAcid) ?? 0.0, unit: recipe.transFattyAcidUOM),
FDNutritionalEntry(type: "Cholesterol", amount: Double(recipe.cholesterol) ?? 0.0, unit: recipe.cholesterolUOM),
FDNutritionalEntry(type: "Sodium", amount: Double(recipe.sodium) ?? 0.0, unit: recipe.sodiumUOM),
FDNutritionalEntry(type: "Total Carbohydrates", amount: Double(recipe.carbohydrates) ?? 0.0, unit: recipe.carbohydratesUOM),
FDNutritionalEntry(type: "Dietary Fiber", amount: Double(recipe.dietaryFiber) ?? 0.0, unit: recipe.dietaryFiberUOM),
FDNutritionalEntry(type: "Total Sugars", amount: Double(recipe.totalSugars) ?? 0.0, unit: recipe.totalSugarsUOM),
FDNutritionalEntry(type: "Protein", amount: Double(recipe.protein) ?? 0.0, unit: recipe.proteinUOM),
FDNutritionalEntry(type: "Calcium", amount: Double(recipe.calcium) ?? 0.0, unit: recipe.calciumUOM),
FDNutritionalEntry(type: "Iron", amount: Double(recipe.iron) ?? 0.0, unit: recipe.ironUOM),
FDNutritionalEntry(type: "Vitamin A", amount: Double(recipe.vitaminA) ?? 0.0, unit: recipe.vitaminAUOM),
FDNutritionalEntry(type: "Vitamin C", amount: Double(recipe.vitaminC) ?? 0.0, unit: recipe.vitaminCUOM),
]
let newItem = FDMenuItem(
id: recipe.componentId,
@ -39,6 +61,7 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] {
category: recipe.category,
allergens: allergens,
calories: calories,
nutritionalEntries: nutritionalEntries,
dietaryMarkers: dietaryMarkers,
ingredients: recipe.ingredientStatement,
price: recipe.sellingPrice,

View File

@ -20,6 +20,6 @@ let tCtoFDMPMap: [Int: (Int, Int)] = [
441: (11, 11), // Loaded Latke
38: (12, 12), // Midnight Oil
26: (14, 4), // RITZ
9041: (18, 17), // The College Grind
35: (18, 17), // The College Grind
24: (15, 14), // The Commons
]

View File

@ -54,6 +54,32 @@ struct FDMealsParser: Decodable, Hashable {
let category: String
let allergenName: String
let calories: String
let carbohydrates: String
let carbohydratesUOM: String
let dietaryFiber: String
let dietaryFiberUOM: String
let fat: String
let fatUOM: String
let protein: String
let proteinUOM: String
let saturatedFat: String
let saturatedFatUOM: String
let transFattyAcid: String
let transFattyAcidUOM: String
let calcium: String
let calciumUOM: String
let cholesterol: String
let cholesterolUOM: String
let iron: String
let ironUOM: String
let sodium: String
let sodiumUOM: String
let vitaminA: String
let vitaminAUOM: String
let vitaminC: String
let vitaminCUOM: String
let totalSugars: String
let totalSugarsUOM: String
let recipeProductDietaryName: String
let ingredientStatement: String
let sellingPrice: Double
@ -74,6 +100,13 @@ struct FDMealsParser: Decodable, Hashable {
let result: [Result]
}
/// A single nutritional entry, including the amount and the unit. Used over a tuple for hashable purposes.
struct FDNutritionalEntry: Hashable {
let type: String
let amount: Double
let unit: String
}
/// A single menu item, stripped down and reorganized to a format that actually makes sense for me to use in the rest of the app.
struct FDMenuItem: Hashable, Identifiable {
let id: Int
@ -82,6 +115,7 @@ struct FDMenuItem: Hashable, Identifiable {
let category: String
let allergens: [String]
let calories: Int
let nutritionalEntries: [FDNutritionalEntry]
let dietaryMarkers: [String]
let ingredients: String
let price: Double

View File

@ -59,15 +59,37 @@ struct MenuItemView: View {
)
}
}
Text("Allergens")
.font(.headline)
.padding(.top, 8)
Text(menuItem.allergens.joined(separator: ", "))
.foregroundStyle(.secondary)
.textSelection(.enabled)
.padding(.bottom, 8)
.padding(.bottom, 12)
if !menuItem.allergens.isEmpty {
Text("Allergens")
.font(.title3)
.fontWeight(.semibold)
Text(menuItem.allergens.joined(separator: ", "))
.foregroundStyle(.secondary)
.textSelection(.enabled)
.padding(.bottom, 12)
.onAppear {
print(menuItem.allergens)
}
}
VStack(alignment: .leading) {
Text("Nutrition Facts")
.font(.title3)
.fontWeight(.semibold)
ForEach(menuItem.nutritionalEntries, id: \.self) { entry in
HStack(alignment: .top) {
Text(entry.type)
Spacer()
Text("\(String(format: "%.1f", entry.amount))\(entry.unit)")
.foregroundStyle(.secondary)
}
Divider()
}
}
.padding(.bottom, 12)
Text("Ingredients")
.font(.headline)
.font(.title3)
.fontWeight(.semibold)
Text(menuItem.ingredients)
.foregroundStyle(.secondary)
.textSelection(.enabled)
@ -88,6 +110,7 @@ struct MenuItemView: View {
category: "Baked Goods",
allergens: ["Wheat", "Gluten", "Egg", "Milk", "Soy"],
calories: 470,
nutritionalEntries: [FDNutritionalEntry(type: "Example", amount: 0.0, unit: "g")],
dietaryMarkers: ["Vegetarian"],
ingredients: "Some ingredients that you'd expect to find inside of a chocolate chip muffin",
price: 2.79,

View File

@ -67,6 +67,9 @@ struct MenuView: View {
let searchedLocations = searchText.isEmpty || item.name.localizedCaseInsensitiveContains(searchText)
return searchedLocations
}
newItems.sort { firstItem, secondItem in
return firstItem.name.localizedCaseInsensitiveCompare(secondItem.name) == .orderedAscending
}
return newItems
}