diff --git a/RIT Dining.xcodeproj/project.pbxproj b/RIT Dining.xcodeproj/project.pbxproj index f866f12..2d9fd7a 100644 --- a/RIT Dining.xcodeproj/project.pbxproj +++ b/RIT Dining.xcodeproj/project.pbxproj @@ -255,7 +255,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 5GF7GKNTK4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -286,7 +286,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 5GF7GKNTK4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/RIT Dining/AboutView.swift b/RIT Dining/AboutView.swift new file mode 100644 index 0000000..3750ffd --- /dev/null +++ b/RIT Dining/AboutView.swift @@ -0,0 +1,29 @@ +// +// AboutView.swift +// RIT Dining +// +// Created by Campbell on 9/12/25. +// + +import SwiftUI + +struct AboutView: View { + var body: some View { + VStack { + Image("Icon") + .resizable() + .frame(width: 128, height: 128) + .clipShape(RoundedRectangle(cornerRadius: 20)) + Text("RIT Dining App") + .font(.title) + Text("because the RIT dining website is slow!") + } + .padding() + .navigationTitle("About") + .navigationBarTitleDisplayMode(.inline) + } +} + +#Preview { + AboutView() +} diff --git a/RIT Dining/Assets.xcassets/Icon.imageset/Contents.json b/RIT Dining/Assets.xcassets/Icon.imageset/Contents.json new file mode 100644 index 0000000..07ef905 --- /dev/null +++ b/RIT Dining/Assets.xcassets/Icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "RIT Dining Temp Logo.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RIT Dining/Assets.xcassets/Icon.imageset/RIT Dining Temp Logo.png b/RIT Dining/Assets.xcassets/Icon.imageset/RIT Dining Temp Logo.png new file mode 100644 index 0000000..b5eb116 Binary files /dev/null and b/RIT Dining/Assets.xcassets/Icon.imageset/RIT Dining Temp Logo.png differ diff --git a/RIT Dining/ContentView.swift b/RIT Dining/ContentView.swift index 842c94d..e32a854 100644 --- a/RIT Dining/ContentView.swift +++ b/RIT Dining/ContentView.swift @@ -54,7 +54,8 @@ struct LocationList: View { } struct ContentView: View { - @State private var isLoading = true + @State private var isLoading: Bool = true + @State private var loadFailed: Bool = false @State private var rotationDegrees: Double = 0 @State private var diningLocations: [DiningLocation] = [] @State private var lastRefreshed: Date? @@ -82,11 +83,25 @@ struct ContentView: View { } } DispatchQueue.global().sync { - diningLocations = newDiningLocations.sorted { $0.name < $1.name } + // Need to sort the locations alphabetically because they get returned in a completely arbitrary order. Also + // need to do so while ignoring the word "the" because a bunch of locations have it and it's not helpful to put + // those all down in "T". + diningLocations = newDiningLocations.sorted { firstLoc, secondLoc in + func removeThe(_ name: String) -> String { + let lowercased = name.lowercased() + if lowercased.hasPrefix("the ") { + return String(name.dropFirst(4)) + } + return name + } + return removeThe(firstLoc.name).localizedCaseInsensitiveCompare(removeThe(secondLoc.name)) == .orderedAscending + } lastRefreshed = Date() isLoading = false } - case .failure(let error): print(error) + case .failure(let error): + print(error) + loadFailed = true } } } @@ -106,18 +121,35 @@ struct ContentView: View { NavigationStack() { if isLoading { VStack { - Image(systemName: "fork.knife.circle") - .resizable() - .frame(width: 75, height: 75) - .foregroundStyle(.accent) - .rotationEffect(.degrees(rotationDegrees)) - .onAppear { - withAnimation(animation) { - rotationDegrees = 360.0 - } + if loadFailed { + Image(systemName: "wifi.exclamationmark.circle") + .resizable() + .frame(width: 75, height: 75) + .foregroundStyle(.accent) + Text("An error occurred while fetching dining data. Please check your network connection and try again.") + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + Button(action: { + loadFailed = false + getDiningData() + }) { + Label("Refresh", systemImage: "arrow.clockwise") } - Text("Loading...") - .foregroundStyle(.secondary) + .padding(.top, 10) + } else { + Image(systemName: "fork.knife.circle") + .resizable() + .frame(width: 75, height: 75) + .foregroundStyle(.accent) + .rotationEffect(.degrees(rotationDegrees)) + .onAppear { + withAnimation(animation) { + rotationDegrees = 360.0 + } + } + Text("Loading...") + .foregroundStyle(.secondary) + } } .padding() } else { @@ -159,6 +191,11 @@ struct ContentView: View { Toggle(isOn: $openLocationsOnly) { Label("Hide Closed Locations", systemImage: "eye.slash") } + NavigationLink(destination: AboutView()) { + Image(systemName: "info.circle") + .foregroundColor(.accentColor) + Text("About") + } } label: { Image(systemName: "slider.horizontal.3") } diff --git a/RIT Dining/FetchData.swift b/RIT Dining/FetchData.swift index 30bb2c1..fa205c2 100644 --- a/RIT Dining/FetchData.swift +++ b/RIT Dining/FetchData.swift @@ -27,10 +27,13 @@ func getAllDiningInfo(date: String?, completionHandler: @escaping (Result OpenStatus { let now = Date() var openStatus: OpenStatus = .closed if now >= openTime && now <= closeTime { - if closeTime < calendar.date(byAdding: .minute, value: 30, to: now)! { + // This is basically just for Bytes, it checks the case where the open and close times are exactly 24 hours apart, which is + // only true for 24-hour locations. + if closeTime == calendar.date(byAdding: .day, value: 1, to: openTime)! { + openStatus = .open + } else if closeTime < calendar.date(byAdding: .minute, value: 30, to: now)! { openStatus = .closingSoon } else { openStatus = .open diff --git a/RIT Dining/VisitingChefs.swift b/RIT Dining/VisitingChefs.swift index 70a0bfe..a49cd3a 100644 --- a/RIT Dining/VisitingChefs.swift +++ b/RIT Dining/VisitingChefs.swift @@ -13,7 +13,7 @@ struct IdentifiableURL: Identifiable { } struct VisitingChefs: View { - @State private var diningLocations: [DiningLocation] = [] + @State private var locationsWithChefs: [DiningLocation] = [] @State private var isLoading: Bool = true @State private var rotationDegrees: Double = 0 @State private var daySwitcherRotation: Double = 0 @@ -46,11 +46,14 @@ struct VisitingChefs: View { let diningInfo = parseLocationInfo(location: locations.locations[i]) print(diningInfo.name) DispatchQueue.global().sync { - newDiningLocations.append(diningInfo) + // Only save the locations that actually have visiting chefs to avoid extra iterations later. + if let visitingChefs = diningInfo.visitingChefs, !visitingChefs.isEmpty { + newDiningLocations.append(diningInfo) + } } } DispatchQueue.global().sync { - diningLocations = newDiningLocations + locationsWithChefs = newDiningLocations isLoading = false } case .failure(let error): print(error) @@ -64,12 +67,12 @@ struct VisitingChefs: View { let dateString: String if !isTomorrow { dateString = getAPIFriendlyDateString(date: Date()) - print("default really really really long string to make this line more obvious for debugging: \(dateString)") + print("fetching visiting chefs for date \(dateString) (today)") } else { let calendar = Calendar.current let tomorrow = calendar.date(byAdding: .day, value: 1, to: Date())! dateString = getAPIFriendlyDateString(date: tomorrow) - print("really really really long string to make this line more obvious for debugging: \(dateString)") + print("fetching visiting chefs for date \(dateString) (tomorrow)") } getDiningDataForDate(date: dateString) } @@ -123,7 +126,12 @@ struct VisitingChefs: View { .frame(maxWidth: .infinity) .padding(.vertical, 25) } else { - ForEach(diningLocations, id: \.self) { location in + if locationsWithChefs.isEmpty { + Text("No visiting chefs today") + .font(.title2) + .foregroundStyle(.secondary) + } + ForEach(locationsWithChefs, id: \.self) { location in if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty { VStack(alignment: .leading) { Divider() @@ -174,15 +182,13 @@ struct VisitingChefs: View { Text(chef.description) } } - .padding(.bottom, 15) + .padding(.bottom, 20) } } } } .padding(.horizontal, 8) } - .navigationTitle("Visiting Chefs") - .navigationBarTitleDisplayMode(.inline) .sheet(item: $safariUrl) { url in SafariView(url: url.url) }