72 lines
3.2 KiB
Swift
72 lines
3.2 KiB
Swift
import AppKit
|
||
import Darwin
|
||
import Foundation
|
||
import ShotdeckCore
|
||
|
||
extension AppModel: SendCapable {
|
||
public func send(anchor: NSView?) async {
|
||
guard !session.isEmpty, !isSending else { return }
|
||
setSending(true)
|
||
defer { setSending(false) }
|
||
|
||
let workingSession = session
|
||
let composer = self.composer
|
||
// Live outbox (FolderSettings), not `paths.outbox` — Settings changes take effect.
|
||
let outboxDir = outboxURL
|
||
let sourceDir = paths.sessionDirectory(workingSession.id)
|
||
let fileName = PDFComposer.fileName(for: workingSession)
|
||
let finalURL = outboxDir.appendingPathComponent(fileName)
|
||
// Same directory as the final target so the rename below is same-volume (atomic).
|
||
let tempURL = outboxDir.appendingPathComponent(".shotdeck-\(UUID().uuidString).pdf")
|
||
let title = "Redline – \(DubaiTime.stamp(workingSession.createdAt))"
|
||
|
||
do {
|
||
// D-13: build off the main actor. Only Sendable values cross into the
|
||
// detached task — never `anchor` (NSView is not Sendable).
|
||
try await Task.detached(priority: .userInitiated) {
|
||
_ = try composer.compose(
|
||
session: workingSession,
|
||
imageURL: { capture in sourceDir.appendingPathComponent(capture.fileName) },
|
||
title: title,
|
||
to: tempURL
|
||
)
|
||
// POSIX rename onto `finalURL` replaces any same-name file in one
|
||
// directory operation; there is never a window where the PDF is gone.
|
||
if Darwin.rename(tempURL.path, finalURL.path) != 0 {
|
||
throw ShotdeckError.pdfCompositionFailed(
|
||
reason: "could not publish the PDF: \(String(cString: strerror(errno)))"
|
||
)
|
||
}
|
||
try AtomicFile.fsyncDirectory(at: outboxDir)
|
||
}.value
|
||
|
||
// File exists on disk now — archive only after that (D-13). A later AirDrop
|
||
// failure never deletes this file.
|
||
guard FileManager.default.fileExists(atPath: finalURL.path) else {
|
||
throw ShotdeckError.pdfCompositionFailed(reason: "the PDF was not written to disk")
|
||
}
|
||
_ = try await spool.archiveCurrent(pdfFileName: fileName)
|
||
replaceSession(try await spool.currentSession())
|
||
|
||
let pageWord = workingSession.captures.count == 1 ? "page" : "pages"
|
||
setStatus("Sent — \(workingSession.captures.count) \(pageWord).")
|
||
|
||
guard let anchor else {
|
||
setStatus("PDF saved to \(outboxDisplayName). Open the panel to AirDrop it.")
|
||
return
|
||
}
|
||
do {
|
||
try Sharing.airDrop(fileURL: finalURL, from: anchor)
|
||
} catch {
|
||
setStatus(
|
||
"AirDrop is not available right now — the PDF is on your \(outboxDisplayName)."
|
||
)
|
||
}
|
||
} catch {
|
||
// Never unlink the published PDF, and never unlink `tempURL` either:
|
||
// a rename failure would leave the complete document at the temp name.
|
||
setStatus((error as? ShotdeckError)?.errorDescription ?? "The PDF could not be built.")
|
||
}
|
||
}
|
||
}
|