Skip to content

[file_selector] Add MIME type support on macOS #3862

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/file_selector/file_selector_macos/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import FlutterMacOS
import UniformTypeIdentifiers
import XCTest

@testable import file_selector_macos
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?] {
Expand All @@ -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)",
]
}

Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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<String?, Error>) -> Void)
func displaySavePanel(
options: SavePanelOptions, completion: @escaping (Result<String?, Error>) -> Void)
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
Expand All @@ -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))
}
}
}
Expand All @@ -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))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/file_selector/file_selector_macos/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down