[file_selector] Add MIME type support on macOS (#3862)
Adds a macOS 11+ codepath that uses `UTType`s, allowing for supporting MIME type (and moving off of the deprecated `allowedFileTypes`).
Fixes https://github.com/flutter/flutter/issues/117843
diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md
index 2e9d4dc..acd657b 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 4e10343..a77f421 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 @@
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 @@
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 836fcf9..8310332 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 @@
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 @@
}
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 828c499..67007e1 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 @@
return [
flutterError.code,
flutterError.message,
- flutterError.details
+ flutterError.details,
]
}
return [
"\(error)",
"\(type(of: error))",
- "Stacktrace: \(Thread.callStackSymbols)"
+ "Stacktrace: \(Thread.callStackSymbols)",
]
}
@@ -140,14 +141,14 @@
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 @@
/// 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<String?, Error>) -> Void)
+ func displaySavePanel(
+ options: SavePanelOptions, completion: @escaping (Result<String?, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -206,17 +209,19 @@
/// 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 @@
/// 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 65b055d..f55079f 100644
--- a/packages/file_selector/file_selector_macos/pubspec.yaml
+++ b/packages/file_selector/file_selector_macos/pubspec.yaml
@@ -2,7 +2,7 @@
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"