You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/ViewModel/SearchViewModel.swift

437 lines
15 KiB

//
// SearchViewModel.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 07/02/2024.
//
import SwiftUI
class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = ""
var debounceTrigger: Double = 0.15
}
class SearchViewModel: ObservableObject, Identifiable {
let id: UUID = UUID()
var allowSelection : Int = 0
var codeClub: String? = nil
var clubName: String? = nil
var ligueName: String? = nil
var showFemaleInMaleAssimilation: Bool = false
var hidePlayers: [String]?
@Published var debouncableText: String = ""
@Published var searchText: String = ""
@Published var task: DispatchWorkItem?
@Published var computedSearchText: String = ""
@Published var tokens = [SearchToken]()
@Published var suggestedTokens = [SearchToken]()
@Published var dataSet: DataSet = .national
@Published var filterOption = PlayerFilterOption.all
@Published var hideAssimilation: Bool = false
@Published var ascending: Bool = true
@Published var sortOption: SortOption = .rank
@Published var selectedPlayers: Set<ImportedPlayer> = Set()
@Published var filterSelectionEnabled: Bool = false
@Published var isPresented: Bool = false
var mostRecentDate: Date? = nil
var selectionIsOver: Bool {
if allowSingleSelection && selectedPlayers.count == 1 {
return true
} else if allowMultipleSelection && selectedPlayers.count == allowSelection {
return true
}
return false
}
var allowMultipleSelection: Bool {
allowSelection > 1 || allowSelection == -1
}
var allowSingleSelection: Bool {
allowSelection == 1
}
var debounceTrigger: Double {
(dataSet == .national || dataSet == .ligue) ? 0.4 : 0.1
}
var throttleTrigger: Double {
(dataSet == .national || dataSet == .ligue) ? 0.15 : 0.1
}
var contentUnavailableMessage: String {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
if tokens.isEmpty {
message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois. Dans ce pas, Padel Club ne pourra pas le trouver.")
}
return message.joined(separator: "\n")
}
func codeClubs() -> [String] {
let clubs: [Club] = DataStore.shared.user.clubsObjects()
return clubs.compactMap { $0.code }
}
func getCodeClub() -> String? {
if let codeClub { return codeClub }
if let userCodeClub = DataStore.shared.user.currentPlayerData()?.clubCode { return userCodeClub }
return nil
}
func getLigueName() -> String? {
if let ligueName { return ligueName }
if let userLigueName = DataStore.shared.user.currentPlayerData()?.ligueName { return userLigueName }
return nil
}
func showIndex() -> Bool {
if (dataSet == .national || dataSet == .ligue) { return false }
if filterOption == .all { return false }
return true
}
func prompt(forDataSet: DataSet) -> String {
switch forDataSet {
case .national:
if let mostRecentDate {
return "base fédérale \(mostRecentDate.monthYearFormatted)"
} else {
return "rechercher"
}
case .ligue:
return "dans cette ligue"
case .club:
return "dans ce club"
case .favoriteClubs, .favoritePlayers:
return "dans mes favoris"
}
}
func label(forDataSet: DataSet) -> String {
switch forDataSet {
case .national:
return "National"
case .ligue:
return (ligueName)?.capitalized ?? "Ma ligue"
case .club:
return (clubName)?.capitalized ?? "Mon club"
case .favoriteClubs:
return "Clubs favoris"
case .favoritePlayers:
return "Joueurs favoris"
}
}
func words() -> [String] {
return searchText.canonicalVersionWithPunctuation.trimmed.components(separatedBy: .whitespaces)
}
func wordsPredicates() -> NSPredicate? {
let words = words().filter({ $0.isEmpty == false })
switch words.count {
case 2:
let predicates = [
NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[0], words[1]),
NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[1], words[0]),
]
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
default:
return nil
}
}
func orPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion
let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation
switch tokens.first {
case .none:
if canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates()
if let wordsPredicates {
predicates.append(wordsPredicates)
} else {
predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation))
}
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation))
let components = canonicalVersionWithoutPunctuation.split(separator: " ").sorted()
let pattern = components.joined(separator: ".*")
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
predicates.append(predicate)
}
case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
} else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .club:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil"))
} else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .rankMoreThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation))
}
case .rankLessThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation))
}
case .rankBetween:
let values = canonicalVersionWithPunctuation.components(separatedBy: ",")
if canonicalVersionWithPunctuation.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
}
}
if predicates.isEmpty {
return nil
}
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}
func predicate() -> NSPredicate? {
var predicates : [NSPredicate?] = [
orPredicate(),
filterOption == .male ?
NSPredicate(format: "male == YES") :
nil,
filterOption == .female ?
NSPredicate(format: "male == NO") :
nil,
]
if let mostRecentDate {
//predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if hideAssimilation {
predicates.append(NSPredicate(format: "assimilation == %@", "Non"))
}
switch dataSet {
case .national:
break
case .ligue:
if let ligueName = getLigueName() {
predicates.append(NSPredicate(format: "ligueName == %@", ligueName))
} else {
predicates.append(NSPredicate(format: "ligueName == nil"))
}
case .club:
if let codeClub = getCodeClub() {
predicates.append(NSPredicate(format: "clubCode == %@", codeClub))
} else {
predicates.append(NSPredicate(format: "clubCode == nil"))
}
case .favoriteClubs:
predicates.append(NSPredicate(format: "clubCode IN %@", codeClubs()))
case .favoritePlayers:
//todo
predicates.append(NSPredicate(format: "license == nil"))
}
if hidePlayers?.isEmpty == false {
predicates.append(NSPredicate(format: "NOT (license IN %@)", hidePlayers!))
}
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 }))
}
func sortDescriptors() -> [SortDescriptor<ImportedPlayer>] {
sortOption.sortDescriptors(ascending, dataSet: dataSet)
}
func nsSortDescriptors() -> [NSSortDescriptor] {
sortDescriptors().map { NSSortDescriptor($0) }
}
}
enum SearchToken: String, CaseIterable, Identifiable {
case club = "club"
case ligue = "ligue"
case rankMoreThan = "rang >"
case rankLessThan = "rang <"
case rankBetween = "rang <>"
var id: String {
rawValue
}
var message: String {
switch self {
case .club:
return "Taper le nom d'un club pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
case .ligue:
return "Taper le nom d'une ligue pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
case .rankMoreThan:
return "Taper un nombre pour chercher les joueurs ayant un classement supérieur ou égale."
case .rankLessThan:
return "Taper un nombre pour chercher les joueurs ayant un classement inférieur ou égale."
case .rankBetween:
return "Taper deux nombres séparés par une virgule pour chercher les joueurs dans cette intervalle de classement"
}
}
var titleLabel: String {
switch self {
case .club:
return "Chercher un club"
case .ligue:
return "Chercher une ligue"
case .rankMoreThan, .rankLessThan:
return "Chercher un rang"
case .rankBetween:
return "Chercher une intervalle de classement"
}
}
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .club:
return "Club"
case .ligue:
return "Ligue"
case .rankMoreThan:
return "Rang supérieur ou égale à"
case .rankLessThan:
return "Rang inférieur ou égale à"
case .rankBetween:
return "Rang entre deux valeurs"
}
}
var shortLocalizedLabel: String {
switch self {
case .club:
return "Club"
case .ligue:
return "Ligue"
case .rankMoreThan:
return "Rang ≥"
case .rankLessThan:
return "Rang ≤"
case .rankBetween:
return "Rang ≥,≤"
}
}
func icon() -> String {
switch self {
case .club:
return "house.and.flag.fill"
case .ligue:
return "house.and.flag.fill"
case .rankMoreThan:
return "figure.racquetball"
case .rankLessThan:
return "figure.racquetball"
case .rankBetween:
return "figure.racquetball"
}
}
var systemImage: String {
switch self {
case .club:
return "house.and.flag.fill"
case .ligue:
return "house.and.flag.fill"
case .rankMoreThan:
return "figure.racquetball"
case .rankLessThan:
return "figure.racquetball"
case .rankBetween:
return "figure.racquetball"
}
}
}
enum DataSet: Int, Identifiable {
case national
case ligue
case club
case favoriteClubs
case favoritePlayers
static let allCases : [DataSet] = [.national, .ligue, .club, .favoriteClubs]
var id: Int { rawValue }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .national:
return "National"
case .ligue:
return "Ligue"
case .club:
return "Club"
case .favoritePlayers, .favoriteClubs:
return "Favori"
}
}
var tokens: [SearchToken] {
switch self {
case .national:
return [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween]
case .ligue:
return [.club, .rankMoreThan, .rankLessThan, .rankBetween]
case .club:
return [.rankMoreThan, .rankLessThan, .rankBetween]
case .favoritePlayers, .favoriteClubs:
return [.rankMoreThan, .rankLessThan, .rankBetween]
}
}
}
enum SortOption: Int, CaseIterable, Identifiable {
case name
case rank
case tournamentCount
case points
var id: Int { self.rawValue }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .name:
return "Nom"
case .rank:
return "Rang"
case .tournamentCount:
return "Tournoi"
case .points:
return "Points"
}
}
func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor<ImportedPlayer>] {
switch self {
case .name:
return [SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation)]
case .rank:
if (dataSet == .national || dataSet == .ligue) {
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)]
} else {
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
}
case .tournamentCount:
return [SortDescriptor(\ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
case .points:
return [SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
}
}
}