Merge branch 'main' of https://stax.alwaysdata.net/gitea/staxriver/PadelClub
commit
30affc2de9
@ -0,0 +1,4 @@ |
||||
<ul class="round"> |
||||
<li class="spacer"> {{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"> </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"> </li> |
||||
<li class="game game-top winner">\(winnerName)</li> |
||||
<li class="spacer"> </li> |
||||
</ul> |
||||
<ul class="main" style="visibility:hidden"> |
||||
</ul> |
||||
""" |
||||
brackets = brackets.appending(winner) |
||||
|
||||
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets) |
||||
return template |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,83 @@ |
||||
// |
||||
// ClubCourtSetupView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 20/05/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
import LeStorage |
||||
|
||||
struct ClubCourtSetupView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
@Bindable var club: Club |
||||
let displayContext: DisplayContext |
||||
@Binding var selectedCourt: Court? |
||||
|
||||
@ViewBuilder |
||||
var body: some View { |
||||
Section { |
||||
TournamentFieldsManagerView(localizedStringKey: "Terrains", count: $club.courtCount) |
||||
.disabled(displayContext == .lockedForEditing) |
||||
.onChange(of: club.courtCount) { |
||||
if displayContext != .addition { |
||||
do { |
||||
try dataStore.clubs.addOrUpdate(instance: club) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
} |
||||
} footer: { |
||||
if displayContext == .lockedForEditing { |
||||
Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) |
||||
} |
||||
} |
||||
|
||||
Section { |
||||
ForEach((0..<club.courtCount), id: \.self) { courtIndex in |
||||
_courtView(atIndex: courtIndex, tournamentClub: club) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _courtView(atIndex index: Int, tournamentClub: Club) -> some View { |
||||
let court = tournamentClub.customizedCourts.first(where: { $0.index == index }) |
||||
LabeledContent { |
||||
if displayContext == .edition { |
||||
FooterButtonView("personnaliser") { |
||||
if let court { |
||||
selectedCourt = court |
||||
} else { |
||||
let newCourt = Court(index: index, club: tournamentClub.id) |
||||
do { |
||||
try dataStore.courts.addOrUpdate(instance: newCourt) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
selectedCourt = newCourt |
||||
} |
||||
} |
||||
} |
||||
} label: { |
||||
if let court { |
||||
Text(court.courtTitle()) |
||||
HStack { |
||||
if court.indoor { |
||||
Text("Couvert") |
||||
} |
||||
if court.exitAllowed { |
||||
Text("Sortie autorisée") |
||||
} |
||||
} |
||||
} else { |
||||
Text(_courtName(atIndex: index)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func _courtName(atIndex index: Int) -> String { |
||||
Court.courtIndexedTitle(atIndex: index) |
||||
} |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,74 @@ |
||||
// |
||||
// TournamentBuildView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 19/05/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct TournamentBuildView: View { |
||||
var tournament: Tournament |
||||
|
||||
@ViewBuilder |
||||
var body: some View { |
||||
Section { |
||||
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.cashier) { |
||||
let tournamentStatus = tournament.cashierStatus() |
||||
LabeledContent { |
||||
Text(tournamentStatus.completion) |
||||
} label: { |
||||
Text("Encaissement") |
||||
Text(tournamentStatus.label) |
||||
} |
||||
} |
||||
} |
||||
|
||||
Section { |
||||
if tournament.groupStages().isEmpty == false { |
||||
NavigationLink(value: Screen.groupStage) { |
||||
LabeledContent { |
||||
Text(tournament.groupStageStatus()) |
||||
} label: { |
||||
Text("Poules") |
||||
} |
||||
} |
||||
} |
||||
|
||||
if tournament.rounds().isEmpty == false { |
||||
NavigationLink(value: Screen.round) { |
||||
LabeledContent { |
||||
Text(tournament.bracketStatus()) |
||||
} label: { |
||||
Text("Tableau") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
TournamentBuildView(tournament: Tournament.mock()) |
||||
} |
||||
@ -0,0 +1,70 @@ |
||||
// |
||||
// TournamentInscriptionView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 19/05/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
import LeStorage |
||||
|
||||
struct TournamentInscriptionView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
var tournament: Tournament |
||||
|
||||
@ViewBuilder |
||||
var body: some View { |
||||
Section { |
||||
NavigationLink(value: Screen.inscription) { |
||||
LabeledContent { |
||||
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) |
||||
} label: { |
||||
Text("Gestion des inscriptions") |
||||
if let closedRegistrationDate = tournament.closedRegistrationDate { |
||||
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) |
||||
} |
||||
} |
||||
} |
||||
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { |
||||
LabeledContent { |
||||
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) |
||||
} label: { |
||||
Text("Date limite") |
||||
} |
||||
} |
||||
|
||||
if tournament.state() != .running { |
||||
NavigationLink(value: Screen.structure) { |
||||
LabeledContent { |
||||
Text(tournament.structureDescriptionLocalizedLabel()) |
||||
.tint(.master) |
||||
} label: { |
||||
LabelStructure() |
||||
} |
||||
} |
||||
} |
||||
} footer: { |
||||
if tournament.inscriptionClosed() == false && tournament.state() == .build && tournament.unsortedTeams().isEmpty == false && tournament.hasStarted() == false { |
||||
Button { |
||||
tournament.lockRegistration() |
||||
_save() |
||||
} label: { |
||||
Text("clôturer les inscriptions") |
||||
.underline() |
||||
} |
||||
.buttonStyle(.borderless) |
||||
} else if tournament.state() != .running { |
||||
Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.") |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func _save() { |
||||
do { |
||||
try dataStore.tournaments.addOrUpdate(instance: tournament) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue