mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-03-03 12:45:28 -05:00
Also fixed a bug where locations with overlapping close and open times would show "Closing Soon" for 30 minutes just to then switch to "Open" when it rolled over.
208 lines
8.5 KiB
Swift
208 lines
8.5 KiB
Swift
//
|
|
// ContentView.swift
|
|
// TigerDine
|
|
//
|
|
// Created by Campbell on 8/31/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
// Save sort/filter options in AppStorage so that they actually get saved.
|
|
@AppStorage("openLocationsOnly") var openLocationsOnly: Bool = false
|
|
@AppStorage("openLocationsFirst") var openLocationsFirst: Bool = false
|
|
|
|
@Environment(DiningModel.self) var model
|
|
|
|
@Binding var targetLocationId: Int?
|
|
@Binding var handledLocationId: Int?
|
|
|
|
@State private var loadFailed: Bool = false
|
|
@State private var showingDonationSheet: Bool = false
|
|
@State private var showingFeedbackSheet: Bool = false
|
|
@State private var searchText: String = ""
|
|
@State private var path = NavigationPath()
|
|
|
|
// Small wrapper around the method on the model so that errors can be handled by showing the uh error screen.
|
|
private func getDiningData(bustCache: Bool = false) async {
|
|
do {
|
|
if bustCache {
|
|
try await model.getHoursByDay()
|
|
}
|
|
else {
|
|
try await model.getHoursByDayCached()
|
|
}
|
|
} catch {
|
|
loadFailed = true
|
|
}
|
|
}
|
|
|
|
// Start a perpetually running timer to refresh the open statuses, so that they automatically switch as appropriate without
|
|
// needing to refresh the data. You don't need to yell at the API again to know that the location opening at 11:00 AM should now
|
|
// display "Open" instead of "Opening Soon" now that it's 11:01.
|
|
private func updateOpenStatuses() async {
|
|
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
|
|
model.updateOpenStatuses()
|
|
// If the last refreshed date isn't today, that means we probably passed midnight and need to refresh the data.
|
|
// So do that.
|
|
if !Calendar.current.isDateInToday(model.lastRefreshed ?? Date()) {
|
|
Task {
|
|
await getDiningData()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleOpenDeepLink() {
|
|
guard
|
|
model.isLoaded,
|
|
let targetLocationId,
|
|
handledLocationId != targetLocationId,
|
|
!model.locationsByDay.isEmpty,
|
|
let location = model.locationsByDay[0].first(where: { $0.id == targetLocationId })
|
|
else { return }
|
|
handledLocationId = targetLocationId
|
|
print("TigerDine opened to \(location.name)")
|
|
// Reset the path back to the root (which is here, ContentView).
|
|
path = NavigationPath()
|
|
// Do this in an async block because apparently SwiftUI won't handle these two NavigationPath changes
|
|
// consecutively. Putting the second change in an async block makes it actually update the path the
|
|
// second time.
|
|
DispatchQueue.main.async {
|
|
path.append(location)
|
|
self.targetLocationId = nil
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack(path: $path) {
|
|
if !model.isLoaded {
|
|
VStack {
|
|
LoadingView(loadFailed: $loadFailed)
|
|
}
|
|
} else {
|
|
VStack() {
|
|
List {
|
|
Section(content: {
|
|
NavigationLink(destination: VisitingChefs()) {
|
|
Text("Upcoming Visiting Chefs")
|
|
}
|
|
NavigationLink(destination: FoodTruckView()) {
|
|
Text("Weekend Food Trucks")
|
|
}
|
|
})
|
|
Section(content: {
|
|
// Prevents crashing if the list is empty. Which shouldn't ever happen but still.
|
|
if !model.locationsByDay.isEmpty {
|
|
LocationList(
|
|
openLocationsFirst: $openLocationsFirst,
|
|
openLocationsOnly: $openLocationsOnly,
|
|
searchText: $searchText
|
|
)
|
|
}
|
|
}, footer: {
|
|
if let lastRefreshed = model.lastRefreshed {
|
|
VStack(alignment: .center) {
|
|
Text("Last refreshed: \(lastRefreshed.formatted())")
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
.navigationDestination(for: DiningLocation.self) { location in
|
|
DetailView(locationId: location.id)
|
|
}
|
|
.onChange(of: targetLocationId) {
|
|
handleOpenDeepLink()
|
|
}
|
|
.onChange(of: model.isLoaded) {
|
|
handleOpenDeepLink()
|
|
}
|
|
}
|
|
.navigationTitle("TigerDine")
|
|
.searchable(text: $searchText, prompt: "Search")
|
|
.refreshable {
|
|
await getDiningData(bustCache: true)
|
|
}
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
NavigationLink(destination: VisitingChefPush()) {
|
|
Image(systemName: "bell.badge")
|
|
}
|
|
Menu {
|
|
Button(action: {
|
|
Task {
|
|
await getDiningData(bustCache: true)
|
|
}
|
|
}) {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
}
|
|
#if DEBUG
|
|
Button(action: {
|
|
model.lastRefreshed = Date(timeIntervalSince1970: 0.0)
|
|
}) {
|
|
Label("Invalidate Cache", systemImage: "ant")
|
|
}
|
|
#endif
|
|
Divider()
|
|
NavigationLink(destination: AboutView()) {
|
|
Image(systemName: "info.circle")
|
|
Text("About")
|
|
}
|
|
Button(action: {
|
|
showingFeedbackSheet = true
|
|
}) {
|
|
Label("Feedback", systemImage: "paperplane")
|
|
}
|
|
Button(action: {
|
|
showingDonationSheet = true
|
|
}) {
|
|
Label("Donate", systemImage: "heart")
|
|
}
|
|
} label: {
|
|
Image(systemName: "slider.horizontal.3")
|
|
}
|
|
}
|
|
ToolbarItemGroup(placement: .bottomBar) {
|
|
Menu {
|
|
Toggle(isOn: $openLocationsOnly) {
|
|
Label("Hide Closed Locations", systemImage: "eye.slash")
|
|
}
|
|
Toggle(isOn: $openLocationsFirst) {
|
|
Label("Open Locations First", systemImage: "arrow.up.arrow.down")
|
|
}
|
|
} label: {
|
|
Image(systemName: "line.3.horizontal.decrease")
|
|
}
|
|
if #unavailable(iOS 26.0) {
|
|
Spacer()
|
|
}
|
|
}
|
|
if #available(iOS 26.0, *) {
|
|
ToolbarSpacer(.flexible, placement: .bottomBar)
|
|
DefaultToolbarItem(kind: .search, placement: .bottomBar)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.task {
|
|
await getDiningData()
|
|
await updateOpenStatuses()
|
|
}
|
|
.sheet(isPresented: $showingDonationSheet) {
|
|
DonationView()
|
|
}
|
|
.sheet(isPresented: $showingFeedbackSheet) {
|
|
FeedbackView()
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
@Previewable @State var targetLocationId: Int?
|
|
@Previewable @State var handledLocationId: Int?
|
|
|
|
ContentView(targetLocationId: $targetLocationId, handledLocationId: $handledLocationId)
|
|
}
|