mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-22 21:30:44 +02:00
feat(dialog): add plugin (#306)
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import PhotosUI
|
||||
import Photos
|
||||
import WebKit
|
||||
import Tauri
|
||||
import SwiftRs
|
||||
|
||||
enum FilePickerEvent {
|
||||
case selected([URL])
|
||||
case cancelled
|
||||
case error(String)
|
||||
}
|
||||
|
||||
class DialogPlugin: Plugin {
|
||||
|
||||
var filePickerController: FilePickerController!
|
||||
var pendingInvoke: Invoke? = nil
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
filePickerController = FilePickerController(self)
|
||||
}
|
||||
|
||||
@objc public func showFilePicker(_ invoke: Invoke) {
|
||||
let multiple = invoke.getBool("multiple", false)
|
||||
let filters = invoke.getArray("filters") ?? []
|
||||
let parsedTypes = parseFiltersOption(filters)
|
||||
|
||||
var isMedia = true
|
||||
var uniqueMimeType: Bool? = nil
|
||||
var mimeKind: String? = nil
|
||||
if !parsedTypes.isEmpty {
|
||||
uniqueMimeType = true
|
||||
for mime in parsedTypes {
|
||||
let kind = mime.components(separatedBy: "/")[0]
|
||||
if kind != "image" && kind != "video" {
|
||||
isMedia = false
|
||||
}
|
||||
if (mimeKind == nil) {
|
||||
mimeKind = kind
|
||||
} else if (mimeKind != kind) {
|
||||
uniqueMimeType = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingInvoke = invoke
|
||||
|
||||
if uniqueMimeType == true || isMedia {
|
||||
DispatchQueue.main.async {
|
||||
if #available(iOS 14, *) {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.selectionLimit = multiple ? 0 : 1
|
||||
|
||||
if uniqueMimeType == true {
|
||||
if mimeKind == "image" {
|
||||
configuration.filter = .images
|
||||
} else if mimeKind == "video" {
|
||||
configuration.filter = .videos
|
||||
}
|
||||
}
|
||||
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self.filePickerController
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
} else {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = self.filePickerController
|
||||
|
||||
if uniqueMimeType == true && mimeKind == "image" {
|
||||
picker.sourceType = .photoLibrary
|
||||
}
|
||||
|
||||
picker.sourceType = .photoLibrary
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
picker.delegate = self.filePickerController
|
||||
picker.allowsMultipleSelection = multiple
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentViewController(_ viewControllerToPresent: UIViewController) {
|
||||
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func parseFiltersOption(_ filters: JSArray) -> [String] {
|
||||
var parsedTypes: [String] = []
|
||||
for (_, filter) in filters.enumerated() {
|
||||
let filterObj = filter as? JSObject
|
||||
if let filterObj = filterObj {
|
||||
let extensions = filterObj["extensions"] as? JSArray
|
||||
if let extensions = extensions {
|
||||
for e in extensions {
|
||||
let ext = e as? String ?? ""
|
||||
guard let utType: String = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String? else {
|
||||
continue
|
||||
}
|
||||
parsedTypes.append(utType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedTypes
|
||||
}
|
||||
|
||||
public func onFilePickerEvent(_ event: FilePickerEvent) {
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
let readData = pendingInvoke?.getBool("readData", false) ?? false
|
||||
do {
|
||||
let filesResult = try urls.map {(url: URL) -> JSObject in
|
||||
var file = JSObject()
|
||||
|
||||
let mimeType = filePickerController.getMimeTypeFromUrl(url)
|
||||
let isVideo = mimeType.hasPrefix("video")
|
||||
let isImage = mimeType.hasPrefix("image")
|
||||
|
||||
if readData {
|
||||
file["data"] = try Data(contentsOf: url).base64EncodedString()
|
||||
}
|
||||
|
||||
if isVideo {
|
||||
file["duration"] = filePickerController.getVideoDuration(url)
|
||||
let (height, width) = filePickerController.getVideoDimensions(url)
|
||||
if let height = height {
|
||||
file["height"] = height
|
||||
}
|
||||
if let width = width {
|
||||
file["width"] = width
|
||||
}
|
||||
} else if isImage {
|
||||
let (height, width) = filePickerController.getImageDimensions(url)
|
||||
if let height = height {
|
||||
file["height"] = height
|
||||
}
|
||||
if let width = width {
|
||||
file["width"] = width
|
||||
}
|
||||
}
|
||||
|
||||
file["modifiedAt"] = filePickerController.getModifiedAtFromUrl(url)
|
||||
file["mimeType"] = mimeType
|
||||
file["name"] = url.lastPathComponent
|
||||
file["path"] = url.absoluteString
|
||||
file["size"] = try filePickerController.getSizeFromUrl(url)
|
||||
return file
|
||||
}
|
||||
pendingInvoke?.resolve(["files": filesResult])
|
||||
} catch let error as NSError {
|
||||
pendingInvoke?.reject(error.localizedDescription, nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
pendingInvoke?.resolve(["files": urls])
|
||||
case .cancelled:
|
||||
let files: JSArray = []
|
||||
pendingInvoke?.resolve(["files": files])
|
||||
case .error(let error):
|
||||
pendingInvoke?.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func showMessageDialog(_ invoke: Invoke) {
|
||||
let manager = self.manager
|
||||
let title = invoke.getString("title")
|
||||
guard let message = invoke.getString("message") else {
|
||||
invoke.reject("The `message` argument is required")
|
||||
return
|
||||
}
|
||||
let okButtonLabel = invoke.getString("okButtonLabel") ?? "OK"
|
||||
let cancelButtonLabel = invoke.getString("cancelButtonLabel") ?? "Cancel"
|
||||
|
||||
DispatchQueue.main.async { [] in
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
|
||||
alert.addAction(UIAlertAction(title: cancelButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in
|
||||
invoke.resolve([
|
||||
"value": false,
|
||||
"cancelled": false
|
||||
])
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: okButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in
|
||||
invoke.resolve([
|
||||
"value": true,
|
||||
"cancelled": false
|
||||
])
|
||||
}))
|
||||
|
||||
manager.viewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_dialog")
|
||||
func initPlugin(name: SRString, webview: WKWebView?) {
|
||||
Tauri.registerPlugin(webview: webview, name: name.toString(), plugin: DialogPlugin())
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let temporaryUrls = try urls.map { try saveTemporaryFile($0) }
|
||||
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
|
||||
} 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 {
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user