From 6d006d2917684a2ef89d57ded4ca823d358f8cd1 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 20 May 2024 08:39:50 +0200 Subject: [PATCH] add prints options --- PadelClub.xcodeproj/project.pbxproj | 60 +++++ PadelClub/Data/GroupStage.swift | 12 + PadelClub/Data/Match.swift | 29 +++ PadelClub/Data/Tournament.swift | 3 + .../HTML Templates/bracket-template.html | 4 + .../HTML Templates/groupstage-template.html | 95 +++++++ .../groupstagecol-template.html | 4 + .../groupstageentrant-template.html | 4 + .../groupstagerow-template.html | 4 + .../groupstagescore-template.html | 5 + .../HTML Templates/hiddenplayer-template.html | 2 + PadelClub/HTML Templates/match-template.html | 8 + PadelClub/HTML Templates/player-template.html | 3 + .../HTML Templates/tournament-template.html | 103 ++++++++ PadelClub/Utils/HtmlGenerator.swift | 198 ++++++++++++++ PadelClub/Utils/HtmlService.swift | 220 ++++++++++++++++ PadelClub/ViewModel/Screen.swift | 1 + PadelClub/Views/Club/ClubDetailView.swift | 2 +- .../Tournament/Screen/PrintSettingsView.swift | 244 ++++++++++++++++++ .../Tournament/TournamentBuildView.swift | 35 +-- .../Views/Tournament/TournamentView.swift | 18 +- 21 files changed, 1034 insertions(+), 20 deletions(-) create mode 100644 PadelClub/HTML Templates/bracket-template.html create mode 100644 PadelClub/HTML Templates/groupstage-template.html create mode 100644 PadelClub/HTML Templates/groupstagecol-template.html create mode 100644 PadelClub/HTML Templates/groupstageentrant-template.html create mode 100644 PadelClub/HTML Templates/groupstagerow-template.html create mode 100644 PadelClub/HTML Templates/groupstagescore-template.html create mode 100644 PadelClub/HTML Templates/hiddenplayer-template.html create mode 100644 PadelClub/HTML Templates/match-template.html create mode 100644 PadelClub/HTML Templates/player-template.html create mode 100644 PadelClub/HTML Templates/tournament-template.html create mode 100644 PadelClub/Utils/HtmlGenerator.swift create mode 100644 PadelClub/Utils/HtmlService.swift create mode 100644 PadelClub/Views/Tournament/Screen/PrintSettingsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 490a368..9a2c640 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -128,6 +128,19 @@ FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DF49A2BD8D23900822FA0 /* BarButtonView.swift */; }; FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */; }; + FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B732BFA00FC000B4573 /* HtmlService.swift */; }; + FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */; }; + FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; + FF1F4B832BFA02A4000B4573 /* tournament-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7F2BFA0105000B4573 /* tournament-template.html */; }; + FF1F4B842BFA02A4000B4573 /* groupstagescore-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */; }; + FF1F4B852BFA02A4000B4573 /* player-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7E2BFA0105000B4573 /* player-template.html */; }; + FF1F4B862BFA02A4000B4573 /* groupstagerow-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */; }; + FF1F4B872BFA02A4000B4573 /* hiddenplayer-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */; }; + FF1F4B882BFA02A4000B4573 /* bracket-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B762BFA0105000B4573 /* bracket-template.html */; }; + FF1F4B892BFA02A4000B4573 /* groupstagecol-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */; }; + FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; }; + FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; }; + FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; }; FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; @@ -432,6 +445,19 @@ FF1DF49A2BD8D23900822FA0 /* BarButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButtonView.swift; sourceTree = ""; }; FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBuildView.swift; sourceTree = ""; }; FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentInscriptionView.swift; sourceTree = ""; }; + FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlGenerator.swift; sourceTree = ""; }; + FF1F4B732BFA00FC000B4573 /* HtmlService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlService.swift; sourceTree = ""; }; + FF1F4B762BFA0105000B4573 /* bracket-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "bracket-template.html"; sourceTree = ""; }; + FF1F4B772BFA0105000B4573 /* groupstage-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstage-template.html"; sourceTree = ""; }; + FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagecol-template.html"; sourceTree = ""; }; + FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstageentrant-template.html"; sourceTree = ""; }; + FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagerow-template.html"; sourceTree = ""; }; + FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagescore-template.html"; sourceTree = ""; }; + FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "hiddenplayer-template.html"; sourceTree = ""; }; + FF1F4B7D2BFA0105000B4573 /* match-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "match-template.html"; sourceTree = ""; }; + FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = ""; }; + FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = ""; }; + FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = ""; }; FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.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 = ""; }; @@ -646,6 +672,7 @@ C425D4042B6D249E002A7B48 /* Assets.xcassets */, FFF024192BF48AEE001F14B4 /* Localization */, FF0EC54D2BB195CA0056B6D1 /* CSV */, + FF1F4B802BFA0105000B4573 /* HTML Templates */, C425D4062B6D249E002A7B48 /* Preview Content */, ); path = PadelClub; @@ -904,6 +931,23 @@ path = Club; sourceTree = ""; }; + FF1F4B802BFA0105000B4573 /* HTML Templates */ = { + isa = PBXGroup; + children = ( + FF1F4B762BFA0105000B4573 /* bracket-template.html */, + FF1F4B772BFA0105000B4573 /* groupstage-template.html */, + FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */, + FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */, + FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */, + FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */, + FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */, + FF1F4B7D2BFA0105000B4573 /* match-template.html */, + FF1F4B7E2BFA0105000B4573 /* player-template.html */, + FF1F4B7F2BFA0105000B4573 /* tournament-template.html */, + ); + path = "HTML Templates"; + sourceTree = ""; + }; FF39719B2B8DE04B004C4E75 /* Navigation */ = { isa = PBXGroup; children = ( @@ -953,6 +997,7 @@ FF1162802BCF945C000C4809 /* TournamentCashierView.swift */, FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */, FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */, + FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -1238,6 +1283,8 @@ C49EF0432BE286780077B5AA /* Key.swift */, FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */, FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */, + FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */, + FF1F4B732BFA00FC000B4573 /* HtmlService.swift */, FF8F26352BAD523300650388 /* PadelRule.swift */, FFF8ACD32B92392C008466FA /* SourceFileManager.swift */, FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, @@ -1402,6 +1449,16 @@ FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */, FF0EC54F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv in Resources */, FF0EC5502BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv in Resources */, + FF1F4B832BFA02A4000B4573 /* tournament-template.html in Resources */, + FF1F4B842BFA02A4000B4573 /* groupstagescore-template.html in Resources */, + FF1F4B852BFA02A4000B4573 /* player-template.html in Resources */, + FF1F4B862BFA02A4000B4573 /* groupstagerow-template.html in Resources */, + FF1F4B872BFA02A4000B4573 /* hiddenplayer-template.html in Resources */, + FF1F4B882BFA02A4000B4573 /* bracket-template.html in Resources */, + FF1F4B892BFA02A4000B4573 /* groupstagecol-template.html in Resources */, + FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */, + FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */, + FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */, FF0EC5512BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv in Resources */, FF0EC5522BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv in Resources */, FF0EC5532BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv in Resources */, @@ -1536,6 +1593,7 @@ FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */, FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, + FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, @@ -1601,6 +1659,7 @@ FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */, FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, @@ -1635,6 +1694,7 @@ FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStageSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, + FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index de2babd..78f349d 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -149,6 +149,18 @@ class GroupStage: ModelObject, Storable { return _matches().filter { matchIndexes.contains($0.index) } } + func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? { + if groupStagePosition == againstPosition { return nil } + let combos = Array((0.. [Match] { let runningMatches = runningMatches() return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ab0c4a3..ad87cfd 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -124,6 +124,30 @@ class Match: ModelObject, Storable { return startDate?.addingTimeInterval(minutesToAdd * 60.0) } + func winner() -> TeamRegistration? { + guard let winningTeamId else { return nil } + return Store.main.findById(winningTeamId) + } + + func localizedStartDate() -> String { + if let startDate { + return startDate.formatted(date: .abbreviated, time: .shortened) + } else { + return "" + } + } + + func scoreLabel() -> String { + if hasWalkoutTeam() == true { + return "WO" + } + let scoreOne = teamScore(.one)?.score?.components(separatedBy: ",") ?? [] + let scoreTwo = teamScore(.two)?.score?.components(separatedBy: ",") ?? [] + let tuples = zip(scoreOne, scoreTwo).map { ($0, $1) } + let scores = tuples.map { $0 + "/" + $1 }.joined(separator: " ") + return scores + } + func resetMatch() { losingTeamId = nil winningTeamId = nil @@ -541,6 +565,11 @@ class Match: ModelObject, Storable { guard let winningTeamId else { return false } return winningTeamId == team?.id } + + func teamWon(atPosition teamPosition: TeamPosition) -> Bool { + guard let winningTeamId else { return false } + return winningTeamId == team(teamPosition)?.id + } func team(_ team: TeamPosition) -> TeamRegistration? { if groupStage != nil { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 8a4edf7..9a7a5f8 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -292,6 +292,7 @@ class Tournament : ModelObject, Storable { case build case running case canceled + case finished } func publishedTeamsDate() -> Date { @@ -401,6 +402,8 @@ class Tournament : ModelObject, Storable { if self.isCanceled == true { return .canceled } + + if self.hasEnded() { return .finished } let isBuild = (groupStageCount > 0 && groupStages().isEmpty == false) || rounds().isEmpty == false diff --git a/PadelClub/HTML Templates/bracket-template.html b/PadelClub/HTML Templates/bracket-template.html new file mode 100644 index 0000000..c344123 --- /dev/null +++ b/PadelClub/HTML Templates/bracket-template.html @@ -0,0 +1,4 @@ +
    +
  •  {{roundLabel}}
  • + {{match-template}} +
diff --git a/PadelClub/HTML Templates/groupstage-template.html b/PadelClub/HTML Templates/groupstage-template.html new file mode 100644 index 0000000..6e203b9 --- /dev/null +++ b/PadelClub/HTML Templates/groupstage-template.html @@ -0,0 +1,95 @@ + + + + + + + + +
+ + + + + {{teamsCol}} + + {{teamsRow}} +
+

{{bracketTitle}}

+

{{bracketStartDate}}

+
+ +
+ + diff --git a/PadelClub/HTML Templates/groupstagecol-template.html b/PadelClub/HTML Templates/groupstagecol-template.html new file mode 100644 index 0000000..b7b017e --- /dev/null +++ b/PadelClub/HTML Templates/groupstagecol-template.html @@ -0,0 +1,4 @@ + + {{team}} + + diff --git a/PadelClub/HTML Templates/groupstageentrant-template.html b/PadelClub/HTML Templates/groupstageentrant-template.html new file mode 100644 index 0000000..d7f2cdb --- /dev/null +++ b/PadelClub/HTML Templates/groupstageentrant-template.html @@ -0,0 +1,4 @@ +
{{playerOne}}
+
{{weightOne}}
+
{{playerTwo}}
+
{{weightTwo}}
diff --git a/PadelClub/HTML Templates/groupstagerow-template.html b/PadelClub/HTML Templates/groupstagerow-template.html new file mode 100644 index 0000000..661386d --- /dev/null +++ b/PadelClub/HTML Templates/groupstagerow-template.html @@ -0,0 +1,4 @@ + + {{team}} + {{scores}} + diff --git a/PadelClub/HTML Templates/groupstagescore-template.html b/PadelClub/HTML Templates/groupstagescore-template.html new file mode 100644 index 0000000..a9d7f7c --- /dev/null +++ b/PadelClub/HTML Templates/groupstagescore-template.html @@ -0,0 +1,5 @@ + +
{{winner}}
+
{{score}}
+ + diff --git a/PadelClub/HTML Templates/hiddenplayer-template.html b/PadelClub/HTML Templates/hiddenplayer-template.html new file mode 100644 index 0000000..054e440 --- /dev/null +++ b/PadelClub/HTML Templates/hiddenplayer-template.html @@ -0,0 +1,2 @@ + +
diff --git a/PadelClub/HTML Templates/match-template.html b/PadelClub/HTML Templates/match-template.html new file mode 100644 index 0000000..aba166a --- /dev/null +++ b/PadelClub/HTML Templates/match-template.html @@ -0,0 +1,8 @@ +
  • + {{entrantOne}} +
  • +
  • {{matchDescription}}
  • +
  • + {{entrantTwo}} +
  • +
  •  
  • diff --git a/PadelClub/HTML Templates/player-template.html b/PadelClub/HTML Templates/player-template.html new file mode 100644 index 0000000..38579d9 --- /dev/null +++ b/PadelClub/HTML Templates/player-template.html @@ -0,0 +1,3 @@ + +
    {{playerOne}}{{weightOne}}
    +
    {{playerTwo}}{{weightTwo}}
    diff --git a/PadelClub/HTML Templates/tournament-template.html b/PadelClub/HTML Templates/tournament-template.html new file mode 100644 index 0000000..9bcb606 --- /dev/null +++ b/PadelClub/HTML Templates/tournament-template.html @@ -0,0 +1,103 @@ + + + + + + + + +

    {{tournamentTitle}}

    +
    + {{brackets}} +
    + + diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift new file mode 100644 index 0000000..c8e8d79 --- /dev/null +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -0,0 +1,198 @@ +// +// HtmlGenerator.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 23/10/2023. +// + +import Foundation +import UIKit +import WebKit +import PDFKit + +class HtmlGenerator: ObservableObject { + + init(tournament: Tournament) { + self.tournament = tournament + } + + let tournament: Tournament + @Published var zoomLevel: CGFloat? = 2.0 + @Published var includeBracket: Bool = true + @Published var includeGroupStage: Bool = true + @Published var includeLoserBracket: Bool = false + @Published var displayHeads: Bool = false + @Published var groupStageIsReady: Bool = false + @Published var displayRank: Bool = false + private var pdfDocument: PDFDocument = PDFDocument() + private var rects: [CGRect] = [] + private var completionHandler: ((Result) -> ())? + @Published var width: CGFloat = 0 + @Published var height: CGFloat = 0 + private var webView: WKWebView = WKWebView() + private var groupStageDone: Int = 0 + + var estimatedPageCount: Int { + if let zoomLevel { + let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) + let numberOfPageInWidth = Int(width / pageSize.width) + 1 + let numberOfPageInHeight = Int(height / pageSize.height) + 1 + return numberOfPageInWidth * numberOfPageInHeight + } else { + return 1 + } + } + + func preparePDF(completionHandler: @escaping ((Result) -> ())) { + self.completionHandler = completionHandler + } + + func generateWebView(webView: WKWebView) { + self.webView = webView + self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in + if complete != nil { + self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in + self.height = height as! CGFloat + }) + self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in + self.width = width as! CGFloat + }) + } + }) + } + + func generateGroupStage(webView: WKWebView) { + webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in + if complete != nil { + webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in + let height = height as! CGFloat + webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in + let width = width as! CGFloat + + print("bracket", width, height) + let config = WKPDFConfiguration() + config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(width))) + webView.createPDF(configuration: config){ result in + switch result{ + case .success(let data): + let newPage = PDFDocument(data: data)! + let page = newPage.page(at: 0)! + let copiedPage = page.copy() as! PDFPage + self.pdfDocument.insert(copiedPage, at: self.pdfDocument.pageCount) + + DispatchQueue.main.async { + self.groupStageDone += 1 + if self.groupStageDone == self.tournament.groupStages().count { + self.groupStageIsReady = true + self.completionHandler?(.success(self.savePDF())) + } + } + case .failure(let error): + self.completionHandler?(.failure(error)) + } + } + + }) + }) + } + }) + + } + + func buildPDF() { + groupStageDone = 0 + groupStageIsReady = false + pdfDocument = PDFDocument() + try? FileManager.default.removeItem(at: pdfURL!) + print("buildPDF", width, height, zoomLevel ?? 0) + if let zoomLevel { + let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) + let numberOfPageInWidth = Int(width / pageSize.width) + 1 + let numberOfPageInHeight = Int(height / pageSize.height) + 1 + for w in 0.. String { + //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() + HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false) + } + + var pdfURL: URL? { + guard let pdfFolderURL = getFilePath() else { + return nil + } + let date = tournament.startDate + let stringDate = date.formatted(.iso8601 + .year() + .month() + .day() + .dateSeparator(.dash)) + + let name = tournament.tournamentLevel.localizedLabel() + "-" + tournament.tournamentCategory.importingRawValue + return pdfFolderURL.appendingPathComponent(stringDate + "-" + name + ".pdf") + } + + func savePDF() -> Bool { + pdfDocument.write(to: pdfURL!) + } + + var isReady: Bool { + FileManager.default.fileExists(atPath: pdfURL!.path()) + } + + func getFilePath() -> URL? { + if FileManager.default.fileExists(atPath: pdfFolderURL.path) { + return pdfFolderURL + } else { + do { + try FileManager.default.createDirectory(at: pdfFolderURL, withIntermediateDirectories: true, attributes: nil) + return pdfFolderURL + } catch { + print("getFilePath", error.localizedDescription) + return nil + } + } + } + + var pdfFolderURL: URL { + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + return URL(fileURLWithPath: documentsPath.appending("/pdfs")) + } +} diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift new file mode 100644 index 0000000..8ab63ef --- /dev/null +++ b/PadelClub/Utils/HtmlService.swift @@ -0,0 +1,220 @@ +// +// HtmlService.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 25/10/2023. +// + +import Foundation + +enum HtmlService { + + case template(tournament: Tournament) + case bracket(tournament: Tournament, roundIndex: Int) + case match(match: Match) + case player(entrant: TeamRegistration) + case hiddenPlayer + case groupstage(groupStage: GroupStage) + case groupstageEntrant(entrant: TeamRegistration) + case groupstageColumn(entrant: TeamRegistration, position: String) + case groupstageRow(entrant: TeamRegistration, teamsPerBracket: Int) + case groupstageScore(score: Match?, shouldHide: Bool) + + var url: URL { + return URL(fileURLWithPath: "\(self.fileName)") + } + + var fileName: String { + switch self { + case .template: + return "tournament-template" + case .bracket: + return "bracket-template" + case .match: + return "match-template" + case .player: + return "player-template" + case .hiddenPlayer: + return "hiddenplayer-template" + case .groupstage: + return "groupstage-template" + case .groupstageEntrant: + return "groupstageentrant-template" + case .groupstageRow: + return "groupstagerow-template" + case .groupstageColumn: + return "groupstagecol-template" + case .groupstageScore: + return "groupstagescore-template" + } + } + + func html(headName: Bool, withRank: Bool, withScore: Bool) -> String { + guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else { + fatalError() + } + + guard let html = try? String(contentsOfFile: file, encoding: String.Encoding.utf8) else { + fatalError() + } + + switch self { + case .groupstage(let bracket): + var template = html + if let startDate = bracket.startDate { + template = template.replacingOccurrences(of: "{{bracketStartDate}}", with: startDate.formatted()) + } else { + template = template.replacingOccurrences(of: "{{bracketStartDate}}", with: "") + } + template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle()) + template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle()) + + var col = "" + var row = "" + bracket.teams().forEach { entrant in + col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withScore: withScore)) + row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withScore: withScore)) + } + template = template.replacingOccurrences(of: "{{teamsCol}}", with: col) + template = template.replacingOccurrences(of: "{{teamsRow}}", with: row) + + return template + case .groupstageEntrant(let entrant): + var template = html + if let playerOne = entrant.players()[safe: 0] { + template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) + if withRank { + template = template.replacingOccurrences(of: "{{weightOne}}", with: "(\(playerOne.formattedRank())") + } else { + template = template.replacingOccurrences(of: "{{weightOne}}", with: "") + } + } + + if let playerTwo = entrant.players()[safe: 1] { + template = template.replacingOccurrences(of: "{{playerTwo}}", with: playerTwo.playerLabel()) + if withRank { + template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank())") + } else { + template = template.replacingOccurrences(of: "{{weightTwo}}", with: "") + } + } + return template + case .groupstageRow(let entrant, let teamsPerBracket): + var template = html + template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withScore: withScore)) + + var scores = "" + (0.. +
  •  
  • +
  • \(winnerName)
  • +
  •  
  • + + + """ + brackets = brackets.appending(winner) + + template = template.replacingOccurrences(of: "{{brackets}}", with: brackets) + return template + } + } +} diff --git a/PadelClub/ViewModel/Screen.swift b/PadelClub/ViewModel/Screen.swift index ec0774a..cb4d0d6 100644 --- a/PadelClub/ViewModel/Screen.swift +++ b/PadelClub/ViewModel/Screen.swift @@ -19,4 +19,5 @@ enum Screen: String, Codable { case rankings case broadcast case event + case print } diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index f9b877b..a848205 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -213,7 +213,7 @@ struct ClubDetailView: View { if displayContext == .edition || displayContext == .lockedForEditing { let isFavorite = club.isFavorite() Section { - RowButtonView(isFavorite ? "Mettre en favori" : "Retirer des favoris", role: isFavorite ? nil : .destructive) { + RowButtonView(isFavorite ? "Retirer des favoris" : "Mettre en favori", role: isFavorite ? .destructive : nil) { if isFavorite { dataStore.user.clubs.removeAll(where: { $0 == club.id }) } else { diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift new file mode 100644 index 0000000..7aa4216 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -0,0 +1,244 @@ +// +// PrintSettingsView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 23/10/2023. +// + +import SwiftUI +import WebKit + +struct PrintSettingsView: View { + let tournament: Tournament + @StateObject var generator: HtmlGenerator + @State private var presentShareView: Bool = false + @State private var prepareGroupStage: Bool = false + + init(tournament: Tournament) { + self.tournament = tournament + _generator = StateObject(wrappedValue: HtmlGenerator(tournament: tournament)) + } + + var body: some View { + List { + Section { +// Toggle(isOn: $generator.displayHeads, label: { +// Text("Afficher les têtes de séries") +// }) + + Toggle(isOn: $generator.displayRank, label: { + Text("Afficher le classement du joueur") + }) + + Toggle(isOn: $generator.includeBracket, label: { + Text("Tableau") + }) + +// Toggle(isOn: $generator.includeLoserBracket, label: { +// Text("Tableau des matchs de classements") +// }) + + if tournament.groupStages().isEmpty == false { + Toggle(isOn: $generator.includeGroupStage, label: { + Text("Poules") + }) + } + } + + if generator.includeBracket { + Section { + Picker(selection: $generator.zoomLevel) { + Text("1 page").tag(nil as Optional) + Text("50%").tag(2.0 as Optional) + Text("100%").tag(1.0 as Optional) + } label: { + Text("Zoom") + } + + HStack { + Text("Nombre de page A4 à imprimer") + Spacer() + Text(generator.estimatedPageCount.formatted()) + } + } header: { + Text("Tableau principal") + } + } + + Section { + NavigationLink { + WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in + }) + } label: { + Text("Aperçu du tableau") + } + } + + ForEach(tournament.groupStages()) { groupStage in + Section { + NavigationLink { + WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateGroupStage(webView: webView) + } else { + print("preparePDF", "is loading") + } + }) + } label: { + Text("Aperçu de la \(groupStage.groupStageTitle())") + } + } + } + } + .background { + WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateWebView(webView: webView) + } else { + print("preparePDF", "is loading") + } + }).opacity(0) + + if prepareGroupStage { + ForEach(tournament.groupStages()) { groupStage in + WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateGroupStage(webView: webView) + } else { + print("preparePDF", "is loading") + } + }).opacity(0) + } + } + } + .navigationTitle("Imprimer") + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(.visible, for: .bottomBar) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .bottomBar) { + Button { + generator.preparePDF { result in + switch result { + case .success(true): + if generator.includeGroupStage && generator.groupStageIsReady == false { + self.prepareGroupStage = true + } else { + self.presentShareView = true + } + case .success(false): + print("didn't save pdf") + break + case .failure(let error): + print(error) + break + } + } + + self.prepareGroupStage = false + self.generator.buildPDF() + + } label: { + Text("Obtenir le PDF") + } + .disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false) + .buttonStyle(.borderedProminent) + } + ToolbarItem(placement: .topBarTrailing) { + Menu { + Section { + ShareLink(item: generator.generateHtml()) { + Text("Tableau") + } + + if let groupStage = tournament.groupStages().first { + ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) { + Text("Poule") + } + } + } header: { + Text("Partager le code source HTML") + } + } label: { + Label("Options", systemImage: "ellipsis.circle") + } + } + } + .sheet(isPresented: $presentShareView) { + if let pdfURL = generator.pdfURL { + ShareSheet(urls: [pdfURL]) + } + } + } +} + +// MARK: Share Sheet +struct ShareSheet: UIViewControllerRepresentable{ + + var urls: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: urls, applicationActivities: nil) + + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { + + } +} + +struct WebView: UIViewRepresentable { + var htmlRawData: String? = nil + var url: URL? = nil + var loadStatusChanged: ((Bool, Error?, WKWebView) -> Void)? = nil + + func makeCoordinator() -> WebView.Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> WKWebView { + let view = WKWebView() + view.navigationDelegate = context.coordinator + + if let htmlRawData { + view.loadHTMLString(htmlRawData, baseURL: nil) + } + if let url { + view.loadFileURL(url, allowingReadAccessTo: url) + } + return view + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + // you can access environment via context.environment here + // Note that this method will be called A LOT + } + + class Coordinator: NSObject, WKNavigationDelegate { + let parent: WebView + + init(_ parent: WebView) { + self.parent = parent + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + parent.loadStatusChanged?(true, nil, webView) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + parent.loadStatusChanged?(false, nil, webView) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + parent.loadStatusChanged?(false, error, webView) + } + } +} + diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index 21c830e..f040181 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -13,26 +13,27 @@ struct TournamentBuildView: View { @ViewBuilder var body: some View { Section { - NavigationLink(value: Screen.schedule) { - let tournamentStatus = tournament.scheduleStatus() - LabeledContent { - Text(tournamentStatus.completion) - } label: { - Text("Horaires") - Text(tournamentStatus.label) + if tournament.state() != .finished { + NavigationLink(value: Screen.schedule) { + let tournamentStatus = tournament.scheduleStatus() + LabeledContent { + Text(tournamentStatus.completion) + } label: { + Text("Horaires") + Text(tournamentStatus.label) + } } - } - - NavigationLink(value: Screen.call) { - let tournamentStatus = tournament.callStatus() - LabeledContent { - Text(tournamentStatus.completion) - } label: { - Text("Convocations") - Text(tournamentStatus.label) + + NavigationLink(value: Screen.call) { + let tournamentStatus = tournament.callStatus() + LabeledContent { + Text(tournamentStatus.completion) + } label: { + Text("Convocations") + Text(tournamentStatus.label) + } } } - NavigationLink(value: Screen.cashier) { let tournamentStatus = tournament.cashierStatus() LabeledContent { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 4543732..5619109 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -42,7 +42,9 @@ struct TournamentView: View { TipView(tournamentRunningTip) .tipStyle(tint: nil) - SubscriptionInfoView() + if tournament.state() != .finished { + SubscriptionInfoView() + } switch tournament.state() { case .canceled: @@ -60,8 +62,13 @@ struct TournamentView: View { case .build: TournamentInscriptionView(tournament: tournament) TournamentInitView(tournament: tournament) + Section { + NavigationLink(value: Screen.print) { + Label("Imprimer", systemImage: "printer") + } + } TournamentBuildView(tournament: tournament) - case .running: + case .running, .finished: TournamentInscriptionView(tournament: tournament) TournamentBuildView(tournament: tournament) if tournament.hasEnded() { @@ -103,6 +110,8 @@ struct TournamentView: View { if let event = tournament.eventObject() { EventView(event: event) } + case .print: + PrintSettingsView(tournament: tournament) } } .environment(tournament) @@ -161,6 +170,11 @@ struct TournamentView: View { NavigationLink(value: Screen.broadcast) { Text("Publication") } + + NavigationLink(value: Screen.print) { + Label("Imprimer", systemImage: "printer") + } + } } label: { LabelOptions()