diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 6a2535f..d1dc787 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -34,13 +34,27 @@ C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; }; FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; }; FF2BE4882B85E27400592328 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; + FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; }; FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */; }; + FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6B42B9248200002987F /* NetworkManager.swift */; }; + FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; }; + FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; + FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; + FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; + FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; + FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; + FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */; }; + FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */; }; + FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */; }; + FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */; }; + FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */; }; FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091612B90F04300AB08DA /* TournamentOrganizerView.swift */; }; - FF7091662B90F0B000AB08DA /* NavigationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091652B90F0B000AB08DA /* NavigationDestination.swift */; }; + FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091652B90F0B000AB08DA /* TabDestination.swift */; }; FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091672B90F79F00AB08DA /* TournamentCellView.swift */; }; FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091692B90F95E00AB08DA /* DateBoxView.swift */; }; FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70916B2B91005400AB08DA /* TournamentView.swift */; }; @@ -51,6 +65,13 @@ FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; }; FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; }; + FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; }; + FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; + FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD12B9238C3008466FA /* FileImportManager.swift */; }; + FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; + FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD52B923960008466FA /* URL+Extensions.swift */; }; + FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD82B923F3C008466FA /* String+Extensions.swift */; }; + FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -128,13 +149,27 @@ C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = ""; }; C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; + FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; + FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = ""; }; FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = ""; }; + FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; + FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = ""; }; + FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = ""; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = ""; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = ""; }; + FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; + FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; + FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; + FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationContext.swift; sourceTree = ""; }; + FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = ""; }; + FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerError.swift; sourceTree = ""; }; + FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Extensions.swift"; sourceTree = ""; }; + FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; FF7091612B90F04300AB08DA /* TournamentOrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentOrganizerView.swift; sourceTree = ""; }; - FF7091652B90F0B000AB08DA /* NavigationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDestination.swift; sourceTree = ""; }; + FF7091652B90F0B000AB08DA /* TabDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabDestination.swift; sourceTree = ""; }; FF7091672B90F79F00AB08DA /* TournamentCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCellView.swift; sourceTree = ""; }; FF7091692B90F95E00AB08DA /* DateBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateBoxView.swift; sourceTree = ""; }; FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = ""; }; @@ -146,6 +181,13 @@ FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; FFD784032B91C280000F62A6 /* EmptyActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyActivityView.swift; sourceTree = ""; }; + FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; + FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; + FFF8ACD12B9238C3008466FA /* FileImportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; + FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; + FFF8ACD52B923960008466FA /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; + FFF8ACD82B923F3C008466FA /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -201,9 +243,11 @@ C425D44E2B6D24E1002A7B48 /* LeStorage.xcodeproj */, C425D4002B6D249D002A7B48 /* PadelClubApp.swift */, FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */, - FF3F74FD2B91A087004CFE0E /* Model */, C4A47D722B72881500ADC637 /* Views */, + FF3F74FD2B91A087004CFE0E /* ViewModel */, C4A47D5F2B6D3B2D00ADC637 /* Data */, + FFF8ACD02B9238A2008466FA /* Manager */, + FFF8ACD72B923F26008466FA /* Extensions */, C425D4042B6D249E002A7B48 /* Assets.xcassets */, C425D4062B6D249E002A7B48 /* Preview Content */, ); @@ -258,6 +302,8 @@ C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */, C4A47D592B6D383C00ADC637 /* Tournament.swift */, C4A47D622B6D3D6500ADC637 /* Club.swift */, + FF6EC9012B94799200EA7F5A /* Coredata */, + FF6EC9022B9479B900EA7F5A /* Federal */, ); path = Data; sourceTree = ""; @@ -265,13 +311,15 @@ C4A47D722B72881500ADC637 /* Views */ = { isa = PBXGroup; children = ( + C425D4022B6D249D002A7B48 /* ContentView.swift */, + C4A47D732B72881F00ADC637 /* ClubView.swift */, FF39719B2B8DE04B004C4E75 /* Navigation */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, - C425D4022B6D249D002A7B48 /* ContentView.swift */, - C4A47D732B72881F00ADC637 /* ClubView.swift */, + FF6EC8FC2B9478C800EA7F5A /* Shared */, C4A47DA02B7D0BD800ADC637 /* Components */, + FFDDD40F2B93B2C900C91A49 /* ViewModifiers */, ); path = Views; sourceTree = ""; @@ -314,6 +362,7 @@ isa = PBXGroup; children = ( C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, + FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, ); path = Components; sourceTree = ""; @@ -323,7 +372,6 @@ children = ( FF59FFB62B90EFBF0061EFF9 /* MainView.swift */, FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, - FFD784012B91C1B4000F62A6 /* WelcomeView.swift */, FFD783FB2B91B919000F62A6 /* Agenda */, FF3F74FA2B91A04B004CFE0E /* Organizer */, FF3F74FB2B91A060004CFE0E /* Toolbox */, @@ -354,6 +402,8 @@ FF3F74F92B91A018004CFE0E /* Screen */ = { isa = PBXGroup; children = ( + FF6EC8FD2B94792300EA7F5A /* Screen.swift */, + FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */, FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, ); path = Screen; @@ -364,6 +414,7 @@ children = ( FF7091612B90F04300AB08DA /* TournamentOrganizerView.swift */, FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */, + FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */, ); path = Organizer; sourceTree = ""; @@ -384,12 +435,49 @@ path = Umpire; sourceTree = ""; }; - FF3F74FD2B91A087004CFE0E /* Model */ = { + FF3F74FD2B91A087004CFE0E /* ViewModel */ = { isa = PBXGroup; children = ( - FF7091652B90F0B000AB08DA /* NavigationDestination.swift */, + FF7091652B90F0B000AB08DA /* TabDestination.swift */, + FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */, + FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */, ); - path = Model; + path = ViewModel; + sourceTree = ""; + }; + FF6EC8FC2B9478C800EA7F5A /* Shared */ = { + isa = PBXGroup; + children = ( + FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, + FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, + ); + path = Shared; + sourceTree = ""; + }; + FF6EC9012B94799200EA7F5A /* Coredata */ = { + isa = PBXGroup; + children = ( + FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */, + FF3795652B9399AA004EA093 /* Persistence.swift */, + ); + path = Coredata; + sourceTree = ""; + }; + FF6EC9022B9479B900EA7F5A /* Federal */ = { + isa = PBXGroup; + children = ( + FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */, + ); + path = Federal; + sourceTree = ""; + }; + FF6EC9072B947A1E00EA7F5A /* Network */ = { + isa = PBXGroup; + children = ( + FF4AB6B42B9248200002987F /* NetworkManager.swift */, + FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */, + ); + path = Network; sourceTree = ""; }; FFD783FB2B91B919000F62A6 /* Agenda */ = { @@ -397,13 +485,44 @@ children = ( FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */, FFD784032B91C280000F62A6 /* EmptyActivityView.swift */, + FFD784012B91C1B4000F62A6 /* WelcomeView.swift */, FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */, - FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */, FFD783FC2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift */, ); path = Agenda; sourceTree = ""; }; + FFDDD40F2B93B2C900C91A49 /* ViewModifiers */ = { + isa = PBXGroup; + children = ( + FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */, + ); + path = ViewModifiers; + sourceTree = ""; + }; + FFF8ACD02B9238A2008466FA /* Manager */ = { + isa = PBXGroup; + children = ( + FFF8ACD12B9238C3008466FA /* FileImportManager.swift */, + FFF8ACD32B92392C008466FA /* SourceFileManager.swift */, + FF6EC9072B947A1E00EA7F5A /* Network */, + ); + path = Manager; + sourceTree = ""; + }; + FFF8ACD72B923F26008466FA /* Extensions */ = { + isa = PBXGroup; + children = ( + FFF8ACD52B923960008466FA /* URL+Extensions.swift */, + FFF8ACD82B923F3C008466FA /* String+Extensions.swift */, + FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */, + FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */, + FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */, + FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -559,41 +678,62 @@ buildActionMask = 2147483647; files = ( C4A47D872B7BA36D00ADC637 /* UserCreationView.swift in Sources */, - FF7091662B90F0B000AB08DA /* NavigationDestination.swift in Sources */, + FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */, + FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */, FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */, + FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FFD783FD2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift in Sources */, + FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, C4A47DA92B85F82100ADC637 /* ChangePasswordView.swift in Sources */, + FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, + FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, + FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, + FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */, + FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */, FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */, + FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, + FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, + FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, + FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, + FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, + FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, + FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, + FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */, FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, + FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */, + FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */, FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */, + FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, + FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, + FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -924,6 +1064,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + FF3795612B9396D0004EA093 /* Model.xcdatamodel */, + ); + currentVersion = FF3795612B9396D0004EA093 /* Model.xcdatamodel */; + path = PadelClubApp.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = C425D3F52B6D249D002A7B48 /* Project object */; } diff --git a/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents b/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents new file mode 100644 index 0000000..7d63e0b --- /dev/null +++ b/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PadelClub/Data/Coredata/Persistence.swift b/PadelClub/Data/Coredata/Persistence.swift new file mode 100644 index 0000000..e5953b6 --- /dev/null +++ b/PadelClub/Data/Coredata/Persistence.swift @@ -0,0 +1,173 @@ +// +// Persistence.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 24/02/2023. +// + +import CoreData + +class PersistenceController: NSObject { + static let shared = PersistenceController() + + private static var _model: NSManagedObjectModel? + private static func model(name: String) throws -> NSManagedObjectModel { + if _model == nil { + _model = try loadModel(name: name, bundle: Bundle.main) + } + return _model! + } + private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel { + guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else { + throw CoreDataError.modelURLNotFound(forResourceName: name) + } + + guard let model = NSManagedObjectModel(contentsOf: modelURL) else { + throw CoreDataError.modelLoadingFailed(forURL: modelURL) + } + return model + } + + enum CoreDataError: Error { + case modelURLNotFound(forResourceName: String) + case modelLoadingFailed(forURL: URL) + } + + lazy var localContainer : NSPersistentContainer = { + let baseURL = NSPersistentContainer.defaultDirectoryURL() + let storeFolderURL = baseURL.appendingPathComponent("CoreDataStores") + let localStoreFolderURL = storeFolderURL.appendingPathComponent("Local") + + let fileManager = FileManager.default + for folderURL in [localStoreFolderURL] where !fileManager.fileExists(atPath: folderURL.path) { + do { + try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) + } catch { + fatalError("#\(#function): Failed to create the store folder: \(error)") + } + } + + let container = NSPersistentContainer(name: "PadelClubApp", managedObjectModel: try! Self.model(name: "PadelClubApp")) + + guard let localStoreDescription = container.persistentStoreDescriptions.first!.copy() as? NSPersistentStoreDescription else { + fatalError("#\(#function): Copying the private store description returned an unexpected value.") + } + localStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + localStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) + localStoreDescription.setValue("DELETE" as NSObject, forPragmaNamed: "journal_mode") + + localStoreDescription.url = localStoreFolderURL.appendingPathComponent("local.sqlite") + + var storeDescriptions = [localStoreDescription] + + /** + Load the persistent stores. + */ + container.persistentStoreDescriptions = storeDescriptions + + container.loadPersistentStores(completionHandler: { (loadedStoreDescription, error) in + guard error == nil else { + fatalError("#\(#function): Failed to load persistent stores:\(error!)") + } +// if UserDefaults.standard.string(forKey: "lastDataSource") == nil { +// UserDefaults.standard.setValue("09-2023", forKey: "lastDataSource") +// } + }) + container.viewContext.automaticallyMergesChangesFromParent = true + container.viewContext.name = "viewContext" + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.undoManager = nil + container.viewContext.shouldDeleteInaccessibleFaults = true + container.viewContext.transactionAuthor = PersistenceController.authorName + + return container + }() + + + /// Creates and configures a private queue context. + private func newTaskContext() -> NSManagedObjectContext { + // Create a private queue context. + /// - Tag: newBackgroundContext + let taskContext = localContainer.newBackgroundContext() + taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + // Set unused undoManager to nil for macOS (it is nil by default on iOS) + // to reduce resource requirements. + taskContext.undoManager = nil + return taskContext + } + + func batchInsertPlayers(_ importedPlayers: [FederalPlayer], importingDate: Date) async { + guard !importedPlayers.isEmpty else { return } + let context = newTaskContext() + context.performAndWait { + context.transactionAuthor = PersistenceController.remoteDataImportAuthorName + + let batchInsert = self.newBatchInsertRequest(with: importedPlayers, importingDate: importingDate) + do { + let result = try context.execute(batchInsert) as? NSBatchInsertResult + if let objectIDs = result?.result as? [NSManagedObjectID], !objectIDs.isEmpty { + let save = [NSInsertedObjectsKey: objectIDs] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: save, into: [localContainer.viewContext]) + } + + } catch { + print(error.localizedDescription) + } + } + } + + private func newBatchInsertRequest(with imported: [FederalPlayer], importingDate: Date) + -> NSBatchInsertRequest { + // 1 + var index = 0 + let total = imported.count + + // 2 + let batchInsert = NSBatchInsertRequest( + entity: ImportedPlayer.entity()) { (managedObject: NSManagedObject) -> Bool in + // 3 + guard index < total else { return true } + + if let importedPlayer = managedObject as? ImportedPlayer { + // 4 + let data = imported[index] + importedPlayer.license = data.license + importedPlayer.ligueName = data.ligue + importedPlayer.rank = Int64(data.rank) + importedPlayer.points = data.points ?? 0 + importedPlayer.assimilation = data.assimilation + importedPlayer.country = data.country + importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0) + importedPlayer.lastName = data.lastName + importedPlayer.firstName = data.firstName + importedPlayer.fullName = data.firstName + " " + data.lastName + importedPlayer.clubName = data.club + importedPlayer.clubCode = data.clubCode.replaceCharactersFromSet(characterSet: .whitespaces) + importedPlayer.male = data.isMale + importedPlayer.importDate = importingDate + } + + // 5 + index += 1 + return false + } + return batchInsert + } + + // MARK: - History Management + private static let authorName = "Padel Tournament" + private static let remoteDataImportAuthorName = "Data Import" + + private func mergeChanges(from transactions: [NSPersistentHistoryTransaction]) { + let context = localContainer.viewContext + context.perform { + transactions.forEach { transaction in + guard let userInfo = transaction.objectIDNotification().userInfo else { + return + } + + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [context]) + } + } + } +} diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 69b3ff3..f89249f 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -47,10 +47,9 @@ class DataStore: ObservableObject { self.clubs = store.registerCollection(synchronized: true) self.tournaments = store.registerCollection(synchronized: false) - + NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil) - } @objc func collectionWasUpdated(notification: Notification) { diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift new file mode 100644 index 0000000..7d69cbe --- /dev/null +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -0,0 +1,94 @@ +// +// FederalPlayer.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +extension ImportedPlayer { + var isAssimilated: Bool { + assimilation == "Oui" + } +} + +struct FederalPlayer { + var rank: Int + var lastName: String + var firstName: String + var country: String + var license: String + var points: Double? + var assimilation: String + var tournamentCount: Int? + var ligue: String + var clubCode: String + var club: String + var isMale: Bool + + var fullNameCanonical: String + + /* + ;RANG;NOM;PRENOM;Nationalité;N° Licence;POINTS;Assimilation;NB. DE TOURNOIS JOUES;LIGUE;CODE CLUB;CLUB; + */ + + var isManPlayer: Bool { + isMale + } + + var isAssimilated: Bool { + assimilation == "Oui" + } + + var currentRank: Int { + rank + } + + init?(_ data: String, isMale: Bool = false) { + self.isMale = isMale + + var result = data.components(separatedBy: .newlines).map { $0.trimmed } + result = result.reversed().drop(while: { + $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + }).reversed() as [String] + + result = Array(result.drop(while: { + $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + })) + + print(result) + if result.count < 11 { + return nil + } + if let _rank = Int(result[0]) { + rank = _rank + } else { + return nil + } + lastName = result[1] + firstName = result[2] + + fullNameCanonical = result[1].canonicalVersion + " " + result[2].canonicalVersion + country = result[3] + license = result[4] + +// let matches = result[5].matches(of: try! Regex("[0-9]{1,5}\\.00")) +// +// if matches.count == 1 { +// let pts = result[5][matches.first!.range] +// points = Double(pts.replacingOccurrences(of: ",", with: ".")) +// if pts.count < result[5].count { +// +// } +// } +// + points = Double(result[5].replacingOccurrences(of: ",", with: ".")) + assimilation = result[6] + tournamentCount = Int(result[7]) + ligue = result[8] + clubCode = result[9] + club = result[10] + } + +} diff --git a/PadelClub/Extensions/Array+Extensions.swift b/PadelClub/Extensions/Array+Extensions.swift new file mode 100644 index 0000000..baa4ea6 --- /dev/null +++ b/PadelClub/Extensions/Array+Extensions.swift @@ -0,0 +1,20 @@ +// +// Array+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import Foundation + +extension Array { + func chunked(into size: Int) -> [[Element]] { + return stride(from: 0, to: count, by: size).map { + Array(self[$0 ..< Swift.min($0 + size, count)]) + } + } + + func anySatisfy(_ p: (Element) -> Bool) -> Bool { + return !self.allSatisfy { !p($0) } + } +} diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift new file mode 100644 index 0000000..b7ec13c --- /dev/null +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -0,0 +1,14 @@ +// +// Date+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +extension Date { + var monthYearFormatted: String { + formatted(.dateTime.month(.wide).year(.defaultDigits)) + } +} diff --git a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift new file mode 100644 index 0000000..739ac41 --- /dev/null +++ b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift @@ -0,0 +1,25 @@ +// +// FixedWidthInteger+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import Foundation + +public extension FixedWidthInteger { + func ordinalFormattedSuffix() -> String { + switch self { + case 1: return "er" + default: return "ème" + } + } + + func ordinalFormatted() -> String { + self.formatted() + self.ordinalFormattedSuffix() + } + + var pluralSuffix: String { + self > 1 ? "s" : "" + } +} diff --git a/PadelClub/Extensions/Sequence+Extensions.swift b/PadelClub/Extensions/Sequence+Extensions.swift new file mode 100644 index 0000000..5de1531 --- /dev/null +++ b/PadelClub/Extensions/Sequence+Extensions.swift @@ -0,0 +1,16 @@ +// +// Sequence+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import Foundation + +extension Sequence { + func sorted(by keyPath: KeyPath) -> [Element] { + return sorted { a, b in + return a[keyPath: keyPath] < b[keyPath: keyPath] + } + } +} diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift new file mode 100644 index 0000000..e1e96fe --- /dev/null +++ b/PadelClub/Extensions/String+Extensions.swift @@ -0,0 +1,22 @@ +// +// String+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +extension String { + var trimmed: String { + trimmingCharacters(in: .whitespacesAndNewlines) + } + + func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { + components(separatedBy: characterSet).joined(separator:replacementString) + } + + var canonicalVersion: String { + trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased() + } +} diff --git a/PadelClub/Extensions/URL+Extensions.swift b/PadelClub/Extensions/URL+Extensions.swift new file mode 100644 index 0000000..2a13bde --- /dev/null +++ b/PadelClub/Extensions/URL+Extensions.swift @@ -0,0 +1,51 @@ +// +// URL+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +extension URL { + + static var savedDateFormatter: DateFormatter = { + let df = DateFormatter() + df.dateFormat = "DD/MM/yyyy" + return df + }() + + static var importDateFormatter: DateFormatter = { + let df = DateFormatter() + df.dateFormat = "MM-yyyy" + return df + }() + + var dateFromPath: Date { + let found = deletingPathExtension().path().components(separatedBy: "-").suffix(2).joined(separator: "-") + if let date = URL.importDateFormatter.date(from: found) { + return date + } else { + return Date() + } + } + + var index: Int { + if let i = path().dropLast(12).last?.wholeNumberValue { + return i + } + return 0 + } + + var manData: Bool { + path().contains("MESSIEURS") + } + + var womanData: Bool { + path().contains("DAMES") + } + + static var seed: URL? { + Bundle.main.url(forResource: "SeedData", withExtension: nil) + } +} diff --git a/PadelClub/Manager/FileImportManager.swift b/PadelClub/Manager/FileImportManager.swift new file mode 100644 index 0000000..4c06819 --- /dev/null +++ b/PadelClub/Manager/FileImportManager.swift @@ -0,0 +1,54 @@ +// +// FileImportManager.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +class FileImportManager { + static let shared = FileImportManager() + + func importDataFromFFT() async -> String? { + if let importingDate = SourceFile.mostRecentDateAvailable { + for source in SourceFile.allCases { + for fileURL in source.currentURLs { + let p = readCSV(inputFile: fileURL) + await importingChunkOfPlayers(p, importingDate: importingDate) + } + } + return URL.importDateFormatter.string(from: importingDate) + } + return nil + } + + + func readCSV(inputFile: URL) -> [FederalPlayer] { + do { + let fileContent = try String(contentsOf: inputFile) + return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) + } catch { + print("error: \(error)") // to do deal with errors + } + return [] + } + + func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + let lines = fileContent.components(separatedBy: "\n") + return lines.compactMap { line in + if line.components(separatedBy: ";").count < 10 { + } else { + let data = line.components(separatedBy: ";").joined(separator: "\n") + return FederalPlayer(data, isMale: isMale) + } + return nil + } + } + + func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { + for chunk in players.chunked(into: 1000) { + await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + } + } +} diff --git a/PadelClub/Manager/Network/NetworkManager.swift b/PadelClub/Manager/Network/NetworkManager.swift new file mode 100644 index 0000000..2fdf8fb --- /dev/null +++ b/PadelClub/Manager/Network/NetworkManager.swift @@ -0,0 +1,46 @@ +// +// NetworkManager.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +class NetworkManager { + static let shared: NetworkManager = NetworkManager() + + func removeRankingData(lastDateString: String, fileName: String) { + let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" + + let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! + let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") + try? FileManager.default.removeItem(at: destinationFileUrl) + } + + func downloadRankingData(lastDateString: String, fileName: String) async throws { + + let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" + + let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! + let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") + let fileURL = URL(string: "https://padelclub.app/static/\(dateString)") + + if FileManager.default.fileExists(atPath: destinationFileUrl.path()) { + return + } + var request = URLRequest(url:fileURL!) + request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition") + request.addValue("text/csv", forHTTPHeaderField: "Content-Type") + + let task = try await URLSession.shared.download(for: request) + if let urlResponse = task.1 as? HTTPURLResponse { + print(urlResponse.statusCode) + if urlResponse.statusCode == 200 { + try FileManager.default.copyItem(at: task.0, to: destinationFileUrl) + } else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" { + throw NetworkManagerError.fileNotYetAvailable + } + } + } +} diff --git a/PadelClub/Manager/Network/NetworkManagerError.swift b/PadelClub/Manager/Network/NetworkManagerError.swift new file mode 100644 index 0000000..04c31a8 --- /dev/null +++ b/PadelClub/Manager/Network/NetworkManagerError.swift @@ -0,0 +1,17 @@ +// +// NetworkManagerError.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import Foundation + +enum NetworkManagerError: LocalizedError { + case maintenance + case fileNotYetAvailable + case mailFailed + case mailNotSent //no network no error + case messageFailed + case messageNotSent //no network no error +} diff --git a/PadelClub/Manager/SourceFileManager.swift b/PadelClub/Manager/SourceFileManager.swift new file mode 100644 index 0000000..428e226 --- /dev/null +++ b/PadelClub/Manager/SourceFileManager.swift @@ -0,0 +1,73 @@ +// +// SourceFileManager.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import Foundation + +enum SourceFile: String, CaseIterable { + case dames = "DAMES" + case messieurs = "MESSIEURS" + + static var mostRecentDateAvailable: Date? { + allFiles(false).first?.dateFromPath + } + + static func removeAllFilesFromServer() { + let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let allFiles = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil) + allFiles.filter { $0.pathExtension == "csv" }.forEach { url in + try? FileManager.default.removeItem(at: url) + } + } + + static var allFiles: [URL] { + let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let allFiles = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil).filter({ url in + url.pathExtension == "csv" + }) + + return (allFiles + (Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil) ?? [])).sorted(by: \.dateFromPath).reversed() + } + + static func allFiles(_ isManPlayer: Bool) -> [URL] { + allFiles.filter({ url in + url.path().contains(isManPlayer ? SourceFile.messieurs.rawValue : SourceFile.dames.rawValue) + }) + } + + static func allFilesSortedByDate(_ isManPlayer: Bool) -> [URL] { + return allFiles(isManPlayer) + } + + var filesFromServer: [URL] { + let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let allFiles = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil) + return allFiles.filter{$0.pathExtension == "csv" && $0.path().contains(rawValue)} + } + + var currentURLs: [URL] { + var files = Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil)?.filter({ url in + url.path().contains(rawValue) + }) ?? [] + + files.append(contentsOf: filesFromServer) + + if let mostRecent = files.sorted(by: \.dateFromPath).reversed().first { + return files.filter({ $0.dateFromPath == mostRecent.dateFromPath }) + } else { + return [] + } + } + + var isMan: Bool { + switch self { + case .dames: + return false + default: + return true + } + } +} diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 89eaa2e..023f73d 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -10,12 +10,15 @@ import LeStorage @main struct PadelClubApp: App { + let persistenceController = PersistenceController.shared + var body: some Scene { WindowGroup { MainView() .onAppear { self._onAppear() } + .environment(\.managedObjectContext, persistenceController.localContainer.viewContext) } } diff --git a/PadelClub/Views/Navigation/Agenda/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift similarity index 100% rename from PadelClub/Views/Navigation/Agenda/AgendaDestination.swift rename to PadelClub/ViewModel/AgendaDestination.swift diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift new file mode 100644 index 0000000..3623064 --- /dev/null +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -0,0 +1,436 @@ +// +// SearchViewModel.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 07/02/2024. +// + +import SwiftUI + +class SearchViewModel: ObservableObject, Identifiable { + let id: UUID = UUID() + var allowSelection : Int = 0 + var user: User? = nil + var codeClub: String? = nil + var clubName: String? = nil + var ligueName: String? = nil + @Published var debouncableText: String = "" + @Published var searchText: String = "" + @Published var task: DispatchWorkItem? + @Published var computedSearchText: String = "" + @Published var tokens = [SearchToken]() + @Published var suggestedTokens = [SearchToken]() + @Published var dataSet: DataSet = .national + @Published var filterOption = PlayerFilterOption.all + @Published var hideAssimilation: Bool = false + @Published var ascending: Bool = true + @Published var sortOption: SortOption = .rank + @Published var selectedPlayers: Set = Set() + @Published var filterSelectionEnabled: Bool = false + var forcedSearch = false + var mostRecentDate: Date? = nil + + var selectionIsOver: Bool { + if allowSingleSelection && selectedPlayers.count == 1 { + return true + } else if allowMultipleSelection && selectedPlayers.count == allowSelection { + return true + } + + return false + } + + var allowMultipleSelection: Bool { + allowSelection > 1 || allowSelection == -1 + } + + var allowSingleSelection: Bool { + allowSelection == 1 + } + + var debounceTrigger: Double { + dataSet == .national ? 0.4 : 0.1 + } + + var throttleTrigger: Double { + dataSet == .national ? 0.15 : 0.1 + } + + var contentUnavailableMessage: String { + var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] + if tokens.isEmpty { + message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois. Dans ce pas, Padel Club ne pourra pas le trouver.") + } + return message.joined(separator: "\n") + } + + func getCodeClub() -> String? { + if let codeClub { return codeClub } +// if let userCodeClub = user?.player?.codeClub { return userCodeClub } + return nil + } + + func getClubName() -> String? { + if let clubName { return clubName } +// if let userClubName = user?.player?.clubName { return userClubName } + return nil + } + + func showIndex() -> Bool { + if dataSet == .national { return false } + if filterOption == .all { return false } + return true + } + + func prompt(forDataSet: DataSet) -> String { + switch forDataSet { + case .national: + if let mostRecentDate { + return "base fédérale \(mostRecentDate.monthYearFormatted)" + } else { + return "rechercher" + } + case .ligue: + return "dans cette ligue" + case .club: + return "dans ce club" + case .favorite: + return "dans mes favoris" + } + } + + func label(forDataSet: DataSet) -> String { + switch forDataSet { + case .national: + return "National" + case .ligue: + return (ligueName)?.capitalized ?? "Ma ligue" + case .club: + return (clubName)?.capitalized ?? "Mon club" + case .favorite: + return "Mes favoris" + } + } + + func words() -> [String] { + searchText.trimmed.components(separatedBy: .whitespaces) + } + + func wordsPredicates() -> NSPredicate? { + let words = words() + switch words.count { + case 2: + let predicates = [ + NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[0], words[1]), + NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[1], words[0]), + ] + return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) + default: + return nil + } + } + + func orPredicate() -> NSPredicate? { + var predicates : [NSPredicate] = [] + + switch tokens.first { + case .none: + if searchText.isEmpty == false { + let wordsPredicates = wordsPredicates() + if let wordsPredicates { + predicates.append(wordsPredicates) + } else { + predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", searchText)) + predicates.append(NSPredicate(format: "license contains[cd] %@", searchText)) + } + } + case .ligue: + if searchText.isEmpty { + predicates.append(NSPredicate(format: "ligueName == nil")) + } else { + predicates.append(NSPredicate(format: "ligueName contains[cd] %@", searchText)) + } + case .club: + if searchText.isEmpty { + predicates.append(NSPredicate(format: "clubName == nil")) + } else { + predicates.append(NSPredicate(format: "clubName contains[cd] %@", searchText)) + } + case .rankMoreThan: + if searchText.isEmpty || Int(searchText) == 0 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank >= %@", searchText)) + } + case .rankLessThan: + if searchText.isEmpty || Int(searchText) == 0 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank <= %@", searchText)) + } + case .rankBetween: + let values = searchText.components(separatedBy: ",") + if searchText.isEmpty || values.count != 2 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) + } + } + + if predicates.isEmpty { + return nil + } + return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) + } + + func predicate() -> NSPredicate? { + var predicates : [NSPredicate?] = [ + orPredicate(), + filterOption == .male ? + NSPredicate(format: "male == YES") : + nil, + filterOption == .female ? + NSPredicate(format: "male == NO") : + nil, + ] + + if let mostRecentDate { + predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + } + + if hideAssimilation { + predicates.append(NSPredicate(format: "assimilation == %@", "Non")) + } + + switch dataSet { + case .national: + break + case .ligue: + if let ligueName { + predicates.append(NSPredicate(format: "ligueName == %@", ligueName)) + } else { + predicates.append(NSPredicate(format: "ligueName == nil")) + } + case .club: + if let codeClub { + predicates.append(NSPredicate(format: "clubCode == %@", codeClub)) + } else { + predicates.append(NSPredicate(format: "clubCode == nil")) + } + case .favorite: + predicates.append(NSPredicate(format: "license == nil")) + } + + return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 })) + } + + + func sortDescriptors() -> [SortDescriptor] { + sortOption.sortDescriptors(ascending, dataSet: dataSet) + } + + func nsSortDescriptors() -> [NSSortDescriptor] { + sortDescriptors().map { NSSortDescriptor($0) } + } +} + +enum SearchToken: String, CaseIterable, Identifiable { + case club = "club" + case ligue = "ligue" + case rankMoreThan = "rang >" + case rankLessThan = "rang <" + case rankBetween = "rang <>" + + var id: String { + rawValue + } + + var message: String { + switch self { + case .club: + return "Taper le nom d'un club pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois." + case .ligue: + return "Taper le nom d'une ligue pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois." + case .rankMoreThan: + return "Taper un nombre pour chercher les joueurs ayant un classement supérieur ou égale." + case .rankLessThan: + return "Taper un nombre pour chercher les joueurs ayant un classement inférieur ou égale." + case .rankBetween: + return "Taper deux nombres séparés par une virgule pour chercher les joueurs dans cette intervalle de classement" + } + } + + var titleLabel: String { + switch self { + case .club: + return "Chercher un club" + case .ligue: + return "Chercher une ligue" + case .rankMoreThan, .rankLessThan: + return "Chercher un rang" + case .rankBetween: + return "Chercher une intervalle de classement" + } + } + + var localizedLabel: String { + switch self { + case .club: + return "Club" + case .ligue: + return "Ligue" + case .rankMoreThan: + return "Rang supérieur ou égale à" + case .rankLessThan: + return "Rang inférieur ou égale à" + case .rankBetween: + return "Rang entre deux valeurs" + } + } + + var shortLocalizedLabel: String { + switch self { + case .club: + return "Club" + case .ligue: + return "Ligue" + case .rankMoreThan: + return "Rang ≥" + case .rankLessThan: + return "Rang ≤" + case .rankBetween: + return "Rang ≥,≤" + } + } + + func icon() -> String { + switch self { + case .club: + return "house.and.flag.fill" + case .ligue: + return "house.and.flag.fill" + case .rankMoreThan: + return "figure.racquetball" + case .rankLessThan: + return "figure.racquetball" + case .rankBetween: + return "figure.racquetball" + } + } + + var systemImage: String { + switch self { + case .club: + return "house.and.flag.fill" + case .ligue: + return "house.and.flag.fill" + case .rankMoreThan: + return "figure.racquetball" + case .rankLessThan: + return "figure.racquetball" + case .rankBetween: + return "figure.racquetball" + } + } +} + +enum DataSet: Int, CaseIterable, Identifiable { + case national + case ligue + case club + case favorite + + var id: Int { rawValue } + var localizedLabel: String { + switch self { + case .national: + return "National" + case .ligue: + return "Ligue" + case .club: + return "Club" + case .favorite: + return "Favori" + } + } + + var tokens: [SearchToken] { + switch self { + case .national: + return [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween] + case .ligue: + return [.club, .rankMoreThan, .rankLessThan, .rankBetween] + case .club: + return [.rankMoreThan, .rankLessThan, .rankBetween] + case .favorite: + return [.rankMoreThan, .rankLessThan, .rankBetween] + } + } +} + +enum SortOption: Int, CaseIterable, Identifiable { + case name + case rank + case tournamentCount + case points + + var id: Int { self.rawValue } + var localizedLabel: String { + switch self { + case .name: + return "Nom" + case .rank: + return "Rang" + case .tournamentCount: + return "Tournoi" + case .points: + return "Points" + } + } + + func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor] { + switch self { + case .name: + return [SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation)] + case .rank: + if dataSet == .national { + return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)] + } else { + return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + } + case .tournamentCount: + return [SortDescriptor(\ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + case .points: + return [SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + } + } +} + +enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable { + case all = -1 + case male = 1 + case female = 0 + + var id: Int { rawValue } + + func icon() -> String { + switch self { + case .all: + return "Tous" + case .male: + return "Homme" + case .female: + return "Femme" + } + } + + var localizedPlayerLabel: String { + switch self { + case .female: + return "joueuse" + default: + return "joueur" + } + } + +} diff --git a/PadelClub/Model/NavigationDestination.swift b/PadelClub/ViewModel/TabDestination.swift similarity index 92% rename from PadelClub/Model/NavigationDestination.swift rename to PadelClub/ViewModel/TabDestination.swift index 0e756b9..5865c24 100644 --- a/PadelClub/Model/NavigationDestination.swift +++ b/PadelClub/ViewModel/TabDestination.swift @@ -1,5 +1,5 @@ // -// NavigationDestination.swift +// TabDestination.swift // PadelClub // // Created by Razmig Sarkissian on 29/02/2024. @@ -7,7 +7,7 @@ import Foundation -enum NavigationDestination: CaseIterable, Identifiable { +enum TabDestination: CaseIterable, Identifiable { var id: Self { self } diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift new file mode 100644 index 0000000..b8db9aa --- /dev/null +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -0,0 +1,44 @@ +// +// RowButtonView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import SwiftUI + +struct RowButtonView: View { + let title: String + var systemImage: String? = nil + var image: String? = nil + let action: () -> () + + var body: some View { + Button { + action() + } label: { + HStack { + Spacer() + if let systemImage { + Image(systemName: systemImage) + } + if let image { + Image(image) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + } + Text(title) + .foregroundColor(.white) + .frame(height: 32) + Spacer() + } + .font(.headline) + } + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + .tint(.launchScreenBackground) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + } +} diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index fd10f8e..22ea31c 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -90,22 +90,25 @@ struct ActivityView: View { } - ToolbarItemGroup(placement: .bottomBar) { + ToolbarItem(placement: .topBarLeading) { Button { filterEnabled.toggle() } label: { Label("Vues", systemImage: "line.3.horizontal.decrease.circle") .symbolVariant(filterEnabled ? .fill : .none) } + } + + ToolbarItem(placement: .topBarTrailing) { Button { } label: { - Label("Ajouter", systemImage: "plus") + Label("Ajouter", systemImage: "plus.circle.fill") } } } } - .navigationTitle(NavigationDestination.activity.title) + .navigationTitle(TabDestination.activity.title) .navigationDestination(for: Tournament.self) { tournament in TournamentView(tournament: tournament) } diff --git a/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift b/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift index bfdaa40..12d91c9 100644 --- a/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift @@ -6,43 +6,6 @@ // import SwiftUI -struct RowButtonView: View { - let title: String - var systemImage: String? = nil - var image: String? = nil - let action: () -> () - - var body: some View { - Button { - action() - } label: { - HStack { - Spacer() - if let systemImage { - Image(systemName: systemImage) - } - if let image { - Image(image) - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) - } - Text(title) - .foregroundColor(.white) - .frame(height: 32) - Spacer() - } - .font(.headline) - } - .frame(maxWidth: .infinity) - .buttonStyle(.borderedProminent) - .tint(.launchScreenBackground) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(.zero)) - } -} - - struct EmptyActivityView: View { var body: some View { diff --git a/PadelClub/Views/Navigation/WelcomeView.swift b/PadelClub/Views/Navigation/Agenda/WelcomeView.swift similarity index 100% rename from PadelClub/Views/Navigation/WelcomeView.swift rename to PadelClub/Views/Navigation/Agenda/WelcomeView.swift diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 4d986e5..9de1a2b 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -9,7 +9,29 @@ import SwiftUI struct MainView: View { @StateObject var dataStore = DataStore.shared + @AppStorage("importingFiles") var importingFiles: Bool = false + @State private var checkingFilesAttempt: Int = 0 + @State private var checkingFiles: Bool = false + + @AppStorage("lastDataSource") var lastDataSource: String? + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [], + animation: .default) + private var players: FetchedResults + + + var _mostRecentDateAvailable: Date? { + SourceFile.mostRecentDateAvailable + } + + var _lastDataSourceDate: Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + var body: some View { TabView { if dataStore.tournaments.isEmpty { @@ -29,23 +51,146 @@ struct MainView: View { .tabItem(for: .padelClub) } .environmentObject(dataStore) + .task { + await self._checkSourceFileAvailability() + } + .refreshable { + Task { + await self._checkSourceFileAvailability() + } + } + .overlay(alignment: .bottom) { + if importingFiles { + _activityStatusBoxView() + } else { + _activityStatusBoxView() + .deferredRendering(for: .seconds(5)) + } + } + } + + func _activityStatusBoxView() -> some View { + _activityStatus() + .font(.title3) + .frame(height: 32) + .padding() + .background { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(.white) + } + .shadow(radius: 2) + .offset(y: -64) + } + + @ViewBuilder + func _activityStatus() -> some View { + if importingFiles { + HStack(spacing: 20) { + ProgressView() + if let _mostRecentDateAvailable { + if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { + Text("import " + _mostRecentDateAvailable.monthYearFormatted) + } + } + } + } else if let _mostRecentDateAvailable { + if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { + Label(_mostRecentDateAvailable.monthYearFormatted + " disponible", systemImage: "exclamationmark.triangle") + .labelStyle(.titleAndIcon) + } else { + Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "checkmark") + .labelStyle(.titleAndIcon) + } + } } + private func _checkSourceFileAvailability() async { + + print("check internet") + print("check files on internet") + print("check if any files on internet are more recent than here") + checkingFiles = true + await fetchData() + checkingFilesAttempt += 1 + checkingFiles = false + + if let _mostRecentDateAvailable, _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { + _startImporting() + } + } + + private func _startImporting() { + importingFiles = true + Task { + lastDataSource = await FileImportManager.shared.importDataFromFFT() + importingFiles = false + } + } + + private func fetchData() async { + if let mostRecent = SourceFile.mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent { + await fetchData(fromDate: current) + } else { + await fetchData(fromDate: Date()) + } + } + + private func _removeAllData(fromDate current: Date) { + let lastStringDate = URL.importDateFormatter.string(from: current) + let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] + files.forEach { fileName in + NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName) + } + } + + private func fetchData(fromDate current: Date) async { + let lastStringDate = URL.importDateFormatter.string(from: current) + + let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] + do { + try await withThrowingTaskGroup(of: Void.self) { group in // Mark 1 + + for file in files { + group.addTask { + try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file) + } + } + + try await group.waitForAll() + } + + if current < Date() { + if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) { + await fetchData(fromDate: nextCurrent) + } + } + } catch { + print("downloadRankingData", error) + + if _mostRecentDateAvailable == nil { + if let previousDate = Calendar.current.date(byAdding: .month, value: -1, to: current) { + await fetchData(fromDate: previousDate) + } + } + } + + } + } fileprivate extension View { - func tabItem(for navigationDestination: NavigationDestination) -> some View { - modifier(TabItemModifier(navigationDestination: navigationDestination)) + func tabItem(for tabDestination: TabDestination) -> some View { + modifier(TabItemModifier(tabDestination: tabDestination)) } } fileprivate struct TabItemModifier: ViewModifier { - let navigationDestination: NavigationDestination + let tabDestination: TabDestination func body(content: Content) -> some View { content .tabItem { - Label(navigationDestination.title, systemImage: navigationDestination.image) + Label(tabDestination.title, systemImage: tabDestination.image) } } } diff --git a/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift b/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift new file mode 100644 index 0000000..089be05 --- /dev/null +++ b/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift @@ -0,0 +1,44 @@ +// +// TournamentButtonView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import SwiftUI + +struct TournamentButtonView: View { + let tournament: Tournament + @Binding var selectedId: String? + + var body: some View { + Button { + if selectedId == tournament.id { + tournament.navigationPath.removeAll() + selectedId = nil +// if tournament.navigationPath.isEmpty { +// selectedId = nil +// } else { +// tournament.navigationPath.removeLast() +// } + } else { + selectedId = tournament.id + } + } label: { + TournamentCellView(tournament: tournament) + .padding(8) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.black, lineWidth: 2) + ) + .fixedSize(horizontal: false, vertical: true) + } + .overlay(alignment: .top) { + if selectedId == tournament.id { + Image(systemName: "ellipsis") + .offset(y: -10) + } + } + + } +} diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index 35aba4e..cb5c08a 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -39,43 +39,6 @@ struct TournamentOrganizerView: View { } } } - - struct TournamentButtonView: View { - let tournament: Tournament - @Binding var selectedId: String? - - var body: some View { - Button { - if selectedId == tournament.id { - tournament.navigationPath.removeAll() - selectedId = nil -// if tournament.navigationPath.isEmpty { -// selectedId = nil -// } else { -// tournament.navigationPath.removeLast() -// } - } else { - selectedId = tournament.id - } - } label: { - TournamentCellView(tournament: tournament) - .padding(8) - .overlay( - RoundedRectangle(cornerRadius: 20) - .stroke(Color.black, lineWidth: 2) - ) - .fixedSize(horizontal: false, vertical: true) - } - .overlay(alignment: .top) { - if selectedId == tournament.id { - Image(systemName: "ellipsis") - .offset(y: -10) - } - } - - } - } - } #Preview { diff --git a/PadelClub/Views/Navigation/PadelClubView.swift b/PadelClub/Views/Navigation/PadelClubView.swift index 75d7c68..a618a5e 100644 --- a/PadelClub/Views/Navigation/PadelClubView.swift +++ b/PadelClub/Views/Navigation/PadelClubView.swift @@ -6,14 +6,155 @@ // import SwiftUI +import SwiftData struct PadelClubView: View { + @State private var checkingFilesAttempt: Int = 0 + @State private var checkingFiles: Bool = false + @State private var importingFiles: Bool = false + + @AppStorage("lastDataSource") var lastDataSource: String? + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [], + animation: .default) + private var players: FetchedResults + + + var _mostRecentDateAvailable: Date? { + SourceFile.mostRecentDateAvailable + } + + var _lastDataSourceDate: Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + var body: some View { NavigationStack { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - .navigationTitle(NavigationDestination.padelClub.title) + List { + if let _lastDataSourceDate { + Section { + HStack { + VStack(alignment: .leading) { + Text("Classement mensuel utilisé").font(.caption).foregroundStyle(.secondary) + Text(_lastDataSourceDate.monthYearFormatted) + } + Spacer() + Image(systemName: "checkmark") + } + } + } + +// +// if players.isEmpty { +// ContentUnavailableView { +// Label("Aucun joueur importé", systemImage: "person.slash") +// } description: { +// Text("Padel peut importer toutes les données publique de la FFT concernant tous les compétiteurs et compétitrices.") +// } actions: { +// RowButtonView(title: "Démarrer l'importation") { +// _startImporting() +// } +// } +// } + } + .navigationTitle(TabDestination.padelClub.title) +// .task { +// await self._checkSourceFileAvailability() +// } +// .refreshable { +// Task { +// await self._checkSourceFileAvailability() +// } +// } + } + } + + @ViewBuilder + func _activityStatus() -> some View { + if checkingFiles || importingFiles { + ProgressView() + } else if let _mostRecentDateAvailable { + if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { + Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation") + } else { + Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "checkmark").labelStyle(.titleAndIcon) + } + } else { + Text("Aucune donnée disponible") + } + } + + private func _checkSourceFileAvailability() async { + + print("check internet") + print("check files on internet") + print("check if any files on internet are more recent than here") + checkingFiles = true + await fetchData() + checkingFilesAttempt += 1 + checkingFiles = false + } + + private func _startImporting() { + importingFiles = true + Task { + lastDataSource = await FileImportManager.shared.importDataFromFFT() + importingFiles = false + } + } + + private func fetchData() async { + if let mostRecent = SourceFile.mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent { + await fetchData(fromDate: current) + } else { + await fetchData(fromDate: Date()) + } + } + + private func _removeAllData(fromDate current: Date) { + let lastStringDate = URL.importDateFormatter.string(from: current) + let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] + files.forEach { fileName in + NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName) } } + + private func fetchData(fromDate current: Date) async { + let lastStringDate = URL.importDateFormatter.string(from: current) + + let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] + do { + try await withThrowingTaskGroup(of: Void.self) { group in // Mark 1 + + for file in files { + group.addTask { + try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file) + } + } + + try await group.waitForAll() + } + + if current < Date() { + if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) { + await fetchData(fromDate: nextCurrent) + } + } + } catch { + print("downloadRankingData", error) + + if _mostRecentDateAvailable == nil { + if let previousDate = Calendar.current.date(byAdding: .month, value: -1, to: current) { + await fetchData(fromDate: previousDate) + } + } + } + + } + } #Preview { diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 1a08e01..e903dcc 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -10,8 +10,14 @@ import SwiftUI struct ToolboxView: View { var body: some View { NavigationStack { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - .navigationTitle(NavigationDestination.toolbox.title) + List { + NavigationLink { + SelectablePlayerListView() + } label: { + Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") + } + } + .navigationTitle(TabDestination.toolbox.title) } } } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index e124d21..4820bc2 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -22,8 +22,8 @@ struct UmpireView: View { } label: { Label("Abonnement", systemImage: "tennisball.circle.fill") } - } + .navigationTitle("Juge-Arbitre") } } } diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift new file mode 100644 index 0000000..b5b46e8 --- /dev/null +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -0,0 +1,71 @@ +// +// ImportedPlayerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import SwiftUI + +struct ImportedPlayerView: View { + let player: ImportedPlayer + var hideLigue: Bool = false + var hideClub: Bool = false + var hidePoints: Bool = false + var index: Int? = nil + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(player.lastName!.capitalized) + Text(player.firstName!.capitalized) + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.rank.formatted()).italic(player.isAssimilated) + .font(.title3) + Text(player.rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + + if hidePoints == false { + + HStack(alignment: .lastTextBaseline, spacing: 0) { + let pts = player.points + if pts > 0 { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + HStack(alignment: .lastTextBaseline, spacing: 0) { + if player.tournamentCount > 0 { + Text(player.tournamentCount.formatted()).font(.title3) + Text(" tournoi" + player.tournamentCount.pluralSuffix).font(.caption) + } + } + } + } + + Text(player.clubName!) + .font(.caption) + Text(player.ligueName!) + .font(.caption) + } + } +} diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift new file mode 100644 index 0000000..4aa9eb9 --- /dev/null +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -0,0 +1,442 @@ +// +// SelectablePlayerListView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 10/02/2024. +// + +import SwiftUI +import CoreData +import Combine +import LeStorage + +typealias PlayerSelectionAction = ((Set) -> ()) +typealias ContentUnavailableAction = ((SearchViewModel) -> ()) + +struct SelectablePlayerListView: View { + let allowSelection: Int + let playerSelectionAction: PlayerSelectionAction? + let contentUnavailableAction: ContentUnavailableAction? + + @StateObject private var searchViewModel: SearchViewModel + @Environment(\.dismiss) var dismiss + @AppStorage("lastDataSource") var lastDataSource: String? + @AppStorage("importingFiles") var importingFiles: Bool = false + + @State private var searchText: String = "" + var mostRecentDate: Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + + init(allowSelection: Int = 0, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + self.allowSelection = allowSelection + self.playerSelectionAction = playerSelectionAction + self.contentUnavailableAction = contentUnavailableAction + let searchViewModel = SearchViewModel() + searchViewModel.user = user + searchViewModel.allowSelection = allowSelection + searchViewModel.codeClub = fromPlayer?.clubCode ?? codeClub + searchViewModel.clubName = nil + searchViewModel.ligueName = fromPlayer?.ligue ?? ligue + searchViewModel.dataSet = dataSet + searchViewModel.filterOption = fromPlayer == nil ? filterOption : fromPlayer!.isManPlayer ? .male : .female + searchViewModel.hideAssimilation = hideAssimilation + searchViewModel.ascending = ascending + searchViewModel.sortOption = sortOption + _searchViewModel = StateObject(wrappedValue: searchViewModel) + } + + var body: some View { + VStack(spacing: 0) { + if importingFiles == false { + if searchViewModel.filterSelectionEnabled == false { + Picker(selection: $searchViewModel.filterOption) { + ForEach(PlayerFilterOption.allCases, id: \.self) { scope in + Text(scope.icon().capitalized) + } + } label: { + } + .pickerStyle(.segmented) + .padding(.bottom) + .padding(.horizontal) + .background(Material.thick) + Divider() + } + MySearchView(searchViewModel: searchViewModel, contentUnavailableAction: contentUnavailableAction) + .environment(\.editMode, searchViewModel.allowMultipleSelection ? .constant(.active) : .constant(.inactive)) + .searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, placement: .navigationBarDrawer(displayMode: .always), prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in + Text(token.shortLocalizedLabel) + }) + // .searchSuggestions({ + // ForEach(searchViewModel.suggestedTokens) { token in + // Button { + // searchViewModel.tokens.append(token) + // } label: { + // Label(token.localizedLabel, systemImage: token.icon()) + // } + // } + // }) + + .onReceive( + searchViewModel.$debouncableText + .debounce(for: .seconds(searchViewModel.debounceTrigger), scheduler: DispatchQueue.main) + .throttle(for: .seconds(searchViewModel.throttleTrigger), scheduler: DispatchQueue.main, latest: true) + ) { + guard !$0.isEmpty else { + if searchViewModel.searchText.isEmpty == false { + searchViewModel.searchText = "" + } + return + } + print(">> searching for: \($0)") + searchViewModel.searchText = $0 + } + .scrollDismissesKeyboard(.immediately) + .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) +// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) + .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) + .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) + .navigationBarTitleDisplayMode(.inline) + } else { + List { + + } + } + } + .id(importingFiles) + .overlay { + if let importedFile = SourceFile.mostRecentDateAvailable, importingFiles { + ContentUnavailableView("Importation en cours", systemImage: "square.and.arrow.down", description: Text("Padel Club récupère les données de \(importedFile.monthYearFormatted)")) + + } + } + .onAppear { + searchViewModel.mostRecentDate = mostRecentDate + if searchViewModel.tokens.isEmpty { + searchViewModel.debouncableText.removeAll() + searchViewModel.searchText.removeAll() + } + searchViewModel.allowSelection = allowSelection + searchViewModel.selectedPlayers.removeAll() + searchViewModel.hideAssimilation = false + searchViewModel.ascending = true + searchViewModel.sortOption = .rank + searchViewModel.suggestedTokens = searchViewModel.dataSet.tokens +// searchViewModel.fromPlayer = nil +// searchViewModel.codeClub = nil +// searchViewModel.ligueName = nil +// searchViewModel.user = nil +// searchViewModel.dataSet = .national +// searchViewModel.filterOption = .all + } + .onChange(of: searchViewModel.selectedPlayers) { + + if let playerSelectionAction, searchViewModel.selectionIsOver { + playerSelectionAction(searchViewModel.selectedPlayers) + dismiss() + } + + if searchViewModel.tokens.isEmpty && searchViewModel.searchText.isEmpty == false { + searchViewModel.debouncableText = "" + } + + if searchViewModel.selectedPlayers.isEmpty && searchViewModel.filterSelectionEnabled { + searchViewModel.filterSelectionEnabled = false + } + } + .onChange(of: searchViewModel.dataSet) { + searchViewModel.suggestedTokens = searchViewModel.dataSet.tokens + if searchViewModel.filterSelectionEnabled { + searchViewModel.filterSelectionEnabled = false + } + } + .toolbar { + if searchViewModel.allowMultipleSelection { + ToolbarItemGroup(placement: .topBarLeading) { + Button(role: .cancel) { + searchViewModel.selectedPlayers.removeAll() + dismiss() + } label: { + Text("Annuler") + } + } + } + } +// .modifierWithCondition(searchViewModel.user != nil) { thisView in +// thisView + .toolbarTitleMenu { + Picker(selection: $searchViewModel.dataSet) { + ForEach(DataSet.allCases) { dataSet in + Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet) + } + } label: { + + } + } +// } +// .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) { +// ZStack { +// HStack{ +// Button { +// searchViewModel.filterSelectionEnabled.toggle() +// } label: { +// if searchViewModel.filterSelectionEnabled { +// Image(systemName: "line.3.horizontal.decrease.circle.fill") +// } else { +// Image(systemName: "line.3.horizontal.decrease.circle") +// } +// } +// Spacer() +// } +// Button { +// if let playerSelectionAction { +// playerSelectionAction(searchViewModel.selectedPlayers) +// } +// dismiss() +// } label: { +// Text("Ajouter le" + searchViewModel.selectedPlayers.count.pluralSuffix + " \(searchViewModel.selectedPlayers.count) joueur" + searchViewModel.selectedPlayers.count.pluralSuffix) +// } +// .buttonStyle(.borderedProminent) +// } +// } + } +} + + +struct MySearchView: View { + @Environment(\.isSearching) private var isSearching + @Environment(\.dismissSearch) private var dismissSearch + @Environment(\.editMode) var editMode + @ObservedObject var searchViewModel: SearchViewModel + + @FetchRequest private var players: FetchedResults + let contentUnavailableAction: ContentUnavailableAction? + + init(searchViewModel: SearchViewModel, contentUnavailableAction: ContentUnavailableAction? = nil) { + self.contentUnavailableAction = contentUnavailableAction + _searchViewModel = ObservedObject(wrappedValue: searchViewModel) + _players = FetchRequest(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate()) + } + + var body: some View { + playersView + .overlay { + overlayView() + } + .onChange(of: isSearching) { + if isSearching && searchViewModel.filterSelectionEnabled { + searchViewModel.filterSelectionEnabled = false + } + } + .onChange(of: searchViewModel.filterSelectionEnabled) { + if searchViewModel.filterSelectionEnabled && isSearching { + dismissSearch() + } + } + .listStyle(.grouped) + .headerProminence(.increased) + .scrollDismissesKeyboard(.immediately) + .onChange(of: searchViewModel.searchText) { + search() + } + .onChange(of: searchViewModel.filterOption) { + search() + } + .onChange(of: searchViewModel.dataSet) { + search() + } + .onChange(of: searchViewModel.sortOption) { + sort() + } + .onChange(of: searchViewModel.ascending) { + sort() + } + .onChange(of: searchViewModel.hideAssimilation) { + search() + } + } + + var specificBugFixUUID: String { + if searchViewModel.dataSet == .national { + return UUID().uuidString + } else { + if searchViewModel.tokens.isEmpty && isSearching { + return UUID().uuidString + } + return "specificBugFixUUID" + } + } + + @ViewBuilder + var playersView: some View { + if searchViewModel.allowMultipleSelection { + List(selection: $searchViewModel.selectedPlayers) { + if searchViewModel.filterSelectionEnabled { + let array = Array(searchViewModel.selectedPlayers) + Section { + ForEach(array) { player in + ImportedPlayerView(player: player) + } + .onDelete { indexSet in + for index in indexSet { + let p = array[index] + searchViewModel.selectedPlayers.remove(p) + } + } + } header: { + Text(searchViewModel.selectedPlayers.count.formatted() + " " + searchViewModel.filterOption.localizedPlayerLabel + searchViewModel.selectedPlayers.count.pluralSuffix) + } + } else { + Section { + ForEach(players, id: \.self) { player in + ImportedPlayerView(player: player, index: nil) + } + } header: { + if players.isEmpty == false { + headerView() + } + } + } + } + .id(specificBugFixUUID) + } else { + List { + if searchViewModel.dataSet == .national { + if searchViewModel.allowSingleSelection { + Section { + ForEach(players) { player in + Button { + searchViewModel.selectedPlayers.insert(player) + } label: { + ImportedPlayerView(player: player) + } + .buttonStyle(.plain) + } + } header: { + if players.isEmpty == false { + headerView() + } + } + .id(UUID()) + } else { + Section { + ForEach(players) { player in + ImportedPlayerView(player: player) + } + } header: { + if players.isEmpty == false { + headerView() + } + } + .id(UUID()) + } + } else { + Section { + ForEach(players.indices, id: \.self) { index in + let player = players[index] + if searchViewModel.allowSingleSelection { + Button { + searchViewModel.selectedPlayers.insert(player) + } label: { + ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity) + .buttonStyle(.plain) + } else { + ImportedPlayerView(player: player) + } + } + } header: { + if players.isEmpty == false { + headerView() + } + } + .id(UUID()) + } + } + } + } + + private func headerView() -> some View { + HStack { + Text(players.count.formatted() + " " + searchViewModel.filterOption.localizedPlayerLabel + players.count.pluralSuffix) + Spacer() + Menu { + Section { + ForEach(SortOption.allCases) { option in + Toggle(isOn: .init(get: { + return searchViewModel.sortOption == option + }, set: { value in + if searchViewModel.sortOption == option { + searchViewModel.ascending.toggle() + } + searchViewModel.sortOption = option + })) { + Label(option.localizedLabel, systemImage: searchViewModel.sortOption == option ? (searchViewModel.ascending ? "chevron.up" : "chevron.down") : "") + } + } + } header: { + Text("Trier par") + } + Divider() + Section { + Toggle(isOn: .init(get: { + return searchViewModel.hideAssimilation == false + }, set: { value in + searchViewModel.hideAssimilation.toggle() + })) { + Text("Afficher") + } + Toggle(isOn: .init(get: { + return searchViewModel.hideAssimilation == true + }, set: { value in + searchViewModel.hideAssimilation.toggle() + })) { + Text("Masquer") + } + } header: { + Text("Assimilés") + } + } label: { + Label(searchViewModel.sortOption.localizedLabel, systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down") + } + } + } + + @ViewBuilder + func overlayView() -> some View { + if let token = searchViewModel.tokens.first, searchViewModel.searchText.isEmpty { + ContentUnavailableView(token.titleLabel, systemImage: token.systemImage, description: Text(token.message)) + } else if players.isEmpty && searchViewModel.filterSelectionEnabled == false && searchViewModel.searchText.isEmpty == false { + ContentUnavailableView { + Label("Aucun résultat pour «\(searchViewModel.searchText)»", systemImage: "magnifyingglass") + } description: { + Text(searchViewModel.contentUnavailableMessage) + } actions: { + + Button { + searchViewModel.debouncableText = "" + } label: { + Text("lancer une nouvelle recherche") + } + if let contentUnavailableAction { + Button { + contentUnavailableAction(searchViewModel) + } label: { + Text("créer \(searchViewModel.searchText)") + } + } + } + } + } + + private func search() { + //players.nsPredicate = searchViewModel.predicate() + } + + private func sort() { + //players.nsSortDescriptors = searchViewModel.nsSortDescriptors() + } +} diff --git a/PadelClub/Views/Tournament/Screen/PresentationContext.swift b/PadelClub/Views/Tournament/Screen/PresentationContext.swift new file mode 100644 index 0000000..81926e6 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/PresentationContext.swift @@ -0,0 +1,13 @@ +// +// PresentationContext.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import Foundation + +enum PresentationContext { + case agenda + case organizer +} diff --git a/PadelClub/Views/Tournament/Screen/Screen.swift b/PadelClub/Views/Tournament/Screen/Screen.swift new file mode 100644 index 0000000..b9dbb5a --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Screen.swift @@ -0,0 +1,13 @@ +// +// Screen.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/03/2024. +// + +import Foundation + +enum Screen: String, Codable { + case inscription + case groupStage +} diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index cc287af..22660c7 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -7,16 +7,6 @@ import SwiftUI -enum Screen: String, Codable { - case inscription - case groupStage -} - -enum PresentationContext { - case agenda - case organizer -} - struct TournamentView: View { @State var tournament: Tournament var presentationContext: PresentationContext = .agenda @@ -66,7 +56,3 @@ struct TournamentView: View { } } } -// -//#Preview { -// TournamentView(tournament: Tournament(name: "", club_id: "", category: 0, playerCount: 0)) -//} diff --git a/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift b/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift new file mode 100644 index 0000000..98c4591 --- /dev/null +++ b/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift @@ -0,0 +1,55 @@ +// +// DeferredViewModifier.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/03/2024. +// + +import SwiftUI + +/// Defers the rendering of a view for the given period. +/// +/// For example: +/// +/// ```swift +/// Text("Hello, world!") +/// .deferredRendering(for: .seconds(5)) +/// ``` +/// +/// will not display the text "Hello, world!" until five seconds after the +/// view is initially rendered. If the view is destroyed within the delay, +/// it never renders. +/// +/// This is based on code xwritten by Yonat and Charlton Provatas on +/// Stack Overflow, see https://stackoverflow.com/a/74765430/1558022 +/// +private struct DeferredViewModifier: ViewModifier { + + let delay: DispatchTimeInterval + + func body(content: Content) -> some View { + _content(content) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.shouldHide = true + } + } + } + + @ViewBuilder + private func _content(_ content: Content) -> some View { + if shouldHide == false { + content + } else { + content.hidden() + } + } + + @State private var shouldHide = false +} + +extension View { + func deferredRendering(for delay: DispatchTimeInterval) -> some View { + modifier(DeferredViewModifier(delay: delay)) + } +}