Added support for Gracie's multiple open times

This commit is contained in:
Campbell 2025-09-01 11:21:38 -04:00
parent f12f12bac8
commit 3f812495b0
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
2 changed files with 108 additions and 71 deletions

View File

@ -9,7 +9,7 @@ import SwiftUI
struct Location: Hashable { struct Location: Hashable {
let name: String let name: String
let todaysHours: String let todaysHours: [String]
let isOpen: openStatus let isOpen: openStatus
} }
@ -17,6 +17,7 @@ struct ContentView: View {
@State private var isLoading = true @State private var isLoading = true
@State private var rotationDegrees: Double = 0 @State private var rotationDegrees: Double = 0
@State private var diningLocations: [Location] = [] @State private var diningLocations: [Location] = []
@State private var lastRefreshed: Date?
private var animation: Animation { private var animation: Animation {
.linear .linear
@ -43,14 +44,15 @@ struct ContentView: View {
// Parse the open status and times to create the hours string. If either time is missing, assume it has no openings // Parse the open status and times to create the hours string. If either time is missing, assume it has no openings
// and use "Not Open Today". If there are times, then set those to be displayed. // and use "Not Open Today". If there are times, then set those to be displayed.
let todaysHours: String var todaysHours: [String] = []
if diningInfo.openTime == .none || diningInfo.closeTime == .none { if diningInfo.diningTimes == .none {
todaysHours = "Not Open Today" todaysHours = ["Not Open Today"]
} else { } else {
print("Open:", display.string(from: diningInfo.openTime!), for time in diningInfo.diningTimes! {
"Close:", display.string(from: diningInfo.closeTime!)) print("Open:", display.string(from: time.openTime),
"Close:", display.string(from: time.closeTime))
todaysHours = "\(display.string(from: diningInfo.openTime!)) - \(display.string(from: diningInfo.closeTime!))" todaysHours.append("\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime))")
}
} }
DispatchQueue.global().sync { DispatchQueue.global().sync {
newDiningLocations.append( newDiningLocations.append(
@ -60,6 +62,7 @@ struct ContentView: View {
isOpen: diningInfo.open isOpen: diningInfo.open
) )
) )
lastRefreshed = Date()
} }
} }
DispatchQueue.global().sync { DispatchQueue.global().sync {
@ -91,26 +94,40 @@ struct ContentView: View {
} }
.padding() .padding()
} else { } else {
Form { VStack() {
ForEach(diningLocations, id: \.self) { location in List {
VStack(alignment: .leading) { Section(content: {
Text(location.name) ForEach(diningLocations, id: \.self) { location in
Text(location.todaysHours) VStack(alignment: .leading) {
switch location.isOpen { Text(location.name)
case .open: ForEach(location.todaysHours, id: \.self) { hours in
Text("Open") Text(hours)
.foregroundStyle(.green) }
case .closed: switch location.isOpen {
Text("Closed") case .open:
.foregroundStyle(.red) Text("Open")
case .openingSoon: .foregroundStyle(.green)
Text("Opening Soon") case .closed:
.foregroundStyle(.orange) Text("Closed")
case .closingSoon: .foregroundStyle(.red)
Text("Closing Soon") case .openingSoon:
.foregroundStyle(.orange) Text("Opening Soon")
.foregroundStyle(.orange)
case .closingSoon:
Text("Closing Soon")
.foregroundStyle(.orange)
}
}
} }
} }, footer: {
if let lastRefreshed {
VStack(alignment: .center) {
Text("Last refreshed: \(lastRefreshed.formatted())")
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
})
} }
} }
.navigationTitle("RIT Dining") .navigationTitle("RIT Dining")

View File

@ -79,11 +79,15 @@ enum openStatus {
case closingSoon case closingSoon
} }
struct DiningTimes: Equatable {
let openTime: Date
let closeTime: Date
}
struct DiningInfo { struct DiningInfo {
let id: Int let id: Int
let name: String let name: String
let openTime: Date? let diningTimes: [DiningTimes]?
let closeTime: Date?
let open: openStatus let open: openStatus
} }
@ -95,13 +99,12 @@ func getLocationInfo(location: DiningLocation) -> DiningInfo {
return DiningInfo( return DiningInfo(
id: location.id, id: location.id,
name: location.name, name: location.name,
openTime: .none, diningTimes: .none,
closeTime: .none,
open: .closed) open: .closed)
} }
let openString: String var openStrings: [String] = []
let closeString: String var closeStrings: [String] = []
// Dining locations have a regular schedule, but then they also have exceptions listed for days like weekends or holidays. If there // Dining locations have a regular schedule, but then they also have exceptions listed for days like weekends or holidays. If there
// are exceptions, use those times for the day, otherwise we can just use the default times. // are exceptions, use those times for the day, otherwise we can just use the default times.
@ -111,67 +114,84 @@ func getLocationInfo(location: DiningLocation) -> DiningInfo {
return DiningInfo( return DiningInfo(
id: location.id, id: location.id,
name: location.name, name: location.name,
openTime: .none, diningTimes: .none,
closeTime: .none,
open: .closed) open: .closed)
} }
openString = location.events[0].exceptions![0].startTime openStrings.append(location.events[0].exceptions![0].startTime)
closeString = location.events[0].exceptions![0].endTime closeStrings.append(location.events[0].exceptions![0].endTime)
} else { } else {
openString = location.events[0].startTime for event in location.events {
closeString = location.events[0].endTime openStrings.append(event.startTime)
closeStrings.append(event.endTime)
}
} }
// I hate all of this date component nonsense. // I hate all of this date component nonsense.
let openParts = openString.split(separator: ":").map { Int($0) ?? 0 } var openDates: [Date] = []
let openTimeComponents = DateComponents(hour: openParts[0], minute: openParts[1], second: openParts[2]) var closeDates: [Date] = []
let closeParts = closeString.split(separator: ":").map { Int($0) ?? 0 }
let closeTimeComponents = DateComponents(hour: closeParts[0], minute: closeParts[1], second: closeParts[2])
let calendar = Calendar.current let calendar = Calendar.current
let now = Date() let now = Date()
let openDate = calendar.date( for i in 0..<openStrings.count {
bySettingHour: openTimeComponents.hour!, let openParts = openStrings[i].split(separator: ":").map { Int($0) ?? 0 }
minute: openTimeComponents.minute!, let openTimeComponents = DateComponents(hour: openParts[0], minute: openParts[1], second: openParts[2])
second: openTimeComponents.second!,
of: now)! let closeParts = closeStrings[i].split(separator: ":").map { Int($0) ?? 0 }
let closeTimeComponents = DateComponents(hour: closeParts[0], minute: closeParts[1], second: closeParts[2])
var closeDate = calendar.date(
bySettingHour: closeTimeComponents.hour!, openDates.append(calendar.date(
minute: closeTimeComponents.minute!, bySettingHour: openTimeComponents.hour!,
second: closeTimeComponents.second!, minute: openTimeComponents.minute!,
of: now)! second: openTimeComponents.second!,
of: now)!)
closeDates.append(calendar.date(
bySettingHour: closeTimeComponents.hour!,
minute: closeTimeComponents.minute!,
second: closeTimeComponents.second!,
of: now)!)
}
// If the closing time is less than or equal to the opening time, it's probably midnight and means either open until midnight // If the closing time is less than or equal to the opening time, it's probably midnight and means either open until midnight
// or open 24/7, in the case of Bytes. // or open 24/7, in the case of Bytes.
if closeDate <= openDate { for i in 0..<closeDates.count {
closeDate = calendar.date(byAdding: .day, value: 1, to: closeDate)! if closeDates[i] <= openDates[i] {
closeDates[i] = calendar.date(byAdding: .day, value: 1, to: closeDates[i])!
}
} }
// This can probably be done in a cleaner way but it's okay for now. If the location is open but the close date is within the next // This can probably be done in a cleaner way but it's okay for now. If the location is open but the close date is within the next
// 30 minutes, label it as closing soon, and do the opposite if it's closed but the open date is within the next 30 minutes. // 30 minutes, label it as closing soon, and do the opposite if it's closed but the open date is within the next 30 minutes.
let isOpen = (now >= openDate && now <= closeDate) var openStatus: openStatus = .closed
let openStatus: openStatus for i in 0..<openDates.count {
if isOpen { let isOpen = (now >= openDates[i] && now <= closeDates[i])
if closeDate < calendar.date(byAdding: .minute, value: 30, to: now)! { if isOpen {
openStatus = .closingSoon if closeDates[i] < calendar.date(byAdding: .minute, value: 30, to: now)! {
openStatus = .closingSoon
break
} else {
openStatus = .open
break
}
} else { } else {
openStatus = .open if openDates[i] < calendar.date(byAdding: .minute, value: 30, to: now)! {
} openStatus = .openingSoon
} else { break
if openDate < calendar.date(byAdding: .minute, value: 30, to: now)! { } else {
openStatus = .openingSoon openStatus = .closed
} else { }
openStatus = .closed
} }
} }
var diningTimes: [DiningTimes] = []
for i in 0..<openDates.count {
diningTimes.append(DiningTimes(openTime: openDates[i], closeTime: closeDates[i]))
}
return DiningInfo( return DiningInfo(
id: location.id, id: location.id,
name: location.name, name: location.name,
openTime: openDate, diningTimes: diningTimes,
closeTime: closeDate,
open: openStatus) open: openStatus)
} }