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.
403 lines
15 KiB
403 lines
15 KiB
//
|
|
// DataStore.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Laurent Morvillier on 02/02/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
import SwiftUI
|
|
|
|
class DataStore: ObservableObject {
|
|
|
|
static let shared = DataStore()
|
|
|
|
@Published var user: CustomUser = CustomUser.placeHolder() {
|
|
didSet {
|
|
let loggedUser = StoreCenter.main.isAuthenticated
|
|
|
|
if loggedUser {
|
|
if self.user.id != self.userStorage.item()?.id {
|
|
self.userStorage.setItemNoSync(self.user)
|
|
StoreCenter.main.initialSynchronization(clear: false)
|
|
self._fixMissingClubCreatorIfNecessary(self.clubs)
|
|
self._fixMissingEventCreatorIfNecessary(self.events)
|
|
}
|
|
} else {
|
|
self._temporaryLocalUser.item = self.user
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate(set) var tournaments: SyncedCollection<Tournament>
|
|
fileprivate(set) var clubs: SyncedCollection<Club>
|
|
fileprivate(set) var courts: SyncedCollection<Court>
|
|
fileprivate(set) var events: SyncedCollection<Event>
|
|
fileprivate(set) var monthData: StoredCollection<MonthData>
|
|
fileprivate(set) var dateIntervals: SyncedCollection<DateInterval>
|
|
fileprivate(set) var purchases: SyncedCollection<Purchase>
|
|
|
|
var userStorage: StoredSingleton<CustomUser>
|
|
|
|
fileprivate var _temporaryLocalUser: OptionalStorage<CustomUser> = OptionalStorage(fileName: "tmp_local_user.json")
|
|
fileprivate(set) var appSettingsStorage: MicroStorage<AppSettings> = MicroStorage(fileName: "appsettings.json")
|
|
|
|
var appSettings: AppSettings {
|
|
appSettingsStorage.item
|
|
}
|
|
|
|
init() {
|
|
let store = Store.main
|
|
StoreCenter.main.blackListUserName("apple-test")
|
|
|
|
// let secureScheme = true
|
|
let domain: String = URLs.activationHost.rawValue
|
|
|
|
#if DEBUG
|
|
if let secure = PListReader.readBool(plist: "local", key: "secure_server"),
|
|
let domain = PListReader.readString(plist: "local", key: "server_domain") {
|
|
StoreCenter.main.configureURLs(secureScheme: secure, domain: domain)
|
|
} else {
|
|
StoreCenter.main.configureURLs(secureScheme: true, domain: domain)
|
|
}
|
|
#else
|
|
StoreCenter.main.configureURLs(secureScheme: true, domain: domain)
|
|
#endif
|
|
|
|
StoreCenter.main.logsFailedAPICalls()
|
|
|
|
var synchronized: Bool = true
|
|
|
|
#if DEBUG
|
|
if let sync = PListReader.readBool(plist: "local", key: "synchronized") {
|
|
synchronized = sync
|
|
}
|
|
#endif
|
|
|
|
StoreCenter.main.forceNoSynchronization = !synchronized
|
|
|
|
|
|
let indexed: Bool = true
|
|
self.clubs = store.registerSynchronizedCollection(indexed: indexed)
|
|
self.courts = store.registerSynchronizedCollection(indexed: indexed)
|
|
self.tournaments = store.registerSynchronizedCollection(indexed: indexed)
|
|
self.events = store.registerSynchronizedCollection(indexed: indexed)
|
|
self.dateIntervals = store.registerSynchronizedCollection(indexed: indexed)
|
|
self.userStorage = store.registerObject(synchronized: synchronized)
|
|
self.purchases = Store.main.registerSynchronizedCollection(inMemory: true)
|
|
|
|
self.monthData = store.registerCollection(indexed: indexed)
|
|
|
|
// Load ApiCallCollection, making them restart at launch and deletable on disconnect
|
|
StoreCenter.main.loadApiCallCollection(type: GroupStage.self)
|
|
StoreCenter.main.loadApiCallCollection(type: Round.self)
|
|
StoreCenter.main.loadApiCallCollection(type: PlayerRegistration.self)
|
|
StoreCenter.main.loadApiCallCollection(type: TeamRegistration.self)
|
|
StoreCenter.main.loadApiCallCollection(type: Match.self)
|
|
StoreCenter.main.loadApiCallCollection(type: TeamScore.self)
|
|
StoreCenter.main.loadApiCallCollection(type: DrawLog.self)
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidUpdate), name: NSNotification.Name.CollectionDidChange, object: nil)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(_willEnterForegroundNotification),
|
|
name: UIScene.willEnterForegroundNotification,
|
|
object: nil)
|
|
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
func saveUser() {
|
|
if user.username.count > 0 {
|
|
self.userStorage.update()
|
|
} else {
|
|
self._temporaryLocalUser.item = self.user
|
|
}
|
|
}
|
|
|
|
@objc func collectionDidLoad(notification: Notification) {
|
|
|
|
if let userSingleton: StoredSingleton<CustomUser> = notification.object as? StoredSingleton<CustomUser> {
|
|
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
|
|
} else if let clubsCollection: SyncedCollection<Club> = notification.object as? SyncedCollection<Club> {
|
|
self._fixMissingClubCreatorIfNecessary(clubsCollection)
|
|
} else if let eventsCollection: SyncedCollection<Event> = notification.object as? SyncedCollection<Event> {
|
|
self._fixMissingEventCreatorIfNecessary(eventsCollection)
|
|
}
|
|
|
|
if Store.main.fileCollectionsAllLoaded() {
|
|
AutomaticPatcher.applyAllWhenApplicable()
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate func _fixMissingClubCreatorIfNecessary(_ clubsCollection: SyncedCollection<Club>) {
|
|
for club in clubsCollection {
|
|
if let userId = StoreCenter.main.userId, club.creator == nil {
|
|
club.creator = userId
|
|
self.userStorage.item()?.addClub(club)
|
|
self.userStorage.update()
|
|
clubsCollection.writeChangeAndInsertOnServer(instance: club)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func _fixMissingEventCreatorIfNecessary(_ eventsCollection: SyncedCollection<Event>) {
|
|
for event in eventsCollection {
|
|
if let userId = StoreCenter.main.userId, event.creator == nil {
|
|
event.creator = userId
|
|
do {
|
|
try event.insertOnServer()
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func collectionDidUpdate(notification: Notification) {
|
|
self.objectWillChange.send()
|
|
}
|
|
|
|
@objc func _willEnterForegroundNotification() {
|
|
Task {
|
|
try await self.purchases.loadDataFromServerIfAllowed(clear: true)
|
|
}
|
|
}
|
|
|
|
func disconnect() {
|
|
|
|
Task {
|
|
if await StoreCenter.main.hasPendingAPICalls() {
|
|
// todo qu'est ce qu'on fait des API Call ?
|
|
}
|
|
|
|
do {
|
|
let services = try StoreCenter.main.service()
|
|
try await services.logout()
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self._localDisconnect()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func deleteAccount() {
|
|
|
|
Task {
|
|
do {
|
|
let services = try StoreCenter.main.service()
|
|
try await services.deleteAccount()
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self._localDisconnect()
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func deleteTournament(_ tournament: Tournament) {
|
|
let event = tournament.eventObject()
|
|
let isLastTournament = event?.tournaments.count == 1
|
|
self.tournaments.delete(instance: tournament)
|
|
if let event, isLastTournament {
|
|
self.events.delete(instance: event)
|
|
}
|
|
StoreCenter.main.destroyStore(identifier: tournament.id)
|
|
}
|
|
|
|
fileprivate func _localDisconnect() {
|
|
|
|
let tournamendIds: [String] = self.tournaments.map { $0.id }
|
|
|
|
TournamentLibrary.shared.reset()
|
|
|
|
self.tournaments.reset()
|
|
self.clubs.reset()
|
|
self.courts.reset()
|
|
self.events.reset()
|
|
self.dateIntervals.reset()
|
|
self.userStorage.reset()
|
|
self.purchases.reset()
|
|
|
|
Guard.main.disconnect()
|
|
|
|
StoreCenter.main.disconnect()
|
|
|
|
for tournament in tournamendIds {
|
|
StoreCenter.main.destroyStore(identifier: tournament.id)
|
|
}
|
|
|
|
self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder()
|
|
self.user.clubs.removeAll()
|
|
|
|
}
|
|
|
|
func tryUserUpdate(userCopy: CustomUser) async throws {
|
|
try await self.userStorage.tryPutBeforeUpdating(userCopy)
|
|
}
|
|
|
|
// func copyToLocalServer(tournament: Tournament) {
|
|
//
|
|
// Task {
|
|
// do {
|
|
//
|
|
// if let url = PListReader.readString(plist: "local", key: "local_server"),
|
|
// let login = PListReader.readString(plist: "local", key: "username"),
|
|
// let pass = PListReader.readString(plist: "local", key: "password") {
|
|
// let service = Services(url: url)
|
|
// let _: CustomUser = try await service.login(username: login, password: pass)
|
|
//
|
|
// tournament.event = nil
|
|
// _ = try await service.post(tournament)
|
|
//
|
|
// for groupStage in tournament.groupStages() {
|
|
// _ = try await service.post(groupStage)
|
|
// }
|
|
// for round in tournament.rounds() {
|
|
// try await self._insertRoundAndChildren(round: round, service: service)
|
|
// }
|
|
// for teamRegistration in tournament.unsortedTeams() {
|
|
// _ = try await service.post(teamRegistration)
|
|
// for playerRegistration in teamRegistration.unsortedPlayers() {
|
|
// _ = try await service.post(playerRegistration)
|
|
// }
|
|
// }
|
|
// for groupStage in tournament.groupStages() {
|
|
// for match in groupStage._matches() {
|
|
// try await self._insertMatch(match: match, service: service)
|
|
// }
|
|
// }
|
|
// for round in tournament.allRounds() {
|
|
// for match in round._matches() {
|
|
// try await self._insertMatch(match: match, service: service)
|
|
// }
|
|
// }
|
|
//
|
|
// }
|
|
// } catch {
|
|
// Logger.error(error)
|
|
// }
|
|
// }
|
|
//
|
|
// }
|
|
//
|
|
// fileprivate func _insertRoundAndChildren(round: Round, service: Services) async throws {
|
|
// _ = try await service.post(round)
|
|
// for loserRound in round.loserRounds() {
|
|
// try await self._insertRoundAndChildren(round: loserRound, service: service)
|
|
// }
|
|
// }
|
|
//
|
|
// fileprivate func _insertMatch(match: Match, service: Services) async throws {
|
|
// _ = try await service.post(match)
|
|
// for teamScore in match.teamScores {
|
|
// _ = try await service.post(teamScore)
|
|
// }
|
|
//
|
|
// }
|
|
|
|
// MARK: - Convenience
|
|
|
|
private var _lastRunningCheckDate: Date? = nil
|
|
private var _cachedRunningMatches: [Match]? = nil
|
|
|
|
func runningMatches() -> [Match] {
|
|
let dateNow : Date = Date()
|
|
if let lastCheck = _lastRunningCheckDate,
|
|
let cachedMatches = _cachedRunningMatches,
|
|
dateNow.timeIntervalSince(lastCheck) < 5 {
|
|
return cachedMatches
|
|
}
|
|
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
|
|
|
|
var runningMatches: [Match] = []
|
|
for tournament in lastTournaments {
|
|
if let store = tournament.tournamentStore {
|
|
let matches = store.matches.filter { match in
|
|
match.disabled == false && match.isRunning()
|
|
}
|
|
runningMatches.append(contentsOf: matches)
|
|
}
|
|
}
|
|
_lastRunningCheckDate = dateNow
|
|
_cachedRunningMatches = runningMatches
|
|
return _cachedRunningMatches!
|
|
}
|
|
|
|
private var _lastRunningAndNextCheckDate: Date? = nil
|
|
private var _cachedRunningAndNextMatches: [Match]? = nil
|
|
|
|
func runningAndNextMatches() -> [Match] {
|
|
let dateNow : Date = Date()
|
|
if let lastCheck = _lastRunningAndNextCheckDate,
|
|
let cachedMatches = _cachedRunningAndNextMatches,
|
|
dateNow.timeIntervalSince(lastCheck) < 5 {
|
|
return cachedMatches
|
|
}
|
|
|
|
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
|
|
|
|
var runningMatches: [Match] = []
|
|
for tournament in lastTournaments {
|
|
if let store = tournament.tournamentStore {
|
|
let matches = store.matches.filter { match in
|
|
match.disabled == false && match.startDate != nil && match.endDate == nil }
|
|
runningMatches.append(contentsOf: matches)
|
|
}
|
|
}
|
|
_lastRunningAndNextCheckDate = dateNow
|
|
_cachedRunningAndNextMatches = runningMatches
|
|
return _cachedRunningAndNextMatches!
|
|
}
|
|
|
|
private var _lastEndCheckDate: Date? = nil
|
|
private var _cachedEndMatches: [Match]? = nil
|
|
|
|
func endMatches() -> [Match] {
|
|
let dateNow : Date = Date()
|
|
|
|
if let lastCheck = _lastEndCheckDate,
|
|
let cachedMatches = _cachedEndMatches,
|
|
dateNow.timeIntervalSince(lastCheck) < 5 {
|
|
return cachedMatches
|
|
}
|
|
|
|
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
|
|
|
|
var runningMatches: [Match] = []
|
|
for tournament in lastTournaments {
|
|
if let store = tournament.tournamentStore {
|
|
let matches = store.matches.filter { match in
|
|
match.disabled == false && match.hasEnded() }
|
|
runningMatches.append(contentsOf: matches)
|
|
}
|
|
}
|
|
|
|
_lastEndCheckDate = dateNow
|
|
_cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending)
|
|
return _cachedEndMatches!
|
|
}
|
|
|
|
func subscriptionUnitlyPayedTournaments(after date: Date) -> Int {
|
|
return DataStore.shared.tournaments.count(where: { tournament in
|
|
tournament.creationDate > date &&
|
|
tournament.payment == .subscriptionUnit &&
|
|
tournament.isCanceled == false &&
|
|
tournament.isDeleted == false })
|
|
}
|
|
|
|
}
|
|
|