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 = "Shotdeck – \(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.") } } }