From c27a524fc5f0d7e3b4c57773398ec2914e6a59f7 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Dec 2024 16:32:30 +0100 Subject: [PATCH 01/13] Adds patch to reset logs --- PadelClub/Utils/Patcher.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/PadelClub/Utils/Patcher.swift b/PadelClub/Utils/Patcher.swift index 66a71ee..7bbd46b 100644 --- a/PadelClub/Utils/Patcher.swift +++ b/PadelClub/Utils/Patcher.swift @@ -16,6 +16,7 @@ enum Patch: String, CaseIterable { case alexisLeDu case importDataFromDevToProd case fixMissingMatches + case cleanLogs var id: String { return "padelclub.app.patch.\(self.rawValue)" @@ -31,7 +32,7 @@ class Patcher { } static func patchIfPossible(_ patch: Patch) { - if UserDefaults.standard.value(forKey: patch.id) == nil { +// if UserDefaults.standard.value(forKey: patch.id) == nil { do { Logger.log(">>> Patches \(patch.rawValue)...") try self._applyPatch(patch) @@ -39,7 +40,7 @@ class Patcher { } catch { Logger.error(error) } - } +// } } fileprivate static func _applyPatch(_ patch: Patch) throws { @@ -47,6 +48,7 @@ class Patcher { case .alexisLeDu: self._patchAlexisLeDu() case .importDataFromDevToProd: try self._importDataFromDev() case .fixMissingMatches: self._patchMissingMatches() + case .cleanLogs: self._cleanLogs() } } @@ -161,4 +163,7 @@ class Patcher { } + fileprivate static func _cleanLogs() { + StoreCenter.main.resetLoggingCollections() + } } From f6cf2c45b3f29bd4b1ceb732b6efcddced9eb3b8 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 5 Dec 2024 16:40:57 +0100 Subject: [PATCH 02/13] v1.0.35 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b4b2873..f9760af 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.34; + MARKETING_VERSION = 1.0.35; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.34; + MARKETING_VERSION = 1.0.35; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 033901d97c8f1ab46386654f48bae9ff72d8b8ee Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 6 Dec 2024 18:32:37 +0100 Subject: [PATCH 03/13] Fix issue --- PadelClub/Utils/Patcher.swift | 232 +++++++++++++++++----------------- 1 file changed, 113 insertions(+), 119 deletions(-) diff --git a/PadelClub/Utils/Patcher.swift b/PadelClub/Utils/Patcher.swift index 7bbd46b..6841bf4 100644 --- a/PadelClub/Utils/Patcher.swift +++ b/PadelClub/Utils/Patcher.swift @@ -13,9 +13,6 @@ enum PatchError: Error { } enum Patch: String, CaseIterable { - case alexisLeDu - case importDataFromDevToProd - case fixMissingMatches case cleanLogs var id: String { @@ -32,7 +29,7 @@ class Patcher { } static func patchIfPossible(_ patch: Patch) { -// if UserDefaults.standard.value(forKey: patch.id) == nil { + if UserDefaults.standard.value(forKey: patch.id) == nil { do { Logger.log(">>> Patches \(patch.rawValue)...") try self._applyPatch(patch) @@ -40,128 +37,125 @@ class Patcher { } catch { Logger.error(error) } -// } + } } fileprivate static func _applyPatch(_ patch: Patch) throws { switch patch { - case .alexisLeDu: self._patchAlexisLeDu() - case .importDataFromDevToProd: try self._importDataFromDev() - case .fixMissingMatches: self._patchMissingMatches() case .cleanLogs: self._cleanLogs() } } - - fileprivate static func _patchAlexisLeDu() { - guard StoreCenter.main.userId == "94f45ed2-8938-4c32-a4b6-e4525073dd33" else { return } - - let clubs = DataStore.shared.clubs - StoreCenter.main.resetApiCalls(collection: clubs) -// clubs.resetApiCalls() - - for club in clubs.filter({ $0.creator == "d5060b89-e979-4c19-bf78-e459a6ed5318"}) { - club.creator = StoreCenter.main.userId - clubs.writeChangeAndInsertOnServer(instance: club) - } - - } - - fileprivate static func _importDataFromDev() throws { - - let devServices = Services(url: "https://xlr.alwaysdata.net/roads/") - guard devServices.hasToken() else { - return - } - guard StoreCenter.main.synchronizationApiURL == "https://padelclub.app/roads/" else { - return - } - - guard let userId = StoreCenter.main.userId else { - return - } - - try StoreCenter.main.migrateToken(devServices) - - - let myClubs: [Club] = DataStore.shared.clubs.filter { $0.creator == userId } - let clubIds: [String] = myClubs.map { $0.id } - - myClubs.forEach { club in - DataStore.shared.clubs.insertIntoCurrentService(item: club) - - let courts = DataStore.shared.courts.filter { clubIds.contains($0.club) } - for court in courts { - DataStore.shared.courts.insertIntoCurrentService(item: court) - } - } - - DataStore.shared.user.clubs = Array(clubIds) - DataStore.shared.saveUser() - - DataStore.shared.events.insertAllIntoCurrentService() - DataStore.shared.tournaments.insertAllIntoCurrentService() - DataStore.shared.dateIntervals.insertAllIntoCurrentService() - - for tournament in DataStore.shared.tournaments { - let store = tournament.tournamentStore - - Task { // need to wait for the collections to load - try await Task.sleep(until: .now + .seconds(2)) - - store.teamRegistrations.insertAllIntoCurrentService() - store.rounds.insertAllIntoCurrentService() - store.groupStages.insertAllIntoCurrentService() - store.matches.insertAllIntoCurrentService() - store.playerRegistrations.insertAllIntoCurrentService() - store.teamScores.insertAllIntoCurrentService() - - } - } - - } - - fileprivate static func _patchMissingMatches() { - - guard let url = StoreCenter.main.synchronizationApiURL else { - return - } - guard url == "https://padelclub.app/roads/" else { - return - } - let services = Services(url: url) - - for tournament in DataStore.shared.tournaments { - - let store = tournament.tournamentStore - let identifier = StoreIdentifier(value: tournament.id, parameterName: "tournament") - - Task { - - do { - // if nothing is online we upload the data - let matches: [Match] = try await services.get(identifier: identifier) - if matches.isEmpty { - store.matches.insertAllIntoCurrentService() - } - - let playerRegistrations: [PlayerRegistration] = try await services.get(identifier: identifier) - if playerRegistrations.isEmpty { - store.playerRegistrations.insertAllIntoCurrentService() - } - - let teamScores: [TeamScore] = try await services.get(identifier: identifier) - if teamScores.isEmpty { - store.teamScores.insertAllIntoCurrentService() - } - - } catch { - Logger.error(error) - } - - } - } - - } +// +// fileprivate static func _patchAlexisLeDu() { +// guard StoreCenter.main.userId == "94f45ed2-8938-4c32-a4b6-e4525073dd33" else { return } +// +// let clubs = DataStore.shared.clubs +// StoreCenter.main.resetApiCalls(collection: clubs) +//// clubs.resetApiCalls() +// +// for club in clubs.filter({ $0.creator == "d5060b89-e979-4c19-bf78-e459a6ed5318"}) { +// club.creator = StoreCenter.main.userId +// clubs.writeChangeAndInsertOnServer(instance: club) +// } +// +// } +// +// fileprivate static func _importDataFromDev() throws { +// +// let devServices = Services(url: "https://xlr.alwaysdata.net/roads/") +// guard devServices.hasToken() else { +// return +// } +// guard StoreCenter.main.synchronizationApiURL == "https://padelclub.app/roads/" else { +// return +// } +// +// guard let userId = StoreCenter.main.userId else { +// return +// } +// +// try StoreCenter.main.migrateToken(devServices) +// +// +// let myClubs: [Club] = DataStore.shared.clubs.filter { $0.creator == userId } +// let clubIds: [String] = myClubs.map { $0.id } +// +// myClubs.forEach { club in +// DataStore.shared.clubs.insertIntoCurrentService(item: club) +// +// let courts = DataStore.shared.courts.filter { clubIds.contains($0.club) } +// for court in courts { +// DataStore.shared.courts.insertIntoCurrentService(item: court) +// } +// } +// +// DataStore.shared.user.clubs = Array(clubIds) +// DataStore.shared.saveUser() +// +// DataStore.shared.events.insertAllIntoCurrentService() +// DataStore.shared.tournaments.insertAllIntoCurrentService() +// DataStore.shared.dateIntervals.insertAllIntoCurrentService() +// +// for tournament in DataStore.shared.tournaments { +// let store = tournament.tournamentStore +// +// Task { // need to wait for the collections to load +// try await Task.sleep(until: .now + .seconds(2)) +// +// store.teamRegistrations.insertAllIntoCurrentService() +// store.rounds.insertAllIntoCurrentService() +// store.groupStages.insertAllIntoCurrentService() +// store.matches.insertAllIntoCurrentService() +// store.playerRegistrations.insertAllIntoCurrentService() +// store.teamScores.insertAllIntoCurrentService() +// +// } +// } +// +// } +// +// fileprivate static func _patchMissingMatches() { +// +// guard let url = StoreCenter.main.synchronizationApiURL else { +// return +// } +// guard url == "https://padelclub.app/roads/" else { +// return +// } +// let services = Services(url: url) +// +// for tournament in DataStore.shared.tournaments { +// +// let store = tournament.tournamentStore +// let identifier = StoreIdentifier(value: tournament.id, parameterName: "tournament") +// +// Task { +// +// do { +// // if nothing is online we upload the data +// let matches: [Match] = try await services.get(identifier: identifier) +// if matches.isEmpty { +// store.matches.insertAllIntoCurrentService() +// } +// +// let playerRegistrations: [PlayerRegistration] = try await services.get(identifier: identifier) +// if playerRegistrations.isEmpty { +// store.playerRegistrations.insertAllIntoCurrentService() +// } +// +// let teamScores: [TeamScore] = try await services.get(identifier: identifier) +// if teamScores.isEmpty { +// store.teamScores.insertAllIntoCurrentService() +// } +// +// } catch { +// Logger.error(error) +// } +// +// } +// } +// +// } fileprivate static func _cleanLogs() { StoreCenter.main.resetLoggingCollections() From fc3708fbc3640e819c1835f0e981d527cf6c188e Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 6 Dec 2024 20:21:17 +0100 Subject: [PATCH 04/13] fix mobile number detection --- PadelClub/Extensions/String+Extensions.swift | 3 +-- .../Views/Calling/Components/PlayersWithoutContactView.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 37094b9..4655383 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -166,8 +166,7 @@ extension String { // MARK: - FFT Source Importing extension String { enum RegexStatic { - static let mobileNumber = /^0[6-7]/ - //static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/ + static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ } func isMobileNumber() -> Bool { diff --git a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift index 4e8d78e..cffae6e 100644 --- a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift +++ b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift @@ -31,7 +31,7 @@ struct PlayersWithoutContactView: View { } } - let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil }) + let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false }) DisclosureGroup { ForEach(withoutPhones) { player in NavigationLink { @@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View { LabeledContent { Text(withoutPhones.count.formatted()) } label: { - Text("Joueurs sans téléphone") + Text("Joueurs sans téléphone portable") } } } header: { From 607a5e65bd8e1b57d485042d29386a43df7a5221 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 6 Dec 2024 20:21:58 +0100 Subject: [PATCH 05/13] v1.0.37 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f9760af..97a0eb4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.35; + MARKETING_VERSION = 1.0.37; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.35; + MARKETING_VERSION = 1.0.37; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 2cf955f378354a07e9514830960c1875c32467f4 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 7 Dec 2024 16:44:42 +0100 Subject: [PATCH 06/13] v1.0.38 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Utils/PadelRule.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 97a0eb4..91e2516 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.37; + MARKETING_VERSION = 1.0.38; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.37; + MARKETING_VERSION = 1.0.38; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index e69e5ee..9a4e040 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -540,7 +540,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case .p25: switch count { case 9...12: - return [17, 13, 11, 9, 7, 5, 4, 3, 2, 1] + return [17, 15, 13, 11, 9, 7, 5, 4, 3, 2, 1] case 13...16: return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1] case 17...20: From afe7aedb29299dfc2de7fa5229c635ee468ba8a5 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 14:01:18 +0100 Subject: [PATCH 07/13] fix ranking (cherry picked from commit 3c0fa45153adea38a597b8a952569fbcfa873e06) --- PadelClub/Data/TeamRegistration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 14909a2..da189ec 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -466,7 +466,7 @@ final class TeamRegistration: ModelObject, Storable { self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in let predicates: [AreInIncreasingOrder] = [ { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, - { $0.rank ?? 0 < $1.rank ?? 0 }, + { $0.rank ?? Int.max < $1.rank ?? Int.max }, { $0.lastName < $1.lastName}, { $0.firstName < $1.firstName } ] From d5a1449b3f0bd631059fe5f0fd202166d6a6dcb0 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 15:39:44 +0100 Subject: [PATCH 08/13] fix sharelink lag --- PadelClub.xcodeproj/project.pbxproj | 4 ++ .../Data/Federal/FederalTournament.swift | 2 +- PadelClub/Data/Tournament.swift | 4 +- PadelClub/Info.plist | 2 - PadelClub/Utils/PadelRule.swift | 10 +-- PadelClub/Utils/URLs.swift | 3 + .../ViewModel/FederalDataViewModel.swift | 2 +- .../Event/TournamentConfiguratorView.swift | 2 +- .../GroupStage/GroupStagesSettingsView.swift | 6 +- .../Agenda/TournamentLookUpView.swift | 4 +- .../Navigation/Toolbox/ToolboxView.swift | 35 +++++++--- PadelClub/Views/Round/RoundSettingsView.swift | 7 +- .../Shared/SelectablePlayerListView.swift | 4 +- .../Views/Shared/TournamentFilterView.swift | 2 +- .../TournamentLevelPickerView.swift | 2 +- .../Screen/InscriptionManagerView.swift | 68 ++++++++++++++++--- .../Shared/TournamentCellView.swift | 4 +- 17 files changed, 120 insertions(+), 41 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 91e2516..89222c8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3434,6 +3434,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3477,6 +3478,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3521,6 +3523,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3562,6 +3565,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index e61f36c..003218e 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -239,7 +239,7 @@ struct CategorieAge: Codable { return FederalTournamentAge(rawValue: id) } if let libelle { - return FederalTournamentAge.allCases.first(where: { $0.localizedLabel().localizedCaseInsensitiveContains(libelle) }) + return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) } return nil } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 132db89..b55e575 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1494,7 +1494,7 @@ defer { return tournamentLevel.localizedLevelLabel(.title) } } - let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") + let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") if displayStyle == .wide, let name { return [title, name].joined(separator: " - ") } else { @@ -2537,7 +2537,7 @@ extension Tournament: FederalTournamentHolder { func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { if isAnimation() { if displayAgeAndCategory(forBuild: build) == false { - return [build.category.localizedLabel(), build.age.localizedLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") + return [build.category.localizedLabel(), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") } else if name != nil { return build.level.localizedLevelLabel(.title) } else { diff --git a/PadelClub/Info.plist b/PadelClub/Info.plist index 40756ec..7b68d4a 100644 --- a/PadelClub/Info.plist +++ b/PadelClub/Info.plist @@ -33,7 +33,5 @@ ITSAppUsesNonExemptEncryption - UIFileSharingEnabled - diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 9a4e040..4b21a3d 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } var identifier: String { - level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() + level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel() } func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { @@ -65,7 +65,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { - age.localizedLabel(displayStyle) + age.localizedFederalAgeLabel(displayStyle) } } @@ -252,7 +252,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { } } - func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + func localizedFederalAgeLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" @@ -265,7 +265,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case .a17_18: return "17/18 ans" case .senior: - return "Senior" + return displayStyle == .short ? "" : "Senior" case .a45: return "+45 ans" case .a55: @@ -274,7 +274,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { } var tournamentDescriptionLabel: String { - return localizedLabel() + return localizedFederalAgeLabel() } func isAgeValid(age: Int?) -> Bool { diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index 16e0fba..20e1013 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -50,6 +50,7 @@ enum URLs: String, Identifiable { } enum PageLink: String, Identifiable, CaseIterable { + case info = "Informations" case teams = "Équipes" case summons = "Convocations" case groupStages = "Poules" @@ -68,6 +69,8 @@ enum PageLink: String, Identifiable, CaseIterable { switch self { case .matches: return "" + case .info: + return "info" case .teams: return "teams" case .summons: diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index a5579b7..2ca3b2f 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -28,7 +28,7 @@ class FederalDataViewModel { var labels: [String] = [] labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList()) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList()) - labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList()) + labels.append(contentsOf: ageCategories.map { $0.localizedFederalAgeLabel() }.formatList()) let clubNames = selectedClubs.compactMap { codeClub in let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub }) return club?.clubTitle(.short) diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index 9333ee9..30ec0f6 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -46,7 +46,7 @@ struct TournamentConfigurationView: View { } Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) { ForEach(FederalTournamentAge.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedFederalAgeLabel(.title)).tag(type) } } LabeledContent { diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 3bf6939..6961bae 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -262,11 +262,15 @@ struct GroupStagesSettingsView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + ShareLink(item: groupStagesPaste(), preview: .init("Données des poules")) } } } + func groupStagesPaste() -> TournamentGroupStageShareContent { + TournamentGroupStageShareContent(tournament: tournament) + } + var menuBuildAllGroupStages: some View { RowButtonView("Refaire les poules", role: .destructive) { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 93e9aa6..783359c 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -369,7 +369,7 @@ struct TournamentLookUpView: View { NavigationLink { List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in - Text(type.localizedLabel()) + Text(type.localizedFederalAgeLabel()) } .navigationTitle("Limites d'âge") .environment(\.editMode, Binding.constant(EditMode.active)) @@ -381,7 +381,7 @@ struct TournamentLookUpView: View { Text("Tous les âges") .foregroundStyle(.secondary) } else { - Text(ages.map({ $0.localizedLabel()}).joined(separator: ", ")) + Text(ages.map({ $0.localizedFederalAgeLabel()}).joined(separator: ", ")) .foregroundStyle(.secondary) } } diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 114112d..487f80c 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -228,10 +228,9 @@ struct ToolboxView: View { ShareLink(item: URLs.appStore.url) { Label("Lien AppStore", systemImage: "link") } - if let zip = _getZip() { - ShareLink(item: zip) { - Label("Mes données", systemImage: "server.rack") - } + + ShareLink(item: ZipLog(), preview: .init("Mon archive")) { + Label("Mes données", systemImage: "server.rack") } } label: { Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) @@ -240,7 +239,14 @@ struct ToolboxView: View { } } } - +} + +//#Preview { +// ToolboxView() +//} + + +struct ZipLog: Transferable { private func _getZip() -> URL? { do { let filePath = try Club.storageDirectoryPath() @@ -250,8 +256,19 @@ struct ToolboxView: View { return nil } } -} -//#Preview { -// ToolboxView() -//} + func shareFile() -> URL? { + print("Generating URL...") + return _getZip() + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .zip) { transferable in + return SentTransferredFile(transferable.shareFile()!) + }.exportingCondition { $0.shareFile() != nil } + + ProxyRepresentation { transferable in + return transferable.shareFile()! + }.exportingCondition { $0.shareFile() != nil } + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 42a45b4..2928a49 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -147,11 +147,16 @@ struct RoundSettingsView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + ShareLink(item: roundsPaste(), preview: .init("Données du tableau")) } } } + func roundsPaste() -> TournamentRoundShareContent { + TournamentRoundShareContent(tournament: tournament) + } + + private func _removeAllSeeds() async { await tournament.removeAllSeeds() self.isEditingTournamentSeed.wrappedValue = true diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 00a4836..352251d 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -109,7 +109,7 @@ struct SelectablePlayerListView: View { Section { Picker(selection: $searchViewModel.selectedAgeCategory) { ForEach(FederalTournamentAge.allCases) { ageCategory in - Text(ageCategory.localizedLabel(.title)).tag(ageCategory) + Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory) } } label: { Text("Catégorie d'âge") @@ -142,7 +142,7 @@ struct SelectablePlayerListView: View { VStack(alignment: .trailing) { Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down") if searchViewModel.selectedAgeCategory != .unlisted { - Text(searchViewModel.selectedAgeCategory.localizedLabel()).font(.caption) + Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption) } } } diff --git a/PadelClub/Views/Shared/TournamentFilterView.swift b/PadelClub/Views/Shared/TournamentFilterView.swift index 6af4d7c..3686aff 100644 --- a/PadelClub/Views/Shared/TournamentFilterView.swift +++ b/PadelClub/Views/Shared/TournamentFilterView.swift @@ -107,7 +107,7 @@ struct TournamentFilterView: View { } } } label: { - Text(category.localizedLabel(.title)) + Text(category.localizedFederalAgeLabel(.title)) } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift index bc81b41..524a65a 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift @@ -42,7 +42,7 @@ struct TournamentLevelPickerView: View { Picker(selection: $tournament.federalTournamentAge, label: Text("Limite d'âge")) { ForEach(FederalTournamentAge.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedFederalAgeLabel(.title)).tag(type) } } Picker(selection: $tournament.groupStageOrderingMode, label: Text("Répartition en poule")) { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index fc7c297..be075c8 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -426,15 +426,11 @@ struct InscriptionManagerView: View { private func _sharingTeamsMenuView() -> some View { Menu { - if let teamPaste = teamPaste() { - ShareLink(item: teamPaste) { - Text("En texte") - } + ShareLink(item: teamPaste(), preview: .init("Inscriptions")) { + Text("En texte") } - if let teamPaste = teamPaste(.csv) { - ShareLink(item: teamPaste) { - Text("En csv") - } + ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) { + Text("En csv") } } label: { Label("Exporter les paires", systemImage: "square.and.arrow.up") @@ -449,8 +445,8 @@ struct InscriptionManagerView: View { tournament.unsortedTeamsWithoutWO() } - func teamPaste(_ exportFormat: ExportFormat = .rawText) -> URL? { - tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle(.short), exportFormat) + func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile { + TournamentShareFile(tournament: tournament, exportFormat: exportFormat) } var unsortedPlayers: [PlayerRegistration] { @@ -1015,3 +1011,55 @@ struct InscriptionManagerView: View { // .environment(Tournament.mock()) // } //} + +struct TournamentRoundShareContent: Transferable { + let tournament: Tournament + + func shareContent() -> String { + print("Generating URL...") + let content = tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n") + return content + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { transferable in + return transferable.shareContent() + } + } +} + +struct TournamentGroupStageShareContent: Transferable { + let tournament: Tournament + + func shareContent() -> String { + print("Generating URL...") + let content = tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n") + return content + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { transferable in + return transferable.shareContent() + } + } +} + +struct TournamentShareFile: Transferable { + let tournament: Tournament + let exportFormat: ExportFormat + + func shareFile() -> URL { + print("Generating URL...") + return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat) + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .utf8PlainText) { transferable in + return SentTransferredFile(transferable.shareFile()) + } + + ProxyRepresentation { transferable in + return transferable.shareFile() + } + } +} diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index c4426d7..e3b862d 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -107,7 +107,7 @@ struct TournamentCellView: View { if displayStyle == .wide, tournament.displayAgeAndCategory(forBuild: build) { VStack(alignment: .leading, spacing: 0) { Text(build.category.localizedLabel()) - Text(build.age.localizedLabel()) + Text(build.age.localizedFederalAgeLabel()) } .font(.caption) } @@ -155,7 +155,7 @@ struct TournamentCellView: View { } } else { Text(build.category.localizedLabel()) - Text(build.age.localizedLabel()) + Text(build.age.localizedFederalAgeLabel()) } } } From 107eabf8ff22942045fcfeb1e0aac4f9e273b7eb Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 16:10:24 +0100 Subject: [PATCH 09/13] fix unecessary count check of month player data --- PadelClub/Views/Navigation/MainView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index ee01859..62f9ceb 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -263,7 +263,7 @@ struct MainView: View { await _startImporting(importingDate: mostRecentDateImported) } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() { await _startImporting(importingDate: mostRecentDateImported) - } else if current.incompleteMode == false || updated == 0 { + } else if updated == 0 { await _calculateMonthData(dataSource: current.monthKey) } } From 75a721179f746b53341fc30caac0abf36a7002be Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 16:11:04 +0100 Subject: [PATCH 10/13] v1.0.39 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 89222c8..7cba62e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.38; + MARKETING_VERSION = 1.0.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.38; + MARKETING_VERSION = 1.0.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From b73d4e49e1ba6f9bad4f1c66e0f8cfc400be60e0 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 14 Dec 2024 09:05:30 +0100 Subject: [PATCH 11/13] short category displayed as wide, no more one letter --- PadelClub/Data/Tournament.swift | 10 ++++++++-- PadelClub/Utils/ContactManager.swift | 4 ++-- .../Views/Calling/CallMessageCustomizationView.swift | 2 +- PadelClub/Views/Calling/CallView.swift | 2 +- PadelClub/Views/Calling/SendToAllView.swift | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b55e575..ea926b8 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1486,7 +1486,7 @@ defer { return unsortedTeams().first(where: { $0.includes(players: players) }) } - func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { + func tournamentTitle(_ displayStyle: DisplayStyle = .wide, hideSenior: Bool = false) -> String { if tournamentLevel == .unlisted, displayStyle == .title { if let name { return name @@ -1494,7 +1494,13 @@ defer { return tournamentLevel.localizedLevelLabel(.title) } } - let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") + let displayStyleCategory = hideSenior ? .short : displayStyle + var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle)] + if displayStyle == .short { + levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedLabel(displayStyle)] + } + let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)] + let title: String = array.filter({ $0.isEmpty == false }).joined(separator: " ") if displayStyle == .wide, let name { return [title, name].joined(separator: " - ") } else { diff --git a/PadelClub/Utils/ContactManager.swift b/PadelClub/Utils/ContactManager.swift index 8c620a7..a72204a 100644 --- a/PadelClub/Utils/ContactManager.swift +++ b/PadelClub/Utils/ContactManager.swift @@ -82,7 +82,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c let date = startDate ?? tournament?.startDate ?? Date() if let tournament { - text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.short)) + text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.title, hideSenior: true)) text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage) } @@ -132,7 +132,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" if let tournament { - return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" + return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" } else { return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)" } diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 0ed9a54..2914565 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -56,7 +56,7 @@ struct CallMessageCustomizationView: View { var finalMessage: String? { let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s" - return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" + return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" } var body: some View { diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 86f12af..1be6236 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -387,7 +387,7 @@ struct CallView: View { recipients: tournament.umpireMail(), bccRecipients: teams.flatMap { $0.getMail() }, body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), - subject: tournament.tournamentTitle(), + subject: tournament.tournamentTitle(hideSenior: true), tournamentBuild: nil) } diff --git a/PadelClub/Views/Calling/SendToAllView.swift b/PadelClub/Views/Calling/SendToAllView.swift index 9f6778d..d261f35 100644 --- a/PadelClub/Views/Calling/SendToAllView.swift +++ b/PadelClub/Views/Calling/SendToAllView.swift @@ -273,7 +273,7 @@ struct SendToAllView: View { if contactMethod == 0 { contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil) } else { - contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(), tournamentBuild: nil) + contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(hideSenior: true), tournamentBuild: nil) } } } From 0ba2f9d76d50736e2062dbf7fdb7667afecd9ce3 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 14 Dec 2024 09:05:58 +0100 Subject: [PATCH 12/13] v1.0.40 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 7cba62e..2ad10c1 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.39; + MARKETING_VERSION = 1.0.40; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.39; + MARKETING_VERSION = 1.0.40; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From cb68d10fb3ba6dd9ed9d25c2021e34082624cf75 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 22 Dec 2024 11:38:16 +0100 Subject: [PATCH 13/13] add the ability to move time slots --- PadelClub/PadelClubApp.swift | 2 +- PadelClub/Utils/Tips.swift | 25 +- PadelClub/Views/Planning/PlanningView.swift | 558 ++++++++++++++++---- 3 files changed, 469 insertions(+), 116 deletions(-) diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 4cdba91..3482b96 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -102,7 +102,7 @@ print("Running in Release mode") //try? Tips.resetDatastore() try? Tips.configure([ - .displayFrequency(.daily), + .displayFrequency(.immediate), .datastoreLocation(.applicationDefault) ]) } diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index bb136dd..ea69ae0 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -549,29 +549,36 @@ struct TeamsExportTip: Tip { } } -struct PlayerTournamentSearchTip: Tip { +struct TimeSlotMoveTip: Tip { var title: Text { - Text("Cherchez un tournoi autour de vous !") + Text("Réorganisez vos créneaux horaires !") } var message: Text? { - Text("Padel Club facilite la recherche de tournois et l'inscription !") + Text("Vous pouvez déplacer les créneaux horaires dans la liste en glissant-déposant.") } var image: Image? { - Image(systemName: "trophy.circle") + Image(systemName: "arrow.up.arrow.down.circle") } +} - var actions: [Action] { - Action(id: ActionKey.selectAction.rawValue, title: "Éssayer") +struct TimeSlotMoveOptionTip: Tip { + var title: Text { + Text("Réorganisez vos créneaux horaires !") } - enum ActionKey: String { - case selectAction = "selectAction" + var message: Text? { + Text("En cliquant ici, vous pouvez déplacer les créneaux horaires dans la liste en glissant-déposant.") + } + + var image: Image? { + Image(systemName: "sparkles") } - } + + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 3a6e07f..0f3193c 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -6,16 +6,21 @@ // import SwiftUI +import LeStorage +import TipKit struct PlanningView: View { + @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament @State private var selectedDay: Date? @Binding var selectedScheduleDestination: ScheduleDestination? @State private var filterOption: PlanningFilterOption = .byDefault @State private var showFinishedMatches: Bool = false + @State private var enableMove: Bool = false let allMatches: [Match] + let timeSlotMoveOptionTip = TimeSlotMoveOptionTip() init(matches: [Match], selectedScheduleDestination: Binding) { self.allMatches = matches @@ -37,23 +42,7 @@ struct PlanningView: View { func keys(timeSlots: [Date:[Match]]) -> [Date] { timeSlots.keys.sorted() } - - enum PlanningFilterOption: Int, CaseIterable, Identifiable { - var id: Int { self.rawValue } - - case byDefault - case byCourt - func localizedPlanningLabel() -> String { - switch self { - case .byCourt: - return "Par terrain" - case .byDefault: - return "Par ordre des matchs" - } - } - } - private func _computedTitle(days: [Date]) -> String { if let selectedDay { return selectedDay.formatted(.dateTime.day().weekday().month()) @@ -71,8 +60,13 @@ struct PlanningView: View { let keys = self.keys(timeSlots: timeSlots) let days = self.days(timeSlots: timeSlots) let matches = matches - BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches) + let notSlots = matches.allSatisfy({ $0.startDate == nil }) + BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay) + .environment(\.filterOption, filterOption) + .environment(\.showFinishedMatches, showFinishedMatches) + .environment(\.enableMove, enableMove) .navigationTitle(Text(_computedTitle(days: days))) + .navigationBarBackButtonHidden(enableMove) .toolbar(content: { if days.count > 1 { ToolbarTitleMenu { @@ -89,42 +83,79 @@ struct PlanningView: View { Text("Jour") } .pickerStyle(.automatic) + .disabled(enableMove) } } + + if enableMove { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler") { + enableMove = false + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button("Sauver") { + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + } catch { + Logger.error(error) + } - ToolbarItemGroup(placement: .topBarTrailing) { - Menu { - Picker(selection: $showFinishedMatches) { - Text("Afficher tous les matchs").tag(true) - Text("Masquer les matchs terminés").tag(false) - } label: { - Text("Option de filtrage") + enableMove = false } - .labelsHidden() - .pickerStyle(.inline) - } label: { - Label("Filtrer", systemImage: "clock.badge.checkmark") - .symbolVariant(showFinishedMatches ? .fill : .none) } - Menu { - Picker(selection: $filterOption) { - ForEach(PlanningFilterOption.allCases) { - Text($0.localizedPlanningLabel()).tag($0) + + } else { + + ToolbarItemGroup(placement: .topBarTrailing) { + if notSlots == false { + Toggle(isOn: $enableMove) { + Label("Déplacer", systemImage: "rectangle.2.swap") + } + .popoverTip(timeSlotMoveOptionTip) + } + + Menu { + Section { + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) + } label: { + Text("Option de filtrage") + } + .labelsHidden() + .pickerStyle(.inline) + } header: { + Text("Option de filtrage") + } + + Divider() + + Section { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } + } label: { + Text("Option de triage") + } + .labelsHidden() + .pickerStyle(.inline) + } header: { + Text("Option de triage") + } } label: { - Text("Option de triage") + Label("Trier", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant(filterOption == .byCourt || showFinishedMatches ? .fill : .none) } - .labelsHidden() - .pickerStyle(.inline) - } label: { - Label("Trier", systemImage: "line.3.horizontal.decrease.circle") - .symbolVariant(filterOption == .byCourt ? .fill : .none) + } - } }) .overlay { - if matches.allSatisfy({ $0.startDate == nil }) { + if notSlots { ContentUnavailableView { Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") } description: { @@ -140,86 +171,213 @@ struct PlanningView: View { struct BySlotView: View { @Environment(Tournament.self) var tournament: Tournament + @Environment(\.filterOption) private var filterOption + @Environment(\.showFinishedMatches) private var showFinishedMatches + @Environment(\.enableMove) private var enableMove + let days: [Date] let keys: [Date] - let timeSlots: [Date:[Match]] + let timeSlots: [Date: [Match]] let matches: [Match] let selectedDay: Date? - let filterOption: PlanningFilterOption - let showFinishedMatches: Bool + let timeSlotMoveTip = TimeSlotMoveTip() var body: some View { List { - if matches.allSatisfy({ $0.startDate == nil }) == false { + + if enableMove { + TipView(timeSlotMoveTip) + .tipStyle(tint: .logoYellow, asSection: true) + } + + if !matches.allSatisfy({ $0.startDate == nil }) { ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in - Section { - ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in - if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { - DisclosureGroup { - ForEach(_matches) { match in - NavigationLink { - MatchDetailView(match: match) - .matchViewStyle(.sectionedStandardStyle) - - } label: { - LabeledContent { - if let courtName = match.courtName() { - Text(courtName) - } - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle(.title)) - } else if let round = match.roundObject { - Text(round.roundTitle()) - } - Text(match.matchTitle()) - } - } - } - } label: { - _timeSlotView(key: key, matches: _matches) - } - } - } - } header: { - HStack { - if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { - Text("Sans horaire") - } else { - Text(day.formatted(.dateTime.day().weekday().month())) - } - Spacer() - let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) - if showFinishedMatches { - Text(self._formattedMatchCount(count)) - } else { - Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") - } - } - } footer: { - if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { - Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") - } - } - .headerProminence(.increased) + DaySectionView( + day: day, + keys: keys.filter({ $0.dayInt == day.dayInt }), + timeSlots: timeSlots, + selectedDay: selectedDay + ) } } } } + } + + + struct DaySectionView: View { + @Environment(Tournament.self) var tournament: Tournament + @Environment(\.filterOption) private var filterOption + @Environment(\.showFinishedMatches) private var showFinishedMatches + @Environment(\.enableMove) private var enableMove + + let day: Date + let keys: [Date] + let timeSlots: [Date: [Match]] + let selectedDay: Date? + + var body: some View { + Section { + ForEach(keys, id: \.self) { key in + TimeSlotSectionView( + key: key, + matches: timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) ?? [] + ) + } + .onMove(perform: enableMove ? moveSection : nil) + } header: { + HeaderView(day: day, timeSlots: timeSlots) + } footer: { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") + } + } + } - private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int { + func moveSection(from source: IndexSet, to destination: Int) { + let daySlots = keys.filter { $0.dayInt == day.dayInt }.sorted() + + guard let sourceIdx = source.first, + sourceIdx < daySlots.count, + destination <= daySlots.count else { + return + } + + // Create a mutable copy of the time slots for this day + var slotsToUpdate = daySlots + + let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination) + + // Perform the move in the array + let sourceTime = slotsToUpdate.remove(at: sourceIdx) + if sourceIdx < destination { + slotsToUpdate.insert(sourceTime, at: destination - 1) + } else { + slotsToUpdate.insert(sourceTime, at: destination) + } + + // Update matches by swapping their startDates + for index in updateRange { + // Find the new time slot for these matches + let oldStartTime = slotsToUpdate[index] + let newStartTime = daySlots[index] + guard let matchesToUpdate = timeSlots[oldStartTime] else { continue } + + // Update each match with the new start time + for match in matchesToUpdate { + match.startDate = newStartTime + } + } + } + } + + + struct TimeSlotSectionView: View { + @Environment(\.enableMove) private var enableMove + let key: Date + let matches: [Match] + + var body: some View { + if !matches.isEmpty { + if enableMove { + TimeSlotHeaderView(key: key, matches: matches) + } else { + DisclosureGroup { + MatchListView(matches: matches) + } label: { + TimeSlotHeaderView(key: key, matches: matches) + } + } + } + } + } + + struct MatchListView: View { + let matches: [Match] + + var body: some View { + ForEach(matches) { match in + NavigationLink { + MatchDetailView(match: match) + .matchViewStyle(.sectionedStandardStyle) + } label: { + MatchRowView(match: match) + } + } + } + } + + struct MatchRowView: View { + let match: Match + + var body: some View { + LabeledContent { + if let courtName = match.courtName() { + Text(courtName) + } + } label: { + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle(.title)) + } else if let round = match.roundObject { + Text(round.roundTitle()) + } + Text(match.matchTitle()) + } + } + } + + + struct HeaderView: View { + @Environment(\.filterOption) private var filterOption + @Environment(\.showFinishedMatches) private var showFinishedMatches + @Environment(\.enableMove) private var enableMove + + let day: Date + let timeSlots: [Date: [Match]] + + var body: some View { + HStack { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire") + } else { + Text(day.formatted(.dateTime.day().weekday().month())) + } + Spacer() + let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) + if showFinishedMatches { + Text(_formattedMatchCount(count)) + } else { + Text("\(_formattedMatchCount(count)) restant\(count.pluralSuffix)") + } + } + } + + private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date: [Match]]) -> Int { timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count } - - private func _timeSlotView(key: Date, matches: [Match]) -> some View { + + private func _formattedMatchCount(_ count: Int) -> String { + return "\(count.formatted()) match\(count.pluralSuffix)" + } + } + + struct TimeSlotHeaderView: View { + let key: Date + let matches: [Match] + @Environment(Tournament.self) var tournament: Tournament + + var body: some View { LabeledContent { - Text(self._formattedMatchCount(matches.count)) + Text("\(matches.count.formatted()) match\(matches.count.pluralSuffix)") } label: { if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { Text("Aucun horaire") } else { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + Text(key.formatted(date: .omitted, time: .shortened)) + .font(.title) + .fontWeight(.semibold) } + if matches.count <= tournament.courtCount { let names = matches.sorted(by: \.computedOrder) .compactMap({ $0.roundTitle() }) @@ -232,15 +390,203 @@ struct PlanningView: View { } else { Text(matches.count.formatted().appending(" matchs")) } + } } - - fileprivate func _formattedMatchCount(_ count: Int) -> String { - return "\(count.formatted()) match\(count.pluralSuffix)" + } + + +// struct BySlotView: View { +// @Environment(Tournament.self) var tournament: Tournament +// let days: [Date] +// let keys: [Date] +// let timeSlots: [Date:[Match]] +// let matches: [Match] +// let selectedDay: Date? +// let filterOption: PlanningFilterOption +// let showFinishedMatches: Bool +// +// var body: some View { +// List { +// if matches.allSatisfy({ $0.startDate == nil }) == false { +// ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in +// Section { +// ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in +// if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { +// DisclosureGroup { +// ForEach(_matches) { match in +// NavigationLink { +// MatchDetailView(match: match) +// .matchViewStyle(.sectionedStandardStyle) +// +// } label: { +// LabeledContent { +// if let courtName = match.courtName() { +// Text(courtName) +// } +// } label: { +// if let groupStage = match.groupStageObject { +// Text(groupStage.groupStageTitle(.title)) +// } else if let round = match.roundObject { +// Text(round.roundTitle()) +// } +// Text(match.matchTitle()) +// } +// } +// } +// } label: { +// _timeSlotView(key: key, matches: _matches) +// } +// } +// } +// .onMove(perform: moveSection) +// } header: { +// HStack { +// if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { +// Text("Sans horaire") +// } else { +// Text(day.formatted(.dateTime.day().weekday().month())) +// } +// Spacer() +// let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) +// if showFinishedMatches { +// Text(self._formattedMatchCount(count)) +// } else { +// Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") +// } +// } +// } footer: { +// if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { +// Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") +// } +// } +// .headerProminence(.increased) +// } +// } +// } +// } +// +// func moveSection(from source: IndexSet, to destination: Int) { +// let daySlots = keys.filter { selectedDay == nil || $0.dayInt == selectedDay?.dayInt }.sorted() +// +// guard let sourceIdx = source.first, +// sourceIdx < daySlots.count, +// destination <= daySlots.count else { +// return +// } +// +// // Create a mutable copy of the time slots for this day +// var slotsToUpdate = daySlots +// +// let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination) - 1 +// print(updateRange) +// +// // Perform the move in the array +// let sourceTime = slotsToUpdate.remove(at: sourceIdx) +// if sourceIdx < destination { +// slotsToUpdate.insert(sourceTime, at: destination - 1) +// } else { +// slotsToUpdate.insert(sourceTime, at: destination) +// } +// +// // Update matches by swapping their startDates +// for index in updateRange { +// // Find the new time slot for these matches +// let oldStartTime = slotsToUpdate[index] +// let newStartTime = daySlots[index] +// guard let matchesToUpdate = timeSlots[oldStartTime] else { continue } +// print("moving", oldStartTime, "to", newStartTime) +// +// // Update each match with the new start time +// for match in matchesToUpdate { +// match.startDate = newStartTime +// } +// } +// +// try? self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches) +// } +// +// +// private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int { +// timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count +// } +// +// private func _timeSlotView(key: Date, matches: [Match]) -> some View { +// LabeledContent { +// Text(self._formattedMatchCount(matches.count)) +// } label: { +// if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { +// Text("Aucun horaire") +// } else { +// Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) +// } +// if matches.count <= tournament.courtCount { +// let names = matches.sorted(by: \.computedOrder) +// .compactMap({ $0.roundTitle() }) +// .reduce(into: [String]()) { uniqueNames, name in +// if !uniqueNames.contains(name) { +// uniqueNames.append(name) +// } +// } +// Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) +// } else { +// Text(matches.count.formatted().appending(" matchs")) +// } +// } +// } +// +// fileprivate func _formattedMatchCount(_ count: Int) -> String { +// return "\(count.formatted()) match\(count.pluralSuffix)" +// } +// } +} + +enum PlanningFilterOption: Int, CaseIterable, Identifiable { + var id: Int { self.rawValue } + + case byDefault + case byCourt + + func localizedPlanningLabel() -> String { + switch self { + case .byCourt: + return "Par terrain" + case .byDefault: + return "Par ordre des matchs" } } } -//#Preview { -// PlanningView(matches: [], selectedScheduleDestination: .constant(nil)) -//} + +struct FilterOptionKey: EnvironmentKey { + static let defaultValue: PlanningFilterOption = .byDefault +} + +extension EnvironmentValues { + var filterOption: PlanningFilterOption { + get { self[FilterOptionKey.self] } + set { self[FilterOptionKey.self] = newValue } + } +} + +struct ShowFinishedMatchesKey: EnvironmentKey { + static let defaultValue: Bool = false +} + +extension EnvironmentValues { + var showFinishedMatches: Bool { + get { self[ShowFinishedMatchesKey.self] } + set { self[ShowFinishedMatchesKey.self] = newValue } + } +} + +struct EnableMoveKey: EnvironmentKey { + static let defaultValue: Bool = false +} + +extension EnvironmentValues { + var enableMove: Bool { + get { self[EnableMoveKey.self] } + set { self[EnableMoveKey.self] = newValue } + } +}