1.6.0 for Android and 1.1.0 for iOS - Finalizing Export and Import

formats :)
This commit is contained in:
2025-09-28 02:37:03 -06:00
parent cf2e2f7c57
commit c3f847e1e6
48 changed files with 6944 additions and 1107 deletions

View File

@@ -29,6 +29,9 @@ class ClimbingDataManager: ObservableObject {
private let decoder = JSONDecoder()
private var liveActivityObserver: NSObjectProtocol?
// Sync service for automatic syncing
let syncService = SyncService()
private enum Keys {
static let gyms = "openclimb_gyms"
static let problems = "openclimb_problems"
@@ -200,6 +203,7 @@ class ClimbingDataManager: ObservableObject {
func addGym(_ gym: Gym) {
gyms.append(gym)
saveGyms()
DataStateManager.shared.updateDataState()
successMessage = "Gym added successfully"
clearMessageAfterDelay()
}
@@ -208,6 +212,7 @@ class ClimbingDataManager: ObservableObject {
if let index = gyms.firstIndex(where: { $0.id == gym.id }) {
gyms[index] = gym
saveGyms()
DataStateManager.shared.updateDataState()
successMessage = "Gym updated successfully"
clearMessageAfterDelay()
}
@@ -229,6 +234,7 @@ class ClimbingDataManager: ObservableObject {
// Delete the gym
gyms.removeAll { $0.id == gym.id }
saveGyms()
DataStateManager.shared.updateDataState()
successMessage = "Gym deleted successfully"
clearMessageAfterDelay()
}
@@ -240,14 +246,19 @@ class ClimbingDataManager: ObservableObject {
func addProblem(_ problem: Problem) {
problems.append(problem)
saveProblems()
DataStateManager.shared.updateDataState()
successMessage = "Problem added successfully"
clearMessageAfterDelay()
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
}
func updateProblem(_ problem: Problem) {
if let index = problems.firstIndex(where: { $0.id == problem.id }) {
problems[index] = problem
saveProblems()
DataStateManager.shared.updateDataState()
successMessage = "Problem updated successfully"
clearMessageAfterDelay()
}
@@ -264,6 +275,7 @@ class ClimbingDataManager: ObservableObject {
// Delete the problem
problems.removeAll { $0.id == problem.id }
saveProblems()
DataStateManager.shared.updateDataState()
}
func problem(withId id: UUID) -> Problem? {
@@ -290,6 +302,7 @@ class ClimbingDataManager: ObservableObject {
saveActiveSession()
saveSessions()
DataStateManager.shared.updateDataState()
successMessage = "Session started successfully"
clearMessageAfterDelay()
@@ -317,9 +330,13 @@ class ClimbingDataManager: ObservableObject {
saveActiveSession()
saveSessions()
DataStateManager.shared.updateDataState()
successMessage = "Session completed successfully"
clearMessageAfterDelay()
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
// MARK: - End Live Activity after session ends
Task {
await LiveActivityManager.shared.endLiveActivity()
@@ -337,6 +354,7 @@ class ClimbingDataManager: ObservableObject {
}
saveSessions()
DataStateManager.shared.updateDataState()
successMessage = "Session updated successfully"
clearMessageAfterDelay()
@@ -359,6 +377,7 @@ class ClimbingDataManager: ObservableObject {
// Delete the session
sessions.removeAll { $0.id == session.id }
saveSessions()
DataStateManager.shared.updateDataState()
successMessage = "Session deleted successfully"
clearMessageAfterDelay()
}
@@ -380,8 +399,12 @@ class ClimbingDataManager: ObservableObject {
func addAttempt(_ attempt: Attempt) {
attempts.append(attempt)
saveAttempts()
DataStateManager.shared.updateDataState()
successMessage = "Attempt logged successfully"
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
clearMessageAfterDelay()
// Update Live Activity when new attempt is added
@@ -392,6 +415,7 @@ class ClimbingDataManager: ObservableObject {
if let index = attempts.firstIndex(where: { $0.id == attempt.id }) {
attempts[index] = attempt
saveAttempts()
DataStateManager.shared.updateDataState()
successMessage = "Attempt updated successfully"
clearMessageAfterDelay()
@@ -403,6 +427,7 @@ class ClimbingDataManager: ObservableObject {
func deleteAttempt(_ attempt: Attempt) {
attempts.removeAll { $0.id == attempt.id }
saveAttempts()
DataStateManager.shared.updateDataState()
successMessage = "Attempt deleted successfully"
clearMessageAfterDelay()
@@ -464,6 +489,7 @@ class ClimbingDataManager: ObservableObject {
userDefaults.removeObject(forKey: Keys.attempts)
userDefaults.removeObject(forKey: Keys.activeSession)
DataStateManager.shared.reset()
successMessage = "All data has been reset"
clearMessageAfterDelay()
}
@@ -473,13 +499,14 @@ class ClimbingDataManager: ObservableObject {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
let exportData = ClimbDataExport(
let exportData = ClimbDataBackup(
exportedAt: dateFormatter.string(from: Date()),
version: "2.0",
gyms: gyms.map { AndroidGym(from: $0) },
problems: problems.map { AndroidProblem(from: $0) },
sessions: sessions.map { AndroidClimbSession(from: $0) },
attempts: attempts.map { AndroidAttempt(from: $0) }
formatVersion: "2.0",
gyms: gyms.map { BackupGym(from: $0) },
problems: problems.map { BackupProblem(from: $0) },
sessions: sessions.map { BackupClimbSession(from: $0) },
attempts: attempts.map { BackupAttempt(from: $0) }
)
// Collect referenced image paths
@@ -529,7 +556,7 @@ class ClimbingDataManager: ObservableObject {
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)
let importData = try decoder.decode(ClimbDataBackup.self, from: importResult.jsonData)
print("Successfully decoded import data:")
print("- Gyms: \(importData.gyms.count)")
@@ -546,16 +573,19 @@ class ClimbingDataManager: ObservableObject {
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() }
self.gyms = try importData.gyms.map { try $0.toGym() }
self.problems = try updatedProblems.map { try $0.toProblem() }
self.sessions = try importData.sessions.map { try $0.toClimbSession() }
self.attempts = try importData.attempts.map { try $0.toAttempt() }
saveGyms()
saveProblems()
saveSessions()
saveAttempts()
// Update data state to current time since we just imported new data
DataStateManager.shared.updateDataState()
successMessage =
"Data imported successfully with \(importResult.imagePathMapping.count) images"
clearMessageAfterDelay()
@@ -584,337 +614,6 @@ class ClimbingDataManager: ObservableObject {
}
}
struct ClimbDataExport: Codable {
let exportedAt: String
let version: String
let gyms: [AndroidGym]
let problems: [AndroidProblem]
let sessions: [AndroidClimbSession]
let attempts: [AndroidAttempt]
init(
exportedAt: String, version: String = "2.0", gyms: [AndroidGym], problems: [AndroidProblem],
sessions: [AndroidClimbSession], attempts: [AndroidAttempt]
) {
self.exportedAt = exportedAt
self.version = version
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 customDifficultyGrades: [String]
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.customDifficultyGrades = gym.customDifficultyGrades
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], customDifficultyGrades: [String] = [],
notes: String?, createdAt: String, updatedAt: String
) {
self.id = id
self.name = name
self.location = location
self.supportedClimbTypes = supportedClimbTypes
self.difficultySystems = difficultySystems
self.customDifficultyGrades = customDifficultyGrades
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: 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 tags: [String]
let location: String?
let imagePaths: [String]?
let isActive: Bool
let dateSet: String?
let notes: 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.tags = problem.tags
self.location = problem.location
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
self.isActive = problem.isActive
self.notes = problem.notes
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
self.dateSet = problem.dateSet != nil ? formatter.string(from: problem.dateSet!) : nil
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, tags: [String] = [],
location: String? = nil,
imagePaths: [String]? = nil, isActive: Bool = true, dateSet: String? = nil,
notes: String? = nil,
createdAt: String, updatedAt: String
) {
self.id = id
self.gymId = gymId
self.name = name
self.description = description
self.climbType = climbType
self.difficulty = difficulty
self.tags = tags
self.location = location
self.imagePaths = imagePaths
self.isActive = isActive
self.dateSet = dateSet
self.notes = notes
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,
tags: tags,
location: location,
imagePaths: imagePaths ?? [],
isActive: isActive,
dateSet: dateSet != nil ? formatter.date(from: dateSet!) : nil,
notes: notes,
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,
tags: self.tags,
location: self.location,
imagePaths: newImagePaths.isEmpty ? nil : newImagePaths,
isActive: self.isActive,
dateSet: self.dateSet,
notes: self.notes,
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: Int64?
let status: SessionStatus
let notes: String?
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 != nil ? Int64(session.duration!) : nil
self.status = session.status
self.notes = session.notes
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: Int64?, status: SessionStatus, notes: String? = nil, 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.notes = notes
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 != nil ? Int(duration!) : nil,
status: status,
notes: notes,
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: Int64?
let restTime: Int64?
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 != nil ? Int64(attempt.duration!) : nil
self.restTime = attempt.restTime != nil ? Int64(attempt.restTime!) : nil
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: Int64?, restTime: Int64?,
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 != nil ? Int(duration!) : nil,
restTime: restTime != nil ? Int(restTime!) : nil,
timestamp: attemptTimestamp,
createdAt: createdDate
)
}
}
extension ClimbingDataManager {
private func collectReferencedImagePaths() -> Set<String> {
var imagePaths = Set<String>()
@@ -949,9 +648,9 @@ extension ClimbingDataManager {
}
private func updateProblemImagePaths(
problems: [AndroidProblem],
problems: [BackupProblem],
imagePathMapping: [String: String]
) -> [AndroidProblem] {
) -> [BackupProblem] {
return problems.map { problem in
let updatedImagePaths = (problem.imagePaths ?? []).compactMap { oldPath in
let fileName = URL(fileURLWithPath: oldPath).lastPathComponent
@@ -1298,7 +997,7 @@ extension ClimbingDataManager {
saveAttempts()
}
private func validateImportData(_ importData: ClimbDataExport) throws {
private func validateImportData(_ importData: ClimbDataBackup) throws {
if importData.gyms.isEmpty {
throw NSError(
domain: "ImportError", code: 1,

View File

@@ -130,14 +130,8 @@ final class LiveActivityManager {
completedProblems: completedProblems
)
do {
await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
print("✅ Live Activity updated successfully")
} catch {
print("❌ Failed to update Live Activity: \(error)")
// If update fails, the activity might have been dismissed
self.currentActivity = nil
}
await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
print("✅ Live Activity updated successfully")
}
/// Call this when a ClimbSession ends to end the Live Activity