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.
282 lines
12 KiB
Swift
282 lines
12 KiB
Swift
//
|
|
// DetailView.swift
|
|
// TigerDine
|
|
//
|
|
// Created by Campbell on 9/1/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SafariServices
|
|
|
|
struct DetailView: View {
|
|
@State var locationId: Int
|
|
|
|
@Environment(DiningModel.self) var model
|
|
@Environment(\.openURL) private var openURL
|
|
|
|
@State private var showingSafari: Bool = false
|
|
@State private var occupancyLoading: Bool = true
|
|
@State private var occupancyPercentage: Double = 0.0
|
|
|
|
// This gets the location that we're meant to be displaying details about using the provided ID.
|
|
private var location: DiningLocation {
|
|
return model.locationsByDay[0].first { $0.id == locationId }!
|
|
}
|
|
|
|
// This creates a list of the time strings for the current day and following 6 days to display in the "Upcoming Hours" section.
|
|
// I realized that it makes a lot more sense to do today + 6 rather than just the current calendar week's hours, because who
|
|
// cares what Tuesday's hours were on Saturday, you want to know what Sunday's hours will be.
|
|
private var weeklyHours: [WeeklyHours] {
|
|
var newWeeklyHours: [WeeklyHours] = []
|
|
for day in model.locationsByDay {
|
|
for location in day {
|
|
if location.id == locationId {
|
|
let weekdayFormatter = DateFormatter()
|
|
weekdayFormatter.dateFormat = "EEEE"
|
|
if let times = location.diningTimes, !times.isEmpty {
|
|
var timeStrings: [String] = []
|
|
for time in times {
|
|
timeStrings.append("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
|
}
|
|
newWeeklyHours.append(
|
|
WeeklyHours(
|
|
day: weekdayFormatter.string(from: location.date),
|
|
date: location.date,
|
|
timeStrings: timeStrings
|
|
))
|
|
} else {
|
|
newWeeklyHours.append(
|
|
WeeklyHours(
|
|
day: weekdayFormatter.string(from: location.date),
|
|
date: location.date,
|
|
timeStrings: ["Closed"]
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return newWeeklyHours
|
|
}
|
|
|
|
// Still a little broken, does not work for refresh. Need to fix.
|
|
private func getOccupancy() async {
|
|
// Only fetch occupancy data if the location is actually open right now. Otherwise, just exit early and hide the spinner.
|
|
if location.open == .open || location.open == .closingSoon {
|
|
occupancyLoading = true
|
|
switch await getOccupancyPercentage(mdoId: location.mdoId) {
|
|
case .success(let occupancy):
|
|
occupancyPercentage = occupancy
|
|
occupancyLoading = false
|
|
case .failure(let error):
|
|
print(error)
|
|
occupancyLoading = false
|
|
}
|
|
} else {
|
|
occupancyLoading = false
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading) {
|
|
VStack(alignment: .leading) {
|
|
Text(location.name)
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
Text(location.summary)
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(.secondary)
|
|
VStack(alignment: .leading) {
|
|
switch location.open {
|
|
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)
|
|
}
|
|
if let times = location.diningTimes, !times.isEmpty {
|
|
ForEach(times, id: \.self) { time in
|
|
Text("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else {
|
|
Text("Not Open Today")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
#if DEBUG
|
|
HStack(spacing: 0) {
|
|
ForEach(Range(1...5), id: \.self) { index in
|
|
if occupancyPercentage > (20 * Double(index)) {
|
|
Image(systemName: "person.fill")
|
|
} else {
|
|
Image(systemName: "person")
|
|
}
|
|
}
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.frame(width: 18, height: 18)
|
|
.opacity(occupancyLoading ? 1 : 0)
|
|
.task {
|
|
await getOccupancy()
|
|
}
|
|
}
|
|
.foregroundStyle(Color.accent.opacity(occupancyLoading ? 0.5 : 1.0))
|
|
.font(.title3)
|
|
#endif
|
|
}
|
|
.padding(.bottom, 12)
|
|
if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
|
|
VStack(alignment: .leading) {
|
|
Text("Today's Visiting Chefs")
|
|
.font(.title3)
|
|
.fontWeight(.semibold)
|
|
ForEach(visitingChefs, id: \.self) { chef in
|
|
HStack(alignment: .top) {
|
|
Text(chef.name)
|
|
Spacer()
|
|
VStack(alignment: .trailing) {
|
|
switch chef.status {
|
|
case .hereNow:
|
|
Text("Here Now")
|
|
.foregroundStyle(.green)
|
|
case .gone:
|
|
Text("Left For Today")
|
|
.foregroundStyle(.red)
|
|
case .arrivingLater:
|
|
Text("Arriving Later")
|
|
.foregroundStyle(.red)
|
|
case .arrivingSoon:
|
|
Text("Arriving Soon")
|
|
.foregroundStyle(.orange)
|
|
case .leavingSoon:
|
|
Text("Leaving Soon")
|
|
.foregroundStyle(.orange)
|
|
}
|
|
Text("\(dateDisplay.string(from: chef.openTime)) - \(dateDisplay.string(from: chef.closeTime))")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Divider()
|
|
}
|
|
}
|
|
.padding(.bottom, 12)
|
|
}
|
|
if let dailySpecials = location.dailySpecials, !dailySpecials.isEmpty {
|
|
VStack(alignment: .leading) {
|
|
Text("Today's Daily Specials")
|
|
.font(.title3)
|
|
.fontWeight(.semibold)
|
|
ForEach(dailySpecials, id: \.self) { special in
|
|
HStack(alignment: .top) {
|
|
Text(special.name)
|
|
Spacer()
|
|
Text(special.type)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Divider()
|
|
}
|
|
}
|
|
.padding(.bottom, 12)
|
|
}
|
|
VStack(alignment: .leading) {
|
|
Text("Upcoming Hours")
|
|
.font(.title3)
|
|
.fontWeight(.semibold)
|
|
ForEach(weeklyHours, id: \.self) { day in
|
|
HStack(alignment: .top) {
|
|
Text(day.day)
|
|
Spacer()
|
|
VStack {
|
|
ForEach(day.timeStrings, id: \.self) { timeString in
|
|
Text(timeString)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
Divider()
|
|
}
|
|
}
|
|
.padding(.bottom, 12)
|
|
// Ideally I'd like this text to be justified to more effectively use the screen space.
|
|
Text(location.desc)
|
|
.font(.body)
|
|
.padding(.bottom, 10)
|
|
Text("IMPORTANT: Some locations' descriptions may refer to them as being cashless during certain hours. This is outdated information, as all RIT Dining locations are now cashless 24/7.")
|
|
.font(.callout)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
// Favorites toggle button.
|
|
Button(action: {
|
|
if model.favorites.contains(location) {
|
|
model.favorites.remove(location)
|
|
} else {
|
|
model.favorites.add(location)
|
|
}
|
|
}) {
|
|
if model.favorites.contains(location) {
|
|
Image(systemName: "star.fill")
|
|
.foregroundStyle(.yellow)
|
|
//.font(.title3)
|
|
} else {
|
|
Image(systemName: "star")
|
|
.foregroundStyle(.yellow)
|
|
//.font(.title3)
|
|
}
|
|
}
|
|
// THIS FEATURE DISABLED AT RIT'S REQUEST FOR SECURITY REASONS.
|
|
// No hard feelings or anything though, I get it.
|
|
// // Open OnDemand. Unfortunately the locations use arbitrary IDs, so just open the main OnDemand page.
|
|
// Button(action: {
|
|
// openURL(URL(string: "https://ondemand.rit.edu")!)
|
|
// }) {
|
|
// Image(systemName: "cart")
|
|
// .font(.title3)
|
|
// }
|
|
// .disabled(location.open == .closed || location.open == .openingSoon)
|
|
// Open this location on the RIT map in embedded Safari.
|
|
Button(action: {
|
|
showingSafari = true
|
|
}) {
|
|
Image(systemName: "map")
|
|
//.font(.title3)
|
|
}
|
|
if let fdmpIds = location.fdmpIds {
|
|
NavigationLink(destination: MenuView(accountId: fdmpIds.accountId, locationId: fdmpIds.locationId)) {
|
|
Image(systemName: "menucard")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Details")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.sheet(isPresented: $showingSafari) {
|
|
SafariView(url: URL(string: location.mapsUrl)!)
|
|
}
|
|
.refreshable {
|
|
do {
|
|
try await model.getHoursByDay()
|
|
} catch {
|
|
print(error)
|
|
}
|
|
await getOccupancy()
|
|
}
|
|
}
|
|
}
|