improvement on fortune wheel

multistore
Razmig Sarkissian 2 years ago
parent 1caa33182d
commit 718e51b324
  1. 8
      PadelClub/Data/Match.swift
  2. 10
      PadelClub/Data/PlayerRegistration.swift
  3. 2
      PadelClub/Data/TeamRegistration.swift
  4. 12
      PadelClub/Data/Tournament.swift
  5. 38
      PadelClub/Extensions/String+Extensions.swift
  6. 2
      PadelClub/ViewModel/AgendaDestination.swift
  7. 107
      PadelClub/Views/Components/FortuneWheelView.swift
  8. 41
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  9. 19
      PadelClub/Views/Round/RoundView.swift
  10. 2
      PadelClub/Views/Round/RoundsView.swift
  11. 8
      PadelClub/Views/Tournament/Screen/BroadcastView.swift

@ -56,11 +56,11 @@ class Match: ModelObject, Storable {
}
func matchWarningSubject() -> String {
[roundTitle(), matchTitle()].compacted().joined(separator: " ")
[roundTitle(), matchTitle(.short)].compacted().joined(separator: " ")
}
func matchWarningMessage() -> String {
[roundTitle(), matchTitle(), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n")
[roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n")
}
func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
@ -448,6 +448,10 @@ class Match: ModelObject, Storable {
endDate ?? .distantFuture
}
func hasSpaceLeft() -> Bool {
teams().count == 1
}
func isReady() -> Bool {
teams().count == 2
}

@ -139,9 +139,15 @@ class PlayerRegistration: ModelObject, Storable {
func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide:
lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
case .short:
lastName.trimmed.capitalized + " " + firstName.trimmed.prefix(1).capitalized + "."
let names = lastName.components(separatedBy: .whitespaces)
if lastName.components(separatedBy: .whitespaces).count > 1 {
if let firstLongWord = names.first(where: { $0.count > 3 }) {
return firstLongWord.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "."
}
}
return lastName.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "."
}
}

@ -123,7 +123,7 @@ class TeamRegistration: ModelObject, Storable {
case .wide:
players().map { $0.playerLabel(displayStyle) }.joined(separator: " & ")
case .short:
players().map { $0.playerLabel(.wide) }.joined(separator: "\n")
players().map { $0.playerLabel(.short) }.joined(separator: "\n")
}
}

@ -328,6 +328,10 @@ class Tournament : ModelObject, Storable {
let groupStages = groupStages()
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
}
func matchesWithSpace() -> [Match] {
getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? []
}
func getActiveRound(withSeeds: Bool = false) -> Round? {
let rounds = rounds()
@ -979,6 +983,14 @@ class Tournament : ModelObject, Storable {
selectedSortedTeams().firstIndex(where: { $0.id == team.id })
}
func labelIndexOf(team: TeamRegistration) -> String? {
if let teamIndex = indexOf(team: team) {
return "#" + (teamIndex + 1).formatted()
} else {
return nil
}
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date())
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)

@ -7,7 +7,12 @@
import Foundation
// MARK: - Trimming and stuff
extension String {
func trunc(length: Int, trailing: String = "") -> String {
return (self.count > length) ? self.prefix(length) + trailing : self
}
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
@ -29,6 +34,7 @@ extension String {
}
}
// MARK: - Club Name
extension String {
func acronym() -> String {
let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
@ -58,16 +64,8 @@ extension String {
}
}
// MARK: - FFT License
extension String {
enum RegexStatic {
static let mobileNumber = /^0[6-7]/
//static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/
}
func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil
}
var computedLicense: String {
if let licenseKey {
return self + licenseKey
@ -138,20 +136,24 @@ extension String {
}
return nil
}
}
extension String {
func licencesFound() -> [String] {
let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) }
}
}
extension LosslessStringConvertible {
var string: String { .init(self) }
}
// MARK: - FFT Source Importing
extension String {
enum RegexStatic {
static let mobileNumber = /^0[6-7]/
//static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/
}
func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil
}
//april 04-2024 bug with accent characters / adobe / fft
mutating func replace(characters: [(Character, Character)]) {
for (targetChar, replacementChar) in characters {
@ -160,7 +162,13 @@ extension String {
}
}
// MARK: - Player Names
extension StringProtocol {
var firstUppercased: String { prefix(1).uppercased() + dropFirst() }
var firstCapitalized: String { prefix(1).capitalized + dropFirst() }
}
// MARK: - todo clean up ??
extension LosslessStringConvertible {
var string: String { .init(self) }
}

@ -53,7 +53,7 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable {
case .history:
DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
case .tenup:
FederalDataViewModel.shared.filteredFederalTournaments.count
FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+)
}
}

