mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-01-17 12:05:57 -05:00
- The favorites model now lives inside of the base dining model, since it was only ever used in places where the main dining model was also available and is only relevant when the dining model is available. - Removed unnecessary instances of models that were going unused. - Moved the favorite/map/menu buttons in the top right of the DetailView into the right side toolbar. - This frees up a good bit of space at the top of the DetailView and looks cleaner, especially with iOS 26's new toolbar style. - Actually added a copyright string to the about screen. More refactors, both internally and for the UI, will be coming soon.
191 lines
7.7 KiB
Swift
191 lines
7.7 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
|
|
|
|
@State private var model = DiningModel()
|
|
@State private var isLoading: Bool = true
|
|
@State private var loadFailed: Bool = false
|
|
@State private var showingDonationSheet: Bool = false
|
|
@State private var rotationDegrees: Double = 0
|
|
@State private var searchText: String = ""
|
|
|
|
private var animation: Animation {
|
|
.linear
|
|
.speed(0.1)
|
|
.repeatForever(autoreverses: false)
|
|
}
|
|
|
|
// Small wrapper around the method on the model so that errors can be handled by showing the uh error screen.
|
|
private func getDiningData() async {
|
|
do {
|
|
try await model.getHoursByDay()
|
|
await model.scheduleAllPushes()
|
|
isLoading = false
|
|
} catch {
|
|
isLoading = true
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack() {
|
|
if isLoading {
|
|
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
|
|
Task {
|
|
await getDiningData()
|
|
}
|
|
}) {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
}
|
|
.padding(.top, 10)
|
|
} else {
|
|
Image(systemName: "fork.knife.circle")
|
|
.resizable()
|
|
.frame(width: 75, height: 75)
|
|
.foregroundStyle(.accent)
|
|
.rotationEffect(.degrees(rotationDegrees))
|
|
.onAppear {
|
|
withAnimation(animation) {
|
|
rotationDegrees = 360.0
|
|
}
|
|
}
|
|
Text("Loading...")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
} else {
|
|
VStack() {
|
|
List {
|
|
Section(content: {
|
|
NavigationLink(destination: VisitingChefs()) {
|
|
Text("Upcoming Visiting Chefs")
|
|
}
|
|
NavigationLink(destination: FoodTruckView()) {
|
|
Text("Weekend Food Trucks")
|
|
}
|
|
})
|
|
Section(content: {
|
|
LocationList(
|
|
diningLocations: $model.locationsByDay[0],
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
.navigationTitle("TigerDine")
|
|
.searchable(text: $searchText, prompt: "Search")
|
|
.refreshable {
|
|
await getDiningData()
|
|
}
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
NavigationLink(destination: VisitingChefPush()) {
|
|
Image(systemName: "bell.badge")
|
|
}
|
|
Menu {
|
|
Button(action: {
|
|
Task {
|
|
await getDiningData()
|
|
}
|
|
}) {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
}
|
|
|
|
Divider()
|
|
NavigationLink(destination: AboutView()) {
|
|
Image(systemName: "info.circle")
|
|
Text("About")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.environment(model)
|
|
.task {
|
|
await getDiningData()
|
|
await updateOpenStatuses()
|
|
}
|
|
.sheet(isPresented: $showingDonationSheet) {
|
|
DonationView()
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ContentView()
|
|
}
|