add prints options

multistore
Razmig Sarkissian 1 year ago
parent 2987c509c5
commit 6d006d2917
  1. 60
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/Data/GroupStage.swift
  3. 29
      PadelClub/Data/Match.swift
  4. 3
      PadelClub/Data/Tournament.swift
  5. 4
      PadelClub/HTML Templates/bracket-template.html
  6. 95
      PadelClub/HTML Templates/groupstage-template.html
  7. 4
      PadelClub/HTML Templates/groupstagecol-template.html
  8. 4
      PadelClub/HTML Templates/groupstageentrant-template.html
  9. 4
      PadelClub/HTML Templates/groupstagerow-template.html
  10. 5
      PadelClub/HTML Templates/groupstagescore-template.html
  11. 2
      PadelClub/HTML Templates/hiddenplayer-template.html
  12. 8
      PadelClub/HTML Templates/match-template.html
  13. 3
      PadelClub/HTML Templates/player-template.html
  14. 103
      PadelClub/HTML Templates/tournament-template.html
  15. 198
      PadelClub/Utils/HtmlGenerator.swift
  16. 220
      PadelClub/Utils/HtmlService.swift
  17. 1
      PadelClub/ViewModel/Screen.swift
  18. 2
      PadelClub/Views/Club/ClubDetailView.swift
  19. 244
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  20. 35
      PadelClub/Views/Tournament/TournamentBuildView.swift
  21. 18
      PadelClub/Views/Tournament/TournamentView.swift

@ -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 = "<group>"; };
FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBuildView.swift; sourceTree = "<group>"; };
FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentInscriptionView.swift; sourceTree = "<group>"; };
FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlGenerator.swift; sourceTree = "<group>"; };
FF1F4B732BFA00FC000B4573 /* HtmlService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlService.swift; sourceTree = "<group>"; };
FF1F4B762BFA0105000B4573 /* bracket-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "bracket-template.html"; sourceTree = "<group>"; };
FF1F4B772BFA0105000B4573 /* groupstage-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstage-template.html"; sourceTree = "<group>"; };
FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagecol-template.html"; sourceTree = "<group>"; };
FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstageentrant-template.html"; sourceTree = "<group>"; };
FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagerow-template.html"; sourceTree = "<group>"; };
FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagescore-template.html"; sourceTree = "<group>"; };
FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "hiddenplayer-template.html"; sourceTree = "<group>"; };
FF1F4B7D2BFA0105000B4573 /* match-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "match-template.html"; sourceTree = "<group>"; };
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
};
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 */,

@ -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..<size).combinations(ofCount: 2))
var matchIndexes = [Int]()
for (index, combo) in combos.enumerated() {
if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing
matchIndexes.append(index)
}
}
return _matches().first(where: { matchIndexes.contains($0.index) })
}
func availableToStart() -> [Match] {
let runningMatches = runningMatches()
return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })

@ -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 {

@ -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

@ -0,0 +1,4 @@
<ul class="round">
<li class="spacer">&nbsp;{{roundLabel}}</li>
{{match-template}}
</ul>

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
main{
display:flex;
display: inline-block;
padding: 1%;
}
/*
* General Styles
*/
body{
font-family:sans-serif;
}
td,
th {
border: 1px solid rgb(190, 190, 190);
padding: 10px;
text-align: left;
height: 4rem;
overflow: hidden;
}
td {
text-align: center;
}
td[scope='hide'] {
background-color: #a9a9a9;
text-align: left;
}
tr {
background-color: #fff;
text-align: left;
}
th[scope='col'] {
background-color: #d7d9f2;
text-align: left;
}
th[scope='row'] {
background-color: #d7d9f2;
text-align: left;
}
caption {
padding: 10px;
caption-side: bottom;
}
table {
border-collapse: collapse;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
table-layout: fixed;
width: 100%;
}
.score {
text-align: center;
}
.player {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body>
<main id="tournament">
<table>
<caption>
<h2>{{bracketTitle}}</h2>
<h3>{{bracketStartDate}}</h3>
</caption>
<tr>
<th scope="col" style="visibility:hidden"></th>
{{teamsCol}}
</tr>
{{teamsRow}}
</table>
</main>
</body>
</html>

@ -0,0 +1,4 @@
<th scope="{{tablePosition}}">
{{team}}
</th>

@ -0,0 +1,4 @@
<div class="player">{{playerOne}}</div>
<div class="player">{{weightOne}}</div>
<div class="player">{{playerTwo}}</div>
<div class="player">{{weightTwo}}</div>

@ -0,0 +1,4 @@
<tr>
{{team}}
{{scores}}
</tr>

@ -0,0 +1,5 @@
<td scope="{{hide}}">
<div class="score">{{winner}}</div>
<div class="score">{{score}}</div>
</td>

@ -0,0 +1,2 @@
<div class="hiddenPlayer"> </div>

@ -0,0 +1,8 @@
<li class="game game-top {{entrantOneWon}}" style="visibility:{{hidden}}">
{{entrantOne}}
</li>
<li class="game game-spacer" style="visibility:{{hidden}}"><div class="multiline">{{matchDescription}}</div></li>
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}">
{{entrantTwo}}
</li>
<li class="spacer">&nbsp;</li>

@ -0,0 +1,3 @@
<div class="player">{{playerOne}}<span>{{weightOne}}</span></div>
<div class="player">{{playerTwo}}<span>{{weightTwo}}</span></div>

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
main{
display:flex;
flex-direction:row;
padding: 1%;
}
.round{
display:flex;
flex-direction:column;
justify-content:center;
min-width: 400px;
list-style:none;
padding:0;
border-right: 1px dashed #ccc;
}
.round[scope='last'] {
border-right: 0px;
}
.round .spacer{ flex-grow:1;
font-size:24px;
text-align: center;
color: #bbb;
font-style:italic;
}
.round .spacer:first-child,
.round .spacer:last-child{ flex-grow:.5; }
.round .game-spacer{
flex-grow:1;
}
/*
* General Styles
*/
body{
font-family:sans-serif;
font-size:32px;
padding:10px;
line-height:32px;
}
li.game{
padding-left:20px;
}
li.game.winner{
font-weight:bold;
}
li.game span{
float:right;
margin-right:5px;
}
li.game-top{
border-bottom:2px solid #4f7a38;
}
li.game-spacer{
border-right:2px solid #4f7a38;
min-height:156px;
text-align: right;
display : flex;
justify-content: center;
align-items : center;
}
.multiline {
white-space: pre-wrap;
}
li.game-bottom{
border-top:2px solid #4f7a38;
}
.player {
font-size:28px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hiddenPlayer {
font-size:28px;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body>
<h1>{{tournamentTitle}}</h1>
<main id="tournament">
{{brackets}}
</main>
</body>
</html>

@ -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<Bool, Error>) -> ())?
@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<Bool, Error>) -> ())) {
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..<numberOfPageInWidth {
for h in 0..<numberOfPageInHeight {
let rect = CGRect(x: CGFloat(w) * pageSize.width, y: CGFloat(h) * pageSize.height, width: pageSize.width, height: pageSize.height)
rects.append(rect)
}
}
} else {
rects = [CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(height)))]
}
if includeBracket {
createPage()
} else {
DispatchQueue.main.async {
self.completionHandler?(.success(true))
}
}
}
func createPage() {
let config = WKPDFConfiguration()
config.rect = rects[pdfDocument.pageCount]
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)
if self.pdfDocument.pageCount < self.rects.count {
self.createPage()
} else {
self.completionHandler?(.success(self.savePDF()))
}
case .failure(let error):
self.completionHandler?(.failure(error))
}
}
}
func generateHtml() -> 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"))
}
}

