Fix Photos save for images + add native Quick Look file preview
Photos/Files bug: the lightbox download handed nativeSaveFile a bare "/files/<id>" with no name, extension, or mime. Empty mime made bzSaveToPhotos short-circuit as "notmedia" — so saveToAlbum (and its permission prompt) never ran, and the image landed in the generic Files folder as an extension-less blob. Now nativeSaveFile trusts the server's Content-Type when the mime is unknown and appends a real extension (bzExtForMime/bzEnsureExt), so images reach the Images folder AND Photos. File preview: replace the @capacitor/share "share sheet" open with a new native FileOpener plugin (QLPreviewController). bzOpenFile now prefers a real Quick Look preview and only falls back to the share sheet if the plugin isn't in the build. Wired file-opener into mobile/package.json and the codemagic SPM diagnostics loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
# NOTE: the pod name MUST be 'FileOpener' (PascalCase of the npm package name 'file-opener').
|
||||
# `cap sync` writes `pod 'FileOpener', :path => '../../plugins/file-opener'` into the generated Podfile,
|
||||
# and CocoaPods then looks for a file literally named FileOpener.podspec whose s.name is 'FileOpener'.
|
||||
# Any other name → "No podspec found for `FileOpener`" and pod install fails. (Same trap that broke the
|
||||
# AudioRoute build — see mobile/plugins/audio-route/AudioRoute.podspec.)
|
||||
s.name = 'FileOpener'
|
||||
s.version = package['version']
|
||||
s.summary = package['description']
|
||||
s.license = package['license']
|
||||
s.homepage = 'https://bizgaze.com'
|
||||
s.author = 'BizGaze'
|
||||
s.source = { :git => 'https://bizgaze.com/file-opener.git', :tag => s.version.to_s }
|
||||
s.source_files = 'ios/Sources/**/*.{swift,h,m}'
|
||||
s.ios.deployment_target = '15.0'
|
||||
s.dependency 'Capacitor'
|
||||
s.swift_version = '5.1'
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FileOpener",
|
||||
platforms: [.iOS(.v15)],
|
||||
products: [
|
||||
.library(name: "FileOpener", targets: ["FileOpenerPlugin"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "FileOpenerPlugin",
|
||||
dependencies: [
|
||||
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
||||
.product(name: "Cordova", package: "capacitor-swift-pm")
|
||||
],
|
||||
path: "ios/Sources/FileOpenerPlugin")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "file-opener",
|
||||
"version": "1.0.0",
|
||||
"description": "Preview a downloaded file with native iOS Quick Look for Biz Connect",
|
||||
"main": "dist/plugin.cjs.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"author": "BizGaze",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist/",
|
||||
"ios/",
|
||||
"FileOpener.podspec",
|
||||
"Package.swift"
|
||||
],
|
||||
"capacitor": {
|
||||
"ios": {
|
||||
"src": "ios"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/core": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^8.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user