import CoreTransferable import Foundation import RastrilloApple import SwiftUI import UniformTypeIdentifiers #if os(macOS) import PDFKit #else import UIKit #endif struct ExportedDocument: Transferable { let url: URL let isPDF: Bool let suggestedName: String static var transferRepresentation: some TransferRepresentation { FileRepresentation(exportedContentType: .pdf) { document in SentTransferredFile(document.url, allowAccessingOriginalFile: false) }.exportingCondition { $0.isPDF }.suggestedFileName { $0.suggestedName } FileRepresentation(exportedContentType: UTType(filenameExtension: "md") ?? .plainText) { document in SentTransferredFile(document.url, allowAccessingOriginalFile: false) }.exportingCondition { !$0.isPDF }.suggestedFileName { $0.suggestedName } } } @MainActor struct DocumentExportControls: View { let client: NativeHTTP let sqid: String let title: String let prepare: () async -> Bool @Binding var request: String? @State private var document: ExportedDocument? @State private var exporting = false @State private var working = false @State private var active = true @State private var type = UTType.pdf @State private var failed = false init(client: NativeHTTP, sqid: String, title: String, prepare: @escaping () async -> Bool, request: Binding = .constant(nil)) { self.client = client; self.sqid = sqid; self.title = title; self.prepare = prepare; self._request = request } var body: some View { Menu { Button { Task { await download(pdf: true, printDocument: false) } } label: { Text("docs.export.pdf.label", bundle: DocsResources.bundle) } Button { Task { await download(pdf: false, printDocument: false) } } label: { Text("docs.export.markdown.label", bundle: DocsResources.bundle) } Button { Task { await download(pdf: true, printDocument: true) } } label: { Text("docs.editor.menu.file.print", bundle: DocsResources.bundle) } } label: { Image(systemName: "square.and.arrow.up") } .onChange(of: request) { _, value in guard let value else { return } request = nil Task { await download(pdf: value != "markdown", printDocument: value == "print") } } .disabled(working || exporting) .accessibilityLabel(Text("native.export", bundle: DocsResources.bundle)) .fileExporter(isPresented: $exporting, item: document, contentTypes: [type], defaultFilename: title, onCompletion: { result in if case .failure = result { failed = true } cleanup() }, onCancellation: { cleanup() }) .onAppear { active = true } .onDisappear { active = false; if !exporting && !working { cleanup() } } .alert(Text("docs.error.generic", bundle: DocsResources.bundle), isPresented: $failed) { Button(role: .cancel) {} label: { Text("native.close", bundle: DocsResources.bundle) } } } static func filename(title: String, extension suffix: String) -> String { let invalid = CharacterSet.controlCharacters.union(CharacterSet(charactersIn: "/\\:")) let cleaned = title.components(separatedBy: invalid).joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines) var base = "" for character in cleaned { guard base.utf8.count + String(character).utf8.count <= 180 else { break } base.append(character) } if base.isEmpty || base == "." || base == ".." { base = String(localized: "docs.document.untitled", bundle: DocsResources.bundle) } return base.lowercased().hasSuffix("." + suffix) ? base : base + "." + suffix } private func cleanup() { if let document { try? FileManager.default.removeItem(at: document.url.deletingLastPathComponent()) } document = nil } private func download(pdf: Bool, printDocument: Bool) async { guard !working, !exporting else { return } working = true defer { working = false } guard await prepare(), active else { return } let directory = FileManager.default.temporaryDirectory .appendingPathComponent("oficina-docs-export-" + UUID().uuidString, isDirectory: true) let filename = Self.filename(title: title, extension: pdf ? "pdf" : "md") let destination = directory.appendingPathComponent(filename) do { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: [.posixPermissions: 0o700]) try await client.download("/d/\(sqid)/export.\(pdf ? "pdf" : "md")", to: destination) guard active else { try? FileManager.default.removeItem(at: directory); return } if printDocument { #if os(macOS) defer { try? FileManager.default.removeItem(at: directory) } guard let pdf = PDFDocument(url: destination), let operation = pdf.printOperation(for: NSPrintInfo.shared, scalingMode: .pageScaleDownToFit, autoRotate: true) else { throw CRDTReplica.Failure.rejected } operation.run() #else let controller = UIPrintInteractionController.shared controller.printingItem = destination let presented = controller.present(animated: true) { _, _, _ in try? FileManager.default.removeItem(at: directory) } if !presented { try? FileManager.default.removeItem(at: directory); failed = true } #endif } else { type = pdf ? .pdf : (UTType(filenameExtension: "md") ?? .plainText) document = ExportedDocument(url: destination, isPDF: pdf, suggestedName: filename) exporting = true } } catch { try? FileManager.default.removeItem(at: directory) failed = true } } }