@ -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..<teamsPerBracket).forEach { index in
let shouldHide = entrant.groupStagePosition! == index
var match: Match? = nil
if shouldHide == false {
match = entrant.groupStageObject()?.matchPlayed(by: entrant.groupStagePosition!, againstPosition: index)
}
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withScore: withScore))
}
template = template.replacingOccurrences(of: "{{scores}}", with: scores)
return template
case .groupstageColumn(let entrant, let position):
var template = html
template = template.replacingOccurrences(of: "{{tablePosition}}", with: position)
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withScore: withScore))
return template
case .groupstageScore(let match, let shouldHide):
var template = html
if match == nil || withScore == false {
template = template.replacingOccurrences(of: "{{winner}}", with: "")
template = template.replacingOccurrences(of: "{{score}}", with: "")
} else {
template = template.replacingOccurrences(of: "{{winner}}", with: match!.winner()!.teamLabel())
template = template.replacingOccurrences(of: "{{score}}", with: match!.scoreLabel())
}
template = template.replacingOccurrences(of: "{{hide}}", with: shouldHide ? "hide" : "")
return template
case .player(let entrant):
var template = html
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel(.short))
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(.short))
if withRank {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank())")
} else {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "")
}
}
return template
case .hiddenPlayer:
return html + html
case .match(let match):
var template = html
if let entrantOne = match.team(.one) {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withScore: withScore))
} else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore))
}
if let entrantTwo = match.team(.two) {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withScore: withScore))
} else {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore))
}
if match.disabled {
template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden")
} else {
template = template.replacingOccurrences(of: "{{hidden}}", with: "")
}
if match.hasEnded() {
if match.teamWon(atPosition: .one) == true {
template = template.replacingOccurrences(of: "{{entrantOneWon}}", with: "winner")
} else if match.teamWon(atPosition: .two) == true {
template = template.replacingOccurrences(of: "{{entrantTwoWon}}", with: "winner")
}
template = template.replacingOccurrences(of: "{{matchDescription}}", with: [match.localizedStartDate(), match.scoreLabel()].joined(separator: "\n"))
}
template = template.replacingOccurrences(of: "{{matchDescription}}", with: "")
return template
case .bracket(let tournament, let roundIndex):
var template = ""
var bracket = ""
if let round = tournament.rounds().first(where: { $0.index == roundIndex }) {
for (_, match) in round.playedMatches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore))
}
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
}
return bracket
case .template(let tournament):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle())
var brackets = ""
for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(tournament: tournament, roundIndex: round.index).html(headName: headName, withRank: withRank, withScore: withScore))
}
var winnerName = ""
// if let tournamentWinner = tournament.winnerEntrant {
// winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withScore: withScore)
// }
let winner = """
<ul class="round" scope="last">
<li class="spacer">&nbsp;</li>
<li class="game game-top winner">\(winnerName)</li>
<li class="spacer">&nbsp;</li>
</ul>
<ul class="main" style="visibility:hidden">
</ul>
"""
brackets = brackets.appending(winner)
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets)
return template
}
}
}

@ -19,4 +19,5 @@ enum Screen: String, Codable {
case rankings
case broadcast
case event
case print
}

@ -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 {

@ -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<CGFloat>)
Text("50%").tag(2.0 as Optional<CGFloat>)
Text("100%").tag(1.0 as Optional<CGFloat>)
} 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)
}
}
}

@ -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 {

@ -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()

Loading…
Cancel
Save