fix crash when when replacing heads

fix undetected tournament
fix re-ranking efficiency
add option to handle re-ranks for all tournaments in a month
add option to handle refresh online reg list in a month
add automatic refresh in inscription manager and tournamentcell
display online reg count in inscription manager
reshow button to delete all teams and let the jap know it's locked due to online reg
sync2
Raz 9 months ago
parent 018e77fda7
commit e029295b6a
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/Data/Federal/FederalTournament.swift
  3. 3
      PadelClub/Data/Match.swift
  4. 20
      PadelClub/Data/PlayerRegistration.swift
  5. 69
      PadelClub/Data/Tournament.swift
  6. 69
      PadelClub/Utils/SwiftParser.swift
  7. 67
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  8. 2
      PadelClub/Views/Round/RoundSettingsView.swift
  9. 17
      PadelClub/Views/Tournament/FileImportView.swift
  10. 19
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  11. 42
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  12. 7
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  13. 9
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  14. 22
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -3344,7 +3344,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.14;
MARKETING_VERSION = 1.1.15;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3389,7 +3389,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.14;
MARKETING_VERSION = 1.1.15;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -236,12 +236,12 @@ struct CategorieAge: Codable {
var tournamentAge: FederalTournamentAge? {
if let id {
return FederalTournamentAge(rawValue: id)
return FederalTournamentAge(rawValue: id) ?? .senior
}
if let libelle {
return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) })
return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) ?? .senior
}
return nil
return .senior
}
}
@ -295,7 +295,7 @@ struct Serie: Codable {
var sexe: String?
var tournamentCategory: TournamentCategory? {
TournamentCategory.allCases.first(where: { $0.requestLabel == code })
TournamentCategory.allCases.first(where: { $0.requestLabel == code }) ?? .men
}
}
@ -348,9 +348,9 @@ struct TypeEpreuve: Codable {
var tournamentLevel: TournamentLevel? {
if let code, let value = Int(code.removingFirstCharacter) {
return TournamentLevel(rawValue: value)
return TournamentLevel(rawValue: value) ?? .p100
}
return nil
return .p100
}
}

