Skip to content

Commit 0471996

Browse files
stuartmorgan-gnploi
authored andcommitted
[file_selector] Add MIME type support on macOS (flutter#3862)
Adds a macOS 11+ codepath that uses `UTType`s, allowing for supporting MIME type (and moving off of the deprecated `allowedFileTypes`). Fixes flutter/flutter#117843
1 parent 207fa1c commit 0471996

File tree

5 files changed

+123
-38
lines changed

5 files changed

+123
-38
lines changed

packages/file_selector/file_selector_macos/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.2
2+
3+
* Adds support for MIME types on macOS 11+.
4+
15
## 0.9.1+1
26

37
* Updates references to the deprecated `macUTIs`.

packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import FlutterMacOS
6+
import UniformTypeIdentifiers
67
import XCTest
78

89
@testable import file_selector_macos
@@ -160,7 +161,7 @@ class exampleTests: XCTestCase {
160161
baseOptions: SavePanelOptions(
161162
allowedFileTypes: AllowedTypes(
162163
extensions: ["txt", "json"],
163-
mimeTypes: [],
164+
mimeTypes: ["text/html"],
164165
utis: ["public.text", "public.image"])))
165166
plugin.displayOpenPanel(options: options) { result in
166167
switch result {
@@ -175,7 +176,62 @@ class exampleTests: XCTestCase {
175176
wait(for: [called], timeout: 0.5)
176177
XCTAssertNotNil(panelController.openPanel)
177178
if let panel = panelController.openPanel {
179+
if #available(macOS 11.0, *) {
180+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText))
181+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json))
182+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.html))
183+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image))
184+
} else {
185+
// MIME type is not supported for the legacy codepath, but the rest should be set.
186+
XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"])
187+
}
188+
}
189+
}
190+
191+
func testOpenWithFilterLegacy() throws {
192+
let panelController = TestPanelController()
193+
let plugin = FileSelectorPlugin(
194+
viewProvider: TestViewProvider(),
195+
panelController: panelController)
196+
plugin.forceLegacyTypes = true
197+
198+
let returnPath = "/foo/bar"
199+
panelController.openURLs = [URL(fileURLWithPath: returnPath)]
200+
201+
let called = XCTestExpectation()
202+
let options = OpenPanelOptions(
203+
allowsMultipleSelection: true,
204+
canChooseDirectories: false,
205+
canChooseFiles: true,
206+
baseOptions: SavePanelOptions(
207+
allowedFileTypes: AllowedTypes(
208+
extensions: ["txt", "json"],
209+
mimeTypes: ["text/html"],
210+
utis: ["public.text", "public.image"])))
211+
plugin.displayOpenPanel(options: options) { result in
212+
switch result {
213+
case .success(let paths):
214+
XCTAssertEqual(paths[0], returnPath)
215+
case .failure(let error):
216+
XCTFail("\(error)")
217+
}
218+
called.fulfill()
219+
}
220+
221+
wait(for: [called], timeout: 0.5)
222+
XCTAssertNotNil(panelController.openPanel)
223+
if let panel = panelController.openPanel {
224+
// On the legacy path, the allowedFileTypes should be set directly.
178225
XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"])
226+
227+
// They should also be translated to corresponding allowed content types.
228+
if #available(macOS 11.0, *) {
229+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText))
230+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json))
231+
XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image))
232+
// MIME type is not supported for the legacy codepath.
233+
XCTAssertFalse(panel.allowedContentTypes.contains(UTType.html))
234+
}
179235
}
180236
}
181237

packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import Cocoa
56
import FlutterMacOS
6-
import Foundation
7+
import UniformTypeIdentifiers
78

89
/// Protocol for showing panels, allowing for depenedency injection in tests.
910
protocol PanelController {
@@ -48,6 +49,8 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi {
4849
private let openDirectoryMethod = "getDirectoryPath"
4950
private let saveMethod = "getSavePath"
5051

52+
var forceLegacyTypes = false
53+
5154
public static func register(with registrar: FlutterPluginRegistrar) {
5255
let instance = FileSelectorPlugin(
5356
viewProvider: DefaultViewProvider(registrar: registrar),
@@ -96,16 +99,31 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi {
9699
}
97100

98101
if let acceptedTypes = options.allowedFileTypes {
99-
var allowedTypes: [String] = []
100-
// The array values are non-null by convention even though Pigeon can't currently express
101-
// that via the types; see messages.dart.
102-
allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! }))
103-
allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! }))
104-
// TODO: Add support for mimeTypes in macOS 11+. See
105-
// https://github.com/flutter/flutter/issues/117843
106-
107-
if !allowedTypes.isEmpty {
108-
panel.allowedFileTypes = allowedTypes
102+
if #available(macOS 11, *), !forceLegacyTypes {
103+
var allowedTypes: [UTType] = []
104+
// The array values are non-null by convention even though Pigeon can't currently express
105+
// that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848
106+
allowedTypes.append(contentsOf: acceptedTypes.utis.compactMap({ UTType($0!) }))
107+
allowedTypes.append(
108+
contentsOf: acceptedTypes.extensions.flatMap({
109+
UTType.types(tag: $0!, tagClass: UTTagClass.filenameExtension, conformingTo: nil)
110+
}))
111+
allowedTypes.append(
112+
contentsOf: acceptedTypes.mimeTypes.flatMap({
113+
UTType.types(tag: $0!, tagClass: UTTagClass.mimeType, conformingTo: nil)
114+
}))
115+
if !allowedTypes.isEmpty {
116+
panel.allowedContentTypes = allowedTypes
117+
}
118+
} else {
119+
var allowedTypes: [String] = []
120+
// The array values are non-null by convention even though Pigeon can't currently express
121+
// that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848
122+
allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! }))
123+
allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! }))
124+
if !allowedTypes.isEmpty {
125+
panel.allowedFileTypes = allowedTypes
126+
}
109127
}
110128
}
111129
}

packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
// See also: https://pub.dev/packages/pigeon
66

77
import Foundation
8+
89
#if os(iOS)
9-
import Flutter
10+
import Flutter
1011
#elseif os(macOS)
11-
import FlutterMacOS
12+
import FlutterMacOS
1213
#else
13-
#error("Unsupported platform.")
14+
#error("Unsupported platform.")
1415
#endif
1516

1617
private func wrapResult(_ result: Any?) -> [Any?] {
@@ -22,13 +23,13 @@ private func wrapError(_ error: Any) -> [Any?] {
2223
return [
2324
flutterError.code,
2425
flutterError.message,
25-
flutterError.details
26+
flutterError.details,
2627
]
2728
}
2829
return [
2930
"\(error)",
3031
"\(type(of: error))",
31-
"Stacktrace: \(Thread.callStackSymbols)"
32+
"Stacktrace: \(Thread.callStackSymbols)",
3233
]
3334
}
3435

@@ -140,14 +141,14 @@ struct OpenPanelOptions {
140141
private class FileSelectorApiCodecReader: FlutterStandardReader {
141142
override func readValue(ofType type: UInt8) -> Any? {
142143
switch type {
143-
case 128:
144-
return AllowedTypes.fromList(self.readValue() as! [Any])
145-
case 129:
146-
return OpenPanelOptions.fromList(self.readValue() as! [Any])
147-
case 130:
148-
return SavePanelOptions.fromList(self.readValue() as! [Any])
149-
default:
150-
return super.readValue(ofType: type)
144+
case 128:
145+
return AllowedTypes.fromList(self.readValue() as! [Any])
146+
case 129:
147+
return OpenPanelOptions.fromList(self.readValue() as! [Any])
148+
case 130:
149+
return SavePanelOptions.fromList(self.readValue() as! [Any])
150+
default:
151+
return super.readValue(ofType: type)
151152
}
152153
}
153154
}
@@ -189,11 +190,13 @@ protocol FileSelectorApi {
189190
/// selected paths.
190191
///
191192
/// An empty list corresponds to a cancelled selection.
192-
func displayOpenPanel(options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void)
193+
func displayOpenPanel(
194+
options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void)
193195
/// Shows a save panel with the given [options], returning the selected path.
194196
///
195197
/// A null return corresponds to a cancelled save.
196-
func displaySavePanel(options: SavePanelOptions, completion: @escaping (Result<String?, Error>) -> Void)
198+
func displaySavePanel(
199+
options: SavePanelOptions, completion: @escaping (Result<String?, Error>) -> Void)
197200
}
198201

199202
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -206,17 +209,19 @@ class FileSelectorApiSetup {
206209
/// selected paths.
207210
///
208211
/// An empty list corresponds to a cancelled selection.
209-
let displayOpenPanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, codec: codec)
212+
let displayOpenPanelChannel = FlutterBasicMessageChannel(
213+
name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger,
214+
codec: codec)
210215
if let api = api {
211216
displayOpenPanelChannel.setMessageHandler { message, reply in
212217
let args = message as! [Any]
213218
let optionsArg = args[0] as! OpenPanelOptions
214219
api.displayOpenPanel(options: optionsArg) { result in
215220
switch result {
216-
case .success(let res):
217-
reply(wrapResult(res))
218-
case .failure(let error):
219-
reply(wrapError(error))
221+
case .success(let res):
222+
reply(wrapResult(res))
223+
case .failure(let error):
224+
reply(wrapError(error))
220225
}
221226
}
222227
}
@@ -226,17 +231,19 @@ class FileSelectorApiSetup {
226231
/// Shows a save panel with the given [options], returning the selected path.
227232
///
228233
/// A null return corresponds to a cancelled save.
229-
let displaySavePanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, codec: codec)
234+
let displaySavePanelChannel = FlutterBasicMessageChannel(
235+
name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger,
236+
codec: codec)
230237
if let api = api {
231238
displaySavePanelChannel.setMessageHandler { message, reply in
232239
let args = message as! [Any]
233240
let optionsArg = args[0] as! SavePanelOptions
234241
api.displaySavePanel(options: optionsArg) { result in
235242
switch result {
236-
case .success(let res):
237-
reply(wrapResult(res))
238-
case .failure(let error):
239-
reply(wrapError(error))
243+
case .success(let res):
244+
reply(wrapResult(res))
245+
case .failure(let error):
246+
reply(wrapError(error))
240247
}
241248
}
242249
}

packages/file_selector/file_selector_macos/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: file_selector_macos
22
description: macOS implementation of the file_selector plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_macos
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
5-
version: 0.9.1+1
5+
version: 0.9.2
66

77
environment:
88
sdk: ">=2.18.0 <4.0.0"

0 commit comments

Comments
 (0)