mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-03-04 21:25:27 -05:00
Added deep links for widgets
- Widgets will now open the appropriate DetailView in the app when you tap on them. - ...except when they don't. This is still a little buggy. It works correctly when the app is already alive in the background but only works about 75% of the time when the app isn't running yet. - Unified the loading view into a shared view used in all places requiring loading
This commit is contained in:
@@ -54,7 +54,7 @@ func parseMultiOpenStatus(diningTimes: [DiningTimes]?, referenceTime: Date) -> O
|
|||||||
|
|
||||||
/// Parses the JSON responses from the TigerCenter API into the format used throughout TigerDine.
|
/// 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) (id: \(location.id))")
|
||||||
|
|
||||||
// The descriptions sometimes have HTML <br /> tags despite also having \n. Those need to be removed.
|
// The descriptions sometimes have HTML <br /> tags despite also having \n. Those need to be removed.
|
||||||
let desc = location.description.replacingOccurrences(of: "<br />", with: "")
|
let desc = location.description.replacingOccurrences(of: "<br />", with: "")
|
||||||
|
|||||||
@@ -292,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 = 29;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = TigerDineWidgets/Info.plist;
|
INFOPLIST_FILE = TigerDineWidgets/Info.plist;
|
||||||
@@ -325,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 = 29;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = TigerDineWidgets/Info.plist;
|
INFOPLIST_FILE = TigerDineWidgets/Info.plist;
|
||||||
@@ -481,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 = 29;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -518,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 = 29;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
|||||||
@@ -14,17 +14,13 @@ struct ContentView: View {
|
|||||||
|
|
||||||
@Environment(DiningModel.self) var model
|
@Environment(DiningModel.self) var model
|
||||||
|
|
||||||
@State private var isLoading: Bool = true
|
@Binding var targetLocationId: Int?
|
||||||
|
@Binding var handledLocationId: Int?
|
||||||
|
|
||||||
@State private var loadFailed: Bool = false
|
@State private var loadFailed: Bool = false
|
||||||
@State private var showingDonationSheet: Bool = false
|
@State private var showingDonationSheet: Bool = false
|
||||||
@State private var rotationDegrees: Double = 0
|
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
|
@State private var path = NavigationPath()
|
||||||
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.
|
// 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 {
|
private func getDiningData(bustCache: Bool = false) async {
|
||||||
@@ -35,9 +31,7 @@ struct ContentView: View {
|
|||||||
else {
|
else {
|
||||||
try await model.getHoursByDayCached()
|
try await model.getHoursByDayCached()
|
||||||
}
|
}
|
||||||
isLoading = false
|
|
||||||
} catch {
|
} catch {
|
||||||
isLoading = true
|
|
||||||
loadFailed = true
|
loadFailed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,43 +52,33 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
var body: some View {
|
||||||
NavigationStack() {
|
NavigationStack(path: $path) {
|
||||||
if isLoading {
|
if !model.isLoaded {
|
||||||
VStack {
|
VStack {
|
||||||
if loadFailed {
|
LoadingView(loadFailed: $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(bustCache: true)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
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 {
|
} else {
|
||||||
VStack() {
|
VStack() {
|
||||||
List {
|
List {
|
||||||
@@ -125,6 +109,15 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
.navigationDestination(for: DiningLocation.self) { location in
|
||||||
|
DetailView(locationId: location.id)
|
||||||
|
}
|
||||||
|
.onChange(of: targetLocationId) {
|
||||||
|
handleOpenDeepLink()
|
||||||
|
}
|
||||||
|
.onChange(of: model.isLoaded) {
|
||||||
|
handleOpenDeepLink()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("TigerDine")
|
.navigationTitle("TigerDine")
|
||||||
.searchable(text: $searchText, prompt: "Search")
|
.searchable(text: $searchText, prompt: "Search")
|
||||||
@@ -144,7 +137,13 @@ struct ContentView: View {
|
|||||||
}) {
|
}) {
|
||||||
Label("Refresh", systemImage: "arrow.clockwise")
|
Label("Refresh", systemImage: "arrow.clockwise")
|
||||||
}
|
}
|
||||||
|
#if DEBUG
|
||||||
|
Button(action: {
|
||||||
|
model.lastRefreshed = Date(timeIntervalSince1970: 0.0)
|
||||||
|
}) {
|
||||||
|
Label("Invalidate Cache", systemImage: "ant")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Divider()
|
Divider()
|
||||||
NavigationLink(destination: AboutView()) {
|
NavigationLink(destination: AboutView()) {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
@@ -192,5 +191,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
@Previewable @State var targetLocationId: Int?
|
||||||
|
@Previewable @State var handledLocationId: Int?
|
||||||
|
|
||||||
|
ContentView(targetLocationId: $targetLocationId, handledLocationId: $handledLocationId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class DiningModel {
|
|||||||
var favorites = Favorites()
|
var favorites = Favorites()
|
||||||
var notifyingChefs = NotifyingChefs()
|
var notifyingChefs = NotifyingChefs()
|
||||||
var visitingChefPushes = VisitingChefPushesModel()
|
var visitingChefPushes = VisitingChefPushesModel()
|
||||||
|
// Loading state to access in the UI.
|
||||||
|
var isLoaded = false
|
||||||
|
|
||||||
func getDaysRepresented() async {
|
func getDaysRepresented() async {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
@@ -41,6 +43,7 @@ class DiningModel {
|
|||||||
|
|
||||||
/// This is the actual method responsible for making requests to the API for the current day and next 6 days to collect all of the information that the app needs for the various view. Making it part of the model allows it to be updated from any view at any time, and prevents excess API requests (if you never refresh, the app will never need to make more than 7 calls per launch).
|
/// This is the actual method responsible for making requests to the API for the current day and next 6 days to collect all of the information that the app needs for the various view. Making it part of the model allows it to be updated from any view at any time, and prevents excess API requests (if you never refresh, the app will never need to make more than 7 calls per launch).
|
||||||
func getHoursByDay() async throws {
|
func getHoursByDay() async throws {
|
||||||
|
print("loading from network")
|
||||||
await getDaysRepresented()
|
await getDaysRepresented()
|
||||||
var newLocationsByDay = [[DiningLocation]]()
|
var newLocationsByDay = [[DiningLocation]]()
|
||||||
for day in daysRepresented {
|
for day in daysRepresented {
|
||||||
@@ -78,8 +81,11 @@ class DiningModel {
|
|||||||
// Then refresh widget timelines with the new data.
|
// Then refresh widget timelines with the new data.
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
|
||||||
// And finally schedule a background refresh 6 hours from now.
|
// Then schedule a background refresh 6 hours from now.
|
||||||
scheduleNextRefresh()
|
scheduleNextRefresh()
|
||||||
|
|
||||||
|
// And finally set the loaded state to true.
|
||||||
|
isLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper function for the real getHoursByDay() that checks the last refreshed stamp and uses cached data if it's fresh or triggers a refresh if it's stale.
|
/// Wrapper function for the real getHoursByDay() that checks the last refreshed stamp and uses cached data if it's fresh or triggers a refresh if it's stale.
|
||||||
@@ -88,6 +94,7 @@ class DiningModel {
|
|||||||
// If we can't access the lastRefreshed key, then there is likely no cache.
|
// If we can't access the lastRefreshed key, then there is likely no cache.
|
||||||
if let lastRefreshed = lastRefreshed {
|
if let lastRefreshed = lastRefreshed {
|
||||||
if Calendar.current.startOfDay(for: now) == Calendar.current.startOfDay(for: lastRefreshed) {
|
if Calendar.current.startOfDay(for: now) == Calendar.current.startOfDay(for: lastRefreshed) {
|
||||||
|
print("cache hit, loading from cache")
|
||||||
// Last refresh happened today, so the cache is fresh and we should load that.
|
// Last refresh happened today, so the cache is fresh and we should load that.
|
||||||
await getDaysRepresented()
|
await getDaysRepresented()
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
@@ -98,8 +105,11 @@ class DiningModel {
|
|||||||
locationsByDay = cachedLocationsByDay
|
locationsByDay = cachedLocationsByDay
|
||||||
updateOpenStatuses()
|
updateOpenStatuses()
|
||||||
await cleanupPushes()
|
await cleanupPushes()
|
||||||
|
|
||||||
|
isLoaded = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
print("cache miss")
|
||||||
// Otherwise, the cache is stale and we can fall out to the call to update it.
|
// Otherwise, the cache is stale and we can fall out to the call to update it.
|
||||||
}
|
}
|
||||||
try await getHoursByDay()
|
try await getHoursByDay()
|
||||||
|
|||||||
@@ -6,6 +6,19 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>dev.ninjacheetah.RIT-Dining.refresh</string>
|
<string>dev.ninjacheetah.RIT-Dining.refresh</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>dev.ninjacheetah.RIT-Dining</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>tigerdine</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import WidgetKit
|
|||||||
struct TigerDineApp: App {
|
struct TigerDineApp: App {
|
||||||
// The model needs to be instantiated here so that it's also available in the context of the refresh background task.
|
// The model needs to be instantiated here so that it's also available in the context of the refresh background task.
|
||||||
@State private var model = DiningModel()
|
@State private var model = DiningModel()
|
||||||
|
@State private var targetLocationId: Int?
|
||||||
|
@State private var handledLocationId: Int?
|
||||||
|
|
||||||
/// Triggers a refresh on the model that will only make network requests if the cache is stale, and then schedules the next refresh.
|
/// Triggers a refresh on the model that will only make network requests if the cache is stale, and then schedules the next refresh.
|
||||||
func handleAppRefresh() async {
|
private func handleAppRefresh() async {
|
||||||
do {
|
do {
|
||||||
try await model.getHoursByDayCached()
|
try await model.getHoursByDayCached()
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
@@ -26,10 +28,29 @@ struct TigerDineApp: App {
|
|||||||
scheduleNextRefresh()
|
scheduleNextRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func parseOpenedURL(url: URL) -> Int? {
|
||||||
|
guard url.scheme == "tigerdine" else { return nil }
|
||||||
|
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
|
||||||
|
if components.path == "/location" {
|
||||||
|
print("opening to a location")
|
||||||
|
if let queryItems = components.queryItems {
|
||||||
|
if queryItems.map(\.name).contains("id") {
|
||||||
|
return Int(queryItems.first(where: { $0.name == "id" })!.value!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView(targetLocationId: $targetLocationId, handledLocationId: $handledLocationId)
|
||||||
.environment(model)
|
.environment(model)
|
||||||
|
.onOpenURL { url in
|
||||||
|
targetLocationId = parseOpenedURL(url: url)
|
||||||
|
handledLocationId = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.backgroundTask(.appRefresh("dev.ninjacheetah.RIT-Dining.refresh")) {
|
.backgroundTask(.appRefresh("dev.ninjacheetah.RIT-Dining.refresh")) {
|
||||||
await handleAppRefresh()
|
await handleAppRefresh()
|
||||||
|
|||||||
@@ -12,15 +12,8 @@ struct FoodTruckView: View {
|
|||||||
@State private var foodTruckEvents: [FoodTruckEvent] = []
|
@State private var foodTruckEvents: [FoodTruckEvent] = []
|
||||||
@State private var isLoading: Bool = true
|
@State private var isLoading: Bool = true
|
||||||
@State private var loadFailed: Bool = false
|
@State private var loadFailed: Bool = false
|
||||||
@State private var rotationDegrees: Double = 0
|
|
||||||
@State private var showingSafari: Bool = false
|
@State private var showingSafari: Bool = false
|
||||||
|
|
||||||
private var animation: Animation {
|
|
||||||
.linear
|
|
||||||
.speed(0.1)
|
|
||||||
.repeatForever(autoreverses: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func doFoodTruckStuff() async {
|
private func doFoodTruckStuff() async {
|
||||||
switch await getFoodTruckPage() {
|
switch await getFoodTruckPage() {
|
||||||
case .success(let schedule):
|
case .success(let schedule):
|
||||||
@@ -35,34 +28,11 @@ struct FoodTruckView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
VStack {
|
VStack {
|
||||||
if loadFailed {
|
LoadingView(loadFailed: $loadFailed, loadingType: .truck)
|
||||||
Image(systemName: "wifi.exclamationmark.circle")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 75, height: 75)
|
|
||||||
.foregroundStyle(.accent)
|
|
||||||
Text("An error occurred while fetching food truck data. Please check your network connection and try again.")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
} else {
|
|
||||||
Image(systemName: "truck.box")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(width: 75, height: 75)
|
|
||||||
.foregroundStyle(.accent)
|
|
||||||
.rotationEffect(.degrees(rotationDegrees))
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(animation) {
|
|
||||||
rotationDegrees = 360.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text("One moment...")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await doFoodTruckStuff()
|
await doFoodTruckStuff()
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
} else {
|
} else {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|||||||
64
TigerDine/Views/Fragments/LoadingView.swift
Normal file
64
TigerDine/Views/Fragments/LoadingView.swift
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// LoadingView.swift
|
||||||
|
// TigerDine
|
||||||
|
//
|
||||||
|
// Created by Campbell on 1/24/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum LoadingType {
|
||||||
|
case normal
|
||||||
|
case truck
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoadingView: View {
|
||||||
|
@Binding var loadFailed: Bool
|
||||||
|
@State var loadingType: LoadingType = .normal
|
||||||
|
|
||||||
|
@State private var rotationDegrees: Double = 0
|
||||||
|
|
||||||
|
private var animation: Animation {
|
||||||
|
.linear
|
||||||
|
.speed(0.1)
|
||||||
|
.repeatForever(autoreverses: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingSymbol: String {
|
||||||
|
switch loadingType {
|
||||||
|
case .normal:
|
||||||
|
return "fork.knife.circle"
|
||||||
|
case .truck:
|
||||||
|
return "truck.box"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if loadFailed {
|
||||||
|
Image(systemName: "wifi.exclamationmark.circle")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 75, height: 75)
|
||||||
|
.foregroundStyle(.accent)
|
||||||
|
Text("An error occurred while loading data. Please check your network connection and try again.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
} else {
|
||||||
|
Image(systemName: loadingSymbol)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 75, height: 75)
|
||||||
|
.foregroundStyle(.accent)
|
||||||
|
.rotationEffect(.degrees(rotationDegrees))
|
||||||
|
.onAppear {
|
||||||
|
withAnimation(animation) {
|
||||||
|
rotationDegrees = 360.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Loading...")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ struct LocationList: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ForEach(filteredLocations, id: \.self) { location in
|
ForEach(filteredLocations, id: \.self) { location in
|
||||||
NavigationLink(destination: DetailView(locationId: location.id)) {
|
NavigationLink(value: location) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(location.name)
|
Text(location.name)
|
||||||
|
|||||||
@@ -14,18 +14,11 @@ struct MenuView: View {
|
|||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
@State private var isLoading: Bool = true
|
@State private var isLoading: Bool = true
|
||||||
@State private var loadFailed: Bool = false
|
@State private var loadFailed: Bool = false
|
||||||
@State private var rotationDegrees: Double = 0
|
|
||||||
@State private var selectedMealPeriod: Int = 0
|
@State private var selectedMealPeriod: Int = 0
|
||||||
@State private var openPeriods: [Int] = []
|
@State private var openPeriods: [Int] = []
|
||||||
@StateObject private var dietaryRestrictionsModel = MenuDietaryRestrictionsModel()
|
@StateObject private var dietaryRestrictionsModel = MenuDietaryRestrictionsModel()
|
||||||
@State private var showingDietaryRestrictionsSheet: Bool = false
|
@State private var showingDietaryRestrictionsSheet: Bool = false
|
||||||
|
|
||||||
private var animation: Animation {
|
|
||||||
.linear
|
|
||||||
.speed(0.1)
|
|
||||||
.repeatForever(autoreverses: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOpenPeriods() async {
|
func getOpenPeriods() async {
|
||||||
// Only run this if we haven't already gotten the open periods. This is somewhat of a bandaid solution to the issue of
|
// Only run this if we haven't already gotten the open periods. This is somewhat of a bandaid solution to the issue of
|
||||||
// fetching this information more than once, but hey it works!
|
// fetching this information more than once, but hey it works!
|
||||||
@@ -114,33 +107,11 @@ struct MenuView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
VStack {
|
VStack {
|
||||||
if loadFailed {
|
LoadingView(loadFailed: $loadFailed)
|
||||||
Image(systemName: "wifi.exclamationmark.circle")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 75, height: 75)
|
|
||||||
.foregroundStyle(.accent)
|
|
||||||
Text("An error occurred while fetching the menu. Please check your network connection and try again.")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
} else {
|
|
||||||
Image(systemName: "fork.knife.circle")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 75, height: 75)
|
|
||||||
.foregroundStyle(.accent)
|
|
||||||
.rotationEffect(.degrees(rotationDegrees))
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(animation) {
|
|
||||||
rotationDegrees = 360.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text("One moment...")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await getOpenPeriods()
|
await getOpenPeriods()
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
if !menuItems.isEmpty {
|
if !menuItems.isEmpty {
|
||||||
@@ -219,7 +190,6 @@ struct MenuView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selectedMealPeriod) {
|
.onChange(of: selectedMealPeriod) {
|
||||||
rotationDegrees = 0
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
await getMenuForPeriod(mealPeriodId: selectedMealPeriod)
|
await getMenuForPeriod(mealPeriodId: selectedMealPeriod)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ struct Provider: AppIntentTimelineProvider {
|
|||||||
name: "Select a Location",
|
name: "Select a Location",
|
||||||
diningTimes: [
|
diningTimes: [
|
||||||
DiningTimes(openTime: startOfToday, closeTime: startOfTomorrow)
|
DiningTimes(openTime: startOfToday, closeTime: startOfTomorrow)
|
||||||
]
|
],
|
||||||
|
url: URL(string: "tigerdine://")!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,8 @@ struct Provider: AppIntentTimelineProvider {
|
|||||||
OpenEntry(
|
OpenEntry(
|
||||||
date: $0,
|
date: $0,
|
||||||
name: baseEntry.name,
|
name: baseEntry.name,
|
||||||
diningTimes: baseEntry.diningTimes
|
diningTimes: baseEntry.diningTimes,
|
||||||
|
url: baseEntry.url
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +81,8 @@ struct Provider: AppIntentTimelineProvider {
|
|||||||
return OpenEntry(
|
return OpenEntry(
|
||||||
date: Date(),
|
date: Date(),
|
||||||
name: location.name,
|
name: location.name,
|
||||||
diningTimes: location.diningTimes
|
diningTimes: location.diningTimes,
|
||||||
|
url: URL(string: "tigerdine:///location?id=\(location.id)")!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +118,7 @@ struct OpenEntry: TimelineEntry {
|
|||||||
let date: Date
|
let date: Date
|
||||||
let name: String
|
let name: String
|
||||||
let diningTimes: [DiningTimes]?
|
let diningTimes: [DiningTimes]?
|
||||||
|
let url: URL
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OpenWidgetEntryView : View {
|
struct OpenWidgetEntryView : View {
|
||||||
@@ -178,6 +182,7 @@ struct HoursWidget: Widget {
|
|||||||
) { entry in
|
) { entry in
|
||||||
OpenWidgetEntryView(entry: entry)
|
OpenWidgetEntryView(entry: entry)
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
.containerBackground(.fill.tertiary, for: .widget)
|
||||||
|
.widgetURL(entry.url)
|
||||||
}
|
}
|
||||||
.configurationDisplayName("Location Hours")
|
.configurationDisplayName("Location Hours")
|
||||||
.description("See today's hours for a chosen location.")
|
.description("See today's hours for a chosen location.")
|
||||||
@@ -196,7 +201,8 @@ struct HoursWidget: Widget {
|
|||||||
openTime: Date(timeIntervalSince1970: 1767963600),
|
openTime: Date(timeIntervalSince1970: 1767963600),
|
||||||
closeTime: Date(timeIntervalSince1970: 1767988800)
|
closeTime: Date(timeIntervalSince1970: 1767988800)
|
||||||
)
|
)
|
||||||
]
|
],
|
||||||
|
url: URL(string: "tigerdine:///location?id=31")!
|
||||||
)
|
)
|
||||||
OpenEntry(
|
OpenEntry(
|
||||||
date: Date(timeIntervalSince1970: 1767978000),
|
date: Date(timeIntervalSince1970: 1767978000),
|
||||||
@@ -206,6 +212,7 @@ struct HoursWidget: Widget {
|
|||||||
openTime: Date(timeIntervalSince1970: 1767963600),
|
openTime: Date(timeIntervalSince1970: 1767963600),
|
||||||
closeTime: Date(timeIntervalSince1970: 1767988800)
|
closeTime: Date(timeIntervalSince1970: 1767988800)
|
||||||
)
|
)
|
||||||
]
|
],
|
||||||
|
url: URL(string: "tigerdine:///location?id=31")!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user