@ -8,24 +8,35 @@
import SwiftUI
protocol SpinDrawable {
func segmentLabel() -> String
func segmentLabel(_ displayStyle: DisplayStyle) -> [String]
}
extension String: SpinDrawable {
func segmentLabel() -> String {
self
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
[self]
}
}
extension Match: SpinDrawable {
func segmentLabel() -> String {
self.matchTitle(.wide)
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
let teams = teams()
if teams.count == 1 {
return teams.first!.segmentLabel(displayStyle)
} else {
return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 }
}
}
}
extension TeamRegistration: SpinDrawable {
func segmentLabel() -> String {
self.teamLabel(.short)
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
var strings: [String] = []
let indexLabel = tournamentObject()?.labelIndexOf(team: self)
if let indexLabel {
strings.append(indexLabel)
}
strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) })
return strings
}
}
@ -40,15 +51,14 @@ struct DrawOption: Identifiable, SpinDrawable {
let initialIndex: Int
let option: SpinDrawable
func segmentLabel() -> String {
option.segmentLabel()
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
option.segmentLabel(displayStyle)
}
}
struct SpinDrawView: View {
@Environment(\.dismiss) private var dismiss
let time: Date = Date()
let drawees: [any SpinDrawable]
@State var segments: [any SpinDrawable]
let completion: ([DrawResult]) -> Void // Completion closure
@ -56,29 +66,20 @@ struct SpinDrawView: View {
@State private var drawCount: Int = 0
@State private var draws: [DrawResult] = [DrawResult]()
@State private var drawOptions: [DrawOption] = [DrawOption]()
@State private var selectedIndex: Int?
var autoMode: Bool {
drawees.count > 1
}
func validationLabel(drawee: Int, result: SpinDrawable) -> String {
let draw = drawees[drawee]
return draw.segmentLabel() + " -> " + result.segmentLabel()
}
@State private var selectedIndex: Int?
var body: some View {
List {
Section {
Text(time.formatted(date: .complete, time: .complete))
Text(time, style: .timer)
}
if selectedIndex != nil {
Section {
Text(validationLabel(drawee: drawCount, result: segments[draws.last!.drawIndex]))
_validationLabelView(drawee: drawCount, result: segments[draws.last!.drawIndex])
if autoMode == false || drawCount == drawees.count {
RowButtonView("ok") {
RowButtonView("Valider le tirage") {
completion(draws)
dismiss()
}
} else {
@ -87,15 +88,15 @@ struct SpinDrawView: View {
}
} else if drawCount < drawees.count {
Section {
Text(drawees[drawCount].segmentLabel())
_segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center)
}
Section {
FortuneWheelTestView(segments: drawOptions, autoMode: autoMode) { index in
FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in
self.selectedIndex = index
self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex))
self.drawOptions.remove(at: index)
if autoMode && drawCount < drawees.count {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.drawCount += 1
@ -117,7 +118,7 @@ struct SpinDrawView: View {
Section {
Text("Tous les tirages sont terminés")
ForEach(draws) { drawResult in
Text(validationLabel(drawee: drawResult.drawee, result: segments[drawResult.drawIndex]))
_validationLabelView(drawee: drawResult.drawee, result: segments[drawResult.drawIndex])
}
}
@ -135,6 +136,18 @@ struct SpinDrawView: View {
Text("Comité du tournoi")
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
}
.navigationBarBackButtonHidden()
.navigationTitle("Tirage au sort")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar(.hidden, for: .tabBar)
.listStyle(.insetGrouped)
.scrollDisabled(true)
.onAppear {
@ -143,9 +156,29 @@ struct SpinDrawView: View {
}
}
}
private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View {
VStack(alignment: horizontalAlignment, spacing: 0.0) {
ForEach(segment, id: \.self) { string in
Text(string)
.frame(maxWidth: .infinity)
}
}
}
@ViewBuilder
private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View {
HStack(spacing: 0.0) {
let draw = drawees[drawee]
_segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .leading)
Image(systemName: "arrowshape.forward.fill")
_segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .trailing)
}
}
}
struct FortuneWheelTestView: View {
struct FortuneWheelContainerView: View {
@State private var rotation: Double = 0
let segments: [any SpinDrawable]
let autoMode: Bool
@ -162,7 +195,6 @@ struct FortuneWheelTestView: View {
.stroke(Color.black, lineWidth: 2)
.frame(width: 20, height: 20)
.rotationEffect(.degrees(180))
}
.onAppear {
if autoMode {
@ -186,7 +218,6 @@ struct FortuneWheelTestView: View {
}
func rollWheel() {
rotation = 0
// Generate a random angle for the wheel to rotate
@ -249,10 +280,16 @@ struct FortuneWheelView: View {
}
.fill(getColor(forIndex:index))
Text(segments[index].segmentLabel()).multilineTextAlignment(.trailing)
.rotationEffect(.degrees(Double(index) * (360 / Double(segments.count)) + (360 / Double(segments.count) / 2)))
.foregroundColor(.white)
.position(arcPosition(index: index, radius: radius))
VStack(alignment: .trailing, spacing: 0.0) {
let strings = segments[index].segmentLabel(.short)
ForEach(strings, id: \.self) { string in
Text(string).bold()
}
}
.padding(.trailing, 30)
.rotationEffect(.degrees(Double(index) * (360 / Double(segments.count)) + (360 / Double(segments.count) / 2)))
.foregroundColor(.white)
.position(arcPosition(index: index, radius: radius))
}
}
}

@ -22,11 +22,30 @@ struct GroupStageTeamView: View {
}
if groupStage.tournamentObject()?.hasEnded() == false {
Section {
NavigationLink {
GroupStageTeamReplacementView(team: team)
} label: {
Text("Chercher à remplacer")
if team.qualified && team.bracketPosition == nil, let tournament = team.tournamentObject() {
Section {
NavigationLink {
SpinDrawView(drawees: [team], segments: tournament.matchesWithSpace()) { results in
}
} label: {
Text("Tirage au sort visuel")
}
}
Section {
RowButtonView("Tirage au sort automatique", role: .destructive) {
}
}
}
if team.qualified == false {
Section {
NavigationLink {
GroupStageTeamReplacementView(team: team)
} label: {
Text("Chercher à remplacer")
}
}
}
@ -46,11 +65,13 @@ struct GroupStageTeamView: View {
}
}
Section {
RowButtonView("Retirer de la poule", role: .destructive) {
team.groupStagePosition = nil
team.groupStage = nil
_save()
if team.qualified == false {
Section {
RowButtonView("Retirer de la poule", role: .destructive) {
team.groupStagePosition = nil
team.groupStage = nil
_save()
}
}
}
}

@ -18,7 +18,9 @@ struct RoundView: View {
List {
let loserRounds = round.loserRounds()
let availableQualifiedTeams = tournament.availableQualifiedTeams()
let displayableMatches = round.displayableMatches()
let spaceLeft = displayableMatches.filter({ $0.hasSpaceLeft() })
if isEditingTournamentSeed.wrappedValue == false {
//(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue })
if loserRounds.isEmpty == false {
@ -33,6 +35,19 @@ struct RoundView: View {
}
}
}
} else if availableQualifiedTeams.isEmpty == false && spaceLeft.isEmpty == false {
NavigationLink("Tirer au sort la position d'un qualifié") {
SpinDrawView(drawees: availableQualifiedTeams, segments: spaceLeft) { results in
results.forEach { drawResult in
print(availableQualifiedTeams[drawResult.drawee].teamLabel())
print(spaceLeft[drawResult.drawIndex].matchTitle())
availableQualifiedTeams[drawResult.drawee].setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
}
try? dataStore.matches.addOrUpdate(contentOfs: spaceLeft)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: availableQualifiedTeams)
isEditingTournamentSeed.wrappedValue.toggle()
}
}
} else if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) {
@ -62,7 +77,7 @@ struct RoundView: View {
}
}
ForEach(round.displayableMatches()) { match in
ForEach(displayableMatches) { match in
Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
} header: {

@ -15,7 +15,7 @@ struct RoundsView: View {
init(tournament: Tournament) {
self.tournament = tournament
_selectedRound = State(wrappedValue: tournament.getActiveRound())
if tournament.availableSeeds().isEmpty == false {
if tournament.availableSeeds().isEmpty == false || tournament.availableQualifiedTeams().isEmpty == false {
_isEditingTournamentSeed = State(wrappedValue: true)
}
}

@ -126,9 +126,13 @@ struct BroadcastView: View {
Label("Partager le lien", systemImage: "link")
}
} label: {
Text("lien")
.underline()
HStack {
Spacer()
Text("lien")
.underline()
}
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderless)
}

Loading…
Cancel
Save