diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index 2e9d4dc49b6..acd657b3cfc 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2 + +* Adds support for MIME types on macOS 11+. + ## 0.9.1+1 * Updates references to the deprecated `macUTIs`. diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift index 4e103431c9b..a77f4217311 100644 --- a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import FlutterMacOS +import UniformTypeIdentifiers import XCTest @testable import file_selector_macos @@ -160,7 +161,7 @@ class exampleTests: XCTestCase { baseOptions: SavePanelOptions( allowedFileTypes: AllowedTypes( extensions: ["txt", "json"], - mimeTypes: [], + mimeTypes: ["text/html"], utis: ["public.text", "public.image"]))) plugin.displayOpenPanel(options: options) { result in switch result { @@ -175,7 +176,62 @@ class exampleTests: XCTestCase { wait(for: [called], timeout: 0.5) XCTAssertNotNil(panelController.openPanel) if let panel = panelController.openPanel { + if #available(macOS 11.0, *) { + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.html)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image)) + } else { + // MIME type is not supported for the legacy codepath, but the rest should be set. + XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"]) + } + } + } + + func testOpenWithFilterLegacy() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + plugin.forceLegacyTypes = true + + let returnPath = "/foo/bar" + panelController.openURLs = [URL(fileURLWithPath: returnPath)] + + let called = XCTestExpectation() + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: AllowedTypes( + extensions: ["txt", "json"], + mimeTypes: ["text/html"], + utis: ["public.text", "public.image"]))) + plugin.displayOpenPanel(options: options) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths[0], returnPath) + case .failure(let error): + XCTFail("\(error)") + } + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + // On the legacy path, the allowedFileTypes should be set directly. XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"]) + + // They should also be translated to corresponding allowed content types. + if #available(macOS 11.0, *) { + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image)) + // MIME type is not supported for the legacy codepath. + XCTAssertFalse(panel.allowedContentTypes.contains(UTType.html)) + } } } diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift index 836fcf94244..83103325cf5 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import Cocoa import FlutterMacOS -import Foundation +import UniformTypeIdentifiers /// Protocol for showing panels, allowing for depenedency injection in tests. protocol PanelController { @@ -48,6 +49,8 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { private let openDirectoryMethod = "getDirectoryPath" private let saveMethod = "getSavePath" + var forceLegacyTypes = false + public static func register(with registrar: FlutterPluginRegistrar) { let instance = FileSelectorPlugin( viewProvider: DefaultViewProvider(registrar: registrar), @@ -96,16 +99,31 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { } if let acceptedTypes = options.allowedFileTypes { - var allowedTypes: [String] = [] - // The array values are non-null by convention even though Pigeon can't currently express - // that via the types; see messages.dart. - allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! })) - allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! })) - // TODO: Add support for mimeTypes in macOS 11+. See - // https://github.com/flutter/flutter/issues/117843 - - if !allowedTypes.isEmpty { - panel.allowedFileTypes = allowedTypes + if #available(macOS 11, *), !forceLegacyTypes { + var allowedTypes: [UTType] = [] + // The array values are non-null by convention even though Pigeon can't currently express + // that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848 + allowedTypes.append(contentsOf: acceptedTypes.utis.compactMap({ UTType($0!) })) + allowedTypes.append( + contentsOf: acceptedTypes.extensions.flatMap({ + UTType.types(tag: $0!, tagClass: UTTagClass.filenameExtension, conformingTo: nil) + })) + allowedTypes.append( + contentsOf: acceptedTypes.mimeTypes.flatMap({ + UTType.types(tag: $0!, tagClass: UTTagClass.mimeType, conformingTo: nil) + })) + if !allowedTypes.isEmpty { + panel.allowedContentTypes = allowedTypes + } + } else { + var allowedTypes: [String] = [] + // The array values are non-null by convention even though Pigeon can't currently express + // that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848 + allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! })) + allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! })) + if !allowedTypes.isEmpty { + panel.allowedFileTypes = allowedTypes + } } } } diff --git a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift index 828c499a218..67007e10081 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift @@ -5,12 +5,13 @@ // See also: https://pub.dev/packages/pigeon import Foundation + #if os(iOS) -import Flutter + import Flutter #elseif os(macOS) -import FlutterMacOS + import FlutterMacOS #else -#error("Unsupported platform.") + #error("Unsupported platform.") #endif private func wrapResult(_ result: Any?) -> [Any?] { @@ -22,13 +23,13 @@ private func wrapError(_ error: Any) -> [Any?] { return [ flutterError.code, flutterError.message, - flutterError.details + flutterError.details, ] } return [ "\(error)", "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)" + "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -140,14 +141,14 @@ struct OpenPanelOptions { private class FileSelectorApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { - case 128: - return AllowedTypes.fromList(self.readValue() as! [Any]) - case 129: - return OpenPanelOptions.fromList(self.readValue() as! [Any]) - case 130: - return SavePanelOptions.fromList(self.readValue() as! [Any]) - default: - return super.readValue(ofType: type) + case 128: + return AllowedTypes.fromList(self.readValue() as! [Any]) + case 129: + return OpenPanelOptions.fromList(self.readValue() as! [Any]) + case 130: + return SavePanelOptions.fromList(self.readValue() as! [Any]) + default: + return super.readValue(ofType: type) } } } @@ -189,11 +190,13 @@ protocol FileSelectorApi { /// selected paths. /// /// An empty list corresponds to a cancelled selection. - func displayOpenPanel(options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void) + func displayOpenPanel( + options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void) /// Shows a save panel with the given [options], returning the selected path. /// /// A null return corresponds to a cancelled save. - func displaySavePanel(options: SavePanelOptions, completion: @escaping (Result) -> Void) + func displaySavePanel( + options: SavePanelOptions, completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -206,17 +209,19 @@ class FileSelectorApiSetup { /// selected paths. /// /// An empty list corresponds to a cancelled selection. - let displayOpenPanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, codec: codec) + let displayOpenPanelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, + codec: codec) if let api = api { displayOpenPanelChannel.setMessageHandler { message, reply in let args = message as! [Any] let optionsArg = args[0] as! OpenPanelOptions api.displayOpenPanel(options: optionsArg) { result in switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -226,17 +231,19 @@ class FileSelectorApiSetup { /// Shows a save panel with the given [options], returning the selected path. /// /// A null return corresponds to a cancelled save. - let displaySavePanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, codec: codec) + let displaySavePanelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, + codec: codec) if let api = api { displaySavePanelChannel.setMessageHandler { message, reply in let args = message as! [Any] let optionsArg = args[0] as! SavePanelOptions api.displaySavePanel(options: optionsArg) { result in switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) } } } diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 65b055d893f..f55079f380a 100644 --- a/packages/file_selector/file_selector_macos/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_macos description: macOS implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.1+1 +version: 0.9.2 environment: sdk: ">=2.18.0 <4.0.0"