mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-01-17 12:05:57 -05:00
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.
218 lines
6.2 KiB
Swift
218 lines
6.2 KiB
Swift
//
|
|
// HoursWidget.swift
|
|
// TigerDineWidgets
|
|
//
|
|
// Created by Campbell on 1/8/26.
|
|
//
|
|
|
|
import WidgetKit
|
|
import SwiftUI
|
|
|
|
// This timeline provider is currently held together by duct tape. But hey, that's what beta testing is for.
|
|
struct Provider: AppIntentTimelineProvider {
|
|
func placeholder(in context: Context) -> OpenEntry {
|
|
let calendar = Calendar.current
|
|
let startOfToday = calendar.startOfDay(for: Date())
|
|
let startOfTomorrow = calendar.date(byAdding: .day, value: 1, to: startOfToday)!
|
|
|
|
return OpenEntry(
|
|
date: Date(),
|
|
name: "Select a Location",
|
|
diningTimes: [
|
|
DiningTimes(openTime: startOfToday, closeTime: startOfTomorrow)
|
|
]
|
|
)
|
|
}
|
|
|
|
func snapshot(
|
|
for configuration: LocationHoursIntent,
|
|
in context: Context
|
|
) async -> OpenEntry {
|
|
loadEntry(for: configuration) ?? placeholder(in: context)
|
|
}
|
|
|
|
func timeline(
|
|
for configuration: LocationHoursIntent,
|
|
in context: Context
|
|
) async -> Timeline<OpenEntry> {
|
|
|
|
guard let baseEntry = loadEntry(for: configuration) else {
|
|
return Timeline(
|
|
entries: [placeholder(in: context)],
|
|
policy: .atEnd
|
|
)
|
|
}
|
|
|
|
let updateDates = buildUpdateSchedule(
|
|
now: Date(),
|
|
open: baseEntry.diningTimes?.first!.openTime,
|
|
close: baseEntry.diningTimes?.first!.closeTime
|
|
)
|
|
|
|
let entries = updateDates.map {
|
|
OpenEntry(
|
|
date: $0,
|
|
name: baseEntry.name,
|
|
diningTimes: baseEntry.diningTimes
|
|
)
|
|
}
|
|
|
|
return Timeline(entries: entries, policy: .atEnd)
|
|
}
|
|
|
|
func loadEntry(for configuration: LocationHoursIntent) -> OpenEntry? {
|
|
guard let selectedLocation = configuration.location else {
|
|
return nil
|
|
}
|
|
|
|
guard
|
|
let data = UserDefaults(suiteName: "group.dev.ninjacheetah.RIT-Dining")?.data(forKey: "cachedLocationsByDay"),
|
|
let decoded = try? JSONDecoder().decode([[DiningLocation]].self, from: data),
|
|
let todayLocations = decoded.first,
|
|
let location = todayLocations.first(where: {
|
|
$0.id == selectedLocation.id
|
|
})
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return OpenEntry(
|
|
date: Date(),
|
|
name: location.name,
|
|
diningTimes: location.diningTimes
|
|
)
|
|
}
|
|
|
|
func buildUpdateSchedule(
|
|
now: Date,
|
|
open: Date?,
|
|
close: Date?
|
|
) -> [Date] {
|
|
|
|
var dates: Set<Date> = []
|
|
|
|
dates.insert(now)
|
|
|
|
if let open = open, let close = close {
|
|
dates.insert(open)
|
|
dates.insert(close)
|
|
}
|
|
|
|
let interval: TimeInterval = 5 * 60
|
|
let end = Calendar.current.date(byAdding: .hour, value: 24, to: now)!
|
|
|
|
var t = now
|
|
while t < end {
|
|
dates.insert(t)
|
|
t = t.addingTimeInterval(interval)
|
|
}
|
|
|
|
return dates
|
|
.filter { $0 >= now }
|
|
.sorted()
|
|
}
|
|
}
|
|
|
|
struct OpenEntry: TimelineEntry {
|
|
let date: Date
|
|
let name: String
|
|
let diningTimes: [DiningTimes]?
|
|
}
|
|
|
|
struct OpenWidgetEntryView : View {
|
|
var entry: Provider.Entry
|
|
|
|
private let calendar = Calendar.current
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
Text(entry.name)
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
// Should maybe try to unify this with the almost-identical UI code in DetailView.
|
|
if let diningTimes = entry.diningTimes {
|
|
let openStatus = parseMultiOpenStatus(diningTimes: diningTimes)
|
|
switch openStatus {
|
|
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)
|
|
}
|
|
|
|
Text("\(dateDisplay.string(from: diningTimes[0].openTime)) - \(dateDisplay.string(from: diningTimes[0].closeTime))")
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text("Closed")
|
|
.font(.title3)
|
|
.foregroundStyle(.red)
|
|
|
|
Text("Not Open Today")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
OpeningHoursGauge(
|
|
diningTimes: entry.diningTimes,
|
|
now: entry.date
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HoursWidget: Widget {
|
|
let kind: String = "HoursWidget"
|
|
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(
|
|
kind: kind,
|
|
intent: LocationHoursIntent.self,
|
|
provider: Provider()
|
|
) { entry in
|
|
OpenWidgetEntryView(entry: entry)
|
|
.containerBackground(.fill.tertiary, for: .widget)
|
|
}
|
|
.configurationDisplayName("Location Hours")
|
|
.description("See today's hours for a chosen location.")
|
|
.supportedFamilies([.systemSmall])
|
|
}
|
|
}
|
|
|
|
#Preview(as: .systemSmall) {
|
|
HoursWidget()
|
|
} timeline: {
|
|
OpenEntry(
|
|
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)
|
|
)
|
|
]
|
|
)
|
|
}
|