import DropTwoCore import Foundation /// Treibt die Verarbeitung der Dateiliste über `JobQueue` an und hält den /// UI-Status pro Datei nach. Kennt keine Views — nur `@Published`-Zustand, /// den SwiftUI beobachtet. @MainActor final class ConversionCoordinator: ObservableObject { @Published private(set) var items: [DropItem] = [] @Published private(set) var isRunning = false private var runningTask: Task? var overallProgress: Double { guard !items.isEmpty else { return 0 } let sum = items.reduce(0.0) { partial, item in switch item.status { case .waiting: return partial case .running(let progress): return partial + progress case .skipped, .rejected, .succeeded, .failed: return partial + 1 } } return sum / Double(items.count) } var problemItems: [DropItem] { items.filter { $0.status.needsAttention } } /// README: "Dateiauswahl vorsortiert nach zuletzt geändert" — neueste zuerst. func addFiles(_ urls: [URL]) { let existing = Set(items.map(\.sourceURL)) let newURLs = urls .filter { !existing.contains($0) } .sorted { modificationDate(for: $0) > modificationDate(for: $1) } items.append(contentsOf: newURLs.map(DropItem.init)) } func removeAll() { guard !isRunning else { return } items.removeAll() } func start(targetFolder: URL) { guard !isRunning, !items.isEmpty else { return } isRunning = true for index in items.indices { items[index].status = .waiting } let jobs = items.map { JobQueue.Job(id: $0.id, sourceURL: $0.sourceURL) } runningTask = Task { let stream = JobQueue.run( jobs: jobs, outputRootDirectory: targetFolder, progress: { [weak self] job, value in Task { @MainActor in self?.updateProgress(jobID: job.id, value: value) } } ) for await result in stream { apply(result) } isRunning = false } } private func updateProgress(jobID: UUID, value: Double) { guard let index = items.firstIndex(where: { $0.id == jobID }) else { return } items[index].status = .running(progress: value) } private func apply(_ result: JobQueue.JobResult) { guard let index = items.firstIndex(where: { $0.id == result.job.id }) else { return } switch result.outcome { case .skipped(_, let reason): items[index].status = .skipped(reason) case .rejected(_, let reason): items[index].status = .rejected(reason) case .succeeded(let success): items[index].status = .succeeded(croppedDestination: success.croppedDestination) case .failed(_, let error): items[index].status = .failed(message: Self.message(for: error)) } } private static func message(for error: Error) -> String { if let localizedError = error as? LocalizedError, let description = localizedError.errorDescription { return description } return error.localizedDescription } private func modificationDate(for url: URL) -> Date { (try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast } }