Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
253 lines
9.4 KiB
Swift
253 lines
9.4 KiB
Swift
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
import UIKit
|
||
import MobileCoreServices
|
||
import PhotosUI
|
||
import Photos
|
||
import Tauri
|
||
|
||
public class FilePickerController: NSObject {
|
||
var plugin: DialogPlugin
|
||
|
||
init(_ dialogPlugin: DialogPlugin) {
|
||
plugin = dialogPlugin
|
||
}
|
||
|
||
private func dismissViewController(_ viewControllerToPresent: UIViewController, completion: (() -> Void)? = nil) {
|
||
viewControllerToPresent.dismiss(animated: true, completion: completion)
|
||
}
|
||
|
||
public func getModifiedAtFromUrl(_ url: URL) -> Int? {
|
||
do {
|
||
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
||
if let modifiedDateInSec = (attributes[.modificationDate] as? Date)?.timeIntervalSince1970 {
|
||
return Int(modifiedDateInSec * 1000.0)
|
||
} else {
|
||
return nil
|
||
}
|
||
} catch let error as NSError {
|
||
Logger.error("getModifiedAtFromUrl failed", error.localizedDescription)
|
||
return nil
|
||
}
|
||
}
|
||
|
||
public func getMimeTypeFromUrl(_ url: URL) -> String {
|
||
let fileExtension = url.pathExtension as CFString
|
||
guard let extUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, nil)?.takeUnretainedValue() else {
|
||
return ""
|
||
}
|
||
guard let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType) else {
|
||
return ""
|
||
}
|
||
return mimeUTI.takeRetainedValue() as String
|
||
}
|
||
|
||
public func getSizeFromUrl(_ url: URL) throws -> Int {
|
||
let values = try url.resourceValues(forKeys: [.fileSizeKey])
|
||
return values.fileSize ?? 0
|
||
}
|
||
|
||
public func getVideoDuration(_ url: URL) -> Int {
|
||
let asset = AVAsset(url: url)
|
||
let duration = asset.duration
|
||
let durationTime = CMTimeGetSeconds(duration)
|
||
return Int(round(durationTime))
|
||
}
|
||
|
||
public func getImageDimensions(_ url: URL) -> (Int?, Int?) {
|
||
if let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) {
|
||
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
|
||
return getHeightAndWidthFromImageProperties(imageProperties)
|
||
}
|
||
}
|
||
return (nil, nil)
|
||
}
|
||
|
||
public func getVideoDimensions(_ url: URL) -> (Int?, Int?) {
|
||
guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return (nil, nil) }
|
||
let size = track.naturalSize.applying(track.preferredTransform)
|
||
let height = abs(Int(size.height))
|
||
let width = abs(Int(size.width))
|
||
return (height, width)
|
||
}
|
||
|
||
private func getHeightAndWidthFromImageProperties(_ properties: [NSObject: AnyObject]) -> (Int?, Int?) {
|
||
let width = properties[kCGImagePropertyPixelWidth] as? Int
|
||
let height = properties[kCGImagePropertyPixelHeight] as? Int
|
||
let orientation = properties[kCGImagePropertyOrientation] as? Int ?? UIImage.Orientation.up.rawValue
|
||
switch orientation {
|
||
case UIImage.Orientation.left.rawValue, UIImage.Orientation.right.rawValue, UIImage.Orientation.leftMirrored.rawValue, UIImage.Orientation.rightMirrored.rawValue:
|
||
return (width, height)
|
||
default:
|
||
return (height, width)
|
||
}
|
||
}
|
||
|
||
private func getFileUrlByPath(_ path: String) -> URL? {
|
||
guard let url = URL.init(string: path) else {
|
||
return nil
|
||
}
|
||
if FileManager.default.fileExists(atPath: url.path) {
|
||
return url
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
/// ## In which cases do we need to save a copy of a file selected by a user to the app sandbox?
|
||
/// In short, only when the file is **not** selected using UIDocumentPickerDelegate.
|
||
/// For the rest of the cases, we need to write a copy of the file to the app sandbox.
|
||
///
|
||
/// For PHPicker (used for photos and videos), `NSItemProvider.loadFileRepresentation` returns a temporary file URL that is deleted after the completion handler.
|
||
/// The recommendation is to [Persist](https://developer.apple.com/documentation/foundation/nsitemprovider/2888338-loadfilerepresentation) the file by moving/copying
|
||
/// it to your app’s directory within the completion handler.
|
||
///
|
||
/// If available, `loadInPlaceFileRepresentation` can open a file in place; Photos assets typically do not support true in-place access,
|
||
/// so fall back to persisting a local file.
|
||
/// Ref: https://developer.apple.com/documentation/foundation/nsitemprovider/2888335-loadinplacefilerepresentation
|
||
///
|
||
/// For UIDocumentPicker, prefer "open in place" and avoid copying when possible.
|
||
/// Ref: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller
|
||
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
|
||
|
||
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
||
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||
directory = cachesDirectory
|
||
}
|
||
|
||
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
|
||
do {
|
||
try deleteFile(targetUrl)
|
||
}
|
||
|
||
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
|
||
return targetUrl
|
||
}
|
||
|
||
private func deleteFile(_ url: URL) throws {
|
||
if FileManager.default.fileExists(atPath: url.path) {
|
||
try FileManager.default.removeItem(atPath: url.path)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension FilePickerController: UIDocumentPickerDelegate {
|
||
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||
do {
|
||
self.plugin.onFilePickerEvent(.selected(urls))
|
||
} catch {
|
||
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
|
||
}
|
||
}
|
||
|
||
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||
self.plugin.onFilePickerEvent(.cancelled)
|
||
}
|
||
}
|
||
|
||
extension FilePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
|
||
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||
dismissViewController(picker)
|
||
self.plugin.onFilePickerEvent(.cancelled)
|
||
}
|
||
|
||
public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
|
||
self.plugin.onFilePickerEvent(.cancelled)
|
||
}
|
||
|
||
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||
self.plugin.onFilePickerEvent(.cancelled)
|
||
}
|
||
|
||
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||
dismissViewController(picker) {
|
||
if let url = info[.mediaURL] as? URL {
|
||
do {
|
||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||
self.plugin.onFilePickerEvent(.selected([temporaryUrl]))
|
||
} catch {
|
||
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
|
||
}
|
||
} else {
|
||
self.plugin.onFilePickerEvent(.cancelled)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@available(iOS 14, *)
|
||
extension FilePickerController: PHPickerViewControllerDelegate {
|
||
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
dismissViewController(picker)
|
||
if results.first == nil {
|
||
self.plugin.onFilePickerEvent(.cancelled)
|
||
return
|
||
}
|
||
var temporaryUrls: [URL] = []
|
||
var errorMessage: String?
|
||
let dispatchGroup = DispatchGroup()
|
||
for result in results {
|
||
if errorMessage != nil {
|
||
break
|
||
}
|
||
if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||
dispatchGroup.enter()
|
||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier, completionHandler: { url, error in
|
||
defer {
|
||
dispatchGroup.leave()
|
||
}
|
||
if let error = error {
|
||
errorMessage = error.localizedDescription
|
||
return
|
||
}
|
||
guard let url = url else {
|
||
errorMessage = "Unknown error"
|
||
return
|
||
}
|
||
do {
|
||
// We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler.
|
||
// This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`.
|
||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||
temporaryUrls.append(temporaryUrl)
|
||
} catch {
|
||
errorMessage = "Failed to create a temporary copy of the file"
|
||
}
|
||
})
|
||
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||
dispatchGroup.enter()
|
||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier, completionHandler: { url, error in
|
||
defer {
|
||
dispatchGroup.leave()
|
||
}
|
||
if let error = error {
|
||
errorMessage = error.localizedDescription
|
||
return
|
||
}
|
||
guard let url = url else {
|
||
errorMessage = "Unknown error"
|
||
return
|
||
}
|
||
do {
|
||
// We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler.
|
||
// This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`.
|
||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||
temporaryUrls.append(temporaryUrl)
|
||
} catch {
|
||
errorMessage = "Failed to create a temporary copy of the file"
|
||
}
|
||
})
|
||
} else {
|
||
errorMessage = "Unsupported file type identifier"
|
||
}
|
||
}
|
||
dispatchGroup.notify(queue: .main) {
|
||
if let errorMessage = errorMessage {
|
||
self.plugin.onFilePickerEvent(.error(errorMessage))
|
||
return
|
||
}
|
||
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
|
||
}
|
||
}
|
||
} |