diff --git a/README.md b/README.md new file mode 100644 index 0000000..45844d4 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# RIT-Dining diff --git a/RIT Dining.xcodeproj/project.pbxproj b/RIT Dining.xcodeproj/project.pbxproj index a91d5b1..a1ac79f 100644 --- a/RIT Dining.xcodeproj/project.pbxproj +++ b/RIT Dining.xcodeproj/project.pbxproj @@ -94,6 +94,8 @@ ); mainGroup = 376AE0522E6495EB00AB698B; minimizedProjectReferenceProxies = 1; + packageReferences = ( + ); preferredProjectObjectVersion = 77; productRefGroup = 376AE05C2E6495EB00AB698B /* Products */; projectDirPath = ""; @@ -261,6 +263,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -289,6 +292,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/RIT Dining/Assets.xcassets/AccentColor.colorset/Contents.json b/RIT Dining/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..7010129 100644 --- a/RIT Dining/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/RIT Dining/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.412", + "red" : "0.969" + } + }, "idiom" : "universal" } ], diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/Contents.json b/RIT Dining/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..dfcf466 100644 --- a/RIT Dining/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/RIT Dining/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,31 +1,172 @@ { "images" : [ { - "idiom" : "universal", - "platform" : "ios", + "filename" : "notification40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "notification60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "settings58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "settings87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "spotlight80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "spotlight120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "iphone120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "iphone180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "ipadNotification20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "ipadNotification40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "ipadSettings29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "ipadSettings58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "ipadSpotlight40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "ipadSpotlight80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "ipad76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "ipad152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "ipadPro167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "appstore1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", "size" : "1024x1024" }, { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "filename" : "mac16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" }, { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "filename" : "mac32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "mac32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "mac64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "mac128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "mac256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "mac256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "mac512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "mac512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "mac1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" } ], "info" : { diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/appstore1024.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/appstore1024.png new file mode 100644 index 0000000..ef6c552 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/appstore1024.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad152.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad152.png new file mode 100644 index 0000000..696a92d Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad152.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad76.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad76.png new file mode 100644 index 0000000..e1fb056 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad76.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png new file mode 100644 index 0000000..667c9a7 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png new file mode 100644 index 0000000..42a2a8d Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadPro167.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadPro167.png new file mode 100644 index 0000000..c0c586a Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadPro167.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png new file mode 100644 index 0000000..0efc679 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png new file mode 100644 index 0000000..fcae781 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png new file mode 100644 index 0000000..42a2a8d Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png new file mode 100644 index 0000000..156d674 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone120.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone120.png new file mode 100644 index 0000000..4b7f247 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone120.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone180.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone180.png new file mode 100644 index 0000000..06eaf6b Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone180.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac1024.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac1024.png new file mode 100644 index 0000000..2361dc3 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac1024.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac128.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac128.png new file mode 100644 index 0000000..efbab44 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac128.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac16.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac16.png new file mode 100644 index 0000000..2c75532 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac16.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac256.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac256.png new file mode 100644 index 0000000..8bc73e6 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac256.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac32.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac32.png new file mode 100644 index 0000000..61bf53f Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac32.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac512.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac512.png new file mode 100644 index 0000000..ae91c24 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac512.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac64.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac64.png new file mode 100644 index 0000000..c6c8aed Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/mac64.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/notification40.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/notification40.png new file mode 100644 index 0000000..42a2a8d Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/notification40.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/notification60.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/notification60.png new file mode 100644 index 0000000..7e2c1be Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/notification60.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/settings58.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/settings58.png new file mode 100644 index 0000000..fcae781 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/settings58.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/settings87.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/settings87.png new file mode 100644 index 0000000..f4bcf7a Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/settings87.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight120.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight120.png new file mode 100644 index 0000000..4b7f247 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight120.png differ diff --git a/RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight80.png b/RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight80.png new file mode 100644 index 0000000..156d674 Binary files /dev/null and b/RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight80.png differ diff --git a/RIT Dining/ContentView.swift b/RIT Dining/ContentView.swift index c816a90..9340b70 100644 --- a/RIT Dining/ContentView.swift +++ b/RIT Dining/ContentView.swift @@ -2,20 +2,126 @@ // ContentView.swift // RIT Dining // -// Created by Campbell Bagley on 8/31/25. +// Created by Campbell on 8/31/25. // import SwiftUI +struct Location: Hashable { + let name: String + let todaysHours: String + let isOpen: openStatus +} + struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + @State private var isLoading = true + @State private var rotationDegrees: Double = 0 + @State private var diningLocations: [Location] = [] + + private var animation: Animation { + .linear + .speed(0.1) + .repeatForever(autoreverses: false) + } + + // Asynchronously fetch the data for all of the locations and parse their data to display it. + private func getDiningData() { + var newDiningLocations: [Location] = [] + getDiningLocation { result in + DispatchQueue.global().async { + switch result { + case .success(let locations): + for i in 0..) -> Void) { + // The endpoint requires that you specify a date, so get today's. + let date_string = Date().formatted(.iso8601 + .year().month().day() + .dateSeparator(.dash)) + let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(date_string)" + + guard let url = URL(string: url_string) else { + print("Invalid URL") + return + } + let request = URLRequest(url: url) + + URLSession.shared.dataTask(with: request) { data, response, error in + guard case .none = error else { return } + + guard let data = data else { + print("Data error.") + return + } + + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + completionHandler(.failure(InvalidHTTPError.invalid)) + return + } + + let decoded: Result = Result(catching: { try JSONDecoder().decode(DiningLocations.self, from: data) }) + completionHandler(decoded) + }.resume() +} + +enum openStatus { + case open + case closed + case openingSoon + case closingSoon +} + +struct DiningInfo { + let id: Int + let name: String + let openTime: Date? + let closeTime: Date? + let open: openStatus +} + +func getLocationInfo(location: DiningLocation) -> DiningInfo { + print("beginning parse for \(location.name)") + + // Early return if there are no events, good for things like the food trucks which can very easily have no openings in a week. + if location.events.isEmpty { + return DiningInfo( + id: location.id, + name: location.name, + openTime: .none, + closeTime: .none, + open: .closed) + } + + let openString: String + let closeString: String + + // Dining locations have a regular schedule, but then they also have exceptions listed for days like weekends or holidays. If there + // are exceptions, use those times for the day, otherwise we can just use the default times. + if let exceptions = location.events[0].exceptions, !exceptions.isEmpty { + // Early return if the exception for the day specifies that the location is closed. Used for things like holidays. + if !location.events[0].exceptions![0].open { + return DiningInfo( + id: location.id, + name: location.name, + openTime: .none, + closeTime: .none, + open: .closed) + } + openString = location.events[0].exceptions![0].startTime + closeString = location.events[0].exceptions![0].endTime + } else { + openString = location.events[0].startTime + closeString = location.events[0].endTime + } + + // I hate all of this date component nonsense. + let openParts = openString.split(separator: ":").map { Int($0) ?? 0 } + let openTimeComponents = DateComponents(hour: openParts[0], minute: openParts[1], second: openParts[2]) + + let closeParts = closeString.split(separator: ":").map { Int($0) ?? 0 } + let closeTimeComponents = DateComponents(hour: closeParts[0], minute: closeParts[1], second: closeParts[2]) + + let calendar = Calendar.current + let now = Date() + + let openDate = calendar.date( + bySettingHour: openTimeComponents.hour!, + minute: openTimeComponents.minute!, + second: openTimeComponents.second!, + of: now)! + + var closeDate = calendar.date( + bySettingHour: closeTimeComponents.hour!, + minute: closeTimeComponents.minute!, + second: closeTimeComponents.second!, + of: now)! + + // If the closing time is less than or equal to the opening time, it's probably midnight and means either open until midnight + // or open 24/7, in the case of Bytes. + if closeDate <= openDate { + closeDate = calendar.date(byAdding: .day, value: 1, to: closeDate)! + } + + // This can probably be done in a cleaner way but it's okay for now. If the location is open but the close date is within the next + // 30 minutes, label it as closing soon, and do the opposite if it's closed but the open date is within the next 30 minutes. + let isOpen = (now >= openDate && now <= closeDate) + let openStatus: openStatus + if isOpen { + if closeDate < calendar.date(byAdding: .minute, value: 30, to: now)! { + openStatus = .closingSoon + } else { + openStatus = .open + } + } else { + if openDate < calendar.date(byAdding: .minute, value: 30, to: now)! { + openStatus = .openingSoon + } else { + openStatus = .closed + } + } + + return DiningInfo( + id: location.id, + name: location.name, + openTime: openDate, + closeTime: closeDate, + open: openStatus) +} diff --git a/RIT Dining/RIT_DiningApp.swift b/RIT Dining/RIT_DiningApp.swift index d01e417..ae77b6c 100644 --- a/RIT Dining/RIT_DiningApp.swift +++ b/RIT Dining/RIT_DiningApp.swift @@ -2,7 +2,7 @@ // RIT_DiningApp.swift // RIT Dining // -// Created by Campbell Bagley on 8/31/25. +// Created by Campbell on 8/31/25. // import SwiftUI