import Foundation import Capacitor import QuickLook // Previews an already-downloaded file with iOS Quick Look — the native "look at this file" surface: swipe, // pinch-zoom, print, and its own share button. Registered by cap sync as window.Capacitor.Plugins.FileOpener. // // WHY A PLUGIN: the app loads its UI from a REMOTE origin (remote.bizgaze.com), so it cannot open a local // file:// URL in the WebView (cross-origin / capacitor local-serving isn't on this origin). @capacitor/share // only offers the share SHEET ("open in another app"), not a preview. Only QLPreviewController, presented // from native code, gives a real in-app preview. Quick Look picks the renderer from the file extension, which // is why the web layer now saves downloads with a correct extension (see bzEnsureExt in home.html). @objc(FileOpenerPlugin) public class FileOpenerPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "FileOpenerPlugin" public let jsName = "FileOpener" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "preview", returnType: CAPPluginReturnPromise) ] // QLPreviewController holds its dataSource weakly, so we must keep a strong reference alive for the // lifetime of the presented preview — otherwise it deallocates and the preview shows blank. private var dataSource: QLDataSource? @objc func preview(_ call: CAPPluginCall) { guard let raw = call.getString("path"), !raw.isEmpty else { call.reject("path is required"); return } let url = FileOpenerPlugin.fileURL(from: raw) guard FileManager.default.fileExists(atPath: url.path) else { call.reject("file not found: \(url.path)"); return } DispatchQueue.main.async { let ds = QLDataSource(url: url) self.dataSource = ds let controller = QLPreviewController() controller.dataSource = ds controller.modalPresentationStyle = .fullScreen guard let base = self.bridge?.viewController else { call.reject("no view controller to present from"); return } // Present on top of whatever is already showing (a modal, another sheet) so it never fails silently. var presenter = base while let top = presenter.presentedViewController { presenter = top } presenter.present(controller, animated: true) { call.resolve(["ok": true]) } } } // Accepts either a file:// URI (what Filesystem.writeFile returns) or a bare absolute path. private static func fileURL(from raw: String) -> URL { if raw.hasPrefix("file://") { if let u = URL(string: raw) { return u } // Un-encoded spaces make URL(string:) fail — percent-encode and retry before giving up. let encoded = raw.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? raw if let u = URL(string: encoded) { return u } } return URL(fileURLWithPath: raw) } } // Single-item data source. NSURL already conforms to QLPreviewItem. final class QLDataSource: NSObject, QLPreviewControllerDataSource { private let url: URL init(url: URL) { self.url = url } func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { return url as NSURL } }