@ -410,6 +410,8 @@ defer {
}
}
//byeState = false
if state != currentState {
roundObject?._cachedSeedInterval = nil
name = nil
do {
@ -417,6 +419,7 @@ defer {
} catch {
Logger.error(error)
}
}
if single == false {
_toggleLoserMatchDisableState(state)

@ -301,21 +301,23 @@ final class PlayerRegistration: ModelObject, Storable {
return await withTaskGroup(of: Line?.self) { group in
for source in filteredSources {
group.addTask {
guard !Task.isCancelled else { print("Cancelled"); return nil }
guard !Task.isCancelled else { return nil }
return try? await source.first { $0.rawValue.contains(";\(license);") }
}
}
if let first = await group.first(where: { $0 != nil }) {
group.cancelAll()
return first
for await result in group {
if let result {
group.cancelAll() // Stop other tasks as soon as we find a match
return result
}
}
return nil
}
}
func historyFromName(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME
#if DEBUG
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -338,9 +340,11 @@ final class PlayerRegistration: ModelObject, Storable {
}
}
if let first = await group.first(where: { $0 != nil }) {
group.cancelAll()
return first
for await result in group {
if let result {
group.cancelAll() // Stop other tasks as soon as we find a match
return result
}
}
return nil
}

@ -70,6 +70,10 @@ final class Tournament : ModelObject, Storable {
var maximumPlayerPerTeam: Int = 2
var information: String? = nil
//local variable
var refreshInProgress: Bool = false
var refreshRanking: Bool = false
@ObservationIgnored
var navigationPath: [Screen] = []
@ -1513,8 +1517,8 @@ defer {
}
}
func updateRank(to newDate: Date?) async throws {
func updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws {
refreshRanking = true
#if DEBUG_TIME
let start = Date()
defer {
@ -1549,16 +1553,42 @@ defer {
let lastRankMan = monthData?.maleUnrankedValue ?? 0
let lastRankWoman = monthData?.femaleUnrankedValue ?? 0
var chunkedParsers: [CSVParser] = []
if let providedSources {
chunkedParsers = providedSources
} else {
// Fetch only the required files
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
guard !dataURLs.isEmpty else { return } // Early return if no files found
let sources = dataURLs.map { CSVParser(url: $0) }
chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
}
let players = unsortedPlayers()
try await players.concurrentForEach { player in
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
try await player.updateRank(from: sources, lastRank: lastRank)
try await player.updateRank(from: chunkedParsers, lastRank: lastRank)
player.setComputedRank(in: self)
}
if providedSources == nil {
try chunkedParsers.forEach { chunk in
try FileManager.default.removeItem(at: chunk.url)
}
}
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
let unsortedTeams = unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
}
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
refreshRanking = false
}
@ -2424,14 +2454,17 @@ defer {
func updateSeedsBracketPosition() async {
await removeAllSeeds()
await removeAllSeeds(saveTeamsAtTheEnd: false)
let drawLogs = drawLogs().reversed()
let seeds = seeds()
await MainActor.run {
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
@ -2440,11 +2473,13 @@ defer {
}
}
func removeAllSeeds() async {
func removeAllSeeds(saveTeamsAtTheEnd: Bool) async {
let teams = unsortedTeams()
teams.forEach({ team in
team.bracketPosition = nil
team._cachedRestingTime = nil
team.finalRanking = nil
team.pointsEarned = nil
})
let allMatches = allRoundMatches()
let ts = allMatches.flatMap { match in
@ -2471,12 +2506,13 @@ defer {
Logger.error(error)
}
if saveTeamsAtTheEnd {
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
updateTournamentState()
}
}
func addNewRound(_ roundIndex: Int) async {
@ -2608,6 +2644,27 @@ defer {
return false
}
func rankSourceShouldBeRefreshed() -> Date? {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate = rankSourceDate, currentRankSourceDate < mostRecentDate, hasEnded() == false {
return mostRecentDate
} else {
return nil
}
}
func onlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
func refreshTeamList() async throws {
guard enableOnlineRegistration, refreshInProgress == false, hasEnded() == false else { return }
refreshInProgress = true
try await self.tournamentStore.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
refreshInProgress = false
}
// MARK: -
func insertOnServer() throws {

@ -70,18 +70,18 @@ struct Line: Identifiable {
struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Line
private let url: URL
let url: URL
private var lineIterator: LineIterator
private let seperator: Character
private let separator: Character
private let quoteCharacter: Character = "\""
private var lineNumber = 0
private let date: Date
let maleData: Bool
init(url: URL, seperator: Character = ";") {
init(url: URL, separator: Character = ";") {
self.date = url.dateFromPath
self.url = url
self.seperator = seperator
self.separator = separator
self.lineIterator = url.lines.makeAsyncIterator()
self.maleData = url.path().contains(SourceFile.messieurs.rawValue)
}
@ -139,7 +139,7 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
inQuote = !inQuote
continue
case seperator:
case separator:
if !inQuote {
data.append(currentString.isEmpty ? nil : currentString)
currentString = ""
@ -157,4 +157,63 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
return data
}
/// Splits the CSV file into multiple temporary CSV files, each containing `size` lines.
/// Returns an array of new `CSVParser` instances pointing to these chunked files.
func getChunkedParser(size: Int) async throws -> [CSVParser] {
var chunkedParsers: [CSVParser] = []
var currentChunk: [String] = []
var iterator = self.makeAsyncIterator()
var chunkIndex = 0
while let line = try await iterator.next()?.rawValue {
currentChunk.append(line)
// When the chunk reaches the desired size, write it to a file
if currentChunk.count == size {
let chunkURL = try writeChunkToFile(chunk: currentChunk, index: chunkIndex)
chunkedParsers.append(CSVParser(url: chunkURL, separator: self.separator))
chunkIndex += 1
currentChunk.removeAll()
}
}
// Handle remaining lines (if any)
if !currentChunk.isEmpty {
let chunkURL = try writeChunkToFile(chunk: currentChunk, index: chunkIndex)
chunkedParsers.append(CSVParser(url: chunkURL, separator: self.separator))
}
return chunkedParsers
}
/// Writes a chunk of CSV lines to a temporary file and returns its URL.
private func writeChunkToFile(chunk: [String], index: Int) throws -> URL {
let tempDirectory = FileManager.default.temporaryDirectory
let chunkURL = tempDirectory.appendingPathComponent("\(url.lastPathComponent)-\(index).csv")
let chunkData = chunk.joined(separator: "\n")
try chunkData.write(to: chunkURL, atomically: true, encoding: .utf8)
return chunkURL
}
}
/// Process all large CSV files concurrently and gather all mini CSVs.
func chunkAllSources(sources: [CSVParser], size: Int) async throws -> [CSVParser] {
var allChunks: [CSVParser] = []
await withTaskGroup(of: [CSVParser].self) { group in
for source in sources {
group.addTask {
return (try? await source.getChunkedParser(size: size)) ?? []
}
}
for await miniCSVs in group {
allChunks.append(contentsOf: miniCSVs)
}
}
return allChunks
}

@ -17,6 +17,11 @@ struct EventListView: View {
let tournaments: [FederalTournamentHolder]
let sortAscending: Bool
var lastDataSource: Date? {
guard let _lastDataSource = dataStore.appSettings.lastDataSource else { return nil }
return URL.importDateFormatter.date(from: _lastDataSource)
}
var body: some View {
let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
switch viewStyle {
@ -101,6 +106,41 @@ struct EventListView: View {
@ViewBuilder
private func _options(_ pcTournaments: [Tournament]) -> some View {
if let lastDataSource, pcTournaments.anySatisfy({ $0.rankSourceShouldBeRefreshed() != nil && $0.hasEnded() == false }) {
Section {
Button {
Task {
do {
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == lastDataSource }
guard !dataURLs.isEmpty else { return } // Early return if no files found
let sources = dataURLs.map { CSVParser(url: $0) }
let chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
try await pcTournaments.concurrentForEach { tournament in
if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() {
try await tournament.updateRank(to: mostRecentDate, forceRefreshLockWeight: false, providedSources: chunkedParsers)
}
}
try chunkedParsers.forEach { chunk in
try FileManager.default.removeItem(at: chunk.url)
}
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
}
} label: {
Text("Rafraîchir les classements")
}
} header: {
Text("Source disponible : \(lastDataSource.monthYearFormatted)")
}
Divider()
}
Section {
if pcTournaments.anySatisfy({ $0.isPrivate == true }) {
Button {
@ -135,7 +175,7 @@ struct EventListView: View {
Text("Visibilité sur Padel Club")
}
Divider()
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) {
Section {
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) {
Button {
@ -152,7 +192,23 @@ struct EventListView: View {
}
}
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) {
Button {
Task {
do {
try await pcTournaments.concurrentForEach { tournament in
try await tournament.refreshTeamList()
}
} catch {
Logger.error(error)
}
}
} label: {
Text("Rafraîchir la liste des équipes inscrites en ligne")
}
Button {
pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = false
@ -207,6 +263,13 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some View {
NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver())
.task {
do {
try await tournament.refreshTeamList()
} catch {
Logger.error(error)
}
}
}
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
.contextMenu {

@ -158,7 +158,7 @@ struct RoundSettingsView: View {
private func _removeAllSeeds() async {
await tournament.removeAllSeeds()
await tournament.removeAllSeeds(saveTeamsAtTheEnd: true)
self.isEditingTournamentSeed.wrappedValue = true
}

@ -120,10 +120,10 @@ struct FileImportView: View {
return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight)
}
private func _deleteTeams() async {
private func _deleteTeams(teams: [TeamRegistration]) async {
await MainActor.run {
do {
try tournamentStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
try tournamentStore.teamRegistrations.delete(contentOfs: teams)
} catch {
Logger.error(error)
}
@ -140,9 +140,18 @@ struct FileImportView: View {
}
}
if tournament.unsortedTeams().count > 0, tournament.enableOnlineRegistration == false {
let unsortedTeams = tournament.unsortedTeams()
let onlineTeams = unsortedTeams.filter({ $0.hasRegisteredOnline() })
if unsortedTeams.count > 0 {
Section {
RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) {
await _deleteTeams()
await _deleteTeams(teams: unsortedTeams)
}
.disabled(onlineTeams.isEmpty == false)
} footer: {
if onlineTeams.isEmpty == false {
Text("Ce tournoi contient des inscriptions en ligne, vous ne pouvez pas effacer toute votre liste d'inscription d'un coup.")
}
}
}

@ -42,24 +42,7 @@ struct UpdateSourceRankDateView: View {
updatingRank = true
Task {
do {
try await tournament.updateRank(to: currentRankSourceDate)
let unsortedPlayers = tournament.unsortedPlayers()
tournament.unsortedPlayers().forEach { player in
player.setComputedRank(in: tournament)
}
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
let unsortedTeams = tournament.unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
}
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
try await tournament.updateRank(to: currentRankSourceDate, forceRefreshLockWeight: forceRefreshLockWeight, providedSources: nil)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)

@ -51,7 +51,6 @@ struct InscriptionManagerView: View {
@State private var pasteString: String?
@State private var registrationIssues: Int? = nil
@State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false
@State private var refreshStatus: Bool?
@State private var showLegendView: Bool = false
@ -259,6 +258,9 @@ struct InscriptionManagerView: View {
}
}
}
.task {
await _refreshList()
}
.refreshable {
await _refreshList()
}
@ -543,28 +545,27 @@ struct InscriptionManagerView: View {
// }
//
private func _refreshList() async {
if refreshInProgress { return }
if tournament.enableOnlineRegistration == false { return }
if tournament.hasEnded() { return }
if tournament.refreshInProgress { return }
refreshResult = nil
refreshStatus = nil
refreshInProgress = true
do {
try await self.tournamentStore.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournament.refreshTeamList()
_setHash()
self.refreshResult = "la synchronization a réussi"
self.refreshResult = "La synchronization a réussi"
self.refreshStatus = true
refreshInProgress = false
} catch {
Logger.error(error)
self.refreshResult = "la synchronization a échoué"
self.refreshResult = "La synchronization a échoué"
self.refreshStatus = false
refreshInProgress = false
tournament.refreshInProgress = false
}
}
@ -717,7 +718,7 @@ struct InscriptionManagerView: View {
@ViewBuilder
private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() {
Section {
TipView(rankUpdateTip) { action in
self.currentRankSourceDate = mostRecentDate
@ -845,19 +846,22 @@ struct InscriptionManagerView: View {
}
} label: {
LabeledContent {
if refreshInProgress {
if tournament.refreshInProgress {
ProgressView()
} else if let refreshStatus {
if refreshStatus {
Image(systemName: "checkmark").foregroundStyle(.green).font(.headline)
} else {
Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline)
}
Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted())
.fontWeight(.bold)
}
} label: {
Text("Récupérer les inscriptions en ligne")
if let refreshStatus {
Text("Inscriptions en ligne")
} else if tournament.refreshInProgress {
Text("Récupération des inscrits en ligne")
} else {
Text("Récupérer des inscrits en ligne")
}
if let refreshResult {
Text(refreshResult)
Text(refreshResult).foregroundStyle(.secondary)
}
}
}

@ -27,6 +27,7 @@ struct RegistrationSetupView: View {
@State private var showMoreInfos: Bool = false
@State private var hasChanges: Bool = false
@State private var displayWarning: Bool = false
@Environment(\.dismiss) private var dismiss
@ -74,6 +75,11 @@ struct RegistrationSetupView: View {
var body: some View {
List {
if displayWarning, tournament.enableOnlineRegistration, tournament.onlineTeams().isEmpty == false {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Pour l'instant Padel Club ne saura pas les prévenir automatiquement, vous devrez les contacter via l'écran de gestion des inscriptions.")
.foregroundStyle(.logoRed)
}
Section {
Toggle(isOn: $enableOnlineRegistration) {
Text("Activer")
@ -249,6 +255,7 @@ struct RegistrationSetupView: View {
}
.onChange(of: targetTeamCount) {
displayWarning = true
_hasChanged()
}

@ -21,6 +21,8 @@ struct TableStructureView: View {
@State private var updatedElements: Set<StructureElement> = Set()
@State private var structurePreset: PadelTournamentStructurePreset = .manual
@State private var buildWildcards: Bool = true
@State private var displayWarning: Bool = false
@FocusState private var stepperFieldIsFocused: Bool
var qualifiedFromGroupStage: Int {
@ -58,6 +60,10 @@ struct TableStructureView: View {
@ViewBuilder
var body: some View {
List {
if displayWarning, tournament.enableOnlineRegistration, tournament.onlineTeams().isEmpty == false {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Pour l'instant Padel Club ne saura pas les prévenir automatiquement, vous devrez les contacter via l'écran de gestion des inscriptions.")
.foregroundStyle(.logoRed)
}
if tournament.state() != .build {
Section {
@ -91,6 +97,9 @@ struct TableStructureView: View {
} label: {
Text("Nombre d'équipes")
}
.onChange(of: teamCount) {
displayWarning = true
}
LabeledContent {
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) {

@ -114,7 +114,9 @@ struct TournamentCellView: View {
}
Spacer()
if let tournament = tournament as? Tournament, displayStyle == .wide {
if tournament.isCanceled {
if tournament.refreshInProgress || tournament.refreshRanking {
ProgressView()
} else if tournament.isCanceled {
Text("Annulé".uppercased())
.capsule(foreground: .white, background: .logoRed)
} else if shouldTournamentBeOver {
@ -164,6 +166,24 @@ struct TournamentCellView: View {
Text(build.category.localizedLabel())
Text(build.age.localizedFederalAgeLabel())
}
if displayStyle == .wide, let tournament = tournament as? Tournament {
if tournament.enableOnlineRegistration {
let value: Int = tournament.onlineTeams().count
HStack {
Spacer()
if value == 0 {
Text("(dont aucune équipe inscrite en ligne)").foregroundStyle(.secondary).font(.footnote)
} else {
Text("(dont " + value.formatted() + " équipe\(value.pluralSuffix) inscrite\(value.pluralSuffix) en ligne)").foregroundStyle(.secondary).font(.footnote)
}
}
}
if tournament.refreshRanking {
Text("mise à jour des classements des joueurs").foregroundStyle(.secondary).font(.footnote)
} else if tournament.refreshInProgress {
Text("synchronisation des inscriptions en ligne").foregroundStyle(.secondary).font(.footnote)
}
}
}
}
.font(.caption)

Loading…
Cancel
Save