import Foundation import SwiftUI /// One row in the drop list. ObservableObject so each row updates independently /// as its async compression finishes. final class ImageItem: ObservableObject, Identifiable { let id = UUID() let url: URL @Published var status: Status = .pending @Published var originalBytes: Int = 0 @Published var newBytes: Int = 0 @Published var message: String = "" enum Status { case pending, working, done, failed, skipped } init(url: URL) { self.url = url self.originalBytes = (try? FileManager.default .attributesOfItem(atPath: url.path)[.size] as? Int) ?? 0 } var savedFraction: Double { originalBytes > 0 && newBytes > 0 ? 1 - Double(newBytes) / Double(originalBytes) : 0 } } final class AppModel: ObservableObject { @Published var items: [ImageItem] = [] @Published var lossless = false @Published var distance = 1.0 @Published var effort = 7 @Published var keepOriginal = true @Published var busy = false var totalSaved: Int { items.reduce(0) { $0 + max(0, $1.originalBytes - $1.newBytes) } } var encoderReady: Bool { Compressor.cjxlPath != nil } func add(urls: [URL]) { for url in urls { let ext = url.pathExtension.lowercased() guard Compressor.supportedExtensions.contains(ext) else { continue } guard !items.contains(where: { $0.url == url }) else { continue } items.append(ImageItem(url: url)) } run() } func run() { guard !busy else { return } let opts = Compressor.Options(lossless: lossless, distance: distance, effort: effort, keepOriginal: keepOriginal) let pending = items.filter { $0.status == .pending } guard !pending.isEmpty else { return } busy = true DispatchQueue.global(qos: .userInitiated).async { for item in pending { DispatchQueue.main.async { item.status = .working } do { let r = try Compressor.compress(item.url, options: opts) DispatchQueue.main.async { item.newBytes = r.newBytes item.status = r.savedFraction > 0 ? .done : .skipped } } catch { DispatchQueue.main.async { item.status = .failed item.message = error.localizedDescription } } } DispatchQueue.main.async { self.busy = false } } } } func human(_ bytes: Int) -> String { ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file) }