Various minor fixes and improvements

- Visiting chef screen now shows "No visiting chefs today" instead of nothing when there are, get this, no visiting chefs today
- 24-hour locations (Bytes) will no longer show as "Closing Soon" for the last 30 minutes of the day only to reset back to "Open" at midnight
- The app will now display a network error instead of loading indefinitely if an error occurs while fetching dining data
- Other minor spacing and layout changes
This commit is contained in:
Campbell 2025-09-14 23:36:11 -04:00
parent 24b71b7b3f
commit 9d16be646a
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
7 changed files with 119 additions and 28 deletions

View File

@ -255,7 +255,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -286,7 +286,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;

View File

@ -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()
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "RIT Dining Temp Logo.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@ -54,7 +54,8 @@ struct LocationList: View {
} }
struct ContentView: 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 rotationDegrees: Double = 0
@State private var diningLocations: [DiningLocation] = [] @State private var diningLocations: [DiningLocation] = []
@State private var lastRefreshed: Date? @State private var lastRefreshed: Date?
@ -82,11 +83,25 @@ struct ContentView: View {
} }
} }
DispatchQueue.global().sync { 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() lastRefreshed = Date()
isLoading = false isLoading = false
} }
case .failure(let error): print(error) case .failure(let error):
print(error)
loadFailed = true
} }
} }
} }
@ -106,6 +121,22 @@ struct ContentView: View {
NavigationStack() { NavigationStack() {
if isLoading { if isLoading {
VStack { VStack {
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")
}
.padding(.top, 10)
} else {
Image(systemName: "fork.knife.circle") Image(systemName: "fork.knife.circle")
.resizable() .resizable()
.frame(width: 75, height: 75) .frame(width: 75, height: 75)
@ -119,6 +150,7 @@ struct ContentView: View {
Text("Loading...") Text("Loading...")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
}
.padding() .padding()
} else { } else {
VStack() { VStack() {
@ -159,6 +191,11 @@ struct ContentView: View {
Toggle(isOn: $openLocationsOnly) { Toggle(isOn: $openLocationsOnly) {
Label("Hide Closed Locations", systemImage: "eye.slash") Label("Hide Closed Locations", systemImage: "eye.slash")
} }
NavigationLink(destination: AboutView()) {
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
Text("About")
}
} label: { } label: {
Image(systemName: "slider.horizontal.3") Image(systemName: "slider.horizontal.3")
} }

View File

@ -27,10 +27,13 @@ func getAllDiningInfo(date: String?, completionHandler: @escaping (Result<Dining
let request = URLRequest(url: url) let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in URLSession.shared.dataTask(with: request) { data, response, error in
guard case .none = error else { return } if let error = error {
completionHandler(.failure(error))
return
}
guard let data = data else { guard let data = data else {
print("Data error.") completionHandler(.failure(URLError(.badServerResponse)))
return return
} }
@ -82,7 +85,11 @@ func parseOpenStatus(openTime: Date, closeTime: Date) -> OpenStatus {
let now = Date() let now = Date()
var openStatus: OpenStatus = .closed var openStatus: OpenStatus = .closed
if now >= openTime && now <= closeTime { 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 openStatus = .closingSoon
} else { } else {
openStatus = .open openStatus = .open

View File

@ -13,7 +13,7 @@ struct IdentifiableURL: Identifiable {
} }
struct VisitingChefs: View { struct VisitingChefs: View {
@State private var diningLocations: [DiningLocation] = [] @State private var locationsWithChefs: [DiningLocation] = []
@State private var isLoading: Bool = true @State private var isLoading: Bool = true
@State private var rotationDegrees: Double = 0 @State private var rotationDegrees: Double = 0
@State private var daySwitcherRotation: Double = 0 @State private var daySwitcherRotation: Double = 0
@ -46,11 +46,14 @@ struct VisitingChefs: View {
let diningInfo = parseLocationInfo(location: locations.locations[i]) let diningInfo = parseLocationInfo(location: locations.locations[i])
print(diningInfo.name) print(diningInfo.name)
DispatchQueue.global().sync { DispatchQueue.global().sync {
// Only save the locations that actually have visiting chefs to avoid extra iterations later.
if let visitingChefs = diningInfo.visitingChefs, !visitingChefs.isEmpty {
newDiningLocations.append(diningInfo) newDiningLocations.append(diningInfo)
} }
} }
}
DispatchQueue.global().sync { DispatchQueue.global().sync {
diningLocations = newDiningLocations locationsWithChefs = newDiningLocations
isLoading = false isLoading = false
} }
case .failure(let error): print(error) case .failure(let error): print(error)
@ -64,12 +67,12 @@ struct VisitingChefs: View {
let dateString: String let dateString: String
if !isTomorrow { if !isTomorrow {
dateString = getAPIFriendlyDateString(date: Date()) 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 { } else {
let calendar = Calendar.current let calendar = Calendar.current
let tomorrow = calendar.date(byAdding: .day, value: 1, to: Date())! let tomorrow = calendar.date(byAdding: .day, value: 1, to: Date())!
dateString = getAPIFriendlyDateString(date: tomorrow) 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) getDiningDataForDate(date: dateString)
} }
@ -123,7 +126,12 @@ struct VisitingChefs: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 25) .padding(.vertical, 25)
} else { } 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 { if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Divider() Divider()
@ -174,15 +182,13 @@ struct VisitingChefs: View {
Text(chef.description) Text(chef.description)
} }
} }
.padding(.bottom, 15) .padding(.bottom, 20)
} }
} }
} }
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
} }
.navigationTitle("Visiting Chefs")
.navigationBarTitleDisplayMode(.inline)
.sheet(item: $safariUrl) { url in .sheet(item: $safariUrl) { url in
SafariView(url: url.url) SafariView(url: url.url)
} }