2023-02-27 05:39:07 +00:00
|
|
|
import Combine
|
2022-12-22 09:53:36 +00:00
|
|
|
import QuickLook
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
public class QuickLook: ObservableObject {
|
2023-02-23 17:57:12 +00:00
|
|
|
@Published public var url: URL? {
|
|
|
|
didSet {
|
|
|
|
if url == nil {
|
|
|
|
cleanup(urls: urls)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-26 05:45:57 +00:00
|
|
|
|
2022-12-22 09:53:36 +00:00
|
|
|
@Published public private(set) var urls: [URL] = []
|
|
|
|
@Published public private(set) var isPreparing: Bool = false
|
|
|
|
@Published public private(set) var latestError: Error?
|
2023-01-17 10:36:01 +00:00
|
|
|
|
|
|
|
public init() {}
|
|
|
|
|
2022-12-22 09:53:36 +00:00
|
|
|
public func prepareFor(urls: [URL], selectedURL: URL) async {
|
2023-01-28 05:45:15 +00:00
|
|
|
var transaction = Transaction(animation: .default)
|
|
|
|
transaction.disablesAnimations = true
|
|
|
|
withTransaction(transaction) {
|
2022-12-22 09:53:36 +00:00
|
|
|
isPreparing = true
|
|
|
|
}
|
|
|
|
do {
|
2023-01-29 06:35:59 +00:00
|
|
|
var order = 0
|
|
|
|
let pathOrderMap = urls.reduce(into: [String: Int]()) { result, url in
|
|
|
|
result[url.lastPathComponent] = order
|
|
|
|
order += 1
|
|
|
|
}
|
2022-12-22 09:53:36 +00:00
|
|
|
let paths: [URL] = try await withThrowingTaskGroup(of: URL.self, body: { group in
|
|
|
|
var paths: [URL] = []
|
|
|
|
for url in urls {
|
|
|
|
group.addTask {
|
|
|
|
try await self.localPathFor(url: url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for try await path in group {
|
|
|
|
paths.append(path)
|
|
|
|
}
|
2023-01-29 06:35:59 +00:00
|
|
|
return paths.sorted { url1, url2 in
|
2023-01-30 06:27:06 +00:00
|
|
|
pathOrderMap[url1.lastPathComponent] ?? 0 < pathOrderMap[url2.lastPathComponent] ?? 0
|
2023-01-29 06:35:59 +00:00
|
|
|
}
|
2022-12-22 09:53:36 +00:00
|
|
|
})
|
2023-01-28 05:45:15 +00:00
|
|
|
withTransaction(transaction) {
|
|
|
|
self.urls = paths
|
|
|
|
url = paths.first(where: { $0.lastPathComponent == selectedURL.lastPathComponent })
|
2022-12-22 09:53:36 +00:00
|
|
|
isPreparing = false
|
|
|
|
}
|
|
|
|
} catch {
|
2023-01-28 05:45:15 +00:00
|
|
|
withTransaction(transaction) {
|
2022-12-22 09:53:36 +00:00
|
|
|
isPreparing = false
|
2023-01-28 05:45:15 +00:00
|
|
|
self.urls = []
|
|
|
|
url = nil
|
|
|
|
latestError = error
|
2022-12-22 09:53:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-26 05:45:57 +00:00
|
|
|
|
2023-02-23 17:57:12 +00:00
|
|
|
private var quickLookDir: URL {
|
|
|
|
try! FileManager.default.url(for: .cachesDirectory,
|
|
|
|
in: .userDomainMask,
|
|
|
|
appropriateFor: nil,
|
|
|
|
create: false)
|
|
|
|
.appending(component: "quicklook")
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-22 09:53:36 +00:00
|
|
|
private func localPathFor(url: URL) async throws -> URL {
|
2023-02-23 17:57:12 +00:00
|
|
|
try? FileManager.default.createDirectory(at: quickLookDir, withIntermediateDirectories: true)
|
|
|
|
let path = quickLookDir.appendingPathComponent(url.lastPathComponent)
|
2023-02-27 05:39:07 +00:00
|
|
|
|
|
|
|
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
|
|
|
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
|
|
|
// boundary.
|
|
|
|
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
|
|
|
|
// There is a Radar tracking this & others like it.
|
2022-12-22 09:53:36 +00:00
|
|
|
let data = try await URLSession.shared.data(from: url).0
|
|
|
|
try data.write(to: path)
|
|
|
|
return path
|
|
|
|
}
|
2023-02-26 05:45:57 +00:00
|
|
|
|
|
|
|
private func cleanup(urls _: [URL]) {
|
2023-02-23 17:57:12 +00:00
|
|
|
try? FileManager.default.removeItem(at: quickLookDir)
|
|
|
|
}
|
2022-12-22 09:53:36 +00:00
|
|
|
}
|