From 065b9ab29c4b1dd874eafb579cea0c7524cb5055 Mon Sep 17 00:00:00 2001 From: George Botros <78520093+george-botros@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:35:11 -0400 Subject: [PATCH 01/21] feat: Add dining hours widget, modify default venues --- PennMobile.xcodeproj/project.pbxproj | 48 +++++-- PennMobile/Auth/OAuth2NetworkManager.swift | 2 +- .../Courses/Models/CoursesViewModel.swift | 16 +-- .../Detail View/MenuDisclosureGroup.swift | 2 +- .../SwiftUI/Views/Venue/DiningVenueRow.swift | 99 --------------- .../SwiftUI/Views/Venue/DiningVenueView.swift | 8 +- .../UserDBManager.swift | 8 ++ .../Cells/Dining/HomeDiningCellItem.swift | 2 +- .../Home/Controllers/HomeViewController.swift | 5 +- .../Setup + Navigation/AppDelegate.swift | 11 ++ PennMobileShared/Dining/DiningAPI.swift | 11 +- PennMobileShared/Dining/DiningVenueRow.swift | 117 +++++++++++++++++ .../Models/DiningVenue+Extensions.swift | 3 - .../Dining/Models/DiningVenue.swift | 2 +- PennMobileShared/General/WidgetKind.swift | 6 + Widget/Dining Hours/DiningHoursProvider.swift | 49 +++++++ Widget/Dining Hours/DiningHoursWidget.swift | 120 ++++++++++++++++++ Widget/WidgetBundle.swift | 1 + 18 files changed, 379 insertions(+), 131 deletions(-) delete mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueRow.swift create mode 100644 PennMobileShared/Dining/DiningVenueRow.swift create mode 100644 Widget/Dining Hours/DiningHoursProvider.swift create mode 100644 Widget/Dining Hours/DiningHoursWidget.swift diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 66a9d335c..280b6d7c4 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -37,7 +37,6 @@ 21640D40200EF881002F33CA /* LaundryCell + Graph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D3F200EF881002F33CA /* LaundryCell + Graph.swift */; }; 21640D4520103EB8002F33CA /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D4420103EB8002F33CA /* HomeViewController.swift */; }; 21640D542010526B002F33CA /* HomeTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D532010526B002F33CA /* HomeTableViewModel.swift */; }; - 21640D5C20105B98002F33CA /* HomeDiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5B20105B98002F33CA /* HomeDiningCell.swift */; }; 21640D5E20105BAC002F33CA /* HomeLaundryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5D20105BAC002F33CA /* HomeLaundryCell.swift */; }; 21640D6220114A29002F33CA /* ControllerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D6120114A28002F33CA /* ControllerModel.swift */; }; 21640FD3204A296D008DB6E8 /* LaundryGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640FD2204A296D008DB6E8 /* LaundryGraphView.swift */; }; @@ -161,7 +160,6 @@ 6CC88D6927B1BF51006896F6 /* DiningViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4827B1BF50006896F6 /* DiningViewHeader.swift */; }; 6CC88D6A27B1BF51006896F6 /* DiningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4927B1BF50006896F6 /* DiningView.swift */; }; 6CC88D6B27B1BF51006896F6 /* DiningVenueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4B27B1BF50006896F6 /* DiningVenueView.swift */; }; - 6CC88D6C27B1BF51006896F6 /* DiningVenueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4C27B1BF50006896F6 /* DiningVenueRow.swift */; }; 6CC88D6D27B1BF51006896F6 /* sample-dining-venue.json in Resources */ = {isa = PBXBuildFile; fileRef = 6CC88D4D27B1BF50006896F6 /* sample-dining-venue.json */; }; 6CC88D6E27B1BF51006896F6 /* DiningVenueDetailLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4F27B1BF50006896F6 /* DiningVenueDetailLocationView.swift */; }; 6CC88D6F27B1BF51006896F6 /* DiningVenueDetailMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D5027B1BF50006896F6 /* DiningVenueDetailMenuView.swift */; }; @@ -178,6 +176,7 @@ 6CE12F9226E82DC600284D9F /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE12F9126E82DC600284D9F /* FirebaseCrashlytics */; }; 6CFA06F626E8352F00944B8E /* HomeStudyRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA06F526E8352F00944B8E /* HomeStudyRoomCell.swift */; }; 6CFA06F826E8355400944B8E /* HomeFeatureCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA06F726E8355400944B8E /* HomeFeatureCellItem.swift */; }; + 6E167A1C2ACC90A700F3709C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 6E167A1B2ACC90A700F3709C /* Kingfisher */; }; 6E4D82192AC8C91C009AB78E /* PennMobileShared.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E4D82182AC8C91C009AB78E /* PennMobileShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6E4D821C2AC8C91C009AB78E /* PennMobileShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */; }; 6E4D821D2AC8C91C009AB78E /* PennMobileShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -203,6 +202,12 @@ 6E5159F42AC8CA1B004B3F41 /* PennMobileShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6E5159F92AC8D88A004B3F41 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5159F82AC8D88A004B3F41 /* SwiftyJSON */; }; 6E903D382AC9C67000F2D384 /* ImageNetworkingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E903D372AC9C67000F2D384 /* ImageNetworkingManager.swift */; }; + 6ECB4C2B2ACA6F4C00F7379A /* DiningVenueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4C27B1BF50006896F6 /* DiningVenueRow.swift */; }; + 6ECB4C2D2ACA6F7600F7379A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 6ECB4C2C2ACA6F7600F7379A /* Kingfisher */; }; + 6ECB4C322ACA6FCB00F7379A /* DiningHoursProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB4C302ACA6FC800F7379A /* DiningHoursProvider.swift */; }; + 6ECB4C342ACA6FE200F7379A /* DiningHoursWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */; }; + 6ECB4C362ACB10D500F7379A /* HomeDiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5B20105B98002F33CA /* HomeDiningCell.swift */; }; + 6ECB4C382ACB11FA00F7379A /* DiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806723D26BC700C23488 /* DiningCell.swift */; }; 8766844128CBE907005CAD32 /* NativeNewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8766844028CBE907005CAD32 /* NativeNewsViewController.swift */; }; 87FE6479290EE4BE00AFADF6 /* NotificationAPIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FE6478290EE4BE00AFADF6 /* NotificationAPIModel.swift */; }; 890DDBC62AA2E4B6006815A3 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890DDBC42AA2E499006815A3 /* ViewExtensions.swift */; }; @@ -231,7 +236,6 @@ 89EA2622290EE3FD008F26CF /* CoursesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EA2620290EE3E9008F26CF /* CoursesProvider.swift */; }; 89EA262E290F9411008F26CF /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 89EA262D290F9411008F26CF /* Intents.intentdefinition */; }; 89EA262F290F958B008F26CF /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 89EA262D290F9411008F26CF /* Intents.intentdefinition */; }; - 97AA806C23D26BC700C23488 /* DiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806723D26BC700C23488 /* DiningCell.swift */; }; 97E6E1F0239D74F500C07D7A /* GSRGroupIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E6E1EF239D74F500C07D7A /* GSRGroupIconView.swift */; }; 97E79E032100DA1200D3D606 /* BuildingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79DFC2100DA1200D3D606 /* BuildingCell.swift */; }; 97E79E042100DA1200D3D606 /* BuildingHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79DFD2100DA1200D3D606 /* BuildingHeaderCell.swift */; }; @@ -599,6 +603,8 @@ 6E5159DF2AC8C9D5004B3F41 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 6E5159E02AC8C9D5004B3F41 /* DiningToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningToken.swift; sourceTree = ""; }; 6E903D372AC9C67000F2D384 /* ImageNetworkingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageNetworkingManager.swift; sourceTree = ""; }; + 6ECB4C302ACA6FC800F7379A /* DiningHoursProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningHoursProvider.swift; sourceTree = ""; }; + 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningHoursWidget.swift; sourceTree = ""; }; 72787B1E070BFCDF84D8C3CA /* Pods-PennMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PennMobile.debug.xcconfig"; path = "Target Support Files/Pods-PennMobile/Pods-PennMobile.debug.xcconfig"; sourceTree = ""; }; 8766844028CBE907005CAD32 /* NativeNewsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeNewsViewController.swift; sourceTree = ""; }; 87FE6478290EE4BE00AFADF6 /* NotificationAPIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAPIModel.swift; sourceTree = ""; }; @@ -752,6 +758,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6ECB4C2D2ACA6F7600F7379A /* Kingfisher in Frameworks */, 6E5159F92AC8D88A004B3F41 /* SwiftyJSON in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -764,6 +771,7 @@ 8932693728FC75A5003D4BF9 /* SwiftUI.framework in Frameworks */, 8932693528FC75A5003D4BF9 /* WidgetKit.framework in Frameworks */, 6E5159F32AC8CA1B004B3F41 /* PennMobileShared.framework in Frameworks */, + 6E167A1C2ACC90A700F3709C /* Kingfisher in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1433,7 +1441,6 @@ isa = PBXGroup; children = ( 6CC88D4B27B1BF50006896F6 /* DiningVenueView.swift */, - 6CC88D4C27B1BF50006896F6 /* DiningVenueRow.swift */, 6CC88D4D27B1BF50006896F6 /* sample-dining-venue.json */, 6CC88D4E27B1BF50006896F6 /* Detail View */, ); @@ -1511,6 +1518,7 @@ children = ( 6E5159D62AC8C9D4004B3F41 /* DiningAnalyticsViewModel.swift */, 6E5159D72AC8C9D4004B3F41 /* DiningAPI.swift */, + 6CC88D4C27B1BF50006896F6 /* DiningVenueRow.swift */, 6E903D3D2AC9C86B00F2D384 /* Models */, ); path = Dining; @@ -1530,6 +1538,15 @@ path = Models; sourceTree = ""; }; + 6ECB4C2F2ACA6FB500F7379A /* Dining Hours */ = { + isa = PBXGroup; + children = ( + 6ECB4C302ACA6FC800F7379A /* DiningHoursProvider.swift */, + 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */, + ); + path = "Dining Hours"; + sourceTree = ""; + }; 8932693828FC75A5003D4BF9 /* Widget */ = { isa = PBXGroup; children = ( @@ -1537,6 +1554,7 @@ 8932538E290F98BD006EE62C /* ConfigurationRepresenting.swift */, 893253912910249B006EE62C /* WidgetBackgroundTypeExtensions.swift */, 890DDBC42AA2E499006815A3 /* ViewExtensions.swift */, + 6ECB4C2F2ACA6FB500F7379A /* Dining Hours */, 89EA261F290EE39E008F26CF /* Courses */, 89CA727129171E2400CF72FE /* Dining */, 8932693F28FC75A6003D4BF9 /* Assets.xcassets */, @@ -2022,6 +2040,7 @@ name = PennMobileShared; packageProductDependencies = ( 6E5159F82AC8D88A004B3F41 /* SwiftyJSON */, + 6ECB4C2C2ACA6F7600F7379A /* Kingfisher */, ); productName = PennSharedCode; productReference = 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */; @@ -2044,6 +2063,7 @@ name = WidgetExtension; packageProductDependencies = ( 8932694C28FC79FB003D4BF9 /* SwiftyJSON */, + 6E167A1B2ACC90A700F3709C /* Kingfisher */, ); productName = WidgetExtension; productReference = 8932693328FC75A5003D4BF9 /* WidgetExtension.appex */; @@ -2325,7 +2345,6 @@ 89CA728D291721D200CF72FE /* KeychainAccessible+Extensions.swift in Sources */, 97E79E052100DA1200D3D606 /* BuildingSectionHeader.swift in Sources */, 217A7843204D995C004F1227 /* ModularTableViewCell.swift in Sources */, - 21640D5C20105B98002F33CA /* HomeDiningCell.swift in Sources */, 2138D559225998A300D67CA2 /* GSRGroupController.swift in Sources */, 217A783D204D97B6004F1227 /* ModularTableViewModel.swift in Sources */, EF389EC621861B8500E29C6A /* HomeNavigationController.swift in Sources */, @@ -2359,6 +2378,7 @@ 6CC88D6E27B1BF51006896F6 /* DiningVenueDetailLocationView.swift in Sources */, 6CAA4B5827A763A400473CC6 /* HomePollsCellHeader.swift in Sources */, F2B8C40C252C58E600922D08 /* GSRClosedView.swift in Sources */, + 6ECB4C382ACB11FA00F7379A /* DiningCell.swift in Sources */, F2568A7824135B6500561295 /* HomeCellHeader.swift in Sources */, 6C6035FC26E723240025FBC7 /* EmptyView.swift in Sources */, 21640FD3204A296D008DB6E8 /* LaundryGraphView.swift in Sources */, @@ -2413,6 +2433,7 @@ 21F5F8F520538AFC005B143F /* FlingPerformer.swift in Sources */, 21E6A109224BDFEB00DC457A /* HomeViewController + Delegates.swift in Sources */, 217A7832204D2C7E004F1227 /* HomeCellItem.swift in Sources */, + 6ECB4C362ACB10D500F7379A /* HomeDiningCell.swift in Sources */, 2189C08D2027CE2600771C1F /* RoomCell.swift in Sources */, 6C6FE1C927B9B8CB0093FD13 /* MoreCell.swift in Sources */, 6CC88D6627B1BF51006896F6 /* VariableStepLineGraphView.swift in Sources */, @@ -2500,14 +2521,12 @@ B62875F92118F95300FB2873 /* BuildingProtocol.swift in Sources */, F212BE8623B6DA8D00ED46A1 /* PrivacyTableViewCell.swift in Sources */, 426A0B51299034910066C7B7 /* DiningAnalyticsGraph.swift in Sources */, - 97AA806C23D26BC700C23488 /* DiningCell.swift in Sources */, 6C6FE1CC27B9B8CB0093FD13 /* PacCodeNetworkManager.swift in Sources */, 6CC88D5927B1BF51006896F6 /* DiningLoginViewSwiftUI.swift in Sources */, 89B454DF28E1161B00BC918B /* PathAtPennNetworkManager.swift in Sources */, 2196D5E31F5263AC002058A0 /* AnnouncementHeaderView.swift in Sources */, F2562A302558330D0021C92F /* CourseAlertSettingsOptions.swift in Sources */, F2770ADB2545E2EB001EA1DD /* CourseAlertCell.swift in Sources */, - 6CC88D6C27B1BF51006896F6 /* DiningVenueRow.swift in Sources */, 6C6FE1D527B9B8CB0093FD13 /* HeaderViewCell.swift in Sources */, 97E79E162100E33100D3D606 /* FitnessAPI.swift in Sources */, 6C6FE1D627B9B8CB0093FD13 /* AboutViewController.swift in Sources */, @@ -2564,6 +2583,7 @@ 6E5159E42AC8C9D5004B3F41 /* ScheduleView.swift in Sources */, 6E5159E32AC8C9D5004B3F41 /* KeychainAccessible.swift in Sources */, 6E5159E22AC8C9D5004B3F41 /* WidgetKind.swift in Sources */, + 6ECB4C2B2ACA6F4C00F7379A /* DiningVenueRow.swift in Sources */, 6E5159EE2AC8C9D5004B3F41 /* PastDiningBalances.swift in Sources */, 6E5159EC2AC8C9D5004B3F41 /* Extensions.swift in Sources */, 6E5159F02AC8C9D5004B3F41 /* DiningMenu.swift in Sources */, @@ -2585,6 +2605,8 @@ 89CA729129174CF900CF72FE /* DiningAnalyticsProvider.swift in Sources */, 89325390290F98E7006EE62C /* ConfigurationRepresenting.swift in Sources */, 89EA262F290F958B008F26CF /* Intents.intentdefinition in Sources */, + 6ECB4C322ACA6FCB00F7379A /* DiningHoursProvider.swift in Sources */, + 6ECB4C342ACA6FE200F7379A /* DiningHoursWidget.swift in Sources */, 8932693A28FC75A5003D4BF9 /* WidgetBundle.swift in Sources */, 89EA261E290EDFA7008F26CF /* CoursesDayWidget.swift in Sources */, 89325393291025A8006EE62C /* WidgetBackgroundTypeExtensions.swift in Sources */, @@ -3124,7 +3146,7 @@ repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 8.6.1; + minimumVersion = 10.0.0; }; }; F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { @@ -3174,11 +3196,21 @@ package = 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseCrashlytics; }; + 6E167A1B2ACC90A700F3709C /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; 6E5159F82AC8D88A004B3F41 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; + 6ECB4C2C2ACA6F7600F7379A /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; 8932694C28FC79FB003D4BF9 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/PennMobile/Auth/OAuth2NetworkManager.swift b/PennMobile/Auth/OAuth2NetworkManager.swift index 95a59ebd2..761a02f1c 100755 --- a/PennMobile/Auth/OAuth2NetworkManager.swift +++ b/PennMobile/Auth/OAuth2NetworkManager.swift @@ -35,7 +35,7 @@ class OAuth2NetworkManager: NSObject { private var clientID = InfoPlistEnvironment.labsOauthClientId private var currentAccessToken: AccessToken? - + static let authQueue = DispatchQueue(label: "org.pennlabs.PennMobile.authqueue") } diff --git a/PennMobile/Courses/Models/CoursesViewModel.swift b/PennMobile/Courses/Models/CoursesViewModel.swift index 13323ddd2..ef60cf2aa 100644 --- a/PennMobile/Courses/Models/CoursesViewModel.swift +++ b/PennMobile/Courses/Models/CoursesViewModel.swift @@ -32,25 +32,25 @@ private func getTimeInt(pathAtPennString: String) -> Int? { extension Course { /// Initializes a course from Path@Penn data. init(_ data: PathAtPennNetworkManager.CourseData) { - var crn = data.crn - var code = data.code - var title = data.title - var section = data.section + let crn = data.crn + let code = data.code + let title = data.title + let section = data.section let instructorHTML = try? SwiftSoup.parse(data.instructordetail_html) let divs = try? instructorHTML?.select("div") - var instructors = (try? divs?.map { try $0.text(trimAndNormaliseWhitespace: true) }) ?? [] + let instructors = (try? divs?.map { try $0.text(trimAndNormaliseWhitespace: true) }) ?? [] let meetingHTML = try? SwiftSoup.parse(data.meeting_html) let a = try? meetingHTML?.select("a").first() - var location = try? a?.text(trimAndNormaliseWhitespace: true) + let location = try? a?.text(trimAndNormaliseWhitespace: true) struct PathAtPennMeetingTime: Decodable { var meet_day: String var start_time: String var end_time: String } - + var meetingTimes: [MeetingTime]? var startDate: Date? var endDate: Date? @@ -87,7 +87,7 @@ extension Course { endDate = nil meetingTimes = nil } - + self.init(crn: crn, code: code, title: title, section: section, instructors: instructors, startDate: startDate, endDate: endDate, meetingTimes: meetingTimes) } } diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift index 8ecba1ef3..71ca7adda 100644 --- a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift @@ -208,7 +208,7 @@ extension String { // Crashes when you pass invalid pattern let regex = try! NSRegularExpression(pattern: pattern) let matches = regex.matches(in: self, range: NSRange(0.. some View { - if venue.hasMealsToday && venue.isOpen { - if venue.isClosingSoon { - return content.foregroundColor(Color.red) - } else { - return content.foregroundColor(Color.green) - } - } else { - return content.foregroundColor(Color.gray) - } - } -} - -struct VenueStatusLabelStyle: LabelStyle { - func makeBody(configuration: Configuration) -> some View { - HStack(spacing: 4) { - configuration.icon.font(.system(size: 9, weight: .semibold)) - configuration.title.font(.system(size: 11, weight: .semibold)) - Spacer() - } - } -} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift index f62f83302..229e4f5b1 100644 --- a/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift +++ b/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift @@ -29,15 +29,15 @@ struct DiningVenueView: View { let venueTask = Task { await diningVM.refreshVenues() } - + let balanceTask = Task { await diningVM.refreshBalance() } - + let menuTask = Task { await diningVM.refreshMenus(cache: true) } - + let analyticsTask = Task { // Only refresh widgets once await diningAnalyticsViewModel.refresh(refreshWidgets: widgetsNeedRefresh) @@ -45,7 +45,7 @@ struct DiningVenueView: View { widgetsNeedRefresh = false } } - + await venueTask.value await balanceTask.value await menuTask.value diff --git a/PennMobile/General/Networking + Analytics/UserDBManager.swift b/PennMobile/General/Networking + Analytics/UserDBManager.swift index 96bfdf1fe..242a67927 100755 --- a/PennMobile/General/Networking + Analytics/UserDBManager.swift +++ b/PennMobile/General/Networking + Analytics/UserDBManager.swift @@ -9,6 +9,7 @@ import UIKit import SwiftyJSON import PennMobileShared +import WidgetKit func getDeviceID() -> String { let deviceID = UIDevice.current.identifierForVendor!.uuidString @@ -128,6 +129,13 @@ extension UserDBManager { request.httpBody = try? JSON(["venues": venueIds]).rawData() request.addValue("application/json", forHTTPHeaderField: "Content-Type") + // Cache a user's favorite dining halls for use by dining hours widget. + let diningVenues = DiningAPI.instance.getVenues(with: venueIds) + Storage.store(diningVenues, to: .groupCaches, as: DiningAPI.cacheFileName) + WidgetKind.diningHoursWidgets.forEach { + WidgetCenter.shared.reloadTimelines(ofKind: $0) + } + let task = URLSession.shared.dataTask(with: request) task.resume() } diff --git a/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift b/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift index 5f6adddaa..e8276a56f 100755 --- a/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift +++ b/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift @@ -35,7 +35,7 @@ final class HomeDiningCellItem: HomeCellItem { UserDBManager.shared.fetchDiningPreferences { result in if let venues = try? result.get() { if venues.count == 0 { - completion([HomeDiningCellItem(for: DiningAPI.instance.getVenues(with: DiningVenue.defaultVenueIds))]) + completion([HomeDiningCellItem(for: DiningAPI.instance.getVenues(with: DiningAPI.defaultVenueIds))]) } else { completion([HomeDiningCellItem(for: venues)]) } diff --git a/PennMobile/Home/Controllers/HomeViewController.swift b/PennMobile/Home/Controllers/HomeViewController.swift index 15f5562c6..39295a2bd 100755 --- a/PennMobile/Home/Controllers/HomeViewController.swift +++ b/PennMobile/Home/Controllers/HomeViewController.swift @@ -221,13 +221,14 @@ extension HomeViewController: DiningCellSettingsDelegate { func saveSelection(for venueIds: [Int]) { guard let diningItem = self.tableViewModel.getItems(for: [HomeItemTypes.instance.dining]).first as? HomeDiningCellItem else { return } if venueIds.count == 0 { - diningItem.venues = DiningAPI.instance.getVenues(with: DiningVenue.defaultVenueIds) + diningItem.venues = DiningAPI.instance.getVenues(with: DiningAPI.defaultVenueIds) + UserDBManager.shared.saveDiningPreference(for: DiningAPI.defaultVenueIds) } else { diningItem.venues = DiningAPI.instance.getVenues(with: venueIds) + UserDBManager.shared.saveDiningPreference(for: venueIds) } reloadItem(diningItem) - UserDBManager.shared.saveDiningPreference(for: venueIds) } } diff --git a/PennMobile/Setup + Navigation/AppDelegate.swift b/PennMobile/Setup + Navigation/AppDelegate.swift index 59f89eaf5..2e1c28a0a 100755 --- a/PennMobile/Setup + Navigation/AppDelegate.swift +++ b/PennMobile/Setup + Navigation/AppDelegate.swift @@ -123,6 +123,10 @@ func migrateDataToGroupContainer() { } } + if Storage.migrate(fileName: DiningAPI.directory, of: [DiningVenue].self, from: .caches, to: .groupCaches) { + print("Migrated course data.") + } + if Storage.migrate(fileName: DiningAnalyticsViewModel.dollarHistoryDirectory, of: [DiningAnalyticsBalance].self, from: .documents, to: .groupDocuments) || Storage.migrate(fileName: DiningAnalyticsViewModel.swipeHistoryDirectory, of: [DiningAnalyticsBalance].self, from: .documents, to: .groupDocuments) { print("Migrated dining analytics data.") WidgetKind.diningAnalyticsWidgets.forEach { @@ -130,6 +134,13 @@ func migrateDataToGroupContainer() { } } + if Storage.migrate(fileName: DiningAPI.cacheFileName, of: [DiningVenue].self, from: .caches, to: .groupCaches) { + print("Migrated dining favorites data.") + WidgetKind.diningHoursWidgets.forEach { + WidgetCenter.shared.reloadTimelines(ofKind: $0) + } + } + // Migrate dining balances if a dining balance file doesn't already exist. if let diningBalance = (UserDefaults.standard as SwiftCompilerSilencing).getDiningBalance() { if !Storage.fileExists(DiningBalance.directory, in: .groupCaches) { diff --git a/PennMobileShared/Dining/DiningAPI.swift b/PennMobileShared/Dining/DiningAPI.swift index 705ef4b1c..b96ef3220 100644 --- a/PennMobileShared/Dining/DiningAPI.swift +++ b/PennMobileShared/Dining/DiningAPI.swift @@ -10,11 +10,16 @@ import SwiftyJSON import Foundation public class DiningAPI { + + public static let directory = "diningVenue-v2.json" + public static let defaultVenueIds: [Int] = [593, 636, 1442, 639] public static let instance = DiningAPI() let diningUrl = "https://pennmobile.org/api/dining/venues/" let diningMenuUrl = "https://pennmobile.org/api/dining/menus/" + + public static let cacheFileName = "diningFavoritesCache" let diningInsightsUrl = "https://pennmobile.org/api/dining/" @@ -59,8 +64,8 @@ public class DiningAPI { public extension DiningAPI { // MARK: - Get Methods func getVenues() -> [DiningVenue] { - if Storage.fileExists(DiningVenue.directory, in: .caches) { - return Storage.retrieve(DiningVenue.directory, from: .caches, as: [DiningVenue].self) + if Storage.fileExists(DiningAPI.directory, in: .groupCaches) { + return Storage.retrieve(DiningAPI.directory, from: .groupCaches, as: [DiningVenue].self) } else { return [] } @@ -88,7 +93,7 @@ public extension DiningAPI { // MARK: - Cache Methods func saveToCache(_ venues: [DiningVenue]) { - Storage.store(venues, to: .caches, as: DiningVenue.directory) + Storage.store(venues, to: .groupCaches, as: DiningAPI.directory) } func saveMenuToCache(id: Int, _ menu: MenuList) { diff --git a/PennMobileShared/Dining/DiningVenueRow.swift b/PennMobileShared/Dining/DiningVenueRow.swift new file mode 100644 index 000000000..a5dfe4ea5 --- /dev/null +++ b/PennMobileShared/Dining/DiningVenueRow.swift @@ -0,0 +1,117 @@ +// +// DiningVenueRow.swift +// PennMobile +// +// Created by CHOI Jongmin on 5/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI +import Kingfisher + +public struct DiningVenueRow: View { + + let venue: DiningVenue + let isWidget: Bool + + public init(for venue: DiningVenue, isWidget: Bool = false) { + self.venue = venue + self.isWidget = isWidget + } + + public var body: some View { + HStack(spacing: 13) { + KFImage(venue.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 64) + .background(Color.grey1) + .clipShape(RoundedRectangle(cornerRadius: 7)) + + VStack(alignment: .leading, spacing: 3) { + Label(venue.statusString, systemImage: venue.statusImageString) + .labelStyle(VenueStatusLabelStyle()) + .modifier(StatusColorModifier(for: venue)) + + Text(venue.name) + .font(.system(size: 17, weight: .medium)) + .minimumScaleFactor(0.2) + .lineLimit(1) + + GeometryReader { geo in + // Ensure widget view is static + if (!isWidget) { + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { proxy in + hoursDisplay(in: geo, fontSize: 14, horizontalPadding: 6) + .onAppear { + withAnimation { + proxy.scrollTo(venue.currentOrNearestMealIndex) + } + } + } + } + } else { + hoursDisplay(in: geo, fontSize: 10.5, horizontalPadding: 4) + // Vertical pipe separator view +// Text(venue.humanFormattedHoursStringForToday) +// .font(.system(size: 12, weight: .light, design: .default)) +// .foregroundColor(Color.gray) +// .scaledToFit() +// .minimumScaleFactor(0.01) +// .lineLimit(1) + } + } + } + .frame(height: 64) + } + } + + private func hoursDisplay(in geo: GeometryProxy, fontSize: CGFloat, horizontalPadding: CGFloat) -> some View { + HStack(spacing: 4) { + ForEach(Array(venue.humanFormattedHoursArrayForToday.enumerated()), id: \.offset) { (index, time) in + Text(time) + .font(.system(size: fontSize, weight: .light, design: .default)) + .padding(.vertical, 3) + .padding(.horizontal, horizontalPadding) + .foregroundColor(index == venue.currentMealIndex ? Color.white : Color.labelPrimary) + .background(index == venue.currentMealIndex ? (venue.isClosingSoon ? Color.redLight : Color.greenLight) : Color.grey5) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .id(index) + .frame(height: geo.frame(in: .global).height) + } + } + } +} + +// MARK: - ViewModifiers +public struct StatusColorModifier: ViewModifier { + + public init(for venue: DiningVenue) { + self.venue = venue + } + + let venue: DiningVenue + + public func body(content: Content) -> some View { + if venue.hasMealsToday && venue.isOpen { + if venue.isClosingSoon { + return content.foregroundColor(Color.red) + } else { + return content.foregroundColor(Color.green) + } + } else { + return content.foregroundColor(Color.gray) + } + } +} + +struct VenueStatusLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 4) { + configuration.icon.font(.system(size: 9, weight: .semibold)) + configuration.title.font(.system(size: 11, weight: .semibold)) + Spacer() + } + } +} diff --git a/PennMobileShared/Dining/Models/DiningVenue+Extensions.swift b/PennMobileShared/Dining/Models/DiningVenue+Extensions.swift index 12f49ecae..f7b89997a 100755 --- a/PennMobileShared/Dining/Models/DiningVenue+Extensions.swift +++ b/PennMobileShared/Dining/Models/DiningVenue+Extensions.swift @@ -21,9 +21,6 @@ public extension VenueType { // MARK: - DiningVenue public extension DiningVenue { - // MARK: - Defaults - static let defaultVenueIds: [Int] = [593, 636, 1442] - // MARK: - Venue Status var mealsToday: Day? { let dateFormatter = DateFormatter() diff --git a/PennMobileShared/Dining/Models/DiningVenue.swift b/PennMobileShared/Dining/Models/DiningVenue.swift index d578ec905..3fe68342e 100755 --- a/PennMobileShared/Dining/Models/DiningVenue.swift +++ b/PennMobileShared/Dining/Models/DiningVenue.swift @@ -9,7 +9,7 @@ import Foundation public struct DiningVenue: Codable, Equatable, Identifiable { - public static let directory = "diningVenue-v2.json" + public static let menuUrlDict: [Int: String] = [593: "https://university-of-pennsylvania.cafebonappetit.com/cafe/1920-commons/", 636: "https://university-of-pennsylvania.cafebonappetit.com/cafe/hill-house/", 637: "https://university-of-pennsylvania.cafebonappetit.com/cafe/kings-court-english-house/", diff --git a/PennMobileShared/General/WidgetKind.swift b/PennMobileShared/General/WidgetKind.swift index 2892f7786..e53ffcc03 100644 --- a/PennMobileShared/General/WidgetKind.swift +++ b/PennMobileShared/General/WidgetKind.swift @@ -12,6 +12,12 @@ public enum WidgetKind { public static let courseWidgets = [ coursesDay ] + + public static let diningHours = "org.pennlabs.PennMobile.widgets.dininghours" + + public static let diningHoursWidgets = [ + diningHours + ] public static let diningAnalyticsHome = "org.pennlabs.PennMobile.widgets.dininganalytics.home" diff --git a/Widget/Dining Hours/DiningHoursProvider.swift b/Widget/Dining Hours/DiningHoursProvider.swift new file mode 100644 index 000000000..22f815a6c --- /dev/null +++ b/Widget/Dining Hours/DiningHoursProvider.swift @@ -0,0 +1,49 @@ +// +// DiningHoursProvider.swift +// DiningHoursWidgetExtension +// +// Created by George Botros on 10/1/23. +// Copyright © 2023 PennLabs. All rights reserved. +// +import WidgetKit +import SwiftUI +import PennMobileShared + +struct DiningEntries: TimelineEntry { + var date: Date + var venues: [DiningVenue] + let configuration: Configuration +} + +extension DiningEntries where Configuration == Void { + init(date: Date, venues: [DiningVenue]) { + self.init(date: date, venues: venues, configuration: ()) + } +} + +private func getDiningPreferences() -> [DiningVenue] { + do { + return try Storage.retrieveThrowing(DiningAPI.cacheFileName, from: .groupCaches, as: [DiningVenue].self) + + } catch let error { + print("Couldn't load dining preferences: \(error)") + return [] + } +} + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> DiningEntries { + DiningEntries(date: .now, venues: []) + } + + func getSnapshot(in context: Context, completion: @escaping (DiningEntries) -> ()) { + let entry = DiningEntries(date: .now, venues: []) + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline>) -> ()) { + let venues: [DiningVenue] = getDiningPreferences() + let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .atEnd) + completion(timeline) + } +} diff --git a/Widget/Dining Hours/DiningHoursWidget.swift b/Widget/Dining Hours/DiningHoursWidget.swift new file mode 100644 index 000000000..e32c7dfd1 --- /dev/null +++ b/Widget/Dining Hours/DiningHoursWidget.swift @@ -0,0 +1,120 @@ +// +// DiningHoursWidget.swift +// DiningHoursWidget +// +// Created by George Botros on 10/1/23. +// Copyright © 2023 PennLabs. All rights reserved. +// +import WidgetKit +import SwiftUI +import PennMobileShared +import Kingfisher + +struct DiningHoursWidgetEntryView : View { + var entries: Provider.Entry + @Environment(\.widgetFamily) var widgetFamily + + var body: some View { + Group { + switch widgetFamily { + case .systemLarge: + let venues = entries.venues.prefix(4) + VStack { + ForEach(venues) { venue in + Spacer() + DiningVenueRow(for: venue, isWidget: true) + Spacer() + } + } + + case .systemMedium: + VStack { + let venues = entries.venues.prefix(2) + ForEach(venues) { venue in + DiningVenueRow(for: venue, isWidget: true) + } + } + + case .systemSmall: + let venue = entries.venues[0] + smallWidget(venue: venue) + + default: + Text("Unsupported") + } + } + } + + private func smallWidget(venue: DiningVenue) -> some View { + ZStack { + KFImage(venue.image) + .resizable() + .scaledToFill() + .background(Color.grey1) + .overlay( + LinearGradient(gradient: Gradient(colors: [.clear, Color.grey6]), + startPoint: .top, + endPoint: .bottom) + ) + VStack(alignment: .leading) { + Spacer() + Spacer() + Spacer() + Spacer() + Spacer() + Spacer() + Spacer() + Label(venue.statusString, systemImage: venue.statusImageString) + .labelStyle(VenueStatusLabelStyle()) + .modifier(StatusColorModifier(for: venue)) + .minimumScaleFactor(0.2) + .lineLimit(1) + .padding(.leading, 10) + + Text(venue.name) + .font(.system(size: 15, weight: .medium)) + .minimumScaleFactor(0.2) + .lineLimit(1) + .padding(.leading, 10) + Spacer() + } + + } + } + +} + +struct VenueStatusLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 4) { + configuration.icon.font(.system(size: 8, weight: .semibold)) + configuration.title.font(.system(size: 10, weight: .semibold)) + } + } +} + +struct DiningHoursWidget: Widget { + let kind: String = WidgetKind.diningHours + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + DiningHoursWidgetEntryView(entries: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + DiningHoursWidgetEntryView(entries: entry) + .padding() + .background() + } + } + .configurationDisplayName("Dining Hours") + .description("Feast your eyes on feast times.") + .contentMarginsDisabled() + //.supportedFamilies([.systemMedium, .systemLarge]) + } +} +#Preview(as: .systemSmall) { + DiningHoursWidget() +} timeline: { + DiningEntries(date: .now, venues: []) +} diff --git a/Widget/WidgetBundle.swift b/Widget/WidgetBundle.swift index 73b48d86b..965980633 100644 --- a/Widget/WidgetBundle.swift +++ b/Widget/WidgetBundle.swift @@ -14,5 +14,6 @@ struct LabsWidgetBundle: WidgetBundle { var body: some Widget { DiningAnalyticsHomeWidget() CoursesDayWidget() + DiningHoursWidget() } } From 6bb98ba2758f07fbe80aa99e39e2a9fa377ffa08 Mon Sep 17 00:00:00 2001 From: George Botros <78520093+george-botros@users.noreply.github.com> Date: Sat, 7 Oct 2023 18:33:25 -0400 Subject: [PATCH 02/21] fix: Fixed small widget alignment --- Widget/Dining Hours/DiningHoursProvider.swift | 2 +- Widget/Dining Hours/DiningHoursWidget.swift | 52 +++++++++---------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/Widget/Dining Hours/DiningHoursProvider.swift b/Widget/Dining Hours/DiningHoursProvider.swift index 22f815a6c..71ee07811 100644 --- a/Widget/Dining Hours/DiningHoursProvider.swift +++ b/Widget/Dining Hours/DiningHoursProvider.swift @@ -37,7 +37,7 @@ struct Provider: TimelineProvider { } func getSnapshot(in context: Context, completion: @escaping (DiningEntries) -> ()) { - let entry = DiningEntries(date: .now, venues: []) + let entry = DiningEntries(date: .now, venues: DiningAPI.instance.getVenues(with: DiningAPI.defaultVenueIds)) completion(entry) } diff --git a/Widget/Dining Hours/DiningHoursWidget.swift b/Widget/Dining Hours/DiningHoursWidget.swift index e32c7dfd1..740876790 100644 --- a/Widget/Dining Hours/DiningHoursWidget.swift +++ b/Widget/Dining Hours/DiningHoursWidget.swift @@ -18,14 +18,14 @@ struct DiningHoursWidgetEntryView : View { Group { switch widgetFamily { case .systemLarge: - let venues = entries.venues.prefix(4) VStack { + let venues = entries.venues.prefix(4) ForEach(venues) { venue in Spacer() DiningVenueRow(for: venue, isWidget: true) Spacer() } - } + }.padding() case .systemMedium: VStack { @@ -33,12 +33,14 @@ struct DiningHoursWidgetEntryView : View { ForEach(venues) { venue in DiningVenueRow(for: venue, isWidget: true) } - } + }.padding() case .systemSmall: - let venue = entries.venues[0] - smallWidget(venue: venue) - + let venues = entries.venues.prefix(1) + ForEach(venues) { venue in + smallWidget(venue: venue) + } + default: Text("Unsupported") } @@ -46,23 +48,21 @@ struct DiningHoursWidgetEntryView : View { } private func smallWidget(venue: DiningVenue) -> some View { - ZStack { - KFImage(venue.image) - .resizable() - .scaledToFill() - .background(Color.grey1) - .overlay( - LinearGradient(gradient: Gradient(colors: [.clear, Color.grey6]), - startPoint: .top, - endPoint: .bottom) - ) - VStack(alignment: .leading) { - Spacer() - Spacer() - Spacer() - Spacer() - Spacer() - Spacer() + ZStack () { + GeometryReader { geo in + KFImage(venue.image) + .resizable() + .scaledToFill() + .frame(width: geo.size.width, height: geo.size.height) + .clipped() + .background(Color.grey1) + .overlay( + LinearGradient(gradient: Gradient(colors: [.clear, Color.grey6]), + startPoint: .top, + endPoint: .bottom) + ) + } + VStack (alignment: .leading) { Spacer() Label(venue.statusString, systemImage: venue.statusImageString) .labelStyle(VenueStatusLabelStyle()) @@ -75,10 +75,9 @@ struct DiningHoursWidgetEntryView : View { .font(.system(size: 15, weight: .medium)) .minimumScaleFactor(0.2) .lineLimit(1) - .padding(.leading, 10) - Spacer() + .padding([.leading, .trailing], 10) } - + .padding(.bottom, 10) } } @@ -110,7 +109,6 @@ struct DiningHoursWidget: Widget { .configurationDisplayName("Dining Hours") .description("Feast your eyes on feast times.") .contentMarginsDisabled() - //.supportedFamilies([.systemMedium, .systemLarge]) } } #Preview(as: .systemSmall) { From fbeefdbde6ffda20f38646a58efdda189289e45d Mon Sep 17 00:00:00 2001 From: George Botros <78520093+george-botros@users.noreply.github.com> Date: Tue, 10 Oct 2023 10:40:39 -0400 Subject: [PATCH 03/21] fix: Removed asynchronously loading images for widget --- PennMobileShared/Dining/DiningVenueRow.swift | 27 +++++--- .../Dining/Models/DiningVenue.swift | 2 + Widget/Dining Hours/DiningHoursProvider.swift | 31 +++++++++- Widget/Dining Hours/DiningHoursWidget.swift | 61 +++++++++++-------- 4 files changed, 85 insertions(+), 36 deletions(-) diff --git a/PennMobileShared/Dining/DiningVenueRow.swift b/PennMobileShared/Dining/DiningVenueRow.swift index a5dfe4ea5..f8274a5dd 100644 --- a/PennMobileShared/Dining/DiningVenueRow.swift +++ b/PennMobileShared/Dining/DiningVenueRow.swift @@ -21,13 +21,26 @@ public struct DiningVenueRow: View { public var body: some View { HStack(spacing: 13) { - KFImage(venue.image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 64) - .background(Color.grey1) - .clipShape(RoundedRectangle(cornerRadius: 7)) - + if isWidget { + if let localImageURL = venue.localImageURL, let uiImage = UIImage(contentsOfFile: localImageURL.path) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 64) + .background(Color.grey1) + .clipShape(RoundedRectangle(cornerRadius: 7)) + } + } else { + KFImage(venue.image) + .setProcessor( + DownsamplingImageProcessor(size: CGSize(width: 200, height: 128))) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 64) + .background(Color.grey1) + .clipShape(RoundedRectangle(cornerRadius: 7)) + } + VStack(alignment: .leading, spacing: 3) { Label(venue.statusString, systemImage: venue.statusImageString) .labelStyle(VenueStatusLabelStyle()) diff --git a/PennMobileShared/Dining/Models/DiningVenue.swift b/PennMobileShared/Dining/Models/DiningVenue.swift index 3fe68342e..370f559a0 100755 --- a/PennMobileShared/Dining/Models/DiningVenue.swift +++ b/PennMobileShared/Dining/Models/DiningVenue.swift @@ -39,6 +39,8 @@ public struct DiningVenue: Codable, Equatable, Identifiable { return .retail } } + + public var localImageURL: URL? } public struct Day: Codable, Equatable { diff --git a/Widget/Dining Hours/DiningHoursProvider.swift b/Widget/Dining Hours/DiningHoursProvider.swift index 71ee07811..ac4f6a635 100644 --- a/Widget/Dining Hours/DiningHoursProvider.swift +++ b/Widget/Dining Hours/DiningHoursProvider.swift @@ -42,8 +42,33 @@ struct Provider: TimelineProvider { } func getTimeline(in context: Context, completion: @escaping (Timeline>) -> ()) { - let venues: [DiningVenue] = getDiningPreferences() - let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .atEnd) - completion(timeline) + var venues: [DiningVenue] = getDiningPreferences() + + let dispatchGroup = DispatchGroup() + + for (index, venue) in venues.enumerated() { + dispatchGroup.enter() + if let imageURL = venue.image { + let task = URLSession.shared.dataTask(with: imageURL) { (data, _, _) in + if let data = data, let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let filename = directory.appendingPathComponent(UUID().uuidString) + try? data.write(to: filename) + venues[index].localImageURL = filename + } + dispatchGroup.leave() + } + task.resume() + } else { + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .atEnd) + completion(timeline) + } +// let venues: [DiningVenue] = getDiningPreferences() +// let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .atEnd) +// completion(timeline) } } diff --git a/Widget/Dining Hours/DiningHoursWidget.swift b/Widget/Dining Hours/DiningHoursWidget.swift index 740876790..20974fec1 100644 --- a/Widget/Dining Hours/DiningHoursWidget.swift +++ b/Widget/Dining Hours/DiningHoursWidget.swift @@ -49,35 +49,44 @@ struct DiningHoursWidgetEntryView : View { private func smallWidget(venue: DiningVenue) -> some View { ZStack () { - GeometryReader { geo in - KFImage(venue.image) - .resizable() - .scaledToFill() - .frame(width: geo.size.width, height: geo.size.height) - .clipped() - .background(Color.grey1) - .overlay( - LinearGradient(gradient: Gradient(colors: [.clear, Color.grey6]), - startPoint: .top, - endPoint: .bottom) - ) + + if let localImageURL = venue.localImageURL, let uiImage = UIImage(contentsOfFile: localImageURL.path) { + GeometryReader { geo in + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: geo.size.width, height: geo.size.height) + .clipped() + .background(Color.grey1) + .overlay( + LinearGradient(gradient: Gradient(colors: [.clear, Color.grey6]), + startPoint: .top, + endPoint: .bottom) + ) + } } - VStack (alignment: .leading) { - Spacer() - Label(venue.statusString, systemImage: venue.statusImageString) - .labelStyle(VenueStatusLabelStyle()) - .modifier(StatusColorModifier(for: venue)) - .minimumScaleFactor(0.2) - .lineLimit(1) - .padding(.leading, 10) + HStack { + VStack (alignment: .leading) { + Spacer() + Label(venue.statusString, systemImage: venue.statusImageString) + .labelStyle(VenueStatusLabelStyle()) + .modifier(StatusColorModifier(for: venue)) + .minimumScaleFactor(0.2) + .lineLimit(1) + .padding(.leading, 20) + .padding(.trailing, 5) + + Text(venue.name) + .font(.system(size: 15, weight: .medium)) + .minimumScaleFactor(0.2) + .lineLimit(1) + .padding(.leading, 20) + .padding(.trailing, 5) + } + .padding(.bottom, 10) - Text(venue.name) - .font(.system(size: 15, weight: .medium)) - .minimumScaleFactor(0.2) - .lineLimit(1) - .padding([.leading, .trailing], 10) + Spacer() } - .padding(.bottom, 10) } } From 4e26c4bdbdfbea3ac39e24fe9b54776c50583eca Mon Sep 17 00:00:00 2001 From: George Botros <78520093+george-botros@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:01:40 -0400 Subject: [PATCH 04/21] perf: Changed widget refresh policy to every 15 minutes --- Widget/Dining Hours/DiningHoursProvider.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Widget/Dining Hours/DiningHoursProvider.swift b/Widget/Dining Hours/DiningHoursProvider.swift index ac4f6a635..5c2e8940a 100644 --- a/Widget/Dining Hours/DiningHoursProvider.swift +++ b/Widget/Dining Hours/DiningHoursProvider.swift @@ -64,11 +64,10 @@ struct Provider: TimelineProvider { } dispatchGroup.notify(queue: .main) { - let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .atEnd) - completion(timeline) + if let nextDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) { + let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .after (nextDate)) + completion(timeline) + } } -// let venues: [DiningVenue] = getDiningPreferences() -// let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .atEnd) -// completion(timeline) } } From c3287fa2d33dc978c7954f5904900927d3394b45 Mon Sep 17 00:00:00 2001 From: DespicableMonkey Date: Tue, 17 Oct 2023 03:18:54 -0400 Subject: [PATCH 05/21] Add Small and Medium Fitness Widget --- PennMobile.xcodeproj/project.pbxproj | 48 ++- PennMobile/Home/Fitness/FitnessModel.swift | 53 ---- .../Home/Fitness/FitnessSettingsView.swift | 1 + PennMobile/Home/Fitness/FitnessView.swift | 1 + PennMobile/Supporting_Files/Info.plist | 1 + .../Fitness/FitnessAPI.swift | 15 +- .../Fitness/FitnessGraph.swift | 10 +- PennMobileShared/Fitness/FitnessModel.swift | 53 ++++ PennMobileShared/General/WidgetKind.swift | 6 + Shared/Intents.intentdefinition | 285 +++++++++++++++++- Widget/Fitness/FitnessProvider.swift | 95 ++++++ Widget/Fitness/FitnessWidget.swift | 238 +++++++++++++++ Widget/WidgetBundle.swift | 1 + 13 files changed, 728 insertions(+), 79 deletions(-) delete mode 100644 PennMobile/Home/Fitness/FitnessModel.swift rename {PennMobile/Home => PennMobileShared}/Fitness/FitnessAPI.swift (80%) rename {PennMobile/Home => PennMobileShared}/Fitness/FitnessGraph.swift (94%) create mode 100644 PennMobileShared/Fitness/FitnessModel.swift create mode 100644 Widget/Fitness/FitnessProvider.swift create mode 100644 Widget/Fitness/FitnessWidget.swift diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 97a4acb12..088ede06c 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 2138D559225998A300D67CA2 /* GSRGroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2138D558225998A300D67CA2 /* GSRGroupController.swift */; }; 2138D55F22599D4700D67CA2 /* GSRGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2138D55E22599D4700D67CA2 /* GSRGroup.swift */; }; 2138E1F82252AFB500E4055A /* GSRLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2138E1F72252AFB500E4055A /* GSRLocationCell.swift */; }; - 214C23B21F2FEE150004487C /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C23B11F2FEE150004487C /* Networking.swift */; }; 214E254922480818004CB9C4 /* GSRDeletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214E254822480818004CB9C4 /* GSRDeletable.swift */; }; 21508166220D2499002F7EA1 /* HomeNewsCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21508165220D2499002F7EA1 /* HomeNewsCellItem.swift */; }; 21508168220D24A1002F7EA1 /* HomeNewsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21508167220D24A1002F7EA1 /* HomeNewsCell.swift */; }; @@ -107,12 +106,10 @@ 21FBC240228514ED00B432D8 /* FeedAnalyticsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FBC23F228514ED00B432D8 /* FeedAnalyticsEngine.swift */; }; 21FBC242228B774E00B432D8 /* PennCashNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FBC241228B774E00B432D8 /* PennCashNetworkManager.swift */; }; 421B03CA29E22035003AE6DC /* FitnessRoomRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B03C929E22035003AE6DC /* FitnessRoomRow.swift */; }; - 421B03CE29E25E92003AE6DC /* FitnessGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B03CD29E25E92003AE6DC /* FitnessGraph.swift */; }; 422C896E2A0FE8C000A7135C /* FitnessSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422C896D2A0FE8C000A7135C /* FitnessSettingsView.swift */; }; 423B7EF52ACC67B900D30504 /* MeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 423B7EF42ACC67B900D30504 /* MeterView.swift */; }; 426A0B51299034910066C7B7 /* DiningAnalyticsGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426A0B4F299034910066C7B7 /* DiningAnalyticsGraph.swift */; }; 426A0B52299034910066C7B7 /* DiningAnalyticsGraphBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426A0B50299034910066C7B7 /* DiningAnalyticsGraphBox.swift */; }; - 42D9237129E0C69200E9E18E /* FitnessModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237029E0C69200E9E18E /* FitnessModel.swift */; }; 42D9237329E0C9AF00E9E18E /* FitnessViewControllerSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237229E0C9AF00E9E18E /* FitnessViewControllerSwiftUI.swift */; }; 42D9237529E0CCFB00E9E18E /* FitnessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237429E0CCFB00E9E18E /* FitnessView.swift */; }; 56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BED8AA4945D67F0ED89FA9B0 /* Pods_PennMobile.framework */; }; @@ -246,7 +243,6 @@ 97E79E072100DA1200D3D606 /* BuildingImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79E002100DA1200D3D606 /* BuildingImageCell.swift */; }; 97E79E082100DA1200D3D606 /* BuildingHoursCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79E012100DA1200D3D606 /* BuildingHoursCell.swift */; }; 97E79E092100DA1200D3D606 /* BuildingFoodMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79E022100DA1200D3D606 /* BuildingFoodMenuCell.swift */; }; - 97E79E162100E33100D3D606 /* FitnessAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79E152100E33100D3D606 /* FitnessAPI.swift */; }; AD220CD2B82D72B87DBF87BC /* libPods-AutomatedScreenshotUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 22D5D1D7F760F28A27C06241 /* libPods-AutomatedScreenshotUITests.a */; }; B6040A361F8F24D900E4B783 /* AddLaundryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6040A351F8F24D900E4B783 /* AddLaundryCell.swift */; }; B62875EB2115302200FB2873 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62875EA2115302200FB2873 /* MapViewController.swift */; }; @@ -271,6 +267,12 @@ C15C4B4E223EB16F00E443FD /* HomeReservationsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15C4B4D223EB16F00E443FD /* HomeReservationsCell.swift */; }; C15C4B50223EB18800E443FD /* HomeReservationsCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15C4B4F223EB18800E443FD /* HomeReservationsCellItem.swift */; }; C1D90F1D2220A25700DAB8EE /* NoReservationsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D90F1C2220A25700DAB8EE /* NoReservationsCell.swift */; }; + C312D4B12AD3129100EDB893 /* FitnessWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C312D4AF2AD3129100EDB893 /* FitnessWidget.swift */; }; + C312D4B22AD4CA0200EDB893 /* FitnessProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C312D4AD2AD3104100EDB893 /* FitnessProvider.swift */; }; + C312D4B62AD4D98100EDB893 /* FitnessModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237029E0C69200E9E18E /* FitnessModel.swift */; }; + C312D4B72AD4DB6600EDB893 /* FitnessAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E79E152100E33100D3D606 /* FitnessAPI.swift */; }; + C312D4B92AD4DC8900EDB893 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C23B11F2FEE150004487C /* Networking.swift */; }; + C31DA0A62ADE632A00C04210 /* FitnessGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B03CD29E25E92003AE6DC /* FitnessGraph.swift */; }; CF29A1751FB788820067D946 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF29A1651FB7887C0067D946 /* Page.swift */; }; CF29A1771FB788820067D946 /* OnboardingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF29A1671FB7887E0067D946 /* OnboardingController.swift */; }; CF29A1781FB788820067D946 /* PageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF29A1681FB7887E0067D946 /* PageCell.swift */; }; @@ -671,6 +673,8 @@ C15C4B4D223EB16F00E443FD /* HomeReservationsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeReservationsCell.swift; sourceTree = ""; }; C15C4B4F223EB18800E443FD /* HomeReservationsCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeReservationsCellItem.swift; sourceTree = ""; }; C1D90F1C2220A25700DAB8EE /* NoReservationsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoReservationsCell.swift; sourceTree = ""; }; + C312D4AD2AD3104100EDB893 /* FitnessProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitnessProvider.swift; sourceTree = ""; }; + C312D4AF2AD3129100EDB893 /* FitnessWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitnessWidget.swift; sourceTree = ""; }; CF29A1651FB7887C0067D946 /* Page.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; CF29A1671FB7887E0067D946 /* OnboardingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingController.swift; sourceTree = ""; }; CF29A1681FB7887E0067D946 /* PageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageCell.swift; sourceTree = ""; }; @@ -1224,10 +1228,10 @@ isa = PBXGroup; children = ( 6E903D372AC9C67000F2D384 /* ImageNetworkingManager.swift */, + 214C23B11F2FEE150004487C /* Networking.swift */, 216640CA1EBADCA400746B8E /* FirebaseAnalyticsManager.swift */, 21FBC23F228514ED00B432D8 /* FeedAnalyticsEngine.swift */, 21EB4D2F203D330C0029A460 /* UserDBManager.swift */, - 214C23B11F2FEE150004487C /* Networking.swift */, ); path = "Networking + Analytics"; sourceTree = ""; @@ -1471,6 +1475,7 @@ 6E4D82172AC8C91C009AB78E /* PennMobileShared */ = { isa = PBXGroup; children = ( + C312D4B52AD4D8E400EDB893 /* Fitness */, 6E903D392AC9C7E300F2D384 /* General */, 6E903D3A2AC9C80A00F2D384 /* Networking + Analytics */, 6E903D3B2AC9C81D00F2D384 /* Courses */, @@ -1542,6 +1547,7 @@ 8932538E290F98BD006EE62C /* ConfigurationRepresenting.swift */, 893253912910249B006EE62C /* WidgetBackgroundTypeExtensions.swift */, 890DDBC42AA2E499006815A3 /* ViewExtensions.swift */, + C312D4AC2AD0B8EB00EDB893 /* Fitness */, 89EA261F290EE39E008F26CF /* Courses */, 89CA727129171E2400CF72FE /* Dining */, 8932693F28FC75A6003D4BF9 /* Assets.xcassets */, @@ -1618,9 +1624,6 @@ 421B03C929E22035003AE6DC /* FitnessRoomRow.swift */, 42D9237229E0C9AF00E9E18E /* FitnessViewControllerSwiftUI.swift */, 42D9237429E0CCFB00E9E18E /* FitnessView.swift */, - 421B03CD29E25E92003AE6DC /* FitnessGraph.swift */, - 42D9237029E0C69200E9E18E /* FitnessModel.swift */, - 97E79E152100E33100D3D606 /* FitnessAPI.swift */, 422C896D2A0FE8C000A7135C /* FitnessSettingsView.swift */, ); name = Fitness; @@ -1762,6 +1765,25 @@ path = "GSR Reservations"; sourceTree = ""; }; + C312D4AC2AD0B8EB00EDB893 /* Fitness */ = { + isa = PBXGroup; + children = ( + C312D4AD2AD3104100EDB893 /* FitnessProvider.swift */, + C312D4AF2AD3129100EDB893 /* FitnessWidget.swift */, + ); + path = Fitness; + sourceTree = ""; + }; + C312D4B52AD4D8E400EDB893 /* Fitness */ = { + isa = PBXGroup; + children = ( + 421B03CD29E25E92003AE6DC /* FitnessGraph.swift */, + 42D9237029E0C69200E9E18E /* FitnessModel.swift */, + 97E79E152100E33100D3D606 /* FitnessAPI.swift */, + ); + path = Fitness; + sourceTree = ""; + }; CF29A15F1FB7873A0067D946 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -2288,9 +2310,7 @@ EF389EC421860B7000E29C6A /* StatusBar.swift in Sources */, 212304C72054EDB900958CE0 /* FlingViewController.swift in Sources */, 211DA1AB20490E4D0065BC2C /* HomeCellConformable.swift in Sources */, - 42D9237129E0C69200E9E18E /* FitnessModel.swift in Sources */, 2189C0AA2027CE4B00771C1F /* TextLayer.swift in Sources */, - 421B03CE29E25E92003AE6DC /* FitnessGraph.swift in Sources */, 6CAA4B4C27A7639700473CC6 /* PollsNetworkManager.swift in Sources */, 21640FD7204A5309008DB6E8 /* LaundryMachineCellTappable.swift in Sources */, 217A7838204D2E2B004F1227 /* HomeGSRCellItem.swift in Sources */, @@ -2431,7 +2451,6 @@ 97E79E042100DA1200D3D606 /* BuildingHeaderCell.swift in Sources */, 6CC88D5C27B1BF51006896F6 /* DiningBalanceView.swift in Sources */, B62875EB2115302200FB2873 /* MapViewController.swift in Sources */, - 214C23B21F2FEE150004487C /* Networking.swift in Sources */, 2189C0A92027CE4B00771C1F /* RangeSlider.swift in Sources */, F2562A262551F6D40021C92F /* CourseAlertSettingsController.swift in Sources */, 87FE6479290EE4BE00AFADF6 /* NotificationAPIModel.swift in Sources */, @@ -2484,6 +2503,7 @@ C15C4B4E223EB16F00E443FD /* HomeReservationsCell.swift in Sources */, B62875F92118F95300FB2873 /* BuildingProtocol.swift in Sources */, F212BE8623B6DA8D00ED46A1 /* PrivacyTableViewCell.swift in Sources */, + C312D4B92AD4DC8900EDB893 /* Networking.swift in Sources */, 426A0B51299034910066C7B7 /* DiningAnalyticsGraph.swift in Sources */, 97AA806C23D26BC700C23488 /* DiningCell.swift in Sources */, 6C6FE1CC27B9B8CB0093FD13 /* PacCodeNetworkManager.swift in Sources */, @@ -2494,7 +2514,6 @@ F2770ADB2545E2EB001EA1DD /* CourseAlertCell.swift in Sources */, 6CC88D6C27B1BF51006896F6 /* DiningVenueRow.swift in Sources */, 6C6FE1D527B9B8CB0093FD13 /* HeaderViewCell.swift in Sources */, - 97E79E162100E33100D3D606 /* FitnessAPI.swift in Sources */, 6C6FE1D627B9B8CB0093FD13 /* AboutViewController.swift in Sources */, 21B556192224FDF700D80F61 /* SplashViewController.swift in Sources */, B650C5641FA43FC600922E98 /* LaundryRoom.swift in Sources */, @@ -2550,9 +2569,12 @@ 6E5159EC2AC8C9D5004B3F41 /* Extensions.swift in Sources */, 6E5159F02AC8C9D5004B3F41 /* DiningMenu.swift in Sources */, 6E5159E52AC8C9D5004B3F41 /* NetworkingError.swift in Sources */, + C31DA0A62ADE632A00C04210 /* FitnessGraph.swift in Sources */, + C312D4B72AD4DB6600EDB893 /* FitnessAPI.swift in Sources */, 6E5159E82AC8C9D5004B3F41 /* DiningAnalyticsViewModel.swift in Sources */, 6E5159EB2AC8C9D5004B3F41 /* DiningPlan.swift in Sources */, 6E5159E72AC8C9D5004B3F41 /* DiningVenue+Extensions.swift in Sources */, + C312D4B62AD4D98100EDB893 /* FitnessModel.swift in Sources */, 6E5159F22AC8C9D5004B3F41 /* DiningToken.swift in Sources */, 6E5159ED2AC8C9D5004B3F41 /* DiningVenue.swift in Sources */, 423B7EF52ACC67B900D30504 /* MeterView.swift in Sources */, @@ -2573,7 +2595,9 @@ 89325393291025A8006EE62C /* WidgetBackgroundTypeExtensions.swift in Sources */, 89EA2622290EE3FD008F26CF /* CoursesProvider.swift in Sources */, 890DDBC62AA2E4B6006815A3 /* ViewExtensions.swift in Sources */, + C312D4B12AD3129100EDB893 /* FitnessWidget.swift in Sources */, 89CA72952917541C00CF72FE /* DiningAnalyticsHomeWidget.swift in Sources */, + C312D4B22AD4CA0200EDB893 /* FitnessProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PennMobile/Home/Fitness/FitnessModel.swift b/PennMobile/Home/Fitness/FitnessModel.swift deleted file mode 100644 index a565e1d2b..000000000 --- a/PennMobile/Home/Fitness/FitnessModel.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// FacilityModel.swift -// PennMobile -// -// Created by Jordan H on 4/7/23. -// Copyright © 2023 PennLabs. All rights reserved. -// - -import Foundation - -struct FitnessRoom: Codable, Equatable, Identifiable { - let id: Int - let name: String - let image_url: URL? - let last_updated: Date // "2023-04-07T12:32:34-04:00" - let count: Int - let capacity: Double - let open: [String] - let close: [String] - var data: FitnessRoomData? -} - -struct FitnessRoomData: Codable, Equatable { - let name: String - let start: String - let end: String - let usage: [String: Double] - - enum CodingKeys: String, CodingKey { - case name = "room_name" - case start = "start_date" - case end = "end_date" - case usage - } - - var usageHours: [DataHour] { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH" - - return usage.sorted(by: { $0.key < $1.key }).map { key, value in - guard let date = dateFormatter.date(from: "\(end) \(key)") else { - fatalError("Invalid date format") - } - return DataHour(date: date, value: value) - } - } -} - -struct DataHour: Identifiable { - let date: Date - let value: Double - var id: Date {date} -} diff --git a/PennMobile/Home/Fitness/FitnessSettingsView.swift b/PennMobile/Home/Fitness/FitnessSettingsView.swift index 71e801481..f54fbe129 100644 --- a/PennMobile/Home/Fitness/FitnessSettingsView.swift +++ b/PennMobile/Home/Fitness/FitnessSettingsView.swift @@ -8,6 +8,7 @@ import SwiftUI import Kingfisher +import PennMobileShared struct FitnessSelectView: View { @Binding var showFitnessSettings: Bool diff --git a/PennMobile/Home/Fitness/FitnessView.swift b/PennMobile/Home/Fitness/FitnessView.swift index 22a905b68..6c79f2bf4 100644 --- a/PennMobile/Home/Fitness/FitnessView.swift +++ b/PennMobile/Home/Fitness/FitnessView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import PennMobileShared struct FitnessView: View { @State var rooms: [FitnessRoom] = [] diff --git a/PennMobile/Supporting_Files/Info.plist b/PennMobile/Supporting_Files/Info.plist index afa368f83..fb4d45a12 100755 --- a/PennMobile/Supporting_Files/Info.plist +++ b/PennMobile/Supporting_Files/Info.plist @@ -59,6 +59,7 @@ ConfigureCoursesDayWidgetIntent ConfigureDiningAnalyticsHomeWidgetIntent + ConfigureFitnessWidgetIntent UIBackgroundModes diff --git a/PennMobile/Home/Fitness/FitnessAPI.swift b/PennMobileShared/Fitness/FitnessAPI.swift similarity index 80% rename from PennMobile/Home/Fitness/FitnessAPI.swift rename to PennMobileShared/Fitness/FitnessAPI.swift index 2cc84dec8..1bd210e01 100755 --- a/PennMobile/Home/Fitness/FitnessAPI.swift +++ b/PennMobileShared/Fitness/FitnessAPI.swift @@ -8,16 +8,15 @@ import Foundation import SwiftyJSON -import PennMobileShared -class FitnessAPI: Requestable { +public class FitnessAPI { - static let instance = FitnessAPI() + public static let instance = FitnessAPI() - let fitnessRoomsUrl = "https://pennmobile.org/api/penndata/fitness/rooms/" - let fitnessDetailUrl = "https://pennmobile.org/api/penndata/fitness/usage/" // + room ID + public let fitnessRoomsUrl = "https://pennmobile.org/api/penndata/fitness/rooms/" + public let fitnessDetailUrl = "https://pennmobile.org/api/penndata/fitness/usage/" // + room ID - func fetchFitnessRooms() async -> Result<[FitnessRoom], NetworkingError> { + public func fetchFitnessRooms() async -> Result<[FitnessRoom], NetworkingError> { guard let (data, _) = try? await URLSession.shared.data(from: URL(string: fitnessRoomsUrl)!) else { return .failure(.serverError) } @@ -33,7 +32,7 @@ class FitnessAPI: Requestable { } } - func fetchFitnessPastData(roomID: Int, date: Date = Date(), num_samples: Int = 3, group_by: String = "week", field: String = "count") async -> Result { + public func fetchFitnessPastData(roomID: Int, date: Date = Date(), num_samples: Int = 3, group_by: String = "week", field: String = "count") async -> Result { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" let dateString = dateFormatter.string(from: date) @@ -51,7 +50,7 @@ class FitnessAPI: Requestable { } } - func fetchFitnessRoomsWithData(rooms: [FitnessRoom]) async -> Result<[FitnessRoom], NetworkingError> { + public func fetchFitnessRoomsWithData(rooms: [FitnessRoom]) async -> Result<[FitnessRoom], NetworkingError> { return await withTaskGroup(of: FitnessRoom.self) { group in var updatedRooms = [FitnessRoom]() updatedRooms.reserveCapacity(rooms.count) diff --git a/PennMobile/Home/Fitness/FitnessGraph.swift b/PennMobileShared/Fitness/FitnessGraph.swift similarity index 94% rename from PennMobile/Home/Fitness/FitnessGraph.swift rename to PennMobileShared/Fitness/FitnessGraph.swift index 83cca8b26..0b3300ed9 100644 --- a/PennMobile/Home/Fitness/FitnessGraph.swift +++ b/PennMobileShared/Fitness/FitnessGraph.swift @@ -9,10 +9,14 @@ import SwiftUI import Charts -struct FitnessGraph: View { +public struct FitnessGraph: View { + private let graphHeight: CGFloat = 100.0 private let padding: CGFloat = 10.0 - var room: FitnessRoom + public var room: FitnessRoom + + public init(room: FitnessRoom) { self.room = room } + var hours: (Date, Date) { let calendar = Calendar.current let currentDate = Date() @@ -30,7 +34,7 @@ struct FitnessGraph: View { return (openDate, closeDate) } - var body: some View { + public var body: some View { if #available(iOS 16.0, *) { Chart { ForEach(room.data?.usageHours ?? []) { diff --git a/PennMobileShared/Fitness/FitnessModel.swift b/PennMobileShared/Fitness/FitnessModel.swift new file mode 100644 index 000000000..8f1b1ecec --- /dev/null +++ b/PennMobileShared/Fitness/FitnessModel.swift @@ -0,0 +1,53 @@ +// +// FacilityModel.swift +// PennMobile +// +// Created by Jordan H on 4/7/23. +// Copyright © 2023 PennLabs. All rights reserved. +// + +import Foundation + +public struct FitnessRoom: Codable, Equatable, Identifiable { + public let id: Int + public let name: String + public let image_url: URL? + public let last_updated: Date // "2023-04-07T12:32:34-04:00" + public let count: Int + public let capacity: Double + public let open: [String] + public let close: [String] + public var data: FitnessRoomData? +} + +public struct FitnessRoomData: Codable, Equatable { + public let name: String + public let start: String + public let end: String + public let usage: [String: Double] + + public enum CodingKeys: String, CodingKey { + case name = "room_name" + case start = "start_date" + case end = "end_date" + case usage + } + + public var usageHours: [DataHour] { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH" + + return usage.sorted(by: { $0.key < $1.key }).map { key, value in + guard let date = dateFormatter.date(from: "\(end) \(key)") else { + fatalError("Invalid date format") + } + return DataHour(date: date, value: value) + } + } +} + +public struct DataHour: Identifiable { + public let date: Date + public let value: Double + public var id: Date {date} +} diff --git a/PennMobileShared/General/WidgetKind.swift b/PennMobileShared/General/WidgetKind.swift index 2892f7786..e0e7a3e52 100644 --- a/PennMobileShared/General/WidgetKind.swift +++ b/PennMobileShared/General/WidgetKind.swift @@ -18,4 +18,10 @@ public enum WidgetKind { public static let diningAnalyticsWidgets = [ diningAnalyticsHome ] + + public static let fitnessHome = "org.pennlabs.PennMobile.widgets.fitness.home" + + public static let fitnessHomeWidgets = [ + fitnessHome + ] } diff --git a/Shared/Intents.intentdefinition b/Shared/Intents.intentdefinition index b61502e39..75f62da3d 100644 --- a/Shared/Intents.intentdefinition +++ b/Shared/Intents.intentdefinition @@ -163,17 +163,130 @@ + + INEnumDisplayName + Fitness Chosen Complex + INEnumDisplayNameID + ytIMeQ + INEnumGeneratesHeader + + INEnumName + FitnessChosenComplex + INEnumType + Regular + INEnumValues + + + INEnumValueDisplayName + Default + INEnumValueDisplayNameID + E1WcWt + INEnumValueName + unknown + + + INEnumValueDisplayName + 1st Floor + INEnumValueDisplayNameID + E9lSer + INEnumValueIndex + 7 + INEnumValueName + First_Floor + + + INEnumValueDisplayName + 2nd Floor + INEnumValueDisplayNameID + SAmoKQ + INEnumValueIndex + 3 + INEnumValueName + Second_Floor + + + INEnumValueDisplayName + 3rd Floor + INEnumValueDisplayNameID + rgnCrJ + INEnumValueIndex + 2 + INEnumValueName + Third_Floor + + + INEnumValueDisplayName + 4th Floor + INEnumValueDisplayNameID + ll5XPi + INEnumValueIndex + 1 + INEnumValueName + Fourth_Floor + + + INEnumValueDisplayName + Basketball Courts + INEnumValueDisplayNameID + 47oEhb + INEnumValueIndex + 4 + INEnumValueName + Basketball_Courts + + + INEnumValueDisplayName + Climbing Wall + INEnumValueDisplayNameID + 1SIech + INEnumValueIndex + 6 + INEnumValueName + Climbing_Wall + + + INEnumValueDisplayName + MPR + INEnumValueDisplayNameID + T2yoES + INEnumValueIndex + 5 + INEnumValueName + MPR + + + INEnumValueDisplayName + Pool-Deep + INEnumValueDisplayNameID + FRz52G + INEnumValueIndex + 9 + INEnumValueName + Pool_Deep + + + INEnumValueDisplayName + Pool-Shallow + INEnumValueDisplayNameID + hqw52y + INEnumValueIndex + 8 + INEnumValueName + Pool_Shallow + + + INIntentDefinitionModelVersion 1.2 INIntentDefinitionNamespace F14SzM INIntentDefinitionSystemVersion - 22C5050e + 23A344 INIntentDefinitionToolsBuildVersion - 14B47b + 15A240d INIntentDefinitionToolsVersion - 14.1 + 15.0 INIntents @@ -518,6 +631,172 @@ INIntentVerb View + + INIntentCategory + information + INIntentDescription + Change settings for the fitness widget. + INIntentDescriptionID + xwKrk1 + INIntentEligibleForWidgets + + INIntentIneligibleForSuggestions + + INIntentLastParameterTag + 4 + INIntentName + ConfigureFitnessWidget + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Background + INIntentParameterDisplayNameID + 4GC5PW + INIntentParameterDisplayPriority + 1 + INIntentParameterEnumType + WidgetBackgroundType + INIntentParameterEnumTypeNamespace + F14SzM + INIntentParameterMetadata + + INIntentParameterMetadataDefaultValue + whiteGray + + INIntentParameterName + background + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${background}’. + INIntentParameterPromptDialogFormatStringID + 030GZO + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${background}’? + INIntentParameterPromptDialogFormatStringID + WfRAor + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 2 + INIntentParameterType + Integer + + + INIntentParameterConfigurable + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Room + INIntentParameterDisplayNameID + 3Pzxc3 + INIntentParameterDisplayPriority + 2 + INIntentParameterEnumType + FitnessChosenComplex + INIntentParameterEnumTypeNamespace + F14SzM + INIntentParameterName + complex + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${complex}’. + INIntentParameterPromptDialogFormatStringID + Db1GyA + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${complex}’? + INIntentParameterPromptDialogFormatStringID + HudvfH + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 4 + INIntentParameterType + Integer + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Configure Fitness Widget + INIntentTitleID + 7f3Z28 + INIntentType + Custom + INIntentVerb + View + INTypes diff --git a/Widget/Fitness/FitnessProvider.swift b/Widget/Fitness/FitnessProvider.swift new file mode 100644 index 000000000..99a0b6c7b --- /dev/null +++ b/Widget/Fitness/FitnessProvider.swift @@ -0,0 +1,95 @@ +// +// FitnessProvider.swift +// PennMobile +// +// Created by Pulkith Paruchuri on 10/8/23. +// Copyright © 2023 PennLabs. All rights reserved. +// + +import Foundation +import WidgetKit +import Intents +import PennMobileShared + +let intentIDToRoomID = [0:0,1:7,2:3,3:2,4:1,5:4,6:6,7:5,8:9,9:8] + +struct FitnessEntry: TimelineEntry { + let date: Date + let rooms: [FitnessRoom]? + let configuration: Configuration +} + +extension FitnessEntry where Configuration == Void { + init(date: Date, rooms: [FitnessRoom]?) { + self.init(date: date, rooms: rooms, configuration: ()) + } +} + +private var cachedRoomData: [FitnessRoom]? +private let cacheAge: TimeInterval = 10 * 60 +private var lastFetchDate: Date? +private var refreshTask: Task? + +private func refresh(roomID: Int) async { + let modelRefreshTask = Task { + do { + let fitnessData = await FitnessAPI.instance.fetchFitnessRooms() + let fitnessRooms = try fitnessData.get() + let selectedRoom = fitnessRooms.filter {room in + room.id == roomID + } + if(selectedRoom.count > 0) { + cachedRoomData = selectedRoom + return selectedRoom + } else { + return cachedRoomData ?? [] + } + } catch let error { + print("Couldn't fetch fitness data: \(error)") + return cachedRoomData ?? [] + } + } + let fitnessData : [FitnessRoom] = await modelRefreshTask.value + cachedRoomData = fitnessData +} + +private func snapshot(configuration: ConfigureFitnessWidgetIntent, roomID: Int) async -> FitnessEntry { + if refreshTask == nil + || cachedRoomData == nil || (cachedRoomData!).count == 0 + || (lastFetchDate != nil && Date().timeIntervalSince(lastFetchDate!) > cacheAge) + || (cachedRoomData!)[0].id != roomID { + refreshTask = Task { + lastFetchDate = Date() + await refresh(roomID: roomID) + } + } + await refreshTask?.value + + return FitnessEntry(date: Date(), rooms: cachedRoomData, configuration: configuration) +} + +private func timeline(configuration: Configuration, roomID: Int) async -> Timeline> { + await Timeline(entries: [snapshot(configuration: configuration, roomID: roomID)], policy: .after(Calendar.current.date(byAdding: .minute, value: 10, to: Date())!)) +} + +struct IntentFitnessProvider: IntentTimelineProvider { + let placeholderConfiguration: Intent.Configuration + + func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (FitnessEntry) -> Void) { + Task { + let roomID = intent.configuration.complex.rawValue //ID of Fitness Complex in backend should match "Index" field of Fitness Complex in Intents Enum + completion(await snapshot(configuration: intent.configuration, roomID: roomID)) + } + } + + func getTimeline(for intent: Intent, in context: Context, completion: @escaping (Timeline>) -> Void) { + Task { + let roomID = intent.configuration.complex.rawValue + completion(await timeline(configuration: intent.configuration, roomID: roomID)) + } + } + + func placeholder(in context: Context) -> FitnessEntry { + FitnessEntry(date: Date(), rooms: nil, configuration: placeholderConfiguration) + } +} diff --git a/Widget/Fitness/FitnessWidget.swift b/Widget/Fitness/FitnessWidget.swift new file mode 100644 index 000000000..8c09ad25b --- /dev/null +++ b/Widget/Fitness/FitnessWidget.swift @@ -0,0 +1,238 @@ +// +// FitnessWidget.swift +// PennMobile +// +// Created by Pulkith Paruchuri on 10/8/23. +// Copyright © 2023 PennLabs. All rights reserved. +// + +import Foundation +import SwiftUI +import PennMobileShared +import WidgetKit + +extension ConfigureFitnessWidgetIntent: ConfigurationRepresenting { + struct Configuration { + let background: WidgetBackgroundType + let complex: FitnessChosenComplex + } + + var configuration: Configuration { + return Configuration(background: background, complex: complex) + } +} + +/* Code from FitnessRoomRow */ +private func lastUpdatedFormattedTime (date: Date) -> String { + let interval = -date.timeIntervalSinceNow + let hours = Int(interval / 3600) + let minutes = Int((interval - 3600 * Double(hours)) / 60) + return "Updated \(hours)h \(minutes)m ago" +} + +private func getBusyString(room: FitnessRoom) -> String { + let hours = getHours(room: room) + let date = Date() + if date < hours.0 || date > hours.1 { + return "" + } else if room.capacity == 0.0 { + return "Empty" + } else if room.capacity < 10.0 { + return "Not very busy" + } else if room.capacity < 30.0 { + return "Slightly busy" + } else if room.capacity < 60.0 { + return "Pretty busy" + } else if room.capacity < 90.0 { + return "Extremely busy" + } else { + return "Packed" + } +} + +private func getOpenString(room: FitnessRoom, showbusystring: Bool) -> String { + let hours = getHours(room: room) + let date = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "h:mm a" + + if(date < hours.0) { + return "Closed • Opens at " + dateFormatter.string(from: hours.0).replacingOccurrences(of: ":00", with: "") + } else if (hours.0 <= date && date <= hours.1) { + return "Open until " + dateFormatter.string(from: hours.1).replacingOccurrences(of: ":00", with: "") + ((showbusystring) ? (" • " + getBusyString(room: room)) : "") + } else { + return "Closed" + } +} + +private func getHours (room: FitnessRoom) -> (Date, Date) { + let weekdayIndex: Int = (Calendar.current.component(.weekday, from: Date()) + 5) % 7 + var hours: (Date, Date) { + let calendar = Calendar.current + let currentDate = Date() + let timeFormatter = DateFormatter() + timeFormatter.dateFormat = "HH:mm:ss" + let openTime = timeFormatter.date(from: room.open[weekdayIndex])! + let closeTime = timeFormatter.date(from: room.close[weekdayIndex])! + + let openDate = calendar.date(bySettingHour: openTime.hour, minute: openTime.minutes, second: 0, of: currentDate)! + let closeDate = calendar.date(bySettingHour: closeTime.hour, minute: closeTime.minutes, second: 0, of: currentDate)! + return (openDate, closeDate) + } + return hours +} + +private func isOpen (room: FitnessRoom) -> Bool { + let date = Date() + let hours = getHours(room: room) + return hours.0 <= date && date <= hours.1 +} + + + +private struct FitnessWidgetSmallView: View { + var rooms: [FitnessRoom] + var configuration: ConfigureFitnessWidgetIntent.Configuration + + + var body: some View { + VStack { + Text(rooms[0].name) + .font(.system(size: 13, weight: .semibold)) + .padding(.top) + Text(getOpenString(room: rooms[0], showbusystring: false)) + .font(.system(size: 11)) + .foregroundStyle(isOpen(room: rooms[0]) ? .green : .blue) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + MeterView(current: isOpen(room: rooms[0]) ? rooms[0].capacity : 0, maximum: 100.0, style: Color.blue, lineWidth: 6) { + VStack { + Text("\(isOpen(room: rooms[0]) ? rooms[0].capacity : 0, specifier: "%.2f")%") + .fontWeight(.bold) + Text("capacity") + .font(.system(size: 10, weight: .light, design: .default)) + + } + } + .frame(width: 90, height: 90) + + Text(lastUpdatedFormattedTime(date: rooms[0].last_updated)) + .font(.system(size: 9)) + .foregroundStyle(.gray) + + Spacer() + }.widgetPadding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + +} + +private struct FitnessWidgetMediumView: View { + var rooms: [FitnessRoom] + var configuration: ConfigureFitnessWidgetIntent.Configuration + + var body: some View { + VStack { + HStack { + Text(rooms[0].name) + .font(.system(size: 12, weight: .semibold)) + .padding(.top) + + Text("") + + Text(getOpenString(room: rooms[0], showbusystring: true)) + .font(.system(size: 12)) + .foregroundStyle(isOpen(room: rooms[0]) ? .green : .blue) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top) + } + .padding([.trailing, .leading], 10) + HStack { + VStack { + MeterView(current: isOpen(room: rooms[0]) ? rooms[0].capacity : 0, maximum: 100.0, style: Color.blue, lineWidth: 6) { + VStack { + Text("\(isOpen(room: rooms[0]) ? rooms[0].capacity : 0, specifier: "%.2f")%") + .fontWeight(.bold) + Text("capacity") + .font(.system(size: 10, weight: .light, design: .default)) + + } + } + .frame(width: 90, height: 90) + + Text(lastUpdatedFormattedTime(date: rooms[0].last_updated)) + .font(.system(size: 9)) + .foregroundStyle(.gray) + + + + Spacer() + } + .padding(.trailing, 7) + + FitnessGraph(room: rooms[0]) + + Spacer() + } + .padding([.trailing, .leading], 10) + }.widgetPadding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +private struct FitnessHomeWidgetView: View { + var entry: FitnessEntry + + @Environment(\.widgetFamily) var widgetFamily + + var body: some View { + Group { + if (entry.configuration.complex.rawValue == 0) { + (Text("To use this widget, choose a room by ") + + Text("touching and holding the widget") + .fontWeight(.bold) + + Text(", then clicking ") + + Text("Edit Widget. ") + .fontWeight(.bold) + ).multilineTextAlignment(.center).widgetPadding() + .fixedSize(horizontal: false, vertical: true) + .padding() + .font(.system(size: 12)) + } + else if (entry.rooms == nil || entry.rooms?.count == 0) { + Text("Error fetching fitness room data.") + .multilineTextAlignment(.center).widgetPadding() + .fixedSize(horizontal: false, vertical: true) + .padding() + .font(.system(size: 15)) + } + else { + let rooms = entry.rooms! + switch widgetFamily { + case .systemSmall: FitnessWidgetSmallView(rooms: rooms, configuration: entry.configuration) + case .systemMedium: FitnessWidgetMediumView(rooms: rooms, configuration: entry.configuration) + default: FitnessWidgetSmallView(rooms: rooms, configuration: entry.configuration) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .widgetBackground(entry.configuration.background) + } + +} + + + +struct FitnessHomeWidget: Widget { + var body: some WidgetConfiguration { + let provider = IntentFitnessProvider(placeholderConfiguration: .init(background: .unknown, complex: .unknown)) + return IntentConfiguration(kind: WidgetKind.fitnessHome, intent: ConfigureFitnessWidgetIntent.self, provider: provider) { entry in + FitnessHomeWidgetView(entry: entry) + } + .configurationDisplayName("Fitness Information") + .description("Fitness room information, at a glance.") + .supportedFamilies([.systemSmall, .systemMedium]) + .contentMarginsDisabled() + } +} diff --git a/Widget/WidgetBundle.swift b/Widget/WidgetBundle.swift index 73b48d86b..aee25852c 100644 --- a/Widget/WidgetBundle.swift +++ b/Widget/WidgetBundle.swift @@ -14,5 +14,6 @@ struct LabsWidgetBundle: WidgetBundle { var body: some Widget { DiningAnalyticsHomeWidget() CoursesDayWidget() + FitnessHomeWidget() } } From adea7500e383ca46a012311e6256f34c512470c1 Mon Sep 17 00:00:00 2001 From: DespicableMonkey Date: Fri, 20 Oct 2023 17:17:45 -0400 Subject: [PATCH 06/21] Fix Fitness Widget Graph Not Showing Up --- Widget/Fitness/FitnessProvider.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Widget/Fitness/FitnessProvider.swift b/Widget/Fitness/FitnessProvider.swift index 99a0b6c7b..55661a738 100644 --- a/Widget/Fitness/FitnessProvider.swift +++ b/Widget/Fitness/FitnessProvider.swift @@ -39,8 +39,13 @@ private func refresh(roomID: Int) async { room.id == roomID } if(selectedRoom.count > 0) { - cachedRoomData = selectedRoom - return selectedRoom + switch await FitnessAPI.instance.fetchFitnessRoomsWithData(rooms: selectedRoom) { + case .failure: + return selectedRoom + case .success(let updatedRooms): + cachedRoomData = updatedRooms + return updatedRooms + } } else { return cachedRoomData ?? [] } From 2ee609e1d4258a2364b32489d539c8e6008c2cc2 Mon Sep 17 00:00:00 2001 From: DespicableMonkey Date: Fri, 27 Oct 2023 16:44:14 -0400 Subject: [PATCH 07/21] Add Text Color and Other Small Changes --- PennMobileShared/Fitness/FitnessModel.swift | 2 +- Widget/Fitness/FitnessProvider.swift | 50 ++++++----- Widget/Fitness/FitnessWidget.swift | 96 +++++++++++---------- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/PennMobileShared/Fitness/FitnessModel.swift b/PennMobileShared/Fitness/FitnessModel.swift index 8f1b1ecec..0f38e6806 100644 --- a/PennMobileShared/Fitness/FitnessModel.swift +++ b/PennMobileShared/Fitness/FitnessModel.swift @@ -14,7 +14,7 @@ public struct FitnessRoom: Codable, Equatable, Identifiable { public let image_url: URL? public let last_updated: Date // "2023-04-07T12:32:34-04:00" public let count: Int - public let capacity: Double + public var capacity: Double public let open: [String] public let close: [String] public var data: FitnessRoomData? diff --git a/Widget/Fitness/FitnessProvider.swift b/Widget/Fitness/FitnessProvider.swift index 55661a738..ac153c1ed 100644 --- a/Widget/Fitness/FitnessProvider.swift +++ b/Widget/Fitness/FitnessProvider.swift @@ -11,27 +11,27 @@ import WidgetKit import Intents import PennMobileShared -let intentIDToRoomID = [0:0,1:7,2:3,3:2,4:1,5:4,6:6,7:5,8:9,9:8] - struct FitnessEntry: TimelineEntry { let date: Date - let rooms: [FitnessRoom]? + let room: FitnessRoom? let configuration: Configuration } extension FitnessEntry where Configuration == Void { - init(date: Date, rooms: [FitnessRoom]?) { - self.init(date: date, rooms: rooms, configuration: ()) + init(date: Date, room: FitnessRoom?) { + self.init(date: date, room: room, configuration: ()) } } -private var cachedRoomData: [FitnessRoom]? +private var cachedRoomData: FitnessRoom? +private var placeHolderRoom: FitnessRoom? + private let cacheAge: TimeInterval = 10 * 60 private var lastFetchDate: Date? private var refreshTask: Task? -private func refresh(roomID: Int) async { - let modelRefreshTask = Task { +private func getRoom(roomID: Int) async -> FitnessRoom? { + let modelRefreshTask : Task = Task { do { let fitnessData = await FitnessAPI.instance.fetchFitnessRooms() let fitnessRooms = try fitnessData.get() @@ -41,36 +41,46 @@ private func refresh(roomID: Int) async { if(selectedRoom.count > 0) { switch await FitnessAPI.instance.fetchFitnessRoomsWithData(rooms: selectedRoom) { case .failure: - return selectedRoom + return nil case .success(let updatedRooms): - cachedRoomData = updatedRooms - return updatedRooms + return updatedRooms[0] } } else { - return cachedRoomData ?? [] + return nil } } catch let error { print("Couldn't fetch fitness data: \(error)") - return cachedRoomData ?? [] + return nil } } - let fitnessData : [FitnessRoom] = await modelRefreshTask.value - cachedRoomData = fitnessData + let fitnessData : FitnessRoom? = await modelRefreshTask.value + return fitnessData +} + +private func refresh(roomID: Int) async { + let fitnessData : FitnessRoom? = await getRoom(roomID: roomID) + cachedRoomData = fitnessData ?? nil } +private func updatePlaceHolder(roomID: Int) async { + let fitnessData : FitnessRoom? = await getRoom(roomID: roomID) + placeHolderRoom = fitnessData ?? nil +} + + + private func snapshot(configuration: ConfigureFitnessWidgetIntent, roomID: Int) async -> FitnessEntry { if refreshTask == nil - || cachedRoomData == nil || (cachedRoomData!).count == 0 + || cachedRoomData == nil || (lastFetchDate != nil && Date().timeIntervalSince(lastFetchDate!) > cacheAge) - || (cachedRoomData!)[0].id != roomID { + || (cachedRoomData!).id != roomID { refreshTask = Task { lastFetchDate = Date() await refresh(roomID: roomID) } } await refreshTask?.value - - return FitnessEntry(date: Date(), rooms: cachedRoomData, configuration: configuration) + return FitnessEntry(date: Date(), room: cachedRoomData, configuration: configuration) } private func timeline(configuration: Configuration, roomID: Int) async -> Timeline> { @@ -95,6 +105,6 @@ struct IntentFitnessProvider: IntentTimeli } func placeholder(in context: Context) -> FitnessEntry { - FitnessEntry(date: Date(), rooms: nil, configuration: placeholderConfiguration) + return FitnessEntry(date: Date(), room: placeHolderRoom, configuration: placeholderConfiguration) } } diff --git a/Widget/Fitness/FitnessWidget.swift b/Widget/Fitness/FitnessWidget.swift index 8c09ad25b..e8a7ea795 100644 --- a/Widget/Fitness/FitnessWidget.swift +++ b/Widget/Fitness/FitnessWidget.swift @@ -27,6 +27,9 @@ private func lastUpdatedFormattedTime (date: Date) -> String { let interval = -date.timeIntervalSinceNow let hours = Int(interval / 3600) let minutes = Int((interval - 3600 * Double(hours)) / 60) + if hours == 0 { + return "Updated \(minutes)m ago" + } return "Updated \(hours)h \(minutes)m ago" } @@ -91,91 +94,92 @@ private func isOpen (room: FitnessRoom) -> Bool { private struct FitnessWidgetSmallView: View { - var rooms: [FitnessRoom] + var room: FitnessRoom var configuration: ConfigureFitnessWidgetIntent.Configuration var body: some View { VStack { - Text(rooms[0].name) + Text(room.name) .font(.system(size: 13, weight: .semibold)) .padding(.top) - Text(getOpenString(room: rooms[0], showbusystring: false)) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + Text(getOpenString(room: room, showbusystring: false)) .font(.system(size: 11)) - .foregroundStyle(isOpen(room: rooms[0]) ? .green : .blue) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : (isOpen(room: room) ? .green : .blue)) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) - MeterView(current: isOpen(room: rooms[0]) ? rooms[0].capacity : 0, maximum: 100.0, style: Color.blue, lineWidth: 6) { + MeterView(current: isOpen(room: room) ? room.capacity : 0, maximum: 100.0, style: (configuration.background.prefersGrayscaleContent ? Color.primary : .blue), lineWidth: 6) { VStack { - Text("\(isOpen(room: rooms[0]) ? rooms[0].capacity : 0, specifier: "%.2f")%") + Text("\(isOpen(room: room) ? room.capacity : 0, specifier: "%.2f")%") .fontWeight(.bold) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) Text("capacity") - .font(.system(size: 10, weight: .light, design: .default)) + .font(.system(size: (configuration.background.prefersGrayscaleContent ? 13 : 10), weight: .light, design: .default)) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) } } .frame(width: 90, height: 90) - Text(lastUpdatedFormattedTime(date: rooms[0].last_updated)) + Text(lastUpdatedFormattedTime(date: room.last_updated)) .font(.system(size: 9)) - .foregroundStyle(.gray) + .foregroundStyle(.secondary) + .padding(.bottom) - Spacer() - }.widgetPadding() + + } .frame(maxWidth: .infinity, maxHeight: .infinity) } } + private struct FitnessWidgetMediumView: View { - var rooms: [FitnessRoom] + var room: FitnessRoom var configuration: ConfigureFitnessWidgetIntent.Configuration var body: some View { - VStack { - HStack { - Text(rooms[0].name) + HStack { + VStack { + Text(room.name) .font(.system(size: 12, weight: .semibold)) .padding(.top) - - Text("") - - Text(getOpenString(room: rooms[0], showbusystring: true)) - .font(.system(size: 12)) - .foregroundStyle(isOpen(room: rooms[0]) ? .green : .blue) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .padding(.top) - } - .padding([.trailing, .leading], 10) - HStack { + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) VStack { - MeterView(current: isOpen(room: rooms[0]) ? rooms[0].capacity : 0, maximum: 100.0, style: Color.blue, lineWidth: 6) { + MeterView(current: isOpen(room: room) ? room.capacity : 0, maximum: 100.0, style: configuration.background.prefersGrayscaleContent ? Color.primary : .blue, lineWidth: 6) { VStack { - Text("\(isOpen(room: rooms[0]) ? rooms[0].capacity : 0, specifier: "%.2f")%") + Text("\(isOpen(room: room) ? room.capacity : 0, specifier: "%.2f")%") .fontWeight(.bold) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) Text("capacity") - .font(.system(size: 10, weight: .light, design: .default)) + .font(.system(size: (configuration.background.prefersGrayscaleContent ? 13 : 10), weight: .light, design: .default)) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) } } .frame(width: 90, height: 90) - Text(lastUpdatedFormattedTime(date: rooms[0].last_updated)) + Text(lastUpdatedFormattedTime(date: room.last_updated)) .font(.system(size: 9)) - .foregroundStyle(.gray) - - - - Spacer() + .foregroundStyle(.secondary) } - .padding(.trailing, 7) - - FitnessGraph(room: rooms[0]) - - Spacer() + .padding(.bottom) + } .padding([.trailing, .leading], 10) + + VStack { + Text(getOpenString(room: room, showbusystring: true)) + .font(.system(size: 12)) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : (isOpen(room: room) ? .green : .blue)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top) + + FitnessGraph(room: room) + .padding(.bottom) + }.padding([.trailing], 10) }.widgetPadding() .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -200,7 +204,7 @@ private struct FitnessHomeWidgetView: View { .padding() .font(.system(size: 12)) } - else if (entry.rooms == nil || entry.rooms?.count == 0) { + else if (entry.room == nil) { Text("Error fetching fitness room data.") .multilineTextAlignment(.center).widgetPadding() .fixedSize(horizontal: false, vertical: true) @@ -208,11 +212,11 @@ private struct FitnessHomeWidgetView: View { .font(.system(size: 15)) } else { - let rooms = entry.rooms! + let room = entry.room! switch widgetFamily { - case .systemSmall: FitnessWidgetSmallView(rooms: rooms, configuration: entry.configuration) - case .systemMedium: FitnessWidgetMediumView(rooms: rooms, configuration: entry.configuration) - default: FitnessWidgetSmallView(rooms: rooms, configuration: entry.configuration) + case .systemSmall: FitnessWidgetSmallView(room: room, configuration: entry.configuration) + case .systemMedium: FitnessWidgetMediumView(room: room, configuration: entry.configuration) + default: FitnessWidgetSmallView(room: room, configuration: entry.configuration) } } } From dcdc5ecfdaff62360404eab34e2285cf62c7c963 Mon Sep 17 00:00:00 2001 From: DespicableMonkey Date: Fri, 27 Oct 2023 17:45:31 -0400 Subject: [PATCH 08/21] Add Widget Preview in Drawer --- PennMobile/Home/Fitness/FitnessRoomRow.swift | 2 +- PennMobileShared/Fitness/FitnessGraph.swift | 8 ++++++-- Widget/Fitness/FitnessProvider.swift | 15 ++++++++------- Widget/Fitness/FitnessWidget.swift | 19 +++++++------------ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/PennMobile/Home/Fitness/FitnessRoomRow.swift b/PennMobile/Home/Fitness/FitnessRoomRow.swift index 18ba8293e..1419bfbcf 100644 --- a/PennMobile/Home/Fitness/FitnessRoomRow.swift +++ b/PennMobile/Home/Fitness/FitnessRoomRow.swift @@ -96,7 +96,7 @@ struct FitnessRoomRow: View { .foregroundColor(Color.labelSecondary) .lineLimit(1) } - FitnessGraph(room: room) + FitnessGraph(room: room, color: .blue) } .padding() } diff --git a/PennMobileShared/Fitness/FitnessGraph.swift b/PennMobileShared/Fitness/FitnessGraph.swift index 0b3300ed9..2a1d18be1 100644 --- a/PennMobileShared/Fitness/FitnessGraph.swift +++ b/PennMobileShared/Fitness/FitnessGraph.swift @@ -14,8 +14,12 @@ public struct FitnessGraph: View { private let graphHeight: CGFloat = 100.0 private let padding: CGFloat = 10.0 public var room: FitnessRoom + public var color: Color - public init(room: FitnessRoom) { self.room = room } + public init(room: FitnessRoom, color: Color) { + self.room = room + self.color = color + } var hours: (Date, Date) { let calendar = Calendar.current @@ -42,7 +46,7 @@ public struct FitnessGraph: View { x: .value("Hour", $0.date, unit: .hour), y: .value("Value", $0.value) ) - .foregroundStyle(Date().hour == $0.date.hour ? Color.blue.gradient : Color.blue.opacity(0.5).gradient) + .foregroundStyle(Date().hour == $0.date.hour ? color.gradient : color.opacity(0.5).gradient) .clipShape(RoundedRectangle(cornerRadius: 10)) .offset(x: -padding) } diff --git a/Widget/Fitness/FitnessProvider.swift b/Widget/Fitness/FitnessProvider.swift index ac153c1ed..de43a5f0d 100644 --- a/Widget/Fitness/FitnessProvider.swift +++ b/Widget/Fitness/FitnessProvider.swift @@ -14,12 +14,13 @@ import PennMobileShared struct FitnessEntry: TimelineEntry { let date: Date let room: FitnessRoom? + let roomID: Int let configuration: Configuration } extension FitnessEntry where Configuration == Void { - init(date: Date, room: FitnessRoom?) { - self.init(date: date, room: room, configuration: ()) + init(date: Date, roomID: Int, room: FitnessRoom?) { + self.init(date: date, room: room, roomID: roomID, configuration: ()) } } @@ -80,7 +81,7 @@ private func snapshot(configuration: ConfigureFitn } } await refreshTask?.value - return FitnessEntry(date: Date(), room: cachedRoomData, configuration: configuration) + return FitnessEntry(date: Date(), room: cachedRoomData, roomID: roomID, configuration: configuration) } private func timeline(configuration: Configuration, roomID: Int) async -> Timeline> { @@ -92,19 +93,19 @@ struct IntentFitnessProvider: IntentTimeli func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (FitnessEntry) -> Void) { Task { - let roomID = intent.configuration.complex.rawValue //ID of Fitness Complex in backend should match "Index" field of Fitness Complex in Intents Enum - completion(await snapshot(configuration: intent.configuration, roomID: roomID)) + let roomID = intent.configuration.complex.rawValue + completion(await snapshot(configuration: intent.configuration, roomID: 7)) //getSnapshot is only called when widget is in drawer, and not when in home screen. Therefore, when in the drawer, set roomID to 7, which corresponds to 1st floor Fitness, to show a 'preview' of what the widget looks like. Then when the widget is actually placed on the home screen, it shows the instructions, because roomID is now set from getTimeline, where it defaults to 0 (which is the ID to show the instructions) } } func getTimeline(for intent: Intent, in context: Context, completion: @escaping (Timeline>) -> Void) { Task { - let roomID = intent.configuration.complex.rawValue + let roomID = intent.configuration.complex.rawValue //ID of Fitness Complex in backend should match "Index" field of Fitness Complex in Intents Enum completion(await timeline(configuration: intent.configuration, roomID: roomID)) } } func placeholder(in context: Context) -> FitnessEntry { - return FitnessEntry(date: Date(), room: placeHolderRoom, configuration: placeholderConfiguration) + return FitnessEntry(date: Date(), room: placeHolderRoom, roomID: 0, configuration: placeholderConfiguration) } } diff --git a/Widget/Fitness/FitnessWidget.swift b/Widget/Fitness/FitnessWidget.swift index e8a7ea795..ff67d1993 100644 --- a/Widget/Fitness/FitnessWidget.swift +++ b/Widget/Fitness/FitnessWidget.swift @@ -177,7 +177,7 @@ private struct FitnessWidgetMediumView: View { .fixedSize(horizontal: false, vertical: true) .padding(.top) - FitnessGraph(room: room) + FitnessGraph(room: room, color: configuration.background.prefersGrayscaleContent ? Color.primary : .blue) .padding(.bottom) }.padding([.trailing], 10) }.widgetPadding() @@ -192,17 +192,12 @@ private struct FitnessHomeWidgetView: View { var body: some View { Group { - if (entry.configuration.complex.rawValue == 0) { - (Text("To use this widget, choose a room by ") + - Text("touching and holding the widget") - .fontWeight(.bold) - + Text(", then clicking ") + - Text("Edit Widget. ") - .fontWeight(.bold) - ).multilineTextAlignment(.center).widgetPadding() - .fixedSize(horizontal: false, vertical: true) - .padding() - .font(.system(size: 12)) + if (entry.roomID == 0) { + Text("To use this widget, choose a room by **touching and holding the widget**, then choosing **Edit Widget**.") + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding() + .font(.system(size: 12)) } else if (entry.room == nil) { Text("Error fetching fitness room data.") From ac68d4c61ccdb81c06755a1c62b1296c68e1ba46 Mon Sep 17 00:00:00 2001 From: JHawk0224 Date: Tue, 31 Oct 2023 09:13:34 -0400 Subject: [PATCH 09/21] Bump version number --- PennMobile.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 87b8f33c3..b275db6cb 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -2624,7 +2624,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CF_BUNDLE_LONG_VERSION_STRING = 6700; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.5; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2686,7 +2686,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CF_BUNDLE_LONG_VERSION_STRING = 6700; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.5; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2743,7 +2743,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.5; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -2784,7 +2784,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.5; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; From bf8311a2e1b8b2f9dcad182f7d3ee54f64dfe644 Mon Sep 17 00:00:00 2001 From: Pulkith Date: Wed, 1 Nov 2023 21:05:00 -0400 Subject: [PATCH 10/21] Fix Color on Backgrounds --- Widget/Fitness/FitnessWidget.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Widget/Fitness/FitnessWidget.swift b/Widget/Fitness/FitnessWidget.swift index ff67d1993..16a97a32c 100644 --- a/Widget/Fitness/FitnessWidget.swift +++ b/Widget/Fitness/FitnessWidget.swift @@ -92,7 +92,6 @@ private func isOpen (room: FitnessRoom) -> Bool { } - private struct FitnessWidgetSmallView: View { var room: FitnessRoom var configuration: ConfigureFitnessWidgetIntent.Configuration @@ -103,7 +102,7 @@ private struct FitnessWidgetSmallView: View { Text(room.name) .font(.system(size: 13, weight: .semibold)) .padding(.top) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + .foregroundStyle(Color.primary) Text(getOpenString(room: room, showbusystring: false)) .font(.system(size: 11)) .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : (isOpen(room: room) ? .green : .blue)) @@ -113,10 +112,10 @@ private struct FitnessWidgetSmallView: View { VStack { Text("\(isOpen(room: room) ? room.capacity : 0, specifier: "%.2f")%") .fontWeight(.bold) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + .foregroundStyle(Color.primary) Text("capacity") .font(.system(size: (configuration.background.prefersGrayscaleContent ? 13 : 10), weight: .light, design: .default)) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + .foregroundStyle(Color.primary) } } @@ -145,16 +144,16 @@ private struct FitnessWidgetMediumView: View { Text(room.name) .font(.system(size: 12, weight: .semibold)) .padding(.top) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + .foregroundStyle(Color.primary) VStack { MeterView(current: isOpen(room: room) ? room.capacity : 0, maximum: 100.0, style: configuration.background.prefersGrayscaleContent ? Color.primary : .blue, lineWidth: 6) { VStack { Text("\(isOpen(room: room) ? room.capacity : 0, specifier: "%.2f")%") .fontWeight(.bold) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + .foregroundStyle(Color.primary) Text("capacity") .font(.system(size: (configuration.background.prefersGrayscaleContent ? 13 : 10), weight: .light, design: .default)) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : .black) + .foregroundStyle(Color.primary) } } @@ -172,7 +171,7 @@ private struct FitnessWidgetMediumView: View { VStack { Text(getOpenString(room: room, showbusystring: true)) .font(.system(size: 12)) - .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : (isOpen(room: room) ? .green : .blue)) + .foregroundStyle(configuration.background.prefersGrayscaleContent ? Color.primary : (isOpen(room: room) ? .green : .blue)) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .padding(.top) From b1a666ef251166dd2f3703b892a4a3bb18d2e773 Mon Sep 17 00:00:00 2001 From: George Botros <78520093+george-botros@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:42:46 -0400 Subject: [PATCH 11/21] fix: Fixed not fetching venues + other fixes in PR comments --- .../UserDBManager.swift | 2 +- .../Setup + Navigation/AppDelegate.swift | 4 +- PennMobileShared/Dining/DiningAPI.swift | 10 ++- PennMobileShared/Dining/DiningVenueRow.swift | 7 --- .../Dining/Models/DiningVenue.swift | 2 + Widget/Dining Hours/DiningHoursProvider.swift | 62 +++++++++++++------ 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/PennMobile/General/Networking + Analytics/UserDBManager.swift b/PennMobile/General/Networking + Analytics/UserDBManager.swift index 242a67927..6a803f1f6 100755 --- a/PennMobile/General/Networking + Analytics/UserDBManager.swift +++ b/PennMobile/General/Networking + Analytics/UserDBManager.swift @@ -131,7 +131,7 @@ extension UserDBManager { // Cache a user's favorite dining halls for use by dining hours widget. let diningVenues = DiningAPI.instance.getVenues(with: venueIds) - Storage.store(diningVenues, to: .groupCaches, as: DiningAPI.cacheFileName) + Storage.store(diningVenues, to: .groupCaches, as: DiningAPI.favoritesCacheFileName) WidgetKind.diningHoursWidgets.forEach { WidgetCenter.shared.reloadTimelines(ofKind: $0) } diff --git a/PennMobile/Setup + Navigation/AppDelegate.swift b/PennMobile/Setup + Navigation/AppDelegate.swift index 2e1c28a0a..5015c301b 100755 --- a/PennMobile/Setup + Navigation/AppDelegate.swift +++ b/PennMobile/Setup + Navigation/AppDelegate.swift @@ -123,7 +123,7 @@ func migrateDataToGroupContainer() { } } - if Storage.migrate(fileName: DiningAPI.directory, of: [DiningVenue].self, from: .caches, to: .groupCaches) { + if Storage.migrate(fileName: DiningVenue.directory, of: [DiningVenue].self, from: .caches, to: .groupCaches) { print("Migrated course data.") } @@ -134,7 +134,7 @@ func migrateDataToGroupContainer() { } } - if Storage.migrate(fileName: DiningAPI.cacheFileName, of: [DiningVenue].self, from: .caches, to: .groupCaches) { + if Storage.migrate(fileName: DiningAPI.favoritesCacheFileName, of: [DiningVenue].self, from: .caches, to: .groupCaches) { print("Migrated dining favorites data.") WidgetKind.diningHoursWidgets.forEach { WidgetCenter.shared.reloadTimelines(ofKind: $0) diff --git a/PennMobileShared/Dining/DiningAPI.swift b/PennMobileShared/Dining/DiningAPI.swift index b96ef3220..12bfe2808 100644 --- a/PennMobileShared/Dining/DiningAPI.swift +++ b/PennMobileShared/Dining/DiningAPI.swift @@ -10,8 +10,6 @@ import SwiftyJSON import Foundation public class DiningAPI { - - public static let directory = "diningVenue-v2.json" public static let defaultVenueIds: [Int] = [593, 636, 1442, 639] public static let instance = DiningAPI() @@ -19,7 +17,7 @@ public class DiningAPI { let diningUrl = "https://pennmobile.org/api/dining/venues/" let diningMenuUrl = "https://pennmobile.org/api/dining/menus/" - public static let cacheFileName = "diningFavoritesCache" + public static let favoritesCacheFileName = "diningFavoritesCache" let diningInsightsUrl = "https://pennmobile.org/api/dining/" @@ -64,8 +62,8 @@ public class DiningAPI { public extension DiningAPI { // MARK: - Get Methods func getVenues() -> [DiningVenue] { - if Storage.fileExists(DiningAPI.directory, in: .groupCaches) { - return Storage.retrieve(DiningAPI.directory, from: .groupCaches, as: [DiningVenue].self) + if Storage.fileExists(DiningVenue.directory, in: .groupCaches) { + return Storage.retrieve(DiningVenue.directory, from: .groupCaches, as: [DiningVenue].self) } else { return [] } @@ -93,7 +91,7 @@ public extension DiningAPI { // MARK: - Cache Methods func saveToCache(_ venues: [DiningVenue]) { - Storage.store(venues, to: .groupCaches, as: DiningAPI.directory) + Storage.store(venues, to: .groupCaches, as: DiningVenue.directory) } func saveMenuToCache(id: Int, _ menu: MenuList) { diff --git a/PennMobileShared/Dining/DiningVenueRow.swift b/PennMobileShared/Dining/DiningVenueRow.swift index f8274a5dd..e3f549232 100644 --- a/PennMobileShared/Dining/DiningVenueRow.swift +++ b/PennMobileShared/Dining/DiningVenueRow.swift @@ -66,13 +66,6 @@ public struct DiningVenueRow: View { } } else { hoursDisplay(in: geo, fontSize: 10.5, horizontalPadding: 4) - // Vertical pipe separator view -// Text(venue.humanFormattedHoursStringForToday) -// .font(.system(size: 12, weight: .light, design: .default)) -// .foregroundColor(Color.gray) -// .scaledToFit() -// .minimumScaleFactor(0.01) -// .lineLimit(1) } } } diff --git a/PennMobileShared/Dining/Models/DiningVenue.swift b/PennMobileShared/Dining/Models/DiningVenue.swift index 370f559a0..df282bf75 100755 --- a/PennMobileShared/Dining/Models/DiningVenue.swift +++ b/PennMobileShared/Dining/Models/DiningVenue.swift @@ -10,6 +10,8 @@ import Foundation public struct DiningVenue: Codable, Equatable, Identifiable { + public static let directory = "diningVenue-v2.json" + public static let menuUrlDict: [Int: String] = [593: "https://university-of-pennsylvania.cafebonappetit.com/cafe/1920-commons/", 636: "https://university-of-pennsylvania.cafebonappetit.com/cafe/hill-house/", 637: "https://university-of-pennsylvania.cafebonappetit.com/cafe/kings-court-english-house/", diff --git a/Widget/Dining Hours/DiningHoursProvider.swift b/Widget/Dining Hours/DiningHoursProvider.swift index 5c2e8940a..e0e371059 100644 --- a/Widget/Dining Hours/DiningHoursProvider.swift +++ b/Widget/Dining Hours/DiningHoursProvider.swift @@ -23,7 +23,7 @@ extension DiningEntries where Configuration == Void { private func getDiningPreferences() -> [DiningVenue] { do { - return try Storage.retrieveThrowing(DiningAPI.cacheFileName, from: .groupCaches, as: [DiningVenue].self) + return try Storage.retrieveThrowing(DiningAPI.favoritesCacheFileName, from: .groupCaches, as: [DiningVenue].self) } catch let error { print("Couldn't load dining preferences: \(error)") @@ -33,41 +33,63 @@ private func getDiningPreferences() -> [DiningVenue] { struct Provider: TimelineProvider { func placeholder(in context: Context) -> DiningEntries { + DiningEntries(date: .now, venues: []) } func getSnapshot(in context: Context, completion: @escaping (DiningEntries) -> ()) { - let entry = DiningEntries(date: .now, venues: DiningAPI.instance.getVenues(with: DiningAPI.defaultVenueIds)) - completion(entry) + Task { + let _ = await DiningAPI.instance.fetchDiningHours() + + var venues: [DiningVenue] = getDiningPreferences() + + if venues.isEmpty { + venues = DiningAPI.instance.getVenues(with: DiningAPI.defaultVenueIds) + } + + for (index, venue) in venues.enumerated() { + if let imageURL = venue.image { + if let (data, _) = try? await URLSession.shared.data(from: imageURL) { + if let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let filename = directory.appendingPathComponent(UUID().uuidString) + try? data.write(to: filename) + venues[index].localImageURL = filename + } + } + } + } + + let entry = DiningEntries(date: .now, venues: venues, configuration: ()) + completion(entry) + } } func getTimeline(in context: Context, completion: @escaping (Timeline>) -> ()) { - var venues: [DiningVenue] = getDiningPreferences() - - let dispatchGroup = DispatchGroup() - + Task { + let _ = await DiningAPI.instance.fetchDiningHours() + + var venues: [DiningVenue] = getDiningPreferences() + + if venues.isEmpty { + venues = DiningAPI.instance.getVenues(with: DiningAPI.defaultVenueIds) + } + for (index, venue) in venues.enumerated() { - dispatchGroup.enter() if let imageURL = venue.image { - let task = URLSession.shared.dataTask(with: imageURL) { (data, _, _) in - if let data = data, let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + if let (data, _) = try? await URLSession.shared.data(from: imageURL) { + if let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { let filename = directory.appendingPathComponent(UUID().uuidString) try? data.write(to: filename) venues[index].localImageURL = filename } - dispatchGroup.leave() } - task.resume() - } else { - dispatchGroup.leave() } } - - dispatchGroup.notify(queue: .main) { - if let nextDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) { - let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .after (nextDate)) - completion(timeline) - } + + if let nextDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) { + let timeline = Timeline(entries: [DiningEntries(date: .now, venues: venues, configuration: ())], policy: .after (nextDate)) + completion(timeline) } + } } } From 37cd80f69101570f8086778b4d7080a0f34220f9 Mon Sep 17 00:00:00 2001 From: Anthony Li Date: Fri, 3 Nov 2023 18:50:47 -0400 Subject: [PATCH 12/21] Fix indentation with pbxproj?? --- PennMobile.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 2b0ce885c..fbf7a4c7f 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -1553,7 +1553,7 @@ 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */, ); path = "Dining Hours"; - }; + }; 890C4EC52ACBA486009650CA /* Laundry */ = { isa = PBXGroup; children = ( From 5f4ff5ad7682ae57a7aa2c4d40de29994de9b17e Mon Sep 17 00:00:00 2001 From: George Botros <78520093+george-botros@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:58:48 -0400 Subject: [PATCH 13/21] fix: Fixed merge conflict bugs --- PennMobile.xcodeproj/project.pbxproj | 32 ++++++++++---------- PennMobileShared/Dining/DiningVenueRow.swift | 6 ++-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index fbf7a4c7f..69a902a8c 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -110,7 +110,6 @@ 426A0B52299034910066C7B7 /* DiningAnalyticsGraphBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426A0B50299034910066C7B7 /* DiningAnalyticsGraphBox.swift */; }; 42CC49D02AAE38C6008C41EE /* PollsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CC49CF2AAE38C6008C41EE /* PollsViewController.swift */; }; 42CC49D22AAE3904008C41EE /* PollsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CC49D12AAE3904008C41EE /* PollsView.swift */; }; - 42D9237129E0C69200E9E18E /* FitnessModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237029E0C69200E9E18E /* FitnessModel.swift */; }; 42D9237329E0C9AF00E9E18E /* FitnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237229E0C9AF00E9E18E /* FitnessViewController.swift */; }; 42D9237529E0CCFB00E9E18E /* FitnessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9237429E0CCFB00E9E18E /* FitnessView.swift */; }; 56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BED8AA4945D67F0ED89FA9B0 /* Pods_PennMobile.framework */; }; @@ -183,6 +182,8 @@ 6CFA06F626E8352F00944B8E /* HomeStudyRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA06F526E8352F00944B8E /* HomeStudyRoomCell.swift */; }; 6CFA06F826E8355400944B8E /* HomeFeatureCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA06F726E8355400944B8E /* HomeFeatureCellItem.swift */; }; 6E167A1C2ACC90A700F3709C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 6E167A1B2ACC90A700F3709C /* Kingfisher */; }; + 6E2F73032AF5B120003997EE /* DiningHoursProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2F73012AF5B120003997EE /* DiningHoursProvider.swift */; }; + 6E2F73042AF5B120003997EE /* DiningHoursWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2F73022AF5B120003997EE /* DiningHoursWidget.swift */; }; 6E4D82192AC8C91C009AB78E /* PennMobileShared.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E4D82182AC8C91C009AB78E /* PennMobileShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6E4D821C2AC8C91C009AB78E /* PennMobileShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */; }; 6E4D821D2AC8C91C009AB78E /* PennMobileShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -209,8 +210,6 @@ 6E903D382AC9C67000F2D384 /* ImageNetworkingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E903D372AC9C67000F2D384 /* ImageNetworkingManager.swift */; }; 6ECB4C2B2ACA6F4C00F7379A /* DiningVenueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC88D4C27B1BF50006896F6 /* DiningVenueRow.swift */; }; 6ECB4C2D2ACA6F7600F7379A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 6ECB4C2C2ACA6F7600F7379A /* Kingfisher */; }; - 6ECB4C322ACA6FCB00F7379A /* DiningHoursProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB4C302ACA6FC800F7379A /* DiningHoursProvider.swift */; }; - 6ECB4C342ACA6FE200F7379A /* DiningHoursWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */; }; 6ECB4C362ACB10D500F7379A /* HomeDiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5B20105B98002F33CA /* HomeDiningCell.swift */; }; 6ECB4C382ACB11FA00F7379A /* DiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806723D26BC700C23488 /* DiningCell.swift */; }; 8766844128CBE907005CAD32 /* NativeNewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8766844028CBE907005CAD32 /* NativeNewsViewController.swift */; }; @@ -585,6 +584,8 @@ 6CDC501127B5BCC700698235 /* Development.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Development.xcconfig; sourceTree = ""; }; 6CFA06F526E8352F00944B8E /* HomeStudyRoomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStudyRoomCell.swift; sourceTree = ""; }; 6CFA06F726E8355400944B8E /* HomeFeatureCellItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeFeatureCellItem.swift; sourceTree = ""; }; + 6E2F73012AF5B120003997EE /* DiningHoursProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningHoursProvider.swift; sourceTree = ""; }; + 6E2F73022AF5B120003997EE /* DiningHoursWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningHoursWidget.swift; sourceTree = ""; }; 6E4D82162AC8C91C009AB78E /* PennMobileShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PennMobileShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6E4D82182AC8C91C009AB78E /* PennMobileShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PennMobileShared.h; sourceTree = ""; }; 6E5159CF2AC8C9D4004B3F41 /* DiningBalance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningBalance.swift; sourceTree = ""; }; @@ -606,8 +607,6 @@ 6E5159DF2AC8C9D5004B3F41 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 6E5159E02AC8C9D5004B3F41 /* DiningToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningToken.swift; sourceTree = ""; }; 6E903D372AC9C67000F2D384 /* ImageNetworkingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageNetworkingManager.swift; sourceTree = ""; }; - 6ECB4C302ACA6FC800F7379A /* DiningHoursProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningHoursProvider.swift; sourceTree = ""; }; - 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningHoursWidget.swift; sourceTree = ""; }; 72787B1E070BFCDF84D8C3CA /* Pods-PennMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PennMobile.debug.xcconfig"; path = "Target Support Files/Pods-PennMobile/Pods-PennMobile.debug.xcconfig"; sourceTree = ""; }; 8766844028CBE907005CAD32 /* NativeNewsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeNewsViewController.swift; sourceTree = ""; }; 87FE6478290EE4BE00AFADF6 /* NotificationAPIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAPIModel.swift; sourceTree = ""; }; @@ -1476,6 +1475,15 @@ name = Frameworks; sourceTree = ""; }; + 6E2F73002AF5B120003997EE /* Dining Hours */ = { + isa = PBXGroup; + children = ( + 6E2F73012AF5B120003997EE /* DiningHoursProvider.swift */, + 6E2F73022AF5B120003997EE /* DiningHoursWidget.swift */, + ); + path = "Dining Hours"; + sourceTree = ""; + }; 6E4D82172AC8C91C009AB78E /* PennMobileShared */ = { isa = PBXGroup; children = ( @@ -1546,14 +1554,6 @@ path = Models; sourceTree = ""; }; - 6ECB4C2F2ACA6FB500F7379A /* Dining Hours */ = { - isa = PBXGroup; - children = ( - 6ECB4C302ACA6FC800F7379A /* DiningHoursProvider.swift */, - 6ECB4C332ACA6FE200F7379A /* DiningHoursWidget.swift */, - ); - path = "Dining Hours"; - }; 890C4EC52ACBA486009650CA /* Laundry */ = { isa = PBXGroup; children = ( @@ -1570,7 +1570,7 @@ 8932538E290F98BD006EE62C /* ConfigurationRepresenting.swift */, 893253912910249B006EE62C /* WidgetBackgroundTypeExtensions.swift */, 890DDBC42AA2E499006815A3 /* ViewExtensions.swift */, - 6ECB4C2F2ACA6FB500F7379A /* Dining Hours */, + 6E2F73002AF5B120003997EE /* Dining Hours */, C312D4AC2AD0B8EB00EDB893 /* Fitness */, 89EA261F290EE39E008F26CF /* Courses */, 89CA727129171E2400CF72FE /* Dining */, @@ -2613,15 +2613,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E2F73032AF5B120003997EE /* DiningHoursProvider.swift in Sources */, 89CA729129174CF900CF72FE /* DiningAnalyticsProvider.swift in Sources */, 89325390290F98E7006EE62C /* ConfigurationRepresenting.swift in Sources */, 89EA262F290F958B008F26CF /* Intents.intentdefinition in Sources */, - 6ECB4C322ACA6FCB00F7379A /* DiningHoursProvider.swift in Sources */, - 6ECB4C342ACA6FE200F7379A /* DiningHoursWidget.swift in Sources */, 8932693A28FC75A5003D4BF9 /* WidgetBundle.swift in Sources */, 89EA261E290EDFA7008F26CF /* CoursesDayWidget.swift in Sources */, 890C4ECA2ACBA4E7009650CA /* LaundryLiveActivity.swift in Sources */, 89325393291025A8006EE62C /* WidgetBackgroundTypeExtensions.swift in Sources */, + 6E2F73042AF5B120003997EE /* DiningHoursWidget.swift in Sources */, 89EA2622290EE3FD008F26CF /* CoursesProvider.swift in Sources */, 890DDBC62AA2E4B6006815A3 /* ViewExtensions.swift in Sources */, C312D4B12AD3129100EDB893 /* FitnessWidget.swift in Sources */, diff --git a/PennMobileShared/Dining/DiningVenueRow.swift b/PennMobileShared/Dining/DiningVenueRow.swift index e3f549232..40cab0c6a 100644 --- a/PennMobileShared/Dining/DiningVenueRow.swift +++ b/PennMobileShared/Dining/DiningVenueRow.swift @@ -112,8 +112,10 @@ public struct StatusColorModifier: ViewModifier { } } -struct VenueStatusLabelStyle: LabelStyle { - func makeBody(configuration: Configuration) -> some View { +public struct VenueStatusLabelStyle: LabelStyle { + public init() {} + + public func makeBody(configuration: Configuration) -> some View { HStack(spacing: 4) { configuration.icon.font(.system(size: 9, weight: .semibold)) configuration.title.font(.system(size: 11, weight: .semibold)) From 70acdbf42036a3c1b1d025d07a2df787bcdedc23 Mon Sep 17 00:00:00 2001 From: JHawk0224 Date: Fri, 3 Nov 2023 20:07:03 -0400 Subject: [PATCH 14/21] Bump version number --- PennMobile.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 69a902a8c..ef8361520 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -2670,7 +2670,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CF_BUNDLE_LONG_VERSION_STRING = 6700; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2732,7 +2732,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CF_BUNDLE_LONG_VERSION_STRING = 6700; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2789,7 +2789,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -2830,7 +2830,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.6; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; From 9b6930b36eaa2f2fad329938900bf45d618e28a8 Mon Sep 17 00:00:00 2001 From: Anthony Li Date: Sun, 5 Nov 2023 13:32:15 -0500 Subject: [PATCH 15/21] Fix course metadata not populating --- PennMobile/Courses/Models/CoursesViewModel.swift | 2 +- PennMobileShared/Courses/Course.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PennMobile/Courses/Models/CoursesViewModel.swift b/PennMobile/Courses/Models/CoursesViewModel.swift index ef60cf2aa..a45e145f5 100644 --- a/PennMobile/Courses/Models/CoursesViewModel.swift +++ b/PennMobile/Courses/Models/CoursesViewModel.swift @@ -88,7 +88,7 @@ extension Course { meetingTimes = nil } - self.init(crn: crn, code: code, title: title, section: section, instructors: instructors, startDate: startDate, endDate: endDate, meetingTimes: meetingTimes) + self.init(crn: crn, code: code, title: title, section: section, instructors: instructors, location: location, startDate: startDate, endDate: endDate, meetingTimes: meetingTimes) } } diff --git a/PennMobileShared/Courses/Course.swift b/PennMobileShared/Courses/Course.swift index ef42d8632..ff7dac714 100644 --- a/PennMobileShared/Courses/Course.swift +++ b/PennMobileShared/Courses/Course.swift @@ -86,10 +86,10 @@ public struct Course: Codable { self.code = code self.title = title self.section = section - self.instructors = [] + self.instructors = instructors self.location = location - self.startDate = Date.distantPast - self.endDate = Date.distantFuture + self.startDate = startDate + self.endDate = endDate self.meetingTimes = meetingTimes } } From 8b9be36d574fba86e82c6867a293d92554cb822f Mon Sep 17 00:00:00 2001 From: JHawk0224 Date: Thu, 9 Nov 2023 21:35:57 -0500 Subject: [PATCH 16/21] Add laundry ids --- PennMobile/Laundry/Cells/LaundryCell.swift | 10 +++---- .../Laundry/Cells/LaundryMachineCell.swift | 18 ++++++++++++ PennMobileShared/Laundry/LaundryMachine.swift | 29 ++++++++++--------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/PennMobile/Laundry/Cells/LaundryCell.swift b/PennMobile/Laundry/Cells/LaundryCell.swift index 01625e08f..cdee4bd1d 100755 --- a/PennMobile/Laundry/Cells/LaundryCell.swift +++ b/PennMobile/Laundry/Cells/LaundryCell.swift @@ -302,7 +302,7 @@ extension LaundryCell { // WashersDryersView _ = washersDryersView.anchor(dividerLine.bottomAnchor, left: bgView.leftAnchor, bottom: nil, right: bgView.rightAnchor, - topConstant: 16, leftConstant: 0, bottomConstant: 10, rightConstant: 0, + topConstant: 0, leftConstant: 0, bottomConstant: 10, rightConstant: 0, widthConstant: 0, heightConstant: 200.0) dividerLine.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true @@ -326,9 +326,9 @@ extension LaundryCell { widthConstant: 0, heightConstant: 0) // Dryer View - _ = dryerView.anchor(nil, left: washersDryersView.leftAnchor, - bottom: washersDryersView.bottomAnchor, right: washersDryersView.rightAnchor, - topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, + _ = dryerView.anchor(washerView.bottomAnchor, left: washersDryersView.leftAnchor, + bottom: nil, right: washersDryersView.rightAnchor, + topConstant: 8, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) dryerView.heightAnchor.constraint( equalTo: washersDryersView.heightAnchor, @@ -343,7 +343,7 @@ extension LaundryCell { // Scrollable Graph View _ = graphViewContainer.anchor(washersDryersView.bottomAnchor, left: bgView.leftAnchor, bottom: bgView.bottomAnchor, right: bgView.rightAnchor, - topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, + topConstant: 14, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) _ = scrollableGraphView!.anchor(graphViewContainer.topAnchor, left: graphViewContainer.leftAnchor, bottom: graphViewContainer.bottomAnchor, right: graphViewContainer.rightAnchor, diff --git a/PennMobile/Laundry/Cells/LaundryMachineCell.swift b/PennMobile/Laundry/Cells/LaundryMachineCell.swift index d04bf5aa3..c6fa19cfe 100755 --- a/PennMobile/Laundry/Cells/LaundryMachineCell.swift +++ b/PennMobile/Laundry/Cells/LaundryMachineCell.swift @@ -34,6 +34,17 @@ class LaundryMachineCell: UICollectionViewCell { iv.isHidden = true return iv }() + + private let idLabel: UILabel = { + let label = UILabel() + label.text = "" + label.font = .secondaryInformationFont + label.textColor = .labelTertiary + label.layer.cornerRadius = 4 + label.layer.masksToBounds = true + label.textAlignment = .center + return label + }() private let timerLabel: UILabel = { let label = UILabel() @@ -67,6 +78,12 @@ class LaundryMachineCell: UICollectionViewCell { self.addSubview(bellView) bellView.centerYAnchor.constraint(equalTo: topAnchor).isActive = true _ = bellView.anchor(nil, left: nil, bottom: nil, right: rightAnchor, topConstant: -8, leftConstant: 0, bottomConstant: 0, rightConstant: -8, widthConstant: 20, heightConstant: 20) + + self.addSubview(idLabel) + _ = idLabel.anchor(bottomAnchor, left: leftAnchor, + bottom: bottomAnchor, right: rightAnchor, + topConstant: 0, leftConstant: 0, bottomConstant: -20, rightConstant: 0, + widthConstant: 0, heightConstant: 20) } func updateCell(with machine: LaundryMachine) { @@ -89,6 +106,7 @@ class LaundryMachineCell: UICollectionViewCell { } else { timerLabel.text = "" } + idLabel.text = "#\(machine.id)" bellView.isHidden = !machine.isUnderNotification() } diff --git a/PennMobileShared/Laundry/LaundryMachine.swift b/PennMobileShared/Laundry/LaundryMachine.swift index c90351f98..bf5a2ef79 100755 --- a/PennMobileShared/Laundry/LaundryMachine.swift +++ b/PennMobileShared/Laundry/LaundryMachine.swift @@ -67,20 +67,21 @@ public class LaundryMachine: Hashable, Codable { // MARK: - Comparable extension LaundryMachine: Comparable { public static func < (lhs: LaundryMachine, rhs: LaundryMachine) -> Bool { - switch (lhs.status, rhs.status) { - case (.running, .open): - return true - case (.open, .running): - return false - case (_, .offline), - (_, .outOfOrder): - return true - case (.offline, _), - (.outOfOrder, _): - return false - default: - return lhs.timeRemaining < rhs.timeRemaining - } +// switch (lhs.status, rhs.status) { +// case (.running, .open): +// return true +// case (.open, .running): +// return false +// case (_, .offline), +// (_, .outOfOrder): +// return true +// case (.offline, _), +// (.outOfOrder, _): +// return false +// default: +// return lhs.timeRemaining < rhs.timeRemaining +// } + return lhs.id < rhs.id } public static func == (lhs: LaundryMachine, rhs: LaundryMachine) -> Bool { From e84b5806240c6ded49f2ce0ec7c1979015afa27f Mon Sep 17 00:00:00 2001 From: Anthony Li Date: Fri, 8 Dec 2023 18:26:14 -0500 Subject: [PATCH 17/21] Change feedback URL --- PennMobile/More Tab/MoreViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PennMobile/More Tab/MoreViewController.swift b/PennMobile/More Tab/MoreViewController.swift index 322679a89..d1846514c 100755 --- a/PennMobile/More Tab/MoreViewController.swift +++ b/PennMobile/More Tab/MoreViewController.swift @@ -80,7 +80,7 @@ class MoreViewController: GenericTableViewController, ShowsAlert { PennLink(title: "Canvas", url: "https://canvas.upenn.edu"), PennLink(title: "Path@Penn", url: "https://path.at.upenn.edu"), PennLink(title: "PennPortal", url: "https://portal.apps.upenn.edu/penn_portal"), - PennLink(title: "Share Your Feedback", url: "https://airtable.com/shrS98E3rj5Nw1wy6")] + PennLink(title: "Share Your Feedback", url: "https://pennlabs.org/feedback/ios")] } From 93ec87d6014cca3b1bb64bdce641897bba050456 Mon Sep 17 00:00:00 2001 From: Anthony Li Date: Sat, 16 Dec 2023 19:41:17 -0500 Subject: [PATCH 18/21] Add PennForms as a dependency --- PennMobile.xcodeproj/project.pbxproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index ef8361520..3f09bc432 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -231,6 +231,7 @@ 89459B9B28E799AA00CE1638 /* CoursesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89459B9A28E799AA00CE1638 /* CoursesViewModel.swift */; }; 89459B9D28E7E8A600CE1638 /* CoursesDayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89459B9C28E7E8A600CE1638 /* CoursesDayView.swift */; }; 895C75E628FA165100A329A0 /* LabsLoginSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895C75E528FA165100A329A0 /* LabsLoginSwiftUI.swift */; }; + 898DB4912B2E7AA20027CC8F /* PennForms in Frameworks */ = {isa = PBXBuildFile; productRef = 898DB4902B2E7AA20027CC8F /* PennForms */; }; 89B454DF28E1161B00BC918B /* PathAtPennNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89B454DE28E1161B00BC918B /* PathAtPennNetworkManager.swift */; }; 89CA728D291721D200CF72FE /* KeychainAccessible+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA728C291721D200CF72FE /* KeychainAccessible+Extensions.swift */; }; 89CA729029173CDE00CF72FE /* UserDefaults + Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA728F29173CDE00CF72FE /* UserDefaults + Helpers.swift */; }; @@ -747,6 +748,7 @@ 6CE12F9026E82DC600284D9F /* FirebaseAnalytics in Frameworks */, F2568A762413534F00561295 /* SnapKit in Frameworks */, 6CA1ACDB271D2D5000EDB967 /* Kingfisher in Frameworks */, + 898DB4912B2E7AA20027CC8F /* PennForms in Frameworks */, 6C4CC1FA26E6B1720000B4A8 /* SwiftyJSON in Frameworks */, 6CE12F9226E82DC600284D9F /* FirebaseCrashlytics in Frameworks */, F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */, @@ -2027,6 +2029,7 @@ 6CE12F8F26E82DC600284D9F /* FirebaseAnalytics */, 6CE12F9126E82DC600284D9F /* FirebaseCrashlytics */, 6CA1ACDA271D2D5000EDB967 /* Kingfisher */, + 898DB4902B2E7AA20027CC8F /* PennForms */, ); productName = PennMobile; productReference = 216640601EBADADA00746B8E /* PennMobile.app */; @@ -2148,6 +2151,7 @@ F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 898DB48F2B2E7AA20027CC8F /* XCRemoteSwiftPackageReference "PennForms" */, ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -3162,6 +3166,14 @@ minimumVersion = 10.0.0; }; }; + 898DB48F2B2E7AA20027CC8F /* XCRemoteSwiftPackageReference "PennForms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pennlabs/PennForms"; + requirement = { + kind = revision; + revision = ee6f12573580aab93920976a1438e28d4bb70f99; + }; + }; F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup"; @@ -3229,6 +3241,11 @@ package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; + 898DB4902B2E7AA20027CC8F /* PennForms */ = { + isa = XCSwiftPackageProductDependency; + package = 898DB48F2B2E7AA20027CC8F /* XCRemoteSwiftPackageReference "PennForms" */; + productName = PennForms; + }; F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */; From e279822c2b4938fbcb9a476c16a4b7c44b3c9cb9 Mon Sep 17 00:00:00 2001 From: Anthony Li Date: Sat, 16 Dec 2023 19:44:11 -0500 Subject: [PATCH 19/21] Make project compile under Xcode 15.1 --- Widget/Dining Hours/DiningHoursWidget.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Widget/Dining Hours/DiningHoursWidget.swift b/Widget/Dining Hours/DiningHoursWidget.swift index 20974fec1..9113ab650 100644 --- a/Widget/Dining Hours/DiningHoursWidget.swift +++ b/Widget/Dining Hours/DiningHoursWidget.swift @@ -120,6 +120,8 @@ struct DiningHoursWidget: Widget { .contentMarginsDisabled() } } + +@available(iOS 17.0, *) #Preview(as: .systemSmall) { DiningHoursWidget() } timeline: { From 9dee060bd3b07c88eea979c7eaf8223c7127a5b8 Mon Sep 17 00:00:00 2001 From: Jordan H <35184414+JHawk0224@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:05:41 -0500 Subject: [PATCH 20/21] Update semester dates --- PennMobileShared/General/Extensions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PennMobileShared/General/Extensions.swift b/PennMobileShared/General/Extensions.swift index 26d877b47..d2c3a441d 100755 --- a/PennMobileShared/General/Extensions.swift +++ b/PennMobileShared/General/Extensions.swift @@ -347,14 +347,14 @@ public extension Date { static var startOfSemester: Date { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" - return formatter.date(from: "2023-08-29")! + return formatter.date(from: "2024-01-18")! } static var endOfSemester: Date { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" - return formatter.date(from: "2023-12-21")! + return formatter.date(from: "2024-05-14")! } } From 29e46c1a82a666ac6a6f4ee45ff546ea9f757857 Mon Sep 17 00:00:00 2001 From: JHawk0224 Date: Wed, 17 Jan 2024 12:11:01 -0500 Subject: [PATCH 21/21] Bump version number --- PennMobile.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 3f09bc432..3533f2706 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -2674,7 +2674,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CF_BUNDLE_LONG_VERSION_STRING = 6700; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.8; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2736,7 +2736,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CF_BUNDLE_LONG_VERSION_STRING = 6700; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.8; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2793,7 +2793,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.8; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -2834,7 +2834,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CF_BUNDLE_SHORT_VERSION_STRING = 7.3.7; + CF_BUNDLE_SHORT_VERSION_STRING = 7.3.8; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution";