1.0.0 for iOS is ready to ship

This commit is contained in:
2025-09-14 23:07:32 -06:00
parent a3e60ce995
commit 127c25f506
33 changed files with 2646 additions and 251 deletions

View File

@@ -1,10 +1,3 @@
//
// ClimbingDataManager.swift
// OpenClimb
//
// Created by OpenClimb on 2025-01-17.
//
import Combine
import Foundation
import SwiftUI
@@ -35,7 +28,14 @@ class ClimbingDataManager: ObservableObject {
}
init() {
_ = ImageManager.shared
loadAllData()
migrateImagePaths()
Task {
try? await Task.sleep(nanoseconds: 2_000_000_000)
await performImageMaintenance()
}
}
private func loadAllData() {
@@ -181,11 +181,12 @@ class ClimbingDataManager: ObservableObject {
attempts.removeAll { $0.problemId == problem.id }
saveAttempts()
// Delete associated images
ImageManager.shared.deleteImages(atPaths: problem.imagePaths)
// Delete the problem
problems.removeAll { $0.id == problem.id }
saveProblems()
successMessage = "Problem deleted successfully"
clearMessageAfterDelay()
}
func problem(withId id: UUID) -> Problem? {
@@ -770,7 +771,6 @@ struct AndroidAttempt: Codable {
}
}
// MARK: - Helper Functions
extension ClimbingDataManager {
private func collectReferencedImagePaths() -> Set<String> {
var imagePaths = Set<String>()
@@ -793,6 +793,137 @@ extension ClimbingDataManager {
}
}
private func migrateImagePaths() {
var needsUpdate = false
let updatedProblems = problems.map { problem in
let migratedPaths = problem.imagePaths.compactMap { path in
// If it's already a relative path, keep it
if !path.hasPrefix("/") {
return path
}
// For absolute paths, try to migrate to relative
let fileName = URL(fileURLWithPath: path).lastPathComponent
if ImageManager.shared.imageExists(atPath: fileName) {
needsUpdate = true
return fileName
}
// If image doesn't exist, remove from paths
needsUpdate = true
return nil
}
if migratedPaths != problem.imagePaths {
return problem.updated(imagePaths: migratedPaths)
}
return problem
}
if needsUpdate {
problems = updatedProblems
saveProblems()
print("Migrated image paths for \(problems.count) problems")
}
}
private func performImageMaintenance() async {
// Run maintenance in background
await Task.detached {
await ImageManager.shared.performMaintenance()
// Log storage information for debugging
let info = await ImageManager.shared.getStorageInfo()
print(
"📊 Image Storage: \(info.primaryCount) primary, \(info.backupCount) backup, \(info.totalSize / 1024)KB total"
)
}.value
}
func manualImageMaintenance() {
Task {
await performImageMaintenance()
}
}
func getImageStorageInfo() -> String {
let info = ImageManager.shared.getStorageInfo()
return """
Image Storage Status:
• Primary: \(info.primaryCount) files
• Backup: \(info.backupCount) files
• Total Size: \(formatBytes(info.totalSize))
"""
}
func cleanupUnusedImages() {
// Get all image paths currently referenced in problems
let referencedImages = Set(
problems.flatMap { $0.imagePaths.map { ImageManager.shared.getRelativePath(from: $0) } }
)
// Get all files in storage
if let primaryFiles = try? FileManager.default.contentsOfDirectory(
atPath: ImageManager.shared.getImagesDirectoryPath())
{
let orphanedFiles = primaryFiles.filter { !referencedImages.contains($0) }
for fileName in orphanedFiles {
_ = ImageManager.shared.deleteImage(atPath: fileName)
}
if !orphanedFiles.isEmpty {
print("🗑️ Cleaned up \(orphanedFiles.count) orphaned image files")
}
}
}
private func formatBytes(_ bytes: Int64) -> String {
let kb = Double(bytes) / 1024.0
let mb = kb / 1024.0
if mb >= 1.0 {
return String(format: "%.1f MB", mb)
} else {
return String(format: "%.0f KB", kb)
}
}
func forceImageRecovery() {
print("🚨 User initiated force image recovery")
ImageManager.shared.forceRecoveryMigration()
// Refresh the UI after recovery
objectWillChange.send()
}
func emergencyImageRestore() {
print("🆘 User initiated emergency image restore")
ImageManager.shared.emergencyImageRestore()
// Refresh the UI after restore
objectWillChange.send()
}
func validateImageStorage() -> Bool {
return ImageManager.shared.validateStorageIntegrity()
}
func getImageRecoveryStatus() -> String {
let isValid = validateImageStorage()
let info = ImageManager.shared.getStorageInfo()
return """
Image Storage Health: \(isValid ? "✅ Good" : "❌ Needs Recovery")
Primary Files: \(info.primaryCount)
Backup Files: \(info.backupCount)
Total Size: \(formatBytes(info.totalSize))
\(isValid ? "No action needed" : "Consider running Force Recovery")
"""
}
private func validateImportData(_ importData: ClimbDataExport) throws {
if importData.gyms.isEmpty {
throw NSError(
@@ -802,7 +933,6 @@ extension ClimbingDataManager {
}
}
// MARK: - Preview Helper
extension ClimbingDataManager {
static var preview: ClimbingDataManager {
let manager = ClimbingDataManager()