diff --git a/Campus-iOS.xcodeproj/project.pbxproj b/Campus-iOS.xcodeproj/project.pbxproj index ab71b7c8..13324e2d 100644 --- a/Campus-iOS.xcodeproj/project.pbxproj +++ b/Campus-iOS.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 3616C4DB27904BD3000A1BC9 /* NewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3616C4DA27904BD3000A1BC9 /* NewsViewModel.swift */; }; 36203E7F2761B9D000C24658 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 36203E7D2761B9D000C24658 /* Localizable.strings */; }; 36203E832761BDD100C24658 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 36203E822761BDD100C24658 /* KeychainAccess */; }; - 36203E8B2761C6EC00C24658 /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36203E882761C6EC00C24658 /* Spinner.swift */; }; + 36203E8B2761C6EC00C24658 /* TUMSplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36203E882761C6EC00C24658 /* TUMSplashScreen.swift */; }; 36203E8C2761C6EC00C24658 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36203E892761C6EC00C24658 /* LoginView.swift */; }; 36203E8D2761C6EC00C24658 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36203E8A2761C6EC00C24658 /* Credentials.swift */; }; 3629BA2C27A1CECA0036AC80 /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3629BA2B27A1CECA0036AC80 /* NewsView.swift */; }; @@ -115,6 +115,24 @@ 36AF61EF27A2FD7800FEBD98 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61D427A2FD7800FEBD98 /* LoadingView.swift */; }; 36AF61F027A2FD7800FEBD98 /* DecoderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61D627A2FD7800FEBD98 /* DecoderProtocol.swift */; }; 36AF61F127A2FD7800FEBD98 /* XMLSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AF61D727A2FD7800FEBD98 /* XMLSerializer.swift */; }; + 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 */; }; + 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 */; }; + 36BB6F6A27AFD2A100F224AB /* PhoneExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F6927AFD2A100F224AB /* PhoneExtension.swift */; }; + 36BB6F6C27AFD2B900F224AB /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F6B27AFD2B900F224AB /* Room.swift */; }; + 36BB6F7027B1197400F224AB /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F6F27B1197400F224AB /* Profile.swift */; }; + 36BB6F7327B1CD9200F224AB /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F7227B1CD9200F224AB /* ProfileViewModel.swift */; }; + 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 */; }; + 36BB6F8627B39C5300F224AB /* LectureSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BB6F8527B39C5300F224AB /* LectureSearchViewModel.swift */; }; 36BBE72F27989F8C0018FD3F /* SFSafariViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BBE72E27989F8C0018FD3F /* SFSafariViewWrapper.swift */; }; 36BBE7322798AFE10018FD3F /* News.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BBE7312798AFE10018FD3F /* News.swift */; }; 36BBE7342798B04D0018FD3F /* NewsSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BBE7332798B04D0018FD3F /* NewsSource.swift */; }; @@ -218,7 +236,7 @@ 3616C4DA27904BD3000A1BC9 /* NewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewModel.swift; sourceTree = ""; }; 36203E7E2761B9D000C24658 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = "Campus-iOS/Base.lproj/Localizable.strings"; sourceTree = ""; }; 36203E802761B9F200C24658 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "Campus-iOS/de.lproj/Localizable.strings"; sourceTree = ""; }; - 36203E882761C6EC00C24658 /* Spinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = ""; }; + 36203E882761C6EC00C24658 /* TUMSplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TUMSplashScreen.swift; sourceTree = ""; }; 36203E892761C6EC00C24658 /* LoginView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 36203E8A2761C6EC00C24658 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; 3629BA2B27A1CECA0036AC80 /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = ""; }; @@ -268,6 +286,24 @@ 36AF61D427A2FD7800FEBD98 /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 36AF61D627A2FD7800FEBD98 /* DecoderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecoderProtocol.swift; sourceTree = ""; }; 36AF61D727A2FD7800FEBD98 /* XMLSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLSerializer.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 36BB6F6927AFD2A100F224AB /* PhoneExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneExtension.swift; sourceTree = ""; }; + 36BB6F6B27AFD2B900F224AB /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; + 36BB6F6F27B1197400F224AB /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; + 36BB6F7227B1CD9200F224AB /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; + 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 = ""; }; + 36BB6F8527B39C5300F224AB /* LectureSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureSearchViewModel.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 = ""; }; 36BBE7332798B04D0018FD3F /* NewsSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsSource.swift; sourceTree = ""; }; @@ -321,8 +357,9 @@ 100803442764E2A90013ED0E /* ProfileComponent */ = { isa = PBXGroup; children = ( - 100803452764E2C50013ED0E /* ProfileToolbar.swift */, - 100803472764E37A0013ED0E /* ProfileView.swift */, + 36BB6F7127B1CD8100F224AB /* ViewModel */, + 36BB6F6E27B1196300F224AB /* Entity */, + 36BB6F6D27B1195600F224AB /* View */, ); path = ProfileComponent; sourceTree = ""; @@ -638,6 +675,10 @@ 366F0E8227580CFB0091651D /* Campus-iOS */ = { isa = PBXGroup; children = ( + 36BB6F8027B39B2200F224AB /* LectureSearchComponent */, + 36BB6F7627B26DCA00F224AB /* TuitionComponent */, + 36BB6F5927AFCDF900F224AB /* PersonSearchComponent */, + 36BB6F5527AFCD7B00F224AB /* PersonDetailedComponent */, 36108C0327A307F9007DC62D /* GradesComponent */, 36108C0027A30762007DC62D /* Enums */, 36108BF127A30516007DC62D /* MoviesComponent */, @@ -789,6 +830,147 @@ path = Helpers; sourceTree = ""; }; + 36BB6F5527AFCD7B00F224AB /* PersonDetailedComponent */ = { + isa = PBXGroup; + children = ( + 36BB6F5827AFCD9C00F224AB /* View */, + 36BB6F5727AFCD9300F224AB /* ViewModel */, + 36BB6F5627AFCD8D00F224AB /* Entity */, + ); + path = PersonDetailedComponent; + sourceTree = ""; + }; + 36BB6F5627AFCD8D00F224AB /* Entity */ = { + isa = PBXGroup; + children = ( + 36BB6F6527AFD12B00F224AB /* PersonDetails.swift */, + 36BB6F6727AFD26500F224AB /* Organization.swift */, + 36BB6F6927AFD2A100F224AB /* PhoneExtension.swift */, + 36BB6F6B27AFD2B900F224AB /* Room.swift */, + ); + path = Entity; + sourceTree = ""; + }; + 36BB6F5727AFCD9300F224AB /* ViewModel */ = { + isa = PBXGroup; + children = ( + 36BB6F6327AFCFFB00F224AB /* PersonDetailedViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 36BB6F5827AFCD9C00F224AB /* View */ = { + isa = PBXGroup; + children = ( + 36BB6F5227AFCCB500F224AB /* PersonDetailedView.swift */, + 36BB6F7C27B356C200F224AB /* PersonDetailedCellView.swift */, + 36BB6F7E27B386D100F224AB /* AddToContactsView.swift */, + ); + path = View; + sourceTree = ""; + }; + 36BB6F5927AFCDF900F224AB /* PersonSearchComponent */ = { + isa = PBXGroup; + children = ( + 36BB6F5A27AFCDF900F224AB /* ViewModel */, + 36BB6F5C27AFCDFA00F224AB /* Entity */, + 36BB6F5E27AFCDFA00F224AB /* View */, + ); + path = PersonSearchComponent; + sourceTree = ""; + }; + 36BB6F5A27AFCDF900F224AB /* ViewModel */ = { + isa = PBXGroup; + children = ( + 36BB6F5B27AFCDF900F224AB /* PersonSearchViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 36BB6F5C27AFCDFA00F224AB /* Entity */ = { + isa = PBXGroup; + children = ( + 36BB6F5D27AFCDFA00F224AB /* Person.swift */, + ); + path = Entity; + sourceTree = ""; + }; + 36BB6F5E27AFCDFA00F224AB /* View */ = { + isa = PBXGroup; + children = ( + 36BB6F5F27AFCDFA00F224AB /* PersonSearchView.swift */, + ); + path = View; + sourceTree = ""; + }; + 36BB6F6D27B1195600F224AB /* View */ = { + isa = PBXGroup; + children = ( + 100803452764E2C50013ED0E /* ProfileToolbar.swift */, + 100803472764E37A0013ED0E /* ProfileView.swift */, + ); + path = View; + sourceTree = ""; + }; + 36BB6F6E27B1196300F224AB /* Entity */ = { + isa = PBXGroup; + children = ( + 36BB6F6F27B1197400F224AB /* Profile.swift */, + 36BB6F7427B1D87200F224AB /* Tuition.swift */, + ); + path = Entity; + sourceTree = ""; + }; + 36BB6F7127B1CD8100F224AB /* ViewModel */ = { + isa = PBXGroup; + children = ( + 36BB6F7227B1CD9200F224AB /* ProfileViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 36BB6F7627B26DCA00F224AB /* TuitionComponent */ = { + isa = PBXGroup; + children = ( + 36BB6F7727B26DD300F224AB /* View */, + ); + path = TuitionComponent; + sourceTree = ""; + }; + 36BB6F7727B26DD300F224AB /* View */ = { + isa = PBXGroup; + children = ( + 36BB6F7827B26DE300F224AB /* TuitionView.swift */, + 36BB6F7A27B27D0D00F224AB /* TuitionCard.swift */, + ); + path = View; + sourceTree = ""; + }; + 36BB6F8027B39B2200F224AB /* LectureSearchComponent */ = { + isa = PBXGroup; + children = ( + 36BB6F8427B39C3D00F224AB /* ViewModel */, + 36BB6F8127B39B3400F224AB /* View */, + ); + path = LectureSearchComponent; + sourceTree = ""; + }; + 36BB6F8127B39B3400F224AB /* View */ = { + isa = PBXGroup; + children = ( + 36BB6F8227B39B4300F224AB /* LectureSearchView.swift */, + ); + path = View; + sourceTree = ""; + }; + 36BB6F8427B39C3D00F224AB /* ViewModel */ = { + isa = PBXGroup; + children = ( + 36BB6F8527B39C5300F224AB /* LectureSearchViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 36BBE72D27989F6E0018FD3F /* HelperViews */ = { isa = PBXGroup; children = ( @@ -813,7 +995,7 @@ 36D0C8A22762075900A142CD /* TokenActivationTutorialView.swift */, 36203E892761C6EC00C24658 /* LoginView.swift */, 3698CBEE2761E6CC001C5735 /* TokenConfirmationView.swift */, - 36203E882761C6EC00C24658 /* Spinner.swift */, + 36203E882761C6EC00C24658 /* TUMSplashScreen.swift */, ); path = Views; sourceTree = ""; @@ -1036,7 +1218,9 @@ buildActionMask = 2147483647; files = ( 36108BEE27A304B6007DC62D /* Panel.swift in Sources */, + 36BB6F6227AFCDFA00F224AB /* PersonSearchView.swift in Sources */, 97270F5A27AB2A4900BB25E4 /* Array+Rearrange.swift in Sources */, + 36BB6F6627AFD12B00F224AB /* PersonDetails.swift in Sources */, 36108BFD27A30517007DC62D /* MovieDetailedView.swift in Sources */, 3683C3182758117900082930 /* Model.swift in Sources */, 36108BEB27A304B6007DC62D /* PanelRow.swift in Sources */, @@ -1050,13 +1234,16 @@ 36108BE927A304B5007DC62D /* MenuView.swift in Sources */, 36108C1D27A307FA007DC62D /* GradeView.swift in Sources */, 36AF61DE27A2FD7800FEBD98 /* APIResponse.swift in Sources */, + 36BB6F7027B1197400F224AB /* Profile.swift in Sources */, 36E964A32774932B0055777F /* CalendarToolbar.swift in Sources */, 36108BC427A3046B007DC62D /* LectureDetailsDetailedInfoRowView.swift in Sources */, 36108C1C27A307FA007DC62D /* GradesView.swift in Sources */, 36108BE227A304B5007DC62D /* Menu.swift in Sources */, 36108BC027A3046B007DC62D /* LecturesView.swift in Sources */, 36AF61D927A2FD7800FEBD98 /* TUMSexyAPI.swift in Sources */, + 36BB6F7F27B386D100F224AB /* AddToContactsView.swift in Sources */, 36AF61DF27A2FD7800FEBD98 /* TUMOnlineAPI.swift in Sources */, + 36BB6F7D27B356C200F224AB /* PersonDetailedCellView.swift in Sources */, 36108BE727A304B5007DC62D /* MenuViewModel.swift in Sources */, 36AF61E127A2FD7800FEBD98 /* TUMCabeAPI.swift in Sources */, 36108BB627A3046B007DC62D /* LectureDetailsViewModel+State.swift in Sources */, @@ -1064,6 +1251,7 @@ 36982BD827A2739000515847 /* Collapsible.swift in Sources */, 36108BFF27A30517007DC62D /* MoviesView.swift in Sources */, 36AF61E927A2FD7800FEBD98 /* ErrorHandler.swift in Sources */, + 36BB6F6027AFCDFA00F224AB /* PersonSearchViewModel.swift in Sources */, 36982BD627A251A700515847 /* NewsCardsHorizontalScrollingView.swift in Sources */, 36AF61E727A2FD7800FEBD98 /* ErrorEmittingViewModifier.swift in Sources */, 36D0C8A32762075900A142CD /* TokenActivationTutorialView.swift in Sources */, @@ -1074,7 +1262,7 @@ 36203E8D2761C6EC00C24658 /* Credentials.swift in Sources */, 3698CBED2761E014001C5735 /* CustomRoundedBorderTextFieldStyle.swift in Sources */, 36108BE627A304B5007DC62D /* MealPlanViewModel.swift in Sources */, - 36203E8B2761C6EC00C24658 /* Spinner.swift in Sources */, + 36203E8B2761C6EC00C24658 /* TUMSplashScreen.swift in Sources */, 36AF61DA27A2FD7800FEBD98 /* EatAPI.swift in Sources */, 36108BEF27A304B6007DC62D /* MapContent.swift in Sources */, 3616C4CF279020D3000A1BC9 /* TUMSexyViewModel.swift in Sources */, @@ -1086,20 +1274,26 @@ 36BBE7342798B04D0018FD3F /* NewsSource.swift in Sources */, 36108BED27A304B6007DC62D /* MapView.swift in Sources */, 36AF61E827A2FD7800FEBD98 /* AlertErrorHandler.swift in Sources */, + 36BB6F5327AFCCB500F224AB /* PersonDetailedView.swift in Sources */, 36108BC327A3046B007DC62D /* LectureDetailsTitleView.swift in Sources */, 36E964A5277493D90055777F /* CalendarViewModel.swift in Sources */, + 36BB6F7927B26DE300F224AB /* TuitionView.swift in Sources */, + 36BB6F6127AFCDFA00F224AB /* Person.swift in Sources */, 36108BEA27A304B6007DC62D /* Toolbar.swift in Sources */, 36108C1527A307F9007DC62D /* GradesViewModel.swift in Sources */, 36108BE427A304B5007DC62D /* MensaEnumService.swift in Sources */, 36108BE127A304B5007DC62D /* Cafeteria.swift in Sources */, 36108BE027A304B5007DC62D /* Dish.swift in Sources */, 36108BBB27A3046B007DC62D /* LecturesScreen.swift in Sources */, + 36BB6F6427AFCFFB00F224AB /* PersonDetailedViewModel.swift in Sources */, 36AF61E427A2FD7800FEBD98 /* NetworkingError.swift in Sources */, + 36BB6F8327B39B4300F224AB /* LectureSearchView.swift in Sources */, 36108BFA27A30517007DC62D /* Movie.swift in Sources */, 36108BB727A3046B007DC62D /* LectureDetailsViewModel.swift in Sources */, 36AF61E027A2FD7800FEBD98 /* Cache.swift in Sources */, 36AF61F027A2FD7800FEBD98 /* DecoderProtocol.swift in Sources */, 3698CBEF2761E6CC001C5735 /* TokenConfirmationView.swift in Sources */, + 36BB6F7527B1D87200F224AB /* Tuition.swift in Sources */, 3616C4CD279020A0000A1BC9 /* TUMSexyView.swift in Sources */, 36108C1E27A307FA007DC62D /* BarChartView.swift in Sources */, 3683C31A2758118A00082930 /* MockModel.swift in Sources */, @@ -1123,14 +1317,19 @@ 36108BC127A3046B007DC62D /* LectureView.swift in Sources */, 366F0E9027580CFD0091651D /* Campus_iOS.xcdatamodeld in Sources */, 36108BFC27A30517007DC62D /* MovieCard.swift in Sources */, + 36BB6F6A27AFD2A100F224AB /* PhoneExtension.swift in Sources */, 36AF61DC27A2FD7800FEBD98 /* NetworkingAPI.swift in Sources */, 100803462764E2C50013ED0E /* ProfileToolbar.swift in Sources */, + 36BB6F7B27B27D0D00F224AB /* TuitionCard.swift in Sources */, 36AF61E227A2FD7800FEBD98 /* Constants.swift in Sources */, + 36BB6F6C27AFD2B900F224AB /* Room.swift in Sources */, 36AF61EB27A2FD7800FEBD98 /* ErrorCategory.swift in Sources */, 36203E8C2761C6EC00C24658 /* LoginView.swift in Sources */, 36E964A7277498540055777F /* CalendarContentView.swift in Sources */, 36AF61E327A2FD7800FEBD98 /* APIConstants.swift in Sources */, 3629BA2C27A1CECA0036AC80 /* NewsView.swift in Sources */, + 36BB6F7327B1CD9200F224AB /* ProfileViewModel.swift in Sources */, + 36BB6F6827AFD26500F224AB /* Organization.swift in Sources */, 3616C4DB27904BD3000A1BC9 /* NewsViewModel.swift in Sources */, 3629BA2E27A1CEFA0036AC80 /* NewsCard.swift in Sources */, 100803482764E37A0013ED0E /* ProfileView.swift in Sources */, @@ -1143,6 +1342,7 @@ 36FF90652773BB8200F4C785 /* Extensions.swift in Sources */, 36108C1427A307F9007DC62D /* GradeColor.swift in Sources */, 36108BBD27A3046B007DC62D /* Lecture.swift in Sources */, + 36BB6F8627B39C5300F224AB /* LectureSearchViewModel.swift in Sources */, 36108BC227A3046B007DC62D /* LectureDetailsBasicInfoView.swift in Sources */, 36108BC727A3046B007DC62D /* LectureDetailsLinkView.swift in Sources */, 36108BB827A3046B007DC62D /* LecturesViewModel.swift in Sources */, diff --git a/Campus-iOS/App.swift b/Campus-iOS/App.swift index 1a6f9a9b..90140d6b 100644 --- a/Campus-iOS/App.swift +++ b/Campus-iOS/App.swift @@ -11,13 +11,10 @@ import KVKCalendar @main struct CampusApp: App { - @StateObject var environmentValues: Model = Model() @StateObject var model: Model = MockModel() let persistenceController = PersistenceController.shared @State var selectedTab = 0 - @State var splashScreenPresented = false - @State private var showingAlert = false init() { UITabBar.appearance().isOpaque = true @@ -29,16 +26,11 @@ struct CampusApp: App { var body: some Scene { WindowGroup { - tabViewComponent() - .sheet(isPresented: $model.isLoginSheetPresented) { - if splashScreenPresented { - Spinner() - .alert(isPresented: $showingAlert) { - Alert(title: Text("There is a problem with the connection"), - message: Text("Please restart the app"), - dismissButton: .default(Text("Got it!"))) - } - } else { + if model.splashScreenPresented { + TUMSplashScreen() + } else { + tabViewComponent() + .sheet(isPresented: $model.isLoginSheetPresented) { NavigationView { LoginView(model: model) .onAppear { @@ -46,8 +38,8 @@ struct CampusApp: App { } } } - } - .environmentObject(environmentValues) + .environmentObject(model) + } } } diff --git a/Campus-iOS/Assets.xcassets/About.imageset/About.pdf b/Campus-iOS/Assets.xcassets/About.imageset/About.pdf deleted file mode 100644 index a0cec064..00000000 Binary files a/Campus-iOS/Assets.xcassets/About.imageset/About.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/About.imageset/Contents.json b/Campus-iOS/Assets.xcassets/About.imageset/Contents.json deleted file mode 100644 index 7fa458fe..00000000 --- a/Campus-iOS/Assets.xcassets/About.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "About.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Calendar.imageset/Calendar.pdf b/Campus-iOS/Assets.xcassets/Calendar.imageset/Calendar.pdf deleted file mode 100644 index e10c5f95..00000000 Binary files a/Campus-iOS/Assets.xcassets/Calendar.imageset/Calendar.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Calendar.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Calendar.imageset/Contents.json deleted file mode 100644 index 6a56336a..00000000 --- a/Campus-iOS/Assets.xcassets/Calendar.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Calendar.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Cards.imageset/Cards.pdf b/Campus-iOS/Assets.xcassets/Cards.imageset/Cards.pdf deleted file mode 100644 index d778ee2f..00000000 Binary files a/Campus-iOS/Assets.xcassets/Cards.imageset/Cards.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Cards.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Cards.imageset/Contents.json deleted file mode 100644 index ace2b116..00000000 --- a/Campus-iOS/Assets.xcassets/Cards.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Cards.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Feedback.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Feedback.imageset/Contents.json deleted file mode 100644 index e0b1e48a..00000000 --- a/Campus-iOS/Assets.xcassets/Feedback.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Feedback.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Feedback.imageset/Feedback.pdf b/Campus-iOS/Assets.xcassets/Feedback.imageset/Feedback.pdf deleted file mode 100644 index 8559811b..00000000 Binary files a/Campus-iOS/Assets.xcassets/Feedback.imageset/Feedback.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Grades.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Grades.imageset/Contents.json deleted file mode 100644 index bbbf4fda..00000000 --- a/Campus-iOS/Assets.xcassets/Grades.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Grades.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Grades.imageset/Grades.pdf b/Campus-iOS/Assets.xcassets/Grades.imageset/Grades.pdf deleted file mode 100644 index 0e6616ec..00000000 Binary files a/Campus-iOS/Assets.xcassets/Grades.imageset/Grades.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Lectures.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Lectures.imageset/Contents.json deleted file mode 100644 index e05c45a2..00000000 --- a/Campus-iOS/Assets.xcassets/Lectures.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Lectures.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Lectures.imageset/Lectures.pdf b/Campus-iOS/Assets.xcassets/Lectures.imageset/Lectures.pdf deleted file mode 100644 index 25a964b4..00000000 Binary files a/Campus-iOS/Assets.xcassets/Lectures.imageset/Lectures.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Library.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Library.imageset/Contents.json deleted file mode 100644 index 3bc9c125..00000000 --- a/Campus-iOS/Assets.xcassets/Library.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Library.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Library.imageset/Library.pdf b/Campus-iOS/Assets.xcassets/Library.imageset/Library.pdf deleted file mode 100644 index e6a5aad7..00000000 Binary files a/Campus-iOS/Assets.xcassets/Library.imageset/Library.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/MVG.imageset/Contents.json b/Campus-iOS/Assets.xcassets/MVG.imageset/Contents.json deleted file mode 100644 index fdb76935..00000000 --- a/Campus-iOS/Assets.xcassets/MVG.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "MVG.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/MVG.imageset/MVG.pdf b/Campus-iOS/Assets.xcassets/MVG.imageset/MVG.pdf deleted file mode 100644 index 4b6fae4d..00000000 Binary files a/Campus-iOS/Assets.xcassets/MVG.imageset/MVG.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Mensa.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Mensa.imageset/Contents.json deleted file mode 100644 index b7e15f65..00000000 --- a/Campus-iOS/Assets.xcassets/Mensa.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Mensa.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Mensa.imageset/Mensa.pdf b/Campus-iOS/Assets.xcassets/Mensa.imageset/Mensa.pdf deleted file mode 100644 index 7709c198..00000000 Binary files a/Campus-iOS/Assets.xcassets/Mensa.imageset/Mensa.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/News.imageset/Contents.json b/Campus-iOS/Assets.xcassets/News.imageset/Contents.json deleted file mode 100644 index a8874a80..00000000 --- a/Campus-iOS/Assets.xcassets/News.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "News.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/News.imageset/News.pdf b/Campus-iOS/Assets.xcassets/News.imageset/News.pdf deleted file mode 100644 index 8b02ce1f..00000000 Binary files a/Campus-iOS/Assets.xcassets/News.imageset/News.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/RoomFinder.imageset/Contents.json b/Campus-iOS/Assets.xcassets/RoomFinder.imageset/Contents.json deleted file mode 100644 index 145353fc..00000000 --- a/Campus-iOS/Assets.xcassets/RoomFinder.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "RoomFinder.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/RoomFinder.imageset/RoomFinder.pdf b/Campus-iOS/Assets.xcassets/RoomFinder.imageset/RoomFinder.pdf deleted file mode 100644 index e94d9028..00000000 Binary files a/Campus-iOS/Assets.xcassets/RoomFinder.imageset/RoomFinder.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Search.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Search.imageset/Contents.json deleted file mode 100644 index 5d16df18..00000000 --- a/Campus-iOS/Assets.xcassets/Search.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Search.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Search.imageset/Search.pdf b/Campus-iOS/Assets.xcassets/Search.imageset/Search.pdf deleted file mode 100644 index 52c947fb..00000000 Binary files a/Campus-iOS/Assets.xcassets/Search.imageset/Search.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Tuition.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Tuition.imageset/Contents.json deleted file mode 100644 index 7d852905..00000000 --- a/Campus-iOS/Assets.xcassets/Tuition.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Tuition.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Tuition.imageset/Tuition.pdf b/Campus-iOS/Assets.xcassets/Tuition.imageset/Tuition.pdf deleted file mode 100644 index 5e7abfe5..00000000 Binary files a/Campus-iOS/Assets.xcassets/Tuition.imageset/Tuition.pdf and /dev/null differ diff --git a/Campus-iOS/Assets.xcassets/Tum.sexy.imageset/Contents.json b/Campus-iOS/Assets.xcassets/Tum.sexy.imageset/Contents.json deleted file mode 100644 index 0808d557..00000000 --- a/Campus-iOS/Assets.xcassets/Tum.sexy.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Tum.sexy.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Campus-iOS/Assets.xcassets/Tum.sexy.imageset/Tum.sexy.pdf b/Campus-iOS/Assets.xcassets/Tum.sexy.imageset/Tum.sexy.pdf deleted file mode 100644 index 5baac139..00000000 Binary files a/Campus-iOS/Assets.xcassets/Tum.sexy.imageset/Tum.sexy.pdf and /dev/null differ diff --git a/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings b/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings index e434911c..4d6ba2fb 100644 --- a/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings +++ b/Campus-iOS/Campus-iOS/Base.lproj/Localizable.strings @@ -82,3 +82,14 @@ "Week" = "Week"; "Month" = "Month"; "Year" = "Year"; + +// PersonSearch +"Unable to find person" = "Unable to find person"; +"Person Search" = "Person Search"; + +// Tuition +"Open Amount" = "Offener Betrag"; + +// LectureSearch +"Lecture Search" = "Lecture Search"; +"Unable to find lecture" = "Unable to find lecture"; diff --git a/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings b/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings index e7a308c9..eaf34f41 100644 --- a/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings +++ b/Campus-iOS/Campus-iOS/de.lproj/Localizable.strings @@ -76,3 +76,14 @@ "Week" = "Woche"; "Month" = "Monat"; "Year" = "Jahr"; + +// PersonSearch +"Unable to find person" = "Person kann nicht gefunden werden"; +"Person Search" = "Personensuche"; + +// Tuition +"Open Amount" = "Offener Betrag"; + +// LectureSearch +"Lecture Search" = "Vorlesungssuche"; +"Unable to find lecture" = "Vorlesung kann nicht gefunden werden"; diff --git a/Campus-iOS/Enums/Enums.swift b/Campus-iOS/Enums/Enums.swift index 639b3d3b..deafe25d 100644 --- a/Campus-iOS/Enums/Enums.swift +++ b/Campus-iOS/Enums/Enums.swift @@ -25,7 +25,7 @@ enum TumCalendarTypes: String, CaseIterable { } var calendarType: CalendarType { - switch(self) { + switch self { case .day: return CalendarType.day case .week: @@ -37,3 +37,54 @@ enum TumCalendarTypes: String, CaseIterable { } } } + +enum Gender: Decodable, Hashable { + case male + case female + case nonBinary + case unkown + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + switch try container.decode(String.self) { + case "M", "m": self = .male + case "F", "f": self = .female + default: self = .unkown + } + } +} + +enum ContactInfo { + case phone(String) + case mobilePhone(String) + case fax(String) + case additionalInfo(String) + case homepage(String) + + init?(key: String, value: String) { + guard !value.isEmpty else { return nil } + switch key { + case "telefon": self = .phone(value) + case "mobiltelefon": self = .mobilePhone(value) + case "fax": self = .fax(value) + case "zusatz_info": self = .additionalInfo(value) + case "www_homepage": self = .homepage(value) + default: return nil + } + } +} + +enum Role: String { + case student = "student" + case extern = "extern" + case employee = "employee" + + var localizedDesription: String { + switch self { + case .student: return "Student".localized + case .extern: return "Extern".localized + case .employee: return "Employee".localized + } + } +} diff --git a/Campus-iOS/GradesComponent/Views/GradeView.swift b/Campus-iOS/GradesComponent/Views/GradeView.swift index fed93330..5423fe09 100644 --- a/Campus-iOS/GradesComponent/Views/GradeView.swift +++ b/Campus-iOS/GradesComponent/Views/GradeView.swift @@ -8,7 +8,6 @@ import SwiftUI struct GradeView: View { - @Environment(\.colorScheme) var colorScheme var grade: Grade @@ -31,7 +30,7 @@ struct GradeView: View { VStack(alignment: .leading, spacing: 8) { Text(grade.title) .fontWeight(.bold) - .foregroundColor(colorScheme == .dark ? Color.white : Color.black) + .foregroundColor(Color(.label)) VStack(alignment: .leading, spacing: 8) { HStack(spacing: 16) { @@ -76,5 +75,7 @@ struct GradeView: View { struct GradeView_Previews: PreviewProvider { static var previews: some View { GradeView(grade: Grade.dummyData.first!) + GradeView(grade: Grade.dummyData.first!) + .preferredColorScheme(.dark) } } diff --git a/Campus-iOS/Info.plist b/Campus-iOS/Info.plist index 4f8cf763..36f8ffcf 100644 --- a/Campus-iOS/Info.plist +++ b/Campus-iOS/Info.plist @@ -2,6 +2,15 @@ + NSContactsUsageDescription + TUM Campus App can add contacts from TUM Online to your address book + UILaunchScreen + + UIColorName + tumBlue + UIImageName + logo-white + NSCalendarsUsageDescription TODO diff --git a/Campus-iOS/LectureComponent/Model/Lecture.swift b/Campus-iOS/LectureComponent/Model/Lecture.swift index f96b0ae3..ee835f05 100644 --- a/Campus-iOS/LectureComponent/Model/Lecture.swift +++ b/Campus-iOS/LectureComponent/Model/Lecture.swift @@ -15,7 +15,7 @@ enum LectureComponents { public var row: [Row] } - struct Row: Decodable, Identifiable { + struct Row: Decodable, Identifiable, Equatable { public var id: UInt64 public var lvNumber: UInt64 public var title: String diff --git a/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift b/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift new file mode 100644 index 00000000..d590af0c --- /dev/null +++ b/Campus-iOS/LectureSearchComponent/View/LectureSearchView.swift @@ -0,0 +1,50 @@ +// +// LectureSearchView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 09.02.22. +// + +import SwiftUI + +struct LectureSearchView: View { + + @ObservedObject var viewModel = LectureSearchViewModel() + @State var searchText = "" + + var body: some View { + List { + ForEach(self.viewModel.result) { lecture in + NavigationLink(destination: LectureDetailsScreen(lecture: lecture)) { + HStack { + Text(lecture.title) + Spacer() + Text(lecture.eventType) + .foregroundColor(Color(.secondaryLabel)) + } + } + } + if(viewModel.errorMessage != "") { + VStack { + Spacer() + Text(self.viewModel.errorMessage).foregroundColor(.gray) + Spacer() + } + } + } + .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) + } + } + .animation(.default, value: self.viewModel.result) + } +} + +struct LectureSearchView_Previews: PreviewProvider { + static var previews: some View { + LectureSearchView() + } +} diff --git a/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift b/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift new file mode 100644 index 00000000..783ee1da --- /dev/null +++ b/Campus-iOS/LectureSearchComponent/ViewModel/LectureSearchViewModel.swift @@ -0,0 +1,40 @@ +// +// LectureSearchViewModel.swift +// Campus-iOS +// +// Created by Milen Vitanov on 09.02.22. +// + +import Foundation + +import Foundation +import Alamofire +import XMLCoder + +class LectureSearchViewModel: ObservableObject { + @Published var result: [Lecture] = [] + @Published var errorMessage: String = "" + + private let sessionManager = Session.defaultSession + + func fetch(searchString: String) { + // activate only when more than 3 characters + + 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 let result = self?.result, result.isEmpty { + self?.errorMessage = NSString(format: "Unable to find lecture".localized as NSString, searchString) as String + } else { + self?.errorMessage = "" + } + } + } +} diff --git a/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift b/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift index c1ae35fc..fde8a6c5 100644 --- a/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift +++ b/Campus-iOS/LoginComponent/ViewModel/LoginViewModel.swift @@ -73,10 +73,12 @@ class LoginViewModel: ObservableObject { #endif self?.showTokenAlert = false self?.model?.isLoginSheetPresented = false - self?.model?.showProfile = false self?.model?.isUserAuthenticated = true + self?.model?.showProfile = false + self?.model?.loadProfile() case let .failure(error): self?.showTokenAlert = true + self?.model?.isUserAuthenticated = false self?.alertMessage = error.localizedDescription } } diff --git a/Campus-iOS/LoginComponent/Views/Spinner.swift b/Campus-iOS/LoginComponent/Views/Spinner.swift deleted file mode 100644 index e91ca9e5..00000000 --- a/Campus-iOS/LoginComponent/Views/Spinner.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// Spinner.swift -// Campus-iOS -// -// Created by Milen Vitanov on 09.12.21. -// - -import Foundation -import SwiftUI - -struct Spinner: View { - let rotationTime: Double = 0.75 - let animationTime: Double = 1.9 // Sum of all animation times - let fullRotation: Angle = .degrees(360) - static let initialDegree: Angle = .degrees(270) - - @State var spinnerStart: CGFloat = 0.0 - @State var spinnerEndS1: CGFloat = 0.03 - @State var spinnerEndS2S3: CGFloat = 0.03 - - @State var rotationDegreeS1 = initialDegree - @State var rotationDegreeS2 = initialDegree - @State var rotationDegreeS3 = initialDegree - - var body: some View { - ZStack { - Color.white - .edgesIgnoringSafeArea(.all) - VStack { - Image("logo-blue") - .resizable() - .frame(width: 175, height: 100, alignment: .center) - Spacer().frame(height: 100) - ZStack { - // S3 - SpinnerCircle(start: spinnerStart, end: spinnerEndS2S3, rotation: rotationDegreeS3, color: Color.blue) - - // S2 - SpinnerCircle(start: spinnerStart, end: spinnerEndS2S3, rotation: rotationDegreeS2, color: Color.white) - - // S1 - SpinnerCircle(start: spinnerStart, end: spinnerEndS1, rotation: rotationDegreeS1, color: Color.blue) - }.frame(width: 150, height: 150) - } - } - .onAppear { - Timer.scheduledTimer(withTimeInterval: animationTime, repeats: true) { _ in - self.animateSpinner() - } - } - } - - // MARK: Animation methods - func animateSpinner(with duration: Double, completion: @escaping (() -> Void)) { - Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { _ in - withAnimation(Animation.easeInOut(duration: self.rotationTime)) { - completion() - } - } - } - - func animateSpinner() { - animateSpinner(with: rotationTime) { self.spinnerEndS1 = 1.0 } - - animateSpinner(with: (rotationTime * 2) - 0.025) { - self.rotationDegreeS1 += fullRotation - self.spinnerEndS2S3 = 0.8 - } - - animateSpinner(with: (rotationTime * 2)) { - self.spinnerEndS1 = 0.03 - self.spinnerEndS2S3 = 0.03 - } - - animateSpinner(with: (rotationTime * 2) + 0.0525) { self.rotationDegreeS2 += fullRotation } - - animateSpinner(with: (rotationTime * 2) + 0.225) { self.rotationDegreeS3 += fullRotation } - } -} - -// MARK: SpinnerCircle -struct SpinnerCircle: View { - var start: CGFloat - var end: CGFloat - var rotation: Angle - var color: Color - - var body: some View { - Circle() - .trim(from: start, to: end) - .stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round)) - .fill(color) - .rotationEffect(rotation) - } -} - -struct Spinner_Previews: PreviewProvider { - static var previews: some View { - ZStack { - Spinner() - } - } -} diff --git a/Campus-iOS/LoginComponent/Views/TUMSplashScreen.swift b/Campus-iOS/LoginComponent/Views/TUMSplashScreen.swift new file mode 100644 index 00000000..69a5fb91 --- /dev/null +++ b/Campus-iOS/LoginComponent/Views/TUMSplashScreen.swift @@ -0,0 +1,82 @@ +// +// Spinner.swift +// Campus-iOS +// +// Created by Milen Vitanov on 09.12.21. +// + +import Foundation +import SwiftUI + +public struct RingProgressViewStyle: ProgressViewStyle { + private let defaultSize: CGFloat = 65 + private let lineWidth: CGFloat = 7 + private let defaultProgress = 0.2 + + @State private var fillRotationAngle = Angle.degrees(-90) + + public func makeBody(configuration: ProgressViewStyleConfiguration) -> some View { + VStack { + configuration.label + progressCircleView(fractionCompleted: configuration.fractionCompleted ?? defaultProgress, + isIndefinite: configuration.fractionCompleted == nil) // UPDATE + configuration.currentValueLabel + } + } + + private func progressCircleView(fractionCompleted: Double, + isIndefinite: Bool) -> some View { + Circle() + .strokeBorder(Color.gray.opacity(0.5), lineWidth: lineWidth, antialiased: true) + .overlay(fillView(fractionCompleted: fractionCompleted, isIndefinite: isIndefinite)) + .frame(width: defaultSize, height: defaultSize) + } + + private func fillView(fractionCompleted: Double, + isIndefinite: Bool) -> some View { + Circle() + .trim(from: 0, to: CGFloat(fractionCompleted)) + .stroke(Color.blue, lineWidth: lineWidth) + .frame(width: defaultSize - lineWidth, height: defaultSize - lineWidth) + .rotationEffect(fillRotationAngle) + .onAppear { + if isIndefinite { + withAnimation(.easeInOut(duration: 1.25).repeatForever(autoreverses: false)) { + fillRotationAngle = .degrees(270) + } + } + } + } +} + +struct TUMSplashScreen: View { + + var body: some View { + ZStack { + Color(.systemBackground) + .edgesIgnoringSafeArea(.all) + VStack { + Image("logo-blue") + .resizable() + .frame(width: 175, height: 100, alignment: .center) + Spacer().frame(height: 100) + VStack { + ProgressView() + .progressViewStyle(RingProgressViewStyle()) + } + .padding() + + } + } + } + +} + +struct TUMSplashScreen_Previews: PreviewProvider { + static var previews: some View { + ZStack { + TUMSplashScreen() + } + .preferredColorScheme(.dark) + } +} diff --git a/Campus-iOS/Model/Model.swift b/Campus-iOS/Model/Model.swift index 90f10c7a..9a22fe0f 100644 --- a/Campus-iOS/Model/Model.swift +++ b/Campus-iOS/Model/Model.swift @@ -14,9 +14,11 @@ import FirebaseAnalytics public class Model: ObservableObject { @Published var showProfile = false - @Published var isLoginSheetPresented = true + @Published var isLoginSheetPresented = false @Published var loginController: AuthenticationHandler @Published var isUserAuthenticated = false + @Published var splashScreenPresented = false + @Published var profile: ProfileViewModel = ProfileViewModel() var anyCancellables: [AnyCancellable] = [] @@ -32,9 +34,13 @@ public class Model: ObservableObject { #if !targetEnvironment(macCatalyst) Analytics.logEvent("token_confirmed", parameters: nil) #endif + self?.splashScreenPresented = false self?.isLoginSheetPresented = false self?.isUserAuthenticated = true + self?.loadProfile() case .failure(_): + self?.isUserAuthenticated = false + self?.splashScreenPresented = false self?.isLoginSheetPresented = true } } @@ -43,11 +49,16 @@ public class Model: ObservableObject { func logout() { loginController.logout() - self.isLoginSheetPresented = true + self.isLoginSheetPresented = self.showProfile ? false : true self.isUserAuthenticated = false + self.unloadProfile() } - func loadAllModels() { - // later load all the models + func unloadProfile() { + self.profile = ProfileViewModel() + } + + func loadProfile() { + self.profile = ProfileViewModel(model: self) } } diff --git a/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift b/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift index a1360260..6b92a569 100644 --- a/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift +++ b/Campus-iOS/MoviesComponent/ViewModel/MoviesViewModel.swift @@ -27,7 +27,7 @@ class MoviesViewModel: ObservableObject { let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) importer.performFetch(handler: { result in - switch(result) { + switch result { case .success(let incoming): self.movies = incoming.sorted(by: { guard let dateOne = $0.date, let dateTwo = $1.date else { diff --git a/Campus-iOS/MoviesComponent/Views/MovieCard.swift b/Campus-iOS/MoviesComponent/Views/MovieCard.swift index 840b0ca8..fc0571fd 100644 --- a/Campus-iOS/MoviesComponent/Views/MovieCard.swift +++ b/Campus-iOS/MoviesComponent/Views/MovieCard.swift @@ -16,7 +16,7 @@ struct MovieCard: View { if let link = self.movie.cover { AsyncImage(url: link) { image in - switch(image) { + switch image { case .empty: ProgressView() case .success(let image): diff --git a/Campus-iOS/MoviesComponent/Views/MovieDetailedView.swift b/Campus-iOS/MoviesComponent/Views/MovieDetailedView.swift index e4a70205..2fc005df 100644 --- a/Campus-iOS/MoviesComponent/Views/MovieDetailedView.swift +++ b/Campus-iOS/MoviesComponent/Views/MovieDetailedView.swift @@ -24,7 +24,7 @@ struct MovieDetailedView: View { VStack(alignment: .center) { if let link = self.movie.cover { AsyncImage(url: link) { image in - switch(image) { + switch image { case .empty: ProgressView() case .success(let image): diff --git a/Campus-iOS/NewsComponent/Service/NewsSource.swift b/Campus-iOS/NewsComponent/Service/NewsSource.swift index 1d3cfcb3..a1bc137d 100644 --- a/Campus-iOS/NewsComponent/Service/NewsSource.swift +++ b/Campus-iOS/NewsComponent/Service/NewsSource.swift @@ -60,7 +60,7 @@ class NewsSource: Entity, ObservableObject { let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) importer.performFetch(handler: { result in - switch(result) { + switch result { case .success(let storage): self.news = storage.filter( { guard let title = $0.title, let link = $0.link else { diff --git a/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift b/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift index 555f9875..61f7eeef 100644 --- a/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift +++ b/Campus-iOS/NewsComponent/ViewModel/NewsViewModel.swift @@ -43,7 +43,7 @@ class NewsViewModel: ObservableObject { let importer = ImporterType(endpoint: endpoint, dateDecodingStrategy: dateDecodingStrategy) importer.performFetch(handler: { result in - switch(result) { + switch result { case .success(let incoming): self.newsSources = incoming case .failure(let error): diff --git a/Campus-iOS/NewsComponent/Views/NewsCard.swift b/Campus-iOS/NewsComponent/Views/NewsCard.swift index 8301d603..3d53d8cb 100644 --- a/Campus-iOS/NewsComponent/Views/NewsCard.swift +++ b/Campus-iOS/NewsComponent/Views/NewsCard.swift @@ -54,7 +54,7 @@ struct NewsCard: View { } } else { AsyncImage(url: URL(string: image)) { image in - switch(image) { + switch image { case .empty: ProgressView() case .success(let image): diff --git a/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift b/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift new file mode 100644 index 00000000..e464cb0c --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/Entity/Organization.swift @@ -0,0 +1,24 @@ +// +// Organization.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import Foundation + +struct Organisation: Decodable { + let name: String + let id: String + let number: String + let title: String + let description: String + + enum CodingKeys: String, CodingKey { + case name = "org" + case id = "kennung" + case number = "org_nr" + case title = "titel" + case description = "beschreibung" + } +} diff --git a/Campus-iOS/PersonDetailedComponent/Entity/PersonDetails.swift b/Campus-iOS/PersonDetailedComponent/Entity/PersonDetails.swift new file mode 100644 index 00000000..420b4b28 --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/Entity/PersonDetails.swift @@ -0,0 +1,115 @@ +// +// PersonDetails.swift +// Campus-iOS +// +// Created by Milen Vitanov on 05.02.22. +// + +import Foundation +import SwiftUI + +struct PersonDetails: Decodable { + let nr: String + let obfuscatedID: String + var personGroup: String? { + let split = obfuscatedID.split(separator: "*") + guard let group = split.first, split.count == 2 else { return nil } + return String(group) + } + var id: String? { + let split = obfuscatedID.split(separator: "*") + guard let id = split.last, split.count == 2 else { return nil } + return String(id) + } + let firstName: String + let name: String + let title: String? + let email: String + let gender: Gender + let officeHours: String? + let officialContact: [ContactInfo] + let privateContact: [ContactInfo] + let image: UIImage? + let organisations: [Organisation] + let rooms: [Room] + let phoneExtensions: [PhoneExtension] + + + enum CodingKeys: String, CodingKey { + case nr + case obfuscatedID = "obfuscated_id" + case firstName = "vorname" + case name = "familienname" + case title = "titel" + case email + case gender = "geschlecht" + case officeHours = "sprechstunde" + case officialContact = "dienstlich" + case privateContact = "privat" + case imageData = "image_data" + case organisationContainer = "gruppen" + case roomContainer = "raeume" + case phoneExtensionContainer = "telefon_nebenstellen" + } + + private struct OrganisationContainer: Decodable { + let organisations: [Organisation] + + enum CodingKeys: String, CodingKey { + case organisations = "gruppe" + } + } + + private struct PhoneExtensionContainer: Decodable { + let phoneExtensions: [PhoneExtension] + + enum CodingKeys: String, CodingKey { + case phoneExtensions = "nebenstelle" + } + } + + private struct RoomContainer: Decodable { + let rooms: [Room] + + enum CodingKeys: String, CodingKey { + case rooms = "raum" + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.firstName = try container.decode(String.self, forKey: .firstName) + self.name = try container.decode(String.self, forKey: .name) + + if let title = try container.decodeIfPresent(String.self, forKey: .title), !title.isEmpty { + self.title = title + } else { + self.title = nil + } + + self.email = try container.decode(String.self, forKey: .email) + + self.nr = try container.decode(String.self, forKey: .nr) + self.obfuscatedID = try container.decode(String.self, forKey: .obfuscatedID) + self.gender = try container.decode(Gender.self, forKey: .gender) + self.officeHours = try container.decode(String.self, forKey: .officeHours) + + self.officialContact = try container.decode([String: String].self, forKey: .officialContact).compactMap { ContactInfo(key: $0.key, value: $0.value) } + self.privateContact = try container.decode([String: String].self, forKey: .privateContact).compactMap { ContactInfo(key: $0.key, value: $0.value) } + + if let imageString = try container.decodeIfPresent(String.self, forKey: .imageData) { + print(imageString) + } + + if let imageString = try container.decodeIfPresent(String.self, forKey: .imageData), let imageData = Data(base64Encoded: imageString, options: [.ignoreUnknownCharacters]), let uiImage = UIImage(data: imageData) { + self.image = uiImage + } else { + self.image = nil + } + + self.organisations = try container.decodeIfPresent(OrganisationContainer.self, forKey: .organisationContainer)?.organisations ?? [] + self.rooms = try container.decodeIfPresent(RoomContainer.self, forKey: .roomContainer)?.rooms ?? [] + self.phoneExtensions = try container.decodeIfPresent(PhoneExtensionContainer.self, forKey: .phoneExtensionContainer)?.phoneExtensions ?? [] + } +} diff --git a/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift b/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift new file mode 100644 index 00000000..1c8709a0 --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/Entity/PhoneExtension.swift @@ -0,0 +1,24 @@ +// +// PhoneExtension.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import Foundation + +struct PhoneExtension: Decodable { + let phoneNumber: String + let countryCode: String + let areaCode: String + let equipmentNumber: String + let branchNumber: String + + enum CodingKeys: String, CodingKey { + case phoneNumber = "telefonnummer" + case countryCode = "tum_anlage_land" + case areaCode = "tum_anlage_ortsvorwahl" + case equipmentNumber = "tum_anlage_nummer" + case branchNumber = "tum_nebenstelle" + } +} diff --git a/Campus-iOS/PersonDetailedComponent/Entity/Room.swift b/Campus-iOS/PersonDetailedComponent/Entity/Room.swift new file mode 100644 index 00000000..eec1fcf4 --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/Entity/Room.swift @@ -0,0 +1,32 @@ +// +// Room.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import Foundation + +struct Room: Decodable { + let number: String + let buildingName: String + let buildingNumber: String + let floorName: String + let floorNumber: String + let id: String + let locationDescription: String + let shortLocationDescription: String + let longLocationDescription: String + + enum CodingKeys: String, CodingKey { + case number = "nummer" + case buildingName = "gebaeudename" + case buildingNumber = "gebaeudenummer" + case floorName = "stockwerkname" + case floorNumber = "stockwerknummer" + case id = "architekt" + case locationDescription = "ortsbeschreibung" + case shortLocationDescription = "kurz" + case longLocationDescription = "lang" + } +} diff --git a/Campus-iOS/PersonDetailedComponent/View/AddToContactsView.swift b/Campus-iOS/PersonDetailedComponent/View/AddToContactsView.swift new file mode 100644 index 00000000..2c140492 --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/View/AddToContactsView.swift @@ -0,0 +1,50 @@ +// +// AddToContactsView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 09.02.22. +// + +import Foundation +import SwiftUI +import ContactsUI + +struct AddToContactsView: UIViewControllerRepresentable { + class Coordinator: NSObject, CNContactViewControllerDelegate, UINavigationControllerDelegate { + private func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNMutableContact?) { + if let c = contact { + self.parent.contact = c + } + + viewController.dismiss(animated: true) + } + + var parent: AddToContactsView + + init(_ parent: AddToContactsView) { + self.parent = parent + } + } + + @State var contact: CNMutableContact + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> CNContactViewController { + guard self.contact.identifier.count != 0 else { + let vc = CNContactViewController(forNewContact: CNContact()) + vc.delegate = context.coordinator + return vc + } + + let vc = CNContactViewController(forNewContact: contact) + vc.delegate = context.coordinator + return vc + } + + func updateUIViewController(_ uiViewController: CNContactViewController, context: UIViewControllerRepresentableContext) { + + } +} diff --git a/Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift b/Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift new file mode 100644 index 00000000..b2c2ab5b --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/View/PersonDetailedCellView.swift @@ -0,0 +1,28 @@ +// +// 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 new file mode 100644 index 00000000..af4868ad --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/View/PersonDetailedView.swift @@ -0,0 +1,120 @@ +// +// PersonDetailedView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 05.02.22. +// + +import SwiftUI +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) + } + + 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() + .frame(width: imageSize, height: imageSize) + .clipShape(Circle()) + } 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)) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .accentColor)) + .padding(2) + } + if(self.viewModel.sections?.count ?? 0 > 1) { + form + } else { + List { + HStack { + Spacer() + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .accentColor)) + .padding(2) + Spacer() + } + } + } + } + .background(Color(.systemGroupedBackground)) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + NavigationLink(destination: AddToContactsView(contact: self.viewModel.cnContact)) { + 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) }) + } + } + } + } + .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 new file mode 100644 index 00000000..60a19249 --- /dev/null +++ b/Campus-iOS/PersonDetailedComponent/ViewModel/PersonDetailedViewModel.swift @@ -0,0 +1,220 @@ +// +// PersonDetailedViewModel.swift +// Campus-iOS +// +// Created by Milen Vitanov on 05.02.22. +// + +import Foundation +import Alamofire +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] +} + +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) + } + + + 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 ?? "") + } + + 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)) + } + if let officeHours = self.person?.officeHours, !officeHours.isEmpty { + general.append(PersonDetailsCell(key: "Office Hours".localized, value: officeHours, actionType: .none)) + } + + 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) + } + } + } + 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) + } + } + } + + 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() } + + let contact = CNMutableContact() + + contact.contactType = .person + if let title = person.title { + contact.namePrefix = title + } + contact.givenName = person.firstName + contact.familyName = person.name + + contact.emailAddresses = [CNLabeledValue(label: CNLabelWork, value: person.email as NSString)] + if let organisation = person.organisations.first { + contact.departmentName = organisation.name + } + + var phoneNumbers: [CNLabeledValue] = person.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 + 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 + return CNLabeledValue(label: CNLabelWork, value: CNPhoneNumber(stringValue: phoneExtension.phoneNumber)) + }) + + contact.phoneNumbers = phoneNumbers + + if let imageData = person.image?.jpegData(compressionQuality: 1) { + contact.imageData = imageData + } + + var urls: [CNLabeledValue] = person.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 + switch info { + case .homepage(let urlString): return CNLabeledValue(label: CNLabelWork, value: urlString as NSString) + default: return nil + } + }) + + contact.urlAddresses = urls + + contact.organizationName = "TUM" + if let room = person.rooms.first { + contact.note = room.locationDescription + } + + return contact + } +} diff --git a/Campus-iOS/PersonSearchComponent/Entity/Person.swift b/Campus-iOS/PersonSearchComponent/Entity/Person.swift new file mode 100644 index 00000000..034b29c2 --- /dev/null +++ b/Campus-iOS/PersonSearchComponent/Entity/Person.swift @@ -0,0 +1,75 @@ +// +// Person.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import Foundation + +struct Person: Decodable, Hashable { + let firstName: String + let name: String + let title: String? + let nr: String + let obfuscatedID: String + let gender: Gender + var personGroup: String? { + let split = obfuscatedID.split(separator: "*") + guard let group = split.first, split.count == 2 else { return nil } + return String(group) + } + var id: String? { + let split = obfuscatedID.split(separator: "*") + guard let id = split.last, split.count == 2 else { return nil } + return String(id) + } + + var fullName: String { + "\(self.title?.appending(" ") ?? "")\(self.firstName.appending(" "))\(self.name)" + } + + /* + Tim + Gymnich + + -1870402 + M + 5*B551662E7E3AD2CB + visitenkarte.showImage?pPersonenGruppe=5&pPersonenId=B551662E7E3AD2CB + */ + + enum CodingKeys: String, CodingKey { + case firstName = "vorname" + case name = "familienname" + case title = "titel" + case nr + case obfuscatedID = "obfuscated_id" + case gender = "geschlecht" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.firstName = try container.decode(String.self, forKey: .firstName) + self.name = try container.decode(String.self, forKey: .name) + if let title = try container.decodeIfPresent(String.self, forKey: .title), !title.isEmpty { + self.title = title + } else { + self.title = nil + } + self.nr = try container.decode(String.self, forKey: .nr) + self.obfuscatedID = try container.decode(String.self, forKey: .obfuscatedID) + self.gender = try container.decode(Gender.self, forKey: .gender) + } + + init(firstName: String, lastName: String, title: String?, nr: String, obfuscatedId: String, gender: Gender) { + self.firstName = firstName + self.name = lastName + self.title = title + self.nr = nr + self.obfuscatedID = obfuscatedId + self.gender = gender + } + +} diff --git a/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift b/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift new file mode 100644 index 00000000..99a9dc32 --- /dev/null +++ b/Campus-iOS/PersonSearchComponent/View/PersonSearchView.swift @@ -0,0 +1,45 @@ +// +// PersonSearchView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import SwiftUI + +struct PersonSearchView: View { + + @ObservedObject var viewModel = PersonSearchViewModel() + @State var searchText = "" + + var body: some View { + List { + ForEach(self.viewModel.result, id: \.nr) { person in + NavigationLink(destination: PersonDetailedView(withPerson: person)) { + Text(person.fullName) + } + } + if(viewModel.errorMessage != "") { + VStack { + Spacer() + Text(self.viewModel.errorMessage).foregroundColor(.gray) + Spacer() + } + } + } + .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) + } + } + .animation(.default, value: self.viewModel.result) + } +} + +struct PersonSearchView_Previews: PreviewProvider { + static var previews: some View { + PersonSearchView() + } +} diff --git a/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift b/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift new file mode 100644 index 00000000..df2750c2 --- /dev/null +++ b/Campus-iOS/PersonSearchComponent/ViewModel/PersonSearchViewModel.swift @@ -0,0 +1,38 @@ +// +// PersonSearchViewModel.swift +// Campus-iOS +// +// Created by Milen Vitanov on 06.02.22. +// + +import Foundation +import Alamofire +import XMLCoder + +class PersonSearchViewModel: ObservableObject { + @Published var result: [Person] = [] + @Published var errorMessage: String = "" + + private let sessionManager = Session.defaultSession + + 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 ?? [] + + if let result = self?.result, result.isEmpty { + self?.errorMessage = NSString(format: "Unable to find person".localized as NSString, searchString) as String + } else { + self?.errorMessage = "" + } + } + } +} diff --git a/Campus-iOS/ProfileComponent/Entity/Profile.swift b/Campus-iOS/ProfileComponent/Entity/Profile.swift new file mode 100644 index 00000000..afed5a54 --- /dev/null +++ b/Campus-iOS/ProfileComponent/Entity/Profile.swift @@ -0,0 +1,97 @@ +// +// Profile.swift +// Campus-iOS +// +// Created by Milen Vitanov on 05.02.22. +// + +import Foundation + +struct Profile: Decodable, Entity { + let firstname: String? + let obfuscatedID: String? + let obfuscatedIDEmployee: String? + let obfuscatedIDExtern: String? + let obfuscatedIDStudent: String? + let surname: String? + let tumID: String? + + var personGroup: String? { + let split = obfuscatedID?.split(separator: "*") + guard let group = split?.first, split?.count == 2 else { return nil } + return String(group) + } + var id: String? { + let split = obfuscatedID?.split(separator: "*") + guard let id = split?.last, split?.count == 2 else { return nil } + return String(id) + } + + var fullName: String { + "\(self.firstname?.appending(" ") ?? "")\(self.surname?.appending(" ") ?? "")" + } + + /* + + ga94zuh + Tim + Gymnich + 3*C551462A7E3AD2CA + + 3*C551462A7E3AD2CA + + + + + */ + + enum CodingKeys: String, CodingKey { + case surname = "familienname" + case tumID = "kennung" + case obfuscatedID = "obfuscated_id" + case obfuscatedIDEmployee = "obfuscated_id_bedienstete" + case obfuscatedIDExtern = "obfuscated_id_extern" + case obfuscatedIDStudent = "obfuscated_id_studierende" + case firstname = "vorname" + } + + var role: Role { + if obfuscatedIDStudent != nil { + return .student + } else if obfuscatedIDEmployee != nil { + return .employee + } else { + return .extern + } + } + + init(firstname: String?, surname: String?, tumId: String?, obfuscatedID: String?, obfuscatedIDEmployee: String?, obfuscatedIDExtern: String?, obfuscatedIDStudent: String?) { + self.firstname = firstname + self.surname = surname + self.obfuscatedID = obfuscatedID + self.obfuscatedIDEmployee = obfuscatedIDEmployee + self.obfuscatedIDExtern = obfuscatedIDExtern + self.obfuscatedIDStudent = obfuscatedIDStudent + self.tumID = tumId + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let surname = try container.decode(String.self, forKey: .surname) + let tumID = try container.decode(String.self, forKey: .tumID) + let obfuscatedID = try container.decode(String.self, forKey: .obfuscatedID) + let obfuscatedIDEmployee = try container.decodeIfPresent(String.self, forKey: .obfuscatedIDEmployee) + let obfuscatedIDExtern = try container.decodeIfPresent(String.self, forKey: .obfuscatedIDExtern) + let obfuscatedIDStudent = try container.decodeIfPresent(String.self, forKey: .obfuscatedIDStudent) + let firstname = try container.decode(String.self, forKey: .firstname) + + self.surname = surname + self.tumID = tumID + self.obfuscatedID = obfuscatedID + self.obfuscatedIDEmployee = obfuscatedIDEmployee + self.obfuscatedIDExtern = obfuscatedIDExtern + self.obfuscatedIDStudent = obfuscatedIDStudent + self.firstname = firstname + } +} diff --git a/Campus-iOS/ProfileComponent/Entity/Tuition.swift b/Campus-iOS/ProfileComponent/Entity/Tuition.swift new file mode 100644 index 00000000..a440b273 --- /dev/null +++ b/Campus-iOS/ProfileComponent/Entity/Tuition.swift @@ -0,0 +1,64 @@ +// +// Tuition.swift +// Campus-iOS +// +// Created by Milen Vitanov on 07.02.22. +// + +import Foundation + +struct Tuition: Entity { + + var amount: NSDecimalNumber? + var deadline: Date? + var semester: String? + var semesterID: String? + + var isOpenAmount: Bool { + guard let amount = self.amount, amount.isEqual(to: 0) else { + return true + } + return false + } + + /* + + 0 + 2019-02-15 + Sommersemester 2019 + 19S + + */ + + enum CodingKeys: String, CodingKey { + case deadline = "frist" + case semester = "semester_bezeichnung" + case semesterID = "semester_id" + case amount = "soll" + } + + init( deadline: Date?, semester: String?, semesterID: String?, amount: NSDecimalNumber) { + self.deadline = deadline + self.semester = semester + self.semesterID = semesterID + self.amount = amount + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let deadline = try container.decode(Date.self, forKey: .deadline) + let semester = try container.decode(String.self, forKey: .semester) + let semesterID = try container.decode(String.self, forKey: .semesterID) + let amountString = try container.decode(String.self, forKey: .amount) + let amount = NSDecimalNumber(string: amountString, locale: Locale.init(identifier: "de")) + if amount == NSDecimalNumber.notANumber { + throw DecodingError.typeMismatch(NSDecimalNumber.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Value for amount could not be converted to Decimal.")) + } + + self.deadline = deadline + self.semester = semester + self.semesterID = semesterID + self.amount = amount + } +} diff --git a/Campus-iOS/ProfileComponent/ProfileToolbar.swift b/Campus-iOS/ProfileComponent/View/ProfileToolbar.swift similarity index 100% rename from Campus-iOS/ProfileComponent/ProfileToolbar.swift rename to Campus-iOS/ProfileComponent/View/ProfileToolbar.swift diff --git a/Campus-iOS/ProfileComponent/ProfileView.swift b/Campus-iOS/ProfileComponent/View/ProfileView.swift similarity index 69% rename from Campus-iOS/ProfileComponent/ProfileView.swift rename to Campus-iOS/ProfileComponent/View/ProfileView.swift index 7587024c..9ad2ca10 100644 --- a/Campus-iOS/ProfileComponent/ProfileView.swift +++ b/Campus-iOS/ProfileComponent/View/ProfileView.swift @@ -16,43 +16,61 @@ struct ProfileView: View { NavigationView { List { - NavigationLink(destination: Text("Profile")) { + NavigationLink(destination: PersonDetailedView(withProfile: self.model.profile.profile ?? ProfileViewModel.defaultProfile)) { HStack(spacing: 24) { - Image(systemName: "person.crop.circle.fill") + self.model.profile.profileImage .resizable() - .foregroundColor(.black.opacity(0.2)) + .foregroundColor(Color(.secondaryLabel)) .frame(width: 75, height: 75) VStack(alignment: .leading) { - Text("Anton Wyrowski") + Text(self.model.profile.profile?.fullName ?? "Not logged in") .font(.title2) - Text("ab00xyz") + Text(self.model.profile.profile?.tumID ?? "TUM ID") .font(.subheadline) + .foregroundColor(.gray) } } .padding(.vertical, 6) - } + }.disabled(!self.model.isUserAuthenticated) Section("MY TUM") { - NavigationLink(destination: Text("Studienbeiträge")) { - Label("Studienbeiträge", systemImage: "eurosign.circle") + NavigationLink(destination: TuitionView(viewModel: self.model.profile).navigationBarTitle(Text("Tuition fees"))) { + if let isOpenAmount = self.model.profile.tuition?.isOpenAmount, isOpenAmount != true { + Label { + HStack { + Text("Tuition fees") + Spacer() + Text("✅") + } + } icon: { + Image(systemName: "eurosign.circle") + } + } else { + Label("Tuition fees", systemImage: "eurosign.circle") + } } - NavigationLink(destination: Text("Person Search")) { + .disabled(!self.model.isUserAuthenticated) + + NavigationLink(destination: PersonSearchView().navigationBarTitle(Text("Person Search")).navigationBarTitleDisplayMode(.large)) { Label("Person Search", systemImage: "magnifyingglass") } - NavigationLink(destination: Text("Lecture Search")) { + .disabled(!self.model.isUserAuthenticated) + + NavigationLink(destination: LectureSearchView().navigationBarTitle(Text("Lecture Search")).navigationBarTitleDisplayMode(.large)) { Label("Lecture Search", systemImage: "brain.head.profile") } + .disabled(!self.model.isUserAuthenticated) } Section("GENERAL") { NavigationLink(destination: TUMSexyView().navigationBarTitle(Text("Useful Links"))) { - Label("TUM.sexy", image: "Tum.sexy") + Label("TUM.sexy", systemImage: "heart") } NavigationLink(destination: Text("Roomfinder")) { - Label("Roomfinder", image: "RoomFinder") + Label("Roomfinder", systemImage: "rectangle.portrait.arrowtriangle.2.inward") } NavigationLink(destination: NewsView() diff --git a/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift b/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift new file mode 100644 index 00000000..425a60bd --- /dev/null +++ b/Campus-iOS/ProfileComponent/ViewModel/ProfileViewModel.swift @@ -0,0 +1,94 @@ +// +// ProfileViewModel.swift +// Campus-iOS +// +// Created by Milen Vitanov on 07.02.22. +// + +import Foundation +import Alamofire +import XMLCoder +import SwiftUI + +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 + + static let defaultProfile = Profile( + firstname: nil, + surname: "Not logged in".localized, + tumId: "TUM ID", + obfuscatedID: nil, + obfuscatedIDEmployee: nil, + obfuscatedIDExtern: nil, + obfuscatedIDStudent: 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() { + 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() + case .failure(let 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() { + + 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 + case .failure(let error): + print(error) + } + }) + } + + } +} diff --git a/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift b/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift index be581c8e..b33cf9b0 100644 --- a/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift +++ b/Campus-iOS/TUMSexyComponent/ViewModel/TUMSexyViewModel.swift @@ -27,7 +27,7 @@ class TUMSexyViewModel: ObservableObject { func fetch() { importer.performFetch( handler: { result in - switch(result) { + switch result { case .success(let storage): var filledLinks = [TUMSexyLink]() storage.values.forEach() { diff --git a/Campus-iOS/TuitionComponent/View/TuitionCard.swift b/Campus-iOS/TuitionComponent/View/TuitionCard.swift new file mode 100644 index 00000000..a34cd035 --- /dev/null +++ b/Campus-iOS/TuitionComponent/View/TuitionCard.swift @@ -0,0 +1,100 @@ +// +// TuitionCard.swift +// Campus-iOS +// +// Created by Milen Vitanov on 08.02.22. +// + +import SwiftUI + +struct TuitionCard: View { + + @State var tuition: Tuition + + static let currencyFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.currencySymbol = "€ " + formatter.numberStyle = .currency + return formatter + }() + + var formattedAmount: String { + guard let amount = self.tuition.amount else { + return "n/a" + } + return Self.currencyFormatter.string(from: amount) ?? "n/a" + } + + var body: some View { + VStack(alignment: .center, spacing: 0) { + + ZStack { + Rectangle().foregroundColor(Color(red: 8/255, green: 100/255, blue: 188/255)) + .frame(minWidth: nil, idealWidth: nil, maxWidth: UIScreen.main.bounds.width, minHeight: nil, idealHeight: nil, maxHeight: UIScreen.main.bounds.height, alignment: .center) + .clipped() + Image("logo-white") + .resizable() + .scaledToFit() + .frame(width: 175, height: 100, alignment: .center) + } + + // Stack bottom half of card + VStack(alignment: .center, spacing: 6) { + Text(self.tuition.semester ?? "") + .fontWeight(Font.Weight.heavy) + HStack { + Text("Deadline") + Text(self.tuition.deadline ?? Date(), style: .date) + } + .font(Font.custom("HelveticaNeue-Bold", size: 16)) + .foregroundColor(Color.gray) + + Divider() + .foregroundColor(Color.gray.opacity(0.3)) + .padding([.leading, .trailing], -12) + + + HStack(alignment: .center, spacing: 6) { + + Text("Open Amount") + .font(Font.system(size: 13)) + .fontWeight(Font.Weight.heavy) + HStack { + Text(self.formattedAmount) + .font(Font.custom("HelveticaNeue-Medium", size: 14)) + .padding([.leading, .trailing], 10) + .padding([.top, .bottom], 5) + .foregroundColor(Color.white) + } + .if(self.tuition.isOpenAmount, transformT: {view in + view.background(.red) + }, transformF: {view in + view.background(.green) + }) + .cornerRadius(7) + Spacer() + } + .padding([.top, .bottom], 8) + } + .padding(12) + + } + .frame(width: UIScreen.main.bounds.width * 0.8, height: UIScreen.main.bounds.width * 0.8) + .background(Color(.systemGray6)) + .cornerRadius(15) + .shadow(color: Color.black.opacity(0.2), radius: 7, x: 0, y: 2) + } +} + +struct TuitionCard_Previews: PreviewProvider { + + static var tuition = Tuition(deadline: Date(), semester: "Winter Semester 2021/2022", semesterID: "22", amount: 0) + + static var previews: some View { + Group { + TuitionCard(tuition: tuition) + TuitionCard(tuition: tuition) + .preferredColorScheme(.dark) + } + } +} diff --git a/Campus-iOS/TuitionComponent/View/TuitionView.swift b/Campus-iOS/TuitionComponent/View/TuitionView.swift new file mode 100644 index 00000000..c96aa0fc --- /dev/null +++ b/Campus-iOS/TuitionComponent/View/TuitionView.swift @@ -0,0 +1,35 @@ +// +// TuitionView.swift +// Campus-iOS +// +// Created by Milen Vitanov on 08.02.22. +// + +import SwiftUI + +struct TuitionView: View { + + @ObservedObject var viewModel: ProfileViewModel + + var body: some View { + List { + VStack(alignment: .center) { + Spacer(minLength: 0.20 * UIScreen.main.bounds.width) + TuitionCard(tuition: self.viewModel.tuition ?? Tuition(deadline: Date(), semester: "Unknown", semesterID: "0", amount: 0)) + } + .listRowBackground(Color(.systemGroupedBackground)) + } + .refreshable { + self.viewModel.checkTuitionFunc() + } + } +} + +struct TuitionView_Previews: PreviewProvider { + + static var previews: some View { + TuitionView(viewModel: ProfileViewModel()) + TuitionView(viewModel: ProfileViewModel()) + .preferredColorScheme(.dark) + } +}