You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

78 lines
2.7 KiB

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)
}