mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-01-17 12:05:57 -05:00
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:
parent
6794c66c37
commit
d3d060b5e2
@ -14,9 +14,22 @@
|
||||
376AE05B2E6495EB00AB698B /* TigerDine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TigerDine.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* 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 */
|
||||
376AE05D2E6495EB00AB698B /* TigerDine */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
37C76ABF2F0F3067009E7074 /* Exceptions for "TigerDine" folder in "TigerDine" target */,
|
||||
);
|
||||
path = TigerDine;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -269,9 +282,11 @@
|
||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = TigerDine/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = TigerDine;
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Campbell Bagley";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -282,7 +297,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -304,9 +319,11 @@
|
||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = TigerDine/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = TigerDine;
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Campbell Bagley";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -317,7 +334,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -11,14 +11,12 @@ struct ContentView: View {
|
||||
// Save sort/filter options in AppStorage so that they actually get saved.
|
||||
@AppStorage("openLocationsOnly") var openLocationsOnly: 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 isLoading: Bool = true
|
||||
@State private var loadFailed: Bool = false
|
||||
@State private var showingDonationSheet: Bool = false
|
||||
@State private var rotationDegrees: Double = 0
|
||||
@State private var diningLocations: [DiningLocation] = []
|
||||
@State private var searchText: String = ""
|
||||
|
||||
private var animation: Animation {
|
||||
@ -176,8 +174,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(favorites)
|
||||
.environment(notifyingChefs)
|
||||
.environment(model)
|
||||
.task {
|
||||
await getDiningData()
|
||||
|
||||
@ -12,13 +12,13 @@ class DiningModel {
|
||||
var locationsByDay = [[DiningLocation]]()
|
||||
var daysRepresented = [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
|
||||
// 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).
|
||||
// External models that should be nested under this one.
|
||||
var favorites = Favorites()
|
||||
var notifyingChefs = NotifyingChefs()
|
||||
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 {
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
@ -47,8 +47,7 @@ class DiningModel {
|
||||
lastRefreshed = Date()
|
||||
}
|
||||
|
||||
// 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.
|
||||
/// 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.
|
||||
func updateOpenStatuses() {
|
||||
locationsByDay = locationsByDay.map { day in
|
||||
day.map { location in
|
||||
@ -59,6 +58,7 @@ class DiningModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules and saves push notifications for all enabled visiting chefs.
|
||||
func scheduleAllPushes() async {
|
||||
for day in locationsByDay {
|
||||
for location in day {
|
||||
@ -80,7 +80,7 @@ class DiningModel {
|
||||
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 {
|
||||
let now = Date()
|
||||
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 {
|
||||
let uuids = visitingChefPushes.pushes.map(\.uuid)
|
||||
await cancelVisitingChefNotifs(uuids: uuids)
|
||||
visitingChefPushes.pushes.removeAll()
|
||||
}
|
||||
|
||||
/// Schedules and saves push notifications for a specific visiting chef.
|
||||
func schedulePushesForChef(_ chefName: String) async {
|
||||
for day in locationsByDay {
|
||||
for location in day {
|
||||
|
||||
@ -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) {
|
||||
var uuids: [String] = []
|
||||
for push in pushes {
|
||||
@ -60,6 +60,7 @@ class VisitingChefPushesModel {
|
||||
save()
|
||||
}
|
||||
|
||||
/// Checks if a push notification meeting the specified criteria is already scheduled.
|
||||
func pushAlreadyRegisered(name: String, location: String, startTime: Date, endTime: Date) -> Bool {
|
||||
for push in pushes {
|
||||
if push.name == name && push.location == location && push.startTime == startTime && push.endTime == endTime {
|
||||
@ -69,6 +70,7 @@ class VisitingChefPushesModel {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Write out the registered push notifications.
|
||||
private func save() {
|
||||
let encoder = JSONEncoder()
|
||||
if let data = try? encoder.encode(pushes) {
|
||||
@ -76,6 +78,7 @@ class VisitingChefPushesModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load registered push notifications.
|
||||
private func load() {
|
||||
let decoder = JSONDecoder()
|
||||
if let data = UserDefaults.standard.data(forKey: key),
|
||||
|
||||
5
TigerDine/Info.plist
Normal file
5
TigerDine/Info.plist
Normal 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>
|
||||
@ -11,6 +11,7 @@ struct AboutView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
let appVersionString: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") 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 {
|
||||
VStack(alignment: .leading) {
|
||||
@ -25,6 +26,9 @@ struct AboutView: View {
|
||||
.font(.subheadline)
|
||||
Text("Version \(appVersionString) (\(buildNumber))")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(copyrightString)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
.padding(.bottom, 2)
|
||||
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.")
|
||||
|
||||
@ -10,9 +10,10 @@ import SafariServices
|
||||
|
||||
struct DetailView: View {
|
||||
@State var locationId: Int
|
||||
@Environment(Favorites.self) var favorites
|
||||
|
||||
@Environment(DiningModel.self) var model
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
@State private var showingSafari: Bool = false
|
||||
@State private var occupancyLoading: Bool = true
|
||||
@State private var occupancyPercentage: Double = 0.0
|
||||
@ -78,112 +79,63 @@ struct DetailView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
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) {
|
||||
Text(location.name)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
Text(location.summary)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.secondary)
|
||||
VStack(alignment: .leading) {
|
||||
switch location.open {
|
||||
case .open:
|
||||
Text("Open")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.green)
|
||||
case .closed:
|
||||
Text("Closed")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.red)
|
||||
case .openingSoon:
|
||||
Text("Opening Soon")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.orange)
|
||||
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")
|
||||
switch location.open {
|
||||
case .open:
|
||||
Text("Open")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.green)
|
||||
case .closed:
|
||||
Text("Closed")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.red)
|
||||
case .openingSoon:
|
||||
Text("Opening Soon")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.orange)
|
||||
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)
|
||||
}
|
||||
#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()
|
||||
VStack(alignment: .trailing) {
|
||||
HStack(alignment: .center) {
|
||||
// Favorites toggle button.
|
||||
Button(action: {
|
||||
if favorites.contains(location) {
|
||||
favorites.remove(location)
|
||||
} 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 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")
|
||||
}
|
||||
}
|
||||
if let fdmpIds = location.fdmpIds {
|
||||
NavigationLink(destination: MenuView(accountId: fdmpIds.accountId, locationId: fdmpIds.locationId)) {
|
||||
Text("View Menu")
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.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)
|
||||
if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
|
||||
@ -267,6 +219,50 @@ struct DetailView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.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")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
@ -15,24 +15,28 @@ struct DonationView: View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(alignment: .center, spacing: 12) {
|
||||
HStack {
|
||||
if #available(iOS 26.0, *) {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundStyle(.red)
|
||||
.symbolEffect(.drawOn, isActive: symbolDrawn)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
|
||||
symbolDrawn = false
|
||||
}
|
||||
if #available(iOS 26.0, *) {
|
||||
Image(systemName: "heart.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundStyle(.red)
|
||||
.symbolEffect(.drawOn, isActive: symbolDrawn)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
|
||||
symbolDrawn = false
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
Text("Donate")
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "heart.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.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!")
|
||||
.fontWeight(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -76,7 +80,7 @@ struct DonationView: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Send Me Money Directly")
|
||||
.fontWeight(.bold)
|
||||
Text("I have nothing specific to say here!")
|
||||
Text("PayPal won't take a cut!")
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
@ -91,7 +95,7 @@ struct DonationView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.frame(maxWidth: .infinity)
|
||||
.toolbar {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
|
||||
@ -66,19 +66,9 @@ struct FoodTruckView: View {
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Weekend Food Trucks")
|
||||
.font(.title)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
showingSafari = true
|
||||
}) {
|
||||
Image(systemName: "network")
|
||||
.foregroundStyle(.accent)
|
||||
.font(.title3)
|
||||
}
|
||||
}
|
||||
Text("Weekend Food Trucks")
|
||||
.font(.title)
|
||||
.fontWeight(.semibold)
|
||||
ForEach(foodTruckEvents, id: \.self) { event in
|
||||
Divider()
|
||||
Text(visitingChefDateDisplay.string(from: event.date))
|
||||
@ -92,11 +82,20 @@ struct FoodTruckView: View {
|
||||
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)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
Button(action: {
|
||||
showingSafari = true
|
||||
}) {
|
||||
Image(systemName: "network")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingSafari) {
|
||||
SafariView(url: URL(string: "https://www.rit.edu/events/weekend-food-trucks")!)
|
||||
}
|
||||
|
||||
@ -14,7 +14,8 @@ struct LocationList: View {
|
||||
@Binding var openLocationsFirst: Bool
|
||||
@Binding var openLocationsOnly: Bool
|
||||
@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.
|
||||
// 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
|
||||
}
|
||||
newLocations.sort { firstLoc, secondLoc in
|
||||
let firstLocIsFavorite = favorites.contains(firstLoc)
|
||||
let secondLocIsFavorite = favorites.contains(secondLoc)
|
||||
let firstLocIsFavorite = model.favorites.contains(firstLoc)
|
||||
let secondLocIsFavorite = model.favorites.contains(secondLoc)
|
||||
// Favorites get priority!
|
||||
if firstLocIsFavorite != secondLocIsFavorite {
|
||||
return firstLocIsFavorite && !secondLocIsFavorite
|
||||
@ -61,7 +62,7 @@ struct LocationList: View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(location.name)
|
||||
if favorites.contains(location) {
|
||||
if model.favorites.contains(location) {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
@ -94,21 +95,21 @@ struct LocationList: View {
|
||||
.swipeActions {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
if favorites.contains(location) {
|
||||
favorites.remove(location)
|
||||
if model.favorites.contains(location) {
|
||||
model.favorites.remove(location)
|
||||
} else {
|
||||
favorites.add(location)
|
||||
model.favorites.add(location)
|
||||
}
|
||||
}
|
||||
|
||||
}) {
|
||||
if favorites.contains(location) {
|
||||
if model.favorites.contains(location) {
|
||||
Label("Unfavorite", systemImage: "star")
|
||||
} else {
|
||||
Label("Favorite", systemImage: "star")
|
||||
}
|
||||
}
|
||||
.tint(favorites.contains(location) ? .yellow : nil)
|
||||
.tint(model.favorites.contains(location) ? .yellow : nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,11 @@ import SwiftUI
|
||||
struct VisitingChefPush: View {
|
||||
@AppStorage("visitingChefPushEnabled") var pushEnabled: Bool = false
|
||||
@AppStorage("notificationOffset") var notificationOffset: Int = 2
|
||||
|
||||
@Environment(DiningModel.self) var model
|
||||
|
||||
@State private var pushAllowed: Bool = false
|
||||
|
||||
private let visitingChefs = [
|
||||
"California Rollin' Sushi",
|
||||
"D'Mangu",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user