From 40d8fe3e7e513af37b5f896b6f8b93cf4ca22987 Mon Sep 17 00:00:00 2001 From: "Philipp, Zagar" Date: Mon, 5 Jun 2023 23:23:35 +0200 Subject: [PATCH] [feature/tca-4.1.1] Minor Version 4.1.1 (#545) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes * Add compiler directive that disables Crashlytics for development (#520) * Simple Crashlytics Service (#522) * implemented simple `CrashlyticsService` * replaced existing crashlytics usage * MAIN -> DEV (#533) * v4.1 (#517) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> * Bring main up to date (#521) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes * Add compiler directive that disables Crashlytics for development (#520) * Simple Crashlytics Service (#522) * implemented simple `CrashlyticsService` * replaced existing crashlytics usage --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski * MAIN -> DEV (#535) * v4.1 (#517) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> * Bring main up to date (#521) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes * Add compiler directive that disables Crashlytics for development (#520) * Simple Crashlytics Service (#522) * implemented simple `CrashlyticsService` * replaced existing crashlytics usage --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski * DEV -> MAIN (#534) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes * Add compiler directive that disables Crashlytics for development (#520) * Simple Crashlytics Service (#522) * implemented simple `CrashlyticsService` * replaced existing crashlytics usage * MAIN -> DEV (#533) * v4.1 (#517) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> * Bring main up to date (#521) * Fix/widget and movie bugs (#511) * change tabbar items of Widgets & Grades to "outline" * change studyroom widget icon * bugfix: show events on same day -> filter past events out before building dict * Improve MovieCard Design * Update Version Number * Make Widgets only available on phone * Fix/token permissions UI (#512) * Adding a new warning if not all permissions are granted. * Spacers and Localizables * bugfix: fix tower image (#509) Closes #508 * Design/login (#513) * Move Calendar Picker to bottom * Rename Widget Tabbaritem * Make LecturesView icons outline * LectureView: reduce Lecture Info Text sizes * Remove Calendar EventsView * Fix Movie Title Image gray bar * - fix TUM logo white pixelation - calendar picker back to top * Redesign some stuff for the Login Process * Remove old tum logo_white images * A few Login Design changes * Add Need Help Button to Check Permissions View * Remove red x from Check Token Button * Add personal NavigationTitle to Widget View * Today Btn press in Calendar forwards to day view * Adjust Login Design to iPad * Localize Mensa Garching traffic * Remove icon from Semester List Group Header on Grades & Lectures * Adjust Grades Info Design to Lectures (-> icon outline, text larger) * LectureDetails: Add contact btn to lecturer info * Change Lecturer Search Icon * Widget View: await name to display navigationTitle * Add Spacer to Widget Detail sheet top * Change color check token permission view text * Open Widget View when logging in * Generate personalized Widget Navigation Title * Reorder Code Widget Screen * Adjust Profile "GET IN CONTACT" Btns to Webview setting * Move NavTitle loading into recommender loading * remove request location always use * A few Login - Token Design changes * Add compiler directive that disables Crashlytics for development (#520) * Simple Crashlytics Service (#522) * implemented simple `CrashlyticsService` * replaced existing crashlytics usage --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski * Fix Swift 5.8 compile errors (#538) * Feature/navigatum-dev (#539) * Copy over changes to branch with "development" as base. * Fix overlay images. * Remove images. Adjust map height. * API Refactoring (#536) * Start to refactor the API buy generalizing all the API methods into one MainAPI enum independent of the API (TUMCabe, TUMOnline, CampusOnline, etc.), the respective data type which (i.e. Grades, Movie, StudyRoom, etc.) and independent of the decoder (XML or JSON). * Created for each API an new enum conforming to the new protocol API to be used with the newly created method makeRequest in the enum MainAPI * Refactored Calendar to work with the Asyn-Await-Pattern, the state enum and the new API. This includes, that Calendar now shows the error screen, if the fetching failed * Refactored LecturesSearch to the new API and introduced the asnc-await-pattern with the state enum * Refactored the API for PersonSearch and added the new async-await pattern and error handling for this view. * Completely rewritten the ProfileDetailView and its ViewModel to adopt to the async-await-pattern, and the state-enum and handling errors properly * Refactoring the complete Authentication procress to work with the new API and to work with asnc-await * Refactored the ProfileViewModel, ProfileView, Model, to work with the new API, including async-await pattern and many many adjustments alongside. * Adding the empty profile image placeholder and removing the old TUMOnlineAPI/CampusOnlineAPI completely. * Renamed the ProfileView2/ProfileViewModel2 -> to ProfileView/ProfileViewModel * Refactoring the News for the API including async-await-pattern, error-handling, state-enum * Renamed the new NewsViewModel, NewsView to the old names * Extracted the new NewsScreen into a own file * Refactored the MovieView for the new API, including error-handling, async-await, and state-enum * Refactored the StudyRoomViewModel for fetching the RoomImageMapping. Adapted for the async-await-patter and enum-state * Removed the old TUMCabeAPI * Refactoring the Cafeterias to work with the new API * Refactored the MealPlanViewModel/View, to work with the new API, adding async-await pattern and error-handling * Refactored the DishLabels to work with the new API * Refactored the StudyRoomService to work with the new API for the StudyRoomApiResponse * Removed the old TUMDevAppAPI completely * Fixed the links for the TUMSexyView, when using the built in WebView. Refactored to the new API, added async-await-pattern and error-handling * Removed the EntityImporter the old AuthenticationHanlder, the defualt Session and the Entity-Protocol * Refactored the Lectures to be a struct instead of the old RowSet-Enum-Construct. This is obsolet since the TUMOnlineAPI response enum can be used for decoding * Refactored LectureDetails to work with the TUMOnlineAPI.Response instead of its own old LectureDetailsComponents * Fixing an issue for MealPlan. When next week's menu isn't ready the API throws a 404 error, but we can fetch meals for the current week. Thus, we need to be patient, i.e. no error needs to be thrown. And the this weeks menu can be shown without any error * Refactoring each view from .onAppear{Task{}} to .task{} since this can be cancled by the view (see https://stackoverflow.com/questions/68114509/what-is-the-difference-between-onappear-and-task-in-swiftui-3) * Deleted a print out and comment * Renaming the APIs to its original naming scheme. And creating files for each API and rename all temporarily created ViewModels to their original naming scheme. Fixing the problem with Cafeterias, which do not have any menus and the API is returning a 404 error. Now just a plain 'No Menus available' text shows up instead of an error message. Fixing a problem with the PersonSearch where the pIdentNr for the API was the wrong number, so the response was an error, now the API gets called with the obfuscated_id and the right results are retrieved. * Fix issue after logout via Profile sheet * Readding old if clause to fix weird bevaiour; Add comments * [fix/swift-5.8] Code adjustments for Swift 5.8 compiler (#542) * Fix swift 5.8 compile errors * Force refresh of token requests * Fix auth issues * Fix loading error. Revert images view since it's not needed. * Forward to NavigaTUM from Lecture Detail View instead of Roomfinder * Reduce NavigaTUM Map Span * Update some Localizables * Fix studitum fetch link --------- Co-authored-by: Atharva Mathapati Co-authored-by: August Wittgenstein * Fix Crash: If links at the bottom of the profile view are clicked, the webview sheet opens twice, hence the app crashes * Fix Merge Conflicts Issues --------- Co-authored-by: August Wittgenstein <75639931+AW-tum@users.noreply.github.com> Co-authored-by: August Wittgenstein Co-authored-by: 14slash12 <59373377+14slash12@users.noreply.github.com> Co-authored-by: Thomas Schuster <82888998+twihno@users.noreply.github.com> Co-authored-by: Anton Wyrowski Co-authored-by: Moritz Aberle <102237363+ge65cer@users.noreply.github.com> Co-authored-by: Atharva <92479959+Atharva-Mathapati@users.noreply.github.com> Co-authored-by: Atharva Mathapati --- Campus-iOS.xcodeproj/project.pbxproj | 622 +++++++++++++++--- .../AnalyticsController.swift | 2 + .../AnalyticsController.swift" | 81 --- .../AnalyticsError.swift" | 12 - .../AnalyticsCom\303\274o/AppUsageData.swift" | 125 ---- .../AppUsageDataEntity.swift" | 28 - .../AnalyticsCom\303\274o/HashFunction.swift" | 20 - .../AnalyticsCom\303\274o/Secrets.xcconfig" | 12 - Campus-iOS/App.swift | 19 +- Campus-iOS/Base/Enums/Enums.swift | 17 +- .../Networking/APIErrors/EatAPIError.swift | 37 ++ .../Networking/APIErrors/MVGAPIError.swift | 37 ++ .../APIErrors/NavigaTUMAPIError.swift | 37 ++ .../APIErrors/TUMCabeAPIError.swift | 37 ++ .../APIErrors/TUMDevAppAPIError.swift | 37 ++ .../APIErrors/TUMOnlineAPIError.swift | 66 ++ .../APIErrors/TUMSexyAPIError.swift | 37 ++ Campus-iOS/Base/Networking/APIs/EatAPI.swift | 46 ++ Campus-iOS/Base/Networking/APIs/MVGAPI.swift | 55 ++ .../Base/Networking/APIs/NavigaTUMAPI.swift | 50 ++ .../Networking/{ => APIs}/TUMCabeAPI.swift | 39 +- .../Base/Networking/APIs/TUMDevAppAPI.swift | 41 ++ .../Base/Networking/APIs/TUMOnlineAPI.swift | 117 ++++ .../Base/Networking/APIs/TUMSexyAPI.swift | 29 + .../Base/Networking/CampusOnlineAPI.swift | 4 +- Campus-iOS/Base/Networking/EatAPI.swift | 107 --- Campus-iOS/Base/Networking/MVGAPI.swift | 61 -- .../Base/Networking/NetworkingAPI.swift | 2 +- .../Networking/Old APIs/APIResponse.swift | 54 ++ .../Old APIs/CampusOnlineAPIOld.swift | 119 ++++ .../Base/Networking/Old APIs/EatAPIOld.swift | 106 +++ .../Base/Networking/Old APIs/MVGAPIOld.swift | 61 ++ .../Old APIs/NetworkingAPIOld.swift | 18 + .../Networking/Old APIs/TUMCabeAPIOld.swift | 77 +++ .../Networking/Old APIs/TUMDevAppAPIOld.swift | 81 +++ .../Networking/Old APIs/TUMOnlineAPIOld.swift | 93 +++ .../Networking/Old APIs/TUMSexyAPIOld.swift | 29 + .../Base/Networking/Protocols/API.swift | 100 +++ .../Base/Networking/Protocols/APIError.swift | 12 + .../Base/Networking/Protocols/MainAPI.swift | 73 ++ .../Base/Networking/Protocols/Service.swift | 18 + Campus-iOS/Base/Networking/TUMDevAppAPI.swift | 81 --- Campus-iOS/Base/Networking/TUMOnlineAPI.swift | 93 --- Campus-iOS/Base/Networking/TUMSexyAPI.swift | 29 - .../{Entity => Model}/CalendarEvent.swift | 4 +- .../{Entity => Model}/TumCalendarStyle.swift | 0 .../Screen/CalendarScreen.swift | 82 +++ .../Service/CalendarService.swift | 16 + .../ViewModel/CalendarViewModel.swift | 73 +- .../Views/CalendarContentView.swift | 29 +- .../Views/CalendarWidgetView.swift | 11 +- .../Campus-iOS/Base.lproj/Localizable.strings | 5 +- .../Campus-iOS/de.lproj/Localizable.strings | 7 +- Campus-iOS/Extensions/Extensions.swift | 10 - Campus-iOS/GradesComponent/Model/Grade.swift | 101 +-- .../GradesComponent/Screen/GradesScreen.swift | 6 +- .../Service/GradesService.swift | 15 +- .../ViewModel/GradesViewModel+State.swift | 16 +- .../ViewModel/GradesViewModel.swift | 21 +- .../ViewModel/MockGradesViewModel.swift | 2 +- .../Views/GradeWidgetView.swift | 6 +- .../GradesComponent/Views/GradesView.swift | 1 + .../HelperViews/ImageFullScreenView.swift | 3 +- .../LectureComponent/Model/Lecture.swift | 110 ++-- .../Model/LectureDetails.swift | 158 +++-- .../Service/LectureDetailsService.swift | 6 +- .../Service/LecturesService.swift | 9 +- .../LectureDetailsBasicInfoView.swift | 18 +- .../LectureDetailsEventInfoView.swift | 4 +- .../Views/LecturesDetailView.swift | 2 +- .../Screen/LectureSearchScreen.swift | 76 +++ .../Service/LectureSearchService.swift | 16 + .../View/LectureSearchListView.swift | 50 -- .../View/LectureSearchView.swift | 33 +- .../ViewModel/LectureSearchViewModel.swift | 52 +- .../LoginComponent/Model/Confirmation.swift | 16 + .../{Service => Model}/Credentials.swift | 0 Campus-iOS/LoginComponent/Model/Token.swift | 16 + .../Service/AuthenticationHandler.swift | 194 +----- .../ViewModel/LoginViewModel.swift | 44 +- .../ViewModel/TokenPermissionsViewModel.swift | 55 +- .../LoginComponent/Views/LoginView.swift | 30 +- .../Views/TokenConfirmationView.swift | 54 +- .../Views/TokenPermissionsView.swift | 4 +- .../Service/CafeteriasService.swift | 53 +- .../MapComponent/Service/DishService.swift | 26 + .../Service/MealPlanService.swift | 75 +++ .../Service/MensaEnumService.swift | 28 - .../Service/StudyRoomsService.swift | 16 +- .../Types/StudyRooms/StudyRoom.swift | 2 +- .../StudyRooms/StudyRoomApiResponse.swift | 2 +- .../Types/StudyRooms/StudyRoomAttribute.swift | 2 +- .../Types/StudyRooms/StudyRoomGroup.swift | 2 +- .../View/Cafeterias/CafeteriaWidgetView.swift | 20 +- .../View/Cafeterias/DishView.swift | 60 ++ .../View/Cafeterias/MealPlanScreen.swift | 42 ++ .../View/Cafeterias/MealPlanView.swift | 40 +- .../View/Cafeterias/MenuView.swift | 112 +--- Campus-iOS/MapComponent/View/MapView.swift | 28 - .../View/PanelContentListView.swift | 6 +- .../MapComponent/View/PanelContentView.swift | 19 +- .../MapImagesHorizontalScrollingView.swift | 27 +- .../StudyRooms/StudyRoomDetailsScreen.swift | 85 +++ .../StudyRooms/StudyRoomDetailsView.swift | 95 ++- .../View/StudyRooms/StudyRoomGroupView.swift | 2 +- .../ViewModel/CafeteriaWidgetViewModel.swift | 30 +- .../ViewModel/DishViewModel.swift | 93 +++ .../MapComponent/ViewModel/MapViewModel.swift | 6 +- .../ViewModel/MealPlanViewModel.swift | 87 +-- .../ViewModel/MenuViewModel.swift | 11 +- .../ViewModel/StudyRoomVIewModel.swift | 36 - .../ViewModel/StudyRoomViewModel.swift | 72 ++ .../ViewModel/StudyRoomWidgetViewModel.swift | 5 +- Campus-iOS/Model/Model.swift | 56 +- .../MoviesComponent/Screen/MoviesScreen.swift | 8 + .../Service/MovieService.swift | 17 + .../MoviesComponent/ViewModel/Movie.swift | 2 +- .../ViewModel/MoviesViewModel.swift | 109 ++- .../MoviesComponent/Views/MoviesView.swift | 61 +- .../{Service => Model}/News.swift | 20 +- .../{Service => Model}/NewsSource.swift | 34 +- .../NewsComponent/NewsScreen/NewsScreen.swift | 53 ++ .../NewsComponent/Service/NewsService.swift | 27 + .../ViewModel/NewsViewModel.swift | 204 +++--- Campus-iOS/NewsComponent/Views/NewsView.swift | 26 +- .../Entity/Organization.swift | 2 +- .../Entity/PhoneExtension.swift | 3 +- .../PersonDetailedComponent/Entity/Room.swift | 2 +- .../Screen/PersonDetailedScreen.swift | 69 ++ .../Service/PersonDetailedService.swift | 16 + .../View/PersonDetailedCellView.swift | 28 - .../View/PersonDetailedView.swift | 225 ++++--- .../ViewModel/PersonDetailedViewModel.swift | 192 ++---- .../Screen/PersonSearchScreen.swift | 74 +++ .../Service/PersonSearchService.swift | 16 + .../View/PersonSearchListView.swift | 45 -- .../View/PersonSearchView.swift | 29 +- .../ViewModel/PersonSearchViewModel.swift | 49 +- .../ProfileComponent/Entity/Profile.swift | 9 +- .../ProfileComponent/Entity/Tuition.swift | 2 +- .../Service/ProfileService.swift | 42 ++ .../View/ProfileMyTumSection.swift | 58 -- .../View/ProfileToolbar.swift | 7 +- .../ProfileComponent/View/ProfileView.swift | 157 +++-- .../ProfileComponent/View/TuitionScreen.swift | 97 +++ .../ViewModel/ProfileViewModel.swift | 306 ++++++--- ...igaTumNavigationAdditionalProperties.swift | 15 + .../NavigaTumNavigationCoordinates.swift | 17 + .../Details/NavigaTumNavigationMaps.swift | 13 + .../Model/Details/NavigaTumOverlaysMaps.swift | 11 + .../Details/NavigaTumRoomFinderMaps.swift | 17 + .../Model/NavigaTumNavigationDetails.swift | 26 + .../Model/NavigaTumNavigationEntity.swift | 46 ++ .../Model/NavigaTumNavigationProperty.swift | 12 + .../Model/NavigaTumOverlayMap.swift | 19 + .../Model/NavigaTumRoomFinderMap.swift | 23 + .../Search/NavigaTumSearchResponse.swift | 16 + .../NavigaTumSearchResponseSection.swift | 17 + .../Service/RoomFinderService.swift | 25 + .../ViewModel/NavigaTumDetailsViewModel.swift | 33 + .../ViewModel/NavigaTumViewModel.swift | 27 + .../ViewModel/RoomFinderViewModel.swift | 46 +- .../NavigaTumDetailsBaseView.swift | 58 ++ .../ViewNavigaTum/NavigaTumDetailsView.swift | 66 ++ .../ViewNavigaTum/NavigaTumListView.swift | 47 ++ .../NavigaTumMapImagesView.swift | 86 +++ .../ViewNavigaTum/NavigaTumMapView.swift | 51 ++ .../ViewNavigaTum/NavigaTumView.swift | 42 ++ .../RoomFinderDetailsMapImagesView.swift | 11 +- .../RoomFinder/Views/RoomFinderView.swift | 12 +- .../TUMSexyComponent/Model/TUMSexyLink.swift | 21 + .../Screen/TUMSexyScreen.swift | 55 ++ .../Service/TUMSexyService.swift | 25 + .../ViewModel/TUMSexyViewModel.swift | 80 ++- .../TUMSexyComponent/Views/TUMSexyView.swift | 49 +- .../TuitionComponent/View/TuitionView.swift | 23 +- .../View/TuitionWidgetView.swift | 47 +- .../Strategy/MLModelDataHandler.swift | 4 +- .../Recommender/WidgetRecommender.swift | 2 +- .../WidgetComponent/Screen/WidgetScreen.swift | 28 +- .../xcshareddata/swiftpm/Package.resolved | 196 ------ 181 files changed, 5662 insertions(+), 2907 deletions(-) delete mode 100644 "Campus-iOS/AnalyticsCom\303\274o/AnalyticsController.swift" delete mode 100644 "Campus-iOS/AnalyticsCom\303\274o/AnalyticsError.swift" delete mode 100644 "Campus-iOS/AnalyticsCom\303\274o/AppUsageData.swift" delete mode 100644 "Campus-iOS/AnalyticsCom\303\274o/AppUsageDataEntity.swift" delete mode 100644 "Campus-iOS/AnalyticsCom\303\274o/HashFunction.swift" delete mode 100644 "Campus-iOS/AnalyticsCom\303\274o/Secrets.xcconfig" create mode 100644 Campus-iOS/Base/Networking/APIErrors/EatAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIErrors/MVGAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIErrors/NavigaTUMAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIErrors/TUMCabeAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIErrors/TUMDevAppAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIErrors/TUMOnlineAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIErrors/TUMSexyAPIError.swift create mode 100644 Campus-iOS/Base/Networking/APIs/EatAPI.swift create mode 100644 Campus-iOS/Base/Networking/APIs/MVGAPI.swift create mode 100644 Campus-iOS/Base/Networking/APIs/NavigaTUMAPI.swift rename Campus-iOS/Base/Networking/{ => APIs}/TUMCabeAPI.swift (74%) create mode 100644 Campus-iOS/Base/Networking/APIs/TUMDevAppAPI.swift create mode 100644 Campus-iOS/Base/Networking/APIs/TUMOnlineAPI.swift create mode 100644 Campus-iOS/Base/Networking/APIs/TUMSexyAPI.swift delete mode 100644 Campus-iOS/Base/Networking/EatAPI.swift delete mode 100644 Campus-iOS/Base/Networking/MVGAPI.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/APIResponse.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/CampusOnlineAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/EatAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/MVGAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/NetworkingAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/TUMCabeAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/TUMDevAppAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/TUMOnlineAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Old APIs/TUMSexyAPIOld.swift create mode 100644 Campus-iOS/Base/Networking/Protocols/API.swift create mode 100644 Campus-iOS/Base/Networking/Protocols/APIError.swift create mode 100644 Campus-iOS/Base/Networking/Protocols/MainAPI.swift create mode 100644 Campus-iOS/Base/Networking/Protocols/Service.swift delete mode 100644 Campus-iOS/Base/Networking/TUMDevAppAPI.swift delete mode 100644 Campus-iOS/Base/Networking/TUMOnlineAPI.swift delete mode 100644 Campus-iOS/Base/Networking/TUMSexyAPI.swift rename Campus-iOS/CalendarComponent/{Entity => Model}/CalendarEvent.swift (98%) rename Campus-iOS/CalendarComponent/{Entity => Model}/TumCalendarStyle.swift (100%) create mode 100644 Campus-iOS/CalendarComponent/Screen/CalendarScreen.swift create mode 100644 Campus-iOS/CalendarComponent/Service/CalendarService.swift create mode 100644 Campus-iOS/LectureSearchComponent/Screen/LectureSearchScreen.swift create mode 100644 Campus-iOS/LectureSearchComponent/Service/LectureSearchService.swift delete mode 100644 Campus-iOS/LectureSearchComponent/View/LectureSearchListView.swift create mode 100644 Campus-iOS/LoginComponent/Model/Confirmation.swift rename Campus-iOS/LoginComponent/{Service => Model}/Credentials.swift (100%) create mode 100644 Campus-iOS/LoginComponent/Model/Token.swift create mode 100644 Campus-iOS/MapComponent/Service/DishService.swift create mode 100644 Campus-iOS/MapComponent/Service/MealPlanService.swift delete mode 100644 Campus-iOS/MapComponent/Service/MensaEnumService.swift create mode 100644 Campus-iOS/MapComponent/View/Cafeterias/DishView.swift create mode 100644 Campus-iOS/MapComponent/View/Cafeterias/MealPlanScreen.swift delete mode 100644 Campus-iOS/MapComponent/View/MapView.swift create mode 100644 Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsScreen.swift create mode 100644 Campus-iOS/MapComponent/ViewModel/DishViewModel.swift delete mode 100644 Campus-iOS/MapComponent/ViewModel/StudyRoomVIewModel.swift create mode 100644 Campus-iOS/MapComponent/ViewModel/StudyRoomViewModel.swift create mode 100644 Campus-iOS/MoviesComponent/Screen/MoviesScreen.swift create mode 100644 Campus-iOS/MoviesComponent/Service/MovieService.swift rename Campus-iOS/NewsComponent/{Service => Model}/News.swift (78%) rename Campus-iOS/NewsComponent/{Service => Model}/NewsSource.swift (52%) create mode 100644 Campus-iOS/NewsComponent/NewsScreen/NewsScreen.swift create mode 100644 Campus-iOS/NewsComponent/Service/NewsService.swift create mode 100644 Campus-iOS/PersonDetailedComponent/Screen/PersonDetailedScreen.swift create mode 100644 Campus-iOS/PersonDetailedComponent/Service/PersonDetailedService.swift delete mode 100644 Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift create mode 100644 Campus-iOS/PersonSearchComponent/Screen/PersonSearchScreen.swift create mode 100644 Campus-iOS/PersonSearchComponent/Service/PersonSearchService.swift delete mode 100644 Campus-iOS/PersonSearchComponent/View/PersonSearchListView.swift create mode 100644 Campus-iOS/ProfileComponent/Service/ProfileService.swift delete mode 100644 Campus-iOS/ProfileComponent/View/ProfileMyTumSection.swift create mode 100644 Campus-iOS/ProfileComponent/View/TuitionScreen.swift create mode 100644 Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationAdditionalProperties.swift create mode 100644 Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationCoordinates.swift create mode 100644 Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationMaps.swift create mode 100644 Campus-iOS/RoomFinder/Model/Details/NavigaTumOverlaysMaps.swift create mode 100644 Campus-iOS/RoomFinder/Model/Details/NavigaTumRoomFinderMaps.swift create mode 100644 Campus-iOS/RoomFinder/Model/NavigaTumNavigationDetails.swift create mode 100644 Campus-iOS/RoomFinder/Model/NavigaTumNavigationEntity.swift create mode 100644 Campus-iOS/RoomFinder/Model/NavigaTumNavigationProperty.swift create mode 100644 Campus-iOS/RoomFinder/Model/NavigaTumOverlayMap.swift create mode 100644 Campus-iOS/RoomFinder/Model/NavigaTumRoomFinderMap.swift create mode 100644 Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponse.swift create mode 100644 Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponseSection.swift create mode 100644 Campus-iOS/RoomFinder/Service/RoomFinderService.swift create mode 100644 Campus-iOS/RoomFinder/ViewModel/NavigaTumDetailsViewModel.swift create mode 100644 Campus-iOS/RoomFinder/ViewModel/NavigaTumViewModel.swift create mode 100644 Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsBaseView.swift create mode 100644 Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsView.swift create mode 100644 Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumListView.swift create mode 100644 Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapImagesView.swift create mode 100644 Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapView.swift create mode 100644 Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumView.swift create mode 100644 Campus-iOS/TUMSexyComponent/Model/TUMSexyLink.swift create mode 100644 Campus-iOS/TUMSexyComponent/Screen/TUMSexyScreen.swift create mode 100644 Campus-iOS/TUMSexyComponent/Service/TUMSexyService.swift delete mode 100644 TUM Campus App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Campus-iOS.xcodeproj/project.pbxproj b/Campus-iOS.xcodeproj/project.pbxproj index 38718604..fe4bbe91 100644 --- a/Campus-iOS.xcodeproj/project.pbxproj +++ b/Campus-iOS.xcodeproj/project.pbxproj @@ -32,7 +32,6 @@ 08DFB97528664CFC00E357DF /* TuitionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DFB97428664CFC00E357DF /* TuitionDetailsView.swift */; }; 08DFB9772866506900E357DF /* WidgetFrameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DFB9762866506900E357DF /* WidgetFrameView.swift */; }; 08DFB97928666AD900E357DF /* CafeteriaWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DFB97828666AD900E357DF /* CafeteriaWidgetView.swift */; }; - 08DFB97D2867800C00E357DF /* CafeteriaWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DFB97C2867800C00E357DF /* CafeteriaWidgetViewModel.swift */; }; 08DFB97F2867AC9200E357DF /* StudyRoomWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DFB97E2867AC9200E357DF /* StudyRoomWidgetView.swift */; }; 08DFB9812867ACB600E357DF /* StudyRoomWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DFB9802867ACB600E357DF /* StudyRoomWidgetViewModel.swift */; }; 08FAFD15287DC484006A0E27 /* CalendarWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FAFD14287DC484006A0E27 /* CalendarWidgetView.swift */; }; @@ -46,6 +45,30 @@ 08FAFD292898B6C8006A0E27 /* SpatioTemporalStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FAFD282898B6C8006A0E27 /* SpatioTemporalStrategy.swift */; }; 100803462764E2C50013ED0E /* ProfileToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 100803452764E2C50013ED0E /* ProfileToolbar.swift */; }; 100803482764E37A0013ED0E /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 100803472764E37A0013ED0E /* ProfileView.swift */; }; + 1F04F16E297A9A700085F273 /* CalendarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F16D297A9A700085F273 /* CalendarService.swift */; }; + 1F04F171297AA5F40085F273 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F170297AA5F30085F273 /* Service.swift */; }; + 1F04F173297AD41B0085F273 /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F172297AD41B0085F273 /* CalendarEvent.swift */; }; + 1F04F175297AD4280085F273 /* CalendarScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F174297AD4280085F273 /* CalendarScreen.swift */; }; + 1F04F179297AED150085F273 /* LectureSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F178297AED150085F273 /* LectureSearchService.swift */; }; + 1F04F17F297BDF1E0085F273 /* PersonSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F17E297BDF1E0085F273 /* PersonSearchService.swift */; }; + 1F04F183297C3EF70085F273 /* PersonDetailedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F182297C3EF70085F273 /* PersonDetailedScreen.swift */; }; + 1F04F185297C3F990085F273 /* PersonDetailedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F184297C3F990085F273 /* PersonDetailedService.swift */; }; + 1F04F18A297C85120085F273 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F189297C85120085F273 /* Token.swift */; }; + 1F04F18C297C85190085F273 /* Confirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F04F18B297C85190085F273 /* Confirmation.swift */; }; + 1F183A172979D19000B5D22D /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F183A162979D19000B5D22D /* APIError.swift */; }; + 1F189E8D29968CE50056BBD8 /* TUMOnlineAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E8C29968CE50056BBD8 /* TUMOnlineAPI.swift */; }; + 1F189E8F29968CFC0056BBD8 /* TUMCabeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E8E29968CFC0056BBD8 /* TUMCabeAPI.swift */; }; + 1F189E9129968D130056BBD8 /* EatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9029968D130056BBD8 /* EatAPI.swift */; }; + 1F189E9329968D260056BBD8 /* TUMDevAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9229968D260056BBD8 /* TUMDevAppAPI.swift */; }; + 1F189E9529968D330056BBD8 /* TUMSexyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9429968D330056BBD8 /* TUMSexyAPI.swift */; }; + 1F189E9729968D490056BBD8 /* MVGAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9629968D490056BBD8 /* MVGAPI.swift */; }; + 1F189E9A29968D790056BBD8 /* TUMOnlineAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9929968D790056BBD8 /* TUMOnlineAPIError.swift */; }; + 1F189E9C29968D880056BBD8 /* TUMCabeAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9B29968D880056BBD8 /* TUMCabeAPIError.swift */; }; + 1F189E9E29968D9B0056BBD8 /* EatAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9D29968D9B0056BBD8 /* EatAPIError.swift */; }; + 1F189EA029968DA90056BBD8 /* TUMDevAppAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189E9F29968DA90056BBD8 /* TUMDevAppAPIError.swift */; }; + 1F189EA229968DB90056BBD8 /* TUMSexyAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189EA129968DB90056BBD8 /* TUMSexyAPIError.swift */; }; + 1F189EA429968DE60056BBD8 /* MVGAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189EA329968DE60056BBD8 /* MVGAPIError.swift */; }; + 1F189EA729968E5C0056BBD8 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F189EA629968E5C0056BBD8 /* API.swift */; }; 1F2068DC28FD6E2800DBDF67 /* LoginViewModel+LoginState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2068DB28FD6E2800DBDF67 /* LoginViewModel+LoginState.swift */; }; 1F2068DE28FD731200DBDF67 /* LoginViewModel+TokenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2068DD28FD731200DBDF67 /* LoginViewModel+TokenState.swift */; }; 1F2068E228FD73C400DBDF67 /* TokenPermissionsViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2068E128FD73C400DBDF67 /* TokenPermissionsViewModel+State.swift */; }; @@ -58,10 +81,50 @@ 1F54244F285CA059008363BC /* token-tutorial.mov in Resources */ = {isa = PBXBuildFile; fileRef = 1F54244E285CA059008363BC /* token-tutorial.mov */; }; 1F542450285CA059008363BC /* token-tutorial.mov in Resources */ = {isa = PBXBuildFile; fileRef = 1F54244E285CA059008363BC /* token-tutorial.mov */; }; 1F542451285CA059008363BC /* token-tutorial.mov in Resources */ = {isa = PBXBuildFile; fileRef = 1F54244E285CA059008363BC /* token-tutorial.mov */; }; + 1F69CE35297DB732005032CE /* NewsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE34297DB732005032CE /* NewsService.swift */; }; + 1F69CE37297DCA22005032CE /* NewsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE36297DCA22005032CE /* NewsScreen.swift */; }; + 1F69CE3C297DCC12005032CE /* MoviesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE3B297DCC12005032CE /* MoviesScreen.swift */; }; + 1F69CE3E297DCC19005032CE /* MovieService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE3D297DCC19005032CE /* MovieService.swift */; }; + 1F69CE40297DDCD3005032CE /* StudyRoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE3F297DDCD3005032CE /* StudyRoomDetailsScreen.swift */; }; + 1F69CE42297EC94E005032CE /* DishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE41297EC94E005032CE /* DishService.swift */; }; + 1F69CE44297EC97E005032CE /* DishViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE43297EC97E005032CE /* DishViewModel.swift */; }; + 1F69CE46297EC99D005032CE /* DishView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE45297EC99D005032CE /* DishView.swift */; }; + 1F69CE48297EDEA3005032CE /* TUMSexyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE47297EDEA3005032CE /* TUMSexyScreen.swift */; }; + 1F69CE4B297EDEC7005032CE /* TUMSexyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE4A297EDEC7005032CE /* TUMSexyService.swift */; }; + 1F69CE4E297EDF12005032CE /* TUMSexyLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F69CE4D297EDF12005032CE /* TUMSexyLink.swift */; }; + 1F71E80329E4611000379428 /* NavigaTumRoomFinderMaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7F629E4611000379428 /* NavigaTumRoomFinderMaps.swift */; }; + 1F71E80429E4611000379428 /* NavigaTumNavigationCoordinates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7F729E4611000379428 /* NavigaTumNavigationCoordinates.swift */; }; + 1F71E80529E4611000379428 /* NavigaTumOverlaysMaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7F829E4611000379428 /* NavigaTumOverlaysMaps.swift */; }; + 1F71E80629E4611000379428 /* NavigaTumNavigationMaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7F929E4611000379428 /* NavigaTumNavigationMaps.swift */; }; + 1F71E80729E4611000379428 /* NavigaTumNavigationAdditionalProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7FA29E4611000379428 /* NavigaTumNavigationAdditionalProperties.swift */; }; + 1F71E80829E4611000379428 /* NavigaTumOverlayMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7FB29E4611000379428 /* NavigaTumOverlayMap.swift */; }; + 1F71E80929E4611000379428 /* NavigaTumNavigationDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7FC29E4611000379428 /* NavigaTumNavigationDetails.swift */; }; + 1F71E80A29E4611000379428 /* NavigaTumSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7FE29E4611000379428 /* NavigaTumSearchResponse.swift */; }; + 1F71E80B29E4611000379428 /* NavigaTumSearchResponseSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E7FF29E4611000379428 /* NavigaTumSearchResponseSection.swift */; }; + 1F71E80C29E4611000379428 /* NavigaTumNavigationEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E80029E4611000379428 /* NavigaTumNavigationEntity.swift */; }; + 1F71E80D29E4611000379428 /* NavigaTumNavigationProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E80129E4611000379428 /* NavigaTumNavigationProperty.swift */; }; + 1F71E80E29E4611000379428 /* NavigaTumRoomFinderMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E80229E4611000379428 /* NavigaTumRoomFinderMap.swift */; }; + 1F71E81629E4611E00379428 /* RoomFinderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81529E4611E00379428 /* RoomFinderService.swift */; }; + 1F71E81929E4613500379428 /* NavigaTumDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81729E4613500379428 /* NavigaTumDetailsViewModel.swift */; }; + 1F71E81A29E4613500379428 /* NavigaTumViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81829E4613500379428 /* NavigaTumViewModel.swift */; }; + 1F71E82229E4613F00379428 /* NavigaTumDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81C29E4613F00379428 /* NavigaTumDetailsView.swift */; }; + 1F71E82329E4613F00379428 /* NavigaTumDetailsBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81D29E4613F00379428 /* NavigaTumDetailsBaseView.swift */; }; + 1F71E82429E4613F00379428 /* NavigaTumMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81E29E4613F00379428 /* NavigaTumMapView.swift */; }; + 1F71E82529E4613F00379428 /* NavigaTumMapImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E81F29E4613F00379428 /* NavigaTumMapImagesView.swift */; }; + 1F71E82629E4613F00379428 /* NavigaTumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E82029E4613F00379428 /* NavigaTumView.swift */; }; + 1F71E82729E4613F00379428 /* NavigaTumListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E82129E4613F00379428 /* NavigaTumListView.swift */; }; + 1F71E82D29E464C400379428 /* CafeteriaWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E82C29E464C400379428 /* CafeteriaWidgetViewModel.swift */; }; + 1F71E82F29E4667C00379428 /* NavigaTUMAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E82E29E4667C00379428 /* NavigaTUMAPI.swift */; }; + 1F71E83129E46A1000379428 /* NavigaTUMAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F71E83029E46A1000379428 /* NavigaTUMAPIError.swift */; }; + 1FA538EE297560CD004C70A8 /* MainAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA538ED297560CD004C70A8 /* MainAPI.swift */; }; + 1FACF3F92996A49300A0B8AC /* TUMDevAppAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FACF3F82996A49300A0B8AC /* TUMDevAppAPIOld.swift */; }; + 1FACF3FB2996A65700A0B8AC /* MealPlanService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FACF3FA2996A65700A0B8AC /* MealPlanService.swift */; }; + 1FACF3FD2996E34200A0B8AC /* MealPlanScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FACF3FC2996E34200A0B8AC /* MealPlanScreen.swift */; }; 1FAF9F0C284D2ABC000ABE93 /* MapScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF9F0B284D2ABC000ABE93 /* MapScreenView.swift */; }; 1FB82E3428F95776007B1858 /* TokenPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB82E3328F95776007B1858 /* TokenPermissionsView.swift */; }; 1FB82E3628F96C9E007B1858 /* TokenPermissionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB82E3528F96C9E007B1858 /* TokenPermissionsViewModel.swift */; }; 1FBFA168285E5B2D00FC1515 /* PanelContentListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBFA167285E5B2D00FC1515 /* PanelContentListView.swift */; }; + 1FFF9AC6297D31830098E874 /* ProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFF9AC5297D31830098E874 /* ProfileService.swift */; }; 226CB51E2798DF9C0043ABCA /* Snap in Frameworks */ = {isa = PBXBuildFile; productRef = 226CB51D2798DF9C0043ABCA /* Snap */; settings = {ATTRIBUTES = (Required, ); }; }; 2F1B2B8528652FC90023BD9A /* MovieDetailsBasicInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F1B2B8428652FC90023BD9A /* MovieDetailsBasicInfoView.swift */; }; 2F1B2B87286530120023BD9A /* MovieDetailsBasicInfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F1B2B86286530120023BD9A /* MovieDetailsBasicInfoRowView.swift */; }; @@ -91,13 +154,11 @@ 36108BE127A304B5007DC62D /* Cafeteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BCD27A304B5007DC62D /* Cafeteria.swift */; }; 36108BE227A304B5007DC62D /* MensaMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BCE27A304B5007DC62D /* MensaMenu.swift */; }; 36108BE327A304B5007DC62D /* DishLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BCF27A304B5007DC62D /* DishLabel.swift */; }; - 36108BE427A304B5007DC62D /* MensaEnumService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BD027A304B5007DC62D /* MensaEnumService.swift */; }; 36108BE527A304B5007DC62D /* MealPlanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BD227A304B5007DC62D /* MealPlanView.swift */; }; 36108BE627A304B5007DC62D /* MealPlanViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BD327A304B5007DC62D /* MealPlanViewModel.swift */; }; 36108BE727A304B5007DC62D /* MenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BD427A304B5007DC62D /* MenuViewModel.swift */; }; 36108BE927A304B5007DC62D /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BD627A304B5007DC62D /* MenuView.swift */; }; 36108BEB27A304B6007DC62D /* CafeteriaRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BD927A304B5007DC62D /* CafeteriaRowView.swift */; }; - 36108BED27A304B6007DC62D /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BDB27A304B5007DC62D /* MapView.swift */; }; 36108BEF27A304B6007DC62D /* MapContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BDD27A304B5007DC62D /* MapContentView.swift */; }; 36108BF027A304B6007DC62D /* PanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BDE27A304B5007DC62D /* PanelContentView.swift */; }; 36108BFA27A30517007DC62D /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36108BF327A30516007DC62D /* Movie.swift */; }; @@ -137,7 +198,6 @@ 3654F364285168D2008AD5DC /* MapImagesHorizontalScrollingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F35E285168D2008AD5DC /* MapImagesHorizontalScrollingView.swift */; }; 3654F365285168D2008AD5DC /* RoomImageMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F35F285168D2008AD5DC /* RoomImageMapping.swift */; }; 3654F366285168D2008AD5DC /* StudyRoomAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F360285168D2008AD5DC /* StudyRoomAttribute.swift */; }; - 3654F368285169AC008AD5DC /* TUMDevAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F367285169AC008AD5DC /* TUMDevAppAPI.swift */; }; 3654F3762851710E008AD5DC /* RoomFinderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F36B2851710E008AD5DC /* RoomFinderViewModel.swift */; }; 3654F3772851710E008AD5DC /* RoomFinderMapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F36C2851710E008AD5DC /* RoomFinderMapViewModel.swift */; }; 3654F3782851710E008AD5DC /* FoundRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F36E2851710E008AD5DC /* FoundRoom.swift */; }; @@ -148,7 +208,7 @@ 3654F37D2851710E008AD5DC /* RoomFinderDetailsMapImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F3742851710E008AD5DC /* RoomFinderDetailsMapImagesView.swift */; }; 3654F37E2851710E008AD5DC /* RoomFinderDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F3752851710E008AD5DC /* RoomFinderDetailsView.swift */; }; 3654F38028517156008AD5DC /* ImageFullScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F37F28517156008AD5DC /* ImageFullScreenView.swift */; }; - 3654F38428517260008AD5DC /* StudyRoomVIewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F38328517260008AD5DC /* StudyRoomVIewModel.swift */; }; + 3654F38428517260008AD5DC /* StudyRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F38328517260008AD5DC /* StudyRoomViewModel.swift */; }; 3654F38628517BB4008AD5DC /* CafeteriaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F38528517BB4008AD5DC /* CafeteriaView.swift */; }; 3654F388285185A4008AD5DC /* StudyRoomGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F387285185A4008AD5DC /* StudyRoomGroupView.swift */; }; 3654F38A28518640008AD5DC /* StudyRoomDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3654F38928518640008AD5DC /* StudyRoomDetailsView.swift */; }; @@ -171,20 +231,19 @@ 36AD5CF227B7FEAD00DAE143 /* TumCalendarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF127B7FEAD00DAE143 /* TumCalendarStyle.swift */; }; 36AD5CF427B8C83500DAE143 /* CalendarSingleEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF327B8C83500DAE143 /* CalendarSingleEventView.swift */; }; 36AD5CF627B8D97500DAE143 /* LectureDetailsEventInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF527B8D97500DAE143 /* LectureDetailsEventInfoView.swift */; }; - 36AD5CF827B96AD200DAE143 /* PersonSearchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF727B96AD200DAE143 /* PersonSearchListView.swift */; }; - 36AD5CFA27B9711B00DAE143 /* LectureSearchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF927B9711B00DAE143 /* LectureSearchListView.swift */; }; - 36AD5CFC27B974F100DAE143 /* ProfileMyTumSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CFB27B974F100DAE143 /* ProfileMyTumSection.swift */; }; + 36AD5CF827B96AD200DAE143 /* PersonSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF727B96AD200DAE143 /* PersonSearchView.swift */; }; + 36AD5CFA27B9711B00DAE143 /* LectureSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CF927B9711B00DAE143 /* LectureSearchView.swift */; }; + 36AD5CFC27B974F100DAE143 /* TuitionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD5CFB27B974F100DAE143 /* TuitionScreen.swift */; }; 36AD5CFE27BA064E00DAE143 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 36AD5CFD27BA064E00DAE143 /* GoogleService-Info.plist */; }; - 36AF61D827A2FD7800FEBD98 /* EntityImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61B927A2FD7700FEBD98 /* EntityImporter.swift */; }; - 36AF61D927A2FD7800FEBD98 /* TUMSexyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BB27A2FD7700FEBD98 /* TUMSexyAPI.swift */; }; - 36AF61DA27A2FD7800FEBD98 /* EatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BC27A2FD7700FEBD98 /* EatAPI.swift */; }; - 36AF61DB27A2FD7800FEBD98 /* MVGAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BD27A2FD7700FEBD98 /* MVGAPI.swift */; }; - 36AF61DC27A2FD7800FEBD98 /* NetworkingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BE27A2FD7700FEBD98 /* NetworkingAPI.swift */; }; - 36AF61DD27A2FD7800FEBD98 /* CampusOnlineAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BF27A2FD7700FEBD98 /* CampusOnlineAPI.swift */; }; + 36AF61D927A2FD7800FEBD98 /* TUMSexyAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BB27A2FD7700FEBD98 /* TUMSexyAPIOld.swift */; }; + 36AF61DA27A2FD7800FEBD98 /* EatAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BC27A2FD7700FEBD98 /* EatAPIOld.swift */; }; + 36AF61DB27A2FD7800FEBD98 /* MVGAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BD27A2FD7700FEBD98 /* MVGAPIOld.swift */; }; + 36AF61DC27A2FD7800FEBD98 /* NetworkingAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BE27A2FD7700FEBD98 /* NetworkingAPIOld.swift */; }; + 36AF61DD27A2FD7800FEBD98 /* CampusOnlineAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61BF27A2FD7700FEBD98 /* CampusOnlineAPIOld.swift */; }; 36AF61DE27A2FD7800FEBD98 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C027A2FD7700FEBD98 /* APIResponse.swift */; }; - 36AF61DF27A2FD7800FEBD98 /* TUMOnlineAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C127A2FD7700FEBD98 /* TUMOnlineAPI.swift */; }; + 36AF61DF27A2FD7800FEBD98 /* TUMOnlineAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C127A2FD7700FEBD98 /* TUMOnlineAPIOld.swift */; }; 36AF61E027A2FD7800FEBD98 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C227A2FD7700FEBD98 /* Cache.swift */; }; - 36AF61E127A2FD7800FEBD98 /* TUMCabeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C327A2FD7700FEBD98 /* TUMCabeAPI.swift */; }; + 36AF61E127A2FD7800FEBD98 /* TUMCabeAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C327A2FD7700FEBD98 /* TUMCabeAPIOld.swift */; }; 36AF61E227A2FD7800FEBD98 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C527A2FD7700FEBD98 /* Constants.swift */; }; 36AF61E327A2FD7800FEBD98 /* APIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C627A2FD7700FEBD98 /* APIConstants.swift */; }; 36AF61E427A2FD7800FEBD98 /* NetworkingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61C827A2FD7700FEBD98 /* NetworkingError.swift */; }; @@ -204,7 +263,7 @@ 36BB6F5327AFCCB500F224AB /* PersonDetailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F5227AFCCB500F224AB /* PersonDetailedView.swift */; }; 36BB6F6027AFCDFA00F224AB /* PersonSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F5B27AFCDF900F224AB /* PersonSearchViewModel.swift */; }; 36BB6F6127AFCDFA00F224AB /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F5D27AFCDFA00F224AB /* Person.swift */; }; - 36BB6F6227AFCDFA00F224AB /* PersonSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F5F27AFCDFA00F224AB /* PersonSearchView.swift */; }; + 36BB6F6227AFCDFA00F224AB /* PersonSearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F5F27AFCDFA00F224AB /* PersonSearchScreen.swift */; }; 36BB6F6427AFCFFB00F224AB /* PersonDetailedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F6327AFCFFB00F224AB /* PersonDetailedViewModel.swift */; }; 36BB6F6627AFD12B00F224AB /* PersonDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F6527AFD12B00F224AB /* PersonDetails.swift */; }; 36BB6F6827AFD26500F224AB /* Organization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F6727AFD26500F224AB /* Organization.swift */; }; @@ -215,11 +274,9 @@ 36BB6F7527B1D87200F224AB /* Tuition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F7427B1D87200F224AB /* Tuition.swift */; }; 36BB6F7927B26DE300F224AB /* TuitionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F7827B26DE300F224AB /* TuitionView.swift */; }; 36BB6F7B27B27D0D00F224AB /* TuitionCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F7A27B27D0D00F224AB /* TuitionCard.swift */; }; - 36BB6F7D27B356C200F224AB /* PersonDetailedCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F7C27B356C200F224AB /* PersonDetailedCellView.swift */; }; 36BB6F7F27B386D100F224AB /* AddToContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F7E27B386D100F224AB /* AddToContactsView.swift */; }; - 36BB6F8327B39B4300F224AB /* LectureSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F8227B39B4300F224AB /* LectureSearchView.swift */; }; + 36BB6F8327B39B4300F224AB /* LectureSearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F8227B39B4300F224AB /* LectureSearchScreen.swift */; }; 36BB6F8627B39C5300F224AB /* LectureSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F8527B39C5300F224AB /* LectureSearchViewModel.swift */; }; - 36BB6F8A27B3D21200F224AB /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F8927B3D21200F224AB /* CalendarEvent.swift */; }; 36BB6F8D27B3F25A00F224AB /* NSMutableString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F8C27B3F25A00F224AB /* NSMutableString+Extensions.swift */; }; 36BBE72F27989F8C0018FD3F /* SFSafariViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BBE72E27989F8C0018FD3F /* SFSafariViewWrapper.swift */; }; 36BBE7322798AFE10018FD3F /* News.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BBE7312798AFE10018FD3F /* News.swift */; }; @@ -301,7 +358,6 @@ 08DFB97428664CFC00E357DF /* TuitionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuitionDetailsView.swift; sourceTree = ""; }; 08DFB9762866506900E357DF /* WidgetFrameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetFrameView.swift; sourceTree = ""; }; 08DFB97828666AD900E357DF /* CafeteriaWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CafeteriaWidgetView.swift; sourceTree = ""; }; - 08DFB97C2867800C00E357DF /* CafeteriaWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CafeteriaWidgetViewModel.swift; sourceTree = ""; }; 08DFB97E2867AC9200E357DF /* StudyRoomWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyRoomWidgetView.swift; sourceTree = ""; }; 08DFB9802867ACB600E357DF /* StudyRoomWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyRoomWidgetViewModel.swift; sourceTree = ""; }; 08FAFD14287DC484006A0E27 /* CalendarWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarWidgetView.swift; sourceTree = ""; }; @@ -315,6 +371,30 @@ 08FAFD282898B6C8006A0E27 /* SpatioTemporalStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpatioTemporalStrategy.swift; sourceTree = ""; }; 100803452764E2C50013ED0E /* ProfileToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileToolbar.swift; sourceTree = ""; }; 100803472764E37A0013ED0E /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 1F04F16D297A9A700085F273 /* CalendarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarService.swift; sourceTree = ""; }; + 1F04F170297AA5F30085F273 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; + 1F04F172297AD41B0085F273 /* CalendarEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarEvent.swift; sourceTree = ""; }; + 1F04F174297AD4280085F273 /* CalendarScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarScreen.swift; sourceTree = ""; }; + 1F04F178297AED150085F273 /* LectureSearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchService.swift; sourceTree = ""; }; + 1F04F17E297BDF1E0085F273 /* PersonSearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonSearchService.swift; sourceTree = ""; }; + 1F04F182297C3EF70085F273 /* PersonDetailedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetailedScreen.swift; sourceTree = ""; }; + 1F04F184297C3F990085F273 /* PersonDetailedService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetailedService.swift; sourceTree = ""; }; + 1F04F189297C85120085F273 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + 1F04F18B297C85190085F273 /* Confirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Confirmation.swift; sourceTree = ""; }; + 1F183A162979D19000B5D22D /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; + 1F189E8C29968CE50056BBD8 /* TUMOnlineAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMOnlineAPI.swift; sourceTree = ""; }; + 1F189E8E29968CFC0056BBD8 /* TUMCabeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMCabeAPI.swift; sourceTree = ""; }; + 1F189E9029968D130056BBD8 /* EatAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EatAPI.swift; sourceTree = ""; }; + 1F189E9229968D260056BBD8 /* TUMDevAppAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMDevAppAPI.swift; sourceTree = ""; }; + 1F189E9429968D330056BBD8 /* TUMSexyAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMSexyAPI.swift; sourceTree = ""; }; + 1F189E9629968D490056BBD8 /* MVGAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVGAPI.swift; sourceTree = ""; }; + 1F189E9929968D790056BBD8 /* TUMOnlineAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMOnlineAPIError.swift; sourceTree = ""; }; + 1F189E9B29968D880056BBD8 /* TUMCabeAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMCabeAPIError.swift; sourceTree = ""; }; + 1F189E9D29968D9B0056BBD8 /* EatAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EatAPIError.swift; sourceTree = ""; }; + 1F189E9F29968DA90056BBD8 /* TUMDevAppAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMDevAppAPIError.swift; sourceTree = ""; }; + 1F189EA129968DB90056BBD8 /* TUMSexyAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMSexyAPIError.swift; sourceTree = ""; }; + 1F189EA329968DE60056BBD8 /* MVGAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVGAPIError.swift; sourceTree = ""; }; + 1F189EA629968E5C0056BBD8 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 1F2068DB28FD6E2800DBDF67 /* LoginViewModel+LoginState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewModel+LoginState.swift"; sourceTree = ""; }; 1F2068DD28FD731200DBDF67 /* LoginViewModel+TokenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewModel+TokenState.swift"; sourceTree = ""; }; 1F2068E128FD73C400DBDF67 /* TokenPermissionsViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TokenPermissionsViewModel+State.swift"; sourceTree = ""; }; @@ -325,10 +405,50 @@ 1F4C836628300E79006971C0 /* CafeteriasService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CafeteriasService.swift; sourceTree = ""; }; 1F4C926E2882FD84003DC7D7 /* RoundedCorners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCorners.swift; sourceTree = ""; }; 1F54244E285CA059008363BC /* token-tutorial.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = "token-tutorial.mov"; sourceTree = ""; }; + 1F69CE34297DB732005032CE /* NewsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsService.swift; sourceTree = ""; }; + 1F69CE36297DCA22005032CE /* NewsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsScreen.swift; sourceTree = ""; }; + 1F69CE3B297DCC12005032CE /* MoviesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesScreen.swift; sourceTree = ""; }; + 1F69CE3D297DCC19005032CE /* MovieService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieService.swift; sourceTree = ""; }; + 1F69CE3F297DDCD3005032CE /* StudyRoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyRoomDetailsScreen.swift; sourceTree = ""; }; + 1F69CE41297EC94E005032CE /* DishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishService.swift; sourceTree = ""; }; + 1F69CE43297EC97E005032CE /* DishViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishViewModel.swift; sourceTree = ""; }; + 1F69CE45297EC99D005032CE /* DishView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishView.swift; sourceTree = ""; }; + 1F69CE47297EDEA3005032CE /* TUMSexyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMSexyScreen.swift; sourceTree = ""; }; + 1F69CE4A297EDEC7005032CE /* TUMSexyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMSexyService.swift; sourceTree = ""; }; + 1F69CE4D297EDF12005032CE /* TUMSexyLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUMSexyLink.swift; sourceTree = ""; }; + 1F71E7F629E4611000379428 /* NavigaTumRoomFinderMaps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumRoomFinderMaps.swift; sourceTree = ""; }; + 1F71E7F729E4611000379428 /* NavigaTumNavigationCoordinates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumNavigationCoordinates.swift; sourceTree = ""; }; + 1F71E7F829E4611000379428 /* NavigaTumOverlaysMaps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumOverlaysMaps.swift; sourceTree = ""; }; + 1F71E7F929E4611000379428 /* NavigaTumNavigationMaps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumNavigationMaps.swift; sourceTree = ""; }; + 1F71E7FA29E4611000379428 /* NavigaTumNavigationAdditionalProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumNavigationAdditionalProperties.swift; sourceTree = ""; }; + 1F71E7FB29E4611000379428 /* NavigaTumOverlayMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumOverlayMap.swift; sourceTree = ""; }; + 1F71E7FC29E4611000379428 /* NavigaTumNavigationDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumNavigationDetails.swift; sourceTree = ""; }; + 1F71E7FE29E4611000379428 /* NavigaTumSearchResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumSearchResponse.swift; sourceTree = ""; }; + 1F71E7FF29E4611000379428 /* NavigaTumSearchResponseSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumSearchResponseSection.swift; sourceTree = ""; }; + 1F71E80029E4611000379428 /* NavigaTumNavigationEntity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumNavigationEntity.swift; sourceTree = ""; }; + 1F71E80129E4611000379428 /* NavigaTumNavigationProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumNavigationProperty.swift; sourceTree = ""; }; + 1F71E80229E4611000379428 /* NavigaTumRoomFinderMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumRoomFinderMap.swift; sourceTree = ""; }; + 1F71E81529E4611E00379428 /* RoomFinderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomFinderService.swift; sourceTree = ""; }; + 1F71E81729E4613500379428 /* NavigaTumDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumDetailsViewModel.swift; sourceTree = ""; }; + 1F71E81829E4613500379428 /* NavigaTumViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumViewModel.swift; sourceTree = ""; }; + 1F71E81C29E4613F00379428 /* NavigaTumDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumDetailsView.swift; sourceTree = ""; }; + 1F71E81D29E4613F00379428 /* NavigaTumDetailsBaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumDetailsBaseView.swift; sourceTree = ""; }; + 1F71E81E29E4613F00379428 /* NavigaTumMapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumMapView.swift; sourceTree = ""; }; + 1F71E81F29E4613F00379428 /* NavigaTumMapImagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumMapImagesView.swift; sourceTree = ""; }; + 1F71E82029E4613F00379428 /* NavigaTumView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumView.swift; sourceTree = ""; }; + 1F71E82129E4613F00379428 /* NavigaTumListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigaTumListView.swift; sourceTree = ""; }; + 1F71E82C29E464C400379428 /* CafeteriaWidgetViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CafeteriaWidgetViewModel.swift; sourceTree = ""; }; + 1F71E82E29E4667C00379428 /* NavigaTUMAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigaTUMAPI.swift; sourceTree = ""; }; + 1F71E83029E46A1000379428 /* NavigaTUMAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigaTUMAPIError.swift; sourceTree = ""; }; + 1FA538ED297560CD004C70A8 /* MainAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainAPI.swift; sourceTree = ""; }; + 1FACF3F82996A49300A0B8AC /* TUMDevAppAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMDevAppAPIOld.swift; sourceTree = ""; }; + 1FACF3FA2996A65700A0B8AC /* MealPlanService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPlanService.swift; sourceTree = ""; }; + 1FACF3FC2996E34200A0B8AC /* MealPlanScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPlanScreen.swift; sourceTree = ""; }; 1FAF9F0B284D2ABC000ABE93 /* MapScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScreenView.swift; sourceTree = ""; }; 1FB82E3328F95776007B1858 /* TokenPermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPermissionsView.swift; sourceTree = ""; }; 1FB82E3528F96C9E007B1858 /* TokenPermissionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPermissionsViewModel.swift; sourceTree = ""; }; 1FBFA167285E5B2D00FC1515 /* PanelContentListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelContentListView.swift; sourceTree = ""; }; + 1FFF9AC5297D31830098E874 /* ProfileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileService.swift; sourceTree = ""; }; 227FBB492762AC440062FEC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 227FBB4A2762AC4C0062FEC3 /* Campus-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Campus-iOS.entitlements"; sourceTree = ""; }; 256D0D4227D77A9C00F5EC38 /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = ""; }; @@ -360,13 +480,11 @@ 36108BCD27A304B5007DC62D /* Cafeteria.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cafeteria.swift; sourceTree = ""; }; 36108BCE27A304B5007DC62D /* MensaMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MensaMenu.swift; sourceTree = ""; }; 36108BCF27A304B5007DC62D /* DishLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DishLabel.swift; sourceTree = ""; }; - 36108BD027A304B5007DC62D /* MensaEnumService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MensaEnumService.swift; sourceTree = ""; }; 36108BD227A304B5007DC62D /* MealPlanView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealPlanView.swift; sourceTree = ""; }; 36108BD327A304B5007DC62D /* MealPlanViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealPlanViewModel.swift; sourceTree = ""; }; 36108BD427A304B5007DC62D /* MenuViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuViewModel.swift; sourceTree = ""; }; 36108BD627A304B5007DC62D /* MenuView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; 36108BD927A304B5007DC62D /* CafeteriaRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CafeteriaRowView.swift; sourceTree = ""; }; - 36108BDB27A304B5007DC62D /* MapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; 36108BDD27A304B5007DC62D /* MapContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapContentView.swift; sourceTree = ""; }; 36108BDE27A304B5007DC62D /* PanelContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanelContentView.swift; sourceTree = ""; }; 36108BF327A30516007DC62D /* Movie.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; @@ -406,7 +524,6 @@ 3654F35E285168D2008AD5DC /* MapImagesHorizontalScrollingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapImagesHorizontalScrollingView.swift; sourceTree = ""; }; 3654F35F285168D2008AD5DC /* RoomImageMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomImageMapping.swift; sourceTree = ""; }; 3654F360285168D2008AD5DC /* StudyRoomAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StudyRoomAttribute.swift; sourceTree = ""; }; - 3654F367285169AC008AD5DC /* TUMDevAppAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMDevAppAPI.swift; sourceTree = ""; }; 3654F36B2851710E008AD5DC /* RoomFinderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomFinderViewModel.swift; sourceTree = ""; }; 3654F36C2851710E008AD5DC /* RoomFinderMapViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomFinderMapViewModel.swift; sourceTree = ""; }; 3654F36E2851710E008AD5DC /* FoundRoom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundRoom.swift; sourceTree = ""; }; @@ -417,7 +534,7 @@ 3654F3742851710E008AD5DC /* RoomFinderDetailsMapImagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomFinderDetailsMapImagesView.swift; sourceTree = ""; }; 3654F3752851710E008AD5DC /* RoomFinderDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomFinderDetailsView.swift; sourceTree = ""; }; 3654F37F28517156008AD5DC /* ImageFullScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFullScreenView.swift; sourceTree = ""; }; - 3654F38328517260008AD5DC /* StudyRoomVIewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StudyRoomVIewModel.swift; sourceTree = ""; }; + 3654F38328517260008AD5DC /* StudyRoomViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StudyRoomViewModel.swift; sourceTree = ""; }; 3654F38528517BB4008AD5DC /* CafeteriaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CafeteriaView.swift; sourceTree = ""; }; 3654F387285185A4008AD5DC /* StudyRoomGroupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StudyRoomGroupView.swift; sourceTree = ""; }; 3654F38928518640008AD5DC /* StudyRoomDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StudyRoomDetailsView.swift; sourceTree = ""; }; @@ -443,20 +560,19 @@ 36AD5CF127B7FEAD00DAE143 /* TumCalendarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TumCalendarStyle.swift; sourceTree = ""; }; 36AD5CF327B8C83500DAE143 /* CalendarSingleEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarSingleEventView.swift; sourceTree = ""; }; 36AD5CF527B8D97500DAE143 /* LectureDetailsEventInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureDetailsEventInfoView.swift; sourceTree = ""; }; - 36AD5CF727B96AD200DAE143 /* PersonSearchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonSearchListView.swift; sourceTree = ""; }; - 36AD5CF927B9711B00DAE143 /* LectureSearchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchListView.swift; sourceTree = ""; }; - 36AD5CFB27B974F100DAE143 /* ProfileMyTumSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileMyTumSection.swift; sourceTree = ""; }; + 36AD5CF727B96AD200DAE143 /* PersonSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonSearchView.swift; sourceTree = ""; }; + 36AD5CF927B9711B00DAE143 /* LectureSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchView.swift; sourceTree = ""; }; + 36AD5CFB27B974F100DAE143 /* TuitionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuitionScreen.swift; sourceTree = ""; }; 36AD5CFD27BA064E00DAE143 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 36AF61B927A2FD7700FEBD98 /* EntityImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntityImporter.swift; sourceTree = ""; }; - 36AF61BB27A2FD7700FEBD98 /* TUMSexyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMSexyAPI.swift; sourceTree = ""; }; - 36AF61BC27A2FD7700FEBD98 /* EatAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EatAPI.swift; sourceTree = ""; }; - 36AF61BD27A2FD7700FEBD98 /* MVGAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MVGAPI.swift; sourceTree = ""; }; - 36AF61BE27A2FD7700FEBD98 /* NetworkingAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingAPI.swift; sourceTree = ""; }; - 36AF61BF27A2FD7700FEBD98 /* CampusOnlineAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CampusOnlineAPI.swift; sourceTree = ""; }; + 36AF61BB27A2FD7700FEBD98 /* TUMSexyAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMSexyAPIOld.swift; sourceTree = ""; }; + 36AF61BC27A2FD7700FEBD98 /* EatAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EatAPIOld.swift; sourceTree = ""; }; + 36AF61BD27A2FD7700FEBD98 /* MVGAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MVGAPIOld.swift; sourceTree = ""; }; + 36AF61BE27A2FD7700FEBD98 /* NetworkingAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingAPIOld.swift; sourceTree = ""; }; + 36AF61BF27A2FD7700FEBD98 /* CampusOnlineAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CampusOnlineAPIOld.swift; sourceTree = ""; }; 36AF61C027A2FD7700FEBD98 /* APIResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = ""; }; - 36AF61C127A2FD7700FEBD98 /* TUMOnlineAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMOnlineAPI.swift; sourceTree = ""; }; + 36AF61C127A2FD7700FEBD98 /* TUMOnlineAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMOnlineAPIOld.swift; sourceTree = ""; }; 36AF61C227A2FD7700FEBD98 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; - 36AF61C327A2FD7700FEBD98 /* TUMCabeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMCabeAPI.swift; sourceTree = ""; }; + 36AF61C327A2FD7700FEBD98 /* TUMCabeAPIOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMCabeAPIOld.swift; sourceTree = ""; }; 36AF61C527A2FD7700FEBD98 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 36AF61C627A2FD7700FEBD98 /* APIConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIConstants.swift; sourceTree = ""; }; 36AF61C827A2FD7700FEBD98 /* NetworkingError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingError.swift; sourceTree = ""; }; @@ -476,7 +592,7 @@ 36BB6F5227AFCCB500F224AB /* PersonDetailedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetailedView.swift; sourceTree = ""; }; 36BB6F5B27AFCDF900F224AB /* PersonSearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonSearchViewModel.swift; sourceTree = ""; }; 36BB6F5D27AFCDFA00F224AB /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = ""; }; - 36BB6F5F27AFCDFA00F224AB /* PersonSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonSearchView.swift; sourceTree = ""; }; + 36BB6F5F27AFCDFA00F224AB /* PersonSearchScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonSearchScreen.swift; sourceTree = ""; }; 36BB6F6327AFCFFB00F224AB /* PersonDetailedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetailedViewModel.swift; sourceTree = ""; }; 36BB6F6527AFD12B00F224AB /* PersonDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetails.swift; sourceTree = ""; }; 36BB6F6727AFD26500F224AB /* Organization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Organization.swift; sourceTree = ""; }; @@ -487,11 +603,9 @@ 36BB6F7427B1D87200F224AB /* Tuition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tuition.swift; sourceTree = ""; }; 36BB6F7827B26DE300F224AB /* TuitionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuitionView.swift; sourceTree = ""; }; 36BB6F7A27B27D0D00F224AB /* TuitionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuitionCard.swift; sourceTree = ""; }; - 36BB6F7C27B356C200F224AB /* PersonDetailedCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetailedCellView.swift; sourceTree = ""; }; 36BB6F7E27B386D100F224AB /* AddToContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToContactsView.swift; sourceTree = ""; }; - 36BB6F8227B39B4300F224AB /* LectureSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchView.swift; sourceTree = ""; }; + 36BB6F8227B39B4300F224AB /* LectureSearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchScreen.swift; sourceTree = ""; }; 36BB6F8527B39C5300F224AB /* LectureSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchViewModel.swift; sourceTree = ""; }; - 36BB6F8927B3D21200F224AB /* CalendarEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CalendarEvent.swift; path = "Campus-iOS/CalendarComponent/Entity/CalendarEvent.swift"; sourceTree = SOURCE_ROOT; }; 36BB6F8C27B3F25A00F224AB /* NSMutableString+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableString+Extensions.swift"; sourceTree = ""; }; 36BBE72E27989F8C0018FD3F /* SFSafariViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSafariViewWrapper.swift; sourceTree = ""; }; 36BBE7312798AFE10018FD3F /* News.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = News.swift; sourceTree = ""; }; @@ -636,6 +750,7 @@ 100803442764E2A90013ED0E /* ProfileComponent */ = { isa = PBXGroup; children = ( + 1FFF9AC4297D317C0098E874 /* Service */, 36BB6F7127B1CD8100F224AB /* ViewModel */, 36BB6F6E27B1196300F224AB /* Entity */, 36BB6F6D27B1195600F224AB /* View */, @@ -643,12 +758,142 @@ path = ProfileComponent; sourceTree = ""; }; + 1F04F16F297AA1E00085F273 /* Old APIs */ = { + isa = PBXGroup; + children = ( + 36AF61C027A2FD7700FEBD98 /* APIResponse.swift */, + 36AF61BB27A2FD7700FEBD98 /* TUMSexyAPIOld.swift */, + 1FACF3F82996A49300A0B8AC /* TUMDevAppAPIOld.swift */, + 36AF61BC27A2FD7700FEBD98 /* EatAPIOld.swift */, + 36AF61BD27A2FD7700FEBD98 /* MVGAPIOld.swift */, + 36AF61BE27A2FD7700FEBD98 /* NetworkingAPIOld.swift */, + 36AF61BF27A2FD7700FEBD98 /* CampusOnlineAPIOld.swift */, + 36AF61C127A2FD7700FEBD98 /* TUMOnlineAPIOld.swift */, + 36AF61C327A2FD7700FEBD98 /* TUMCabeAPIOld.swift */, + ); + path = "Old APIs"; + sourceTree = ""; + }; + 1F04F176297AD42E0085F273 /* Screen */ = { + isa = PBXGroup; + children = ( + 1F04F174297AD4280085F273 /* CalendarScreen.swift */, + ); + path = Screen; + sourceTree = ""; + }; + 1F04F177297AD4350085F273 /* Service */ = { + isa = PBXGroup; + children = ( + 1F04F16D297A9A700085F273 /* CalendarService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F04F17A297AED190085F273 /* Service */ = { + isa = PBXGroup; + children = ( + 1F04F178297AED150085F273 /* LectureSearchService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F04F17B297B0BC30085F273 /* Screen */ = { + isa = PBXGroup; + children = ( + 36BB6F8227B39B4300F224AB /* LectureSearchScreen.swift */, + ); + path = Screen; + sourceTree = ""; + }; + 1F04F180297BDF250085F273 /* Service */ = { + isa = PBXGroup; + children = ( + 1F04F17E297BDF1E0085F273 /* PersonSearchService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F04F181297C33860085F273 /* Screen */ = { + isa = PBXGroup; + children = ( + 36BB6F5F27AFCDFA00F224AB /* PersonSearchScreen.swift */, + ); + path = Screen; + sourceTree = ""; + }; + 1F04F186297C3FA20085F273 /* Service */ = { + isa = PBXGroup; + children = ( + 1F04F184297C3F990085F273 /* PersonDetailedService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F04F187297C6BC40085F273 /* Screen */ = { + isa = PBXGroup; + children = ( + 1F04F182297C3EF70085F273 /* PersonDetailedScreen.swift */, + ); + path = Screen; + sourceTree = ""; + }; + 1F04F188297C85030085F273 /* Model */ = { + isa = PBXGroup; + children = ( + 36203E8A2761C6EC00C24658 /* Credentials.swift */, + 1F04F189297C85120085F273 /* Token.swift */, + 1F04F18B297C85190085F273 /* Confirmation.swift */, + ); + path = Model; + sourceTree = ""; + }; + 1F189E8B29968CD70056BBD8 /* APIs */ = { + isa = PBXGroup; + children = ( + 1F189E8C29968CE50056BBD8 /* TUMOnlineAPI.swift */, + 1F189E8E29968CFC0056BBD8 /* TUMCabeAPI.swift */, + 1F189E9029968D130056BBD8 /* EatAPI.swift */, + 1F189E9229968D260056BBD8 /* TUMDevAppAPI.swift */, + 1F189E9429968D330056BBD8 /* TUMSexyAPI.swift */, + 1F189E9629968D490056BBD8 /* MVGAPI.swift */, + 1F71E82E29E4667C00379428 /* NavigaTUMAPI.swift */, + ); + path = APIs; + sourceTree = ""; + }; + 1F189E9829968D620056BBD8 /* APIErrors */ = { + isa = PBXGroup; + children = ( + 1F189E9929968D790056BBD8 /* TUMOnlineAPIError.swift */, + 1F189E9B29968D880056BBD8 /* TUMCabeAPIError.swift */, + 1F189E9D29968D9B0056BBD8 /* EatAPIError.swift */, + 1F189E9F29968DA90056BBD8 /* TUMDevAppAPIError.swift */, + 1F189EA129968DB90056BBD8 /* TUMSexyAPIError.swift */, + 1F189EA329968DE60056BBD8 /* MVGAPIError.swift */, + 1F71E83029E46A1000379428 /* NavigaTUMAPIError.swift */, + ); + path = APIErrors; + sourceTree = ""; + }; + 1F189EA529968E150056BBD8 /* Protocols */ = { + isa = PBXGroup; + children = ( + 1FA538ED297560CD004C70A8 /* MainAPI.swift */, + 1F189EA629968E5C0056BBD8 /* API.swift */, + 1F183A162979D19000B5D22D /* APIError.swift */, + 1F04F170297AA5F30085F273 /* Service.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 1F4C836528300E6F006971C0 /* Service */ = { isa = PBXGroup; children = ( - 36108BD027A304B5007DC62D /* MensaEnumService.swift */, 1F4C836628300E79006971C0 /* CafeteriasService.swift */, 3654F357285167C3008AD5DC /* StudyRoomsService.swift */, + 1F69CE41297EC94E005032CE /* DishService.swift */, + 1FACF3FA2996A65700A0B8AC /* MealPlanService.swift */, ); path = Service; sourceTree = ""; @@ -661,6 +906,118 @@ path = VideoAssets; sourceTree = ""; }; + 1F69CE33297DB729005032CE /* Service */ = { + isa = PBXGroup; + children = ( + 1F69CE34297DB732005032CE /* NewsService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F69CE38297DCA2B005032CE /* NewsScreen */ = { + isa = PBXGroup; + children = ( + 1F69CE36297DCA22005032CE /* NewsScreen.swift */, + ); + path = NewsScreen; + sourceTree = ""; + }; + 1F69CE39297DCBFE005032CE /* Service */ = { + isa = PBXGroup; + children = ( + 1F69CE3D297DCC19005032CE /* MovieService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F69CE3A297DCC04005032CE /* Screen */ = { + isa = PBXGroup; + children = ( + 1F69CE3B297DCC12005032CE /* MoviesScreen.swift */, + ); + path = Screen; + sourceTree = ""; + }; + 1F69CE49297EDEB5005032CE /* Screen */ = { + isa = PBXGroup; + children = ( + 1F69CE47297EDEA3005032CE /* TUMSexyScreen.swift */, + ); + path = Screen; + sourceTree = ""; + }; + 1F69CE4C297EDECA005032CE /* Service */ = { + isa = PBXGroup; + children = ( + 1F69CE4A297EDEC7005032CE /* TUMSexyService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F69CE4F297EDF32005032CE /* Model */ = { + isa = PBXGroup; + children = ( + 1F69CE4D297EDF12005032CE /* TUMSexyLink.swift */, + ); + path = Model; + sourceTree = ""; + }; + 1F71E7F429E4611000379428 /* Model */ = { + isa = PBXGroup; + children = ( + 1F71E7F529E4611000379428 /* Details */, + 1F71E7FB29E4611000379428 /* NavigaTumOverlayMap.swift */, + 1F71E7FC29E4611000379428 /* NavigaTumNavigationDetails.swift */, + 1F71E7FD29E4611000379428 /* Search */, + 1F71E80029E4611000379428 /* NavigaTumNavigationEntity.swift */, + 1F71E80129E4611000379428 /* NavigaTumNavigationProperty.swift */, + 1F71E80229E4611000379428 /* NavigaTumRoomFinderMap.swift */, + ); + path = Model; + sourceTree = ""; + }; + 1F71E7F529E4611000379428 /* Details */ = { + isa = PBXGroup; + children = ( + 1F71E7F629E4611000379428 /* NavigaTumRoomFinderMaps.swift */, + 1F71E7F729E4611000379428 /* NavigaTumNavigationCoordinates.swift */, + 1F71E7F829E4611000379428 /* NavigaTumOverlaysMaps.swift */, + 1F71E7F929E4611000379428 /* NavigaTumNavigationMaps.swift */, + 1F71E7FA29E4611000379428 /* NavigaTumNavigationAdditionalProperties.swift */, + ); + path = Details; + sourceTree = ""; + }; + 1F71E7FD29E4611000379428 /* Search */ = { + isa = PBXGroup; + children = ( + 1F71E7FE29E4611000379428 /* NavigaTumSearchResponse.swift */, + 1F71E7FF29E4611000379428 /* NavigaTumSearchResponseSection.swift */, + ); + path = Search; + sourceTree = ""; + }; + 1F71E81429E4611E00379428 /* Service */ = { + isa = PBXGroup; + children = ( + 1F71E81529E4611E00379428 /* RoomFinderService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1F71E81B29E4613F00379428 /* ViewNavigaTum */ = { + isa = PBXGroup; + children = ( + 1F71E81C29E4613F00379428 /* NavigaTumDetailsView.swift */, + 1F71E81D29E4613F00379428 /* NavigaTumDetailsBaseView.swift */, + 1F71E81E29E4613F00379428 /* NavigaTumMapView.swift */, + 1F71E81F29E4613F00379428 /* NavigaTumMapImagesView.swift */, + 1F71E82029E4613F00379428 /* NavigaTumView.swift */, + 1F71E82129E4613F00379428 /* NavigaTumListView.swift */, + ); + path = ViewNavigaTum; + sourceTree = ""; + }; 1FFEF086284E417E00ADD201 /* Recovered References */ = { isa = PBXGroup; children = ( @@ -669,6 +1026,14 @@ name = "Recovered References"; sourceTree = ""; }; + 1FFF9AC4297D317C0098E874 /* Service */ = { + isa = PBXGroup; + children = ( + 1FFF9AC5297D31830098E874 /* ProfileService.swift */, + ); + path = Service; + sourceTree = ""; + }; 36108B9C27A3046B007DC62D /* LectureComponent */ = { isa = PBXGroup; children = ( @@ -773,6 +1138,8 @@ 36108BD927A304B5007DC62D /* CafeteriaRowView.swift */, 36108BD227A304B5007DC62D /* MealPlanView.swift */, 36108BD627A304B5007DC62D /* MenuView.swift */, + 1F69CE45297EC99D005032CE /* DishView.swift */, + 1FACF3FC2996E34200A0B8AC /* MealPlanScreen.swift */, ); path = Cafeterias; sourceTree = ""; @@ -782,7 +1149,6 @@ children = ( 3654F382285171F6008AD5DC /* StudyRooms */, 36108BD127A304B5007DC62D /* Cafeterias */, - 36108BDB27A304B5007DC62D /* MapView.swift */, 1FAF9F0B284D2ABC000ABE93 /* MapScreenView.swift */, 36108BDD27A304B5007DC62D /* MapContentView.swift */, 36108BDE27A304B5007DC62D /* PanelContentView.swift */, @@ -797,6 +1163,8 @@ 36108BF127A30516007DC62D /* MoviesComponent */ = { isa = PBXGroup; children = ( + 1F69CE3A297DCC04005032CE /* Screen */, + 1F69CE39297DCBFE005032CE /* Service */, 36108BF227A30516007DC62D /* ViewModel */, 36108BF527A30516007DC62D /* Views */, ); @@ -902,6 +1270,9 @@ children = ( 3616C4CB27902086000A1BC9 /* ViewModel */, 3616C4CA27902075000A1BC9 /* Views */, + 1F69CE49297EDEB5005032CE /* Screen */, + 1F69CE4C297EDECA005032CE /* Service */, + 1F69CE4F297EDF32005032CE /* Model */, ); path = TUMSexyComponent; sourceTree = ""; @@ -925,9 +1296,11 @@ 3616C4D727904BA7000A1BC9 /* NewsComponent */ = { isa = PBXGroup; children = ( + 1F69CE33297DB729005032CE /* Service */, 3629BA2A27A1CEAD0036AC80 /* Views */, - 36BBE7302798AFCC0018FD3F /* Service */, + 36BBE7302798AFCC0018FD3F /* Model */, 3616C4D827904BB5000A1BC9 /* ViewModel */, + 1F69CE38297DCA2B005032CE /* NewsScreen */, ); path = NewsComponent; sourceTree = ""; @@ -943,6 +1316,7 @@ 36203E872761C6EC00C24658 /* LoginComponent */ = { isa = PBXGroup; children = ( + 1F04F188297C85030085F273 /* Model */, 36E9649D277492150055777F /* Service */, 36E9649C277491F10055777F /* ViewModel */, 36E9649B277491E90055777F /* Views */, @@ -997,6 +1371,9 @@ 3654F3692851710E008AD5DC /* RoomFinder */ = { isa = PBXGroup; children = ( + 1F71E81B29E4613F00379428 /* ViewNavigaTum */, + 1F71E81429E4611E00379428 /* Service */, + 1F71E7F429E4611000379428 /* Model */, 3654F36A2851710E008AD5DC /* ViewModel */, 3654F36D2851710E008AD5DC /* Entity */, 3654F36F2851710E008AD5DC /* Views */, @@ -1007,6 +1384,8 @@ 3654F36A2851710E008AD5DC /* ViewModel */ = { isa = PBXGroup; children = ( + 1F71E81729E4613500379428 /* NavigaTumDetailsViewModel.swift */, + 1F71E81829E4613500379428 /* NavigaTumViewModel.swift */, 3654F36B2851710E008AD5DC /* RoomFinderViewModel.swift */, 3654F36C2851710E008AD5DC /* RoomFinderMapViewModel.swift */, ); @@ -1037,13 +1416,14 @@ 3654F381285171D7008AD5DC /* ViewModel */ = { isa = PBXGroup; children = ( - 3654F38328517260008AD5DC /* StudyRoomVIewModel.swift */, + 3654F38328517260008AD5DC /* StudyRoomViewModel.swift */, 36108BD327A304B5007DC62D /* MealPlanViewModel.swift */, 36108BD427A304B5007DC62D /* MenuViewModel.swift */, 1F4C836128300306006971C0 /* MapViewModel.swift */, 1F4C836328300D25006971C0 /* MapViewModel+State.swift */, - 08DFB97C2867800C00E357DF /* CafeteriaWidgetViewModel.swift */, + 1F71E82C29E464C400379428 /* CafeteriaWidgetViewModel.swift */, 08DFB9802867ACB600E357DF /* StudyRoomWidgetViewModel.swift */, + 1F69CE43297EC97E005032CE /* DishViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -1056,6 +1436,7 @@ 3654F38928518640008AD5DC /* StudyRoomDetailsView.swift */, 3654F387285185A4008AD5DC /* StudyRoomGroupView.swift */, 3654F35E285168D2008AD5DC /* MapImagesHorizontalScrollingView.swift */, + 1F69CE3F297DDCD3005032CE /* StudyRoomDetailsScreen.swift */, ); path = StudyRooms; sourceTree = ""; @@ -1169,7 +1550,6 @@ isa = PBXGroup; children = ( 36108C0027A30762007DC62D /* Enums */, - 36AF61B827A2FD7700FEBD98 /* Entity */, 36AF61BA27A2FD7700FEBD98 /* Networking */, 36AF61C427A2FD7700FEBD98 /* Constants */, 36AF61C727A2FD7700FEBD98 /* Errors */, @@ -1179,27 +1559,14 @@ path = Base; sourceTree = ""; }; - 36AF61B827A2FD7700FEBD98 /* Entity */ = { - isa = PBXGroup; - children = ( - 36AF61B927A2FD7700FEBD98 /* EntityImporter.swift */, - ); - path = Entity; - sourceTree = ""; - }; 36AF61BA27A2FD7700FEBD98 /* Networking */ = { isa = PBXGroup; children = ( - 3654F367285169AC008AD5DC /* TUMDevAppAPI.swift */, - 36AF61BB27A2FD7700FEBD98 /* TUMSexyAPI.swift */, - 36AF61BC27A2FD7700FEBD98 /* EatAPI.swift */, - 36AF61BD27A2FD7700FEBD98 /* MVGAPI.swift */, - 36AF61BE27A2FD7700FEBD98 /* NetworkingAPI.swift */, - 36AF61BF27A2FD7700FEBD98 /* CampusOnlineAPI.swift */, - 36AF61C027A2FD7700FEBD98 /* APIResponse.swift */, - 36AF61C127A2FD7700FEBD98 /* TUMOnlineAPI.swift */, + 1F189EA529968E150056BBD8 /* Protocols */, + 1F189E8B29968CD70056BBD8 /* APIs */, + 1F189E9829968D620056BBD8 /* APIErrors */, 36AF61C227A2FD7700FEBD98 /* Cache.swift */, - 36AF61C327A2FD7700FEBD98 /* TUMCabeAPI.swift */, + 1F04F16F297AA1E00085F273 /* Old APIs */, ); path = Networking; sourceTree = ""; @@ -1251,9 +1618,11 @@ 36BB6F5527AFCD7B00F224AB /* PersonDetailedComponent */ = { isa = PBXGroup; children = ( - 36BB6F5827AFCD9C00F224AB /* View */, 36BB6F5727AFCD9300F224AB /* ViewModel */, + 1F04F187297C6BC40085F273 /* Screen */, + 36BB6F5827AFCD9C00F224AB /* View */, 36BB6F5627AFCD8D00F224AB /* Entity */, + 1F04F186297C3FA20085F273 /* Service */, ); path = PersonDetailedComponent; sourceTree = ""; @@ -1281,7 +1650,6 @@ isa = PBXGroup; children = ( 36BB6F5227AFCCB500F224AB /* PersonDetailedView.swift */, - 36BB6F7C27B356C200F224AB /* PersonDetailedCellView.swift */, 36BB6F7E27B386D100F224AB /* AddToContactsView.swift */, ); path = View; @@ -1292,7 +1660,9 @@ children = ( 36BB6F5A27AFCDF900F224AB /* ViewModel */, 36BB6F5C27AFCDFA00F224AB /* Entity */, + 1F04F181297C33860085F273 /* Screen */, 36BB6F5E27AFCDFA00F224AB /* View */, + 1F04F180297BDF250085F273 /* Service */, ); path = PersonSearchComponent; sourceTree = ""; @@ -1316,8 +1686,7 @@ 36BB6F5E27AFCDFA00F224AB /* View */ = { isa = PBXGroup; children = ( - 36BB6F5F27AFCDFA00F224AB /* PersonSearchView.swift */, - 36AD5CF727B96AD200DAE143 /* PersonSearchListView.swift */, + 36AD5CF727B96AD200DAE143 /* PersonSearchView.swift */, ); path = View; sourceTree = ""; @@ -1327,7 +1696,7 @@ children = ( 100803452764E2C50013ED0E /* ProfileToolbar.swift */, 100803472764E37A0013ED0E /* ProfileView.swift */, - 36AD5CFB27B974F100DAE143 /* ProfileMyTumSection.swift */, + 36AD5CFB27B974F100DAE143 /* TuitionScreen.swift */, ); path = View; sourceTree = ""; @@ -1372,7 +1741,9 @@ isa = PBXGroup; children = ( 36BB6F8427B39C3D00F224AB /* ViewModel */, + 1F04F17B297B0BC30085F273 /* Screen */, 36BB6F8127B39B3400F224AB /* View */, + 1F04F17A297AED190085F273 /* Service */, ); path = LectureSearchComponent; sourceTree = ""; @@ -1380,8 +1751,7 @@ 36BB6F8127B39B3400F224AB /* View */ = { isa = PBXGroup; children = ( - 36BB6F8227B39B4300F224AB /* LectureSearchView.swift */, - 36AD5CF927B9711B00DAE143 /* LectureSearchListView.swift */, + 36AD5CF927B9711B00DAE143 /* LectureSearchView.swift */, ); path = View; sourceTree = ""; @@ -1394,13 +1764,13 @@ path = ViewModel; sourceTree = ""; }; - 36BB6F8B27B3D58700F224AB /* Entity */ = { + 36BB6F8B27B3D58700F224AB /* Model */ = { isa = PBXGroup; children = ( - 36BB6F8927B3D21200F224AB /* CalendarEvent.swift */, + 1F04F172297AD41B0085F273 /* CalendarEvent.swift */, 36AD5CF127B7FEAD00DAE143 /* TumCalendarStyle.swift */, ); - path = Entity; + path = Model; sourceTree = ""; }; 36BBE72D27989F6E0018FD3F /* HelperViews */ = { @@ -1413,13 +1783,13 @@ path = HelperViews; sourceTree = ""; }; - 36BBE7302798AFCC0018FD3F /* Service */ = { + 36BBE7302798AFCC0018FD3F /* Model */ = { isa = PBXGroup; children = ( 36BBE7312798AFE10018FD3F /* News.swift */, 36BBE7332798B04D0018FD3F /* NewsSource.swift */, ); - path = Service; + path = Model; sourceTree = ""; }; 36E9649B277491E90055777F /* Views */ = { @@ -1451,7 +1821,6 @@ isa = PBXGroup; children = ( 36FF906E2773BE8100F4C785 /* AuthenticationHandler.swift */, - 36203E8A2761C6EC00C24658 /* Credentials.swift */, ); path = Service; sourceTree = ""; @@ -1459,9 +1828,11 @@ 36E9649E277492AE0055777F /* CalendarComponent */ = { isa = PBXGroup; children = ( - 36BB6F8B27B3D58700F224AB /* Entity */, + 36BB6F8B27B3D58700F224AB /* Model */, 36E964A0277492C00055777F /* ViewModel */, 36E9649F277492B70055777F /* Views */, + 1F04F177297AD4350085F273 /* Service */, + 1F04F176297AD42E0085F273 /* Screen */, ); path = CalendarComponent; sourceTree = ""; @@ -1697,33 +2068,43 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 36BB6F6227AFCDFA00F224AB /* PersonSearchView.swift in Sources */, + 36BB6F6227AFCDFA00F224AB /* PersonSearchScreen.swift in Sources */, 97270F5A27AB2A4900BB25E4 /* Array+Rearrange.swift in Sources */, - 3654F38428517260008AD5DC /* StudyRoomVIewModel.swift in Sources */, + 1F71E82629E4613F00379428 /* NavigaTumView.swift in Sources */, + 3654F38428517260008AD5DC /* StudyRoomViewModel.swift in Sources */, 2F1B2B87286530120023BD9A /* MovieDetailsBasicInfoRowView.swift in Sources */, 36BB6F6627AFD12B00F224AB /* PersonDetails.swift in Sources */, 36108BFD27A30517007DC62D /* MovieDetailedView.swift in Sources */, 3683C3182758117900082930 /* Model.swift in Sources */, + 1F04F179297AED150085F273 /* LectureSearchService.swift in Sources */, 08FAFD24288DF553006A0E27 /* WidgetRecommendation.swift in Sources */, 08DFB96F286647E900E357DF /* WidgetScreen.swift in Sources */, 974D5B9A27E5E9CB00FD7B11 /* GlowBorder.swift in Sources */, 08DFB97328664BC400E357DF /* TuitionWidgetView.swift in Sources */, - 36BB6F8A27B3D21200F224AB /* CalendarEvent.swift in Sources */, 36108BEB27A304B6007DC62D /* CafeteriaRowView.swift in Sources */, + 1F71E80B29E4611000379428 /* NavigaTumSearchResponseSection.swift in Sources */, 08DFB9772866506900E357DF /* WidgetFrameView.swift in Sources */, 366F0E8D27580CFD0091651D /* Persistence.swift in Sources */, 36108BE327A304B5007DC62D /* DishLabel.swift in Sources */, + 1F71E80929E4611000379428 /* NavigaTumNavigationDetails.swift in Sources */, 085DE9C628AB7C530045095F /* AnalyticsController.swift in Sources */, + 1F71E82429E4613F00379428 /* NavigaTumMapView.swift in Sources */, 08441F2B2874E2D00033F5B1 /* WidgetLoadingView.swift in Sources */, 36108BB927A3046B007DC62D /* LecturesViewModel+State.swift in Sources */, 36AF61EE27A2FD7800FEBD98 /* GroupBoxLabelView.swift in Sources */, + 1F69CE3C297DCC12005032CE /* MoviesScreen.swift in Sources */, + 1F69CE48297EDEA3005032CE /* TUMSexyScreen.swift in Sources */, 36108BFB27A30517007DC62D /* MoviesViewModel.swift in Sources */, 36E964AC277499860055777F /* CalendarDisplayView.swift in Sources */, 1F4C926F2882FD85003DC7D7 /* RoundedCorners.swift in Sources */, + 1F71E81A29E4613500379428 /* NavigaTumViewModel.swift in Sources */, + 1F71E80C29E4611000379428 /* NavigaTumNavigationEntity.swift in Sources */, 36FAE365277472EF00628799 /* LoginViewModel.swift in Sources */, 36108BE927A304B5007DC62D /* MenuView.swift in Sources */, 36108C1D27A307FA007DC62D /* GradeView.swift in Sources */, 36AF61DE27A2FD7800FEBD98 /* APIResponse.swift in Sources */, + 1F71E81629E4611E00379428 /* RoomFinderService.swift in Sources */, + 1F69CE4E297EDF12005032CE /* TUMSexyLink.swift in Sources */, 3654F37A2851710E008AD5DC /* RoomFinderDetailsBaseView.swift in Sources */, 3654F388285185A4008AD5DC /* StudyRoomGroupView.swift in Sources */, 0815249428E445030098A2C3 /* Date+Time.swift in Sources */, @@ -1734,40 +2115,50 @@ 36108BC427A3046B007DC62D /* LectureDetailsDetailedInfoRowView.swift in Sources */, 36108C1C27A307FA007DC62D /* GradesView.swift in Sources */, 3654F365285168D2008AD5DC /* RoomImageMapping.swift in Sources */, + 1FFF9AC6297D31830098E874 /* ProfileService.swift in Sources */, 2FCF38B1286C9B9200F10915 /* MovieDetailsDetailedInfoRowView.swift in Sources */, - 36AD5CFC27B974F100DAE143 /* ProfileMyTumSection.swift in Sources */, + 36AD5CFC27B974F100DAE143 /* TuitionScreen.swift in Sources */, 36108BE227A304B5007DC62D /* MensaMenu.swift in Sources */, 36108BC027A3046B007DC62D /* LecturesView.swift in Sources */, 0805DB7928C933AE00712FF2 /* Operators.swift in Sources */, 97F8A79327E641570099EE83 /* AcademicDegree.swift in Sources */, - 36AF61D927A2FD7800FEBD98 /* TUMSexyAPI.swift in Sources */, + 36AF61D927A2FD7800FEBD98 /* TUMSexyAPIOld.swift in Sources */, + 1F189E9329968D260056BBD8 /* TUMDevAppAPI.swift in Sources */, 3654F38E28518B3D008AD5DC /* StudyGroupRowView.swift in Sources */, 3654F358285167C3008AD5DC /* StudyRoomsService.swift in Sources */, 36AD5CF227B7FEAD00DAE143 /* TumCalendarStyle.swift in Sources */, + 1FA538EE297560CD004C70A8 /* MainAPI.swift in Sources */, 36BB6F7F27B386D100F224AB /* AddToContactsView.swift in Sources */, - 36AF61DF27A2FD7800FEBD98 /* TUMOnlineAPI.swift in Sources */, + 1F189EA729968E5C0056BBD8 /* API.swift in Sources */, + 36AF61DF27A2FD7800FEBD98 /* TUMOnlineAPIOld.swift in Sources */, 3654F37B2851710E008AD5DC /* RoomFinderListView.swift in Sources */, 36C70FB32854D2AB0097416E /* PanelContentStudyGroupsListView.swift in Sources */, 36C70FB128538A190097416E /* PanelContentCafeteriasListView.swift in Sources */, - 36BB6F7D27B356C200F224AB /* PersonDetailedCellView.swift in Sources */, 36108BE727A304B5007DC62D /* MenuViewModel.swift in Sources */, - 36AF61E127A2FD7800FEBD98 /* TUMCabeAPI.swift in Sources */, + 36AF61E127A2FD7800FEBD98 /* TUMCabeAPIOld.swift in Sources */, 3654F37D2851710E008AD5DC /* RoomFinderDetailsMapImagesView.swift in Sources */, 36108BB627A3046B007DC62D /* LectureDetailsViewModel+State.swift in Sources */, + 1FACF3F92996A49300A0B8AC /* TUMDevAppAPIOld.swift in Sources */, 36108BBF27A3046B007DC62D /* LectureDetailsService.swift in Sources */, 0805E72C28CC2278003C5CFD /* HashFunction.swift in Sources */, 36982BD827A2739000515847 /* Collapsible.swift in Sources */, 0815249E28E4A6310098A2C3 /* Date+daysBetween.swift in Sources */, 1F2068DE28FD731200DBDF67 /* LoginViewModel+TokenState.swift in Sources */, - 36AD5CFA27B9711B00DAE143 /* LectureSearchListView.swift in Sources */, + 36AD5CFA27B9711B00DAE143 /* LectureSearchView.swift in Sources */, + 1F189E9529968D330056BBD8 /* TUMSexyAPI.swift in Sources */, 1F4C836228300306006971C0 /* MapViewModel.swift in Sources */, + 1F189E9729968D490056BBD8 /* MVGAPI.swift in Sources */, 36108BFF27A30517007DC62D /* MoviesView.swift in Sources */, + 1F71E80729E4611000379428 /* NavigaTumNavigationAdditionalProperties.swift in Sources */, + 1F189E9129968D130056BBD8 /* EatAPI.swift in Sources */, 36AF61E927A2FD7800FEBD98 /* ErrorHandler.swift in Sources */, 36BB6F6027AFCDFA00F224AB /* PersonSearchViewModel.swift in Sources */, 36982BD627A251A700515847 /* NewsCardsHorizontalScrollingView.swift in Sources */, 36AF61E727A2FD7800FEBD98 /* ErrorEmittingViewModifier.swift in Sources */, + 1F71E80629E4611000379428 /* NavigaTumNavigationMaps.swift in Sources */, 3654F362285168D2008AD5DC /* StudyRoomApiResponse.swift in Sources */, 36108BC527A3046B007DC62D /* LectureDetailsDetailedInfoView.swift in Sources */, + 1F04F18A297C85120085F273 /* Token.swift in Sources */, 3654F363285168D2008AD5DC /* StudyRoom.swift in Sources */, 08573BA5287847DC006AC06F /* MapLocation.swift in Sources */, 3629BA3127A1D0AD0036AC80 /* ScrollableCardsViewModifier.swift in Sources */, @@ -1781,20 +2172,25 @@ 3698CBED2761E014001C5735 /* CustomRoundedBorderTextFieldStyle.swift in Sources */, 36108BE627A304B5007DC62D /* MealPlanViewModel.swift in Sources */, 36203E8B2761C6EC00C24658 /* TUMSplashScreen.swift in Sources */, - 36AF61DA27A2FD7800FEBD98 /* EatAPI.swift in Sources */, + 36AF61DA27A2FD7800FEBD98 /* EatAPIOld.swift in Sources */, 36108BEF27A304B6007DC62D /* MapContentView.swift in Sources */, + 1F71E80E29E4611000379428 /* NavigaTumRoomFinderMap.swift in Sources */, + 1F71E80A29E4611000379428 /* NavigaTumSearchResponse.swift in Sources */, 3616C4CF279020D3000A1BC9 /* TUMSexyViewModel.swift in Sources */, + 1F69CE44297EC97E005032CE /* DishViewModel.swift in Sources */, 36AF61EC27A2FD7800FEBD98 /* Error+Category.swift in Sources */, - 36AF61DD27A2FD7800FEBD98 /* CampusOnlineAPI.swift in Sources */, + 1F71E82229E4613F00379428 /* NavigaTumDetailsView.swift in Sources */, + 36AF61DD27A2FD7800FEBD98 /* CampusOnlineAPIOld.swift in Sources */, 36AD5CF427B8C83500DAE143 /* CalendarSingleEventView.swift in Sources */, 36108BBC27A3046B007DC62D /* LectureDetails.swift in Sources */, 36108BC627A3046B007DC62D /* LectureDetailsBasicInfoRowView.swift in Sources */, + 1F189E9E29968D9B0056BBD8 /* EatAPIError.swift in Sources */, 08FAFD20288DEE3B006A0E27 /* Widget.swift in Sources */, 36108BDF27A304B5007DC62D /* MealPlan.swift in Sources */, 36BBE7342798B04D0018FD3F /* NewsSource.swift in Sources */, 3654F38628517BB4008AD5DC /* CafeteriaView.swift in Sources */, 36BB6F8D27B3F25A00F224AB /* NSMutableString+Extensions.swift in Sources */, - 36108BED27A304B6007DC62D /* MapView.swift in Sources */, + 1F04F175297AD4280085F273 /* CalendarScreen.swift in Sources */, 36AF61E827A2FD7800FEBD98 /* AlertErrorHandler.swift in Sources */, 36BB6F5327AFCCB500F224AB /* PersonDetailedView.swift in Sources */, 3654F37E2851710E008AD5DC /* RoomFinderDetailsView.swift in Sources */, @@ -1802,6 +2198,7 @@ 3654F364285168D2008AD5DC /* MapImagesHorizontalScrollingView.swift in Sources */, 36E964A5277493D90055777F /* CalendarViewModel.swift in Sources */, 0805E72428CAABB3003C5CFD /* AnalyticsError.swift in Sources */, + 1F71E82F29E4667C00379428 /* NavigaTUMAPI.swift in Sources */, 3654F361285168D2008AD5DC /* StudyRoomGroup.swift in Sources */, 36BB6F7927B26DE300F224AB /* TuitionView.swift in Sources */, 3654F3782851710E008AD5DC /* FoundRoom.swift in Sources */, @@ -1809,19 +2206,26 @@ 0805DB7728C7F3E600712FF2 /* AnalyticsOptInView.swift in Sources */, 1FAF9F0C284D2ABC000ABE93 /* MapScreenView.swift in Sources */, 36108C1527A307F9007DC62D /* GradesViewModel.swift in Sources */, - 36108BE427A304B5007DC62D /* MensaEnumService.swift in Sources */, + 1F69CE35297DB732005032CE /* NewsService.swift in Sources */, 1F2068E428FD73CB00DBDF67 /* TokenPermissionsViewModel+PermissionType.swift in Sources */, 36108BE127A304B5007DC62D /* Cafeteria.swift in Sources */, + 1F04F183297C3EF70085F273 /* PersonDetailedScreen.swift in Sources */, 36108BE027A304B5007DC62D /* Dish.swift in Sources */, + 1F71E80429E4611000379428 /* NavigaTumNavigationCoordinates.swift in Sources */, 36108BBB27A3046B007DC62D /* LecturesScreen.swift in Sources */, 36BB6F6427AFCFFB00F224AB /* PersonDetailedViewModel.swift in Sources */, 36AF61E427A2FD7800FEBD98 /* NetworkingError.swift in Sources */, - 36BB6F8327B39B4300F224AB /* LectureSearchView.swift in Sources */, + 1FACF3FB2996A65700A0B8AC /* MealPlanService.swift in Sources */, + 36BB6F8327B39B4300F224AB /* LectureSearchScreen.swift in Sources */, 36108BFA27A30517007DC62D /* Movie.swift in Sources */, 36108BB727A3046B007DC62D /* LectureDetailsViewModel.swift in Sources */, 36AF61E027A2FD7800FEBD98 /* Cache.swift in Sources */, + 1F69CE37297DCA22005032CE /* NewsScreen.swift in Sources */, 36AF61F027A2FD7800FEBD98 /* DecoderProtocol.swift in Sources */, + 1F189E9A29968D790056BBD8 /* TUMOnlineAPIError.swift in Sources */, + 1F69CE40297DDCD3005032CE /* StudyRoomDetailsScreen.swift in Sources */, 08FAFD1C288DEDBF006A0E27 /* TimeStrategy.swift in Sources */, + 1F04F171297AA5F40085F273 /* Service.swift in Sources */, 08FAFD17288474FC006A0E27 /* WidgetMapBackgroundView.swift in Sources */, 3698CBEF2761E6CC001C5735 /* TokenConfirmationView.swift in Sources */, 0805E72828CC0954003C5CFD /* AppUsageDataEntity.swift in Sources */, @@ -1829,34 +2233,47 @@ 3616C4CD279020A0000A1BC9 /* TUMSexyView.swift in Sources */, 1F4C836728300E79006971C0 /* CafeteriasService.swift in Sources */, 36108C1E27A307FA007DC62D /* BarChartView.swift in Sources */, - 3654F368285169AC008AD5DC /* TUMDevAppAPI.swift in Sources */, 3683C31A2758118A00082930 /* MockModel.swift in Sources */, + 1F04F16E297A9A700085F273 /* CalendarService.swift in Sources */, + 1F04F17F297BDF1E0085F273 /* PersonSearchService.swift in Sources */, 08DFB9812867ACB600E357DF /* StudyRoomWidgetViewModel.swift in Sources */, 3629BA3327A1E4A90036AC80 /* RoundedCornersShape.swift in Sources */, 1F2068E228FD73C400DBDF67 /* TokenPermissionsViewModel+State.swift in Sources */, + 1F71E82729E4613F00379428 /* NavigaTumListView.swift in Sources */, 3654F37C2851710E008AD5DC /* RoomFinderView.swift in Sources */, + 1F71E80529E4611000379428 /* NavigaTumOverlaysMaps.swift in Sources */, 36BBE72F27989F8C0018FD3F /* SFSafariViewWrapper.swift in Sources */, 08FAFD272898A2B8006A0E27 /* LocationStrategy.swift in Sources */, + 1F183A172979D19000B5D22D /* APIError.swift in Sources */, 1F2068DC28FD6E2800DBDF67 /* LoginViewModel+LoginState.swift in Sources */, 36AF61E627A2FD7800FEBD98 /* View+Error.swift in Sources */, 36BBE7322798AFE10018FD3F /* News.swift in Sources */, 36108C1627A307F9007DC62D /* GradesViewModel+ChartData.swift in Sources */, - 36AD5CF827B96AD200DAE143 /* PersonSearchListView.swift in Sources */, + 1F71E82D29E464C400379428 /* CafeteriaWidgetViewModel.swift in Sources */, + 36AD5CF827B96AD200DAE143 /* PersonSearchView.swift in Sources */, 36108C1727A307F9007DC62D /* GradesViewModel+State.swift in Sources */, + 1F04F173297AD41B0085F273 /* CalendarEvent.swift in Sources */, + 1F189E9C29968D880056BBD8 /* TUMCabeAPIError.swift in Sources */, + 1F189E8F29968CFC0056BBD8 /* TUMCabeAPI.swift in Sources */, 08038F96287430FB0048DAE5 /* WidgetTitleView.swift in Sources */, 36FF906F2773BE8100F4C785 /* AuthenticationHandler.swift in Sources */, 36108BFE27A30517007DC62D /* MovieDetailCellView.swift in Sources */, + 1F69CE46297EC99D005032CE /* DishView.swift in Sources */, + 1F69CE4B297EDEC7005032CE /* TUMSexyService.swift in Sources */, 2FCF38AD286C9B5600F10915 /* MovieDetailsDetailedInfoView.swift in Sources */, 36108BE527A304B5007DC62D /* MealPlanView.swift in Sources */, 36AF61EF27A2FD7800FEBD98 /* LoadingView.swift in Sources */, - 36AF61D827A2FD7800FEBD98 /* EntityImporter.swift in Sources */, + 1F69CE3E297DCC19005032CE /* MovieService.swift in Sources */, 36AF61EA27A2FD7800FEBD98 /* Environment+Error.swift in Sources */, 3654F38A28518640008AD5DC /* StudyRoomDetailsView.swift in Sources */, 36AF61ED27A2FD7800FEBD98 /* FailedView.swift in Sources */, 36108BBE27A3046B007DC62D /* LecturesService.swift in Sources */, 99706870298569E10028D235 /* CrashlyticsService.swift in Sources */, + 1F71E83129E46A1000379428 /* NavigaTUMAPIError.swift in Sources */, 36108BF027A304B6007DC62D /* PanelContentView.swift in Sources */, 36AF61F127A2FD7800FEBD98 /* XMLSerializer.swift in Sources */, + 1F71E81929E4613500379428 /* NavigaTumDetailsViewModel.swift in Sources */, + 1F71E80829E4611000379428 /* NavigaTumOverlayMap.swift in Sources */, 08FAFD1A288DED6F006A0E27 /* WidgetRecommender.swift in Sources */, 08D9535A28E34596007ED2F1 /* Array+Groups.swift in Sources */, 36108C1A27A307FA007DC62D /* Modus.swift in Sources */, @@ -1864,10 +2281,13 @@ 36108BC127A3046B007DC62D /* LectureView.swift in Sources */, 366F0E9027580CFD0091651D /* Campus_iOS.xcdatamodeld in Sources */, 36108BFC27A30517007DC62D /* MovieCard.swift in Sources */, + 1F71E82529E4613F00379428 /* NavigaTumMapImagesView.swift in Sources */, 36BB6F6A27AFD2A100F224AB /* PhoneExtension.swift in Sources */, - 36AF61DC27A2FD7800FEBD98 /* NetworkingAPI.swift in Sources */, + 36AF61DC27A2FD7800FEBD98 /* NetworkingAPIOld.swift in Sources */, + 1F71E82329E4613F00379428 /* NavigaTumDetailsBaseView.swift in Sources */, 08DFB97528664CFC00E357DF /* TuitionDetailsView.swift in Sources */, 0815249628E45C390098A2C3 /* MLModelDataHandler.swift in Sources */, + 1F71E80D29E4611000379428 /* NavigaTumNavigationProperty.swift in Sources */, 3654F366285168D2008AD5DC /* StudyRoomAttribute.swift in Sources */, 1FBFA168285E5B2D00FC1515 /* PanelContentListView.swift in Sources */, 100803462764E2C50013ED0E /* ProfileToolbar.swift in Sources */, @@ -1876,11 +2296,15 @@ 36BB6F6C27AFD2B900F224AB /* Room.swift in Sources */, 0815249828E492070098A2C3 /* RecommenderError.swift in Sources */, 36AF61EB27A2FD7800FEBD98 /* ErrorCategory.swift in Sources */, + 1F04F185297C3F990085F273 /* PersonDetailedService.swift in Sources */, + 1F04F18C297C85190085F273 /* Confirmation.swift in Sources */, + 1F69CE42297EC94E005032CE /* DishService.swift in Sources */, 36203E8C2761C6EC00C24658 /* LoginView.swift in Sources */, 1F33B2ED282B084100C898E4 /* MockGradesViewModel.swift in Sources */, 36E964A7277498540055777F /* CalendarContentView.swift in Sources */, 0815249C28E4A38D0098A2C3 /* CLLocation+isInvalid.swift in Sources */, 36AF61E327A2FD7800FEBD98 /* APIConstants.swift in Sources */, + 1F71E80329E4611000379428 /* NavigaTumRoomFinderMaps.swift in Sources */, 3629BA2C27A1CECA0036AC80 /* NewsView.swift in Sources */, 1FB82E3428F95776007B1858 /* TokenPermissionsView.swift in Sources */, 36BB6F7327B1CD9200F224AB /* ProfileViewModel.swift in Sources */, @@ -1891,7 +2315,7 @@ 08DFB97928666AD900E357DF /* CafeteriaWidgetView.swift in Sources */, 3629BA2E27A1CEFA0036AC80 /* NewsCard.swift in Sources */, 100803482764E37A0013ED0E /* ProfileView.swift in Sources */, - 08DFB97D2867800C00E357DF /* CafeteriaWidgetViewModel.swift in Sources */, + 1F189EA229968DB90056BBD8 /* TUMSexyAPIError.swift in Sources */, 36AD5CF627B8D97500DAE143 /* LectureDetailsEventInfoView.swift in Sources */, 1F4C836428300D25006971C0 /* MapViewModel+State.swift in Sources */, 08DFB97F2867AC9200E357DF /* StudyRoomWidgetView.swift in Sources */, @@ -1900,10 +2324,14 @@ 3654F38C285187C5008AD5DC /* PanelSearchBarView.swift in Sources */, 1FB82E3628F96C9E007B1858 /* TokenPermissionsViewModel.swift in Sources */, 36108C1B27A307FA007DC62D /* GradesService.swift in Sources */, + 1F189E8D29968CE50056BBD8 /* TUMOnlineAPI.swift in Sources */, 36AF61E527A2FD7800FEBD98 /* BackendError.swift in Sources */, 3654F3772851710E008AD5DC /* RoomFinderMapViewModel.swift in Sources */, - 36AF61DB27A2FD7800FEBD98 /* MVGAPI.swift in Sources */, + 36AF61DB27A2FD7800FEBD98 /* MVGAPIOld.swift in Sources */, + 1FACF3FD2996E34200A0B8AC /* MealPlanScreen.swift in Sources */, 366F0E8427580CFB0091651D /* App.swift in Sources */, + 1F189EA029968DA90056BBD8 /* TUMDevAppAPIError.swift in Sources */, + 1F189EA429968DE60056BBD8 /* MVGAPIError.swift in Sources */, 36FF90652773BB8200F4C785 /* Extensions.swift in Sources */, 36108C1427A307F9007DC62D /* GradeColor.swift in Sources */, 36108BBD27A3046B007DC62D /* Lecture.swift in Sources */, diff --git a/Campus-iOS/AnalyticsComponent/AnalyticsController.swift b/Campus-iOS/AnalyticsComponent/AnalyticsController.swift index 8a037fca..9ba5ebde 100644 --- a/Campus-iOS/AnalyticsComponent/AnalyticsController.swift +++ b/Campus-iOS/AnalyticsComponent/AnalyticsController.swift @@ -33,6 +33,7 @@ struct AnalyticsController { print("Info: app usage data upload is disabled.") return + /* if !didOptIn { return } @@ -89,5 +90,6 @@ struct AnalyticsController { request.setValue(postToken, forHTTPHeaderField: "Authorization") let (_, _) = try await URLSession.shared.data(for: request) + */ } } diff --git "a/Campus-iOS/AnalyticsCom\303\274o/AnalyticsController.swift" "b/Campus-iOS/AnalyticsCom\303\274o/AnalyticsController.swift" deleted file mode 100644 index 10b07be8..00000000 --- "a/Campus-iOS/AnalyticsCom\303\274o/AnalyticsController.swift" +++ /dev/null @@ -1,81 +0,0 @@ -// -// Analytics.swift -// Campus-iOS -// -// Created by Robyn Kölle on 16.08.22. -// - -import Foundation -import MapKit -import SwiftUI - -struct AnalyticsController { - - @AppStorage("analyticsOptIn") private static var didOptIn = false - - static func store(entry: AppUsageData) { - if let _ = try? AppUsageDataEntity(data: entry, context: PersistenceController.shared.container.viewContext) { - PersistenceController.shared.save() - } - } - - static func upload(entry: AppUsageData) async throws { - - if !didOptIn { - return - } - - guard let postToken = Bundle.main.object(forInfoDictionaryKey: "ANALYTICS_POST_TOKEN") as? String, !postToken.isEmpty, - let analyticsApi = Bundle.main.object(forInfoDictionaryKey: "ANALYTICS_API") as? String else { - return - } - - guard var components = URLComponents(string: "https://" + analyticsApi) else { - return - } - - /* Query items */ - - guard let deviceIdentifier = await UIDevice.current.identifierForVendor?.uuidString else { - return - } - - guard let startDate = entry.getStartTime(), let endDate = entry.getEndTime(), let view = entry.getView() else { - return - } - - let latitude = entry.getLatitude() ?? AppUsageData.invalidLocation - let longitude = entry.getLongitude() ?? AppUsageData.invalidLocation - - let hashedId = HashFunction.sha256(deviceIdentifier) - let formatter = DateFormatter() - formatter.dateFormat = "YY-MM-dd HH-mm-ss" - let startTime = formatter.string(from: startDate) - let endTime = formatter.string(from: endDate) - - components.queryItems = [ - URLQueryItem(name: "user_id", value: hashedId), - URLQueryItem(name: "latitude", value: String(latitude)), - URLQueryItem(name: "longitude", value: String(longitude)), - URLQueryItem(name: "start_time", value: startTime), - URLQueryItem(name: "end_time", value: endTime), - URLQueryItem(name: "view", value: view.rawValue) - ] - - guard let url = components.url else { - return - } - -#if targetEnvironment(simulator) - print("🟢 Query items:") - print(components.queryItems ?? []) - return -#endif - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue(postToken, forHTTPHeaderField: "Authorization") - - let (_, _) = try await URLSession.shared.data(for: request) - } -} diff --git "a/Campus-iOS/AnalyticsCom\303\274o/AnalyticsError.swift" "b/Campus-iOS/AnalyticsCom\303\274o/AnalyticsError.swift" deleted file mode 100644 index e6986036..00000000 --- "a/Campus-iOS/AnalyticsCom\303\274o/AnalyticsError.swift" +++ /dev/null @@ -1,12 +0,0 @@ -// -// AnalyticsError.swift -// Campus-iOS -// -// Created by Robyn Kölle on 09.09.22. -// - -import Foundation - -enum AnalyticsError: Error { - case missingValues -} diff --git "a/Campus-iOS/AnalyticsCom\303\274o/AppUsageData.swift" "b/Campus-iOS/AnalyticsCom\303\274o/AppUsageData.swift" deleted file mode 100644 index a8fa7019..00000000 --- "a/Campus-iOS/AnalyticsCom\303\274o/AppUsageData.swift" +++ /dev/null @@ -1,125 +0,0 @@ -// -// AppUsageData.swift -// Campus-iOS -// -// Created by Robyn Kölle on 09.09.22. -// - -import Foundation -import MapKit -import Combine - -/// A wrapper for the usage data we collect for specific views inside the app. -/// Persists the data when explicitly calling `exitView`, or when the app enters the background, or when a sheet blocks the respective view. -/// Instantiate as a `@State` object inside the view, and call `visitView` in `.onAppear` or `.task`. -/// Call `didExitView` in `.onDisappear`. -class AppUsageData { - - /* CoreData's double values (for latitude, longitude) are not optional. - * However, we still want to store the other data when we cannot get the location. - * Thus we symbolize invalid locations with an impossible latitude / longitude value in the CoreData entity. */ - static let invalidLocation: Double = 200 - - private var view: CampusAppView? - private var latitude: Double? - private var longitude: Double? - private var startTime: Date? - private var endTime: Date? - - // To set the end timestamp, and persist the data when the app enters the background. - private var didEnterBackgroundListener: AnyCancellable? - - // To set the start timestamp (etc.) when the app wakes up. - private var wakeUpListener: AnyCancellable? - - private var sheetActiveListener: AnyCancellable? - - private var sheetInactiveListener: AnyCancellable? - - func visitView(view: CampusAppView) { - - self.startTime = Date() - self.view = view - - if let location = CLLocationManager().location { - self.latitude = location.coordinate.latitude - self.longitude = location.coordinate.longitude - } else { - self.latitude = nil - self.longitude = nil - } - - self.didEnterBackgroundListener = NotificationCenter.default - .publisher(for: UIApplication.willResignActiveNotification) - .sink { _ in self.didEnterBackground(currentView: view) } - - self.sheetActiveListener = NotificationCenter.default - .publisher(for: Notification.Name.tcaSheetBecameActiveNotification) - .sink { _ in self.didOpenSheet(currentView: view) } - } - - /// Call this function when exiting a view, e.g. `onDisappear`. - public func didExitView() { - didEnterBackgroundListener?.cancel() - wakeUpListener?.cancel() - sheetActiveListener?.cancel() - sheetInactiveListener?.cancel() - - commit() - } - - private func didEnterBackground(currentView: CampusAppView) { - didEnterBackgroundListener?.cancel() - sheetActiveListener?.cancel() - sheetInactiveListener?.cancel() - - wakeUpListener = NotificationCenter.default - .publisher(for: UIApplication.didBecomeActiveNotification) - .sink { _ in - self.visitView(view: currentView) - } - - commit() - } - - private func didOpenSheet(currentView: CampusAppView) { - - didEnterBackgroundListener?.cancel() - wakeUpListener?.cancel() - sheetActiveListener?.cancel() - - sheetInactiveListener = NotificationCenter.default - .publisher(for: Notification.Name.tcaSheetBecameInactiveNotification) - .sink { _ in - self.visitView(view: currentView) - } - - commit() - } - - private func commit() { - self.endTime = Date() - AnalyticsController.store(entry: self) - Task { try? await AnalyticsController.upload(entry: self) } - } - - public func getView() -> CampusAppView? { - return self.view - } - - public func getLatitude() -> Double? { - return self.latitude - } - - public func getLongitude() -> Double? { - return self.longitude - } - - public func getStartTime() -> Date? { - return self.startTime - } - - public func getEndTime() -> Date? { - return self.endTime - } -} diff --git "a/Campus-iOS/AnalyticsCom\303\274o/AppUsageDataEntity.swift" "b/Campus-iOS/AnalyticsCom\303\274o/AppUsageDataEntity.swift" deleted file mode 100644 index 95f1d806..00000000 --- "a/Campus-iOS/AnalyticsCom\303\274o/AppUsageDataEntity.swift" +++ /dev/null @@ -1,28 +0,0 @@ -// -// AppUsageDataEntity.swift -// Campus-iOS -// -// Created by Robyn Kölle on 10.09.22. -// - -import Foundation -import CoreData - -extension AppUsageDataEntity { - - convenience init(data: AppUsageData, context: NSManagedObjectContext) throws { - - guard let view = data.getView()?.rawValue, - let startTime = data.getStartTime(), - let endTime = data.getEndTime() else { - throw AnalyticsError.missingValues - } - - self.init(context: context) - self.view = view - self.startTime = startTime - self.endTime = endTime - self.latitude = data.getLatitude() ?? AppUsageData.invalidLocation - self.longitude = data.getLongitude() ?? AppUsageData.invalidLocation - } -} diff --git "a/Campus-iOS/AnalyticsCom\303\274o/HashFunction.swift" "b/Campus-iOS/AnalyticsCom\303\274o/HashFunction.swift" deleted file mode 100644 index 656dc19d..00000000 --- "a/Campus-iOS/AnalyticsCom\303\274o/HashFunction.swift" +++ /dev/null @@ -1,20 +0,0 @@ -// -// HashFunction.swift -// Campus-iOS -// -// Created by Robyn Kölle on 10.09.22. -// - -import Foundation -import CryptoKit - -struct HashFunction { - - // Source: - // https://www.hackingwithswift.com/example-code/cryptokit/how-to-calculate-the-sha-hash-of-a-string-or-data-instance - static func sha256(_ string: String) -> String { - let inputData = Data(string.utf8) - let hashed = SHA256.hash(data: inputData) - return hashed.compactMap { String(format: "%02x", $0) }.joined() - } -} diff --git "a/Campus-iOS/AnalyticsCom\303\274o/Secrets.xcconfig" "b/Campus-iOS/AnalyticsCom\303\274o/Secrets.xcconfig" deleted file mode 100644 index d851db4c..00000000 --- "a/Campus-iOS/AnalyticsCom\303\274o/Secrets.xcconfig" +++ /dev/null @@ -1,12 +0,0 @@ -// -// Secrets.xcconfig -// Campus-iOS -// -// Created by Robyn Kölle on 10.09.22. -// - -// Configuration settings file format documentation can be found at: -// https://help.apple.com/xcode/#/dev745c5c974 - -ANALYTICS_API = -ANALYTICS_POST_TOKEN = diff --git a/Campus-iOS/App.swift b/Campus-iOS/App.swift index 5efdf8e5..7045d8f8 100644 --- a/Campus-iOS/App.swift +++ b/Campus-iOS/App.swift @@ -11,6 +11,7 @@ import KVKCalendar import Firebase @main +@MainActor struct CampusApp: App { @StateObject var model: Model = Model() @@ -46,13 +47,29 @@ struct CampusApp: App { }) .environmentObject(model) .environment(\.managedObjectContext, persistenceController.container.viewContext) + .task { + guard let credentials = model.loginController.credentials else { + model.isUserAuthenticated = false + model.isLoginSheetPresented = true + return + } + + switch credentials { + case .noTumID: + model.isUserAuthenticated = false + model.isLoginSheetPresented = false + case .tumID(tumID: _, token: _), .tumIDAndKey(tumID: _, token: _, key: _): + model.isUserAuthenticated = true + model.isLoginSheetPresented = false + } + } } } func tabViewComponent() -> some View { TabView(selection: $selectedTab) { NavigationView { - CalendarContentView( + CalendarScreen( model: model, refresh: $model.isUserAuthenticated ) diff --git a/Campus-iOS/Base/Enums/Enums.swift b/Campus-iOS/Base/Enums/Enums.swift index d7eb5836..c35e9e89 100644 --- a/Campus-iOS/Base/Enums/Enums.swift +++ b/Campus-iOS/Base/Enums/Enums.swift @@ -31,7 +31,22 @@ enum Gender: Decodable, Hashable { } } -enum ContactInfo { +enum ContactInfo: Identifiable { + var id: String { + switch self { + case .phone(let phone): + return phone + case .mobilePhone(let mobile): + return mobile + case .fax(let fax): + return fax + case .additionalInfo(let info): + return info + case .homepage(let homepage): + return homepage + } + } + case phone(String) case mobilePhone(String) case fax(String) diff --git a/Campus-iOS/Base/Networking/APIErrors/EatAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/EatAPIError.swift new file mode 100644 index 00000000..d111c5ae --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/EatAPIError.swift @@ -0,0 +1,37 @@ +// +// EatAPIError.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +enum EatAPIError: APIError, LocalizedError { + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + } + } +} diff --git a/Campus-iOS/Base/Networking/APIErrors/MVGAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/MVGAPIError.swift new file mode 100644 index 00000000..e240661f --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/MVGAPIError.swift @@ -0,0 +1,37 @@ +// +// MVGAPIError.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +enum MVGAPIError: APIError, LocalizedError { + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + } + } +} diff --git a/Campus-iOS/Base/Networking/APIErrors/NavigaTUMAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/NavigaTUMAPIError.swift new file mode 100644 index 00000000..280a03df --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/NavigaTUMAPIError.swift @@ -0,0 +1,37 @@ +// +// NAvigaTUMAPIError.swift +// Campus-iOS +// +// Created by David Lin on 10.04.23. +// + +import Foundation + +enum NavigaTUMAPIError: APIError, LocalizedError { + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + } + } +} diff --git a/Campus-iOS/Base/Networking/APIErrors/TUMCabeAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/TUMCabeAPIError.swift new file mode 100644 index 00000000..bc4b762b --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/TUMCabeAPIError.swift @@ -0,0 +1,37 @@ +// +// TUMCabeAPIError.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +enum TUMCabeAPIError: APIError, LocalizedError { + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + } + } +} diff --git a/Campus-iOS/Base/Networking/APIErrors/TUMDevAppAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/TUMDevAppAPIError.swift new file mode 100644 index 00000000..75158695 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/TUMDevAppAPIError.swift @@ -0,0 +1,37 @@ +// +// TUMDevAppAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +enum TUMDevAppAPIError: APIError, LocalizedError { + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + } + } +} diff --git a/Campus-iOS/Base/Networking/APIErrors/TUMOnlineAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/TUMOnlineAPIError.swift new file mode 100644 index 00000000..68f785bf --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/TUMOnlineAPIError.swift @@ -0,0 +1,66 @@ +// +// TUMOnlineAPIError.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +enum TUMOnlineAPIError: APIError, LocalizedError { + case noPermission + case tokenNotConfirmed + case invalidToken + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message = "message" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + case let str where str.contains("Keine Rechte für Funktion"): + self = .noPermission + case "Token ist nicht bestätigt!": + self = .tokenNotConfirmed + case "Token ist ungültig!": + self = .invalidToken + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case .noPermission: + return "No Permission".localized + case .tokenNotConfirmed: + return "Token not confirmed".localized + case .invalidToken: + return "Token invalid".localized + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + + } + } + + public var recoverySuggestion: String? { + switch self { + case .noPermission: + return "Make sure to enable the right permissions for your token.".localized + case .tokenNotConfirmed: + return "Go to TUMonline and confirm your token.".localized + case .invalidToken: + return "Try creating a new token.".localized + default: + return nil + } + } +} diff --git a/Campus-iOS/Base/Networking/APIErrors/TUMSexyAPIError.swift b/Campus-iOS/Base/Networking/APIErrors/TUMSexyAPIError.swift new file mode 100644 index 00000000..09356b20 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIErrors/TUMSexyAPIError.swift @@ -0,0 +1,37 @@ +// +// TUMSexyAPIError.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +enum TUMSexyAPIError: APIError, LocalizedError { + case unknown(String) + + enum CodingKeys: String, CodingKey { + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let error = try container.decode(String.self, forKey: .message) + + switch error { + default: + self = .unknown(error) + } + } + + init(message: String) { + self = .unknown(message) + } + + public var errorDescription: String? { + switch self { + case let .unknown(message): + return "\("Unkonw error".localized): \(message)" + } + } +} diff --git a/Campus-iOS/Base/Networking/APIs/EatAPI.swift b/Campus-iOS/Base/Networking/APIs/EatAPI.swift new file mode 100644 index 00000000..0bdbe9a4 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIs/EatAPI.swift @@ -0,0 +1,46 @@ +// +// EatAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation +import Alamofire + +enum EatAPI: API { + case canteens + case languages + case labels + case all + case all_ref + case menu(location: String, year: Int = Date().year, week: Int = Date().weekOfYear) + + static var baseURL: String = "https://tum-dev.github.io/eat-api/" + + static var baseHeaders: Alamofire.HTTPHeaders = [] + + static var error: APIError.Type = EatAPIError.self + + var paths: String { + switch self { + case .canteens: return "enums/canteens.json" + case .languages: return "enums/languages.json" + case .labels: return "enums/labels.json" + case .all: return "all.json" + case .all_ref: return "all_ref.json" + case let .menu(location, year, week): return "\(location)/\(year)/\(String(format: "%02d", week)).json" + } + } + + var parameters: [String : String] { [:] } + + var needsAuth: Bool { false } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd) + + return try jsonDecoder.decode(type, from: data) + } +} diff --git a/Campus-iOS/Base/Networking/APIs/MVGAPI.swift b/Campus-iOS/Base/Networking/APIs/MVGAPI.swift new file mode 100644 index 00000000..f4b7cff7 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIs/MVGAPI.swift @@ -0,0 +1,55 @@ +// +// MVGAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation +import Alamofire + +enum MVGAPI: API { + case nearby(latitude: String, longitude: String) + case departure(id: Int) + case station(name: String) + case id(id: Int) + case interruptions + + static var baseURL: String = "https://www.mvg.de/" + + static let apiKey = "5af1beca494712ed38d313714d4caff6" + static var baseHeaders: Alamofire.HTTPHeaders = ["X-MVG-Authorization-Key": apiKey] + + static var error: APIError.Type = MVGAPIError.self + + var paths: String { + switch self { + case .nearby: return "fahrinfo/api/location/nearby" + case .departure(let id): return "fahrinfo/api/departure/\(id)" + case .station: return "fahrinfo/api/location/queryWeb" + case .id: return "fahrinfo/api/location/query" + case .interruptions: return ".rest/betriebsaenderungen/api/interruption" + } + } + + var parameters: [String : String] { + switch self { + case .station(name: let name): + return ["q": name] + case .id(id: let id): + return ["q": String(id)] + case .departure(id: _): + return ["footway": String(0)] + case .nearby(latitude: let latitude, longitude: let longitude): + return ["latitude": latitude, "longitude": longitude] + default: + return [:] + } + } + + var needsAuth: Bool { false } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + return try JSONDecoder().decode(type, from: data) + } +} diff --git a/Campus-iOS/Base/Networking/APIs/NavigaTUMAPI.swift b/Campus-iOS/Base/Networking/APIs/NavigaTUMAPI.swift new file mode 100644 index 00000000..65994ad9 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIs/NavigaTUMAPI.swift @@ -0,0 +1,50 @@ +// +// NavigaTUMAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.04.23. +// + +import Foundation +import Alamofire + +enum NavigaTUMAPI: API { + case search(query: String) + case details(id: String, language: String) + case images(id: String) + case overlayImages(id: String) + + static var baseURL: String = "https://nav.tum.de/" + + static var baseHeaders: Alamofire.HTTPHeaders = [] + + static var error: APIError.Type = NavigaTUMAPIError.self + + var paths: String { + switch self { + case .search: return "api/search" + case .details(let id, _): return "api/get" + "/" + id + case .images(let id): return "cdn/maps/roomfinder" + "/" + id + case .overlayImages(let id): return "cdn/maps/overlay" + "/" + id + } + } + + var parameters: [String : String] { + switch self { + case .search(let query): return ["q": query] + case .details(_, let language): return ["lang": language] + case .images(_): return [:] + case .overlayImages(_): return [:] + } + } + + var needsAuth: Bool { + switch self { + case .search, .details, .images, .overlayImages: return false + } + } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + return try JSONDecoder().decode(type, from: data) + } +} diff --git a/Campus-iOS/Base/Networking/TUMCabeAPI.swift b/Campus-iOS/Base/Networking/APIs/TUMCabeAPI.swift similarity index 74% rename from Campus-iOS/Base/Networking/TUMCabeAPI.swift rename to Campus-iOS/Base/Networking/APIs/TUMCabeAPI.swift index 2159737f..40638e9a 100644 --- a/Campus-iOS/Base/Networking/TUMCabeAPI.swift +++ b/Campus-iOS/Base/Networking/APIs/TUMCabeAPI.swift @@ -1,15 +1,16 @@ // // TUMCabeAPI.swift -// TUM Campus App +// Campus-iOS // -// Created by Tim Gymnich on 1/4/19. -// Copyright © 2019 TUM. All rights reserved. +// Created by David Lin on 10.02.23. // +import Foundation import Alamofire import UIKit -enum TUMCabeAPI: URLRequestConvertible { +enum TUMCabeAPI: API { + // Different data types of data which determine the path, parameters and if authentication is needed. case movie case cafeteria case news(source: String) @@ -30,14 +31,14 @@ enum TUMCabeAPI: URLRequestConvertible { case ticketPurchase case stripeKey - static let baseURLString = "https://app.tum.de/api" - static let serverTrustPolicies: [String: ServerTrustEvaluating] = ["app.tum.de" : PinnedCertificatesTrustEvaluator()] + static let baseURL = "https://app.tum.de/api/" static let baseHeaders: HTTPHeaders = ["X-DEVICE-ID": UIDevice.current.identifierForVendor?.uuidString ?? "not available", "X-APP-VERSION": Bundle.main.version, "X-APP-BUILD": Bundle.main.build, "X-OS-VERSION": UIDevice.current.systemVersion,] + static var error: APIError.Type = TUMCabeAPIError.self - var path: String { + var paths: String { switch self { case .movie: return "kino" case .cafeteria: return "mensen" @@ -45,7 +46,7 @@ enum TUMCabeAPI: URLRequestConvertible { case .newsSources: return "news/sources" case .newsAlert: return "news/alert" case .roomSearch(let room): return "roomfinder/room/search/\(room.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? "")" - case .roomMaps(let room): return "roomfinder/room/availableMaps/\(room)" + case .roomMaps(let room): return "roomfinder/room/availableMaps/\(room.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? "")" case .roomCoordinates(let room): return "roomfinder/room/coordinates/\(room)" case .defaultMap(let room): return "roomfinder/room/defaultMap/\(room)" case .mapImage(let room, let id): return "roomfinder/room/map/\(room)/\(id)" @@ -61,17 +62,25 @@ enum TUMCabeAPI: URLRequestConvertible { } } - var method: HTTPMethod { - switch self { - default: return .get - } + var parameters: [String : String] { + return [:] } - static var requiresAuth: [String] = [] + var needsAuth: Bool { + // No authentication needed + return false + } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMddhhmmss) + + return try jsonDecoder.decode(type, from: data) + } func asURLRequest() throws -> URLRequest { - let url = try TUMCabeAPI.baseURLString.asURL() - let urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method, headers: TUMCabeAPI.baseHeaders) + let url = try Self.baseURL.asURL() + let urlRequest = try URLRequest(url: url.appendingPathComponent(paths), method: .get, headers: Self.baseHeaders) return urlRequest } } diff --git a/Campus-iOS/Base/Networking/APIs/TUMDevAppAPI.swift b/Campus-iOS/Base/Networking/APIs/TUMDevAppAPI.swift new file mode 100644 index 00000000..54f5fbb7 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIs/TUMDevAppAPI.swift @@ -0,0 +1,41 @@ +// +// TUMDevAppAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation +import Alamofire + +enum TUMDevAppAPI: API { + case room(roomNr: Int) + case rooms + + static var baseURL: String = "https://www.devapp.it.tum.de/" + + static var baseHeaders: Alamofire.HTTPHeaders = [] + + static var error: APIError.Type = TUMDevAppAPIError.self + + var paths: String { + switch self { + case .room, .rooms: return "iris/ris_api.php" + } + } + + var parameters: [String : String] { + switch self { + case .room(roomNr: let roomNr): + return ["format": "json", "raum": String(roomNr)] + case .rooms: + return ["format": "json"] + } + } + + var needsAuth: Bool { false } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + return try JSONDecoder().decode(type, from: data) + } +} diff --git a/Campus-iOS/Base/Networking/APIs/TUMOnlineAPI.swift b/Campus-iOS/Base/Networking/APIs/TUMOnlineAPI.swift new file mode 100644 index 00000000..3fcc4810 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIs/TUMOnlineAPI.swift @@ -0,0 +1,117 @@ +// +// TUMOnlineAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation +import Alamofire +import XMLCoder +import UIKit + +enum TUMOnlineAPI: API { + // Different data types of data which determine the path, parameters and if authentication is needed. + case personSearch(search: String) + case tokenRequest(tumID: String, tokenName: String?) + case tokenConfirmation + case tuitionStatus + case calendar + case personDetails(identNumber: String) + case personalLectures + case personalGrades + case lectureSearch(search: String) + case lectureDetails(lvNr: String) + case identify + case secretUpload + case profileImage(personGroup: String, id: String) + + + static let baseURL: String = "https://campus.tum.de/tumonline/" + + static let baseHeaders: Alamofire.HTTPHeaders = [] + + static var error: APIError.Type = TUMOnlineAPIError.self + + var paths: String { + switch self { + case .personSearch: return "wbservicesbasic.personenSuche" + case .tokenRequest: return "wbservicesbasic.requestToken" + case .tokenConfirmation: return "wbservicesbasic.isTokenConfirmed" + case .tuitionStatus: return "wbservicesbasic.studienbeitragsstatus" + case .calendar: return "wbservicesbasic.kalender" + case .personDetails: return "wbservicesbasic.personenDetails" + case .personalLectures: return "wbservicesbasic.veranstaltungenEigene" + case .personalGrades: return "wbservicesbasic.noten" + case .lectureSearch: return "wbservicesbasic.veranstaltungenSuche" + case .lectureDetails: return "wbservicesbasic.veranstaltungenDetails" + case .identify: return "wbservicesbasic.id" + case .secretUpload: return "wbservicesbasic.secretUpload" + case .profileImage: return "visitenkarte.showImage" + } + } + + var parameters: [String : String] { + switch self { + case .personSearch(search: let search): + return ["pSuche": search] + case .tokenRequest(tumID: let tumID, tokenName: let tokenName): + let tokenName = tokenName ?? "TCA - \(UIDevice.current.name)" + return ["pUsername" : tumID, "pTokenName" : tokenName] + case .personDetails(identNumber: let identNumber): + return ["pIdentNr": identNumber] + case .lectureSearch(search: let search): + return ["pSuche": search] + case .lectureDetails(lvNr: let lvNr): + return ["pLVNr": lvNr] + case .profileImage(personGroup: let personGroup, id: let id): + return ["pPersonenGruppe": personGroup, "pPersonenId": id] + default: + return [:] + } + } + + var needsAuth: Bool { + switch self { + case .personSearch(search: _), + .tokenConfirmation, + .tuitionStatus, + .calendar, + .personDetails(identNumber: _), + .personalLectures, + .personalGrades, + .lectureSearch(search: _), + .lectureDetails(_), + .identify: return true + default: + return false + } + } + + var dateDecodingStrategy: DateFormatter { + switch self { + case .calendar : + return DateFormatter.yyyyMMddhhmmss + default : + return DateFormatter.yyyyMMdd + } + } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + let xmlDecoder = XMLDecoder() + xmlDecoder.dateDecodingStrategy = .formatted(self.dateDecodingStrategy) + + return try xmlDecoder.decode(type, from: data) + } + + struct Response: Decodable { + public var row: [T] + } + + struct CalendarResponse: Decodable { + // This is needed because for .calendar the response is not "rowset" and "row", instead it is "events" and "event" + public var event: [CalendarEvent] + } + + static let imageCache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) +} diff --git a/Campus-iOS/Base/Networking/APIs/TUMSexyAPI.swift b/Campus-iOS/Base/Networking/APIs/TUMSexyAPI.swift new file mode 100644 index 00000000..31df30e4 --- /dev/null +++ b/Campus-iOS/Base/Networking/APIs/TUMSexyAPI.swift @@ -0,0 +1,29 @@ +// +// TUMSexyAPI.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation +import Alamofire + +enum TUMSexyAPI: API { + case standard + + static var baseURL: String = "https://json.tum.sexy/" + + static var baseHeaders: Alamofire.HTTPHeaders = [] + + static var error: APIError.Type = TUMSexyAPIError.self + + var paths: String { "" } + + var parameters: [String : String] { [:] } + + var needsAuth: Bool { false } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + return try JSONDecoder().decode(type, from: data) + } +} diff --git a/Campus-iOS/Base/Networking/CampusOnlineAPI.swift b/Campus-iOS/Base/Networking/CampusOnlineAPI.swift index 1c3310b0..3c0d874f 100644 --- a/Campus-iOS/Base/Networking/CampusOnlineAPI.swift +++ b/Campus-iOS/Base/Networking/CampusOnlineAPI.swift @@ -23,9 +23,9 @@ struct CampusOnlineAPI: NetworkingAPI { // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) - static func makeRequest(endpoint: APIConstants, token: String? = nil, forcedRefresh: Bool = false) async throws -> T { + static func makeRequest(endpoint: APIConstants, token: String? = nil, forcedRefresh: Bool? = false) async throws -> T { // Check cache first - if !forcedRefresh, + if !(forcedRefresh ?? false), let data = cache.value(forKey: endpoint.fullRequestURL), let typedData = data as? T { return typedData diff --git a/Campus-iOS/Base/Networking/EatAPI.swift b/Campus-iOS/Base/Networking/EatAPI.swift deleted file mode 100644 index bffb46a3..00000000 --- a/Campus-iOS/Base/Networking/EatAPI.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// EatAPI.swift -// Campus-iOS -// -// Created by August Wittgenstein on 22.12.21. -// - -import Alamofire -import Foundation -import CoreLocation - -enum EatAPI: URLRequestConvertible { - case canteens - case languages - case labels - case all - case all_ref - case menu(location: String, year: Int = Date().year, week: Int = Date().weekOfYear) - - static let baseURLString = "https://tum-dev.github.io/eat-api/" - - var method: HTTPMethod { - switch self { - default: - return .get - } - } - - var path: String { - switch self { - case .canteens: return "enums/canteens.json" - case .languages: return "enums/languages.json" - case .labels: return "enums/labels.json" - case .all: return "all.json" - case .all_ref: return "all_ref.json" - case let .menu(location, year, week): return "\(location)/\(year)/\(String(format: "%02d", week)).json" - } - } - - // MARK: URLRequestConvertible - func asURLRequest() throws -> URLRequest { - let url = try EatAPI.baseURLString.asURL() - let urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) - return urlRequest - } - - static let decoder = JSONDecoder() - - // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min - static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) - - static func fetchCafeterias(forcedRefresh: Bool) async throws -> [Cafeteria] { - - let fullRequestURL = baseURLString + self.canteens.path - - if !forcedRefresh, let rawCafeterias = cache.value(forKey: baseURLString + self.canteens.path), let cafeterias = rawCafeterias as? [Cafeteria] { - print("Canteen data from cache") - return cafeterias - } else { - print("Canteen data from server") - // Fetch new data and store in cache. - var cafeteriaData: Data - do { - cafeteriaData = try await AF.request(self.canteens).serializingData().value - } catch { - print(error) - throw NetworkingError.deviceIsOffline - } - - var cafeteriasWithoutQueue = [Cafeteria]() - do { - cafeteriasWithoutQueue = try decoder.decode([Cafeteria].self, from: cafeteriaData) - } catch { - print(error) - throw error - } - - - - // Requesting the queue data if there is an API for the cafeteria. - var cafeterias = cafeteriasWithoutQueue - - for i in cafeterias.indices { - var queueData: Data - if let queue = cafeterias[i].queueStatusApi { - print("NAME " + cafeterias[i].name) - do { - queueData = try await AF.request(queue).serializingData().value - } catch { - print(error) - throw NetworkingError.deviceIsOffline - } - - do { - cafeterias[i].queue = try decoder.decode(Queue.self, from: queueData) - } catch { - throw error - } - } - } - - // Write value to cache - cache.setValue(cafeterias, forKey: fullRequestURL, cost: cafeterias.count) - return cafeterias - } - } -} diff --git a/Campus-iOS/Base/Networking/MVGAPI.swift b/Campus-iOS/Base/Networking/MVGAPI.swift deleted file mode 100644 index fa4a6a14..00000000 --- a/Campus-iOS/Base/Networking/MVGAPI.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// MVGAPI.swift -// TUM Campus App -// -// Created by Tim Gymnich on 2/23/19. -// Copyright © 2019 TUM. All rights reserved. -// - -import Foundation -import Alamofire - -enum MVGAPI: URLRequestConvertible { - case nearby(latitude: String, longitude: String) - case departure(id: Int) - case station(name: String) - case id(id: Int) - case interruptions - - static let baseURL = "https://www.mvg.de" - static let apiKey = "5af1beca494712ed38d313714d4caff6" - static let baseHeaders: HTTPHeaders = ["X-MVG-Authorization-Key": apiKey] - - var path: String { - switch self { - case .nearby: return "fahrinfo/api/location/nearby" - case .departure(let id): return "fahrinfo/api/departure/\(id)" - case .station: return "fahrinfo/api/location/queryWeb" - case .id: return "fahrinfo/api/location/query" - case .interruptions: return ".rest/betriebsaenderungen/api/interruption" - } - } - - var method: HTTPMethod { - switch self { - default: return .get - } - } - - static var requiresAuth: [String] = [] - - func asURLRequest() throws -> URLRequest { - let url = try MVGAPI.baseURL.asURL() - var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) - - switch self { - - case .station(let name): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["q": name]) - case .id(let id): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["q": id]) - case .departure: - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["footway": 0]) - case .nearby(let latitude, let longitude): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["latitude": latitude, "longitude": longitude]) - default: - break - } - - return urlRequest - } -} diff --git a/Campus-iOS/Base/Networking/NetworkingAPI.swift b/Campus-iOS/Base/Networking/NetworkingAPI.swift index eab4f628..f151d479 100644 --- a/Campus-iOS/Base/Networking/NetworkingAPI.swift +++ b/Campus-iOS/Base/Networking/NetworkingAPI.swift @@ -14,5 +14,5 @@ protocol NetworkingAPI { static var decoder: DecoderType { get } static var cache: Cache { get } - static func makeRequest(endpoint: APIConstants, token: String?, forcedRefresh: Bool) async throws -> T + static func makeRequest(endpoint: APIConstants, token: String?, forcedRefresh: Bool?) async throws -> T } diff --git a/Campus-iOS/Base/Networking/Old APIs/APIResponse.swift b/Campus-iOS/Base/Networking/Old APIs/APIResponse.swift new file mode 100644 index 00000000..ca204757 --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/APIResponse.swift @@ -0,0 +1,54 @@ +// +// APIResponse.swift +// TUM Campus App +// +// Created by Tim Gymnich on 2/27/19. +// Copyright © 2019 TUM. All rights reserved. +// + +import Foundation +import FirebaseCrashlytics + +//struct APIResponse: Decodable { +// var response: ResponseType +// +// init(from decoder: Decoder) throws { +// if let error = try? ErrorType(from: decoder) { +// throw error +// } else { +// let response = try ResponseType(from: decoder) +// self.response = response +// } +// } +//} + +//struct TUMOnlineAPIResponse: Decodable { +// var rows: [T]? +// +// enum CodingKeys: String, CodingKey { +// case rows = "row" +// } +// +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// self.rows = try container.decode([Throwable].self, forKey: .rows).compactMap { +// do { +// return try $0.result.get() +// } +// catch { +// Crashlytics.crashlytics().record(error: error) +// return nil +// } +// } +// } +//} +// +//struct Throwable: Decodable { +// let result: Result +// +// init(from decoder: Decoder) throws { +// result = Result(catching: { try T(from: decoder) }) +// } +//} + + diff --git a/Campus-iOS/Base/Networking/Old APIs/CampusOnlineAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/CampusOnlineAPIOld.swift new file mode 100644 index 00000000..77a6569b --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/CampusOnlineAPIOld.swift @@ -0,0 +1,119 @@ +// +// CampusOnlineAPI.swift +// Campus-iOS +// +// Created by Philipp Zagar on 22.12.21. +// + +import Foundation +import Alamofire +import XMLCoder + +//struct CampusOnlineAPI: NetworkingAPI { +// static let decoder: XMLDecoder = { +// let decoder = XMLDecoder() +// +// let dateFormatter = DateFormatter() +// dateFormatter.dateFormat = "yyyy-MM-dd" +// decoder.dateDecodingStrategy = .formatted(dateFormatter) +// +// return decoder +// }() +// +// // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min +// static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) +// +// static func makeRequest(endpoint: APIConstants, token: String? = nil, forcedRefresh: Bool = false) async throws -> T { +// // Check cache first +// if !forcedRefresh, +// let data = cache.value(forKey: endpoint.fullRequestURL), +// let typedData = data as? T { +// return typedData +// // Otherwise make the request +// } else { +// var data: Data +// do { +// data = try await endpoint.asRequest(token: token).serializingData().value +// } catch { +// print(error) +// throw NetworkingError.deviceIsOffline +// } +// +// // Check this first cause otherwise no error is thrown by the XMLDecoder +// if let error = try? Self.decoder.decode(Error.self, from: data) { +// print(error) +// throw error +// } +// +// do { +// let decodedData = try Self.decoder.decode(T.self, from: data) +// +// // Write value to cache +// cache.setValue(decodedData, forKey: endpoint.fullRequestURL, cost: data.count) +// +// return decodedData +// } catch { +// print(error) +// throw Error.unknown(error.localizedDescription) +// } +// } +// } +// +// enum Error: APIError { +// case noPermission +// case tokenNotConfirmed +// case invalidToken +// case unknown(String) +// +// enum CodingKeys: String, CodingKey { +// case message = "message" +// } +// +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// let error = try container.decode(String.self, forKey: .message) +// +// switch error { +// case let str where str.contains("Keine Rechte für Funktion"): +// self = .noPermission +// case "Token ist nicht bestätigt!": +// self = .tokenNotConfirmed +// case "Token ist ungültig!": +// self = .invalidToken +// default: +// self = .unknown(error) +// } +// } +// +// init(message: String) { +// self = .unknown(message) +// } +// +// public var errorDescription: String? { +// switch self { +// case .noPermission: +// return "No Permission".localized +// case .tokenNotConfirmed: +// return "Token not confirmed".localized +// case .invalidToken: +// return "Token invalid".localized +// case let .unknown(message): +// return "Unknown error".localized + ": \(message)" +// +// } +// } +// +// public var recoverySuggestion: String? { +// switch self { +// case .noPermission: +// return "Make sure to enable the right permissions for your token." +// case .tokenNotConfirmed: +// return "Go to TUMonline and confirm your token." +// case .invalidToken: +// return "Try creating a new token." +// default: +// return nil +// } +// } +// } +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/EatAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/EatAPIOld.swift new file mode 100644 index 00000000..597124de --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/EatAPIOld.swift @@ -0,0 +1,106 @@ +// +// EatAPI.swift +// Campus-iOS +// +// Created by August Wittgenstein on 22.12.21. +// + +import Alamofire +import Foundation +import CoreLocation + +//enum EatAPI: URLRequestConvertible { +// case canteens +// case languages +// case labels +// case all +// case all_ref +// case menu(location: String, year: Int = Date().year, week: Int = Date().weekOfYear) +// +// static let baseURLString = "https://tum-dev.github.io/eat-api/" +// +// var method: HTTPMethod { +// switch self { +// default: +// return .get +// } +// } +// +// var path: String { +// switch self { +// case .canteens: return "enums/canteens.json" +// case .languages: return "enums/languages.json" +// case .labels: return "enums/labels.json" +// case .all: return "all.json" +// case .all_ref: return "all_ref.json" +// case let .menu(location, year, week): return "\(location)/\(year)/\(String(format: "%02d", week)).json" +// } +// } +// +// // MARK: URLRequestConvertible +// func asURLRequest() throws -> URLRequest { +// let url = try EatAPI.baseURLString.asURL() +// let urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) +// return urlRequest +// } +// +// static let decoder = JSONDecoder() +// +// // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min +// static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) +// +// static func fetchCafeterias(forcedRefresh: Bool) async throws -> [Cafeteria] { +// +// let fullRequestURL = baseURLString + self.canteens.path +// +// if !forcedRefresh, let rawCafeterias = cache.value(forKey: baseURLString + self.canteens.path), let cafeterias = rawCafeterias as? [Cafeteria] { +// +// return cafeterias +// } else { +// // Fetch new data and store in cache. +// var cafeteriaData: Data +// do { +// cafeteriaData = try await AF.request(self.canteens).serializingData().value +// } catch { +// print(error) +// throw NetworkingError.deviceIsOffline +// } +// +// var cafeteriasWithoutQueue = [Cafeteria]() +// do { +// cafeteriasWithoutQueue = try decoder.decode([Cafeteria].self, from: cafeteriaData) +// } catch { +// print(error) +// throw error +// } +// +// +// +// // Requesting the queue data if there is an API for the cafeteria. +// var cafeterias = cafeteriasWithoutQueue +// +// for i in cafeterias.indices { +// var queueData: Data +// if let queue = cafeterias[i].queueStatusApi { +// print("NAME " + cafeterias[i].name) +// do { +// queueData = try await AF.request(queue).serializingData().value +// } catch { +// print(error) +// throw NetworkingError.deviceIsOffline +// } +// +// do { +// cafeterias[i].queue = try decoder.decode(Queue.self, from: queueData) +// } catch { +// throw error +// } +// } +// } +// +// // Write value to cache +// cache.setValue(cafeterias, forKey: fullRequestURL, cost: cafeterias.count) +// return cafeterias +// } +// } +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/MVGAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/MVGAPIOld.swift new file mode 100644 index 00000000..58922958 --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/MVGAPIOld.swift @@ -0,0 +1,61 @@ +// +// MVGAPI.swift +// TUM Campus App +// +// Created by Tim Gymnich on 2/23/19. +// Copyright © 2019 TUM. All rights reserved. +// + +import Foundation +import Alamofire + +//enum MVGAPI: URLRequestConvertible { +// case nearby(latitude: String, longitude: String) +// case departure(id: Int) +// case station(name: String) +// case id(id: Int) +// case interruptions +// +// static let baseURL = "https://www.mvg.de" +// static let apiKey = "5af1beca494712ed38d313714d4caff6" +// static let baseHeaders: HTTPHeaders = ["X-MVG-Authorization-Key": apiKey] +// +// var path: String { +// switch self { +// case .nearby: return "fahrinfo/api/location/nearby" +// case .departure(let id): return "fahrinfo/api/departure/\(id)" +// case .station: return "fahrinfo/api/location/queryWeb" +// case .id: return "fahrinfo/api/location/query" +// case .interruptions: return ".rest/betriebsaenderungen/api/interruption" +// } +// } +// +// var method: HTTPMethod { +// switch self { +// default: return .get +// } +// } +// +// static var requiresAuth: [String] = [] +// +// func asURLRequest() throws -> URLRequest { +// let url = try MVGAPI.baseURL.asURL() +// var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) +// +// switch self { +// +// case .station(let name): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["q": name]) +// case .id(let id): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["q": id]) +// case .departure: +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["footway": 0]) +// case .nearby(let latitude, let longitude): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["latitude": latitude, "longitude": longitude]) +// default: +// break +// } +// +// return urlRequest +// } +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/NetworkingAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/NetworkingAPIOld.swift new file mode 100644 index 00000000..65288555 --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/NetworkingAPIOld.swift @@ -0,0 +1,18 @@ +// +// NetworkingAPI.swift +// Campus-iOS +// +// Created by Philipp Zagar on 22.12.21. +// + +import Foundation +import Combine + +//protocol NetworkingAPI { +// // Renaming to `DecoderType` as we otherwise have a conflict between the `Decoder` associatedtype of `Decodable` and the `Decoder` associatedtype of `NetworkingAPI` +// associatedtype DecoderType: TopLevelDecoder +// static var decoder: DecoderType { get } +// static var cache: Cache { get } +// +// static func makeRequest(endpoint: APIConstants, token: String?, forcedRefresh: Bool) async throws -> T +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/TUMCabeAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/TUMCabeAPIOld.swift new file mode 100644 index 00000000..df10fa6c --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/TUMCabeAPIOld.swift @@ -0,0 +1,77 @@ +// +// TUMCabeAPI.swift +// TUM Campus App +// +// Created by Tim Gymnich on 1/4/19. +// Copyright © 2019 TUM. All rights reserved. +// + +import Alamofire +import UIKit + +//enum TUMCabeAPI: URLRequestConvertible { +// case movie +// case cafeteria +// case news(source: String) +// case newsSources +// case newsAlert +// case roomSearch(query: String) +// case roomMaps(room: String) +// case roomCoordinates(room: String) +// case mapImage(room: String, id: Int) +// case defaultMap(room: String) +// case registerDevice(publicKey: String) +// case events +// case myEvents +// case ticketTypes(event: Int) +// case ticketStats(event: Int) +// case ticketReservation +// case ticketReservationCancellation +// case ticketPurchase +// case stripeKey +// +// static let baseURLString = "https://app.tum.de/api" +// static let serverTrustPolicies: [String: ServerTrustEvaluating] = ["app.tum.de" : PinnedCertificatesTrustEvaluator()] +// static let baseHeaders: HTTPHeaders = ["X-DEVICE-ID": UIDevice.current.identifierForVendor?.uuidString ?? "not available", +// "X-APP-VERSION": Bundle.main.version, +// "X-APP-BUILD": Bundle.main.build, +// "X-OS-VERSION": UIDevice.current.systemVersion,] +// +// var path: String { +// switch self { +// case .movie: return "kino" +// case .cafeteria: return "mensen" +// case .news(let source): return "news/\(source)/getAll" +// case .newsSources: return "news/sources" +// case .newsAlert: return "news/alert" +// case .roomSearch(let room): return "roomfinder/room/search/\(room.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? "")" +// case .roomMaps(let room): return "roomfinder/room/availableMaps/\(room)" +// case .roomCoordinates(let room): return "roomfinder/room/coordinates/\(room)" +// case .defaultMap(let room): return "roomfinder/room/defaultMap/\(room)" +// case .mapImage(let room, let id): return "roomfinder/room/map/\(room)/\(id)" +// case .registerDevice(let publicKey): return "device/register/\(publicKey)" +// case .events: return "event/list" +// case .myEvents: return "event/ticket/my" +// case .ticketTypes(let event): return "event/ticket/type/\(event)" +// case .ticketStats(let event): return "event/ticket/status/\(event)" +// case .ticketReservation: return "event/ticket/reserve" +// case .ticketReservationCancellation: return "event/ticket/reserve/cancel" +// case .ticketPurchase: return "event/ticket/payment/stripe/purchase" +// case .stripeKey: return "event/ticket/payment/stripe/ephemeralkey" +// } +// } +// +// var method: HTTPMethod { +// switch self { +// default: return .get +// } +// } +// +// static var requiresAuth: [String] = [] +// +// func asURLRequest() throws -> URLRequest { +// let url = try TUMCabeAPI.baseURLString.asURL() +// let urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method, headers: TUMCabeAPI.baseHeaders) +// return urlRequest +// } +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/TUMDevAppAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/TUMDevAppAPIOld.swift new file mode 100644 index 00000000..f42db8cf --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/TUMDevAppAPIOld.swift @@ -0,0 +1,81 @@ +// +// TUMDevAppAPI.swift +// Campus-iOS +// +// Created by Milen Vitanov on 05.05.22. +// + +import Foundation +import Alamofire +import CoreLocation + +//enum TUMDevAppAPI: URLRequestConvertible { +// case room(roomNr: Int) +// case rooms +// +// static let baseURL = "https://www.devapp.it.tum.de" +// +// var path: String { +// switch self { +// case .room, .rooms: return "iris/ris_api.php" +// } +// } +// +// var method: HTTPMethod { +// switch self { +// default: return .get +// } +// } +// +// static var requiresAuth: [String] = [] +// +// func asURLRequest() throws -> URLRequest { +// let url = try TUMDevAppAPI.baseURL.asURL() +// var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) +// +// switch self { +// case .room(let roomNr): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["format": "json", "raum": roomNr]) +// case .rooms: +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["format": "json"]) +// } +// +// return urlRequest +// } +// +// // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min +// static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) +// +// static func fetchStudyRooms(forcedRefresh: Bool) async throws -> StudyRoomApiRespose { +// +// let fullRequestURL = baseURL + self.rooms.path +// +// if !forcedRefresh, let rawStudyRoomsResponse = cache.value(forKey: baseURL + self.rooms.path), let studyRoomsResponse = rawStudyRoomsResponse as? StudyRoomApiRespose { +// print("Study rooms data from cache") +// return studyRoomsResponse +// } else { +// print("Study rooms data from server") +// // Fetch new data and store in cache. +// var studyRoomsData: Data +// do { +// studyRoomsData = try await AF.request(self.rooms).serializingData().value +// } catch { +// print(error) +// throw NetworkingError.deviceIsOffline +// } +// +// var studyRoomsResponse = StudyRoomApiRespose() +// do { +// studyRoomsResponse = try JSONDecoder().decode(StudyRoomApiRespose.self, from: studyRoomsData) +// } catch { +// print(error) +// throw error +// } +// +// // Write value to cache +// cache.setValue(studyRoomsResponse, forKey: fullRequestURL) +// +// return studyRoomsResponse +// } +// } +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/TUMOnlineAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/TUMOnlineAPIOld.swift new file mode 100644 index 00000000..879cdeef --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/TUMOnlineAPIOld.swift @@ -0,0 +1,93 @@ +// +// TUMOnlineAPI.swift +// TUM Campus App +// +// Created by Tim Gymnich on 1/3/19. +// Copyright © 2019 TUM. All rights reserved. +// + +import UIKit.UIDevice +import Alamofire + +//enum TUMOnlineAPI: URLRequestConvertible { +// case personSearch(search: String) +// case tokenRequest(tumID: String, tokenName: String?) +// case tokenConfirmation +// case tuitionStatus +// case calendar +// case personDetails(identNumber: String) +// case personalLectures +// case personalGrades +// case lectureSearch(search: String) +// case lectureDetails(lvNr: String) +// case identify +// case secretUpload +// case profileImage(personGroup: String, id: String) +// +// static let baseURLString = "https://campus.tum.de/tumonline" +// +// var method: HTTPMethod { +// switch self { +// default: +// return .get +// } +// } +// +// var path: String { +// switch self { +// case .personSearch: return "wbservicesbasic.personenSuche" +// case .tokenRequest: return "wbservicesbasic.requestToken" +// case .tokenConfirmation: return "wbservicesbasic.isTokenConfirmed" +// case .tuitionStatus: return "wbservicesbasic.studienbeitragsstatus" +// case .calendar: return "wbservicesbasic.kalender" +// case .personDetails: return "wbservicesbasic.personenDetails" +// case .personalLectures: return "wbservicesbasic.veranstaltungenEigene" +// case .personalGrades: return "wbservicesbasic.noten" +// case .lectureSearch: return "wbservicesbasic.veranstaltungenSuche" +// case .lectureDetails: return "wbservicesbasic.veranstaltungenDetails" +// case .identify: return "wbservicesbasic.id" +// case .secretUpload: return "wbservicesbasic.secretUpload" +// case .profileImage: return "visitenkarte.showImage?pPersonenGruppe=3&pPersonenId=9C4E2144041FAB5D" +// } +// } +// +// static var requiresAuth: [String] = [ +// "wbservicesbasic.personenSuche", +// "wbservicesbasic.isTokenConfirmed", +// "wbservicesbasic.studienbeitragsstatus", +// "wbservicesbasic.kalender", +// "wbservicesbasic.personenDetails", +// "wbservicesbasic.veranstaltungenEigene", +// "wbservicesbasic.noten", +// "wbservicesbasic.veranstaltungenSuche", +// "wbservicesbasic.veranstaltungenDetails", +// "wbservicesbasic.id", +// ] +// +// +// // MARK: URLRequestConvertible +// +// func asURLRequest() throws -> URLRequest { +// let url = try TUMOnlineAPI.baseURLString.asURL() +// var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) +// +// switch self { +// case let .personSearch(search): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pSuche": search]) +// case let .tokenRequest(tumID, tokenName): +// let tokenName = tokenName ?? "TCA - \(UIDevice.current.name)" +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pUsername" : tumID, "pTokenName" : tokenName]) +// case let .personDetails(identNumber): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pIdentNr": identNumber]) +// case let .lectureSearch(search): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pSuche": search]) +// case let .lectureDetails(lvNr): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pLVNr": lvNr]) +// case let .profileImage(personGroup, id): +// urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pPersonenGruppe": personGroup, "pPersonenId": id]) +// default: +// break +// } +// return urlRequest +// } +//} diff --git a/Campus-iOS/Base/Networking/Old APIs/TUMSexyAPIOld.swift b/Campus-iOS/Base/Networking/Old APIs/TUMSexyAPIOld.swift new file mode 100644 index 00000000..85788992 --- /dev/null +++ b/Campus-iOS/Base/Networking/Old APIs/TUMSexyAPIOld.swift @@ -0,0 +1,29 @@ +// +// TUMSexyAPI.swift +// TUM Campus App +// +// Created by Tim Gymnich on 2/23/19. +// Copyright © 2019 TUM. All rights reserved. +// + +import Alamofire +import Foundation + +//struct TUMSexyAPI: URLRequestConvertible { +// static let baseURLString = "https://json.tum.sexy" +// +// var method: HTTPMethod { +// switch self { +// default: return .get +// } +// } +// +// static var requiresAuth: [String] = [] +// +// func asURLRequest() throws -> URLRequest { +// let url = try TUMSexyAPI.baseURLString.asURL() +// let urlRequest = try URLRequest(url: url, method: method) +// return urlRequest +// } +// +//} diff --git a/Campus-iOS/Base/Networking/Protocols/API.swift b/Campus-iOS/Base/Networking/Protocols/API.swift new file mode 100644 index 00000000..118c833d --- /dev/null +++ b/Campus-iOS/Base/Networking/Protocols/API.swift @@ -0,0 +1,100 @@ +// +// API.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation +import Alamofire +import XMLCoder +import UIKit + +protocol API { + // The base URL will be the entry point for fetching the data. + static var baseURL: String { get } + // If the API requires headers, otherwise declare it as an empty array. + static var baseHeaders: HTTPHeaders { get } + // Type of error to handle errors properly for each API. + static var error: APIError.Type { get } + + // This property should return the respective path for each data type + var paths: String { get } + // The different parameters used for each data type if they are needed, otherwise return an empty dict [:]. + var parameters: [String: String] { get } + // Indicates which data types can only be fetched with authentication + var needsAuth: Bool { get } + + // Returns the baseURL combinded with the relative paths. This is typically used in the asReqeust(token:) method. + var basePathsURL: String { get } + // Returns the basePathURL combined with all the parameters. This is typically used as an identifier for the cache. + var basePathsParametersURL: String { get } + + + /// Produces the final request depending considering if a data type needs authentication. + /// + /// ``` + /// let api = CampusOnline.personalLectures + /// let token = "1234" + /// let request = api.asRequest(token) // A data request used to fetch the data + /// + /// do { + /// let data = try await request.serializingData.value + /// } catch { + /// print("Error occured fetching data: \(String(describing: error))") + /// } + /// ``` + /// + /// - Parameters: + /// - token: The token used to authenticate. + /// - Returns: An `Alamofire.DataRequest` depending on the `token`. + func asRequest(token: String?) -> DataRequest + + /// Uses a decoder (either JSON or XML depending on the API) to decode the fetched data. + /// + /// ``` + /// let data = ... + /// let api = CampusOnline.personalGrades + /// do { + /// let decodedData = try api.decode(type: Grades.self, from: data) + /// } catch { + /// print("Error occurred while decoding: \(String(describing: error))") + /// } + /// ``` + /// + /// - Parameters: + /// - type: Generic data type, which conforms to `Decodable`. + /// - data: The data to be decoded into `type`. + /// - Throws: Throws decoding error if decoding failed. + /// - Returns: The data in the decoded data format. + func decode(_ type: T.Type, from data: Data) throws -> T +} + +extension API { + var basePathsURL: String { + Self.baseURL + self.paths + } + + var basePathsParametersURL: String { + if parameters.isEmpty { + return basePathsURL + } else { + return basePathsURL + "?" + parameters.flatMap({ key, value in + key + "=" + value + }) + } + } + + func asRequest(token: String?) -> Alamofire.DataRequest { + let finalParameters = self.needsAuth ? self.parameters.merging(["pToken": token ?? ""], uniquingKeysWith: { (current, _) in current }) : self.parameters + + return AF.request(self.basePathsURL, parameters: finalParameters, headers: Self.baseHeaders).cacheResponse(using: ResponseCacher(behavior: .cache)) + } +} + +enum APIState { + case na + case loading + case success(data: T) + case failed(error: Error) +} diff --git a/Campus-iOS/Base/Networking/Protocols/APIError.swift b/Campus-iOS/Base/Networking/Protocols/APIError.swift new file mode 100644 index 00000000..5b02b3a9 --- /dev/null +++ b/Campus-iOS/Base/Networking/Protocols/APIError.swift @@ -0,0 +1,12 @@ +// +// APIErrors.swift +// Campus-iOS +// +// Created by David Lin on 19.01.23. +// + +import Foundation + +protocol APIError: Error, Decodable { + init(message: String) +} diff --git a/Campus-iOS/Base/Networking/Protocols/MainAPI.swift b/Campus-iOS/Base/Networking/Protocols/MainAPI.swift new file mode 100644 index 00000000..904227c2 --- /dev/null +++ b/Campus-iOS/Base/Networking/Protocols/MainAPI.swift @@ -0,0 +1,73 @@ +// +// MainAPI.swift +// Campus-iOS +// +// Created by David Lin on 16.01.23. +// + +import Foundation +import Alamofire +import XMLCoder + + +enum MainAPI { + // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min + static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) + + /// Returns a generic value of type `T` fetched from the API or the cache. + /// + /// ``` + /// print(hello("world")) // "Hello, world!" + /// ``` + /// This method uses the specified `endpoint` to make a request, i.e. fetch the data, decode it and to check if any given error occured. + /// If the cache is stil valid (lifetime not expired yet) and the `forcedRefresh` is `false` then data is not fetched from the `endpoint`. + /// Instead the data is retrieved from the cache. + /// + /// > Warning: Some APIs need a token for authentication purposes. + /// + /// - Parameters: + /// - endpoint: An value conforming to the `API`protocol. + /// - token: A string representing the authentication token + /// - forcedRefresh + /// - Throws: Depending on the error, either the a networking or decoding error occurred. + /// - Returns: The retrieved data in as of generic type `T`. + static func makeRequest(endpoint: S, token: String? = nil, forcedRefresh: Bool = false) async throws -> T { + // Check cache first + if !forcedRefresh, let data = cache.value(forKey: endpoint.basePathsParametersURL), let typedData = data as? T { + return typedData + // Otherwise make the request + } else { + var data: Data + do { + data = try await endpoint.asRequest(token: token).serializingData().value + /* + //For debugging; print only for certain types, i.e. Profile responses. + if T.self is TUMOnlineAPI.Response.Type { + print("\(String(data: data, encoding: .utf8))") + } + */ + } catch { + print(error) + throw NetworkingError.deviceIsOffline + } + + if let error = try? endpoint.decode(S.error, from: data) { + print(error) + throw error + } + + do { + // Decode data from the respective endpoint. + let decodedData = try endpoint.decode(T.self, from: data) + // Write value to cache + cache.setValue(decodedData, forKey: endpoint.basePathsParametersURL, cost: data.count) + + return decodedData + + } catch { + print(error) + throw S.error.init(message: error.localizedDescription) + } + } + } +} diff --git a/Campus-iOS/Base/Networking/Protocols/Service.swift b/Campus-iOS/Base/Networking/Protocols/Service.swift new file mode 100644 index 00000000..623690cd --- /dev/null +++ b/Campus-iOS/Base/Networking/Protocols/Service.swift @@ -0,0 +1,18 @@ +// +// ServiceProtocols.swift +// Campus-iOS +// +// Created by David Lin on 20.01.23. +// + +import Foundation + +protocol ServiceTokenProtocol { + associatedtype T : Decodable + func fetch(token: String, forcedRefresh: Bool) async throws -> [T] +} + +protocol ServiceProtocol { + associatedtype T : Decodable + func fetch(forcedRefresh: Bool) async throws -> [T] +} diff --git a/Campus-iOS/Base/Networking/TUMDevAppAPI.swift b/Campus-iOS/Base/Networking/TUMDevAppAPI.swift deleted file mode 100644 index df40f374..00000000 --- a/Campus-iOS/Base/Networking/TUMDevAppAPI.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// TUMDevAppAPI.swift -// Campus-iOS -// -// Created by Milen Vitanov on 05.05.22. -// - -import Foundation -import Alamofire -import CoreLocation - -enum TUMDevAppAPI: URLRequestConvertible { - case room(roomNr: Int) - case rooms - - static let baseURL = "https://www.devapp.it.tum.de" - - var path: String { - switch self { - case .room, .rooms: return "iris/ris_api.php" - } - } - - var method: HTTPMethod { - switch self { - default: return .get - } - } - - static var requiresAuth: [String] = [] - - func asURLRequest() throws -> URLRequest { - let url = try TUMDevAppAPI.baseURL.asURL() - var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) - - switch self { - case .room(let roomNr): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["format": "json", "raum": roomNr]) - case .rooms: - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["format": "json"]) - } - - return urlRequest - } - - // Maximum size of cache: 500kB, Maximum cache entries: 1000, Lifetime: 10min - static let cache = Cache(totalCostLimit: 500_000, countLimit: 1_000, entryLifetime: 10 * 60) - - static func fetchStudyRooms(forcedRefresh: Bool) async throws -> StudyRoomApiRespose { - - let fullRequestURL = baseURL + self.rooms.path - - if !forcedRefresh, let rawStudyRoomsResponse = cache.value(forKey: baseURL + self.rooms.path), let studyRoomsResponse = rawStudyRoomsResponse as? StudyRoomApiRespose { - print("Study rooms data from cache") - return studyRoomsResponse - } else { - print("Study rooms data from server") - // Fetch new data and store in cache. - var studyRoomsData: Data - do { - studyRoomsData = try await AF.request(self.rooms).serializingData().value - } catch { - print(error) - throw NetworkingError.deviceIsOffline - } - - var studyRoomsResponse = StudyRoomApiRespose() - do { - studyRoomsResponse = try JSONDecoder().decode(StudyRoomApiRespose.self, from: studyRoomsData) - } catch { - print(error) - throw error - } - - // Write value to cache - cache.setValue(studyRoomsResponse, forKey: fullRequestURL) - - return studyRoomsResponse - } - } -} diff --git a/Campus-iOS/Base/Networking/TUMOnlineAPI.swift b/Campus-iOS/Base/Networking/TUMOnlineAPI.swift deleted file mode 100644 index 838d38f1..00000000 --- a/Campus-iOS/Base/Networking/TUMOnlineAPI.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// TUMOnlineAPI.swift -// TUM Campus App -// -// Created by Tim Gymnich on 1/3/19. -// Copyright © 2019 TUM. All rights reserved. -// - -import UIKit.UIDevice -import Alamofire - -enum TUMOnlineAPI: URLRequestConvertible { - case personSearch(search: String) - case tokenRequest(tumID: String, tokenName: String?) - case tokenConfirmation - case tuitionStatus - case calendar - case personDetails(identNumber: String) - case personalLectures - case personalGrades - case lectureSearch(search: String) - case lectureDetails(lvNr: String) - case identify - case secretUpload - case profileImage(personGroup: String, id: String) - - static let baseURLString = "https://campus.tum.de/tumonline" - - var method: HTTPMethod { - switch self { - default: - return .get - } - } - - var path: String { - switch self { - case .personSearch: return "wbservicesbasic.personenSuche" - case .tokenRequest: return "wbservicesbasic.requestToken" - case .tokenConfirmation: return "wbservicesbasic.isTokenConfirmed" - case .tuitionStatus: return "wbservicesbasic.studienbeitragsstatus" - case .calendar: return "wbservicesbasic.kalender" - case .personDetails: return "wbservicesbasic.personenDetails" - case .personalLectures: return "wbservicesbasic.veranstaltungenEigene" - case .personalGrades: return "wbservicesbasic.noten" - case .lectureSearch: return "wbservicesbasic.veranstaltungenSuche" - case .lectureDetails: return "wbservicesbasic.veranstaltungenDetails" - case .identify: return "wbservicesbasic.id" - case .secretUpload: return "wbservicesbasic.secretUpload" - case .profileImage: return "visitenkarte.showImage?pPersonenGruppe=3&pPersonenId=9C4E2144041FAB5D" - } - } - - static var requiresAuth: [String] = [ - "wbservicesbasic.personenSuche", - "wbservicesbasic.isTokenConfirmed", - "wbservicesbasic.studienbeitragsstatus", - "wbservicesbasic.kalender", - "wbservicesbasic.personenDetails", - "wbservicesbasic.veranstaltungenEigene", - "wbservicesbasic.noten", - "wbservicesbasic.veranstaltungenSuche", - "wbservicesbasic.veranstaltungenDetails", - "wbservicesbasic.id", - ] - - - // MARK: URLRequestConvertible - - func asURLRequest() throws -> URLRequest { - let url = try TUMOnlineAPI.baseURLString.asURL() - var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method) - - switch self { - case let .personSearch(search): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pSuche": search]) - case let .tokenRequest(tumID, tokenName): - let tokenName = tokenName ?? "TCA - \(UIDevice.current.name)" - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pUsername" : tumID, "pTokenName" : tokenName]) - case let .personDetails(identNumber): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pIdentNr": identNumber]) - case let .lectureSearch(search): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pSuche": search]) - case let .lectureDetails(lvNr): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pLVNr": lvNr]) - case let .profileImage(personGroup, id): - urlRequest = try URLEncoding.default.encode(urlRequest, with: ["pPersonenGruppe": personGroup, "pPersonenId": id]) - default: - break - } - return urlRequest - } -} diff --git a/Campus-iOS/Base/Networking/TUMSexyAPI.swift b/Campus-iOS/Base/Networking/TUMSexyAPI.swift deleted file mode 100644 index 740e03ed..00000000 --- a/Campus-iOS/Base/Networking/TUMSexyAPI.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// TUMSexyAPI.swift -// TUM Campus App -// -// Created by Tim Gymnich on 2/23/19. -// Copyright © 2019 TUM. All rights reserved. -// - -import Alamofire -import Foundation - -struct TUMSexyAPI: URLRequestConvertible { - static let baseURLString = "https://json.tum.sexy" - - var method: HTTPMethod { - switch self { - default: return .get - } - } - - static var requiresAuth: [String] = [] - - func asURLRequest() throws -> URLRequest { - let url = try TUMSexyAPI.baseURLString.asURL() - let urlRequest = try URLRequest(url: url, method: method) - return urlRequest - } - -} diff --git a/Campus-iOS/CalendarComponent/Entity/CalendarEvent.swift b/Campus-iOS/CalendarComponent/Model/CalendarEvent.swift similarity index 98% rename from Campus-iOS/CalendarComponent/Entity/CalendarEvent.swift rename to Campus-iOS/CalendarComponent/Model/CalendarEvent.swift index 23c4a4c9..2988f3af 100644 --- a/Campus-iOS/CalendarComponent/Entity/CalendarEvent.swift +++ b/Campus-iOS/CalendarComponent/Model/CalendarEvent.swift @@ -11,14 +11,14 @@ import UIKit // XMLDecoder cannot use [Event].self so we have to wrap the events in Calendar.self. This is probably a bug in parsing the root node. struct CalendarAPIResponse: Decodable { - var events: [CalendarEvent]? + var events: [CalendarEvent] enum CodingKeys: String, CodingKey { case events = "event" } } -struct CalendarEvent: Identifiable, Equatable, Entity { +struct CalendarEvent: Decodable, Identifiable, Equatable { var descriptionText: String? var endDate: Date? var id: Int64 diff --git a/Campus-iOS/CalendarComponent/Entity/TumCalendarStyle.swift b/Campus-iOS/CalendarComponent/Model/TumCalendarStyle.swift similarity index 100% rename from Campus-iOS/CalendarComponent/Entity/TumCalendarStyle.swift rename to Campus-iOS/CalendarComponent/Model/TumCalendarStyle.swift diff --git a/Campus-iOS/CalendarComponent/Screen/CalendarScreen.swift b/Campus-iOS/CalendarComponent/Screen/CalendarScreen.swift new file mode 100644 index 00000000..7fafb2ed --- /dev/null +++ b/Campus-iOS/CalendarComponent/Screen/CalendarScreen.swift @@ -0,0 +1,82 @@ +// +// CalendarScreen.swift +// Campus-iOS +// +// Created by David Lin on 20.01.23. +// + +import SwiftUI + +struct CalendarScreen: View { + @StateObject var vm: CalendarViewModel + @Binding var refresh: Bool + + init(model: Model, refresh: Binding) { + self._vm = StateObject(wrappedValue: + CalendarViewModel( + model: model, + service: CalendarService() + ) + ) + self._refresh = refresh + } + + var body: some View { + Group { + switch vm.state { + case .success(let events): + VStack { + CalendarContentView( + model: self.vm.model, events: events + ) + .refreshable { + await vm.getCalendar(forcedRefresh: true) + } + } + case .loading, .na: + LoadingView(text: "Fetching Calendar") + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: vm.getCalendar + ) + } + } + .task { + await vm.getCalendar() + } + // Refresh whenever user authentication status changes + .onChange(of: self.refresh) { _ in + Task { + await vm.getCalendar() + } + } + // As LoginView is just a sheet displayed in front of the GradeScreen + // Listen to changes on the token, then fetch the grades + .onChange(of: self.vm.model.token ?? "") { _ in + Task { + await vm.getCalendar() + } + } + .alert( + "Error while fetching Grades", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getCalendar(forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMOnlineAPIError { + Text(apiError.errorDescription ?? "TUMOnlineAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } +} diff --git a/Campus-iOS/CalendarComponent/Service/CalendarService.swift b/Campus-iOS/CalendarComponent/Service/CalendarService.swift new file mode 100644 index 00000000..94fb8889 --- /dev/null +++ b/Campus-iOS/CalendarComponent/Service/CalendarService.swift @@ -0,0 +1,16 @@ +// +// CalendarService.swift +// Campus-iOS +// +// Created by David Lin on 20.01.23. +// + +import Foundation + +struct CalendarService: ServiceTokenProtocol { + func fetch(token: String, forcedRefresh: Bool = false) async throws -> [CalendarEvent] { + let response: TUMOnlineAPI.CalendarResponse = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.calendar, token: token, forcedRefresh: forcedRefresh) + + return response.event + } +} diff --git a/Campus-iOS/CalendarComponent/ViewModel/CalendarViewModel.swift b/Campus-iOS/CalendarComponent/ViewModel/CalendarViewModel.swift index 8a7643f2..eed01188 100644 --- a/Campus-iOS/CalendarComponent/ViewModel/CalendarViewModel.swift +++ b/Campus-iOS/CalendarComponent/ViewModel/CalendarViewModel.swift @@ -8,56 +8,53 @@ import Foundation import XMLCoder +@MainActor class CalendarViewModel: ObservableObject { - typealias ImporterType = Importer - private static let endpoint = TUMOnlineAPI.calendar - - @Published var events: [CalendarEvent] = [] + @Published var state: APIState<[CalendarEvent]> = .na + @Published var hasError: Bool = false let model: Model - var state: State = .na + let service: CalendarService - init(model: Model) { + init(model: Model, service: CalendarService) { self.model = model - fetch() + self.service = service } - - func fetch(callback: @escaping (Result) -> Void = {_ in }) { - if(self.model.isUserAuthenticated) { - let importer = ImporterType(endpoint: Self.endpoint, predicate: nil, dateDecodingStrategy: .formatted(.yyyyMMddhhmmss)) - DispatchQueue.main.async { - importer.performFetch(handler: { result in - switch result { - case .success(let storage): - self.events = storage.events?.filter( { $0.status != "CANCEL" } ).sorted(by: { - guard let dateOne = $0.startDate, let dateTwo = $1.startDate else { - return false - } - return dateOne > dateTwo - }) ?? [] - - if let _ = storage.events { - callback(.success(true)) - } else { - callback(.failure(CampusOnlineAPI.Error.noPermission)) - } - case .failure(let error): - self.state = .failed(error: error) - } - }) - } + func getCalendar(forcedRefresh: Bool = false) async { + if !forcedRefresh { + self.state = .loading + } + self.hasError = false + + guard let token = self.model.token else { + self.state = .failed(error: NetworkingError.unauthorized) + self.hasError = true + return + } + + do { + let events = try await service.fetch(token: token, forcedRefresh: forcedRefresh) - } else { - self.events = [] + self.state = .success( + data: events.filter( { $0.status != "CANCEL" } ).sorted {$0.startDate ?? .distantPast > $1.startDate ?? .distantPast}) + } catch { + self.state = .failed(error: error) + self.hasError = true } } var eventsByDate: [Date? : [CalendarEvent]] { - let sortedEvents = events.sorted { $0.startDate ?? Date() < $1.startDate ?? Date() } - let filteredEvents = sortedEvents.filter { Date() <= $0.startDate ?? Date() } - let dictionary = Dictionary(grouping: filteredEvents, by: { $0.startDate?.removeTimeStamp }) - return dictionary + if case .success(let data) = state { + let sortedEvents = data.sorted { $0.startDate ?? Date() < $1.startDate ?? Date() } + let filteredEvents = sortedEvents.filter { Date() <= $0.startDate ?? Date() } + let dictionary = Dictionary(grouping: filteredEvents, by: { $0.startDate?.removeTimeStamp }) + + return dictionary + + } else { + return [:] + } } } diff --git a/Campus-iOS/CalendarComponent/Views/CalendarContentView.swift b/Campus-iOS/CalendarComponent/Views/CalendarContentView.swift index 54176ec3..5db1f781 100644 --- a/Campus-iOS/CalendarComponent/Views/CalendarContentView.swift +++ b/Campus-iOS/CalendarComponent/Views/CalendarContentView.swift @@ -9,20 +9,15 @@ import SwiftUI import KVKCalendar struct CalendarContentView: View { - - @StateObject var viewModel: CalendarViewModel - @Binding var refresh: Bool @AppStorage("calendarWeekDays") var calendarWeekDays: Int = 7 @State var selectedType: CalendarType = .week @State var selectedEventID: String? @State var isTodayPressed: Bool = false @State private var data = AppUsageData() - - init(model: Model, refresh: Binding) { - self._viewModel = StateObject(wrappedValue: CalendarViewModel(model: model)) - self._refresh = refresh - } + + let model: Model + var events: [CalendarEvent] = [] var body: some View { VStack { @@ -31,19 +26,19 @@ struct CalendarContentView: View { switch self.selectedType { case .week: CalendarDisplayView( - events: self.viewModel.events.map({ $0.kvkEvent }), + events: self.events.map({ $0.kvkEvent }), type: .week, selectedEventID: self.$selectedEventID, frame: Self.getSafeAreaFrame(geometry: geo), todayPressed: self.$isTodayPressed, calendarWeekDays: UInt(calendarWeekDays)) case .day: CalendarDisplayView( - events: self.viewModel.events.map({ $0.kvkEvent }), + events: self.events.map({ $0.kvkEvent }), type: .day, selectedEventID: self.$selectedEventID, frame: Self.getSafeAreaFrame(geometry: geo), todayPressed: self.$isTodayPressed, calendarWeekDays: UInt(calendarWeekDays)) case .month: CalendarDisplayView( - events: self.viewModel.events.map({ $0.kvkEvent }), + events: self.events.map({ $0.kvkEvent }), type: .month, selectedEventID: self.$selectedEventID, frame: Self.getSafeAreaFrame(geometry: geo), todayPressed: self.$isTodayPressed, calendarWeekDays: UInt(calendarWeekDays)) @@ -52,16 +47,12 @@ struct CalendarContentView: View { } } } - // Refresh whenever user authentication status changes - .onChange(of: self.refresh) { _ in - self.viewModel.fetch() - } .sheet(item: self.$selectedEventID) { eventId in - let chosenEvent = self.viewModel.events + let chosenEvent = self.events .first(where: { $0.id.description == eventId }) CalendarSingleEventView( viewModel: LectureDetailsViewModel( - model: viewModel.model, + model: model, service: LectureDetailsService(), // Yes, it is a really hacky solution... lecture: Lecture(id: UInt64(chosenEvent?.lvNr ?? "") ?? 0, lvNumber: UInt64(chosenEvent?.lvNr ?? "") ?? 0, title: "", duration: "", stp_sp_sst: "", eventTypeDefault: "", eventTypeTag: "", semesterYear: "", semesterType: "", semester: "", semesterID: "", organisationNumber: 0, organisation: "", organisationTag: "", speaker: "") @@ -106,7 +97,7 @@ struct CalendarContentView: View { } } ToolbarItemGroup(placement: .navigationBarTrailing) { - ProfileToolbar(model: viewModel.model) + ProfileToolbar(model: model) } } .task { @@ -128,7 +119,7 @@ struct CalendarContentView_Previews: PreviewProvider { static var previews: some View { CalendarContentView( model: MockModel(), - refresh: .constant(false) + events: [] ) } } diff --git a/Campus-iOS/CalendarComponent/Views/CalendarWidgetView.swift b/Campus-iOS/CalendarComponent/Views/CalendarWidgetView.swift index bddb41d3..0357da8f 100644 --- a/Campus-iOS/CalendarComponent/Views/CalendarWidgetView.swift +++ b/Campus-iOS/CalendarComponent/Views/CalendarWidgetView.swift @@ -18,7 +18,7 @@ struct CalendarWidgetView: View { @Binding var refresh: Bool init(model: Model, size: WidgetSize, refresh: Binding = .constant(false)) { - self._viewModel = StateObject(wrappedValue: CalendarViewModel(model: model)) // Fetches in init. + self._viewModel = StateObject(wrappedValue: CalendarViewModel(model: model, service: CalendarService())) self._size = State(initialValue: size) self.initialSize = size self.model = model @@ -45,7 +45,12 @@ struct CalendarWidgetView: View { ) .onChange(of: refresh) { _ in if showDetails { return } - viewModel.fetch() + Task { + await viewModel.getCalendar() + } + } + .task { + await viewModel.getCalendar() } .onTapGesture { showDetails.toggle() @@ -53,7 +58,7 @@ struct CalendarWidgetView: View { .sheet(isPresented: $showDetails) { VStack { Spacer().frame(height: 10) - CalendarContentView(model: model, refresh: .constant(false)) + CalendarScreen(model: self.model, refresh: .constant(false)) } } .expandable(size: $size, initialSize: initialSize, scale: $scale) diff --git a/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings b/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings index ed06fb86..93aa1990 100644 --- a/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings +++ b/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings @@ -43,7 +43,7 @@ "No Links" = "No Links"; "Cafeteria Map" = "Cafeteria Map"; "Search Rooms" = "Search Rooms"; -"Room Finder" = "Room Finder"; +"Roomfinder" = "Roomfinder"; "Unable to find room" = "Unable to find room '%@'"; "No Menu" = "No Menu"; "List View" = "List View"; @@ -126,6 +126,8 @@ "Lecture Search" = "Lecture Search"; "Unable to find lecture" = "Unable to find lecture"; "Choose Speaker" = "Choose Speaker"; +"The search query must be at least 4 characters." = "The search query must be at least 4 characters."; +"Fetching Lectures" = "Fetching Lectures"; // Grades "Fetching Grades" = "Fetching Grades"; @@ -172,6 +174,7 @@ "Room" = "Room"; "Detail" = "Detail"; "Obergeschoß" = "Obergeschoß"; +"Room Details" = "Room Details"; // Map "Search ..." = "Search ..."; diff --git a/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings b/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings index 01d54482..29a06e5a 100644 --- a/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings +++ b/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings @@ -43,7 +43,7 @@ "No Links" = "Keine Links"; "Cafeteria Map" = "Mensa Landkarte"; "Search Rooms" = "Räume suchen"; -"Room Finder" = "Room Finder"; +"Roomfinder" = "Roomfinder"; "Unable to find room" = "Raum '%@' konnte nicht gefunden werden"; "No Menu" = "Kein Menü"; "List View" = "Listenansicht"; @@ -120,6 +120,8 @@ "Lecture Search" = "Vorlesungssuche"; "Unable to find lecture" = "Vorlesung kann nicht gefunden werden"; "Choose Speaker" = "Wähle Vortragenden"; +"The search query must be at least 4 characters." = "Die Suchanfrage muss aus mindestens 4 Zeichen bestehen."; +"Fetching Lectures" = "Lade Vorlesungen"; // Grades "Fetching Grades" = "Lade Noten"; @@ -161,11 +163,12 @@ "Unknown" = "Unbekannt"; // Roomfinder -"Roomfinder" = "Raum Finder"; +"Roomfinder" = "Roomfinder"; "Building" = "Gebäude"; "Room" = "Raum"; "Detail" = "Detail"; "Floor" = "Obergeschoß"; +"Room Details" = "Raum Details"; // Map "Search ..." = "Suchen ..."; diff --git a/Campus-iOS/Extensions/Extensions.swift b/Campus-iOS/Extensions/Extensions.swift index e4ef730c..f3ccadaa 100644 --- a/Campus-iOS/Extensions/Extensions.swift +++ b/Campus-iOS/Extensions/Extensions.swift @@ -18,16 +18,6 @@ extension Bundle { var userAgent: String { "TCA iOS \(version)/\(build)" } } -extension Session { - static let defaultSession: Session = { - let adapterAndRetrier = Interceptor(adapter: AuthenticationHandler(), retrier: AuthenticationHandler()) - let cacher = ResponseCacher(behavior: .cache) -// let trustManager = ServerTrustManager(evaluators: TUMCabeAPI.serverTrustPolicies) - let manager = Session(interceptor: adapterAndRetrier, redirectHandler: ForceHTTPSRedirectHandler(), cachedResponseHandler: cacher) - return manager - }() -} - extension DataRequest { @discardableResult public func responseXML(queue: DispatchQueue = .main, diff --git a/Campus-iOS/GradesComponent/Model/Grade.swift b/Campus-iOS/GradesComponent/Model/Grade.swift index 1aa84948..9dc852ba 100644 --- a/Campus-iOS/GradesComponent/Model/Grade.swift +++ b/Campus-iOS/GradesComponent/Model/Grade.swift @@ -7,58 +7,47 @@ import Foundation -// As XMLDecoding is complete BS -typealias Grade = GradeComponents.Row - -enum GradeComponents { - struct RowSet: Decodable { - public var row: [Row] +struct Grade: Decodable, Identifiable { + // Create own identifier as there isn't one + public var id: String { + date.formatted() + "-" + lvNumber } - - struct Row: Identifiable { - // Create own identifier as there isn't one - public var id: String { - date.formatted() + "-" + lvNumber - } - public var date: Date - public var lvNumber: String - public var semester: String - public var title: String - public var examiner: String - public var grade: String - public var examType: String - public var modus: String - public var studyID: String - public var studyDesignation: String - public var studyNumber: UInt64 - - var modusShort: String { - switch self.modus { - case "Schriftlich": return "Written".localized - case "Beurteilt/immanenter Prüfungscharakter": return "Graded".localized - case "Schriftlich und Mündlich": return "Written/Oral".localized - case "Mündlich": return "Oral".localized - default: return "Unknown".localized - } - } - - enum CodingKeys: String, CodingKey { - case date = "datum" - case lvNumber = "lv_nummer" - case semester = "lv_semester" - case title = "lv_titel" - case examiner = "pruefer_nachname" - case grade = "uninotenamekurz" - case examType = "exam_typ_name" - case modus = "modus" - case studyID = "studienidentifikator" - case studyDesignation = "studienbezeichnung" - case studyNumber = "st_studium_nr" + public var date: Date + public var lvNumber: String + public var semester: String + public var title: String + public var examiner: String + public var grade: String + public var examType: String + public var modus: String + public var studyID: String + public var studyDesignation: String + public var studyNumber: UInt64 + + var modusShort: String { + switch self.modus { + case "Schriftlich": return "Written".localized + case "Beurteilt/immanenter Prüfungscharakter": return "Graded".localized + case "Schriftlich und Mündlich": return "Written/Oral".localized + case "Mündlich": return "Oral".localized + default: return "Unknown".localized } } -} - -extension Grade: Decodable { + + enum CodingKeys: String, CodingKey { + case date = "datum" + case lvNumber = "lv_nummer" + case semester = "lv_semester" + case title = "lv_titel" + case examiner = "pruefer_nachname" + case grade = "uninotenamekurz" + case examType = "exam_typ_name" + case modus = "modus" + case studyID = "studienidentifikator" + case studyDesignation = "studienbezeichnung" + case studyNumber = "st_studium_nr" + } + // Need for a custom Decoder implementation as the XMLCoder library isn't able to handle missing Date properties and the entire decoding fails in case of a non-existing Date value init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -80,6 +69,20 @@ extension Grade: Decodable { studyDesignation = try container.decode(String.self, forKey: .studyDesignation) studyNumber = try container.decode(UInt64.self, forKey: .studyNumber) } + + internal init(date: Date, lvNumber: String, semester: String, title: String, examiner: String, grade: String, examType: String, modus: String, studyID: String, studyDesignation: String, studyNumber: UInt64) { + self.date = date + self.lvNumber = lvNumber + self.semester = semester + self.title = title + self.examiner = examiner + self.grade = grade + self.examType = examType + self.modus = modus + self.studyID = studyID + self.studyDesignation = studyDesignation + self.studyNumber = studyNumber + } } extension Grade { diff --git a/Campus-iOS/GradesComponent/Screen/GradesScreen.swift b/Campus-iOS/GradesComponent/Screen/GradesScreen.swift index 0433e3f4..a5762755 100644 --- a/Campus-iOS/GradesComponent/Screen/GradesScreen.swift +++ b/Campus-iOS/GradesComponent/Screen/GradesScreen.swift @@ -54,7 +54,7 @@ struct GradesScreen: View { } // As LoginView is just a sheet displayed in front of the GradeScreen // Listen to changes on the token, then fetch the grades - .onChange(of: self.vm.token ?? "") { _ in + .onChange(of: self.vm.model.token ?? "") { _ in Task { await vm.getGrades() } @@ -72,8 +72,8 @@ struct GradesScreen: View { Button("Cancel", role: .cancel) { } } message: { detail in if case let .failed(error) = detail { - if let campusOnlineError = error as? CampusOnlineAPI.Error { - Text(campusOnlineError.errorDescription ?? "CampusOnline Error") + if let apiError = error as? TUMOnlineAPIError { + Text(apiError.errorDescription ?? "TUMOnlineAPI Error") } else { Text(error.localizedDescription) } diff --git a/Campus-iOS/GradesComponent/Service/GradesService.swift b/Campus-iOS/GradesComponent/Service/GradesService.swift index 4d625808..683af8be 100644 --- a/Campus-iOS/GradesComponent/Service/GradesService.swift +++ b/Campus-iOS/GradesComponent/Service/GradesService.swift @@ -8,20 +8,9 @@ import Foundation import Alamofire -protocol GradesServiceProtocol { - func fetch(token: String, forcedRefresh: Bool) async throws -> [Grade] -} - -struct GradesService: GradesServiceProtocol { +struct GradesService: ServiceTokenProtocol { func fetch(token: String, forcedRefresh: Bool = false) async throws -> [Grade] { - let response: GradeComponents.RowSet = - try await - CampusOnlineAPI - .makeRequest( - endpoint: Constants.API.CampusOnline.personalGrades, - token: token, - forcedRefresh: forcedRefresh - ) + let response: TUMOnlineAPI.Response = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.personalGrades, token: token, forcedRefresh: forcedRefresh) return response.row } diff --git a/Campus-iOS/GradesComponent/ViewModel/GradesViewModel+State.swift b/Campus-iOS/GradesComponent/ViewModel/GradesViewModel+State.swift index 263b43b5..69946ec8 100644 --- a/Campus-iOS/GradesComponent/ViewModel/GradesViewModel+State.swift +++ b/Campus-iOS/GradesComponent/ViewModel/GradesViewModel+State.swift @@ -7,11 +7,11 @@ import Foundation -extension GradesViewModel { - enum State { - case na - case loading - case success(data: [Grade]) - case failed(error: Error) - } -} +//extension GradesViewModel { +// enum State { +// case na +// case loading +// case success(data: [Grade]) +// case failed(error: Error) +// } +//} diff --git a/Campus-iOS/GradesComponent/ViewModel/GradesViewModel.swift b/Campus-iOS/GradesComponent/ViewModel/GradesViewModel.swift index d6826dee..f4d5920d 100644 --- a/Campus-iOS/GradesComponent/ViewModel/GradesViewModel.swift +++ b/Campus-iOS/GradesComponent/ViewModel/GradesViewModel.swift @@ -14,22 +14,11 @@ protocol GradesViewModelProtocol: ObservableObject { @MainActor class GradesViewModel: GradesViewModelProtocol { - @Published var state: State = .na + @Published var state: APIState<[Grade]> = .na @Published var hasError: Bool = false - private let model: Model - private let service: GradesServiceProtocol - - var token: String? { - switch self.model.loginController.credentials { - case .none, .noTumID: - return nil - case .tumID(_, let token): - return token - case .tumIDAndKey(_, let token, _): - return token - } - } + let model: Model + private let service: GradesService var gradesByDegreeAndSemester: [(String, [(String, [Grade])])] { guard case .success(let data) = self.state else { @@ -85,7 +74,7 @@ class GradesViewModel: GradesViewModelProtocol { } } - init(model: Model, service: GradesServiceProtocol) { + init(model: Model, service: GradesService) { self.model = model self.service = service } @@ -96,7 +85,7 @@ class GradesViewModel: GradesViewModelProtocol { } self.hasError = false - guard let token = self.token else { + guard let token = self.model.token else { self.state = .failed(error: NetworkingError.unauthorized) self.hasError = true return diff --git a/Campus-iOS/GradesComponent/ViewModel/MockGradesViewModel.swift b/Campus-iOS/GradesComponent/ViewModel/MockGradesViewModel.swift index 9fe4cb80..67ab06e0 100644 --- a/Campus-iOS/GradesComponent/ViewModel/MockGradesViewModel.swift +++ b/Campus-iOS/GradesComponent/ViewModel/MockGradesViewModel.swift @@ -16,7 +16,7 @@ class MockGradesViewModel: GradesViewModel { ("1630 17 030", [("Wintersemester 2020/21", Grade.dummyData20W)]) ] - override init(model: Model, service: GradesServiceProtocol) { + override init(model: Model, service: GradesService) { super.init(model: model, service: service) self.state = .success(data: Grade.dummyDataAll) diff --git a/Campus-iOS/GradesComponent/Views/GradeWidgetView.swift b/Campus-iOS/GradesComponent/Views/GradeWidgetView.swift index 2adf75bd..15eb7af3 100644 --- a/Campus-iOS/GradesComponent/Views/GradeWidgetView.swift +++ b/Campus-iOS/GradesComponent/Views/GradeWidgetView.swift @@ -17,7 +17,9 @@ struct GradeWidgetView: View { @Binding var refresh: Bool init(model: Model, size: WidgetSize, refresh: Binding = .constant(false)) { +// self._viewModel = StateObject(wrappedValue: GradesViewModel(model: model, service: GradesService())) self._viewModel = StateObject(wrappedValue: GradesViewModel(model: model, service: GradesService())) + self.size = size self.initialSize = size self._refresh = refresh @@ -70,9 +72,9 @@ struct SimpleGradeWidgetContent: View { .foregroundColor(.clear) .frame(maxHeight: .infinity) .overlay { - if let grade, let gradeString = grade.grade { + if let grade { GeometryReader { g in - Text(gradeString.isEmpty ? "tbd" : gradeString) + Text(grade.grade.isEmpty ? "tbd" : grade.grade) .bold() .font(.system(size: g.size.height * 0.75)) .foregroundColor(GradesViewModel.GradeColor.color(for: grade)) diff --git a/Campus-iOS/GradesComponent/Views/GradesView.swift b/Campus-iOS/GradesComponent/Views/GradesView.swift index 6826efcc..07fc5651 100644 --- a/Campus-iOS/GradesComponent/Views/GradesView.swift +++ b/Campus-iOS/GradesComponent/Views/GradesView.swift @@ -9,6 +9,7 @@ import SwiftUI import SwiftUICharts struct GradesView: View { + @StateObject var vm: GradesViewModel @State private var data = AppUsageData() diff --git a/Campus-iOS/HelperViews/ImageFullScreenView.swift b/Campus-iOS/HelperViews/ImageFullScreenView.swift index ee784c5c..2a2066b0 100644 --- a/Campus-iOS/HelperViews/ImageFullScreenView.swift +++ b/Campus-iOS/HelperViews/ImageFullScreenView.swift @@ -24,8 +24,7 @@ struct ImageFullScreenView: View { }) .gesture(MagnificationGesture().onChanged { val in self.scale = val.magnitude - } - ) + }) } } diff --git a/Campus-iOS/LectureComponent/Model/Lecture.swift b/Campus-iOS/LectureComponent/Model/Lecture.swift index e98dbc2a..344a925d 100644 --- a/Campus-iOS/LectureComponent/Model/Lecture.swift +++ b/Campus-iOS/LectureComponent/Model/Lecture.swift @@ -1,5 +1,5 @@ // -// APIConstants.swift +// LectureComponents.swift // Campus-iOS // // Created by Philipp Zagar on 21.12.21. @@ -7,68 +7,60 @@ import Foundation -// As XMLDecoding is complete BS -typealias Lecture = LectureComponents.Row - -enum LectureComponents { - struct RowSet: Decodable { - public var row: [Row] - } - - struct Row: Decodable, Identifiable, Equatable { - public var id: UInt64 - public var lvNumber: UInt64 - public var title: String - public var duration: String - public var stp_sp_sst: String - public var eventTypeDefault: String - public var eventTypeTag: String - public var semesterYear: String - public var semesterType: String - public var semester: String - public var semesterID: String - public var organisationNumber: UInt64 - public var organisation: String - public var organisationTag: String - public var speaker: String - - public var eventType: String { - switch self.eventTypeDefault { - case "Vorlesung": - return "Lecture".localized - case "Tutorium", "Übung": - return "Exercise".localized - case "Praktikum": - return "Practical course".localized - case "Seminar": - return "Seminar".localized - case "Vorlesung mit integrierten Übungen": - return "Lecture with integrated Exercises".localized - default: - return "" - } - } - - enum CodingKeys: String, CodingKey { - case id = "stp_sp_nr" - case lvNumber = "stp_lv_nr" - case title = "stp_sp_titel" - case duration = "dauer_info" - case stp_sp_sst = "stp_sp_sst" - case eventTypeDefault = "stp_lv_art_name" - case eventTypeTag = "stp_lv_art_kurz" - case semesterYear = "sj_name" - case semesterType = "semester" - case semester = "semester_name" - case semesterID = "semester_id" - case organisationNumber = "org_nr_betreut" - case organisation = "org_name_betreut" - case organisationTag = "org_kennung_betreut" - case speaker = "vortragende_mitwirkende" +struct Lecture: Decodable, Identifiable, Equatable { + public var id: UInt64 + public var lvNumber: UInt64 + public var title: String + public var duration: String + public var stp_sp_sst: String + public var eventTypeDefault: String + public var eventTypeTag: String + public var semesterYear: String + public var semesterType: String + public var semester: String + public var semesterID: String + public var organisationNumber: UInt64 + public var organisation: String + public var organisationTag: String + public var speaker: String + + public var eventType: String { + switch self.eventTypeDefault { + case "Vorlesung": + return "Lecture".localized + case "Tutorium", "Übung": + return "Exercise".localized + case "Praktikum": + return "Practical course".localized + case "Seminar": + return "Seminar".localized + case "Vorlesung mit integrierten Übungen": + return "Lecture with integrated Exercises".localized + default: + return "" } } + + enum CodingKeys: String, CodingKey { + case id = "stp_sp_nr" + case lvNumber = "stp_lv_nr" + case title = "stp_sp_titel" + case duration = "dauer_info" + case stp_sp_sst = "stp_sp_sst" + case eventTypeDefault = "stp_lv_art_name" + case eventTypeTag = "stp_lv_art_kurz" + case semesterYear = "sj_name" + case semesterType = "semester" + case semester = "semester_name" + case semesterID = "semester_id" + case organisationNumber = "org_nr_betreut" + case organisation = "org_name_betreut" + case organisationTag = "org_kennung_betreut" + case speaker = "vortragende_mitwirkende" + } } + extension Lecture { static let dummyData: [Lecture] = [ Lecture(id: 950396293, lvNumber: 90049615, title: "Practical course - Program optimization with LLVM (IN0012, IN2106, IN4236)", duration: "6", stp_sp_sst: "6", eventTypeDefault: "Praktikum", eventTypeTag: "PR", semesterYear: "2018/19", semesterType: "W", semester: "Wintersemester 2018/19", semesterID: "18W", organisationNumber: 15427, organisation: "Informatik 2 - Lehrstuhl für Sprachen und Beschreibungsstrukturen in der Informatik (Prof. Seidl)", organisationTag: "TUINI02", speaker: "Seidl H [L], Petter M"), diff --git a/Campus-iOS/LectureComponent/Model/LectureDetails.swift b/Campus-iOS/LectureComponent/Model/LectureDetails.swift index 53f961e1..ffa7b1a9 100644 --- a/Campus-iOS/LectureComponent/Model/LectureDetails.swift +++ b/Campus-iOS/LectureComponent/Model/LectureDetails.swift @@ -7,94 +7,86 @@ import Foundation -// As XMLDecoding is complete BS -typealias LectureDetails = LectureDetailsComponents.Row - -enum LectureDetailsComponents { - struct RowSet: Decodable { - public var row: [Row] +struct LectureDetails: Decodable, Identifiable { + let id: UInt64 + let lvNumber: UInt64 + let title: String + let duration: String + let stp_sp_sst: String + let eventTypeDefault: String + let eventTypeTag: String + let semester: String + let semesterType: String + let semesterID: String + let semesterYear: String + let organisationNumber: UInt64 + let organisation: String + let organisationTag: String + let speaker: String + let courseContents: String? + let requirements: String? + let courseObjective: String? + let teachingMethod: String? + let anmeld_lv: String? + let firstScheduledDate: String? + let examinationMode: String? + let studienbehelfe: String? + let note: String? + let curriculumURL: URL? + let scheduledDatesURL: URL? + let examDateURL: URL? + + var speakerArray: [String] { + self.speaker.split(separator: ",").map({ $0.trimmingCharacters(in: .whitespaces) }) } - - struct Row: Decodable, Identifiable { - let id: UInt64 - let lvNumber: UInt64 - let title: String - let duration: String - let stp_sp_sst: String - let eventTypeDefault: String - let eventTypeTag: String - let semester: String - let semesterType: String - let semesterID: String - let semesterYear: String - let organisationNumber: UInt64 - let organisation: String - let organisationTag: String - let speaker: String - let courseContents: String? - let requirements: String? - let courseObjective: String? - let teachingMethod: String? - let anmeld_lv: String? - let firstScheduledDate: String? - let examinationMode: String? - let studienbehelfe: String? - let note: String? - let curriculumURL: URL? - let scheduledDatesURL: URL? - let examDateURL: URL? - - var speakerArray: [String] { - self.speaker.split(separator: ",").map({ $0.trimmingCharacters(in: .whitespaces) }) - } - - public var eventType: String { - switch self.eventTypeDefault { - case "Vorlesung": - return "Lecture".localized - case "Tutorium": - return "Exercise".localized - case "Praktikum": - return "Practice".localized - case "Vorlesung mit integrierten Übungen": - return "Lecture with integrated Exercises".localized - default: - return "" - } - } - - enum CodingKeys: String, CodingKey { - case id = "stp_sp_nr" - case lvNumber = "stp_lv_nr" - case title = "stp_sp_titel" - case duration = "dauer_info" - case stp_sp_sst = "stp_sp_sst" - case eventTypeDefault = "stp_lv_art_name" - case eventTypeTag = "stp_lv_art_kurz" - case semesterYear = "sj_name" - case semesterType = "semester" - case semester = "semester_name" - case semesterID = "semester_id" - case organisationNumber = "org_nr_betreut" - case organisation = "org_name_betreut" - case organisationTag = "org_kennung_betreut" - case speaker = "vortragende_mitwirkende" - case courseContents = "lehrinhalt" - case requirements = "voraussetzung_lv" - case courseObjective = "lehrziel" - case teachingMethod = "lehrmethode" - case anmeld_lv - case firstScheduledDate = "ersttermin" - case examinationMode = "pruefmodus" - case studienbehelfe - case note = "anmerkung" - case curriculumURL = "stellung_im_stp_url" - case scheduledDatesURL = "termine_url" - case examDateURL = "pruef_termine_url" + + public var eventType: String { + switch self.eventTypeDefault { + case "Vorlesung": + return "Lecture".localized + case "Tutorium": + return "Exercise".localized + case "Praktikum": + return "Practice".localized + case "Vorlesung mit integrierten Übungen": + return "Lecture with integrated Exercises".localized + default: + return "" } } + + enum CodingKeys: String, CodingKey { + case id = "stp_sp_nr" + case lvNumber = "stp_lv_nr" + case title = "stp_sp_titel" + case duration = "dauer_info" + case stp_sp_sst = "stp_sp_sst" + case eventTypeDefault = "stp_lv_art_name" + case eventTypeTag = "stp_lv_art_kurz" + case semesterYear = "sj_name" + case semesterType = "semester" + case semester = "semester_name" + case semesterID = "semester_id" + case organisationNumber = "org_nr_betreut" + case organisation = "org_name_betreut" + case organisationTag = "org_kennung_betreut" + case speaker = "vortragende_mitwirkende" + case courseContents = "lehrinhalt" + case requirements = "voraussetzung_lv" + case courseObjective = "lehrziel" + case teachingMethod = "lehrmethode" + case anmeld_lv + case firstScheduledDate = "ersttermin" + case examinationMode = "pruefmodus" + case studienbehelfe + case note = "anmerkung" + case curriculumURL = "stellung_im_stp_url" + case scheduledDatesURL = "termine_url" + case examDateURL = "pruef_termine_url" + } } + extension LectureDetails { static let dummyData: LectureDetails = .init( id: 1234, diff --git a/Campus-iOS/LectureComponent/Service/LectureDetailsService.swift b/Campus-iOS/LectureComponent/Service/LectureDetailsService.swift index c31831d8..d0fa24dd 100644 --- a/Campus-iOS/LectureComponent/Service/LectureDetailsService.swift +++ b/Campus-iOS/LectureComponent/Service/LectureDetailsService.swift @@ -15,11 +15,11 @@ protocol LectureDetailsServiceProtocol { struct LectureDetailsService: LectureDetailsServiceProtocol { func fetch(token: String, lvNr: UInt64, forcedRefresh: Bool = false) async throws -> LectureDetails { - let response: LectureDetailsComponents.RowSet = + let response: TUMOnlineAPI.Response = try await - CampusOnlineAPI + MainAPI .makeRequest( - endpoint: Constants.API.CampusOnline.lectureDetails(lvNr: String(lvNr)), + endpoint: TUMOnlineAPI.lectureDetails(lvNr: String(lvNr)), token: token, forcedRefresh: forcedRefresh ) diff --git a/Campus-iOS/LectureComponent/Service/LecturesService.swift b/Campus-iOS/LectureComponent/Service/LecturesService.swift index 3f8ac040..bd399095 100644 --- a/Campus-iOS/LectureComponent/Service/LecturesService.swift +++ b/Campus-iOS/LectureComponent/Service/LecturesService.swift @@ -15,14 +15,9 @@ protocol LecturesServiceProtocol { struct LecturesService: LecturesServiceProtocol { func fetch(token: String, forcedRefresh: Bool = false) async throws -> [Lecture] { - let response: LectureComponents.RowSet = + let response: TUMOnlineAPI.Response = try await - CampusOnlineAPI - .makeRequest( - endpoint: Constants.API.CampusOnline.personalLectures, - token: token, - forcedRefresh: forcedRefresh - ) + MainAPI.makeRequest(endpoint: TUMOnlineAPI.personalLectures, token: token, forcedRefresh: forcedRefresh) return response.row } diff --git a/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsBasicInfoView.swift b/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsBasicInfoView.swift index b99a17cc..bb1172bf 100644 --- a/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsBasicInfoView.swift +++ b/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsBasicInfoView.swift @@ -14,13 +14,13 @@ struct LectureDetailsBasicInfoView: View { @State private var chosenSpeaker = "" var lectureDetails: LectureDetails + @StateObject var personVM: PersonSearchViewModel + let model: Model - var viewModelPersonSearch: PersonSearchViewModel { - let viewModel = PersonSearchViewModel() - if self.chosenSpeaker.count > 3 { - viewModel.fetch(searchString: self.chosenSpeaker) - } - return viewModel + init(model: Model, lectureDetails: LectureDetails) { + self.model = model + self._personVM = StateObject(wrappedValue: PersonSearchViewModel(model: model, service: PersonSearchService())) + self.lectureDetails = lectureDetails } var actionSheetButtons: [ActionSheet.Button] { @@ -41,9 +41,9 @@ struct LectureDetailsBasicInfoView: View { .padding(.bottom, 10) ) { NavigationLink(isActive: self.$navigationLinkActive, destination: { - PersonSearchView(viewModel: self.viewModelPersonSearch, searchText: self.chosenSpeaker) + PersonSearchScreen(model: model, findPerson: chosenSpeaker) }) { - EmptyView() + EmptyView().onAppear{print("empty view")} } VStack(alignment: .leading, spacing: 8) { @@ -94,6 +94,6 @@ struct LectureDetailsBasicInfoView: View { struct LectureDetailsBasicInfoView_Previews: PreviewProvider { static var previews: some View { - LectureDetailsBasicInfoView(lectureDetails: LectureDetails.dummyData) + LectureDetailsBasicInfoView(model: Model(), lectureDetails: LectureDetails.dummyData) } } diff --git a/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsEventInfoView.swift b/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsEventInfoView.swift index 09df0bee..90bb5443 100644 --- a/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsEventInfoView.swift +++ b/Campus-iOS/LectureComponent/Views/LectureDetailsViews/LectureDetailsEventInfoView.swift @@ -47,7 +47,9 @@ struct LectureDetailsEventInfoView: View { ) HStack { Spacer() - NavigationLink(destination: RoomFinderView(model: viewModel.model, viewModel: RoomFinderViewModel(), searchText: extract(room: self.location))) { + NavigationLink(destination: NavigaTumView(model: viewModel.model, searchText: extract(room: self.location)) + .navigationTitle(Text("Roomfinder")) + .navigationBarTitleDisplayMode(.large)) { HStack { Text("Open in RoomFinder") Image(systemName: "arrow.right.circle") diff --git a/Campus-iOS/LectureComponent/Views/LecturesDetailView.swift b/Campus-iOS/LectureComponent/Views/LecturesDetailView.swift index 673f9ea5..14ed8101 100644 --- a/Campus-iOS/LectureComponent/Views/LecturesDetailView.swift +++ b/Campus-iOS/LectureComponent/Views/LecturesDetailView.swift @@ -23,7 +23,7 @@ struct LecturesDetailView: View { LectureDetailsEventInfoView(viewModel: viewModel, event: event) } - LectureDetailsBasicInfoView(lectureDetails: lectureDetails) + LectureDetailsBasicInfoView(model: viewModel.model, lectureDetails: lectureDetails) LectureDetailsDetailedInfoView(lectureDetails: lectureDetails) diff --git a/Campus-iOS/LectureSearchComponent/Screen/LectureSearchScreen.swift b/Campus-iOS/LectureSearchComponent/Screen/LectureSearchScreen.swift new file mode 100644 index 00000000..66f61815 --- /dev/null +++ b/Campus-iOS/LectureSearchComponent/Screen/LectureSearchScreen.swift @@ -0,0 +1,76 @@ +// +// LectureSearchView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 09.02.22. +// + +import SwiftUI + +struct LectureSearchScreen: View { + @StateObject var vm: LectureSearchViewModel + @State var searchText = "" + + init(model: Model) { + self._vm = StateObject(wrappedValue: LectureSearchViewModel(model: model, service: LectureSearchService())) + } + + var body: some View { + Group { + switch vm.state { + case .success(let lectures): + VStack { + LectureSearchView(model: vm.model, lectures: lectures) + .background(Color(.systemGroupedBackground)) + } + case .loading: + if searchText.count > 3 { + LoadingView(text: "Fetching Lectures") + } else { + Text("The search query must be at least 4 characters.") + } + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: {_ in await vm.getLectures(for: self.searchText, forcedRefresh: true)} + ) + case .na: + if searchText.count > 0 && searchText.count <= 3 { + Text("The search query must be at least 4 characters.") + } + } + } + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .onChange(of: self.searchText) { query in + Task { + await vm.getLectures(for: query, forcedRefresh: true) + } + } + .alert( + "Error while fetching Lectures", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getLectures(for: self.searchText, forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMOnlineAPIError { + Text(apiError.errorDescription ?? "TUMOnlineAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } +} + +//struct LectureSearchView_Previews: PreviewProvider { +// static var previews: some View { +// LectureSearchView(model: MockModel()) +// } +//} diff --git a/Campus-iOS/LectureSearchComponent/Service/LectureSearchService.swift b/Campus-iOS/LectureSearchComponent/Service/LectureSearchService.swift new file mode 100644 index 00000000..6651966f --- /dev/null +++ b/Campus-iOS/LectureSearchComponent/Service/LectureSearchService.swift @@ -0,0 +1,16 @@ +// +// LectureSearchService.swift +// Campus-iOS +// +// Created by David Lin on 20.01.23. +// + +import Foundation + +struct LectureSearchService { + func fetch(for query: String, token: String, forcedRefresh: Bool) async throws -> [Lecture] { + let response : TUMOnlineAPI.Response = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.lectureSearch(search: query), token: token, forcedRefresh: forcedRefresh) + + return response.row + } +} diff --git a/Campus-iOS/LectureSearchComponent/View/LectureSearchListView.swift b/Campus-iOS/LectureSearchComponent/View/LectureSearchListView.swift deleted file mode 100644 index e3e4765b..00000000 --- a/Campus-iOS/LectureSearchComponent/View/LectureSearchListView.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// LectureSearchListView.swift -// Campus-iOS -// -// Created by Milen Vitanov on 13.02.22. -// - -import SwiftUI - -struct LectureSearchListView: View { - @StateObject var model: Model - @Environment(\.isSearching) private var isSearching - @ObservedObject var viewModel: LectureSearchViewModel - - var body: some View { - List { - ForEach(self.viewModel.result) { lecture in - NavigationLink( - destination: LectureDetailsScreen(model: self.model, lecture: lecture) - .navigationBarTitleDisplayMode(.inline) - ) { - HStack { - Text(lecture.title) - Spacer() - Text(lecture.eventType) - .foregroundColor(Color(.secondaryLabel)) - } - } - } - if viewModel.errorMessage != "" { - VStack { - Spacer() - Text(self.viewModel.errorMessage).foregroundColor(.gray) - Spacer() - } - } - } - .onChange(of: isSearching) { newValue in - if !newValue { - self.viewModel.result = [] - } - } - } -} - -struct LectureSearchListView_Previews: PreviewProvider { - static var previews: some View { - LectureSearchListView(model: MockModel(), viewModel: LectureSearchViewModel()) - } -} diff --git a/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift b/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift index 17c6d86f..6edd3a3d 100644 --- a/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift +++ b/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift @@ -1,32 +1,37 @@ // -// LectureSearchView.swift +// LectureSearchListView.swift // Campus-iOS // -// Created by Milen Vitanov on 09.02.22. +// Created by Milen Vitanov on 13.02.22. // import SwiftUI struct LectureSearchView: View { - @StateObject var model: Model - @ObservedObject var viewModel = LectureSearchViewModel() - @State var searchText = "" + let model: Model + let lectures: [Lecture] var body: some View { - LectureSearchListView(model: self.model, viewModel: self.viewModel) - .background(Color(.systemGroupedBackground)) - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) - .onChange(of: self.searchText) { searchValue in - if searchValue.count > 3 { - self.viewModel.fetch(searchString: searchValue) + List { + ForEach(lectures) { lecture in + NavigationLink( + destination: LectureDetailsScreen(model: self.model, lecture: lecture) + .navigationBarTitleDisplayMode(.inline) + ) { + HStack { + Text(lecture.title) + Spacer() + Text(lecture.eventType) + .foregroundColor(Color(.secondaryLabel)) + } } } - .animation(.default, value: self.viewModel.result) + } } } -struct LectureSearchView_Previews: PreviewProvider { +struct LectureSearchListView_Previews: PreviewProvider { static var previews: some View { - LectureSearchView(model: MockModel()) + LectureSearchView(model: Model(), lectures: []) } } diff --git a/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift b/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift index 783ee1da..e019d0f9 100644 --- a/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift +++ b/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift @@ -11,30 +11,44 @@ import Foundation import Alamofire import XMLCoder +@MainActor class LectureSearchViewModel: ObservableObject { - @Published var result: [Lecture] = [] - @Published var errorMessage: String = "" + @Published var state: APIState<[Lecture]> = .na + @Published var hasError: Bool = false - private let sessionManager = Session.defaultSession + let model: Model + let service: LectureSearchService - func fetch(searchString: String) { - // activate only when more than 3 characters + init(model: Model, service: LectureSearchService) { + self.model = model + self.service = service + } + + func getLectures(for query: String, forcedRefresh: Bool) async { + guard query.count > 3 else { + // Since requests under 4 char is not allowed + return + } - let endpoint = TUMOnlineAPI.lectureSearch(search: searchString) - sessionManager.cancelAllRequests() - let request = sessionManager.request(endpoint) - request.responseDecodable(of: TUMOnlineAPIResponse.self, decoder: XMLDecoder()) { [weak self] response in - guard !request.isCancelled else { - // cancelAllRequests doesn't seem to cancel all requests, so better check for this explicitly - return - } - self?.result = response.value?.rows ?? [] + if !forcedRefresh { + self.state = .loading + } + self.hasError = false - if let result = self?.result, result.isEmpty { - self?.errorMessage = NSString(format: "Unable to find lecture".localized as NSString, searchString) as String - } else { - self?.errorMessage = "" - } + guard let token = self.model.token else { + self.state = .failed(error: NetworkingError.unauthorized) + self.hasError = true + return + } + + do { + self.state = .success( + data: try await service.fetch(for: query, token: token, forcedRefresh: forcedRefresh) + ) + + } catch { + self.state = .failed(error: error) + self.hasError = true } } } diff --git a/Campus-iOS/LoginComponent/Model/Confirmation.swift b/Campus-iOS/LoginComponent/Model/Confirmation.swift new file mode 100644 index 00000000..650a9c5f --- /dev/null +++ b/Campus-iOS/LoginComponent/Model/Confirmation.swift @@ -0,0 +1,16 @@ +// +// Confirmation.swift +// Campus-iOS +// +// Created by David Lin on 21.01.23. +// + +import Foundation + +struct Confirmation: Decodable { + let value: Bool + + private enum CodingKeys: String, CodingKey { + case value = "" + } +} diff --git a/Campus-iOS/LoginComponent/Service/Credentials.swift b/Campus-iOS/LoginComponent/Model/Credentials.swift similarity index 100% rename from Campus-iOS/LoginComponent/Service/Credentials.swift rename to Campus-iOS/LoginComponent/Model/Credentials.swift diff --git a/Campus-iOS/LoginComponent/Model/Token.swift b/Campus-iOS/LoginComponent/Model/Token.swift new file mode 100644 index 00000000..440bbc8a --- /dev/null +++ b/Campus-iOS/LoginComponent/Model/Token.swift @@ -0,0 +1,16 @@ +// +// Token.swift +// Campus-iOS +// +// Created by David Lin on 21.01.23. +// + +import Foundation + +struct Token: Decodable { + let value: String + + private enum CodingKeys: String, CodingKey { + case value = "" + } +} diff --git a/Campus-iOS/LoginComponent/Service/AuthenticationHandler.swift b/Campus-iOS/LoginComponent/Service/AuthenticationHandler.swift index addfc92e..f1b22657 100644 --- a/Campus-iOS/LoginComponent/Service/AuthenticationHandler.swift +++ b/Campus-iOS/LoginComponent/Service/AuthenticationHandler.swift @@ -33,15 +33,7 @@ enum LoginError: LocalizedError { } } -/// Handles authentication for TUMOnline, TUMCabe and the MVGAPI -final class AuthenticationHandler: RequestAdapter, RequestRetrier { - typealias Completion = (Result) -> Void - - private let lock = NSLock() - private let sessionManager = Session() - private var isRefreshing = false - private var requestsToRetry: [(RetryResult) -> Void] = [] - +class AuthenticationHandler { private static let keychain = Keychain(service: "de.tum.campusapp") .synchronizable(true) .accessibility(.afterFirstUnlock) @@ -55,151 +47,54 @@ final class AuthenticationHandler: RequestAdapter, RequestRetrier { return Credentials.noTumID } - guard let data = AuthenticationHandler.keychain[data: "credentials"] else { return nil } + guard let data = Self.keychain[data: "credentials"] else { return nil } return try? PropertyListDecoder().decode(Credentials.self, from: data) } set { if let newValue = newValue { let data = try! PropertyListEncoder().encode(newValue) - AuthenticationHandler.keychain[data: "credentials"] = data + Self.keychain[data: "credentials"] = data } else { - AuthenticationHandler.keychain[data: "credentials"] = nil - } - } - } - - // MARK: - RequestAdapter - - func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { - var urlRequest = urlRequest - guard let urlString = urlRequest.url?.absoluteString else { return completion(.success(urlRequest)) } - var pToken: String? - - switch credentials { - case .tumID(_, let token)?, - .tumIDAndKey(_, let token, _)?: - pToken = token - default: - break - } - - switch urlString { - case urlString where TUMOnlineAPI.requiresAuth.contains { urlString.contains($0) }: - guard let pToken = pToken else { return completion(.failure(LoginError.missingToken)) } - do { - let encodedRequest = try URLEncoding.default.encode(urlRequest, with: ["pToken": pToken]) - return completion(.success(encodedRequest)) - } catch let error { - CrashlyticsService.log(error) - return completion(.failure(error)) + Self.keychain[data: "credentials"] = nil } - case urlString where TUMCabeAPI.requiresAuth.contains { urlString.contains($0)}: - return completion(.success(urlRequest)) - case urlString where urlString.hasPrefix(MVGAPI.baseURL): - urlRequest.addValue(MVGAPI.apiKey, forHTTPHeaderField: "X-MVG-Authorization-Key") - return completion(.success(urlRequest)) - default: - return completion(.success(urlRequest)) } } - // MARK: - RequestRetrier - - func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { - lock.lock() ; defer { lock.unlock() } - - requestsToRetry.append(completion) - - guard isRefreshing else { - completion(.doNotRetry) - return - } - - let tumID: String - switch credentials { - case .none, - .noTumID?: - completion(.doNotRetry) - return - case .tumID(let id,_)?, - .tumIDAndKey(let id,_,_)?: - tumID = id - } - - createToken(tumID: tumID) { [weak self] result in - guard let strongSelf = self else { return } - strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() } - - switch result { - case .success(let token): - // Auth succeeded retry failed request. - switch strongSelf.credentials { - case .none: strongSelf.credentials = .tumID(tumID: tumID, token: token) - case .noTumID?: strongSelf.credentials = .tumID(tumID: tumID, token: token) - case .tumID(let tumID, _)?: strongSelf.credentials = .tumID(tumID: tumID, token: token) - case .tumIDAndKey(let tumID, _, let key)?: strongSelf.credentials = .tumIDAndKey(tumID: tumID, token: token, key: key) - } - - default: - // Auth failed don't retry. - break - } - - strongSelf.requestsToRetry.forEach { $0(.retry) } - strongSelf.requestsToRetry.removeAll() + func createToken(tumID: String, completion: @escaping (Result) -> Void) async { + do { + let tokenName = "TCA - \(await UIDevice.current.name)" + + let token: Token = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.tokenRequest(tumID: tumID, tokenName: tokenName), forcedRefresh: true) + print(token.value) + self.credentials = Credentials.tumID(tumID: tumID, token: token.value) + completion(.success(token.value)) + } catch { + print(error) + completion(.failure(LoginError.serverError(message: error.localizedDescription))) } } - - func createToken(tumID: String, completion: @escaping Completion) { - guard !isRefreshing else { return } - isRefreshing = true - let tokenName = "TCA - \(UIDevice.current.name)" - - sessionManager.request(TUMOnlineAPI.tokenRequest(tumID: tumID, tokenName: tokenName)) - .validate(statusCode: 200..<300) - .validate(contentType: ["text/xml"]) - .responseXML { [weak self] xml in - guard let strongSelf = self else { return } - guard let newToken = xml.value?["token"].element?.text else { - strongSelf.isRefreshing = false - if let error = xml.error { - return completion(.failure(error)) - } else if let errorMessage = xml.value?["error"]["message"].element?.text { - return completion(.failure(LoginError.serverError(message: errorMessage))) - } - return completion(.failure(LoginError.unknown)) - } - strongSelf.credentials = Credentials.tumID(tumID: tumID, token: newToken) - strongSelf.isRefreshing = false - #if !targetEnvironment(macCatalyst) - Analytics.logEvent("token_created", parameters: nil) - #endif - completion(.success(newToken)) + + func confirmToken() async -> Result { + guard let credentials else { + return .failure(TUMOnlineAPIError.invalidToken) } - } - - func confirmToken(callback: @escaping (Result) -> Void) { - let sessionManager: Session = Session.defaultSession - + switch credentials { - case .none: callback(.failure(LoginError.missingToken)) - case .noTumID?: callback(.failure(LoginError.missingToken)) - case .tumID?, - .tumIDAndKey?: - sessionManager.request(TUMOnlineAPI.tokenConfirmation) - .validate(statusCode: 200..<300) - .validate(contentType: ["text/xml"]) - .responseXML { xml in - if let error = xml.error { - callback(.failure(error)) - } else if xml.value?["confirmed"].element?.text == "true" { - callback(.success(true)) - } else if xml.value?["confirmed"].element?.text == "false" { - callback(.failure(TUMOnlineAPIError.tokenNotConfirmed)) + case .noTumID: + return .failure(LoginError.missingToken) + case .tumID(tumID: _, token: let token), .tumIDAndKey(tumID: _, token: let token, key: _): + do { + let confirmation: Confirmation = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.tokenConfirmation, token: token, forcedRefresh: true) + + if confirmation.value { + return .success(true) } else { - callback(.failure(LoginError.unknown)) + return .failure(TUMOnlineAPIError.tokenNotConfirmed) } - } + } catch { + print(error.localizedDescription) + return .failure(LoginError.serverError(message: error.localizedDescription)) + } } } @@ -207,7 +102,7 @@ final class AuthenticationHandler: RequestAdapter, RequestRetrier { #if !targetEnvironment(macCatalyst) Analytics.logEvent("logout", parameters: nil) #endif - // deletes uthenticationHandler.keychain[data: "credentials"] + // deletes authenticationHandler.keychain[data: "credentials"] credentials = nil } @@ -217,25 +112,4 @@ final class AuthenticationHandler: RequestAdapter, RequestRetrier { #endif credentials = .noTumID } - -} - - -final class ForceHTTPSRedirectHandler: RedirectHandler { - func task(_ task: URLSessionTask, - willBeRedirectedTo request: URLRequest, - for response: HTTPURLResponse, - completion: @escaping (URLRequest?) -> Void) { - - guard let url = request.url else { return completion(request) } - - if url.scheme == "http" { - let modifiedURL = url.absoluteString.replacingOccurrences(of: "http", with: "https") - var modifiedRequest = request - modifiedRequest.url = URL(string: modifiedURL) - return completion(modifiedRequest) - } - - return completion(request) - } } diff --git a/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift b/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift index cf5b02d1..0c71a3e0 100644 --- a/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift +++ b/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift @@ -11,6 +11,7 @@ import SwiftUI import FirebaseAnalytics #endif +@MainActor class LoginViewModel: ObservableObject { @Published var firstTextField = "" @Published var numbersTextField = "" @@ -18,7 +19,7 @@ class LoginViewModel: ObservableObject { @Published var alertMessage = "" private static let hapticFeedbackGenerator = UINotificationFeedbackGenerator() - weak var model: Model? + var model: Model var loginController = AuthenticationHandler() var isContinueEnabled: Bool { @@ -33,51 +34,52 @@ class LoginViewModel: ObservableObject { return "\(firstTextField)\(numbersTextField)\(secondTextField)" } - init(model: Model?) { + init(model: Model) { self.model = model } - func loginWithContinue(callback: @escaping (Result) -> Void) { + func loginWithContinue(callback: @escaping (Result) -> Void) async { guard let tumID = tumID else { callback(.failure(LoginError.serverError(message: "No TUM ID"))) return } - loginController.createToken(tumID: tumID) { [weak self] result in + + await loginController.createToken(tumID: tumID, completion: { result in switch result { case .success: - self?.alertMessage = "" + DispatchQueue.main.async { + self.alertMessage = "" + } callback(.success(true)) case let .failure(error): - self?.alertMessage = error.localizedDescription + self.alertMessage = error.localizedDescription callback(.failure(error)) } - } + }) } func loginWithContinueWithoutTumID() { loginController.skipLogin() } - func checkAuthorization(callback: @escaping (Result) -> Void) { - loginController.confirmToken() { [weak self] result in - switch result { + func checkAuthorization(callback: @escaping (Result) -> Void) async { + switch await model.loginController.confirmToken() { case .success: #if !targetEnvironment(macCatalyst) Analytics.logEvent("token_confirmed", parameters: nil) #endif - //wself?.model?.isLoginSheetPresented = false - self?.model?.isUserAuthenticated = true - self?.model?.showProfile = false - self?.model?.loadProfile() - - Self.hapticFeedbackGenerator.notificationOccurred(.success) - + + DispatchQueue.main.async { + self.model.isUserAuthenticated = true + self.model.showProfile = false + } + callback(.success(true)) - case let .failure(error): - self?.model?.isUserAuthenticated = false - self?.alertMessage = error.localizedDescription + case .failure(let error): + self.model.isUserAuthenticated = false + self.alertMessage = error.localizedDescription + callback(.failure(error)) - } } } } diff --git a/Campus-iOS/LoginComponent/ViewModel/TokenPermissionsViewModel.swift b/Campus-iOS/LoginComponent/ViewModel/TokenPermissionsViewModel.swift index e5624b8a..3b5ba904 100644 --- a/Campus-iOS/LoginComponent/ViewModel/TokenPermissionsViewModel.swift +++ b/Campus-iOS/LoginComponent/ViewModel/TokenPermissionsViewModel.swift @@ -55,53 +55,44 @@ class TokenPermissionsViewModel: ObservableObject { self.states[.grades] = .failed(error: error) } case .calendar: - let calenderVM = CalendarViewModel(model: self.model) - - calenderVM.fetch { result in - if case let .success(data) = result { - print("Success") - self.states[.calendar] = .success(data: data) - } else { - print("No success") - self.states[.calendar] = .failed(error: CampusOnlineAPI.Error.noPermission) - } + do { + self.states[.calendar] = .success( + data: try await CalendarService().fetch(token: token, forcedRefresh: true)) + } catch { + self.states[.calendar] = .failed(error: error) } - case .lectures: do { self.states[.lectures] = .success( data: try await LecturesService().fetch(token: token, forcedRefresh: true)) } catch { self.states[.lectures] = .failed(error: error) - print(error) } case .tuitionFees: - - let profileVM = ProfileViewModel(model: self.model) - profileVM.fetch() - profileVM.checkTuitionFunc() { result in - print(result) - if case let .success(data) = result { - print("Success") - self.states[.tuitionFees] = .success(data: data) - } else { - print("no success") - self.states[.tuitionFees] = .failed(error: CampusOnlineAPI.Error.noPermission) + do { + guard let tuitionFees: Tuition = try await ProfileService().fetch(token: token, forcedRefresh: true) else { + self.states[.identification] = .failed(error: TUMOnlineAPIError(message: "Tuition couldn't be loaded.")) + break } + + self.states[.tuitionFees] = .success( + data: tuitionFees) + } catch { + self.states[.identification] = .failed(error: error) } case .identification: - let profileVM = ProfileViewModel(model: self.model) - profileVM.fetch() { result in - print(result) - if case let .success(data) = result { - print("Success") - self.states[.identification] = .success(data: data) - } else { - print("no success") - self.states[.identification] = .failed(error: CampusOnlineAPI.Error.noPermission) + do { + guard let profile: Profile = try await ProfileService().fetch(token: token, forcedRefresh: true) else { + self.states[.identification] = .failed(error: TUMOnlineAPIError(message: "Tuition couldn't be loaded.")) + break } + + self.states[.identification] = .success( + data: profile) + } catch { + self.states[.identification] = .failed(error: error) } } } diff --git a/Campus-iOS/LoginComponent/Views/LoginView.swift b/Campus-iOS/LoginComponent/Views/LoginView.swift index f171a86d..a8d87698 100644 --- a/Campus-iOS/LoginComponent/Views/LoginView.swift +++ b/Campus-iOS/LoginComponent/Views/LoginView.swift @@ -106,20 +106,22 @@ struct LoginView: View { if showLoginButton { Button { if logInState != .loggedIn { - self.viewModel.loginWithContinue() { result in - switch result { - case .success: - withAnimation() { - buttonBackgroundColor = .blue - logInState = .loggedIn - } - print("Log in Successfull") - case .failure(_): - withAnimation() { - buttonBackgroundColor = .red - logInState = .logInError + Task { + await self.viewModel.loginWithContinue { result in + switch result { + case .success: + withAnimation() { + buttonBackgroundColor = .blue + logInState = .loggedIn + } + print("Log in Successfull") + case .failure(_): + withAnimation() { + buttonBackgroundColor = .red + logInState = .logInError + } + print("Loggin Error") } - print("Loggin Error") } } } @@ -175,7 +177,7 @@ struct LoginView: View { Button(action: { self.viewModel.loginWithContinueWithoutTumID() - self.viewModel.model?.isLoginSheetPresented = false + self.viewModel.model.isLoginSheetPresented = false }) { Text("Continue without TUM ID").lineLimit(1).font(.caption) .frame(alignment: .center) diff --git a/Campus-iOS/LoginComponent/Views/TokenConfirmationView.swift b/Campus-iOS/LoginComponent/Views/TokenConfirmationView.swift index 23aa8da3..1a9b5518 100644 --- a/Campus-iOS/LoginComponent/Views/TokenConfirmationView.swift +++ b/Campus-iOS/LoginComponent/Views/TokenConfirmationView.swift @@ -131,19 +131,21 @@ struct TokenConfirmationView: View { if !tokenPermissionButton { Button(action: { - self.viewModel.checkAuthorization() { result in - switch result { - case .success: - withAnimation { - tokenState = .active - buttonBackgroundColor = .green - showTokenHelp = false - } - case .failure(_): - withAnimation { - tokenState = .inactive - buttonBackgroundColor = .red - showTokenHelp = true + Task { + await self.viewModel.checkAuthorization() { result in + switch result { + case .success: + withAnimation { + tokenState = .active + buttonBackgroundColor = .green + showTokenHelp = false + } + case .failure(_): + withAnimation { + tokenState = .inactive + buttonBackgroundColor = .red + showTokenHelp = true + } } } } @@ -176,17 +178,15 @@ struct TokenConfirmationView: View { } } case .active: - if let model = self.viewModel.model { - NavigationLink(destination: TokenPermissionsView(viewModel: TokenPermissionsViewModel(model: model)).navigationTitle("Check Permissions"), isActive: $isActive) { - Text("Next") - .lineLimit(1) - .font(.body) - .frame(width: 200, height: 48, alignment: .center) - .foregroundColor(.white) - .background(.green) - .cornerRadius(10) - .buttonStyle(.plain) - } + NavigationLink(destination: TokenPermissionsView(viewModel: TokenPermissionsViewModel(model: self.viewModel.model)).navigationTitle("Check Permissions"), isActive: $isActive) { + Text("Next") + .lineLimit(1) + .font(.body) + .frame(width: 200, height: 48, alignment: .center) + .foregroundColor(.white) + .background(.green) + .cornerRadius(10) + .buttonStyle(.plain) } } } @@ -255,11 +255,7 @@ struct TokenConfirmationView: View { private func switchSteps() async { // Delay of 5 seconds (1 second = 1_000_000_000 nanoseconds) - guard let model = self.viewModel.model else { - return - } - - while (!model.isUserAuthenticated) { + while (!self.viewModel.model.isUserAuthenticated) { switch currentStep { case 1: try? await Task.sleep(nanoseconds: 5_160_000_000) diff --git a/Campus-iOS/LoginComponent/Views/TokenPermissionsView.swift b/Campus-iOS/LoginComponent/Views/TokenPermissionsView.swift index 7fc1f9d7..8f20cbee 100644 --- a/Campus-iOS/LoginComponent/Views/TokenPermissionsView.swift +++ b/Campus-iOS/LoginComponent/Views/TokenPermissionsView.swift @@ -49,7 +49,7 @@ struct TokenPermissionsView: View { .padding() VStack { - HStack (){ + HStack { Button { self.showTUMOnline = true self.doneButton = false @@ -179,7 +179,7 @@ struct TokenPermissionsView: View { case .failed(let error): switch error { - case CampusOnlineAPI.Error.noPermission: + case TUMOnlineAPIError.noPermission: Image(systemName: "x.circle.fill").foregroundColor(.red) case NetworkingError.deviceIsOffline: Image(systemName: "wifi.slash").foregroundColor(.red) diff --git a/Campus-iOS/MapComponent/Service/CafeteriasService.swift b/Campus-iOS/MapComponent/Service/CafeteriasService.swift index 2b9efed3..fcb35451 100644 --- a/Campus-iOS/MapComponent/Service/CafeteriasService.swift +++ b/Campus-iOS/MapComponent/Service/CafeteriasService.swift @@ -6,13 +6,54 @@ // import Foundation +import Alamofire -protocol CafeteriasServiceProtocol { - func fetch(forcedRefresh: Bool) async throws -> [Cafeteria] -} - -struct CafeteriasService: CafeteriasServiceProtocol { +struct CafeteriasService: ServiceProtocol { + typealias T = Cafeteria + func fetch(forcedRefresh: Bool) async throws -> [Cafeteria] { - return try await EatAPI.fetchCafeterias(forcedRefresh: forcedRefresh) + let endpoint = EatAPI.canteens + + var response: [Cafeteria] = try await MainAPI.makeRequest(endpoint: endpoint) + + for i in response.indices { + if let queueStatusApi = response[i].queueStatusApi { + response[i].queue = try await fetch(eatAPI: endpoint, queueStatusApi: queueStatusApi, forcedRefresh: forcedRefresh) + } + } + + return response + } + + func fetch(eatAPI: EatAPI, queueStatusApi: String, forcedRefresh: Bool) async throws -> Queue { + if !forcedRefresh, let data = MainAPI.cache.value(forKey: queueStatusApi), let typedData = data as? Queue { + return typedData + } else { + var data: Data + do { + data = try await AF.request(queueStatusApi).serializingData().value + } catch { + print(error) + throw NetworkingError.deviceIsOffline + } + + if let error = try? eatAPI.decode(EatAPI.error, from: data) { + print(error) + throw error + } + + do { + // Decode data from the respective endpoint. + let decodedData = try eatAPI.decode(Queue.self, from: data) + // Write value to cache + MainAPI.cache.setValue(decodedData, forKey: queueStatusApi, cost: data.count) + + return decodedData + + } catch { + print(error) + throw EatAPI.error.init(message: error.localizedDescription) + } + } } } diff --git a/Campus-iOS/MapComponent/Service/DishService.swift b/Campus-iOS/MapComponent/Service/DishService.swift new file mode 100644 index 00000000..00f9bbc9 --- /dev/null +++ b/Campus-iOS/MapComponent/Service/DishService.swift @@ -0,0 +1,26 @@ +// +// DishService.swift +// Campus-iOS +// +// Created by David Lin on 23.01.23. +// + +import Foundation + +struct DishService { + func fetch(forcedRefresh: Bool) async -> [String: DishLabel]? { + do { + let response: [DishLabel] = try await MainAPI.makeRequest(endpoint: EatAPI.labels, forcedRefresh: forcedRefresh) + var labels = [String: DishLabel]() + for dishLabel in response { + labels[dishLabel.name] = dishLabel + } + + return labels + } catch { + print(error) + return nil + // No error is thrown, since the labels, can still be displayed, but just as text, instead of an emoji. + } + } +} diff --git a/Campus-iOS/MapComponent/Service/MealPlanService.swift b/Campus-iOS/MapComponent/Service/MealPlanService.swift new file mode 100644 index 00000000..0d99ace0 --- /dev/null +++ b/Campus-iOS/MapComponent/Service/MealPlanService.swift @@ -0,0 +1,75 @@ +// +// MealPlanService.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import Foundation + +struct MealPlanService { + func fetch(cafeteria: Cafeteria, forcedRefresh: Bool) async throws -> [Menu] { + let thisWeekAPI = EatAPI.menu(location: cafeteria.id, year: Date().year, week: Date().weekOfYear) + + let thisWeekMealPlanResponse = try await fetch(menu: thisWeekAPI, forcedRefresh: forcedRefresh) + + let thisWeekMenu: [Menu] = getMenuPerDay(mealPlan: thisWeekMealPlanResponse) + + guard let nextWeek = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: Date()) else { + return thisWeekMenu + } + let nextWeekAPI = EatAPI.menu(location: cafeteria.id, year: nextWeek.year, week: nextWeek.weekOfYear) + + var menus = thisWeekMenu + + do { + let nextWeekMealPlanResponse = try await fetch(menu: nextWeekAPI, forcedRefresh: forcedRefresh) + + let nextWeekMenu = getMenuPerDay(mealPlan: nextWeekMealPlanResponse) + + menus = menus + nextWeekMenu.filter{ menu in !thisWeekMenu.contains(where: {$0.date == menu.date}) } // don't re-add already existent days + } catch { + //Throw no error, since sometimes the next weeks menu isn't ready yet, thus a 404 error is thrown, but at the end of the week the next week's menu is ready. + print(error) + } + + return menus + } + + func fetch(menu: EatAPI, forcedRefresh: Bool) async throws -> MealPlan { + let response: MealPlan = try await MainAPI.makeRequest(endpoint: menu, forcedRefresh: forcedRefresh) + + return response + } + + + + func getMenuPerDay(mealPlan: MealPlan) -> [Menu] { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + var menus = [Menu]() + menus = mealPlan.days + .filter { !$0.dishes.isEmpty && ($0.date.isToday || $0.date.isLaterThanOrEqual(to: Date())) } + .sorted { $0.date < $1.date } + .map { + let categories = categories(from: $0.dishes) + return Menu(date: $0.date, categories: categories) + } + + return menus.sorted { $0.date < $1.date } + } + + func categories(from dishes: [Dish]) -> [MenuCategory] { + return dishes + .sorted { $0.dishType < $1.dishType } + .reduce(into: [:]) { (acc: inout [String: [Dish]], dish: Dish) -> () in + let type = dish.dishType.isEmpty ? "Sonstige" : dish.dishType + if acc[type] != nil { + acc[type]?.append(dish) + } + acc[type] = [dish] + } + .map { MenuCategory(name: $0.key, dishes: $0.value) } + } +} diff --git a/Campus-iOS/MapComponent/Service/MensaEnumService.swift b/Campus-iOS/MapComponent/Service/MensaEnumService.swift deleted file mode 100644 index 26fe5ab3..00000000 --- a/Campus-iOS/MapComponent/Service/MensaEnumService.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MensaEnumService.swift -// Campus-iOS -// -// Created by August Wittgenstein on 14.01.22. -// - -import Foundation -import Alamofire - -final class MensaEnumService { - static let shared = MensaEnumService() - private var labels: [String : DishLabel]? - - public func getLabels() -> [String : DishLabel] { - if self.labels == nil { - self.labels = [:] - AF.request(EatAPI.labels).responseDecodable(of: [DishLabel].self) { (response) in - let labels = response.value ?? [] - for label in labels{ - self.labels?[label.name] = label - } - } - } - - return self.labels! - } -} diff --git a/Campus-iOS/MapComponent/Service/StudyRoomsService.swift b/Campus-iOS/MapComponent/Service/StudyRoomsService.swift index 18f9a5b5..48115893 100644 --- a/Campus-iOS/MapComponent/Service/StudyRoomsService.swift +++ b/Campus-iOS/MapComponent/Service/StudyRoomsService.swift @@ -7,12 +7,16 @@ import Foundation -protocol StudyRoomsServiceProtocol { - func fetch(forcedRefresh: Bool) async throws -> StudyRoomApiRespose -} - -struct StudyRoomsService: StudyRoomsServiceProtocol { +struct StudyRoomsService { func fetch(forcedRefresh: Bool) async throws -> StudyRoomApiRespose { - return try await TUMDevAppAPI.fetchStudyRooms(forcedRefresh: forcedRefresh) + let response: StudyRoomApiRespose = try await MainAPI.makeRequest(endpoint: TUMDevAppAPI.rooms, forcedRefresh: forcedRefresh) + + return response + } + + func fetchMap(room: String, forcedRefresh: Bool) async throws -> [RoomImageMapping] { + let response: [RoomImageMapping] = try await MainAPI.makeRequest(endpoint: TUMCabeAPI.roomMaps(room: room)) + + return response } } diff --git a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoom.swift b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoom.swift index fb701e98..f4cf5533 100644 --- a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoom.swift +++ b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoom.swift @@ -8,7 +8,7 @@ import Foundation import SwiftUI -struct StudyRoom: Entity { +struct StudyRoom: Decodable { var buildingCode: String? var buildingName: String? var buildingNumber: Int64 diff --git a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomApiResponse.swift b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomApiResponse.swift index 93e21d93..2377c0e0 100644 --- a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomApiResponse.swift +++ b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomApiResponse.swift @@ -7,7 +7,7 @@ import Foundation -struct StudyRoomApiRespose: Entity, Equatable { +struct StudyRoomApiRespose: Decodable, Equatable { static func == (lhs: StudyRoomApiRespose, rhs: StudyRoomApiRespose) -> Bool { lhs.groups?.map({$0.id}) == rhs.groups?.map({$0.id}) && lhs.rooms?.map({$0.id}) == rhs.rooms?.map({$0.id}) diff --git a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomAttribute.swift b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomAttribute.swift index 147c675d..24f8d1e7 100644 --- a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomAttribute.swift +++ b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomAttribute.swift @@ -7,7 +7,7 @@ import Foundation -struct StudyRoomAttribute: Entity { +struct StudyRoomAttribute: Decodable { var detail: String? var name: String? diff --git a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomGroup.swift b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomGroup.swift index e399c433..2f58ba3e 100644 --- a/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomGroup.swift +++ b/Campus-iOS/MapComponent/Types/StudyRooms/StudyRoomGroup.swift @@ -8,7 +8,7 @@ import Foundation import MapKit -struct StudyRoomGroup: Entity, Equatable { +struct StudyRoomGroup: Decodable, Equatable { var detail: String? var id: Int64 var name: String? diff --git a/Campus-iOS/MapComponent/View/Cafeterias/CafeteriaWidgetView.swift b/Campus-iOS/MapComponent/View/Cafeterias/CafeteriaWidgetView.swift index 539c00f0..eed863f5 100644 --- a/Campus-iOS/MapComponent/View/Cafeterias/CafeteriaWidgetView.swift +++ b/Campus-iOS/MapComponent/View/Cafeterias/CafeteriaWidgetView.swift @@ -33,13 +33,12 @@ struct CafeteriaWidgetView: View { WidgetLoadingView(text: "Searching nearby cafeteria") default: if let cafeteria = viewModel.cafeteria, - let title = cafeteria.title, - let coordinate = cafeteria.coordinate { + let title = cafeteria.title { CafeteriaWidgetContent( size: size, cafeteria: title, - dishes: viewModel.menuViewModel?.getDishes() ?? [], - coordinate: coordinate + dishes: viewModel.menu?.getDishes() ?? [], + coordinate: cafeteria.coordinate ) } else { TextWidgetView(text: "There was an error getting the menu from the nearest cafeteria.") @@ -62,13 +61,13 @@ struct CafeteriaWidgetView: View { } .sheet(isPresented: $showDetails) { VStack { - if let cafeteria = viewModel.cafeteria, let mealVm = viewModel.mealPlanViewModel { + if let cafeteria = viewModel.cafeteria { CafeteriaView( vm: MapViewModel(cafeteriaService: CafeteriasService(), studyRoomsService: StudyRoomsService()), selectedCanteen: .constant(cafeteria), canDismiss: false ) - MealPlanView(viewModel: mealVm) + MealPlanScreen(cafeteria: cafeteria) } else { ProgressView() } @@ -164,13 +163,16 @@ struct CompactMenuView: View { struct CompactDishView: View { - var dish: Dish + @StateObject var vm: DishViewModel + init(dish: Dish) { + self._vm = StateObject(wrappedValue: DishViewModel(dish: dish)) + } var body: some View { VStack(alignment: .leading) { - Text(dish.name) + Text(vm.dish.name) .lineLimit(1) - Text(DishView.formatPrice(dish: dish, pricingGroup: "students")) + Text(vm.formatPrice(dish: vm.dish, pricingGroup: "students")) .font(.caption) .bold() } diff --git a/Campus-iOS/MapComponent/View/Cafeterias/DishView.swift b/Campus-iOS/MapComponent/View/Cafeterias/DishView.swift new file mode 100644 index 00000000..5196cb75 --- /dev/null +++ b/Campus-iOS/MapComponent/View/Cafeterias/DishView.swift @@ -0,0 +1,60 @@ +// +// DishView.swift +// Campus-iOS +// +// Created by David Lin on 23.01.23. +// + +import SwiftUI + +struct DishView: View { + @StateObject var vm: DishViewModel + @State private var isExpanded = false + + init(dish: Dish) { + self._vm = StateObject(wrappedValue: DishViewModel(dish: dish)) + } + + var body: some View { + Group { + switch vm.state { + case .success(data: let generalLabels): + DisclosureGroup(isExpanded: $isExpanded) { + HStack{ + VStack{ + ForEach(vm.dish.labels, id: \.self) { label in + Text(vm.labelToAbbreviation(generalLabel: generalLabels, label: label)) + } + } + .padding(.trailing, 10.0) + VStack(alignment: .leading){ + ForEach(vm.dish.labels, id: \.self) { label in + Text(vm.labelToDescription(generalLabel: generalLabels, label: label)) + } + } + } + } label: { + VStack(alignment: .leading, spacing: 10){ + Spacer().frame(height: 0) + Text(vm.dish.name).bold() + HStack{ + Spacer() + Text(vm.formatPrice(dish: vm.dish, pricingGroup: "students")) + .lineLimit(1) + .font(.system(size: 15)) + } + Spacer().frame(height: 0) + } + } + .buttonStyle(PlainButtonStyle()).disabled(true) + case .loading, .na: + LoadingView(text: "Fetching Dish Labels") + case .failed(error: let error): + FailedView(errorDescription: error.localizedDescription, retryClosure: vm.getDishLabels + ) + } + }.task { + await vm.getDishLabels() + } + } +} diff --git a/Campus-iOS/MapComponent/View/Cafeterias/MealPlanScreen.swift b/Campus-iOS/MapComponent/View/Cafeterias/MealPlanScreen.swift new file mode 100644 index 00000000..028333cd --- /dev/null +++ b/Campus-iOS/MapComponent/View/Cafeterias/MealPlanScreen.swift @@ -0,0 +1,42 @@ +// +// MealPlanScreen.swift +// Campus-iOS +// +// Created by David Lin on 10.02.23. +// + +import SwiftUI + +struct MealPlanScreen: View { + @StateObject var vm: MealPlanViewModel + + init(cafeteria: Cafeteria) { + self._vm = StateObject(wrappedValue: MealPlanViewModel(cafeteria: cafeteria)) + } + + var body: some View { + Group { + switch vm.state { + case .success(let menus): + if let firstMenu = menus.first { + VStack { + MealPlanView(menus: menus, cafeteria: vm.cafeteria, selectedMenu: firstMenu) .refreshable { + await vm.getMenus() + } + } + } + case .loading, .na: + LoadingView(text: "Fetching Menus") + case .failed(_): + VStack { + Spacer() + // Since some cafeterias do not update their menus this is how we handle error here. There could be a better differentiation. + Text("No Menu available") + Spacer() + } + } + }.task { + await vm.getMenus() + } + } +} diff --git a/Campus-iOS/MapComponent/View/Cafeterias/MealPlanView.swift b/Campus-iOS/MapComponent/View/Cafeterias/MealPlanView.swift index 3d64070c..b7b3a6a9 100644 --- a/Campus-iOS/MapComponent/View/Cafeterias/MealPlanView.swift +++ b/Campus-iOS/MapComponent/View/Cafeterias/MealPlanView.swift @@ -10,27 +10,29 @@ import Alamofire struct MealPlanView: View { @Environment(\.colorScheme) var colorScheme - @ObservedObject var viewModel: MealPlanViewModel + let menus: [Menu] + let cafeteria: Cafeteria + @State var selectedMenu: Menu var body: some View { VStack { HStack{ - if viewModel.menus.count > 0 { + if self.menus.count > 0 { VStack{ HStack{ - ForEach(viewModel.menus.prefix(7), id: \.id){ menu in + ForEach(self.menus.prefix(7), id: \.id){ menu in Button(action: { - viewModel.selectedMenu = menu + self.selectedMenu = menu }){ VStack{ Circle() - .fill(menu === viewModel.selectedMenu ? + .fill(menu === self.selectedMenu ? (Calendar.current.isDateInToday(menu.date) ? Color("tumBlue") : Color(UIColor.label)) : Color.clear) .aspectRatio(contentMode: .fit) .overlay( Text(getFormattedDate(date: menu.date, format: "d")) .fontWeight(.semibold).fixedSize() - .foregroundColor(menu === viewModel.selectedMenu ? Color(UIColor.systemBackground) : (Calendar.current.isDateInToday(menu.date) ? Color("tumBlue") : Color(UIColor.label))) + .foregroundColor(menu === self.selectedMenu ? Color(UIColor.systemBackground) : (Calendar.current.isDateInToday(menu.date) ? Color("tumBlue") : Color(UIColor.label))) ) .frame(maxWidth: UIScreen.main.bounds.width, minHeight: 35, maxHeight: 35) @@ -42,18 +44,22 @@ struct MealPlanView: View { } .padding(.horizontal, 5.0) - if let menu = viewModel.selectedMenu { - MenuView(viewModel: menu) + MenuView(menu: selectedMenu) + + /* + if let menu = selectedMenu { + } else { Spacer().frame(height: 20) Text("No Menu available today").foregroundColor(colorScheme == .dark ? .init(UIColor.lightGray) : .init(UIColor.darkGray)) } + */ } } else { Text("No Menus available") } } - .navigationTitle(viewModel.title) + .navigationTitle(cafeteria.title ?? "No Cafeteria Title") Spacer(minLength: 0.0) } } @@ -65,11 +71,11 @@ struct MealPlanView: View { } } -struct MealPlanView_Previews: PreviewProvider { - static var previews: some View { - MealPlanView(viewModel: MealPlanViewModel(cafeteria: Cafeteria(location: Location(latitude: 0.0, longitude: 0.0, address: ""), - name: "", - id: "", - queueStatusApi: ""))) - } -} +//struct MealPlanView_Previews: PreviewProvider { +// static var previews: some View { +// MealPlanView(viewModel: MealPlanViewModel(cafeteria: Cafeteria(location: Location(latitude: 0.0, longitude: 0.0, address: ""), +// name: "", +// id: "", +// queueStatusApi: ""))) +// } +//} diff --git a/Campus-iOS/MapComponent/View/Cafeterias/MenuView.swift b/Campus-iOS/MapComponent/View/Cafeterias/MenuView.swift index c4fc6944..14de8d69 100644 --- a/Campus-iOS/MapComponent/View/Cafeterias/MenuView.swift +++ b/Campus-iOS/MapComponent/View/Cafeterias/MenuView.swift @@ -8,11 +8,11 @@ import SwiftUI struct MenuView: View { - @ObservedObject var viewModel: MenuViewModel + let menu: Menu var body: some View { List { - ForEach($viewModel.categories.sorted { $0.wrappedValue.name < $1.wrappedValue.name }) { $category in + ForEach(menu.categories.sorted { $0.name < $1.name }) { category in Section(category.name) { ForEach(category.dishes, id: \.self) { dish in DishView(dish: dish) @@ -23,106 +23,8 @@ struct MenuView: View { } } -struct DishView: View { - @State var dish: Dish - @State private var isExpanded = false - - var body: some View { - DisclosureGroup(isExpanded: $isExpanded) { - HStack{ - VStack{ - ForEach(dish.labels, id: \.self){label in - Text(DishView.labelToAbbreviation(label: label)) - } - } - .padding(.trailing, 10.0) - VStack(alignment: .leading){ - ForEach(dish.labels, id: \.self){label in - Text(DishView.labelToDescription(label: label)) - } - } - } - } label: { - VStack(alignment: .leading, spacing: 10){ - Spacer().frame(height: 0) - Text(dish.name).bold() - HStack{ - Spacer() - Text(DishView.formatPrice(dish: dish, pricingGroup: "students")) - .lineLimit(1) - .font(.system(size: 15)) - } - Spacer().frame(height: 0) - } - } - .buttonStyle(PlainButtonStyle()).disabled(true) - } - - static func formatPrice(dish: Dish, pricingGroup: String) -> String { - let priceFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.currencySymbol = "€" - formatter.numberStyle = .currency - return formatter - }() - - let price: Price - - var basePriceString: String? - var unitPriceString: String? - - switch pricingGroup { - case "staff": - price = dish.prices["staff"]! - case "guests": - price = dish.prices["guests"]! - default: - price = dish.prices["students"]! - } - - if let basePrice = price.basePrice, basePrice != 0 { - basePriceString = priceFormatter.string(for: basePrice) - } - - if let unitPrice = price.unitPrice, let unit = price.unit, unitPrice != 0 { - unitPriceString = priceFormatter.string(for: unitPrice)?.appending(" / " + unit) - } - - let divider: String = !(basePriceString?.isEmpty ?? true) && !(unitPriceString?.isEmpty ?? true) ? " + " : "" - - let finalPrice: String = (basePriceString ?? "") + divider + (unitPriceString ?? "") - - return finalPrice - } - - static func labelToAbbreviation(label: String) -> String { - let labelLookup = MensaEnumService.shared.getLabels() - - if let labelObject = labelLookup[label] { - return labelObject.abbreviation - } - return label - } - - static func labelToDescription(label: String) -> String { - let labelLookup = MensaEnumService.shared.getLabels() - - if let labelObject = labelLookup[label], let text = labelObject.text["DE"] { - return text - } - return label - } - - static func labelsToString(labels: [String]) -> String { - let shortenedLabels = labels.map{label -> String in - return labelToAbbreviation(label: label) - } - return shortenedLabels.joined(separator:", ") - } -} - -struct MenuView_Previews: PreviewProvider { - static var previews: some View { - MenuView(viewModel: MenuViewModel(date: Date(), categories: [])) - } -} +//struct MenuView_Previews: PreviewProvider { +// static var previews: some View { +// MenuView(viewModel: MenuViewModel(date: Date(), categories: [])) +// } +//} diff --git a/Campus-iOS/MapComponent/View/MapView.swift b/Campus-iOS/MapComponent/View/MapView.swift deleted file mode 100644 index b36b3ee1..00000000 --- a/Campus-iOS/MapComponent/View/MapView.swift +++ /dev/null @@ -1,28 +0,0 @@ -//// -//// MapView.swift -//// Campus-iOS -//// -//// Created by August Wittgenstein on 16.12.21. -//// -// -//import SwiftUI -//import MapKit -// -//struct MapView: View { -// @StateObject var vm: MapViewModel -// -// var body: some View { -// ZStack { -//// MapContentView(vm: self.vm) -// //PanelView(vm: self.vm) -// PanelContentView(vm: self.vm) -// } -// -// } -//} -// -//struct MapView_Previews: PreviewProvider { -// static var previews: some View { -// MapView(vm: MapViewModel(cafeteriaService: CafeteriasService(), studyRoomsService: StudyRoomsService())) -// } -//} diff --git a/Campus-iOS/MapComponent/View/PanelContentListView.swift b/Campus-iOS/MapComponent/View/PanelContentListView.swift index 8041ea26..655e856d 100644 --- a/Campus-iOS/MapComponent/View/PanelContentListView.swift +++ b/Campus-iOS/MapComponent/View/PanelContentListView.swift @@ -126,7 +126,11 @@ struct PanelContentListView: View { Button("Cancel", role: .cancel) {} } message: { detail in if case let .failed(error) = detail { - Text(error.localizedDescription) + if let apiError = error as? TUMDevAppAPIError { + Text(apiError.errorDescription ?? "TUMDevAppAPI Error") + } else { + Text(error.localizedDescription) + } } } } diff --git a/Campus-iOS/MapComponent/View/PanelContentView.swift b/Campus-iOS/MapComponent/View/PanelContentView.swift index a6394838..b2f05fc5 100644 --- a/Campus-iOS/MapComponent/View/PanelContentView.swift +++ b/Campus-iOS/MapComponent/View/PanelContentView.swift @@ -12,7 +12,7 @@ struct PanelContentView: View { @StateObject var vm: MapViewModel @State private var searchString = "" - @State private var mealPlanViewModel: MealPlanViewModel? +// @State private var mealPlanViewModel: MealPlanViewModel? @State private var sortedGroups: [StudyRoomGroup] = [] @State private var cafeteriasData = AppUsageData() @State private var studyRoomsData = AppUsageData() @@ -37,13 +37,12 @@ struct PanelContentView: View { .frame(width: 40, height: CGFloat(5.0)) .foregroundColor(Color.primary.opacity(0.2)) - if vm.selectedCafeteria != nil { + if let cafeteria = vm.selectedCafeteria { CafeteriaView(vm: vm, selectedCanteen: $vm.selectedCafeteria, panelHeight: $panelHeight) - if let viewModel = mealPlanViewModel { - MealPlanView(viewModel: viewModel) - } + + MealPlanScreen(cafeteria: cafeteria) } else if vm.selectedStudyGroup != nil { StudyRoomGroupView( @@ -140,11 +139,11 @@ struct PanelContentView: View { } } } - .onChange(of: vm.selectedCafeteria) { optionalCafeteria in - if let cafeteria = optionalCafeteria { - mealPlanViewModel = MealPlanViewModel(cafeteria: cafeteria) - } - } +// .onChange(of: vm.selectedCafeteria) { optionalCafeteria in +// if let cafeteria = optionalCafeteria { +// mealPlanViewModel = MealPlanViewModel(cafeteria: cafeteria) +// } +// } .task(id: vm.panelPos) { if panelHeight != vm.panelPos.rawValue { withAnimation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0)) { diff --git a/Campus-iOS/MapComponent/View/StudyRooms/MapImagesHorizontalScrollingView.swift b/Campus-iOS/MapComponent/View/StudyRooms/MapImagesHorizontalScrollingView.swift index 3bcbf681..a388ab28 100644 --- a/Campus-iOS/MapComponent/View/StudyRooms/MapImagesHorizontalScrollingView.swift +++ b/Campus-iOS/MapComponent/View/StudyRooms/MapImagesHorizontalScrollingView.swift @@ -9,15 +9,16 @@ import SwiftUI struct MapImagesHorizontalScrollingView: View { - @ObservedObject var viewModel: StudyRoomViewModel + let room: StudyRoom + let roomImageMapping: [RoomImageMapping] var body: some View { - if viewModel.roomImageMapping.count > 0 { + if self.roomImageMapping.count > 0 { ScrollView(.horizontal, showsIndicators: true) { HStack(spacing: 10) { - ForEach(viewModel.roomImageMapping, id: \.id) { map in + ForEach(self.roomImageMapping, id: \.id) { map in GeometryReader { geometry in - if let link = viewModel.getImageURL(imageMappingId: map.id) { + if let link = getImageURL(for: self.room, imageMappingId: map.id) { AsyncImage(url: link) { image in switch image { case .empty: @@ -59,10 +60,18 @@ struct MapImagesHorizontalScrollingView: View { } } } -} - -struct MapImagesHorizontalScrollingView_Previews: PreviewProvider { - static var previews: some View { - MapImagesHorizontalScrollingView(viewModel: StudyRoomViewModel(studyRoom: StudyRoom())) + + func getImageURL(for room: StudyRoom, imageMappingId: Int) -> URL? { + if let raumNr = room.raum_nr_architekt { + return try? TUMCabeAPI.mapImage(room: raumNr, id: imageMappingId).asURLRequest().urlRequest?.url + } else { + return nil + } } } + +//struct MapImagesHorizontalScrollingView_Previews: PreviewProvider { +// static var previews: some View { +// MapImagesHorizontalScrollingView(viewModel: StudyRoomViewModel(studyRoom: StudyRoom())) +// } +//} diff --git a/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsScreen.swift b/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsScreen.swift new file mode 100644 index 00000000..aaa980ee --- /dev/null +++ b/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsScreen.swift @@ -0,0 +1,85 @@ +// +// StudyRoomDetailsScreen.swift +// Campus-iOS +// +// Created by David Lin on 22.01.23. +// + +import SwiftUI + +struct StudyRoomDetailsScreen: View { + @StateObject var vm = StudyRoomViewModel() + let room: StudyRoom + + var body: some View { + Group { + switch vm.state { + case .success(let roomImageMapping): + VStack { + StudyRoomDetailsView(studyRoom: self.room, roomImageMapping: roomImageMapping) .refreshable { + await vm.getRoomImageMapping(for: room, forcedRefresh: true) + } + } + case .loading, .na: + VStack { + Spacer() + LoadingView(text: "Fetching RoomImages") + Spacer() + }.task { + await vm.getRoomImageMapping(for: room) + } + case .failed(let error): + VStack { + Text("Error: \(error.localizedDescription)") + Button(action: { + Task { + await self.vm.getRoomImageMapping(for: room, forcedRefresh: true) + } + }) { + Text("Try Again".uppercased()) + .lineLimit(1).font(.body) + .frame(width: 200, height: 48, alignment: .center) + } + .font(.title) + .foregroundColor(.white) + .background(Color(.tumBlue)) + .cornerRadius(10) + .padding() + } + } + }.alert( + "Error while fetching Room Images", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getRoomImageMapping(for: self.room, forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMCabeAPIError { + Text(apiError.errorDescription ?? "TUMCabeAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } + + func printCell(key: String, value: String?) -> some View { + if let val = value { + return AnyView(HStack { + Text(key) + .foregroundColor(Color(UIColor.darkGray)) + Spacer() + Text(val).foregroundColor(.gray) + }) + } + + return AnyView(EmptyView()) + } +} + diff --git a/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsView.swift b/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsView.swift index 49695412..a62c5eec 100644 --- a/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsView.swift +++ b/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomDetailsView.swift @@ -8,13 +8,14 @@ import SwiftUI struct StudyRoomDetailsView: View { - - @ObservedObject var viewModel: StudyRoomViewModel - @State var showPopup = false - init(studyRoom room: StudyRoom) { - self.viewModel = StudyRoomViewModel(studyRoom: room) + let room: StudyRoom + let roomImageMapping: [RoomImageMapping] + + init(studyRoom room: StudyRoom, roomImageMapping: [RoomImageMapping]) { + self.room = room + self.roomImageMapping = roomImageMapping } func printCell(key: String, value: String?) -> some View { @@ -33,24 +34,24 @@ struct StudyRoomDetailsView: View { var body: some View { VStack(alignment: .leading) { Spacer() - if viewModel.roomImageMapping.count > 0 { + if self.roomImageMapping.count > 0 { HStack { Image(systemName: "map.fill").foregroundColor(.blue) Text("Available Maps") .fontWeight(.bold) .font(.headline) } - MapImagesHorizontalScrollingView(viewModel: viewModel) + MapImagesHorizontalScrollingView(room: self.room, roomImageMapping: self.roomImageMapping) Spacer(minLength: 10) } - printCell(key: "Building:", value: viewModel.room.buildingName) - printCell(key: "Building Number:", value: String(viewModel.room.buildingNumber)) - printCell(key: "Building Code:", value: viewModel.room.buildingCode) - if let id = viewModel.room.raum_nr_architekt { + printCell(key: "Building:", value: self.room.buildingName) + printCell(key: "Building Number:", value: String(self.room.buildingNumber)) + printCell(key: "Building Code:", value: self.room.buildingCode) + if let id = self.room.raum_nr_architekt { printCell(key: "ID:", value: id) } - if let attributes = viewModel.room.attributes, attributes.count > 0 { + if let attributes = self.room.attributes, attributes.count > 0 { Text("Attributes:") .foregroundColor(Color(UIColor.darkGray)) ForEach(attributes, id: \.name) { attribute in @@ -67,8 +68,68 @@ struct StudyRoomDetailsView: View { } } -struct StudyRoomDetailsView_Previews: PreviewProvider { - static var previews: some View { - StudyRoomDetailsView(studyRoom: StudyRoom()) - } -} +//struct StudyRoomDetailsView: View { +// +// @ObservedObject var viewModel: StudyRoomViewModel +// +// @State var showPopup = false +// +// init(studyRoom room: StudyRoom) { +// self.viewModel = StudyRoomViewModel(studyRoom: room) +// } +// +// func printCell(key: String, value: String?) -> some View { +// if let val = value { +// return AnyView(HStack { +// Text(key) +// .foregroundColor(Color(UIColor.darkGray)) +// Spacer() +// Text(val).foregroundColor(.gray) +// }) +// } +// +// return AnyView(EmptyView()) +// } +// +// var body: some View { +// VStack(alignment: .leading) { +// Spacer() +// if viewModel.roomImageMapping.count > 0 { +// HStack { +// Image(systemName: "map.fill").foregroundColor(.blue) +// Text("Available Maps") +// .fontWeight(.bold) +// .font(.headline) +// } +//// MapImagesHorizontalScrollingView(viewModel: viewModel) +// Spacer(minLength: 10) +// } +// +// printCell(key: "Building:", value: viewModel.room.buildingName) +// printCell(key: "Building Number:", value: String(viewModel.room.buildingNumber)) +// printCell(key: "Building Code:", value: viewModel.room.buildingCode) +// if let id = viewModel.room.raum_nr_architekt { +// printCell(key: "ID:", value: id) +// } +// if let attributes = viewModel.room.attributes, attributes.count > 0 { +// Text("Attributes:") +// .foregroundColor(Color(UIColor.darkGray)) +// ForEach(attributes, id: \.name) { attribute in +// HStack { +// Spacer() +// Text("\(attribute.name ?? "") \(attribute.detail ?? "")") +// .foregroundColor(.gray) +// Spacer() +// } +// } +// } +// Spacer() +// }.padding([.trailing], 15) +// } +//} +// +//struct StudyRoomDetailsView_Previews: PreviewProvider { +// static var previews: some View { +// StudyRoomDetailsView(studyRoom: StudyRoom()) +// } +//} diff --git a/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomGroupView.swift b/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomGroupView.swift index 9b179512..64e56c99 100644 --- a/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomGroupView.swift +++ b/Campus-iOS/MapComponent/View/StudyRooms/StudyRoomGroupView.swift @@ -115,7 +115,7 @@ struct StudyRoomGroupView: View { List { ForEach(self.sortedRooms, id: \.id) { room in DisclosureGroup(content: { - StudyRoomDetailsView(studyRoom: room) + StudyRoomDetailsScreen(room: room) }, label: { AnyView( HStack { diff --git a/Campus-iOS/MapComponent/ViewModel/CafeteriaWidgetViewModel.swift b/Campus-iOS/MapComponent/ViewModel/CafeteriaWidgetViewModel.swift index 8e9bf1cf..40f01300 100644 --- a/Campus-iOS/MapComponent/ViewModel/CafeteriaWidgetViewModel.swift +++ b/Campus-iOS/MapComponent/ViewModel/CafeteriaWidgetViewModel.swift @@ -13,16 +13,14 @@ import Alamofire class CafeteriaWidgetViewModel: ObservableObject { @Published var cafeteria: Cafeteria? - @Published var menuViewModel: MenuViewModel? - @Published var mealPlanViewModel: MealPlanViewModel? + @Published var menu: Menu? @Published var status: CafeteriaWidgetStatus - private let cafeteriaService: CafeteriasServiceProtocol - private let sessionManager = Session.defaultSession + private let cafeteriaService: CafeteriasService private let locationManager = CLLocationManager() - init(cafeteriaService: CafeteriasServiceProtocol) { + init(cafeteriaService: CafeteriasService) { self.status = .loading self.cafeteriaService = cafeteriaService @@ -54,24 +52,10 @@ class CafeteriaWidgetViewModel: ObservableObject { // Get today's menu plan of the closest cafeteria, if it exists. - let endpoint = EatAPI.menu(location: cafeteria.id, year: Date().year, week: Date().weekOfYear) - sessionManager.request(endpoint).responseDecodable(of: MealPlan.self, decoder: MealPlanViewModel.decoder()) { [self] response in - - guard let mealPlan = response.value else { - self.status = .noMenu - return - } - - guard let todaysPlan = mealPlan.days.first(where: { $0.date.isToday }) else { - self.status = .noMenu - return - } - - let categories = MealPlanViewModel.categories(from: todaysPlan.dishes) - self.menuViewModel = MenuViewModel(date: todaysPlan.date, categories: categories) - self.mealPlanViewModel = MealPlanViewModel(cafeteria: cafeteria) - self.status = .success - } + let menus = try await MealPlanService().fetch(cafeteria: cafeteria, forcedRefresh: false) + self.menu = menus.first + self.status = .success + } catch { self.status = .error } diff --git a/Campus-iOS/MapComponent/ViewModel/DishViewModel.swift b/Campus-iOS/MapComponent/ViewModel/DishViewModel.swift new file mode 100644 index 00000000..a4cc6691 --- /dev/null +++ b/Campus-iOS/MapComponent/ViewModel/DishViewModel.swift @@ -0,0 +1,93 @@ +// +// DishViewModel.swift +// Campus-iOS +// +// Created by David Lin on 23.01.23. +// + +import Foundation + +@MainActor +class DishViewModel: ObservableObject { + @Published var state: APIState<[String: DishLabel]> = .na + + let service = DishService() + let dish: Dish + + init(dish: Dish) { + self.dish = dish + } + + func getDishLabels(forcedRefresh: Bool = false) async { + if !forcedRefresh { + self.state = .loading + } + + let dishLabels = await service.fetch(forcedRefresh: forcedRefresh) + if let generalLabels = dishLabels { + self.state = .success( + data: generalLabels + ) + } else { + self.state = .success(data: [:]) + } + } + + func formatPrice(dish: Dish, pricingGroup: String) -> String { + let priceFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.currencySymbol = "€" + formatter.numberStyle = .currency + return formatter + }() + + let price: Price + + var basePriceString: String? + var unitPriceString: String? + + switch pricingGroup { + case "staff": + price = dish.prices["staff"]! + case "guests": + price = dish.prices["guests"]! + default: + price = dish.prices["students"]! + } + + if let basePrice = price.basePrice, basePrice != 0 { + basePriceString = priceFormatter.string(for: basePrice) + } + + if let unitPrice = price.unitPrice, let unit = price.unit, unitPrice != 0 { + unitPriceString = priceFormatter.string(for: unitPrice)?.appending(" / " + unit) + } + + let divider: String = !(basePriceString?.isEmpty ?? true) && !(unitPriceString?.isEmpty ?? true) ? " + " : "" + + let finalPrice: String = (basePriceString ?? "") + divider + (unitPriceString ?? "") + + return finalPrice + } + + func labelToAbbreviation(generalLabel: [String:DishLabel]?, label: String) -> String { + if let labelObject = generalLabel?[label] { + return labelObject.abbreviation + } + return label + } + + func labelToDescription(generalLabel: [String:DishLabel]?, label: String) -> String { + if let labelObject = generalLabel?[label], let text = labelObject.text["DE"] { + return text + } + return label + } + + func labelsToString(generalLabel: [String:DishLabel]?, labels: [String]) -> String { + let shortenedLabels = labels.map{label -> String in + return labelToAbbreviation(generalLabel: generalLabel, label: label) + } + return shortenedLabels.joined(separator:", ") + } +} diff --git a/Campus-iOS/MapComponent/ViewModel/MapViewModel.swift b/Campus-iOS/MapComponent/ViewModel/MapViewModel.swift index f6b7ff59..f2f4e3c1 100644 --- a/Campus-iOS/MapComponent/ViewModel/MapViewModel.swift +++ b/Campus-iOS/MapComponent/ViewModel/MapViewModel.swift @@ -38,8 +38,8 @@ class MapViewModel: MapViewModelProtocol { private let mock: Bool - private let cafeteriaService: CafeteriasServiceProtocol - private let studyRoomsService: StudyRoomsServiceProtocol + private let cafeteriaService: CafeteriasService + private let studyRoomsService: StudyRoomsService var cafeterias: [Cafeteria] { get { @@ -73,7 +73,7 @@ class MapViewModel: MapViewModelProtocol { } } - init(cafeteriaService: CafeteriasServiceProtocol, studyRoomsService: StudyRoomsServiceProtocol, mock: Bool = false) { + init(cafeteriaService: CafeteriasService, studyRoomsService: StudyRoomsService, mock: Bool = false) { self.cafeteriaService = cafeteriaService self.studyRoomsService = studyRoomsService self.mock = mock diff --git a/Campus-iOS/MapComponent/ViewModel/MealPlanViewModel.swift b/Campus-iOS/MapComponent/ViewModel/MealPlanViewModel.swift index f3f16e54..da81414e 100644 --- a/Campus-iOS/MapComponent/ViewModel/MealPlanViewModel.swift +++ b/Campus-iOS/MapComponent/ViewModel/MealPlanViewModel.swift @@ -9,81 +9,34 @@ import Foundation import SwiftUI import Alamofire -final class MealPlanViewModel: ObservableObject { - private let cafeteria: Cafeteria - private let endpoint = EatAPI.canteens - private let sessionManager = Session.defaultSession +@MainActor +class MealPlanViewModel: ObservableObject { + @Published var state: APIState<[Menu]> = .na + @Published var hasError: Bool = false - @Published private(set) var title: String - @Published private(set) var menus: [MenuViewModel] = [] - @Published var selectedMenu: MenuViewModel? + let service = MealPlanService() + let cafeteria: Cafeteria init(cafeteria: Cafeteria) { self.cafeteria = cafeteria - self.title = cafeteria.name - - fetch() } - func fetch() { - let thisWeekEndpoint = EatAPI.menu(location: cafeteria.id, year: Date().year, week: Date().weekOfYear) - - sessionManager.request(thisWeekEndpoint).responseDecodable(of: MealPlan.self, decoder: MealPlanViewModel.decoder()) { [self] response in - guard let mealPlans = response.value else { return } - addMealPlans(mealPlans: mealPlans) - if self.menus.count > 0 { - selectedMenu = self.menus[0] - } + func getMenus(forcedRefresh: Bool = false) async { + if !forcedRefresh { + self.state = .loading } - - guard let nextWeek = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: Date()) else { return } - - let nextWeekEndpoint = EatAPI.menu(location: cafeteria.id, year: nextWeek.year, week: nextWeek.weekOfYear) - - sessionManager.request(nextWeekEndpoint).responseDecodable(of: MealPlan.self, decoder: MealPlanViewModel.decoder()) { [self] response in - guard let mealPlans = response.value else { return } - addMealPlans(mealPlans: mealPlans) + self.hasError = false + + do { + let data = try await service.fetch(cafeteria: self.cafeteria, forcedRefresh: forcedRefresh) + print(data) + self.state = .success( + data: data + ) + } catch { + self.state = .failed(error: error) + self.hasError = true } - - // initiate loading of labels here, to prevent showing of placeholders - _ = MensaEnumService.shared.getLabels() - } - - func addMealPlans(mealPlans: MealPlan){ - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - - self.menus.append(contentsOf: mealPlans.days - .filter { !$0.dishes.isEmpty && ($0.date.isToday || $0.date.isLaterThanOrEqual(to: Date())) } - .sorted { $0.date < $1.date } - .map { - let categories = MealPlanViewModel.categories(from: $0.dishes) - return MenuViewModel(date: $0.date, categories: categories) } - .filter{ menu in !self.menus.contains(where: {$0.date == menu.date}) } // don't re-add already existent days - ) - - self.menus = self.menus.sorted { $0.date < $1.date } } - static func categories(from dishes: [Dish]) -> [MenuCategory] { - return dishes - .sorted { $0.dishType < $1.dishType } - .reduce(into: [:]) { (acc: inout [String: [Dish]], dish: Dish) -> () in - let type = dish.dishType.isEmpty ? "Sonstige" : dish.dishType - if acc[type] != nil { - acc[type]?.append(dish) - } - acc[type] = [dish] - } - .map { MenuCategory(name: $0.key, dishes: $0.value) } - } - - static func decoder() -> JSONDecoder { - let decoder = JSONDecoder() - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - decoder.dateDecodingStrategy = .formatted(formatter) - - return decoder - } } diff --git a/Campus-iOS/MapComponent/ViewModel/MenuViewModel.swift b/Campus-iOS/MapComponent/ViewModel/MenuViewModel.swift index e5780c9f..b6b6b4fa 100644 --- a/Campus-iOS/MapComponent/ViewModel/MenuViewModel.swift +++ b/Campus-iOS/MapComponent/ViewModel/MenuViewModel.swift @@ -8,11 +8,10 @@ import Foundation import SwiftUI - -final class MenuViewModel: ObservableObject, Identifiable { - let id = UUID() +final class Menu: Identifiable, Decodable { + var id = UUID() let date: Date - @Published var categories: [MenuCategory] + var categories: [MenuCategory] init(date: Date, categories: [MenuCategory]) { self.date = date @@ -31,8 +30,8 @@ final class MenuViewModel: ObservableObject, Identifiable { } } -struct MenuCategory: Identifiable { - let id = UUID() +struct MenuCategory: Identifiable, Decodable { + var id = UUID() let name: String let dishes: [Dish] diff --git a/Campus-iOS/MapComponent/ViewModel/StudyRoomVIewModel.swift b/Campus-iOS/MapComponent/ViewModel/StudyRoomVIewModel.swift deleted file mode 100644 index 16708130..00000000 --- a/Campus-iOS/MapComponent/ViewModel/StudyRoomVIewModel.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// StudyRoomVIewModel.swift -// Campus-iOS -// -// Created by Milen Vitanov on 02.06.22. -// - -import Foundation -import Alamofire - -final class StudyRoomViewModel: ObservableObject { - @Published var roomImageMapping = [RoomImageMapping]() - @Published var room: StudyRoom - - private let endpoint: TUMCabeAPI - private let sessionManager = Session.defaultSession - - init(studyRoom room: StudyRoom) { - self.room = room - self.endpoint = TUMCabeAPI.roomMaps(room: String(room.raum_nr_architekt ?? "")) - fetchImageMapping() - } - - func fetchImageMapping() { - sessionManager.request(endpoint).responseDecodable(of: [RoomImageMapping].self, decoder: JSONDecoder()) { [self] response in - guard let mapping = response.value else { - return - } - self.roomImageMapping = mapping - } - } - - func getImageURL(imageMappingId: Int) -> URL? { - return TUMCabeAPI.mapImage(room: String(self.room.raum_nr_architekt ?? ""), id: imageMappingId).urlRequest?.url - } -} diff --git a/Campus-iOS/MapComponent/ViewModel/StudyRoomViewModel.swift b/Campus-iOS/MapComponent/ViewModel/StudyRoomViewModel.swift new file mode 100644 index 00000000..545426d9 --- /dev/null +++ b/Campus-iOS/MapComponent/ViewModel/StudyRoomViewModel.swift @@ -0,0 +1,72 @@ +// +// StudyRoomVIewModel.swift +// Campus-iOS +// +// Created by Milen Vitanov on 02.06.22. +// + +import Foundation +import Alamofire + +@MainActor +class StudyRoomViewModel: ObservableObject { + @Published var state: APIState<[RoomImageMapping]> = .na + @Published var hasError: Bool = false + + let service: StudyRoomsService = StudyRoomsService() + + func getRoomImageMapping(for room: StudyRoom, forcedRefresh: Bool = false) async { + guard let raumNr = room.raum_nr_architekt else { + return + } + + if !forcedRefresh { + self.state = .loading + } + self.hasError = false + + do { + self.state = .success( + data: try await service.fetchMap(room: raumNr, forcedRefresh: forcedRefresh) + ) + } catch { + self.state = .failed(error: error) + self.hasError = true + } + } + + func getImageURL(for room: StudyRoom, imageMappingId: Int) -> URL? { + if let raumNr = room.raum_nr_architekt { + return try? TUMCabeAPI.mapImage(room: raumNr, id: imageMappingId).asURLRequest().urlRequest?.url + } else { + return nil + } + } +} + +//final class StudyRoomViewModel: ObservableObject { +// @Published var roomImageMapping = [RoomImageMapping]() +// @Published var room: StudyRoom +// +// private let endpoint: TUMCabeAPI +// private let sessionManager = Session.defaultSession +// +// init(studyRoom room: StudyRoom) { +// self.room = room +// self.endpoint = TUMCabeAPI.roomMaps(room: String(room.raum_nr_architekt ?? "")) +// fetchImageMapping() +// } +// +// func fetchImageMapping() { +// sessionManager.request(endpoint).responseDecodable(of: [RoomImageMapping].self, decoder: JSONDecoder()) { [self] response in +// guard let mapping = response.value else { +// return +// } +// self.roomImageMapping = mapping +// } +// } +// +// func getImageURL(imageMappingId: Int) -> URL? { +// return TUMCabeAPI.mapImage(room: String(self.room.raum_nr_architekt ?? ""), id: imageMappingId).urlRequest?.url +// } +//} diff --git a/Campus-iOS/MapComponent/ViewModel/StudyRoomWidgetViewModel.swift b/Campus-iOS/MapComponent/ViewModel/StudyRoomWidgetViewModel.swift index eda0aad4..60b3c90a 100644 --- a/Campus-iOS/MapComponent/ViewModel/StudyRoomWidgetViewModel.swift +++ b/Campus-iOS/MapComponent/ViewModel/StudyRoomWidgetViewModel.swift @@ -16,12 +16,11 @@ class StudyRoomWidgetViewModel: ObservableObject { @Published var rooms: [StudyRoom]? @Published var status: StudyRoomWidgetStatus - private let studyRoomService: StudyRoomsServiceProtocol - private let sessionManager = Session.defaultSession + private let studyRoomService: StudyRoomsService private let locationManager = CLLocationManager() - init(studyRoomService: StudyRoomsServiceProtocol) { + init(studyRoomService: StudyRoomsService) { self.status = .loading self.studyRoomService = studyRoomService diff --git a/Campus-iOS/Model/Model.swift b/Campus-iOS/Model/Model.swift index dd295b97..9d578de8 100644 --- a/Campus-iOS/Model/Model.swift +++ b/Campus-iOS/Model/Model.swift @@ -12,6 +12,7 @@ import SwiftUI import FirebaseAnalytics #endif +@MainActor public class Model: ObservableObject { @Published var showProfile = false { @@ -34,53 +35,28 @@ public class Model: ObservableObject { } } - @Published var loginController: AuthenticationHandler + @Published var loginController = AuthenticationHandler() @Published var isUserAuthenticated = false - @Published var profile: ProfileViewModel = ProfileViewModel() +// @Published var profile: ProfileViewModel = ProfileViewModel() var anyCancellables: [AnyCancellable] = [] - init() { - loginController = AuthenticationHandler() - - if loginController.credentials == Credentials.noTumID { - isUserAuthenticated = false - } else { - loginController.confirmToken() { [weak self] result in - switch result { - case .success: - #if !targetEnvironment(macCatalyst) - Analytics.logEvent("token_confirmed", parameters: nil) - #endif - self?.isLoginSheetPresented = false - self?.isUserAuthenticated = true - self?.loadProfile() - case .failure(_): - self?.isUserAuthenticated = false - if let model = self { - if !model.showProfile { - model.isLoginSheetPresented = true - } - } else { - self?.isLoginSheetPresented = true - } - } - } + var token: String? { + switch self.loginController.credentials { + case .none, .noTumID: + return nil + case .tumID(_, let token): + return token + case .tumIDAndKey(_, let token, _): + return token } } func logout() { - loginController.logout() - self.isLoginSheetPresented = self.showProfile ? false : true - self.isUserAuthenticated = false - self.unloadProfile() - } - - func unloadProfile() { - self.profile = ProfileViewModel() - } - - func loadProfile() { - self.profile = ProfileViewModel(model: self) + DispatchQueue.main.async { + self.loginController.logout() + self.isLoginSheetPresented = true + self.isUserAuthenticated = false + } } } diff --git a/Campus-iOS/MoviesComponent/Screen/MoviesScreen.swift b/Campus-iOS/MoviesComponent/Screen/MoviesScreen.swift new file mode 100644 index 00000000..22c8cd37 --- /dev/null +++ b/Campus-iOS/MoviesComponent/Screen/MoviesScreen.swift @@ -0,0 +1,8 @@ +// +// MovieScreen.swift +// Campus-iOS +// +// Created by David Lin on 22.01.23. +// + +import Foundation diff --git a/Campus-iOS/MoviesComponent/Service/MovieService.swift b/Campus-iOS/MoviesComponent/Service/MovieService.swift new file mode 100644 index 00000000..55eddf59 --- /dev/null +++ b/Campus-iOS/MoviesComponent/Service/MovieService.swift @@ -0,0 +1,17 @@ +// +// MovieService.swift +// Campus-iOS +// +// Created by David Lin on 22.01.23. +// + +import Foundation + +struct MoviesService: ServiceProtocol { + func fetch(forcedRefresh: Bool = false) async throws -> [Movie] { + + let response: [Movie] = try await MainAPI.makeRequest(endpoint: TUMCabeAPI.movie, forcedRefresh: forcedRefresh) + + return response + } +} diff --git a/Campus-iOS/MoviesComponent/ViewModel/Movie.swift b/Campus-iOS/MoviesComponent/ViewModel/Movie.swift index ba96c073..f6c5c0cd 100644 --- a/Campus-iOS/MoviesComponent/ViewModel/Movie.swift +++ b/Campus-iOS/MoviesComponent/ViewModel/Movie.swift @@ -7,7 +7,7 @@ import Foundation -struct Movie: Entity { +struct Movie: Decodable { var id: Int64 var actors: String? diff --git a/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift b/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift index 3aab5b49..f153f7bb 100644 --- a/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift +++ b/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift @@ -9,45 +9,90 @@ import Foundation import Alamofire import FirebaseCrashlytics +@MainActor class MoviesViewModel: ObservableObject { + @Published var state: APIState<[Movie]> = .na + @Published var hasError: Bool = false - @Published var movies = [Movie]() + let service: MoviesService = MoviesService() - typealias ImporterType = Importer - private let sessionManager: Session = Session.defaultSession - - init() { - // TODO: Get from cache, if not found, then fetch - fetch() + func getMovies(forcedRefresh: Bool = false) async { + if !forcedRefresh { + self.state = .loading + } + self.hasError = false + + do { + let movies = try await service.fetch(forcedRefresh: forcedRefresh) + + self.state = .success( + data: filterAndSort(for: movies) + ) + } catch { + self.state = .failed(error: error) + self.hasError = true + } } - func fetch() { - let endpoint: URLRequestConvertible = TUMCabeAPI.movie - let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) - let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) + func filterAndSort(for movies: [Movie]) -> [Movie] { + let relevantMovies = movies.filter ({ + if let date = $0.date { + return Date.now <= date + } else { + // If no date available keep movie just in case + return true; + } + }) - importer.performFetch(handler: { result in - switch result { - case .success(let incoming): - // Remove all movies from list that are older than today - let relevantMovies = incoming.filter ({ - if let date = $0.date { - return Date.now <= date - } else { - // If no date available keep movie just in case - return true; - } - }) - - self.movies = relevantMovies.sorted(by: { - guard let dateOne = $0.date, let dateTwo = $1.date else { - return false - } - return dateOne < dateTwo - }) - case .failure(let error): - print(error) + return relevantMovies.sorted(by: { + guard let dateOne = $0.date, let dateTwo = $1.date else { + return false } + return dateOne < dateTwo }) + } } + +//class MoviesViewModel: ObservableObject { +// +// @Published var movies = [Movie]() +// +// typealias ImporterType = Importer +// private let sessionManager: Session = Session.defaultSession +// +// init() { +// // TODO: Get from cache, if not found, then fetch +// fetch() +// } +// +// func fetch() { +// let endpoint: URLRequestConvertible = TUMCabeAPI.movie +// let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) +// let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) +// +// importer.performFetch(handler: { result in +// switch result { +// case .success(let incoming): +// // Remove all movies from list that are older than today +// let relevantMovies = incoming.filter ({ +// if let date = $0.date { +// return Date.now <= date +// } else { +// // If no date available keep movie just in case +// return true; +// } +// }) +// +// self.movies = relevantMovies.sorted(by: { +// guard let dateOne = $0.date, let dateTwo = $1.date else { +// return false +// } +// return dateOne < dateTwo +// }) +// case .failure(let error): +// print(error) +// } +// }) +// } +//} diff --git a/Campus-iOS/MoviesComponent/Views/MoviesView.swift b/Campus-iOS/MoviesComponent/Views/MoviesView.swift index 0e20d911..b9fc19f8 100644 --- a/Campus-iOS/MoviesComponent/Views/MoviesView.swift +++ b/Campus-iOS/MoviesComponent/Views/MoviesView.swift @@ -7,9 +7,54 @@ import SwiftUI -struct MoviesView: View { +struct MoviesScreen: View { + @StateObject var vm = MoviesViewModel() - @ObservedObject var viewModel = MoviesViewModel() + var body: some View { + Group { + switch vm.state { + case .success(let movies): + VStack { + MoviesView(movies: movies) + .refreshable { + await vm.getMovies(forcedRefresh: true) + } + } + case .loading, .na: + LoadingView(text: "Fetching News") + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: vm.getMovies + ) + } + }.task { + await vm.getMovies() + }.alert( + "Error while fetching News", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getMovies(forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMCabeAPIError { + Text(apiError.errorDescription ?? "TUMCabeAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } +} + +struct MoviesView: View { + let movies: [Movie] @State private var selectedMovie: Movie? = nil var items: [GridItem] { @@ -22,7 +67,7 @@ struct MoviesView: View { .foregroundColor(Color(UIColor.lightGray)) ScrollView(.vertical) { LazyVGrid(columns: items, spacing: 10) { - ForEach(self.viewModel.movies, id: \.id ) { movie in + ForEach(self.movies, id: \.id ) { movie in MovieCard(movie: movie).padding(7) .onTapGesture { selectedMovie = movie @@ -39,8 +84,8 @@ struct MoviesView: View { } } -struct MoviesView_Previews: PreviewProvider { - static var previews: some View { - MoviesView() - } -} +//struct MoviesView_Previews: PreviewProvider { +// static var previews: some View { +// MoviesView() +// } +//} diff --git a/Campus-iOS/NewsComponent/Service/News.swift b/Campus-iOS/NewsComponent/Model/News.swift similarity index 78% rename from Campus-iOS/NewsComponent/Service/News.swift rename to Campus-iOS/NewsComponent/Model/News.swift index eb43915f..80e4b1bc 100644 --- a/Campus-iOS/NewsComponent/Service/News.swift +++ b/Campus-iOS/NewsComponent/Model/News.swift @@ -7,7 +7,7 @@ import Foundation -struct News: Entity { +struct News: Decodable { var id: String? var sourceID: Int64 var date: Date? @@ -44,18 +44,28 @@ struct News: Entity { guard let sourceID = Int64(sourceString) else { throw DecodingError.typeMismatch(Int64.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Value for source could not be converted to Int64")) } - let date = try container.decode(Date.self, forKey: .date) let created = try container.decode(Date.self, forKey: .created) let title = try container.decode(String.self, forKey: .title) - let link = try container.decode(URL.self, forKey: .link) let imageURLString = try container.decode(String.self, forKey: .imageURL) + do { + self.date = try container.decode(Date.self, forKey: .date) + } catch { + self.date = Date.distantPast + print("News decoding error for property date: \(error)") + } + + do { + self.link = try container.decode(URL.self, forKey: .link) + } catch { + self.link = nil + print("News decoding error for property link: \(error)") + } + self.id = id self.sourceID = sourceID - self.date = date self.created = created self.title = title - self.link = link self.imageURL = imageURLString.replacingOccurrences(of: " ", with: "%20") } } diff --git a/Campus-iOS/NewsComponent/Service/NewsSource.swift b/Campus-iOS/NewsComponent/Model/NewsSource.swift similarity index 52% rename from Campus-iOS/NewsComponent/Service/NewsSource.swift rename to Campus-iOS/NewsComponent/Model/NewsSource.swift index a1bc137d..9a719214 100644 --- a/Campus-iOS/NewsComponent/Service/NewsSource.swift +++ b/Campus-iOS/NewsComponent/Model/NewsSource.swift @@ -9,14 +9,12 @@ import Alamofire import Combine import FirebaseCrashlytics -class NewsSource: Entity, ObservableObject { - - typealias ImporterType = Importer +struct NewsSource: Decodable, Identifiable { public var id: Int64? public var title: String? public var icon: URL? - @Published var news: [News] + public var news: [News] enum CodingKeys: String, CodingKey { case id = "source" @@ -31,7 +29,7 @@ class NewsSource: Entity, ObservableObject { self.news = news } - required init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let idString = try container.decode(String.self, forKey: .id) @@ -46,31 +44,5 @@ class NewsSource: Entity, ObservableObject { self.title = title self.icon = icon self.news = [] - fetchNews() - } - - func fetchNews() { - guard let id = self.id else { - print("NewsSource contain no id") - return - } - - let endpoint: URLRequestConvertible = TUMCabeAPI.news(source: id.description) - let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) - let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) - - importer.performFetch(handler: { result in - switch result { - case .success(let storage): - self.news = storage.filter( { - guard let title = $0.title, let link = $0.link else { - return false - } - return !title.isEmpty && !link.description.isEmpty - } ) - case .failure(let error): - print(error) - } - }) } } diff --git a/Campus-iOS/NewsComponent/NewsScreen/NewsScreen.swift b/Campus-iOS/NewsComponent/NewsScreen/NewsScreen.swift new file mode 100644 index 00000000..0b588370 --- /dev/null +++ b/Campus-iOS/NewsComponent/NewsScreen/NewsScreen.swift @@ -0,0 +1,53 @@ +// +// NewsScreen.swift +// Campus-iOS +// +// Created by David Lin on 22.01.23. +// + +import SwiftUI + +struct NewsScreen: View { + @StateObject var vm = NewsViewModel() + + var body: some View { + Group { + switch vm.state { + case .success(let newsSources): + VStack { + NewsView(latestFiveNews: vm.latestFiveNews, newsSources: newsSources) .refreshable { + await vm.getNewsSources(forcedRefresh: true) + } + } + case .loading, .na: + LoadingView(text: "Fetching News") + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: vm.getNewsSources + ) + } + }.task { + await vm.getNewsSources() + }.alert( + "Error while fetching News", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getNewsSources(forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMCabeAPIError { + Text(apiError.errorDescription ?? "TUMCabeAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } +} diff --git a/Campus-iOS/NewsComponent/Service/NewsService.swift b/Campus-iOS/NewsComponent/Service/NewsService.swift new file mode 100644 index 00000000..f334543c --- /dev/null +++ b/Campus-iOS/NewsComponent/Service/NewsService.swift @@ -0,0 +1,27 @@ +// +// NewsService.swift +// Campus-iOS +// +// Created by David Lin on 22.01.23. +// + +import Foundation + +struct NewsService: ServiceProtocol { + func fetch(forcedRefresh: Bool = false) async throws -> [NewsSource] { + + var newsSourceResponse: [NewsSource] = try await MainAPI.makeRequest(endpoint: TUMCabeAPI.newsSources, forcedRefresh: forcedRefresh) + + for i in newsSourceResponse.indices { + guard let idDescription = newsSourceResponse[i].id?.description else { + break + } + + let news: [News] = try await MainAPI.makeRequest(endpoint: TUMCabeAPI.news(source: String(idDescription))) + + newsSourceResponse[i].news = news + } + + return newsSourceResponse + } +} diff --git a/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift b/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift index b17c338b..bcfc7707 100644 --- a/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift +++ b/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift @@ -10,24 +10,33 @@ import FirebaseCrashlytics @MainActor class NewsViewModel: ObservableObject { - - @Published var newsSources = [NewsSource]() - @Published var news = [News]() - @Published var sourcesAndNews = [(Int64?, [News])]() - //@Published var news = [News]() + @Published var state: APIState<[NewsSource]> = .na + @Published var hasError: Bool = false - private let sessionManager: Session = Session.defaultSession + let service: NewsService = NewsService() - init() { - // TODO: Get from cache, if not found, then fetch - - fetch() -// fetchNews(sourceId: 1) + func getNewsSources(forcedRefresh: Bool = false) async { + if !forcedRefresh { + self.state = .loading + } + self.hasError = false + + do { + self.state = .success( + data: try await service.fetch(forcedRefresh: forcedRefresh) + ) + } catch { + self.state = .failed(error: error) + self.hasError = true + } } var latestFiveNews: [(String?, News?)] { - print(">> latestFiveNews loaded") - let latestNews = Array(self.newsSources + guard case .success(let newsSources) = self.state else { + return [] + } + + let latestNews = Array(newsSources .map({$0.news}) .reduce([], +) .filter({$0.created != nil && $0.sourceID != 2}) @@ -44,71 +53,110 @@ class NewsViewModel: ObservableObject { return latestFiveNews } - - func fetch() { - typealias ImporterType = Importer - - let endpoint: URLRequestConvertible = TUMCabeAPI.newsSources - let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) - let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) - - importer.performFetch(handler: { result in - switch result { - case .success(let incoming): - incoming.forEach { newsSource in - self.fetchNews(sourceId: newsSource.id) - } - - self.newsSources = incoming - case .failure(let error): - print(error) - } - }) - } - - func fetchNews(sourceId: Int64?) { - typealias ImporterTypeNews = Importer - - guard let id = sourceId else { - print("NewsSource contains no id") - return - } - - let endpointNews: URLRequestConvertible = TUMCabeAPI.news(source: id.description) - let dateDecodingStrategyNews: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) - let importerNews = ImporterTypeNews(endpoint: endpointNews, dateDecodingStrategy: dateDecodingStrategyNews) - - importerNews.performFetch(handler: { result in - switch result { - case .success(let storage): - let news = storage.filter( { - guard let title = $0.title, let link = $0.link else { - return false - } - return !title.isEmpty && !link.description.isEmpty - } ) - self.sourcesAndNews.append((sourceId, news)) - case .failure(let error): - print(error) - } - }) - } - - } -class MockNewsViewModel: NewsViewModel { - - static let mockNewsA = News(id: "1", sourceId: 1, date: Date(), created: Date(), title: "Dummy Title", link: URL(string: "https://github.com/orgs/TUM-Dev"), imageURL: "https://app.tum.de/File/news/newspread/dab04abdf3954d3e1bf56cef44d68662.jpg") - static let mockNewsB = News(id: "3", sourceId: 3, date: Date(), created: Date(), title: "Dummy Title", link: URL(string: "https://github.com/orgs/TUM-Dev"), imageURL: "https://app.tum.de/File/news/newspread/dab04abdf3954d3e1bf56cef44d68662.jpg") - static let newsSourceA = NewsSource(id: 1, title: "TUM News A", icon: nil, news: [mockNewsA, mockNewsA, mockNewsA]) - static let newsSourceB = NewsSource(id: 3, title: "TUM News B", icon: nil, news: [mockNewsB, mockNewsB, mockNewsB]) - - let mockNewsSources = [newsSourceA, newsSourceB] - - - override func fetch() { - self.newsSources = mockNewsSources - } -} +//@MainActor +//class NewsViewModel: ObservableObject { +// +// @Published var newsSources = [NewsSource]() +// @Published var news = [News]() +// @Published var sourcesAndNews = [(Int64?, [News])]() +// //@Published var news = [News]() +// +// private let sessionManager: Session = Session.defaultSession +// +// init() { +// // TODO: Get from cache, if not found, then fetch +// +// fetch() +//// fetchNews(sourceId: 1) +// } +// +// var latestFiveNews: [(String?, News?)] { +// print(">> latestFiveNews loaded") +// let latestNews = Array(self.newsSources +// .map({$0.news}) +// .reduce([], +) +// .filter({$0.created != nil && $0.sourceID != 2}) +// .sorted(by: { +// guard let date1 = $0.created, let date2 = $1.created else { +// return false +// } +// return date1.compare(date2) == .orderedDescending +// }).prefix(5)) +// +// let latestFiveNews = latestNews.map { news in +// (newsSources.first(where: {$0.id == news.sourceID})?.title, news) +// } +// +// return latestFiveNews +// } +// +// func fetch() { +// typealias ImporterType = Importer +// +// let endpoint: URLRequestConvertible = TUMCabeAPI.newsSources +// let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) +// let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) +// +// importer.performFetch(handler: { result in +// switch result { +// case .success(let incoming): +// incoming.forEach { newsSource in +// self.fetchNews(sourceId: newsSource.id) +// } +// +// self.newsSources = incoming +// case .failure(let error): +// print(error) +// } +// }) +// } +// +// func fetchNews(sourceId: Int64?) { +// typealias ImporterTypeNews = Importer +// +// guard let id = sourceId else { +// print("NewsSource contains no id") +// return +// } +// +// let endpointNews: URLRequestConvertible = TUMCabeAPI.news(source: id.description) +// let dateDecodingStrategyNews: JSONDecoder.DateDecodingStrategy? = .formatted(.yyyyMMddhhmmss) +// let importerNews = ImporterTypeNews(endpoint: endpointNews, dateDecodingStrategy: dateDecodingStrategyNews) +// +// importerNews.performFetch(handler: { result in +// switch result { +// case .success(let storage): +// let news = storage.filter( { +// guard let title = $0.title, let link = $0.link else { +// return false +// } +// return !title.isEmpty && !link.description.isEmpty +// } ) +// self.sourcesAndNews.append((sourceId, news)) +// case .failure(let error): +// print(error) +// } +// }) +// } +// +// +//} + +//class MockNewsViewModel: NewsViewModel { +// +// static let mockNewsA = News(id: "1", sourceId: 1, date: Date(), created: Date(), title: "Dummy Title", link: URL(string: "https://github.com/orgs/TUM-Dev"), imageURL: "https://app.tum.de/File/news/newspread/dab04abdf3954d3e1bf56cef44d68662.jpg") +// static let mockNewsB = News(id: "3", sourceId: 3, date: Date(), created: Date(), title: "Dummy Title", link: URL(string: "https://github.com/orgs/TUM-Dev"), imageURL: "https://app.tum.de/File/news/newspread/dab04abdf3954d3e1bf56cef44d68662.jpg") +// +// static let newsSourceA = NewsSource(id: 1, title: "TUM News A", icon: nil, news: [mockNewsA, mockNewsA, mockNewsA]) +// static let newsSourceB = NewsSource(id: 3, title: "TUM News B", icon: nil, news: [mockNewsB, mockNewsB, mockNewsB]) +// +// let mockNewsSources = [newsSourceA, newsSourceB] +// +// +// override func fetch() { +// self.newsSources = mockNewsSources +// } +//} diff --git a/Campus-iOS/NewsComponent/Views/NewsView.swift b/Campus-iOS/NewsComponent/Views/NewsView.swift index 67c573ef..ce4c6ef0 100644 --- a/Campus-iOS/NewsComponent/Views/NewsView.swift +++ b/Campus-iOS/NewsComponent/Views/NewsView.swift @@ -9,18 +9,20 @@ import SwiftUI struct NewsView: View { - @StateObject var viewModel: NewsViewModel @AppStorage("useBuildInWebView") var useBuildInWebView: Bool = true @Environment(\.scenePhase) var scenePhase @State var isWebViewShowed = false @State var selectedLink: URL? = nil + let latestFiveNews: [(String?, News?)] + let newsSources: [NewsSource] + var body: some View { ScrollView(.vertical) { VStack(alignment: .center) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 30) { - ForEach(viewModel.latestFiveNews, id: \.1?.id) { oneLatestNews in + ForEach(latestFiveNews, id: \.1?.id) { oneLatestNews in GeometryReader { geometry in if let url = oneLatestNews.1?.link { if self.useBuildInWebView { @@ -43,16 +45,15 @@ struct NewsView: View { // adjust height Spacer(minLength: 1) }.sheet(item: $selectedLink) { selectedLink in - if let link = selectedLink { - SFSafariViewWrapper(url: link) - } + SFSafariViewWrapper(url: selectedLink) } Spacer() }.padding() } Spacer() - ForEach(viewModel.newsSources.filter({!$0.news.isEmpty && $0.id != 2}), id: \.id) { source in + + ForEach(newsSources.filter({!$0.news.isEmpty && $0.id != 2}), id: \.id) { source in Collapsible(title: { AnyView(HStack(alignment: .center) { Image(systemName: "list.bullet").foregroundColor(.blue) @@ -67,17 +68,4 @@ struct NewsView: View { } } } - - init(viewModel: NewsViewModel) { - self._viewModel = StateObject(wrappedValue: viewModel) - viewModel.fetch() - } -} - - -struct NewsView_Previews: PreviewProvider { - static var previews: some View { - let vm = MockNewsViewModel() - NewsView(viewModel: vm) - } } diff --git a/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift b/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift index e464cb0c..5812e1ca 100644 --- a/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift +++ b/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift @@ -7,7 +7,7 @@ import Foundation -struct Organisation: Decodable { +struct Organisation: Decodable, Identifiable { let name: String let id: String let number: String diff --git a/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift b/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift index 1c8709a0..7e75cf75 100644 --- a/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift +++ b/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift @@ -7,7 +7,8 @@ import Foundation -struct PhoneExtension: Decodable { +struct PhoneExtension: Decodable, Identifiable { + let id = UUID() let phoneNumber: String let countryCode: String let areaCode: String diff --git a/Campus-iOS/PersonDetailedComponent/Entity/Room.swift b/Campus-iOS/PersonDetailedComponent/Entity/Room.swift index eec1fcf4..7d92181c 100644 --- a/Campus-iOS/PersonDetailedComponent/Entity/Room.swift +++ b/Campus-iOS/PersonDetailedComponent/Entity/Room.swift @@ -7,7 +7,7 @@ import Foundation -struct Room: Decodable { +struct Room: Decodable, Identifiable { let number: String let buildingName: String let buildingNumber: String diff --git a/Campus-iOS/PersonDetailedComponent/Screen/PersonDetailedScreen.swift b/Campus-iOS/PersonDetailedComponent/Screen/PersonDetailedScreen.swift new file mode 100644 index 00000000..2a988a8c --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/Screen/PersonDetailedScreen.swift @@ -0,0 +1,69 @@ +// +// PersonDetailedScreen.swift +// Campus-iOS +// +// Created by David Lin on 21.01.23. +// + +import SwiftUI + +struct PersonDetailedScreen: View { + @StateObject var vm: PersonDetailedViewModel + + init(model: Model, person: Person) { + self._vm = StateObject(wrappedValue: PersonDetailedViewModel(model: model, service: PersonDetailedService(), type: .Person(person))) + } + + init(model: Model, profile: Profile) { + self._vm = StateObject(wrappedValue: PersonDetailedViewModel(model: model, service: PersonDetailedService(), type: .Profile(profile))) + } + + var body: some View { + Group { + switch vm.state { + case .success(let personDetails): + VStack { + PersonDetailedView(personDetails: personDetails) + .background(Color(.systemGroupedBackground)) + } + case .loading, .na: + LoadingView(text: "Fetching Person Details") + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: {_ in await vm.getDetails(forcedRefresh: true)} + ) + } + } + .task { + await vm.getDetails(forcedRefresh: true) + } + .alert("Error while fetching Person Details", isPresented: $vm.hasError, presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getDetails(forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMOnlineAPIError { + Text(apiError.errorDescription ?? "TUMOnlineAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + NavigationLink( + destination: AddToContactsView(contact: self.vm.cnContact) + .navigationBarTitleDisplayMode(.inline) + ) { + Label("", systemImage: "person.crop.circle.badge.plus") + } + } + } + } +} diff --git a/Campus-iOS/PersonDetailedComponent/Service/PersonDetailedService.swift b/Campus-iOS/PersonDetailedComponent/Service/PersonDetailedService.swift new file mode 100644 index 00000000..c1dd0076 --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/Service/PersonDetailedService.swift @@ -0,0 +1,16 @@ +// +// PersonDetailedService.swift +// Campus-iOS +// +// Created by David Lin on 21.01.23. +// + +import Foundation + +struct PersonDetailedService { + func fetch(for id: String, token: String, forcedRefresh: Bool) async throws -> PersonDetails { + let response : PersonDetails = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.personDetails(identNumber: id), token: token, forcedRefresh: forcedRefresh) + + return response + } +} diff --git a/Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift b/Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift deleted file mode 100644 index b2c2ab5b..00000000 --- a/Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SwiftUIView.swift -// Campus-iOS -// -// Created by Milen Vitanov on 09.02.22. -// - -import SwiftUI - -struct PersonDetailedCellView: View { - - @State var cell: PersonDetailsCell - - var body: some View { - VStack(alignment: .leading) { - Text(cell.key) - .foregroundColor(Color(.label)) - Text(cell.value) - .foregroundColor(.blue) - } - } -} - -struct PersonDetailedCellView_Previews: PreviewProvider { - static var previews: some View { - PersonDetailedCellView(cell: PersonDetailsCell(key: "E-Mail", value: "test@example.com", actionType: PersonDetailsCell.ActionType.mail)) - } -} diff --git a/Campus-iOS/PersonDetailedComponent/View/PersonDetailedView.swift b/Campus-iOS/PersonDetailedComponent/View/PersonDetailedView.swift index 96167105..e1fef093 100644 --- a/Campus-iOS/PersonDetailedComponent/View/PersonDetailedView.swift +++ b/Campus-iOS/PersonDetailedComponent/View/PersonDetailedView.swift @@ -10,115 +10,144 @@ import ContactsUI struct PersonDetailedView: View { let imageSize: CGFloat = 125.0 - - @ObservedObject var viewModel: PersonDetailedViewModel - - init(withPerson person: Person) { - self.viewModel = PersonDetailedViewModel(withPerson: person) - } - - init(withProfile profile: Profile) { - self.viewModel = PersonDetailedViewModel(withProfile: profile) - } + let personDetails: PersonDetails var body: some View { VStack { - Spacer() - if let header = viewModel.sections?.first(where: { $0.name == "Header" })?.cells.first, let cell = header as? PersonDetailsHeader { - if let image = cell.image { - Image(uiImage: image) - .resizable() - .clipShape(Circle()) - .aspectRatio(contentMode: .fill) - .frame(width: imageSize, height: imageSize) - } else { - Image(systemName: "person.crop.circle.fill") - .resizable() - .foregroundColor(Color(.secondaryLabel)) - .frame(width: imageSize, height: imageSize) - } - Spacer().frame(height: 10) - Text("\(cell.name)").font(.system(size: 18)) + if let image = personDetails.image { + Image(uiImage: image) + .resizable() + .clipShape(Circle()) + .aspectRatio(contentMode: .fill) + .frame(width: imageSize, height: imageSize) } else { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .accentColor)) - .padding(2) + Image(systemName: "person.crop.circle.fill") + .resizable() + .foregroundColor(Color(.secondaryLabel)) + .frame(width: imageSize, height: imageSize) } - if self.viewModel.sections?.count ?? 0 > 1 { - form - } else { - List { - HStack { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .accentColor)) - .padding(2) - Spacer() + Spacer().frame(height: 10) + Text("\(personDetails.firstName) \(personDetails.name)").font(.system(size: 18)) + List { + if !personDetails.email.isEmpty || !(personDetails.officeHours?.isEmpty ?? false) { + Section(header: Text("General")) { + if !personDetails.email.isEmpty, let mailURL = URL(string: "mailto:\(personDetails.email)") { + VStack(alignment: .leading) { + Text("E-Mail") + Link(personDetails.email, destination: mailURL) + } + } + if let officeHours = personDetails.officeHours, !officeHours.isEmpty { + VStack(alignment: .leading) { + Text("Office Hours") + Text(officeHours) + } + } } } - } - } - .background(Color(.systemGroupedBackground)) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - NavigationLink( - destination: AddToContactsView(contact: self.viewModel.cnContact) - .navigationBarTitleDisplayMode(.inline) - ) { - Label("", systemImage: "person.crop.circle.badge.plus") - }.disabled(self.viewModel.sections?.count ?? 0 < 2) - } - } - .onAppear { - self.viewModel.fetch() - } - } - - var form: some View { - Form { - ForEach(self.viewModel.sections?.filter({ $0.name != "Header" }) ?? []) { section in - Section(section.name) { - ForEach(section.cells as? [PersonDetailsCell] ?? []) { singleCell in - Button(action: { - Self.cellActionBasedOnType(cell: singleCell) - }, label: { PersonDetailedCellView(cell: singleCell) }) + if !personDetails.officialContact.isEmpty { + Section(header: Text("Offical Contact")) { + ForEach(personDetails.officialContact) { contactInfo in + VStack(alignment: .leading) { + switch contactInfo { + case .phone(let phone): + let number = phone.replacingOccurrences(of: " ", with: "") + if let phoneURL = URL(string: "tel:\(number)") { + Text("Phone") + Link("\(phone)", destination: phoneURL) + } + case .mobilePhone(let mobilePhone): + let number = mobilePhone.replacingOccurrences(of: " ", with: "") + if let mobilePhoneURL = URL(string: "tel:\(number)") { + Text("Mobile") + Link("\(mobilePhone)", destination: mobilePhoneURL) + } + case .fax(let fax): + Text("Fax") + Text("\(fax)") + case .additionalInfo(let additionalInfo): + Text("Additional Info") + Text("\(additionalInfo)") + case .homepage(let homepage): + if let homepageURL = URL(string: homepage) { + Text("Hoomepage") + Link("\(homepage)", destination: homepageURL) + } + } + } + } + } + } + if !personDetails.privateContact.isEmpty { + Section(header: Text("Offical Contact")) { + ForEach(personDetails.privateContact) { contactInfo in + VStack(alignment: .leading) { + switch contactInfo { + case .phone(let phone): + let number = phone.replacingOccurrences(of: " ", with: "") + if let phoneURL = URL(string: "tel:\(number)") { + Text("Phone") + Link("\(phone)", destination: phoneURL) + } + case .mobilePhone(let mobilePhone): + let number = mobilePhone.replacingOccurrences(of: " ", with: "") + if let mobilePhoneURL = URL(string: "tel:\(number)") { + Text("Mobile") + Link("\(mobilePhone)", destination: mobilePhoneURL) + } + case .fax(let fax): + Text("Fax") + Text("\(fax)") + case .additionalInfo(let additionalInfo): + Text("Additional Info") + Text("\(additionalInfo)") + case .homepage(let homepage): + if let homepageURL = URL(string: homepage) { + Text("Hoomepage") + Link("\(homepage)", destination: homepageURL) + } + } + } + } + } + } + if !personDetails.phoneExtensions.isEmpty { + Section(header: Text("Phone Extensions")) { + ForEach(personDetails.phoneExtensions) { phoneExtension in + let number = phoneExtension.phoneNumber.replacingOccurrences(of: " ", with: "") + if let phoneNumberURL = URL(string: "tel:\(number)") { + VStack(alignment: .leading) { + Text("Office") + Link("\(phoneExtension.phoneNumber)", destination: phoneNumberURL) + } + } + } + } + } + + if !personDetails.organisations.isEmpty { + Section(header: Text("Organisations")) { + ForEach(personDetails.organisations) { organisation in + VStack(alignment: .leading) { + Text("Organisation") + Text("\(organisation.name)") + } + } + } + } + + if !personDetails.rooms.isEmpty { + Section(header: Text("Rooms")) { + ForEach(personDetails.rooms) { room in + VStack(alignment: .leading) { + Text("Room") + Text("\(room.shortLocationDescription)") + } + } } } } } - .edgesIgnoringSafeArea(.all) } - static func cellActionBasedOnType(cell: PersonDetailsCell) { - switch cell.actionType { - case .none, .showRoom: - break - case .call: - let number = cell.value.replacingOccurrences(of: " ", with: "") - if let url = URL(string: "tel://\(number)") { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - case .mail: - if let url = URL(string: "mailto:\(cell.value)") { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - case .openURL: - if let url = URL(string: cell.value) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - } -} - -struct PersonDetailedView_Previews: PreviewProvider { - static var previews: some View { - Group { - PersonDetailedView(withPerson: Person(firstName: "Milen", lastName: "Vitanov", title: nil, nr: "12654465", obfuscatedId: "445555dd4", gender: Gender.male)) - .preferredColorScheme(.light) - .previewInterfaceOrientation(.portrait) - PersonDetailedView(withPerson: Person(firstName: "Milen", lastName: "Vitanov", title: nil, nr: "12654465", obfuscatedId: "445555dd4", gender: Gender.male)) - .preferredColorScheme(.dark) - .previewInterfaceOrientation(.portrait) - } - } } diff --git a/Campus-iOS/PersonDetailedComponent/ViewModel/PersonDetailedViewModel.swift b/Campus-iOS/PersonDetailedComponent/ViewModel/PersonDetailedViewModel.swift index 60a19249..da28fe42 100644 --- a/Campus-iOS/PersonDetailedComponent/ViewModel/PersonDetailedViewModel.swift +++ b/Campus-iOS/PersonDetailedComponent/ViewModel/PersonDetailedViewModel.swift @@ -11,197 +11,107 @@ import XMLCoder import SwiftUI import Contacts -struct PersonDetailsHeader: Identifiable, Hashable { - let id = UUID() - let image: UIImage? - let imageURL: URL? - let name: String - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(name) - } -} - -struct PersonDetailsCell: Identifiable, Hashable { - enum ActionType { - case call - case mail - case openURL - case showRoom - } - - let id = UUID() - let key: String - let value: String - let actionType: ActionType? -} - -struct PersonDetailsSection: Identifiable, Hashable { - let id = UUID() - let name: String - let cells: [AnyHashable] +enum DetailsType { + case Person(Person) + case Profile(Profile) } +@MainActor class PersonDetailedViewModel: ObservableObject { - var person: PersonDetails? - var endpoint: TUMOnlineAPI - - @Published var sections: [PersonDetailsSection]? - - private let sessionManager = Session.defaultSession - - init(withId id: String) { - self.endpoint = TUMOnlineAPI.personDetails(identNumber: id) - } - - init(withPerson person: Person) { - let header: PersonDetailsHeader - if let personGroup = person.personGroup, let id = person.id { - header = PersonDetailsHeader(image: nil, imageURL: TUMOnlineAPI.profileImage(personGroup: personGroup, id: id).urlRequest?.url, name: "\(person.title?.appending(" ") ?? "")\(person.firstName) \(person.name)") - } else { - header = PersonDetailsHeader(image: nil, imageURL: nil, name: "\(person.title?.appending(" ") ?? "")\(person.firstName) \(person.name)") - } - - self.sections = [PersonDetailsSection(name: "Header", cells: [header])] - self.endpoint = TUMOnlineAPI.personDetails(identNumber: person.obfuscatedID) - } + @Published var state: APIState = .na + @Published var hasError: Bool = false + let model: Model + let service: PersonDetailedService + let type: DetailsType - init(withProfile profile: Profile) { - var sections: [PersonDetailsSection] = [] - - let header: PersonDetailsHeader - if let personGroup = profile.personGroup, let id = profile.id { - header = PersonDetailsHeader(image: nil, imageURL: TUMOnlineAPI.profileImage(personGroup: personGroup, id: id).urlRequest?.url, name: "\(profile.firstname ?? "") \(profile.surname ?? "")") - } else { - header = PersonDetailsHeader(image: nil, imageURL: nil, name: "\(profile.firstname ?? "") \(profile.surname ?? "")") - } - sections.append(PersonDetailsSection(name: "Header", cells: [header])) - - if let tumID = profile.tumID { - sections.append(PersonDetailsSection(name: "General", cells: [PersonDetailsCell(key: "TUM ID".localized, value: tumID, actionType: .none)])) - } - - self.sections = sections - self.endpoint = TUMOnlineAPI.personDetails(identNumber: profile.obfuscatedID ?? "") + init(model: Model, service: PersonDetailedService, type: DetailsType) { + self.model = model + self.service = service + self.type = type } - func fetch() { - self.sessionManager.request(self.endpoint).responseDecodable(of: PersonDetails.self, decoder: XMLDecoder()) { [weak self] response in - guard let value = response.value else { return } - self?.person = value - self?.fillFromProfileDetails() - } - } - - func fillFromProfileDetails() { - let header: PersonDetailsHeader - if let personGroup = self.person?.personGroup, let id = self.person?.id { - header = PersonDetailsHeader(image: self.person?.image, imageURL: TUMOnlineAPI.profileImage(personGroup: personGroup, id: id).urlRequest?.url, name: "\(self.person?.title?.appending(" ") ?? "")\(self.person?.firstName.appending(" ") ?? "")\(self.person?.name ?? "")") - } else { - header = PersonDetailsHeader(image: self.person?.image, imageURL: nil, name: "\(self.person?.title?.appending(" ") ?? "")\(self.person?.firstName.appending(" ") ?? "")\(self.person?.name ?? "")") - } - - var general: [PersonDetailsCell] = [] - if let email = self.person?.email, !email.isEmpty { - general.append(PersonDetailsCell(key: "E-Mail".localized, value: email, actionType: .mail)) + func getDetails(forcedRefresh: Bool) async { + if !forcedRefresh { + self.state = .loading } - if let officeHours = self.person?.officeHours, !officeHours.isEmpty { - general.append(PersonDetailsCell(key: "Office Hours".localized, value: officeHours, actionType: .none)) + self.hasError = false + + guard let token = self.model.token else { + self.state = .failed(error: NetworkingError.unauthorized) + self.hasError = true + return } - - var officialContact: [PersonDetailsCell] = [] - if let contactInfo = self.person?.officialContact, contactInfo.count > 0 { - officialContact = contactInfo.map { info in - switch info { - case let .phone(number): return PersonDetailsCell(key: "Phone".localized, value: number, actionType: .call) - case let .mobilePhone(number): return PersonDetailsCell(key: "Mobile".localized, value: number, actionType: .call) - case let .fax(number): return PersonDetailsCell(key: "Fax".localized, value: number, actionType: .none) - case let .additionalInfo(additionalInfo): return PersonDetailsCell(key: "Additional Info".localized, value: additionalInfo, actionType: .none) - case let .homepage(urlString): return PersonDetailsCell(key: "Homepage".localized, value: urlString, actionType: .openURL) - } + + do { + + if case .Person(let person) = type { + self.state = .success( + data: try await service.fetch(for: person.obfuscatedID, token: token, forcedRefresh: forcedRefresh) + ) } - } - var privateContact: [PersonDetailsCell] = [] - if let privateContactInfo = self.person?.privateContact, privateContactInfo.count > 0 { - privateContact = privateContactInfo.map { info in - switch info { - case let .phone(number): return PersonDetailsCell(key: "Phone".localized, value: number, actionType: .call) - case let .mobilePhone(number): return PersonDetailsCell(key: "Mobile".localized, value: number, actionType: .call) - case let .fax(number): return PersonDetailsCell(key: "Fax".localized, value: number, actionType: .none) - case let .additionalInfo(additionalInfo): return PersonDetailsCell(key: "Additional Info".localized, value: additionalInfo, actionType: .none) - case let .homepage(urlString): return PersonDetailsCell(key: "Homepage".localized, value: urlString, actionType: .openURL) - } + if case .Profile(let profile) = type, let obfuscatedID = profile.obfuscatedID { + self.state = .success( + data: try await service.fetch(for: obfuscatedID, token: token, forcedRefresh: forcedRefresh) + ) } + } catch { + self.state = .failed(error: error) + self.hasError = true } - - let phoneExtensions = self.person?.phoneExtensions.map { PersonDetailsCell(key: "Office".localized, value: $0.phoneNumber, actionType: .call) } ?? [] - - let organisations = self.person?.organisations.map { PersonDetailsCell(key: "Organisation".localized, value: $0.name, actionType: .none) } ?? [] - - let rooms = self.person?.rooms.map { PersonDetailsCell(key: "Room".localized, value: $0.shortLocationDescription, actionType: .showRoom) } ?? [] - - self.sections = [ - PersonDetailsSection(name: "Header", cells: [header]), - PersonDetailsSection(name: "General", cells: general), - PersonDetailsSection(name: "Official Contact", cells: officialContact), - PersonDetailsSection(name: "Private Contact", cells: privateContact), - PersonDetailsSection(name: "Phone Extensions", cells: phoneExtensions), - PersonDetailsSection(name: "Organisations", cells: organisations), - PersonDetailsSection(name: "Rooms", cells: rooms) - ].filter { !$0.cells.isEmpty } } var cnContact: CNMutableContact { - guard let person = self.person else { return CNMutableContact() } + guard case .success(let personDetails) = state else { + return CNMutableContact() + } let contact = CNMutableContact() contact.contactType = .person - if let title = person.title { + if let title = personDetails.title { contact.namePrefix = title } - contact.givenName = person.firstName - contact.familyName = person.name + contact.givenName = personDetails.firstName + contact.familyName = personDetails.name - contact.emailAddresses = [CNLabeledValue(label: CNLabelWork, value: person.email as NSString)] - if let organisation = person.organisations.first { + contact.emailAddresses = [CNLabeledValue(label: CNLabelWork, value: personDetails.email as NSString)] + if let organisation = personDetails.organisations.first { contact.departmentName = organisation.name } - var phoneNumbers: [CNLabeledValue] = person.privateContact.compactMap { info in + var phoneNumbers: [CNLabeledValue] = personDetails.privateContact.compactMap { info in switch info { case .phone(let number), .mobilePhone(let number) : return CNLabeledValue(label: CNLabelWork, value: CNPhoneNumber(stringValue: number)) default: return nil } } - phoneNumbers.append(contentsOf: person.officialContact.compactMap { info in + phoneNumbers.append(contentsOf: personDetails.officialContact.compactMap { info in switch info { case .phone(let number), .mobilePhone(let number) : return CNLabeledValue(label: CNLabelWork, value: CNPhoneNumber(stringValue: number)) default: return nil } }) - phoneNumbers.append(contentsOf: person.phoneExtensions.map { phoneExtension in + phoneNumbers.append(contentsOf: personDetails.phoneExtensions.map { phoneExtension in return CNLabeledValue(label: CNLabelWork, value: CNPhoneNumber(stringValue: phoneExtension.phoneNumber)) }) contact.phoneNumbers = phoneNumbers - if let imageData = person.image?.jpegData(compressionQuality: 1) { + if let imageData = personDetails.image?.jpegData(compressionQuality: 1) { contact.imageData = imageData } - var urls: [CNLabeledValue] = person.privateContact.compactMap{ info in + var urls: [CNLabeledValue] = personDetails.privateContact.compactMap{ info in switch info { case .homepage(let urlString): return CNLabeledValue(label: CNLabelWork, value: urlString as NSString) default: return nil } } - urls.append(contentsOf: person.officialContact.compactMap { info in + urls.append(contentsOf: personDetails.officialContact.compactMap { info in switch info { case .homepage(let urlString): return CNLabeledValue(label: CNLabelWork, value: urlString as NSString) default: return nil @@ -211,7 +121,7 @@ class PersonDetailedViewModel: ObservableObject { contact.urlAddresses = urls contact.organizationName = "TUM" - if let room = person.rooms.first { + if let room = personDetails.rooms.first { contact.note = room.locationDescription } diff --git a/Campus-iOS/PersonSearchComponent/Screen/PersonSearchScreen.swift b/Campus-iOS/PersonSearchComponent/Screen/PersonSearchScreen.swift new file mode 100644 index 00000000..c717f989 --- /dev/null +++ b/Campus-iOS/PersonSearchComponent/Screen/PersonSearchScreen.swift @@ -0,0 +1,74 @@ +// +// PersonSearchView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import SwiftUI + +struct PersonSearchScreen: View { + @StateObject var vm: PersonSearchViewModel + @State var searchText = "" + + init(model: Model) { + self._vm = StateObject(wrappedValue: PersonSearchViewModel(model: model, service: PersonSearchService())) + } + + init(model: Model, findPerson: String) { + self._vm = StateObject(wrappedValue: PersonSearchViewModel(model: model, service: PersonSearchService())) + self._searchText = State(wrappedValue: findPerson) + } + + var body: some View { + Group { + switch vm.state { + case .success(let persons): + VStack { + PersonSearchView(model: vm.model, persons: persons) + .background(Color(.systemGroupedBackground)) + } + case .loading: + LoadingView(text: "Fetching Persons") + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: {_ in await vm.getPersons(for: self.searchText, forcedRefresh: true)} + ) + case .na: + EmptyView() + } + } + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .onChange(of: self.searchText) { query in + Task { + await vm.getPersons(for: query, forcedRefresh: true) + } + } + .task { + if !searchText.isEmpty { + await vm.getPersons(for: searchText, forcedRefresh: true) + } + } + .alert( + "Error while fetching Persons", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getPersons(for: self.searchText, forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMOnlineAPIError { + Text(apiError.errorDescription ?? "TUMOnlineAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } +} diff --git a/Campus-iOS/PersonSearchComponent/Service/PersonSearchService.swift b/Campus-iOS/PersonSearchComponent/Service/PersonSearchService.swift new file mode 100644 index 00000000..a8432771 --- /dev/null +++ b/Campus-iOS/PersonSearchComponent/Service/PersonSearchService.swift @@ -0,0 +1,16 @@ +// +// PersonSearchService.swift +// Campus-iOS +// +// Created by David Lin on 21.01.23. +// + +import Foundation + +struct PersonSearchService { + func fetch(for query: String, token: String, forcedRefresh: Bool) async throws -> [Person] { + let response : TUMOnlineAPI.Response = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.personSearch(search: query), token: token, forcedRefresh: forcedRefresh) + + return response.row + } +} diff --git a/Campus-iOS/PersonSearchComponent/View/PersonSearchListView.swift b/Campus-iOS/PersonSearchComponent/View/PersonSearchListView.swift deleted file mode 100644 index 8956bf62..00000000 --- a/Campus-iOS/PersonSearchComponent/View/PersonSearchListView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// PersonSearchListView.swift -// Campus-iOS -// -// Created by Milen Vitanov on 13.02.22. -// - -import SwiftUI - -struct PersonSearchListView: View { - - @Environment(\.isSearching) private var isSearching - @ObservedObject var viewModel: PersonSearchViewModel - - var body: some View { - List { - ForEach(self.viewModel.result, id: \.nr) { person in - NavigationLink( - destination: PersonDetailedView(withPerson: person) - .navigationBarTitleDisplayMode(.inline) - ) { - Text(person.fullName) - } - } - if viewModel.errorMessage != "" { - VStack { - Spacer() - Text(self.viewModel.errorMessage).foregroundColor(.gray) - Spacer() - } - } - } - .onChange(of: isSearching) { newValue in - if !newValue { - self.viewModel.result = [] - } - } - } -} - -struct PersonSearchListView_Previews: PreviewProvider { - static var previews: some View { - PersonSearchListView(viewModel: PersonSearchViewModel()) - } -} diff --git a/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift b/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift index 89109bae..83565638 100644 --- a/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift +++ b/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift @@ -1,34 +1,33 @@ // -// PersonSearchView.swift +// PersonSearchListView.swift // Campus-iOS // -// Created by Milen Vitanov on 06.02.22. +// Created by Milen Vitanov on 13.02.22. // import SwiftUI struct PersonSearchView: View { - - @Environment(\.isSearching) private var isSearching - - @ObservedObject var viewModel = PersonSearchViewModel() - @State var searchText = "" + let model: Model + let persons: [Person] var body: some View { - PersonSearchListView(viewModel: self.viewModel) - .background(Color(.systemGroupedBackground)) - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) - .onChange(of: self.searchText) { searchValue in - if searchValue.count > 3 { - self.viewModel.fetch(searchString: searchValue) + List { + ForEach(self.persons, id: \.nr) { person in + NavigationLink( + destination: + PersonDetailedScreen(model: model, person: person) + .navigationBarTitleDisplayMode(.inline) + ) { + Text(person.fullName) } } - .animation(.default, value: self.viewModel.result) + } } } struct PersonSearchView_Previews: PreviewProvider { static var previews: some View { - PersonSearchView() + PersonSearchView(model: Model(), persons: []) } } diff --git a/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift b/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift index df2750c2..0fcc9d7e 100644 --- a/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift +++ b/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift @@ -9,30 +9,39 @@ import Foundation import Alamofire import XMLCoder +@MainActor class PersonSearchViewModel: ObservableObject { - @Published var result: [Person] = [] - @Published var errorMessage: String = "" + @Published var state: APIState<[Person]> = .na + @Published var hasError: Bool = false - private let sessionManager = Session.defaultSession + let model: Model + let service: PersonSearchService - func fetch(searchString: String) { - // activate only when more than 3 characters - - let endpoint = TUMOnlineAPI.personSearch(search: searchString) - sessionManager.cancelAllRequests() - let request = sessionManager.request(endpoint) - request.responseDecodable(of: TUMOnlineAPIResponse.self, decoder: XMLDecoder()) { [weak self] response in - guard !request.isCancelled else { - // cancelAllRequests doesn't seem to cancel all requests, so better check for this explicitly - return - } - self?.result = response.value?.rows ?? [] + init(model: Model, service: PersonSearchService) { + self.model = model + self.service = service + } + + func getPersons(for query: String, forcedRefresh: Bool) async { + if !forcedRefresh { + self.state = .loading + } + self.hasError = false - if let result = self?.result, result.isEmpty { - self?.errorMessage = NSString(format: "Unable to find person".localized as NSString, searchString) as String - } else { - self?.errorMessage = "" - } + guard let token = self.model.token else { + self.state = .failed(error: NetworkingError.unauthorized) + self.hasError = true + return + } + + do { + self.state = .success( + data: try await service.fetch(for: query, token: token, forcedRefresh: forcedRefresh) + ) + + } catch { + self.state = .failed(error: error) + self.hasError = true } } } diff --git a/Campus-iOS/ProfileComponent/Entity/Profile.swift b/Campus-iOS/ProfileComponent/Entity/Profile.swift index afed5a54..fb2ef368 100644 --- a/Campus-iOS/ProfileComponent/Entity/Profile.swift +++ b/Campus-iOS/ProfileComponent/Entity/Profile.swift @@ -6,8 +6,9 @@ // import Foundation +import SwiftUI -struct Profile: Decodable, Entity { +struct Profile: Decodable { let firstname: String? let obfuscatedID: String? let obfuscatedIDEmployee: String? @@ -31,6 +32,8 @@ struct Profile: Decodable, Entity { "\(self.firstname?.appending(" ") ?? "")\(self.surname?.appending(" ") ?? "")" } + var image: Image? + /* ga94zuh @@ -65,7 +68,7 @@ struct Profile: Decodable, Entity { } } - init(firstname: String?, surname: String?, tumId: String?, obfuscatedID: String?, obfuscatedIDEmployee: String?, obfuscatedIDExtern: String?, obfuscatedIDStudent: String?) { + init(firstname: String?, surname: String?, tumId: String?, obfuscatedID: String?, obfuscatedIDEmployee: String?, obfuscatedIDExtern: String?, obfuscatedIDStudent: String?, image: Image?) { self.firstname = firstname self.surname = surname self.obfuscatedID = obfuscatedID @@ -73,6 +76,7 @@ struct Profile: Decodable, Entity { self.obfuscatedIDExtern = obfuscatedIDExtern self.obfuscatedIDStudent = obfuscatedIDStudent self.tumID = tumId + self.image = image } init(from decoder: Decoder) throws { @@ -93,5 +97,6 @@ struct Profile: Decodable, Entity { self.obfuscatedIDExtern = obfuscatedIDExtern self.obfuscatedIDStudent = obfuscatedIDStudent self.firstname = firstname + self.image = nil } } diff --git a/Campus-iOS/ProfileComponent/Entity/Tuition.swift b/Campus-iOS/ProfileComponent/Entity/Tuition.swift index 58dc2bbc..34d1993d 100644 --- a/Campus-iOS/ProfileComponent/Entity/Tuition.swift +++ b/Campus-iOS/ProfileComponent/Entity/Tuition.swift @@ -7,7 +7,7 @@ import Foundation -struct Tuition: Entity { +struct Tuition: Decodable { var amount: NSDecimalNumber? var deadline: Date? diff --git a/Campus-iOS/ProfileComponent/Service/ProfileService.swift b/Campus-iOS/ProfileComponent/Service/ProfileService.swift new file mode 100644 index 00000000..576ef35a --- /dev/null +++ b/Campus-iOS/ProfileComponent/Service/ProfileService.swift @@ -0,0 +1,42 @@ +// +// ProfileService.swift +// Campus-iOS +// +// Created by David Lin on 22.01.23. +// + +import Foundation +import SwiftUI +import UIKit + +struct ProfileService { + func fetch(token: String, forcedRefresh: Bool) async throws -> Profile? { + let response : TUMOnlineAPI.Response = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.identify, token: token, forcedRefresh: forcedRefresh) + + return response.row.first + } + + func fetch(token: String, forcedRefresh: Bool) async throws -> Tuition? { + let response : TUMOnlineAPI.Response = try await MainAPI.makeRequest(endpoint: TUMOnlineAPI.tuitionStatus, token: token, forcedRefresh: forcedRefresh) + + return response.row.first + } + + struct superImage: Decodable { + let value: Image? + + enum CodingKeys: String, CodingKey { + case imageData = "" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let imageString = try container.decodeIfPresent(String.self, forKey: .imageData), let imageData = Data(base64Encoded: imageString, options: [.ignoreUnknownCharacters]), let uiImage = UIImage(data: imageData) { + self.value = Image(uiImage: uiImage) + } else { + self.value = nil + } + } + } +} diff --git a/Campus-iOS/ProfileComponent/View/ProfileMyTumSection.swift b/Campus-iOS/ProfileComponent/View/ProfileMyTumSection.swift deleted file mode 100644 index df25bfd9..00000000 --- a/Campus-iOS/ProfileComponent/View/ProfileMyTumSection.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ProfileTuitionNavigationLink.swift -// Campus-iOS -// -// Created by Milen Vitanov on 13.02.22. -// - -import SwiftUI - -struct ProfileMyTumSection: View { - - @EnvironmentObject private var model: Model - - var formattedAmount: String { - guard let amount = self.model.profile.tuition?.amount else { - return "n/a" - } - return OpenTuitionAmountView.currencyFormatter.string(from: amount) ?? "n/a" - } - - var body: some View { - Section("MY TUM") { - NavigationLink(destination: TuitionView(viewModel: self.model.profile).navigationBarTitle(Text("Tuition fees"))) { - Label { - HStack { - Text("Tuition fees") - if let isOpenAmount = self.model.profile.tuition?.isOpenAmount, isOpenAmount != true { - Spacer() - Text("✅") - } else { - Spacer() - Text(self.formattedAmount).foregroundColor(.red) - } - } - } icon: { - Image(systemName: "eurosign.circle") - } - } - .disabled(!self.model.isUserAuthenticated) - - NavigationLink(destination: PersonSearchView().navigationBarTitle(Text("Person Search")).navigationBarTitleDisplayMode(.large)) { - Label("Person Search", systemImage: "magnifyingglass") - } - .disabled(!self.model.isUserAuthenticated) - - NavigationLink(destination: LectureSearchView(model: model).navigationBarTitle(Text("Lecture Search")).navigationBarTitleDisplayMode(.large)) { - Label("Lecture Search", systemImage: "brain.head.profile") - } - .disabled(!self.model.isUserAuthenticated) - } - } -} - -struct ProfileMyTumSection_Previews: PreviewProvider { - static var previews: some View { - ProfileMyTumSection() - } -} diff --git a/Campus-iOS/ProfileComponent/View/ProfileToolbar.swift b/Campus-iOS/ProfileComponent/View/ProfileToolbar.swift index 36beaffb..b4be5f3d 100644 --- a/Campus-iOS/ProfileComponent/View/ProfileToolbar.swift +++ b/Campus-iOS/ProfileComponent/View/ProfileToolbar.swift @@ -8,14 +8,15 @@ import SwiftUI struct ProfileToolbar: View { - @ObservedObject var model: Model + @StateObject var model: Model + @State var showProfile = false var body: some View { - Button(action: {model.showProfile.toggle()}) { + Button(action: {self.showProfile.toggle()}) { Image(systemName: "person.crop.circle") } - .sheet(isPresented: $model.showProfile) { + .sheet(isPresented: $showProfile) { ProfileView(model: model) } diff --git a/Campus-iOS/ProfileComponent/View/ProfileView.swift b/Campus-iOS/ProfileComponent/View/ProfileView.swift index dbdc579e..db8157ad 100644 --- a/Campus-iOS/ProfileComponent/View/ProfileView.swift +++ b/Campus-iOS/ProfileComponent/View/ProfileView.swift @@ -8,82 +8,83 @@ import SwiftUI struct ProfileView: View { + @StateObject var vm: ProfileViewModel + @State var showActionSheet = false - @ObservedObject var model: Model @AppStorage("useBuildInWebView") var useBuildInWebView: Bool = true @AppStorage("calendarWeekDays") var calendarWeekDays: Int = 7 @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss @State var isWebViewShowed = false - @State var selectedLink: URL? = nil - + @State private var showSheet: Bool = false + @State private var url: URL? + + init(model: Model) { + self._vm = StateObject(wrappedValue: ProfileViewModel(model: model, service: ProfileService())) + self.url = .init(string: "https://google.com") + } + var body: some View { - NavigationView { - List { - NavigationLink(destination: PersonDetailedView(withProfile: self.model.profile.profile ?? ProfileViewModel.defaultProfile)) { - HStack(spacing: 24) { - self.model.profile.profileImage - .resizable() - .clipShape(Circle()) - .aspectRatio(contentMode: .fill) - .frame(width: 75, height: 75) - .foregroundColor(Color(.secondaryLabel)) + if case .success(let profile) = vm.profileState { + NavigationLink(destination: PersonDetailedScreen(model: self.vm.model, profile: profile)) { - VStack(alignment: .leading) { - if self.model.isUserAuthenticated { - Text(self.model.profile.profile?.fullName ?? "TUM Student") - .font(.title2) - } else { - Text("Not logged in") - .font(.title2) - } - - Text(self.model.profile.profile?.tumID ?? "TUM ID") - .font(.subheadline) - .foregroundColor(.gray) - } - } - .padding(.vertical, 6) - }.disabled(!self.model.isUserAuthenticated) + ProfileCell(model: self.vm.model, profile: profile) + }.disabled(!self.vm.model.isUserAuthenticated) + } else { + ProfileCell(model: self.vm.model, profile: ProfileViewModel.defaultProfile) + } - ProfileMyTumSection() + Section("MY TUM") { + TuitionScreen(vm: self.vm) + + NavigationLink(destination: PersonSearchScreen(model: self.vm.model).navigationBarTitle(Text("Person Search")).navigationBarTitleDisplayMode(.large)) { + Label("Person Search", systemImage: "magnifyingglass") + } + .disabled(!self.vm.model.isUserAuthenticated) + + NavigationLink(destination: LectureSearchScreen(model: vm.model).navigationBarTitle(Text("Lecture Search")).navigationBarTitleDisplayMode(.large)) { + Label("Lecture Search", systemImage: "brain.head.profile") + } + .disabled(!self.vm.model.isUserAuthenticated) + } Section("GENERAL") { - NavigationLink(destination: TUMSexyView().navigationBarTitle(Text("Useful Links"))) { + NavigationLink(destination: TUMSexyScreen().navigationBarTitle(Text("Useful Links"))) { Label("TUM.sexy", systemImage: "heart") } NavigationLink( - destination: RoomFinderView(model: self.model) + destination: NavigaTumView(model: self.vm.model) .navigationTitle(Text("Roomfinder")) .navigationBarTitleDisplayMode(.large) ) { Label("Roomfinder", systemImage: "rectangle.portrait.arrowtriangle.2.inward") } - NavigationLink(destination: NewsView(viewModel: NewsViewModel()) - .navigationBarTitle(Text("News")) - .navigationBarTitleDisplayMode(.large) + NavigationLink(destination: NewsScreen() + .navigationBarTitle(Text("News")) + .navigationBarTitleDisplayMode(.large) ) { Label("News", systemImage: "newspaper") } - NavigationLink(destination: MoviesView() - .navigationBarTitle(Text("Movies")) - .navigationBarTitleDisplayMode(.large) + NavigationLink(destination: MoviesScreen() + .navigationBarTitle(Text("Movies")) + .navigationBarTitleDisplayMode(.large) ) { Label("Movies", systemImage: "film") } - NavigationLink(destination: TokenPermissionsView(viewModel: TokenPermissionsViewModel(model: self.model), dismissWhenDone: true).navigationBarTitle("Check Permissions")) { - if self.model.isUserAuthenticated { + NavigationLink(destination: TokenPermissionsView(viewModel: TokenPermissionsViewModel(model: self.vm.model), dismissWhenDone: true).navigationBarTitle("Check Permissions")) { + if self.vm.model.isUserAuthenticated { Label("Token Permissions", systemImage: "key") } else { Label("Token Permissions (You are logged out)", systemImage: "key") } - }.disabled(!self.model.isUserAuthenticated) + }.disabled(!self.vm.model.isUserAuthenticated) } Section() { @@ -114,15 +115,18 @@ struct ProfileView: View { Section("GET IN CONTACT") { if self.useBuildInWebView { Button("Join Beta") { - self.selectedLink = URL(string: "https://testflight.apple.com/join/4Ddi6f2f") + self.url = URL(string: "https://testflight.apple.com/join/4Ddi6f2f")! + showSheet = true } Button("TUM Dev on Github") { - self.selectedLink = URL(string: "https://github.com/TUM-Dev") + self.url = URL(string: "https://github.com/TUM-Dev")! + showSheet = true } Button("TUM Dev Website") { - self.selectedLink = URL(string: "https://tum.app") + self.url = URL(string: "https://tum.app")! + showSheet = true } } else { Link(LocalizedStringKey("Join Beta"), destination: URL(string: "https://testflight.apple.com/join/4Ddi6f2f")!) @@ -136,7 +140,7 @@ struct ProfileView: View { let mailToString = "mailto:app@tum.de?subject=[IOS]&body=Hello I have an issue...".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) let mailToUrl = URL(string: mailToString!)! if UIApplication.shared.canOpenURL(mailToUrl) { - UIApplication.shared.open(mailToUrl, options: [:]) + UIApplication.shared.open(mailToUrl, options: [:]) } } } @@ -144,15 +148,15 @@ struct ProfileView: View { Section() { HStack(alignment: .bottom) { Spacer() - if model.isUserAuthenticated { + if vm.model.isUserAuthenticated { Button(action: { - model.logout() + vm.model.logout() }) { Text("Sign Out").foregroundColor(.red) } } else { Button(action: { - model.isLoginSheetPresented = true + vm.model.isLoginSheetPresented = true }) { Text("Sign In").foregroundColor(.green) } @@ -190,30 +194,67 @@ struct ProfileView: View { } .listRowBackground(Color.clear) } - .sheet(isPresented: $model.isLoginSheetPresented) { + .sheet(isPresented: $vm.model.isLoginSheetPresented) { NavigationView { - LoginView(model: model) + LoginView(model: vm.model) } } .navigationTitle("Profile") .navigationBarTitleDisplayMode(.inline) .toolbar { - Button(action: {model.showProfile.toggle()}) { + Button { + dismiss() + } label: { Text("Done").bold() } + } - .sheet(item: $selectedLink) { selectedLink in - if let link = selectedLink { - SFSafariViewWrapper(url: link) - } + .sheet(isPresented: $showSheet) { + if showSheet { SFSafariViewWrapper(url: url!) } } + }.task { + await vm.getProfile(forcedRefresh: false) } } } -struct ProfileView_Previews: PreviewProvider { +struct ProfileCell: View { + @StateObject var model: Model + let profile: Profile - static var previews: some View { - ProfileView(model: MockModel()).environmentObject(MockModel()) + var body: some View { + HStack(spacing: 24) { + if let image = profile.image { + image + .resizable() + .clipShape(Circle()) + .aspectRatio(contentMode: .fill) + .frame(width: 75, height: 75) + .foregroundColor(Color(.secondaryLabel)) + } else { + Image(systemName: "person.crop.circle.fill") + .resizable() + .clipShape(Circle()) + .aspectRatio(contentMode: .fill) + .frame(width: 75, height: 75) + .foregroundColor(Color(.secondaryLabel)) + } + + VStack(alignment: .leading) { + if self.model.isUserAuthenticated { + Text(profile.fullName) + .font(.title2) + Text(profile.tumID ?? "TUM ID") + .font(.subheadline) + .foregroundColor(.gray) + } else { + Text("Not logged in") + .font(.title2) + } + + + } + } + .padding(.vertical, 6) } } diff --git a/Campus-iOS/ProfileComponent/View/TuitionScreen.swift b/Campus-iOS/ProfileComponent/View/TuitionScreen.swift new file mode 100644 index 00000000..31570d47 --- /dev/null +++ b/Campus-iOS/ProfileComponent/View/TuitionScreen.swift @@ -0,0 +1,97 @@ +// +// ProfileTuitionNavigationLink.swift +// Campus-iOS +// +// Created by Milen Vitanov on 13.02.22. +// + +import SwiftUI + +struct TuitionScreen: View { + + @StateObject var vm: ProfileViewModel + + var body: some View { + Group { + switch vm.tuitionState { + case .success(let tuition): + NavigationLink(destination: TuitionView(tuition: tuition).navigationBarTitle(Text("Tuition fees"))) { + Label { + HStack { + Text("Tuition fees") + if !tuition.isOpenAmount { + Spacer() + Text("✅") + } else { + if let amount = tuition.amount, let formattedAmount = OpenTuitionAmountView.currencyFormatter.string(from: amount) { + Spacer() + Text(formattedAmount).foregroundColor(.red) + } else { + Text("Open amount couldn't be fetched.") + } + } + } + } icon: { + Image(systemName: "eurosign.circle") + } + } + case .loading, .na: + Text("Loading") + case .failed(error: let error): + Text(error.localizedDescription) + } + }.task { + await vm.getTuition(forcedRefresh: true) + } + } +} + +//struct ProfileMyTumSection: View { +// +// @EnvironmentObject private var model: Model +// +// var formattedAmount: String { +// guard let amount = self.model.profile.tuition?.amount else { +// return "n/a" +// } +// return OpenTuitionAmountView.currencyFormatter.string(from: amount) ?? "n/a" +// } +// +// var body: some View { +// Section("MY TUM") { +//// NavigationLink(destination: TuitionView(viewModel: self.model.profile).navigationBarTitle(Text("Tuition fees"))) { +//// Label { +//// HStack { +//// Text("Tuition fees") +//// if let isOpenAmount = self.model.profile.tuition?.isOpenAmount, isOpenAmount != true { +//// Spacer() +//// Text("✅") +//// } else { +//// Spacer() +//// Text(self.formattedAmount).foregroundColor(.red) +//// } +//// } +//// } icon: { +//// Image(systemName: "eurosign.circle") +//// } +//// } +//// .disabled(!self.model.isUserAuthenticated) +// +// NavigationLink(destination: PersonSearchScreen(model: self.model).navigationBarTitle(Text("Person Search")).navigationBarTitleDisplayMode(.large)) { +// Label("Person Search", systemImage: "magnifyingglass") +// } +// .disabled(!self.model.isUserAuthenticated) +// +// NavigationLink(destination: LectureSearchScreen(model: model).navigationBarTitle(Text("Lecture Search")).navigationBarTitleDisplayMode(.large)) { +// Label("Lecture Search", systemImage: "brain.head.profile") +// } +// .disabled(!self.model.isUserAuthenticated) +// } +// } +//} +// +//struct ProfileMyTumSection_Previews: PreviewProvider { +// static var previews: some View { +// ProfileMyTumSection() +// } +//} diff --git a/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift b/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift index dfa4b497..161e5d69 100644 --- a/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift +++ b/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift @@ -10,16 +10,15 @@ import Alamofire import XMLCoder import SwiftUI +@MainActor class ProfileViewModel: ObservableObject { + @Published var profileState: APIState = .na + @Published var profileHasError: Bool = false + @Published var tuitionState: APIState = .na + @Published var tuitionHasError: Bool = false - @Published var profile: Profile? - @Published var tuition: Tuition? - @Published var profileImage = Image(systemName: "person.crop.circle.fill") - - private let sessionManager = Session.defaultSession - - var profileState : ProfileState = .na - var tuitionState : TuitionState = .na + var model: Model + let service: ProfileService static let defaultProfile = Profile( firstname: nil, @@ -28,100 +27,235 @@ class ProfileViewModel: ObservableObject { obfuscatedID: nil, obfuscatedIDEmployee: nil, obfuscatedIDExtern: nil, - obfuscatedIDStudent: nil + obfuscatedIDStudent: nil, + image: nil ) - init() { - self.profile = Self.defaultProfile + init(model: Model, service: ProfileService) { + self.model = model + self.service = service } - init(model: Model) { - switch model.loginController.credentials { - case .none, .noTumID: - self.profile = Self.defaultProfile - case .tumID(_, _), .tumIDAndKey(_, _, _): - fetch() + func getProfile(forcedRefresh: Bool) async { + if !forcedRefresh { + self.profileState = .loading } - } - - func fetch(callback: @escaping (Result) -> Void = {_ in }) { - let importer = Importer, XMLDecoder>(endpoint: TUMOnlineAPI.identify) - importer.performFetch( handler: { result in - DispatchQueue.main.async { - switch result { - case .success(let storage): - self.profile = storage.rows?.first - if let personGroup = self.profile?.personGroup, let personId = self.profile?.id, let obfuscatedID = self.profile?.obfuscatedID { - self.downloadProfileImage(personGroup: personGroup, personId: personId, obfuscatedID: obfuscatedID) - } - - self.checkTuitionFunc() - - if let _ = self.profile { - callback(.success(true)) - } else { - callback(.failure(CampusOnlineAPI.Error.noPermission)) - } - case .failure(let error): - callback(.failure(error)) - print(error) - } + self.profileHasError = false + + guard let token = self.model.token else { + self.profileState = .failed(error: NetworkingError.unauthorized) + self.profileHasError = true + return + } + + do { + guard var profile: Profile = try await service.fetch(token: token, forcedRefresh: forcedRefresh) else { + self.profileState = .failed(error: TUMOnlineAPIError(message: "No profile found")) + return + } + + if let personGroup = profile.personGroup, let id = profile.id, let obfuscatedID = profile.obfuscatedID, let image = await downloadProfileImage(personGroup: personGroup, personId: id, obfuscatedID: obfuscatedID, forcedRefresh: forcedRefresh) { + profile.image = image } - }) + print(profile) + self.profileState = .success(data: profile) + } catch { + self.profileState = .failed(error: error) + self.profileHasError = true + } } - func downloadProfileImage(personGroup: String, personId: String, obfuscatedID: String) { - let imageRequest = TUMOnlineAPI.profileImage(personGroup: personGroup, id: personId) - self.sessionManager.request(imageRequest).responseData(completionHandler: { response in - if let imageData = response.value, let image = UIImage(data: imageData) { - self.profileImage = Image(uiImage: image) + func getTuition(forcedRefresh: Bool) async { + if !forcedRefresh { + self.tuitionState = .loading + } + self.tuitionHasError = false + + guard let token = self.model.token else { + self.tuitionState = .failed(error: NetworkingError.unauthorized) + self.tuitionHasError = true + return + } + + do { + guard let tuition: Tuition = try await service.fetch(token: token, forcedRefresh: forcedRefresh) else { + self.tuitionState = .failed(error: TUMOnlineAPIError(message: "No profile found")) return } - - self.sessionManager.request(TUMOnlineAPI.personDetails(identNumber: obfuscatedID)).responseDecodable(of: PersonDetails.self, decoder: XMLDecoder()) { response in - guard let image = response.value?.image else { return } - self.profileImage = Image(uiImage: image) - } - }) + + self.tuitionState = .success(data: tuition) + } catch { + self.tuitionState = .failed(error: error) + self.tuitionHasError = true + } } - func checkTuitionFunc(callback: @escaping (Result) -> Void = {_ in }) { + func downloadProfileImage(personGroup: String, personId: String, obfuscatedID: String, forcedRefresh: Bool = false) async -> Image? { + // Neu machen mit Alamofire (not async) + guard let token = self.model.token else { + return nil + } - let importerTuition = Importer, - XMLDecoder>(endpoint: TUMOnlineAPI.tuitionStatus, dateDecodingStrategy: .formatted(DateFormatter.yyyyMMdd)) + let endpoint = TUMOnlineAPI.profileImage(personGroup: personGroup, id: personId) - DispatchQueue.main.async { - importerTuition.performFetch(handler: { result in - switch result { - case .success(let storage): - self.tuition = storage.rows?.first - if let _ = self.tuition { - callback(.success(true)) - } else { - callback(.failure(CampusOnlineAPI.Error.noPermission)) - } - case .failure(let error): - callback(.failure(error)) - print(error) + if !forcedRefresh, let imageData = TUMOnlineAPI.imageCache.value(forKey: endpoint.basePathsParametersURL), let uiImage = UIImage(data: imageData) { + return Image(uiImage: uiImage) + } else { + var image: Image? + + TUMOnlineAPI.profileImage(personGroup: personGroup, id: personId).asRequest(token: token).responseData { response in + if let imageData = response.value, let uiImage = UIImage(data: imageData) { + TUMOnlineAPI.imageCache.setValue(imageData, forKey: endpoint.basePathsParametersURL, cost: imageData.count) + image = Image(uiImage: uiImage) + return + } + } + + if image != nil { + return image + } + + do { + let personDetails = try await PersonDetailedService().fetch(for: obfuscatedID, token: token, forcedRefresh: forcedRefresh) + guard let uiImage = personDetails.image else { + return nil } - }) + return Image(uiImage: uiImage) + } catch { + print(error) + return nil + } } - + + // let imageRequest = TUMOnlineAPI.profileImage(personGroup: personGroup, id: personId) + // + // self.sessionManager.request(imageRequest).responseData(completionHandler: { response in + // if let imageData = response.value, let image = UIImage(data: imageData) { + // return Image(uiImage: image) + // } + // + // self.sessionManager.request(TUMOnlineAPI.personDetails(identNumber: obfuscatedID)).responseDecodable(of: PersonDetails.self, decoder: XMLDecoder()) { response in + // guard let image = response.value?.image else { return } + // self.profileImage = Image(uiImage: image) + // } + // }) } } -extension ProfileViewModel { - enum ProfileState { - case na - case loading - case success(data: Profile?) - case failed(error: Error) - } - - enum TuitionState { - case na - case loading - case success(data: Tuition?) - case failed(error: Error) - } -} +//@MainActor +//class ProfileViewModel: ObservableObject { +// +// @Published var profile: Profile? +// @Published var tuition: Tuition? +// @Published var profileImage = Image(systemName: "person.crop.circle.fill") +// +// private let sessionManager = Session.defaultSession +// +// var profileState : ProfileState = .na +// var tuitionState : TuitionState = .na +// +// static let defaultProfile = Profile( +// firstname: nil, +// surname: "TUM Student".localized, +// tumId: "TUM ID", +// obfuscatedID: nil, +// obfuscatedIDEmployee: nil, +// obfuscatedIDExtern: nil, +// obfuscatedIDStudent: nil, +// image: nil +// ) +// +// init() { +// self.profile = Self.defaultProfile +// } +// +// init(model: Model) { +// switch model.loginController.credentials { +// case .none, .noTumID: +// self.profile = Self.defaultProfile +// case .tumID(_, _), .tumIDAndKey(_, _, _): +// fetch() +// } +// } +// +// func fetch(callback: @escaping (Result) -> Void = {_ in }) { +// let importer = Importer, XMLDecoder>(endpoint: TUMOnlineAPI.identify) +// importer.performFetch( handler: { result in +// DispatchQueue.main.async { +// switch result { +// case .success(let storage): +// self.profile = storage.rows?.first +// if let personGroup = self.profile?.personGroup, let personId = self.profile?.id, let obfuscatedID = self.profile?.obfuscatedID { +// self.downloadProfileImage(personGroup: personGroup, personId: personId, obfuscatedID: obfuscatedID) +// } +// +// self.checkTuitionFunc() +// +// if let _ = self.profile { +// callback(.success(true)) +// } else { +// callback(.failure(CampusOnlineAPI.Error.noPermission)) +// } +// case .failure(let error): +// callback(.failure(error)) +// print(error) +// } +// } +// }) +// } +// +// func downloadProfileImage(personGroup: String, personId: String, obfuscatedID: String) { +// let imageRequest = TUMOnlineAPI.profileImage(personGroup: personGroup, id: personId) +// self.sessionManager.request(imageRequest).responseData(completionHandler: { response in +// if let imageData = response.value, let image = UIImage(data: imageData) { +// self.profileImage = Image(uiImage: image) +// return +// } +// +// self.sessionManager.request(TUMOnlineAPI.personDetails(identNumber: obfuscatedID)).responseDecodable(of: PersonDetails.self, decoder: XMLDecoder()) { response in +// guard let image = response.value?.image else { return } +// self.profileImage = Image(uiImage: image) +// } +// }) +// } +// +// func checkTuitionFunc(callback: @escaping (Result) -> Void = {_ in }) { +// +// let importerTuition = Importer, +// XMLDecoder>(endpoint: TUMOnlineAPI.tuitionStatus, dateDecodingStrategy: .formatted(DateFormatter.yyyyMMdd)) +// +// DispatchQueue.main.async { +// importerTuition.performFetch(handler: { result in +// switch result { +// case .success(let storage): +// self.tuition = storage.rows?.first +// if let _ = self.tuition { +// callback(.success(true)) +// } else { +// callback(.failure(CampusOnlineAPI.Error.noPermission)) +// } +// case .failure(let error): +// callback(.failure(error)) +// print(error) +// } +// }) +// } +// +// } +//} + +//extension ProfileViewModel { +// enum ProfileState { +// case na +// case loading +// case success(data: Profile?) +// case failed(error: Error) +// } +// +// enum TuitionState { +// case na +// case loading +// case success(data: Tuition?) +// case failed(error: Error) +// } +//} diff --git a/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationAdditionalProperties.swift b/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationAdditionalProperties.swift new file mode 100644 index 00000000..932631ae --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationAdditionalProperties.swift @@ -0,0 +1,15 @@ +// +// NavigationAdditionalProperties.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumNavigationAdditionalProperties: Codable { + let properties: [NavigaTumNavigationProperty] + + enum CodingKeys: String, CodingKey { + case properties = "computed" + } +} diff --git a/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationCoordinates.swift b/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationCoordinates.swift new file mode 100644 index 00000000..37ba002e --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationCoordinates.swift @@ -0,0 +1,17 @@ +// +// NavigationCoordinates.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumNavigationCoordinates: Codable { + let latitude: Double + let longitude: Double + + enum CodingKeys: String, CodingKey { + case latitude = "lat" + case longitude = "lon" + } +} diff --git a/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationMaps.swift b/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationMaps.swift new file mode 100644 index 00000000..d1fc47bf --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Details/NavigaTumNavigationMaps.swift @@ -0,0 +1,13 @@ +// +// NavigationMaps.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumNavigationMaps: Codable { + let `default`: String + let roomfinder: NavigaTumRoomFinderMaps? + let overlays: NavigaTumOverlaysMaps? +} diff --git a/Campus-iOS/RoomFinder/Model/Details/NavigaTumOverlaysMaps.swift b/Campus-iOS/RoomFinder/Model/Details/NavigaTumOverlaysMaps.swift new file mode 100644 index 00000000..aca65f26 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Details/NavigaTumOverlaysMaps.swift @@ -0,0 +1,11 @@ +// +// NavigaTumOverlaysMaps.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 07.03.23. +// +import Foundation + +struct NavigaTumOverlaysMaps: Codable { + let available: [NavigaTumOverlayMap] +} diff --git a/Campus-iOS/RoomFinder/Model/Details/NavigaTumRoomFinderMaps.swift b/Campus-iOS/RoomFinder/Model/Details/NavigaTumRoomFinderMaps.swift new file mode 100644 index 00000000..eee574e2 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Details/NavigaTumRoomFinderMaps.swift @@ -0,0 +1,17 @@ +// +// RoomFinderMaps.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumRoomFinderMaps: Codable { + let available: [NavigaTumRoomFinderMap] + let defaultMapId: String + + enum CodingKeys: String, CodingKey { + case available + case defaultMapId = "default" + } +} diff --git a/Campus-iOS/RoomFinder/Model/NavigaTumNavigationDetails.swift b/Campus-iOS/RoomFinder/Model/NavigaTumNavigationDetails.swift new file mode 100644 index 00000000..a90070f6 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/NavigaTumNavigationDetails.swift @@ -0,0 +1,26 @@ +// +// NavigationDetails.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumNavigationDetails: Codable { + let id: String + let name: String + let parentNames: [String] + let type: String + let typeCommonName: String + let additionalProperties: NavigaTumNavigationAdditionalProperties + let coordinates: NavigaTumNavigationCoordinates + let maps: NavigaTumNavigationMaps + + enum CodingKeys: String, CodingKey { + case id, name, type, maps + case typeCommonName = "type_common_name" + case additionalProperties = "props" + case coordinates = "coords" + case parentNames = "parent_names" + } +} diff --git a/Campus-iOS/RoomFinder/Model/NavigaTumNavigationEntity.swift b/Campus-iOS/RoomFinder/Model/NavigaTumNavigationEntity.swift new file mode 100644 index 00000000..a3d2c502 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/NavigaTumNavigationEntity.swift @@ -0,0 +1,46 @@ +// +// NavigationEntity.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumNavigationEntity: Codable, Identifiable, Equatable { + let id: String + let type: String + let name: String + let subtext: String + let parsedId: String? + + enum CodingKeys: String, CodingKey { + case id, type, name, subtext + case parsedId = "parsed_id" + } + + func getFormattedName() -> String { + guard let parsedId = parsedId else { + return removeHighlight(name) + } + + return removeHighlight(parsedId) + " ➤ " + removeHighlight(name) + } + + func getFormattedSubtext() -> String { + return removeHighlight(subtext) + } + + private func removeHighlight(_ field: String) -> String { + /*** + * Info from NavigaTum swagger: https://editor.swagger.io/?url=https://raw.githubusercontent.com/TUM-Dev/navigatum/main/openapi.yaml + * In future maybe there will be query parameter for this + * "Some fields support highlighting the query terms and it uses DC3 (\x19 or \u{0019}) + * and DC1 (\x17 or \u{0017}) to mark the beginning/end of a highlighted sequence" + */ + field + .replacingOccurrences(of: "\u{0019}", with: "") + .replacingOccurrences(of: "\u{0017}", with: "") + .replacingOccurrences(of: "\\x19", with: "") + .replacingOccurrences(of: "\\x17", with: "") + } +} diff --git a/Campus-iOS/RoomFinder/Model/NavigaTumNavigationProperty.swift b/Campus-iOS/RoomFinder/Model/NavigaTumNavigationProperty.swift new file mode 100644 index 00000000..d3ac3123 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/NavigaTumNavigationProperty.swift @@ -0,0 +1,12 @@ +// +// NavigationProperty.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumNavigationProperty: Codable { + let name: String + let text: String +} diff --git a/Campus-iOS/RoomFinder/Model/NavigaTumOverlayMap.swift b/Campus-iOS/RoomFinder/Model/NavigaTumOverlayMap.swift new file mode 100644 index 00000000..fff98ce7 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/NavigaTumOverlayMap.swift @@ -0,0 +1,19 @@ +// +// NavigaTumOverlayMap.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 07.03.23. +// +import Foundation + +struct NavigaTumOverlayMap: Codable, Identifiable { + let id: Int + let floor: String + let imageUrl: String + let name: String + + enum CodingKeys: String, CodingKey { + case id, floor, name + case imageUrl = "file" + } +} diff --git a/Campus-iOS/RoomFinder/Model/NavigaTumRoomFinderMap.swift b/Campus-iOS/RoomFinder/Model/NavigaTumRoomFinderMap.swift new file mode 100644 index 00000000..2bd82022 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/NavigaTumRoomFinderMap.swift @@ -0,0 +1,23 @@ +// +// RoomFinderMap.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumRoomFinderMap: Codable, Identifiable { + let id: String + let name: String + let imageUrl: String // let baseMapUrl = "https://nav.tum.sexy/cdn/maps/roomfinder/" + let height: Int + let width: Int + let x: Int + let y: Int + let scale: String + + enum CodingKeys: String, CodingKey { + case id, name, height, width, x, y, scale + case imageUrl = "file" + } +} diff --git a/Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponse.swift b/Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponse.swift new file mode 100644 index 00000000..7fe40682 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponse.swift @@ -0,0 +1,16 @@ +// +// NavigaTumSearchResponse.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumSearchResponse: Codable { + let id = UUID() + let sections: [NavigaTumSearchResponseSection] + + enum CodingKeys: CodingKey { + case sections + } +} diff --git a/Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponseSection.swift b/Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponseSection.swift new file mode 100644 index 00000000..7cfbc175 --- /dev/null +++ b/Campus-iOS/RoomFinder/Model/Search/NavigaTumSearchResponseSection.swift @@ -0,0 +1,17 @@ +// +// NavigaTumSearchResponseSection.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation + +struct NavigaTumSearchResponseSection: Codable { + let type: String + let entries: [NavigaTumNavigationEntity] + + enum CodingKeys: String, CodingKey { + case type = "facet" + case entries + } +} diff --git a/Campus-iOS/RoomFinder/Service/RoomFinderService.swift b/Campus-iOS/RoomFinder/Service/RoomFinderService.swift new file mode 100644 index 00000000..60dfbb32 --- /dev/null +++ b/Campus-iOS/RoomFinder/Service/RoomFinderService.swift @@ -0,0 +1,25 @@ +// +// RoomFinderService.swift +// Campus-iOS +// +// Created by Philipp Zagar on 01.01.23. +// +import Foundation +import Alamofire + +protocol RoomFinderServiceProtocol { + func search(query: String) async throws -> NavigaTumSearchResponse + func details(id: String) async throws -> NavigaTumNavigationDetails +} + +struct RoomFinderService: RoomFinderServiceProtocol { + func search(query: String) async throws -> NavigaTumSearchResponse { + return try await MainAPI.makeRequest(endpoint: NavigaTUMAPI.search(query: query)) + } + + func details(id: String) async throws -> NavigaTumNavigationDetails { + let language = (Locale.current.languageCode == "de") ? "de" : "en" + + return try await MainAPI.makeRequest(endpoint: NavigaTUMAPI.details(id: id, language: language)) + } +} diff --git a/Campus-iOS/RoomFinder/ViewModel/NavigaTumDetailsViewModel.swift b/Campus-iOS/RoomFinder/ViewModel/NavigaTumDetailsViewModel.swift new file mode 100644 index 00000000..54593e42 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewModel/NavigaTumDetailsViewModel.swift @@ -0,0 +1,33 @@ +// +// NavigaTumDetailsViewModel.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 07.01.23. +// +import Foundation + +import Foundation +import Alamofire +import XMLCoder + +class NavigaTumDetailsViewModel: ObservableObject { + @Published var details: NavigaTumNavigationDetails? + @Published var errorMessage = "" + let id: String + + init(id: String) { + self.id = id + } + + @MainActor func fetchDetails() async { + guard !id.isEmpty else { + self.errorMessage = "Couldn't fetch room details" + return + } + do { + self.details = try await RoomFinderService().details(id: id) + } catch { + self.errorMessage = "Room finder service failed" + } + } +} diff --git a/Campus-iOS/RoomFinder/ViewModel/NavigaTumViewModel.swift b/Campus-iOS/RoomFinder/ViewModel/NavigaTumViewModel.swift new file mode 100644 index 00000000..084dc2f6 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewModel/NavigaTumViewModel.swift @@ -0,0 +1,27 @@ +// +// NavigaTumViewModel.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 07.01.23. +// +import Foundation +import Alamofire +import XMLCoder + +class NavigaTumViewModel: ObservableObject { + @Published var searchResults: [NavigaTumNavigationEntity] = [] + @Published var errorMessage: String = "" + + @MainActor func fetch(searchString: String) async { + guard !searchString.isEmpty else { + self.errorMessage = "" + return + } + do { + let results = try await RoomFinderService().search(query: searchString) + self.searchResults = results.sections.flatMap(\.entries) + } catch { + self.errorMessage = "Room Service Failed" + } + } +} diff --git a/Campus-iOS/RoomFinder/ViewModel/RoomFinderViewModel.swift b/Campus-iOS/RoomFinder/ViewModel/RoomFinderViewModel.swift index 56a15a38..0fd51698 100644 --- a/Campus-iOS/RoomFinder/ViewModel/RoomFinderViewModel.swift +++ b/Campus-iOS/RoomFinder/ViewModel/RoomFinderViewModel.swift @@ -9,35 +9,41 @@ import Foundation import Alamofire import XMLCoder +@MainActor class RoomFinderViewModel: ObservableObject { @Published var result: [FoundRoom] = [] @Published var errorMessage: String = "" - private let sessionManager = Session.defaultSession - - func fetch(searchString: String) { + func fetch(searchString: String) async { guard !searchString.isEmpty else { - sessionManager.cancelAllRequests() self.errorMessage = "" return } - let endpoint = TUMCabeAPI.roomSearch(query: searchString) - sessionManager.cancelAllRequests() - let request = sessionManager.request(endpoint) - request.responseDecodable(of: [FoundRoom].self, decoder: JSONDecoder()) { [weak self] response in - guard !request.isCancelled else { - // cancelAllRequests doesn't seem to cancel all requests, so better check for this explicitly - return - } - - self?.result = response.value ?? [] - - if let result = self?.result, result.isEmpty { - self?.errorMessage = NSString(format: "Unable to find room".localized as NSString, searchString) as String - } else { - self?.errorMessage = "" - } + do { + self.result = try await MainAPI.makeRequest(endpoint: TUMCabeAPI.roomSearch(query: searchString)) + self.errorMessage = "" + } catch { + print(error) + self.errorMessage = NSString(format: "Unable to find room".localized as NSString, searchString) as String } + +// let endpoint = TUMCabeAPI.roomSearch(query: searchString) +// sessionManager.cancelAllRequests() +// let request = sessionManager.request(endpoint) +// request.responseDecodable(of: [FoundRoom].self, decoder: JSONDecoder()) { [weak self] response in +// guard !request.isCancelled else { +// // cancelAllRequests doesn't seem to cancel all requests, so better check for this explicitly +// return +// } +// +// self?.result = response.value ?? [] +// +// if let result = self?.result, result.isEmpty { +// self?.errorMessage = NSString(format: "Unable to find room".localized as NSString, searchString) as String +// } else { +// self?.errorMessage = "" +// } +// } } } diff --git a/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsBaseView.swift b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsBaseView.swift new file mode 100644 index 00000000..8bae73c5 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsBaseView.swift @@ -0,0 +1,58 @@ +// +// NavigaTumDetailsBaseView.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 10.01.23. +// +import SwiftUI + +struct NavigaTumDetailsBaseView: View { + @State var chosenRoom: NavigaTumNavigationDetails + + var body: some View { + GroupBox( + label: GroupBoxLabelView( + iconName: "info.circle.fill", + text: "Room Details".localized + ) + .padding(.bottom, 10) + ) { + VStack(alignment: .leading, spacing: 8) { + ForEach(chosenRoom.additionalProperties.properties, id: \.name) { property in + LectureDetailsBasicInfoRowView(iconName: iconName(property.name), text: property.text) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame( + maxWidth: .infinity, + alignment: .topLeading + ) + } + + func iconName(_ name: String) -> String { + if name == "Roomcode" || name == "Raumkennung" { + return "qrcode.viewfinder" + } else if name == "Architect's name" || name == "Architekten-Name" || name == "Gebäudekennung" || name == "Buildingcode" { + return "building.columns" + } else if name == "Address" || name == "Adresse" { + return "location.fill" + } else if name == "Stockwerk" || name == "Floor" { + return "figure.stairs" + } else if name == "Sitzplätze" || name == "Seats" { + return "person.2.fill" + } else if name == "Anzahl Räume" || name == "Number of rooms"{ + return "door.right.hand.closed" + } else if name == "Anzahl Gebäude" || name == "Number of buildings" { + return "building.2" + } else { + return "questionmark.circle" + } + } +} + +struct NavigaTumDetailsBaseView_Previews: PreviewProvider { + static var previews: some View { + NavigaTumDetailsBaseView(chosenRoom: NavigaTumDetailsView_Previews.chosenRoom) + } +} diff --git a/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsView.swift b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsView.swift new file mode 100644 index 00000000..8dd2e0b2 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumDetailsView.swift @@ -0,0 +1,66 @@ +// +// NavigaTumDetailsView.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 07.01.23. +// +import SwiftUI + + +struct NavigaTumDetailsView: View { + @StateObject var viewModel: NavigaTumDetailsViewModel + + var body: some View { + ScrollView { + Text(viewModel.errorMessage) + if let chosenRoom = viewModel.details { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { + Text(chosenRoom.name) + .font(.title) + .multilineTextAlignment(.leading) + Text(chosenRoom.typeCommonName) + .font(.subheadline) + } + + Spacer().frame(height: 30) + + NavigaTumDetailsBaseView(chosenRoom: chosenRoom) + NavigaTumMapView(chosenRoom: chosenRoom) + // Unsure how to show images/overlays that provide useful information + // NavigaTumMapImagesView(chosenRoom: chosenRoom) + } + .frame( + maxWidth: .infinity, + alignment: .topLeading + ) + .padding(.horizontal) + } + } + .navigationBarTitleDisplayMode(.inline) + .task { + await viewModel.fetchDetails() + try? await Task.sleep(nanoseconds: 2_000_000_000) + viewModel.details?.additionalProperties.properties.forEach { entry in + print(entry.name) + } + } + } +} + +struct NavigaTumDetailsView_Previews: PreviewProvider { + static let props = [NavigaTumNavigationProperty(name: "Roomcode", text: "5606.EG.036"), NavigaTumNavigationProperty(name: "Architect's name", text: "00.06.036"), NavigaTumNavigationProperty(name: "Address", text: "Boltzmannstr. 3, EG, 85748 Garching b. München")] + static let additionalProperties = NavigaTumNavigationAdditionalProperties(properties: props) + static let coords = NavigaTumNavigationCoordinates(latitude: 48.26217845031176, longitude: 11.668693278105701) + static let available = [NavigaTumRoomFinderMap(id: "rf95", name: "FMI Garching BT06 EG", imageUrl: "rf95.webp", height: 605, width: 318, x: 207, y: 217, scale: "500"), + NavigaTumRoomFinderMap(id: "rf142", name: "FMI Übersicht", imageUrl: "rf142.webp", height: 461, width: 639, x: 443, y: 242, scale: "2000"), + NavigaTumRoomFinderMap(id: "rf80", name: "Lageplan Campus Garching", imageUrl: "rf80.webp", height: 480, width: 676, x: 329, y: 344, scale: "10000"), + NavigaTumRoomFinderMap(id: "rf54", name: "München", imageUrl: "rf54.webp", height: 603, width: 640, x: 444, y: 36, scale: "200000"), + NavigaTumRoomFinderMap(id: "rf156", name: "München und Umgebung", imageUrl: "rf156.webp", height: 515, width: 420, x: 265, y: 167, scale: "400000")] + static let maps = NavigaTumNavigationMaps(default: "rf95", roomfinder: NavigaTumRoomFinderMaps(available: available , defaultMapId: "rf95"), overlays: nil) + static var chosenRoom = NavigaTumNavigationDetails(id: "5606.EG.036", name: "5606.EG.036 (MPI Fachschaftsbüro im MI)", parentNames: ["Standorte", "Garching Forschungszentrum","Fakultät Mathematik & Informatik (FMI oder MI)", "Finger 06 (BT06)"], type: "room", typeCommonName: "Office", additionalProperties: additionalProperties, coordinates: coords, maps: maps) + static var viewmodel = NavigaTumDetailsViewModel(id: "5606.EG.036") + static var previews: some View { + NavigaTumDetailsView(viewModel: viewmodel) + } +} diff --git a/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumListView.swift b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumListView.swift new file mode 100644 index 00000000..769905ea --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumListView.swift @@ -0,0 +1,47 @@ +// +// NavigaTumListView.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 06.01.23. +// +import SwiftUI + +struct NavigaTumListView: View { + @StateObject var model: Model + @Environment(\.isSearching) private var isSearching + @ObservedObject var viewModel: NavigaTumViewModel + + var body: some View { + List { + ForEach(viewModel.searchResults) { entry in + NavigationLink( + destination: NavigaTumDetailsView(viewModel: NavigaTumDetailsViewModel(id: entry.id)) + ) { + VStack(alignment: .leading) { + HStack { + Text(entry.name) + } + } + } + } + if viewModel.errorMessage != "" { + VStack { + Spacer() + Text(self.viewModel.errorMessage).foregroundColor(.gray) + Spacer() + } + } + } + .onChange(of: isSearching) { newValue in + if !newValue { + self.viewModel.searchResults = [] + } + } + } +} + +struct NavigaTumListView_Previews: PreviewProvider { + static var previews: some View { + NavigaTumListView(model: MockModel(), viewModel: NavigaTumViewModel()) + } +} diff --git a/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapImagesView.swift b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapImagesView.swift new file mode 100644 index 00000000..d4b9b662 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapImagesView.swift @@ -0,0 +1,86 @@ +// +// NavigaTumMapImagesView.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 07.02.23. +// +import SwiftUI + +struct NavigaTumMapImagesView: View { + + @State var chosenRoom: NavigaTumNavigationDetails + + var body: some View { + if let roomfinder = chosenRoom.maps.roomfinder { + GroupBox( + label: GroupBoxLabelView( + iconName: "photo.fill.on.rectangle.fill", + text: "Room".localized + ) + ) { + if roomfinder.available.count > 0 { + ScrollView (.horizontal, showsIndicators: true) { + HStack (spacing: 20) { + ForEach(roomfinder.available) { map in + GeometryReader { _ in + actualImage(id: map.imageUrl, isRoomFinderImage: true) + } + .frame(width: 200, height: 200) + Spacer(minLength: 1) + } + if let overlays = chosenRoom.maps.overlays { + ForEach(overlays.available) { map in + GeometryReader { _ in + actualImage(id: map.imageUrl, isRoomFinderImage: false) + } + .frame(width: 400, height: 200) + Spacer(minLength: 1) + } + } + } + } + .padding([.top, .bottom], 15) + } + } + } + } + + func actualImage(id: String, isRoomFinderImage: Bool) -> some View { + let path: String + if isRoomFinderImage { + path = NavigaTUMAPI.images(id: id).basePathsParametersURL + } else { + path = NavigaTUMAPI.overlayImages(id: id).basePathsParametersURL + } + + return AsyncImage(url: URL(string: path)) { image in + switch image { + case .empty: + ProgressView() + case .success(let image): + NavigationLink(destination: ImageFullScreenView(image: image)) { + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(minWidth: nil, idealWidth: nil, maxWidth: UIScreen.main.bounds.width, minHeight: nil, idealHeight: nil, maxHeight: UIScreen.main.bounds.height, alignment: .center) + .clipped() + .cornerRadius(10.0) + } + case .failure: + Image(systemName: "photo") + @unknown default: + // Since the AsyncImagePhase enum isn't frozen, + // we need to add this currently unused fallback + // to handle any new cases that might be added + // in the future: + EmptyView() + } + } + } +} + +struct NavigaTumMapImagesView_Previews: PreviewProvider { + static var previews: some View { + NavigaTumMapImagesView(chosenRoom: NavigaTumDetailsView_Previews.chosenRoom) + } +} diff --git a/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapView.swift b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapView.swift new file mode 100644 index 00000000..1b231d36 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumMapView.swift @@ -0,0 +1,51 @@ +// +// NavigaTumMapView.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 10.01.23. +// +import SwiftUI +import MapKit + +struct NavigaTumMapView: View { + @State var chosenRoom: NavigaTumNavigationDetails + + var body: some View { + GroupBox( + label: HStack { + GroupBoxLabelView( + iconName: "map.fill", + text: "Building".localized + ) + .padding(.bottom, 10) + Spacer() +// Button("Open in Maps") { +// // TODO: Update Info.plist to allow maps? +// let url = URL(string: "maps://?saddr&daddr=\(chosenRoom.coordinates.latitude), \(chosenRoom.coordinates.longitude)") +// if UIApplication.shared.canOpenURL(url!) { +// UIApplication.shared.open(url!, options: [:], completionHandler: nil) +// } +// } +// .font(.footnote) + } + ) { + let coords = CLLocationCoordinate2D(latitude: chosenRoom.coordinates.latitude, longitude: chosenRoom.coordinates.longitude) + let mapRegion = MKCoordinateRegion(center: coords , span: MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)) + Map(coordinateRegion: .constant(mapRegion), showsUserLocation: true, annotationItems: [RoomFinderLocation(coordinate: coords)]) { location in + MapMarker(coordinate: location.coordinate) + } + .frame(height: 360) + .cornerRadius(10) + } + .frame( + maxWidth: .infinity, + alignment: .topLeading + ) + } +} + +struct NavigaTumMapView_Previews: PreviewProvider { + static var previews: some View { + NavigaTumMapView(chosenRoom: NavigaTumDetailsView_Previews.chosenRoom) + } +} diff --git a/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumView.swift b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumView.swift new file mode 100644 index 00000000..57a10a12 --- /dev/null +++ b/Campus-iOS/RoomFinder/ViewNavigaTum/NavigaTumView.swift @@ -0,0 +1,42 @@ +// +// NavigaTumView.swift +// Campus-iOS +// +// Created by Atharva Mathapati on 06.01.23. +// +import SwiftUI + +struct NavigaTumView: View { + @ObservedObject var model: Model + @StateObject var viewModel = NavigaTumViewModel() + @State var searchText = "" + + var body: some View { + NavigaTumListView(model: self.model, viewModel: self.viewModel) + .background(Color(.systemGroupedBackground)) + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .onChange(of: self.searchText) { searchValue in + if searchValue.count > 3 { + Task { + await search(searchValue) + } + } + } + .task { + if !searchText.isEmpty { + await search(searchText) + } + } + .animation(.default, value: self.viewModel.searchResults) + } + + func search(_ searchValue: String) async { + await self.viewModel.fetch(searchString: searchValue) + } +} + +struct NavigaTumView_Previews: PreviewProvider { + static var previews: some View { + NavigaTumView(model: MockModel()) + } +} diff --git a/Campus-iOS/RoomFinder/Views/RoomFinderDetailsMapImagesView.swift b/Campus-iOS/RoomFinder/Views/RoomFinderDetailsMapImagesView.swift index 899cae2a..fe7fb82c 100644 --- a/Campus-iOS/RoomFinder/Views/RoomFinderDetailsMapImagesView.swift +++ b/Campus-iOS/RoomFinder/Views/RoomFinderDetailsMapImagesView.swift @@ -8,7 +8,7 @@ import SwiftUI struct RoomFinderDetailsMapImagesView: View { - + @StateObject var vm = StudyRoomViewModel() @State var room: FoundRoom var body: some View { @@ -21,13 +21,16 @@ struct RoomFinderDetailsMapImagesView: View { ) { Divider() - - MapImagesHorizontalScrollingView(viewModel: StudyRoomViewModel(studyRoom: StudyRoom(room: room))) + if case .success(let roomImageMapping) = vm.state { + MapImagesHorizontalScrollingView(room: StudyRoom(room: self.room), roomImageMapping: roomImageMapping) + } } .frame( maxWidth: .infinity, alignment: .topLeading - ) + ).task { + await vm.getRoomImageMapping(for: StudyRoom(room: self.room)) + } } } diff --git a/Campus-iOS/RoomFinder/Views/RoomFinderView.swift b/Campus-iOS/RoomFinder/Views/RoomFinderView.swift index 9e1b7239..e365c582 100644 --- a/Campus-iOS/RoomFinder/Views/RoomFinderView.swift +++ b/Campus-iOS/RoomFinder/Views/RoomFinderView.swift @@ -19,20 +19,22 @@ struct RoomFinderView: View { .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) .onChange(of: self.searchText) { searchValue in if searchValue.count > 3 { - search(searchValue) + Task { + await search(searchValue) + } } } - .onAppear { + .task { if !searchText.isEmpty { - search(searchText) + await search(searchText) } } .animation(.default, value: self.viewModel.result) } - func search(_ searchValue: String) { - self.viewModel.fetch(searchString: searchValue) + func search(_ searchValue: String) async { + await self.viewModel.fetch(searchString: searchValue) } } diff --git a/Campus-iOS/TUMSexyComponent/Model/TUMSexyLink.swift b/Campus-iOS/TUMSexyComponent/Model/TUMSexyLink.swift new file mode 100644 index 00000000..2a58b910 --- /dev/null +++ b/Campus-iOS/TUMSexyComponent/Model/TUMSexyLink.swift @@ -0,0 +1,21 @@ +// +// TUMSexyLink.swift +// Campus-iOS +// +// Created by David Lin on 23.01.23. +// + +import Foundation + +struct TUMSexyLink: Decodable, Identifiable { + var id = UUID() + var description: String? + var target: String? + var moodleID: String? + + enum CodingKeys: String, CodingKey { + case description = "description" + case target = "target" + case moodleID = "moodleID" + } +} diff --git a/Campus-iOS/TUMSexyComponent/Screen/TUMSexyScreen.swift b/Campus-iOS/TUMSexyComponent/Screen/TUMSexyScreen.swift new file mode 100644 index 00000000..00ef9090 --- /dev/null +++ b/Campus-iOS/TUMSexyComponent/Screen/TUMSexyScreen.swift @@ -0,0 +1,55 @@ +// +// TUMSexyScreen.swift +// Campus-iOS +// +// Created by David Lin on 23.01.23. +// + +import SwiftUI + +struct TUMSexyScreen: View { + @StateObject var vm = TUMSexyViewModel() + + var body: some View { + Group { + switch vm.state { + case .success(let links): + VStack { + TUMSexyView(links: links) + .refreshable { + await vm.getLinks(forcedRefresh: true) + } + } + case .loading, .na: + LoadingView(text: "Fetching Links") + case .failed(let error): + FailedView( + errorDescription: error.localizedDescription, + retryClosure: vm.getLinks + ) + } + }.task { + await vm.getLinks() + } + .alert( + "Error while fetching Links", + isPresented: $vm.hasError, + presenting: vm.state) { detail in + Button("Retry") { + Task { + await vm.getLinks(forcedRefresh: true) + } + } + + Button("Cancel", role: .cancel) { } + } message: { detail in + if case let .failed(error) = detail { + if let apiError = error as? TUMSexyAPIError { + Text(apiError.errorDescription ?? "TUMSexyAPI Error") + } else { + Text(error.localizedDescription) + } + } + } + } +} diff --git a/Campus-iOS/TUMSexyComponent/Service/TUMSexyService.swift b/Campus-iOS/TUMSexyComponent/Service/TUMSexyService.swift new file mode 100644 index 00000000..ff7cf94b --- /dev/null +++ b/Campus-iOS/TUMSexyComponent/Service/TUMSexyService.swift @@ -0,0 +1,25 @@ +// +// TUMSexyService.swift +// Campus-iOS +// +// Created by David Lin on 23.01.23. +// + +import Foundation + +struct TUMSexyService: ServiceProtocol { + typealias T = TUMSexyLink + + func fetch(forcedRefresh: Bool) async throws -> [TUMSexyLink] { + let response: [String : TUMSexyLink] = try await MainAPI.makeRequest(endpoint: TUMSexyAPI.standard, forcedRefresh: forcedRefresh) + + var links = [TUMSexyLink]() + response.values.forEach { + if $0.target != nil && $0.description != nil { + links.append($0) + } + } + + return links + } +} diff --git a/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift b/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift index fc46160c..f1abc82d 100644 --- a/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift +++ b/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift @@ -5,40 +5,58 @@ // Created by Milen Vitanov on 13.01.22. // -import UIKit - -struct TUMSexyLink: Entity { - var description: String? - var target: String? - var moodleID: String? -} +import Foundation +@MainActor class TUMSexyViewModel: ObservableObject { - @Published var links: [TUMSexyLink] = [] - - typealias ImporterType = Importer - private let importer = ImporterType(endpoint: TUMSexyAPI()) - + @Published var state: APIState<[TUMSexyLink]> = .na + @Published var hasError: Bool = false - init() { - // TODO: Get from cache, if not found, then fetch - fetch() - } + let service = TUMSexyService() - func fetch() { - importer.performFetch( handler: { result in - switch result { - case .success(let storage): - var filledLinks = [TUMSexyLink]() - storage.values.forEach() { - if $0.target != nil && $0.description != nil { - filledLinks.append($0) - } - } - self.links = filledLinks - case .failure(let error): - print(error) - } - }) + func getLinks(forcedRefresh: Bool = false) async { + if !forcedRefresh { + self.state = .loading + } + self.hasError = false + + do { + self.state = .success( + data: try await service.fetch(forcedRefresh: forcedRefresh) + ) + } catch { + self.state = .failed(error: error) + self.hasError = true + } } } + +//class TUMSexyViewModel: ObservableObject { +// @Published var links: [TUMSexyLink] = [] +// +// typealias ImporterType = Importer +// private let importer = ImporterType(endpoint: TUMSexyAPI()) +// +// +// init() { +// // TODO: Get from cache, if not found, then fetch +// fetch() +// } +// +// func fetch() { +// importer.performFetch( handler: { result in +// switch result { +// case .success(let storage): +// var filledLinks = [TUMSexyLink]() +// storage.values.forEach() { +// if $0.target != nil && $0.description != nil { +// filledLinks.append($0) +// } +// } +// self.links = filledLinks +// case .failure(let error): +// print(error) +// } +// }) +// } +//} diff --git a/Campus-iOS/TUMSexyComponent/Views/TUMSexyView.swift b/Campus-iOS/TUMSexyComponent/Views/TUMSexyView.swift index 495b1fe2..3899a3b7 100644 --- a/Campus-iOS/TUMSexyComponent/Views/TUMSexyView.swift +++ b/Campus-iOS/TUMSexyComponent/Views/TUMSexyView.swift @@ -9,41 +9,52 @@ import SwiftUI struct TUMSexyView: View { - @ObservedObject var viewModel = TUMSexyViewModel() + let links: [TUMSexyLink] + @AppStorage("useBuildInWebView") var useBuildInWebView: Bool = true @State var isWebViewShowed = false + @State var shownLink: TUMSexyLink? = nil @State private var searchText = "" var body: some View { - List(searchResults, id: \.target) { link in - if useBuildInWebView { - Text(link.description ?? "") - .foregroundColor(.blue) - .onTapGesture { - isWebViewShowed.toggle() + List { + ForEach(searchResults, id: \.id) { link in + Group { + if useBuildInWebView { + Button { + self.shownLink = link + isWebViewShowed.toggle() + } label: { + Text(link.description ?? "") + }.foregroundColor(.blue) + } else { + Link(link.description ?? "", destination: URL(string: link.target ?? "")!) } - .sheet(isPresented: $isWebViewShowed, content: { - SFSafariViewWrapper(url: URL(string: link.target ?? "")!) - }) - } else { - Link(link.description ?? "", destination: URL(string: link.target ?? "")!) + } + +// } } .searchable(text: $searchText) .navigationTitle("Useful Links") + .sheet(item: $shownLink) { link in + if let target = link.target, let url = URL(string: target) { + SFSafariViewWrapper(url: url) + } + } } var searchResults: [TUMSexyLink] { if searchText.isEmpty { - return viewModel.links + return links } else { - return viewModel.links.filter { $0.description!.localizedLowercase.contains(searchText.localizedLowercase) } + return links.filter { $0.description!.localizedLowercase.contains(searchText.localizedLowercase) } } } } -struct TUMSexyView_Previews: PreviewProvider { - static var previews: some View { - TUMSexyView() - } -} +//struct TUMSexyView_Previews: PreviewProvider { +// static var previews: some View { +// TUMSexyView() +// } +//} diff --git a/Campus-iOS/TuitionComponent/View/TuitionView.swift b/Campus-iOS/TuitionComponent/View/TuitionView.swift index ff0742c9..e96eaf47 100644 --- a/Campus-iOS/TuitionComponent/View/TuitionView.swift +++ b/Campus-iOS/TuitionComponent/View/TuitionView.swift @@ -9,20 +9,17 @@ import SwiftUI struct TuitionView: View { - @ObservedObject var viewModel: ProfileViewModel + let tuition: Tuition @State private var data = AppUsageData() var body: some View { List { VStack(alignment: .center) { Spacer(minLength: 0.10 * UIScreen.main.bounds.width) - TuitionCard(tuition: self.viewModel.tuition ?? Tuition.unknown) + TuitionCard(tuition: self.tuition) } .listRowBackground(Color(.systemGroupedBackground)) } - .refreshable { - self.viewModel.checkTuitionFunc() - } .task { data.visitView(view: .tuition) } @@ -32,11 +29,11 @@ struct TuitionView: View { } } -struct TuitionView_Previews: PreviewProvider { - - static var previews: some View { - TuitionView(viewModel: ProfileViewModel()) - TuitionView(viewModel: ProfileViewModel()) - .preferredColorScheme(.dark) - } -} +//struct TuitionView_Previews: PreviewProvider { +// +// static var previews: some View { +// TuitionView(viewModel: ProfileViewModel()) +// TuitionView(viewModel: ProfileViewModel()) +// .preferredColorScheme(.dark) +// } +//} diff --git a/Campus-iOS/TuitionComponent/View/TuitionWidgetView.swift b/Campus-iOS/TuitionComponent/View/TuitionWidgetView.swift index c975a436..8b5fcc40 100644 --- a/Campus-iOS/TuitionComponent/View/TuitionWidgetView.swift +++ b/Campus-iOS/TuitionComponent/View/TuitionWidgetView.swift @@ -14,45 +14,56 @@ struct TuitionWidgetView: View { @State private var scale: CGFloat = 1 @Binding var refresh: Bool - init(size: TuitionWidgetSize, refresh: Binding = .constant(false)) { + let model: Model + + init(model: Model ,size: TuitionWidgetSize, refresh: Binding = .constant(false)) { + self.model = model self._size = State(initialValue: size.value) self.initialSize = size.value self._refresh = refresh } var body: some View { - WidgetFrameView(size: size, content: TuitionWidgetContent(size: size, refresh: $refresh)) + WidgetFrameView(size: size, content: TuitionWidgetContent(viewModel: ProfileViewModel(model: model, service: ProfileService()), size: size, refresh: $refresh)) .expandable(size: $size, initialSize: initialSize, biggestSize: .rectangle, scale: $scale) } } struct TuitionWidgetContent: View { - @StateObject var viewModel = ProfileViewModel() + @StateObject var viewModel: ProfileViewModel let size: WidgetSize @Binding var refresh: Bool var body: some View { Group { - if let tuition = self.viewModel.tuition, - let amount = tuition.amount, - let deadline = tuition.deadline, - let semester = tuition.semesterID { - TuitionWidgetInfoView( - amount: amount, - openAmount: tuition.isOpenAmount, - deadline: deadline, - semester: semester, - big: size != .square - ) - } else { - WidgetLoadingView(text: "Loading tuition fee") + switch viewModel.tuitionState { + case .success(let tuition): + if let amount = tuition.amount, + let deadline = tuition.deadline, + let semester = tuition.semesterID { + TuitionWidgetInfoView( + amount: amount, + openAmount: tuition.isOpenAmount, + deadline: deadline, + semester: semester, + big: size != .square + ) + } + case .loading, .na: + WidgetLoadingView(text: "Loading tuition fee") + case .failed(error: let error): + WidgetLoadingView(text: "Error: \(error)") } } .onChange(of: refresh) { _ in - viewModel.fetch() + Task { + await viewModel.getTuition(forcedRefresh: true) + } } .task { - viewModel.fetch() + Task { + await viewModel.getTuition(forcedRefresh: false) + } } } } diff --git a/Campus-iOS/WidgetComponent/Recommender/Strategy/MLModelDataHandler.swift b/Campus-iOS/WidgetComponent/Recommender/Strategy/MLModelDataHandler.swift index 5f2fc0a6..4e95d739 100644 --- a/Campus-iOS/WidgetComponent/Recommender/Strategy/MLModelDataHandler.swift +++ b/Campus-iOS/WidgetComponent/Recommender/Strategy/MLModelDataHandler.swift @@ -160,7 +160,7 @@ class MLModelDataHandler { private func groupedTimes(from data: [AppUsageDataEntity]) -> [[Date.Time]] { let times = data.compactMap { $0.startTime?.time } - var result = times.groups(where: { Date.Time.minutesBetween($0, $1) <= timeNearbyThreshold }) + let result = times.groups(where: { Date.Time.minutesBetween($0, $1) <= timeNearbyThreshold }) // Remove duplicate groups. return Array(Set(result)) @@ -168,7 +168,7 @@ class MLModelDataHandler { private func groupedDates(from data: [AppUsageDataEntity]) -> [[Date]] { let dates = data.compactMap { $0.startTime } - var result = dates.groups(where: { Calendar.current.dateComponents([.day], from: $0, to: $1).day! <= dateNearbyThreshold }) + let result = dates.groups(where: { Calendar.current.dateComponents([.day], from: $0, to: $1).day! <= dateNearbyThreshold }) // Remove duplicate groups. return Array(Set(result)) diff --git a/Campus-iOS/WidgetComponent/Recommender/WidgetRecommender.swift b/Campus-iOS/WidgetComponent/Recommender/WidgetRecommender.swift index 221c2067..1d06c262 100644 --- a/Campus-iOS/WidgetComponent/Recommender/WidgetRecommender.swift +++ b/Campus-iOS/WidgetComponent/Recommender/WidgetRecommender.swift @@ -40,7 +40,7 @@ class WidgetRecommender: ObservableObject { case .calendar: CalendarWidgetView(model: model, size: size, refresh: refresh) case .tuition: - TuitionWidgetView(size: TuitionWidgetSize.from(widgetSize: size), refresh: refresh) + TuitionWidgetView(model: model, size: TuitionWidgetSize.from(widgetSize: size), refresh: refresh) case .grades: GradeWidgetView(model: model, size: size, refresh: refresh) } diff --git a/Campus-iOS/WidgetComponent/Screen/WidgetScreen.swift b/Campus-iOS/WidgetComponent/Screen/WidgetScreen.swift index 9ded3a0c..ae3a86f4 100644 --- a/Campus-iOS/WidgetComponent/Screen/WidgetScreen.swift +++ b/Campus-iOS/WidgetComponent/Screen/WidgetScreen.swift @@ -11,13 +11,16 @@ import MapKit struct WidgetScreen: View { @StateObject private var recommender: WidgetRecommender - @StateObject var model: Model = Model() + var model: Model + var profileViewModel: ProfileViewModel @State private var refresh = false - @State private var widgetTitle = String() + @State private var widgetTitle = "" private let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() init(model: Model) { self._recommender = StateObject(wrappedValue: WidgetRecommender(strategy: SpatioTemporalStrategy(), model: model)) + self.model = model + self.profileViewModel = ProfileViewModel(model: model, service: ProfileService()) } var body: some View { @@ -29,7 +32,7 @@ struct WidgetScreen: View { case .success: ScrollView { self.generateContent( - views: recommender.recommendations.map { recommender.getWidget(for: $0.widget, size: $0.size(), refresh: $refresh) } + views: recommender.recommendations.map { recommender.getWidget(for: $0.widget, size: $0.size(), refresh: $refresh) }, widgetTitle: self.widgetTitle ) .frame(maxWidth: .infinity) } @@ -41,8 +44,13 @@ struct WidgetScreen: View { } .task { try? await recommender.fetchRecommendations() - if let firstName = model.profile.profile?.firstname { widgetTitle = "Hi, " + firstName } - else { widgetTitle = "Welcome"} + await profileViewModel.getProfile(forcedRefresh: false) + + if case .success(let profile) = profileViewModel.profileState, let firstname = profile.firstname { + self.widgetTitle = "Hi, " + firstname + } else { + self.widgetTitle = "Welcome" + } } .onReceive(timer) { _ in refresh.toggle() @@ -50,15 +58,19 @@ struct WidgetScreen: View { } // Source: https://stackoverflow.com/a/58876712 - private func generateContent(views: [T]) -> some View { + private func generateContent(views: [T], widgetTitle: String) -> some View { var width = CGFloat.zero var height = CGFloat.zero var previousHeight = CGFloat.zero let maxWidth = WidgetSize.bigSquare.dimensions.0 + 2 * WidgetSize.padding - if let firstName = model.profile.profile?.firstname { widgetTitle = "Hi, " + firstName } - else { widgetTitle = "Welcome"} + if case let .success(profile) = profileViewModel.profileState, + let firstName = profile.firstname { + self.widgetTitle = "Hi, " + firstName + } else { + self.widgetTitle = "Welcome" + } return ZStack(alignment: .topLeading) { ForEach(0..