Started work on refactors

- The favorites model now lives inside of the base dining model, since it was only ever used in places where the main dining model was also available and is only relevant when the dining model is available.
- Removed unnecessary instances of models that were going unused.
- Moved the favorite/map/menu buttons in the top right of the DetailView into the right side toolbar.
  - This frees up a good bit of space at the top of the DetailView and looks cleaner, especially with iOS 26's new toolbar style.
- Actually added a copyright string to the about screen.
More refactors, both internally and for the UI, will be coming soon.
This commit is contained in:
Campbell 2026-01-07 19:29:30 -05:00
parent 6794c66c37
commit d3d060b5e2
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
11 changed files with 186 additions and 156 deletions

View File

@ -14,9 +14,22 @@
376AE05B2E6495EB00AB698B /* TigerDine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TigerDine.app; sourceTree = BUILT_PRODUCTS_DIR; }; 376AE05B2E6495EB00AB698B /* TigerDine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TigerDine.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
37C76ABF2F0F3067009E7074 /* Exceptions for "TigerDine" folder in "TigerDine" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 376AE05A2E6495EB00AB698B /* TigerDine */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
376AE05D2E6495EB00AB698B /* TigerDine */ = { 376AE05D2E6495EB00AB698B /* TigerDine */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
37C76ABF2F0F3067009E7074 /* Exceptions for "TigerDine" folder in "TigerDine" target */,
);
path = TigerDine; path = TigerDine;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -269,9 +282,11 @@
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TigerDine/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TigerDine; INFOPLIST_KEY_CFBundleDisplayName = TigerDine;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Campbell Bagley";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -282,7 +297,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining"; PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -304,9 +319,11 @@
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TigerDine/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TigerDine; INFOPLIST_KEY_CFBundleDisplayName = TigerDine;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Campbell Bagley";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -317,7 +334,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining"; PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -11,14 +11,12 @@ struct ContentView: View {
// Save sort/filter options in AppStorage so that they actually get saved. // Save sort/filter options in AppStorage so that they actually get saved.
@AppStorage("openLocationsOnly") var openLocationsOnly: Bool = false @AppStorage("openLocationsOnly") var openLocationsOnly: Bool = false
@AppStorage("openLocationsFirst") var openLocationsFirst: Bool = false @AppStorage("openLocationsFirst") var openLocationsFirst: Bool = false
@State private var favorites = Favorites()
@State private var notifyingChefs = NotifyingChefs()
@State private var model = DiningModel() @State private var model = DiningModel()
@State private var isLoading: Bool = true @State private var isLoading: Bool = true
@State private var loadFailed: Bool = false @State private var loadFailed: Bool = false
@State private var showingDonationSheet: Bool = false @State private var showingDonationSheet: Bool = false
@State private var rotationDegrees: Double = 0 @State private var rotationDegrees: Double = 0
@State private var diningLocations: [DiningLocation] = []
@State private var searchText: String = "" @State private var searchText: String = ""
private var animation: Animation { private var animation: Animation {
@ -176,8 +174,6 @@ struct ContentView: View {
} }
} }
} }
.environment(favorites)
.environment(notifyingChefs)
.environment(model) .environment(model)
.task { .task {
await getDiningData() await getDiningData()

View File

@ -12,13 +12,13 @@ class DiningModel {
var locationsByDay = [[DiningLocation]]() var locationsByDay = [[DiningLocation]]()
var daysRepresented = [Date]() var daysRepresented = [Date]()
var lastRefreshed: Date? var lastRefreshed: Date?
var visitingChefPushes = VisitingChefPushesModel()
var notifyingChefs = NotifyingChefs()
// This is the actual method responsible for making requests to the API for the current day and next 6 days to collect all // External models that should be nested under this one.
// of the information that the app needs for the various view. Making it part of the model allows it to be updated from var favorites = Favorites()
// any view at any time, and prevents excess API requests (if you never refresh, the app will never need to make more than 7 var notifyingChefs = NotifyingChefs()
// calls per launch). var visitingChefPushes = VisitingChefPushesModel()
/// This is the actual method responsible for making requests to the API for the current day and next 6 days to collect all of the information that the app needs for the various view. Making it part of the model allows it to be updated from any view at any time, and prevents excess API requests (if you never refresh, the app will never need to make more than 7 calls per launch).
func getHoursByDay() async throws { func getHoursByDay() async throws {
let calendar = Calendar.current let calendar = Calendar.current
let today = calendar.startOfDay(for: Date()) let today = calendar.startOfDay(for: Date())
@ -47,8 +47,7 @@ class DiningModel {
lastRefreshed = Date() lastRefreshed = Date()
} }
// Iterates through all of the locations and updates their open status indicator based on the current time. Does a replace /// Iterates through all of the locations and updates their open status indicator based on the current time. Does a replace to make sure that it updates any views observing this model.
// to make sure that it updates any views observing this model.
func updateOpenStatuses() { func updateOpenStatuses() {
locationsByDay = locationsByDay.map { day in locationsByDay = locationsByDay.map { day in
day.map { location in day.map { location in
@ -59,6 +58,7 @@ class DiningModel {
} }
} }
/// Schedules and saves push notifications for all enabled visiting chefs.
func scheduleAllPushes() async { func scheduleAllPushes() async {
for day in locationsByDay { for day in locationsByDay {
for location in day { for location in day {
@ -80,7 +80,7 @@ class DiningModel {
await cleanupPushes() await cleanupPushes()
} }
// Cleanup old push notifications that have already gone by so we're not still tracking them forever and ever. /// Cleans up old push notifications that have already been delivered so that we're not still tracking them forever.
func cleanupPushes() async { func cleanupPushes() async {
let now = Date() let now = Date()
for push in visitingChefPushes.pushes { for push in visitingChefPushes.pushes {
@ -90,12 +90,14 @@ class DiningModel {
} }
} }
/// Cancels all pending push notifications. Used when disabling push notifications as a whole.
func cancelAllPushes() async { func cancelAllPushes() async {
let uuids = visitingChefPushes.pushes.map(\.uuid) let uuids = visitingChefPushes.pushes.map(\.uuid)
await cancelVisitingChefNotifs(uuids: uuids) await cancelVisitingChefNotifs(uuids: uuids)
visitingChefPushes.pushes.removeAll() visitingChefPushes.pushes.removeAll()
} }
/// Schedules and saves push notifications for a specific visiting chef.
func schedulePushesForChef(_ chefName: String) async { func schedulePushesForChef(_ chefName: String) async {
for day in locationsByDay { for day in locationsByDay {
for location in day { for location in day {

View File

@ -44,7 +44,7 @@ class VisitingChefPushesModel {
} }
} }
/// Cancel all reigstered push notifications for a specified visiting chef. /// Cancels all reigstered push notifications for a specified visiting chef.
func cancelPushesForChef(name: String) { func cancelPushesForChef(name: String) {
var uuids: [String] = [] var uuids: [String] = []
for push in pushes { for push in pushes {
@ -60,6 +60,7 @@ class VisitingChefPushesModel {
save() save()
} }
/// Checks if a push notification meeting the specified criteria is already scheduled.
func pushAlreadyRegisered(name: String, location: String, startTime: Date, endTime: Date) -> Bool { func pushAlreadyRegisered(name: String, location: String, startTime: Date, endTime: Date) -> Bool {
for push in pushes { for push in pushes {
if push.name == name && push.location == location && push.startTime == startTime && push.endTime == endTime { if push.name == name && push.location == location && push.startTime == startTime && push.endTime == endTime {
@ -69,6 +70,7 @@ class VisitingChefPushesModel {
return false return false
} }
/// Write out the registered push notifications.
private func save() { private func save() {
let encoder = JSONEncoder() let encoder = JSONEncoder()
if let data = try? encoder.encode(pushes) { if let data = try? encoder.encode(pushes) {
@ -76,6 +78,7 @@ class VisitingChefPushesModel {
} }
} }
/// Load registered push notifications.
private func load() { private func load() {
let decoder = JSONDecoder() let decoder = JSONDecoder()
if let data = UserDefaults.standard.data(forKey: key), if let data = UserDefaults.standard.data(forKey: key),

5
TigerDine/Info.plist Normal file
View File

@ -0,0 +1,5 @@
<?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/>
</plist>

View File

@ -11,6 +11,7 @@ struct AboutView: View {
@Environment(\.openURL) private var openURL @Environment(\.openURL) private var openURL
let appVersionString: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String let appVersionString: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
let buildNumber: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String let buildNumber: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
let copyrightString: String = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") as! String
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -25,6 +26,9 @@ struct AboutView: View {
.font(.subheadline) .font(.subheadline)
Text("Version \(appVersionString) (\(buildNumber))") Text("Version \(appVersionString) (\(buildNumber))")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(copyrightString)
.foregroundStyle(.secondary)
.font(.caption)
.padding(.bottom, 2) .padding(.bottom, 2)
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
Text("Dining locations, their descriptions, and their opening hours are sourced from the RIT student-run TigerCenter API. Building occupancy information is sourced from the official RIT maps API. Menu and nutritional information is sourced from the data provided to FD MealPlanner by RIT Dining through the FD MealPlanner API.") Text("Dining locations, their descriptions, and their opening hours are sourced from the RIT student-run TigerCenter API. Building occupancy information is sourced from the official RIT maps API. Menu and nutritional information is sourced from the data provided to FD MealPlanner by RIT Dining through the FD MealPlanner API.")

View File

@ -10,9 +10,10 @@ import SafariServices
struct DetailView: View { struct DetailView: View {
@State var locationId: Int @State var locationId: Int
@Environment(Favorites.self) var favorites
@Environment(DiningModel.self) var model @Environment(DiningModel.self) var model
@Environment(\.openURL) private var openURL @Environment(\.openURL) private var openURL
@State private var showingSafari: Bool = false @State private var showingSafari: Bool = false
@State private var occupancyLoading: Bool = true @State private var occupancyLoading: Bool = true
@State private var occupancyPercentage: Double = 0.0 @State private var occupancyPercentage: Double = 0.0
@ -78,112 +79,63 @@ struct DetailView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .center) { VStack(alignment: .leading) {
Text(location.name)
.font(.title)
.fontWeight(.bold)
Text(location.summary)
.font(.title2)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(location.name) switch location.open {
.font(.title) case .open:
.fontWeight(.bold) Text("Open")
Text(location.summary) .font(.title3)
.font(.title2) .foregroundStyle(.green)
.fontWeight(.semibold) case .closed:
.foregroundStyle(.secondary) Text("Closed")
VStack(alignment: .leading) { .font(.title3)
switch location.open { .foregroundStyle(.red)
case .open: case .openingSoon:
Text("Open") Text("Opening Soon")
.font(.title3) .font(.title3)
.foregroundStyle(.green) .foregroundStyle(.orange)
case .closed: case .closingSoon:
Text("Closed") Text("Closing Soon")
.font(.title3) .font(.title3)
.foregroundStyle(.red) .foregroundStyle(.orange)
case .openingSoon: }
Text("Opening Soon") if let times = location.diningTimes, !times.isEmpty {
.font(.title3) ForEach(times, id: \.self) { time in
.foregroundStyle(.orange) Text("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
case .closingSoon:
Text("Closing Soon")
.font(.title3)
.foregroundStyle(.orange)
}
if let times = location.diningTimes, !times.isEmpty {
ForEach(times, id: \.self) { time in
Text("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
.foregroundStyle(.secondary)
}
} else {
Text("Not Open Today")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} else {
Text("Not Open Today")
.foregroundStyle(.secondary)
} }
#if DEBUG
HStack(spacing: 0) {
ForEach(Range(1...5), id: \.self) { index in
if occupancyPercentage > (20 * Double(index)) {
Image(systemName: "person.fill")
} else {
Image(systemName: "person")
}
}
ProgressView()
.progressViewStyle(.circular)
.frame(width: 18, height: 18)
.opacity(occupancyLoading ? 1 : 0)
.task {
await getOccupancy()
}
}
.foregroundStyle(Color.accent.opacity(occupancyLoading ? 0.5 : 1.0))
.font(.title3)
#endif
} }
Spacer() #if DEBUG
VStack(alignment: .trailing) { HStack(spacing: 0) {
HStack(alignment: .center) { ForEach(Range(1...5), id: \.self) { index in
// Favorites toggle button. if occupancyPercentage > (20 * Double(index)) {
Button(action: { Image(systemName: "person.fill")
if favorites.contains(location) { } else {
favorites.remove(location) Image(systemName: "person")
} else {
favorites.add(location)
}
}) {
if favorites.contains(location) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.font(.title3)
} else {
Image(systemName: "star")
.foregroundStyle(.yellow)
.font(.title3)
}
}
// THIS FEATURE DISABLED AT RIT'S REQUEST FOR SECURITY REASONS.
// No hard feelings or anything though, I get it.
// // Open OnDemand. Unfortunately the locations use arbitrary IDs, so just open the main OnDemand page.
// Button(action: {
// openURL(URL(string: "https://ondemand.rit.edu")!)
// }) {
// Image(systemName: "cart")
// .font(.title3)
// }
// .disabled(location.open == .closed || location.open == .openingSoon)
// Open this location on the RIT map in embedded Safari.
Button(action: {
showingSafari = true
}) {
Image(systemName: "map")
.font(.title3)
} }
} }
if let fdmpIds = location.fdmpIds { ProgressView()
NavigationLink(destination: MenuView(accountId: fdmpIds.accountId, locationId: fdmpIds.locationId)) { .progressViewStyle(.circular)
Text("View Menu") .frame(width: 18, height: 18)
.opacity(occupancyLoading ? 1 : 0)
.task {
await getOccupancy()
} }
.padding(.top, 5)
}
Spacer()
} }
.foregroundStyle(Color.accent.opacity(occupancyLoading ? 0.5 : 1.0))
.font(.title3)
#endif
} }
.padding(.bottom, 12) .padding(.bottom, 12)
if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty { if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
@ -267,6 +219,50 @@ struct DetailView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
// Favorites toggle button.
Button(action: {
if model.favorites.contains(location) {
model.favorites.remove(location)
} else {
model.favorites.add(location)
}
}) {
if model.favorites.contains(location) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
//.font(.title3)
} else {
Image(systemName: "star")
.foregroundStyle(.yellow)
//.font(.title3)
}
}
// THIS FEATURE DISABLED AT RIT'S REQUEST FOR SECURITY REASONS.
// No hard feelings or anything though, I get it.
// // Open OnDemand. Unfortunately the locations use arbitrary IDs, so just open the main OnDemand page.
// Button(action: {
// openURL(URL(string: "https://ondemand.rit.edu")!)
// }) {
// Image(systemName: "cart")
// .font(.title3)
// }
// .disabled(location.open == .closed || location.open == .openingSoon)
// Open this location on the RIT map in embedded Safari.
Button(action: {
showingSafari = true
}) {
Image(systemName: "map")
//.font(.title3)
}
if let fdmpIds = location.fdmpIds {
NavigationLink(destination: MenuView(accountId: fdmpIds.accountId, locationId: fdmpIds.locationId)) {
Image(systemName: "menucard")
}
}
}
}
} }
.navigationTitle("Details") .navigationTitle("Details")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)

View File

@ -15,24 +15,28 @@ struct DonationView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack(alignment: .center, spacing: 12) { VStack(alignment: .center, spacing: 12) {
HStack { if #available(iOS 26.0, *) {
if #available(iOS 26.0, *) { Image(systemName: "heart.fill")
Image(systemName: "heart.fill") .resizable()
.foregroundStyle(.red) .scaledToFit()
.symbolEffect(.drawOn, isActive: symbolDrawn) .frame(width: 50, height: 50)
.onAppear { .foregroundStyle(.red)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { .symbolEffect(.drawOn, isActive: symbolDrawn)
symbolDrawn = false .onAppear {
} DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
symbolDrawn = false
} }
} else { }
Image(systemName: "heart.fill") } else {
.foregroundStyle(.red) Image(systemName: "heart.fill")
} .resizable()
Text("Donate") .scaledToFit()
.fontWeight(.bold) .frame(width: 50, height: 50)
.foregroundStyle(.red)
} }
.font(.title) Text("Donate")
.fontWeight(.bold)
.font(.title)
Text("The TigerDine app is free and open source software!") Text("The TigerDine app is free and open source software!")
.fontWeight(.bold) .fontWeight(.bold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -76,7 +80,7 @@ struct DonationView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Send Me Money Directly") Text("Send Me Money Directly")
.fontWeight(.bold) .fontWeight(.bold)
Text("I have nothing specific to say here!") Text("PayPal won't take a cut!")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
} }
@ -91,7 +95,7 @@ struct DonationView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity)
.toolbar { .toolbar {
Button(action: { Button(action: {
dismiss() dismiss()

View File

@ -66,19 +66,9 @@ struct FoodTruckView: View {
} else { } else {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { Text("Weekend Food Trucks")
Text("Weekend Food Trucks") .font(.title)
.font(.title) .fontWeight(.semibold)
.fontWeight(.semibold)
Spacer()
Button(action: {
showingSafari = true
}) {
Image(systemName: "network")
.foregroundStyle(.accent)
.font(.title3)
}
}
ForEach(foodTruckEvents, id: \.self) { event in ForEach(foodTruckEvents, id: \.self) { event in
Divider() Divider()
Text(visitingChefDateDisplay.string(from: event.date)) Text(visitingChefDateDisplay.string(from: event.date))
@ -92,11 +82,20 @@ struct FoodTruckView: View {
Spacer() Spacer()
} }
Spacer() Spacer()
Text("Food truck data is sourced directly from the RIT Events website, and may not be presented correctly. Use the button in the top right to access the RIT Events website directly to see the original source of the information.") Text("Food truck data is sourced directly from the RIT Events website, and may not be presented correctly. Use the globe button in the top right to access the RIT Events website directly to see the original source of the information.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
} }
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button(action: {
showingSafari = true
}) {
Image(systemName: "network")
}
}
}
.sheet(isPresented: $showingSafari) { .sheet(isPresented: $showingSafari) {
SafariView(url: URL(string: "https://www.rit.edu/events/weekend-food-trucks")!) SafariView(url: URL(string: "https://www.rit.edu/events/weekend-food-trucks")!)
} }

View File

@ -14,7 +14,8 @@ struct LocationList: View {
@Binding var openLocationsFirst: Bool @Binding var openLocationsFirst: Bool
@Binding var openLocationsOnly: Bool @Binding var openLocationsOnly: Bool
@Binding var searchText: String @Binding var searchText: String
@Environment(Favorites.self) var favorites
@Environment(DiningModel.self) var model
// The dining locations need to be sorted before being displayed. Favorites should always be shown first, followed by non-favorites. // The dining locations need to be sorted before being displayed. Favorites should always be shown first, followed by non-favorites.
// Afterwards, filters the sorted list based on any current search text and the "open locations only" filtering option. // Afterwards, filters the sorted list based on any current search text and the "open locations only" filtering option.
@ -29,8 +30,8 @@ struct LocationList: View {
return name return name
} }
newLocations.sort { firstLoc, secondLoc in newLocations.sort { firstLoc, secondLoc in
let firstLocIsFavorite = favorites.contains(firstLoc) let firstLocIsFavorite = model.favorites.contains(firstLoc)
let secondLocIsFavorite = favorites.contains(secondLoc) let secondLocIsFavorite = model.favorites.contains(secondLoc)
// Favorites get priority! // Favorites get priority!
if firstLocIsFavorite != secondLocIsFavorite { if firstLocIsFavorite != secondLocIsFavorite {
return firstLocIsFavorite && !secondLocIsFavorite return firstLocIsFavorite && !secondLocIsFavorite
@ -61,7 +62,7 @@ struct LocationList: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
Text(location.name) Text(location.name)
if favorites.contains(location) { if model.favorites.contains(location) {
Image(systemName: "star.fill") Image(systemName: "star.fill")
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
} }
@ -94,21 +95,21 @@ struct LocationList: View {
.swipeActions { .swipeActions {
Button(action: { Button(action: {
withAnimation { withAnimation {
if favorites.contains(location) { if model.favorites.contains(location) {
favorites.remove(location) model.favorites.remove(location)
} else { } else {
favorites.add(location) model.favorites.add(location)
} }
} }
}) { }) {
if favorites.contains(location) { if model.favorites.contains(location) {
Label("Unfavorite", systemImage: "star") Label("Unfavorite", systemImage: "star")
} else { } else {
Label("Favorite", systemImage: "star") Label("Favorite", systemImage: "star")
} }
} }
.tint(favorites.contains(location) ? .yellow : nil) .tint(model.favorites.contains(location) ? .yellow : nil)
} }
} }
} }

View File

@ -10,8 +10,11 @@ import SwiftUI
struct VisitingChefPush: View { struct VisitingChefPush: View {
@AppStorage("visitingChefPushEnabled") var pushEnabled: Bool = false @AppStorage("visitingChefPushEnabled") var pushEnabled: Bool = false
@AppStorage("notificationOffset") var notificationOffset: Int = 2 @AppStorage("notificationOffset") var notificationOffset: Int = 2
@Environment(DiningModel.self) var model @Environment(DiningModel.self) var model
@State private var pushAllowed: Bool = false @State private var pushAllowed: Bool = false
private let visitingChefs = [ private let visitingChefs = [
"California Rollin' Sushi", "California Rollin' Sushi",
"D'Mangu", "D'Mangu",