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