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.
103 lines
3.5 KiB
103 lines
3.5 KiB
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<Void, Never>?
|
|
|
|
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
|
|
}
|
|
}
|