Fixed multi opening period locations in widgets

Widgets for locations with multiple opening periods will still only display the first time span, but the bar will now show multiple filled in sections and the opening status label will correctly represent both periods.
This commit is contained in:
Campbell 2026-01-14 00:16:40 -05:00
parent f78de2f6ff
commit 71c37749e3
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
7 changed files with 105 additions and 87 deletions

View File

@ -1,5 +1,5 @@
// //
// SharedComponents.swift // SharedUtils.swift
// TigerDine // TigerDine
// //
// Created by Campbell on 9/8/25. // Created by Campbell on 9/8/25.

View File

@ -7,6 +7,7 @@
import Foundation import Foundation
/// Gets the current open status of a location based on the open time and close time.
func parseOpenStatus(openTime: Date, closeTime: Date) -> OpenStatus { func parseOpenStatus(openTime: Date, closeTime: Date) -> OpenStatus {
// This can probably be done a little cleaner but it's okay for now. If the location is open but the close date is within the next // This can probably be done a little cleaner 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.
@ -31,6 +32,25 @@ func parseOpenStatus(openTime: Date, closeTime: Date) -> OpenStatus {
return openStatus return openStatus
} }
/// Gets the current open status of a location with multiple opening periods based on all of its open and close times.
func parseMultiOpenStatus(diningTimes: [DiningTimes]?) -> OpenStatus {
var openStatus: OpenStatus = .closed
if let diningTimes = diningTimes, !diningTimes.isEmpty {
for i in diningTimes.indices {
openStatus = parseOpenStatus(openTime: diningTimes[i].openTime, closeTime: diningTimes[i].closeTime)
// If the first event pass came back closed, loop again in case a later event has a different status. This is mostly to
// accurately catch Gracie's/Brick City Cafe's multiple open periods each day.
if openStatus != .closed {
break
}
}
return openStatus
} else {
return .closed
}
}
/// Parses the JSON responses from the TigerCenter API into the format used throughout TigerDine.
func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> DiningLocation { func parseLocationInfo(location: DiningLocationParser, forDate: Date?) -> DiningLocation {
print("beginning parse for \(location.name)") print("beginning parse for \(location.name)")
@ -277,20 +297,8 @@ extension DiningLocation {
// Updates the open status of a location and of its visiting chefs, so that the labels in the UI update automatically as // Updates the open status of a location and of its visiting chefs, so that the labels in the UI update automatically as
// time progresses and locations open/close/etc. // time progresses and locations open/close/etc.
mutating func updateOpenStatus() { mutating func updateOpenStatus() {
var openStatus: OpenStatus = .closed // Gets the open status with the multi opening period compatible function.
if let diningTimes = diningTimes, !diningTimes.isEmpty { self.open = parseMultiOpenStatus(diningTimes: diningTimes)
for i in diningTimes.indices {
openStatus = parseOpenStatus(openTime: diningTimes[i].openTime, closeTime: diningTimes[i].closeTime)
// If the first event pass came back closed, loop again in case a later event has a different status. This is mostly to
// accurately catch Gracie's multiple open periods each day.
if openStatus != .closed {
break
}
}
self.open = openStatus
} else {
self.open = .closed
}
if let visitingChefs = visitingChefs, !visitingChefs.isEmpty { if let visitingChefs = visitingChefs, !visitingChefs.isEmpty {
let now = Date() let now = Date()
for i in visitingChefs.indices { for i in visitingChefs.indices {

View File

@ -55,7 +55,10 @@
374CDA742F10B24F00D8C50A /* Exceptions for "Shared" folder in "TigerDineWidgets" target */ = { 374CDA742F10B24F00D8C50A /* Exceptions for "Shared" folder in "TigerDineWidgets" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
SharedComponents.swift, Components/SharedUtils.swift,
Components/TigerCenterParsers.swift,
Data/Static/FDMPMealPeriods.swift,
Data/Static/TCtoFDMPMap.swift,
Types/TigerCenterTypes.swift, Types/TigerCenterTypes.swift,
); );
target = 374CDA572F10A19500D8C50A /* TigerDineWidgets */; target = 374CDA572F10A19500D8C50A /* TigerDineWidgets */;
@ -289,7 +292,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TigerDineWidgets/Info.plist; INFOPLIST_FILE = TigerDineWidgets/Info.plist;
@ -322,7 +325,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TigerDineWidgets/Info.plist; INFOPLIST_FILE = TigerDineWidgets/Info.plist;
@ -478,7 +481,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -515,7 +518,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;

View File

@ -8,28 +8,21 @@
import SwiftUI import SwiftUI
struct OpeningHoursGauge: View { struct OpeningHoursGauge: View {
let openTime: Date? let diningTimes: [DiningTimes]?
let closeTime: Date?
let now: Date let now: Date
private let dayDuration: TimeInterval = 86_400 private let dayDuration: TimeInterval = 86_400
private var barFillColor: Color { private var barFillColor: Color {
let calendar = Calendar.current if let diningTimes = diningTimes {
let openStatus = parseMultiOpenStatus(diningTimes: diningTimes)
if let openTime = openTime, let closeTime = closeTime { switch openStatus {
if now >= openTime && now <= closeTime { case .open:
if closeTime == calendar.date(byAdding: .day, value: 1, to: openTime)! {
return Color.green return Color.green
} else if closeTime < calendar.date(byAdding: .minute, value: 30, to: now)! { case .closed:
return Color.orange
} else {
return Color.green
}
} else if openTime <= calendar.date(byAdding: .minute, value: 30, to: now)! && closeTime > now {
return Color.orange
} else {
return Color.red return Color.red
case .openingSoon, .closingSoon:
return Color.orange
} }
} else { } else {
return Color.red return Color.red
@ -53,11 +46,13 @@ struct OpeningHoursGauge: View {
// We can skip drawing this entire capsule if the location is never open, since there would be no opening period // We can skip drawing this entire capsule if the location is never open, since there would be no opening period
// to draw. // to draw.
if let openTime = openTime, let closeTime = closeTime { if let diningTimes = diningTimes {
let openX = position(for: openTime, start: startOfToday, width: width) // Need to iterate here to account for locations that have multiple opening periods (Gracie's/Brick City Cafe).
ForEach(diningTimes, id: \.self) { diningTime in
let openX = position(for: diningTime.openTime, start: startOfToday, width: width)
let closeX = position( let closeX = position(
for: closeTime, for: diningTime.closeTime,
start: closeTime < openTime ? startOfTomorrow : startOfToday, start: diningTime.closeTime < diningTime.openTime ? startOfTomorrow : startOfToday,
width: width width: width
) )
@ -72,6 +67,7 @@ struct OpeningHoursGauge: View {
.frame(width: max(0, closeX - openX), height: barHeight) .frame(width: max(0, closeX - openX), height: barHeight)
.offset(x: openX) .offset(x: openX)
} }
}
Circle() Circle()
.fill(Color.white) .fill(Color.white)

View File

@ -18,8 +18,9 @@ struct Provider: AppIntentTimelineProvider {
return OpenEntry( return OpenEntry(
date: Date(), date: Date(),
name: "Select a Location", name: "Select a Location",
openTime: startOfToday, diningTimes: [
closeTime: startOfTomorrow DiningTimes(openTime: startOfToday, closeTime: startOfTomorrow)
]
) )
} }
@ -44,16 +45,15 @@ struct Provider: AppIntentTimelineProvider {
let updateDates = buildUpdateSchedule( let updateDates = buildUpdateSchedule(
now: Date(), now: Date(),
open: baseEntry.openTime, open: baseEntry.diningTimes?.first!.openTime,
close: baseEntry.closeTime close: baseEntry.diningTimes?.first!.closeTime
) )
let entries = updateDates.map { let entries = updateDates.map {
OpenEntry( OpenEntry(
date: $0, date: $0,
name: baseEntry.name, name: baseEntry.name,
openTime: baseEntry.openTime, diningTimes: baseEntry.diningTimes
closeTime: baseEntry.closeTime
) )
} }
@ -79,8 +79,7 @@ struct Provider: AppIntentTimelineProvider {
return OpenEntry( return OpenEntry(
date: Date(), date: Date(),
name: location.name, name: location.name,
openTime: location.diningTimes?.first?.openTime, diningTimes: location.diningTimes
closeTime: location.diningTimes?.first?.closeTime
) )
} }
@ -117,8 +116,7 @@ struct Provider: AppIntentTimelineProvider {
struct OpenEntry: TimelineEntry { struct OpenEntry: TimelineEntry {
let date: Date let date: Date
let name: String let name: String
let openTime: Date? let diningTimes: [DiningTimes]?
let closeTime: Date?
} }
struct OpenWidgetEntryView : View { struct OpenWidgetEntryView : View {
@ -133,32 +131,28 @@ struct OpenWidgetEntryView : View {
.fontWeight(.bold) .fontWeight(.bold)
// Should maybe try to unify this with the almost-identical UI code in DetailView. // Should maybe try to unify this with the almost-identical UI code in DetailView.
if let openTime = entry.openTime, let closeTime = entry.closeTime { if let diningTimes = entry.diningTimes {
if entry.date >= openTime && entry.date <= closeTime { let openStatus = parseMultiOpenStatus(diningTimes: diningTimes)
if closeTime == calendar.date(byAdding: .day, value: 1, to: openTime)! { switch openStatus {
case .open:
Text("Open") Text("Open")
.font(.title3) .font(.title3)
.foregroundStyle(.green) .foregroundStyle(.green)
} else if closeTime < calendar.date(byAdding: .minute, value: 30, to: entry.date)! { case .closed:
Text("Closing Soon")
.font(.title3)
.foregroundStyle(.orange)
} else {
Text("Open")
.font(.title3)
.foregroundStyle(.green)
}
} else if openTime <= calendar.date(byAdding: .minute, value: 30, to: entry.date)! && closeTime > entry.date {
Text("Opening Soon")
.font(.title3)
.foregroundStyle(.orange)
} else {
Text("Closed") Text("Closed")
.font(.title3) .font(.title3)
.foregroundStyle(.red) .foregroundStyle(.red)
case .openingSoon:
Text("Opening Soon")
.font(.title3)
.foregroundStyle(.orange)
case .closingSoon:
Text("Closing Soon")
.font(.title3)
.foregroundStyle(.orange)
} }
Text("\(dateDisplay.string(from: openTime)) - \(dateDisplay.string(from: closeTime))") Text("\(dateDisplay.string(from: diningTimes[0].openTime)) - \(dateDisplay.string(from: diningTimes[0].closeTime))")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
Text("Closed") Text("Closed")
@ -172,8 +166,7 @@ struct OpenWidgetEntryView : View {
Spacer() Spacer()
OpeningHoursGauge( OpeningHoursGauge(
openTime: entry.openTime, diningTimes: entry.diningTimes,
closeTime: entry.closeTime,
now: entry.date now: entry.date
) )
} }
@ -201,6 +194,24 @@ struct HoursWidget: Widget {
#Preview(as: .systemSmall) { #Preview(as: .systemSmall) {
HoursWidget() HoursWidget()
} timeline: { } timeline: {
OpenEntry(date: .now, name: "Beanz", openTime: Date(timeIntervalSince1970: 1767963600), closeTime: Date(timeIntervalSince1970: 1767988800)) OpenEntry(
OpenEntry(date: Date(timeIntervalSince1970: 1767978000), name: "Beanz", openTime: Date(timeIntervalSince1970: 1767963600), closeTime: Date(timeIntervalSince1970: 1767988800)) date: .now,
name: "Beanz",
diningTimes: [
DiningTimes(
openTime: Date(timeIntervalSince1970: 1767963600),
closeTime: Date(timeIntervalSince1970: 1767988800)
)
]
)
OpenEntry(
date: Date(timeIntervalSince1970: 1767978000),
name: "Beanz",
diningTimes: [
DiningTimes(
openTime: Date(timeIntervalSince1970: 1767963600),
closeTime: Date(timeIntervalSince1970: 1767988800)
)
]
)
} }