// // PhotoManager.swift // Reflect // // Manages photo storage for mood entries. // Photos are stored as JPEG files in the app group Documents directory. // import Foundation import UIKit import SwiftUI import os.log @MainActor class PhotoManager: ObservableObject { static let shared = PhotoManager() // MARK: - Constants private let compressionQuality: CGFloat = 0.8 private let thumbnailSize = CGSize(width: 200, height: 200) // MARK: - Storage Location private var photosDirectory: URL? { guard let containerURL = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: Constants.currentGroupShareId ) else { AppLogger.photos.error("Failed to get app group container") return nil } let photosURL = containerURL.appendingPathComponent("Photos", isDirectory: true) // Create directory if it doesn't exist if !FileManager.default.fileExists(atPath: photosURL.path) { do { try FileManager.default.createDirectory(at: photosURL, withIntermediateDirectories: true) } catch { AppLogger.photos.error("Failed to create photos directory: \(error.localizedDescription)") return nil } } return photosURL } private var thumbnailsDirectory: URL? { guard let photosDir = photosDirectory else { return nil } let thumbnailsURL = photosDir.appendingPathComponent("Thumbnails", isDirectory: true) if !FileManager.default.fileExists(atPath: thumbnailsURL.path) { do { try FileManager.default.createDirectory(at: thumbnailsURL, withIntermediateDirectories: true) } catch { AppLogger.photos.error("Failed to create thumbnails directory: \(error.localizedDescription)") return nil } } return thumbnailsURL } // MARK: - Save Photo func savePhoto(_ image: UIImage) -> UUID? { guard let photosDir = photosDirectory, let thumbnailsDir = thumbnailsDirectory else { return nil } let photoID = UUID() let filename = "\(photoID.uuidString).jpg" // Save full resolution let fullURL = photosDir.appendingPathComponent(filename) guard let fullData = image.jpegData(compressionQuality: compressionQuality) else { AppLogger.photos.error("Failed to create JPEG data") return nil } do { try fullData.write(to: fullURL) } catch { AppLogger.photos.error("Failed to save photo: \(error.localizedDescription)") return nil } // Save thumbnail let thumbnailURL = thumbnailsDir.appendingPathComponent(filename) if let thumbnail = createThumbnail(from: image), let thumbnailData = thumbnail.jpegData(compressionQuality: 0.6) { do { try thumbnailData.write(to: thumbnailURL) } catch { AppLogger.photos.error("Failed to save thumbnail: \(error)") } } AnalyticsManager.shared.track(.photoAdded) return photoID } // MARK: - Load Photo func loadPhoto(id: UUID) -> UIImage? { guard let photosDir = photosDirectory else { return nil } let filename = "\(id.uuidString).jpg" let fullURL = photosDir.appendingPathComponent(filename) guard FileManager.default.fileExists(atPath: fullURL.path) else { return nil } do { let data = try Data(contentsOf: fullURL) guard let image = UIImage(data: data) else { AppLogger.photos.error("Failed to create UIImage from photo data: \(id)") return nil } return image } catch { AppLogger.photos.error("Failed to read photo data for \(id): \(error)") return nil } } func loadThumbnail(id: UUID) -> UIImage? { guard let thumbnailsDir = thumbnailsDirectory else { return nil } let filename = "\(id.uuidString).jpg" let thumbnailURL = thumbnailsDir.appendingPathComponent(filename) // Try thumbnail first if FileManager.default.fileExists(atPath: thumbnailURL.path) { do { let data = try Data(contentsOf: thumbnailURL) if let image = UIImage(data: data) { return image } } catch { AppLogger.photos.error("Failed to read thumbnail data for \(id): \(error)") } } // Fall back to full image if thumbnail doesn't exist return loadPhoto(id: id) } // MARK: - Delete Photo func deletePhoto(id: UUID) -> Bool { guard let photosDir = photosDirectory, let thumbnailsDir = thumbnailsDirectory else { return false } let filename = "\(id.uuidString).jpg" let fullURL = photosDir.appendingPathComponent(filename) let thumbnailURL = thumbnailsDir.appendingPathComponent(filename) var success = true // Delete full image if FileManager.default.fileExists(atPath: fullURL.path) { do { try FileManager.default.removeItem(at: fullURL) } catch { AppLogger.photos.error("Failed to delete photo: \(error.localizedDescription)") success = false } } // Delete thumbnail if FileManager.default.fileExists(atPath: thumbnailURL.path) { do { try FileManager.default.removeItem(at: thumbnailURL) } catch { AppLogger.photos.error("Failed to delete thumbnail: \(error)") } } if success { AnalyticsManager.shared.track(.photoDeleted) } return success } // MARK: - Helpers private func createThumbnail(from image: UIImage) -> UIImage? { let size = thumbnailSize let aspectRatio = image.size.width / image.size.height var targetSize: CGSize if aspectRatio > 1 { // Landscape targetSize = CGSize(width: size.width, height: size.width / aspectRatio) } else { // Portrait or square targetSize = CGSize(width: size.height * aspectRatio, height: size.height) } UIGraphicsBeginImageContextWithOptions(targetSize, false, 1.0) image.draw(in: CGRect(origin: .zero, size: targetSize)) let thumbnail = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return thumbnail } // MARK: - Storage Info var totalPhotoCount: Int { guard let photosDir = photosDirectory else { return 0 } do { let files = try FileManager.default.contentsOfDirectory(atPath: photosDir.path) return files.filter { $0.hasSuffix(".jpg") }.count } catch { AppLogger.photos.error("Failed to list photos directory: \(error)") return 0 } } var totalStorageUsed: Int64 { guard let photosDir = photosDirectory else { return 0 } var totalSize: Int64 = 0 let fileManager = FileManager.default if let enumerator = fileManager.enumerator(at: photosDir, includingPropertiesForKeys: [.fileSizeKey]) { for case let fileURL as URL in enumerator { if let fileSize = try? fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize { totalSize += Int64(fileSize) } } } return totalSize } var formattedStorageUsed: String { let bytes = totalStorageUsed let formatter = ByteCountFormatter() formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } } // MARK: - SwiftUI Image Loading extension PhotoManager { func image(for id: UUID?) -> Image? { guard let id = id, let uiImage = loadPhoto(id: id) else { return nil } return Image(uiImage: uiImage) } func thumbnail(for id: UUID?) -> Image? { guard let id = id, let uiImage = loadThumbnail(id: id) else { return nil } return Image(uiImage: uiImage) } }