1.5.0 Initial run as iOS in a monorepo
This commit is contained in:
834
ios/OpenClimb/ViewModels/ClimbingDataManager.swift
Normal file
834
ios/OpenClimb/ViewModels/ClimbingDataManager.swift
Normal file
@@ -0,0 +1,834 @@
|
||||
//
|
||||
// ClimbingDataManager.swift
|
||||
// OpenClimb
|
||||
//
|
||||
// Created by OpenClimb on 2025-01-17.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@MainActor
|
||||
class ClimbingDataManager: ObservableObject {
|
||||
|
||||
@Published var gyms: [Gym] = []
|
||||
@Published var problems: [Problem] = []
|
||||
@Published var sessions: [ClimbSession] = []
|
||||
@Published var attempts: [Attempt] = []
|
||||
@Published var activeSession: ClimbSession?
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var successMessage: String?
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
private enum Keys {
|
||||
static let gyms = "openclimb_gyms"
|
||||
static let problems = "openclimb_problems"
|
||||
static let sessions = "openclimb_sessions"
|
||||
static let attempts = "openclimb_attempts"
|
||||
static let activeSession = "openclimb_active_session"
|
||||
}
|
||||
|
||||
init() {
|
||||
loadAllData()
|
||||
}
|
||||
|
||||
private func loadAllData() {
|
||||
loadGyms()
|
||||
loadProblems()
|
||||
loadSessions()
|
||||
loadAttempts()
|
||||
loadActiveSession()
|
||||
}
|
||||
|
||||
private func loadGyms() {
|
||||
if let data = userDefaults.data(forKey: Keys.gyms),
|
||||
let loadedGyms = try? decoder.decode([Gym].self, from: data)
|
||||
{
|
||||
self.gyms = loadedGyms
|
||||
}
|
||||
}
|
||||
|
||||
private func loadProblems() {
|
||||
if let data = userDefaults.data(forKey: Keys.problems),
|
||||
let loadedProblems = try? decoder.decode([Problem].self, from: data)
|
||||
{
|
||||
self.problems = loadedProblems
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSessions() {
|
||||
if let data = userDefaults.data(forKey: Keys.sessions),
|
||||
let loadedSessions = try? decoder.decode([ClimbSession].self, from: data)
|
||||
{
|
||||
self.sessions = loadedSessions
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAttempts() {
|
||||
if let data = userDefaults.data(forKey: Keys.attempts),
|
||||
let loadedAttempts = try? decoder.decode([Attempt].self, from: data)
|
||||
{
|
||||
self.attempts = loadedAttempts
|
||||
}
|
||||
}
|
||||
|
||||
private func loadActiveSession() {
|
||||
if let data = userDefaults.data(forKey: Keys.activeSession),
|
||||
let loadedActiveSession = try? decoder.decode(ClimbSession.self, from: data)
|
||||
{
|
||||
self.activeSession = loadedActiveSession
|
||||
}
|
||||
}
|
||||
|
||||
private func saveGyms() {
|
||||
if let data = try? encoder.encode(gyms) {
|
||||
userDefaults.set(data, forKey: Keys.gyms)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveProblems() {
|
||||
if let data = try? encoder.encode(problems) {
|
||||
userDefaults.set(data, forKey: Keys.problems)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveSessions() {
|
||||
if let data = try? encoder.encode(sessions) {
|
||||
userDefaults.set(data, forKey: Keys.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAttempts() {
|
||||
if let data = try? encoder.encode(attempts) {
|
||||
userDefaults.set(data, forKey: Keys.attempts)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveActiveSession() {
|
||||
if let activeSession = activeSession,
|
||||
let data = try? encoder.encode(activeSession)
|
||||
{
|
||||
userDefaults.set(data, forKey: Keys.activeSession)
|
||||
} else {
|
||||
userDefaults.removeObject(forKey: Keys.activeSession)
|
||||
}
|
||||
}
|
||||
|
||||
func addGym(_ gym: Gym) {
|
||||
gyms.append(gym)
|
||||
saveGyms()
|
||||
successMessage = "Gym added successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func updateGym(_ gym: Gym) {
|
||||
if let index = gyms.firstIndex(where: { $0.id == gym.id }) {
|
||||
gyms[index] = gym
|
||||
saveGyms()
|
||||
successMessage = "Gym updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGym(_ gym: Gym) {
|
||||
// Delete associated problems and their attempts first
|
||||
let problemsToDelete = problems.filter { $0.gymId == gym.id }
|
||||
for problem in problemsToDelete {
|
||||
deleteProblem(problem)
|
||||
}
|
||||
|
||||
// Delete associated sessions and their attempts
|
||||
let sessionsToDelete = sessions.filter { $0.gymId == gym.id }
|
||||
for session in sessionsToDelete {
|
||||
deleteSession(session)
|
||||
}
|
||||
|
||||
// Delete the gym
|
||||
gyms.removeAll { $0.id == gym.id }
|
||||
saveGyms()
|
||||
successMessage = "Gym deleted successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func gym(withId id: UUID) -> Gym? {
|
||||
return gyms.first { $0.id == id }
|
||||
}
|
||||
|
||||
func addProblem(_ problem: Problem) {
|
||||
problems.append(problem)
|
||||
saveProblems()
|
||||
successMessage = "Problem added successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func updateProblem(_ problem: Problem) {
|
||||
if let index = problems.firstIndex(where: { $0.id == problem.id }) {
|
||||
problems[index] = problem
|
||||
saveProblems()
|
||||
successMessage = "Problem updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteProblem(_ problem: Problem) {
|
||||
// Delete associated attempts first
|
||||
attempts.removeAll { $0.problemId == problem.id }
|
||||
saveAttempts()
|
||||
|
||||
// Delete the problem
|
||||
problems.removeAll { $0.id == problem.id }
|
||||
saveProblems()
|
||||
successMessage = "Problem deleted successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func problem(withId id: UUID) -> Problem? {
|
||||
return problems.first { $0.id == id }
|
||||
}
|
||||
|
||||
func problems(forGym gymId: UUID) -> [Problem] {
|
||||
return problems.filter { $0.gymId == gymId }
|
||||
}
|
||||
|
||||
func activeProblems(forGym gymId: UUID) -> [Problem] {
|
||||
return problems.filter { $0.gymId == gymId && $0.isActive }
|
||||
}
|
||||
|
||||
func startSession(gymId: UUID, notes: String? = nil) {
|
||||
|
||||
if let currentActive = activeSession {
|
||||
endSession(currentActive.id)
|
||||
}
|
||||
|
||||
let newSession = ClimbSession(gymId: gymId, notes: notes)
|
||||
activeSession = newSession
|
||||
sessions.append(newSession)
|
||||
|
||||
saveActiveSession()
|
||||
saveSessions()
|
||||
|
||||
successMessage = "Session started successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func endSession(_ sessionId: UUID) {
|
||||
if let session = sessions.first(where: { $0.id == sessionId && $0.status == .active }),
|
||||
let index = sessions.firstIndex(where: { $0.id == sessionId })
|
||||
{
|
||||
|
||||
let completedSession = session.completed()
|
||||
sessions[index] = completedSession
|
||||
|
||||
if activeSession?.id == sessionId {
|
||||
activeSession = nil
|
||||
}
|
||||
|
||||
saveActiveSession()
|
||||
saveSessions()
|
||||
successMessage = "Session completed successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSession(_ session: ClimbSession) {
|
||||
if let index = sessions.firstIndex(where: { $0.id == session.id }) {
|
||||
sessions[index] = session
|
||||
|
||||
if activeSession?.id == session.id {
|
||||
activeSession = session
|
||||
saveActiveSession()
|
||||
}
|
||||
|
||||
saveSessions()
|
||||
successMessage = "Session updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteSession(_ session: ClimbSession) {
|
||||
// Delete associated attempts first
|
||||
attempts.removeAll { $0.sessionId == session.id }
|
||||
saveAttempts()
|
||||
|
||||
// Remove from active session if it's the current one
|
||||
if activeSession?.id == session.id {
|
||||
activeSession = nil
|
||||
saveActiveSession()
|
||||
}
|
||||
|
||||
// Delete the session
|
||||
sessions.removeAll { $0.id == session.id }
|
||||
saveSessions()
|
||||
successMessage = "Session deleted successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func session(withId id: UUID) -> ClimbSession? {
|
||||
return sessions.first { $0.id == id }
|
||||
}
|
||||
|
||||
func sessions(forGym gymId: UUID) -> [ClimbSession] {
|
||||
return sessions.filter { $0.gymId == gymId }
|
||||
}
|
||||
|
||||
func getLastUsedGym() -> Gym? {
|
||||
let recentSessions = sessions.sorted { $0.date > $1.date }
|
||||
guard let lastSession = recentSessions.first else { return nil }
|
||||
return gym(withId: lastSession.gymId)
|
||||
}
|
||||
|
||||
func addAttempt(_ attempt: Attempt) {
|
||||
attempts.append(attempt)
|
||||
saveAttempts()
|
||||
|
||||
successMessage = "Attempt logged successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func updateAttempt(_ attempt: Attempt) {
|
||||
if let index = attempts.firstIndex(where: { $0.id == attempt.id }) {
|
||||
attempts[index] = attempt
|
||||
saveAttempts()
|
||||
successMessage = "Attempt updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAttempt(_ attempt: Attempt) {
|
||||
attempts.removeAll { $0.id == attempt.id }
|
||||
saveAttempts()
|
||||
successMessage = "Attempt deleted successfully"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func attempts(forSession sessionId: UUID) -> [Attempt] {
|
||||
return attempts.filter { $0.sessionId == sessionId }.sorted { $0.timestamp < $1.timestamp }
|
||||
}
|
||||
|
||||
func attempts(forProblem problemId: UUID) -> [Attempt] {
|
||||
return attempts.filter { $0.problemId == problemId }.sorted { $0.timestamp > $1.timestamp }
|
||||
}
|
||||
|
||||
func successfulAttempts(forProblem problemId: UUID) -> [Attempt] {
|
||||
return attempts.filter { $0.problemId == problemId && $0.result.isSuccessful }
|
||||
}
|
||||
|
||||
func completedSessions() -> [ClimbSession] {
|
||||
return sessions.filter { $0.status == .completed }
|
||||
}
|
||||
|
||||
func totalAttempts() -> Int {
|
||||
return attempts.count
|
||||
}
|
||||
|
||||
func successfulAttempts() -> Int {
|
||||
return attempts.filter { $0.result.isSuccessful }.count
|
||||
}
|
||||
|
||||
func completedProblems() -> Int {
|
||||
let completedProblemIds = Set(
|
||||
attempts.filter { $0.result.isSuccessful }.map { $0.problemId })
|
||||
return completedProblemIds.count
|
||||
}
|
||||
|
||||
func favoriteGym() -> Gym? {
|
||||
let gymSessionCounts = Dictionary(grouping: sessions, by: { $0.gymId })
|
||||
.mapValues { $0.count }
|
||||
|
||||
guard let mostUsedGymId = gymSessionCounts.max(by: { $0.value < $1.value })?.key else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gym(withId: mostUsedGymId)
|
||||
}
|
||||
|
||||
func resetAllData() {
|
||||
gyms.removeAll()
|
||||
problems.removeAll()
|
||||
sessions.removeAll()
|
||||
attempts.removeAll()
|
||||
activeSession = nil
|
||||
|
||||
userDefaults.removeObject(forKey: Keys.gyms)
|
||||
userDefaults.removeObject(forKey: Keys.problems)
|
||||
userDefaults.removeObject(forKey: Keys.sessions)
|
||||
userDefaults.removeObject(forKey: Keys.attempts)
|
||||
userDefaults.removeObject(forKey: Keys.activeSession)
|
||||
|
||||
successMessage = "All data has been reset"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func exportData() -> Data? {
|
||||
do {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
|
||||
let exportData = ClimbDataExport(
|
||||
exportedAt: dateFormatter.string(from: Date()),
|
||||
gyms: gyms.map { AndroidGym(from: $0) },
|
||||
problems: problems.map { AndroidProblem(from: $0) },
|
||||
sessions: sessions.map { AndroidClimbSession(from: $0) },
|
||||
attempts: attempts.map { AndroidAttempt(from: $0) }
|
||||
)
|
||||
|
||||
// Collect referenced image paths
|
||||
let referencedImagePaths = collectReferencedImagePaths()
|
||||
|
||||
return try ZipUtils.createExportZip(
|
||||
exportData: exportData,
|
||||
referencedImagePaths: referencedImagePaths
|
||||
)
|
||||
} catch {
|
||||
setError("Export failed: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func importData(from data: Data) throws {
|
||||
do {
|
||||
let importResult = try ZipUtils.extractImportZip(data: data)
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .custom { decoder in
|
||||
let container = try decoder.singleValueContainer()
|
||||
let dateString = try container.decode(String.self)
|
||||
|
||||
if let date = ISO8601DateFormatter().date(from: dateString) {
|
||||
return date
|
||||
}
|
||||
|
||||
if let date = dateFormatter.date(from: dateString) {
|
||||
return date
|
||||
}
|
||||
|
||||
return Date()
|
||||
}
|
||||
|
||||
print("Raw JSON content preview:")
|
||||
print(String(decoding: importResult.jsonData.prefix(500), as: UTF8.self) + "...")
|
||||
|
||||
let importData = try decoder.decode(ClimbDataExport.self, from: importResult.jsonData)
|
||||
|
||||
print("Successfully decoded import data:")
|
||||
print("- Gyms: \(importData.gyms.count)")
|
||||
print("- Problems: \(importData.problems.count)")
|
||||
print("- Sessions: \(importData.sessions.count)")
|
||||
print("- Attempts: \(importData.attempts.count)")
|
||||
|
||||
try validateImportData(importData)
|
||||
|
||||
resetAllData()
|
||||
|
||||
let updatedProblems = updateProblemImagePaths(
|
||||
problems: importData.problems,
|
||||
imagePathMapping: importResult.imagePathMapping
|
||||
)
|
||||
|
||||
self.gyms = importData.gyms.map { $0.toGym() }
|
||||
self.problems = updatedProblems.map { $0.toProblem() }
|
||||
self.sessions = importData.sessions.map { $0.toClimbSession() }
|
||||
self.attempts = importData.attempts.map { $0.toAttempt() }
|
||||
|
||||
saveGyms()
|
||||
saveProblems()
|
||||
saveSessions()
|
||||
saveAttempts()
|
||||
|
||||
successMessage =
|
||||
"Data imported successfully with \(importResult.imagePathMapping.count) images"
|
||||
clearMessageAfterDelay()
|
||||
} catch {
|
||||
setError("Import failed: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func clearMessages() {
|
||||
errorMessage = nil
|
||||
successMessage = nil
|
||||
}
|
||||
|
||||
private func clearMessageAfterDelay() {
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
|
||||
successMessage = nil
|
||||
errorMessage = nil
|
||||
}
|
||||
}
|
||||
|
||||
func setError(_ message: String) {
|
||||
errorMessage = message
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
struct ClimbDataExport: Codable {
|
||||
let exportedAt: String
|
||||
let gyms: [AndroidGym]
|
||||
let problems: [AndroidProblem]
|
||||
let sessions: [AndroidClimbSession]
|
||||
let attempts: [AndroidAttempt]
|
||||
|
||||
init(
|
||||
exportedAt: String, gyms: [AndroidGym], problems: [AndroidProblem],
|
||||
sessions: [AndroidClimbSession], attempts: [AndroidAttempt]
|
||||
) {
|
||||
self.exportedAt = exportedAt
|
||||
self.gyms = gyms
|
||||
self.problems = problems
|
||||
self.sessions = sessions
|
||||
self.attempts = attempts
|
||||
}
|
||||
}
|
||||
|
||||
struct AndroidGym: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let location: String?
|
||||
let supportedClimbTypes: [ClimbType]
|
||||
let difficultySystems: [DifficultySystem]
|
||||
let notes: String?
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
init(from gym: Gym) {
|
||||
self.id = gym.id.uuidString
|
||||
self.name = gym.name
|
||||
self.location = gym.location
|
||||
self.supportedClimbTypes = gym.supportedClimbTypes
|
||||
self.difficultySystems = gym.difficultySystems
|
||||
self.notes = gym.notes
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
self.createdAt = formatter.string(from: gym.createdAt)
|
||||
self.updatedAt = formatter.string(from: gym.updatedAt)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String, name: String, location: String?, supportedClimbTypes: [ClimbType],
|
||||
difficultySystems: [DifficultySystem], notes: String?, createdAt: String, updatedAt: String
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.location = location
|
||||
self.supportedClimbTypes = supportedClimbTypes
|
||||
self.difficultySystems = difficultySystems
|
||||
self.notes = notes
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
func toGym() -> Gym {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
|
||||
let gymId = UUID(uuidString: id) ?? UUID()
|
||||
let createdDate = formatter.date(from: createdAt) ?? Date()
|
||||
let updatedDate = formatter.date(from: updatedAt) ?? Date()
|
||||
|
||||
return Gym.fromImport(
|
||||
id: gymId,
|
||||
name: name,
|
||||
location: location,
|
||||
supportedClimbTypes: supportedClimbTypes,
|
||||
difficultySystems: difficultySystems,
|
||||
customDifficultyGrades: [],
|
||||
notes: notes,
|
||||
createdAt: createdDate,
|
||||
updatedAt: updatedDate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AndroidProblem: Codable {
|
||||
let id: String
|
||||
let gymId: String
|
||||
let name: String?
|
||||
let description: String?
|
||||
let climbType: ClimbType
|
||||
let difficulty: DifficultyGrade
|
||||
let imagePaths: [String]?
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
init(from problem: Problem) {
|
||||
self.id = problem.id.uuidString
|
||||
self.gymId = problem.gymId.uuidString
|
||||
self.name = problem.name
|
||||
self.description = problem.description
|
||||
self.climbType = problem.climbType
|
||||
self.difficulty = problem.difficulty
|
||||
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
self.createdAt = formatter.string(from: problem.createdAt)
|
||||
self.updatedAt = formatter.string(from: problem.updatedAt)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String, gymId: String, name: String?, description: String?, climbType: ClimbType,
|
||||
difficulty: DifficultyGrade, imagePaths: [String]?, createdAt: String, updatedAt: String
|
||||
) {
|
||||
self.id = id
|
||||
self.gymId = gymId
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.climbType = climbType
|
||||
self.difficulty = difficulty
|
||||
self.imagePaths = imagePaths
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
func toProblem() -> Problem {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
|
||||
let problemId = UUID(uuidString: id) ?? UUID()
|
||||
let preservedGymId = UUID(uuidString: gymId) ?? UUID()
|
||||
let createdDate = formatter.date(from: createdAt) ?? Date()
|
||||
let updatedDate = formatter.date(from: updatedAt) ?? Date()
|
||||
|
||||
return Problem.fromImport(
|
||||
id: problemId,
|
||||
gymId: preservedGymId,
|
||||
name: name,
|
||||
description: description,
|
||||
climbType: climbType,
|
||||
difficulty: difficulty,
|
||||
setter: nil,
|
||||
tags: [],
|
||||
location: nil,
|
||||
imagePaths: imagePaths ?? [],
|
||||
isActive: true,
|
||||
dateSet: nil,
|
||||
notes: nil,
|
||||
createdAt: createdDate,
|
||||
updatedAt: updatedDate
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedImagePaths(_ newImagePaths: [String]) -> AndroidProblem {
|
||||
return AndroidProblem(
|
||||
id: self.id,
|
||||
gymId: self.gymId,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
climbType: self.climbType,
|
||||
difficulty: self.difficulty,
|
||||
imagePaths: newImagePaths.isEmpty ? nil : newImagePaths,
|
||||
createdAt: self.createdAt,
|
||||
updatedAt: self.updatedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AndroidClimbSession: Codable {
|
||||
let id: String
|
||||
let gymId: String
|
||||
let date: String
|
||||
let startTime: String?
|
||||
let endTime: String?
|
||||
let duration: Int?
|
||||
let status: SessionStatus
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
init(from session: ClimbSession) {
|
||||
self.id = session.id.uuidString
|
||||
self.gymId = session.gymId.uuidString
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
self.date = formatter.string(from: session.date)
|
||||
self.startTime = session.startTime != nil ? formatter.string(from: session.startTime!) : nil
|
||||
self.endTime = session.endTime != nil ? formatter.string(from: session.endTime!) : nil
|
||||
self.duration = session.duration
|
||||
self.status = session.status
|
||||
self.createdAt = formatter.string(from: session.createdAt)
|
||||
self.updatedAt = formatter.string(from: session.updatedAt)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String, gymId: String, date: String, startTime: String?, endTime: String?,
|
||||
duration: Int?, status: SessionStatus, createdAt: String, updatedAt: String
|
||||
) {
|
||||
self.id = id
|
||||
self.gymId = gymId
|
||||
self.date = date
|
||||
self.startTime = startTime
|
||||
self.endTime = endTime
|
||||
self.duration = duration
|
||||
self.status = status
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
func toClimbSession() -> ClimbSession {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
|
||||
// Preserve original IDs and dates
|
||||
let sessionId = UUID(uuidString: id) ?? UUID()
|
||||
let preservedGymId = UUID(uuidString: gymId) ?? UUID()
|
||||
let sessionDate = formatter.date(from: date) ?? Date()
|
||||
let sessionStartTime = startTime != nil ? formatter.date(from: startTime!) : nil
|
||||
let sessionEndTime = endTime != nil ? formatter.date(from: endTime!) : nil
|
||||
let createdDate = formatter.date(from: createdAt) ?? Date()
|
||||
let updatedDate = formatter.date(from: updatedAt) ?? Date()
|
||||
|
||||
return ClimbSession.fromImport(
|
||||
id: sessionId,
|
||||
gymId: preservedGymId,
|
||||
date: sessionDate,
|
||||
startTime: sessionStartTime,
|
||||
endTime: sessionEndTime,
|
||||
duration: duration,
|
||||
status: status,
|
||||
notes: nil,
|
||||
createdAt: createdDate,
|
||||
updatedAt: updatedDate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AndroidAttempt: Codable {
|
||||
let id: String
|
||||
let sessionId: String
|
||||
let problemId: String
|
||||
let result: AttemptResult
|
||||
let highestHold: String?
|
||||
let notes: String?
|
||||
let duration: Int?
|
||||
let restTime: Int?
|
||||
let timestamp: String
|
||||
let createdAt: String
|
||||
|
||||
init(from attempt: Attempt) {
|
||||
self.id = attempt.id.uuidString
|
||||
self.sessionId = attempt.sessionId.uuidString
|
||||
self.problemId = attempt.problemId.uuidString
|
||||
self.result = attempt.result
|
||||
self.highestHold = attempt.highestHold
|
||||
self.notes = attempt.notes
|
||||
self.duration = attempt.duration
|
||||
self.restTime = attempt.restTime
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
self.timestamp = formatter.string(from: attempt.timestamp)
|
||||
self.createdAt = formatter.string(from: attempt.createdAt)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String, sessionId: String, problemId: String, result: AttemptResult,
|
||||
highestHold: String?, notes: String?, duration: Int?, restTime: Int?,
|
||||
timestamp: String, createdAt: String
|
||||
) {
|
||||
self.id = id
|
||||
self.sessionId = sessionId
|
||||
self.problemId = problemId
|
||||
self.result = result
|
||||
self.highestHold = highestHold
|
||||
self.notes = notes
|
||||
self.duration = duration
|
||||
self.restTime = restTime
|
||||
self.timestamp = timestamp
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
func toAttempt() -> Attempt {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
|
||||
let attemptId = UUID(uuidString: id) ?? UUID()
|
||||
let preservedSessionId = UUID(uuidString: sessionId) ?? UUID()
|
||||
let preservedProblemId = UUID(uuidString: problemId) ?? UUID()
|
||||
let attemptTimestamp = formatter.date(from: timestamp) ?? Date()
|
||||
let createdDate = formatter.date(from: createdAt) ?? Date()
|
||||
|
||||
return Attempt.fromImport(
|
||||
id: attemptId,
|
||||
sessionId: preservedSessionId,
|
||||
problemId: preservedProblemId,
|
||||
result: result,
|
||||
highestHold: highestHold,
|
||||
notes: notes,
|
||||
duration: duration,
|
||||
restTime: restTime,
|
||||
timestamp: attemptTimestamp,
|
||||
createdAt: createdDate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
extension ClimbingDataManager {
|
||||
private func collectReferencedImagePaths() -> Set<String> {
|
||||
var imagePaths = Set<String>()
|
||||
for problem in problems {
|
||||
imagePaths.formUnion(problem.imagePaths)
|
||||
}
|
||||
return imagePaths
|
||||
}
|
||||
|
||||
private func updateProblemImagePaths(
|
||||
problems: [AndroidProblem],
|
||||
imagePathMapping: [String: String]
|
||||
) -> [AndroidProblem] {
|
||||
return problems.map { problem in
|
||||
let updatedImagePaths = (problem.imagePaths ?? []).compactMap { oldPath in
|
||||
let fileName = URL(fileURLWithPath: oldPath).lastPathComponent
|
||||
return imagePathMapping[fileName]
|
||||
}
|
||||
return problem.withUpdatedImagePaths(updatedImagePaths)
|
||||
}
|
||||
}
|
||||
|
||||
private func validateImportData(_ importData: ClimbDataExport) throws {
|
||||
if importData.gyms.isEmpty {
|
||||
throw NSError(
|
||||
domain: "ImportError", code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Import data is invalid: no gyms found"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Helper
|
||||
extension ClimbingDataManager {
|
||||
static var preview: ClimbingDataManager {
|
||||
let manager = ClimbingDataManager()
|
||||
|
||||
let sampleGym = Gym(
|
||||
name: "Sample Climbing Gym",
|
||||
location: "123 Rock St, Boulder, CO",
|
||||
supportedClimbTypes: [.boulder, .rope],
|
||||
difficultySystems: [.vScale, .yds]
|
||||
)
|
||||
|
||||
manager.gyms = [sampleGym]
|
||||
|
||||
let sampleProblem = Problem(
|
||||
gymId: sampleGym.id,
|
||||
name: "Crimpy Overhang",
|
||||
description: "Technical overhang with small holds",
|
||||
climbType: .boulder,
|
||||
difficulty: DifficultyGrade(system: .vScale, grade: "V4"),
|
||||
setter: "John Doe",
|
||||
tags: ["technical", "overhang"],
|
||||
location: "Cave area"
|
||||
)
|
||||
|
||||
manager.problems = [sampleProblem]
|
||||
|
||||
return manager
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user