1.0.0 for iOS is ready to ship
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user